From 951bdab6945787e79211fb8ae4f7b3b92f6fce8f Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Sat, 14 Jun 2025 17:12:10 +0200 Subject: [PATCH 01/15] fxied trackers text --- .../SettingsView/SettingsSubViews/SettingsViewTrackers.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift index 2d4ac59..f90db74 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift @@ -147,7 +147,7 @@ struct SettingsViewTrackers: View { .frame(height: 18) } else if isAnilistLoggedIn { HStack(spacing: 0) { - Text(NSLocalizedString("Logged in as", comment: "")) + Text(NSLocalizedString("Logged in as ", comment: "")) .font(.footnote) .foregroundStyle(.gray) Text(anilistUsername) @@ -240,7 +240,7 @@ struct SettingsViewTrackers: View { .frame(height: 18) } else if isTraktLoggedIn { HStack(spacing: 0) { - Text(NSLocalizedString("Logged in as", comment: "")) + Text(NSLocalizedString("Logged in as ", comment: "")) .font(.footnote) .foregroundStyle(.gray) Text(traktUsername) From b216f55b0ded91f8ed1cdbad2dac76b1fc9c57b1 Mon Sep 17 00:00:00 2001 From: cranci <100066266+cranci1@users.noreply.github.com> Date: Sat, 14 Jun 2025 19:53:31 +0200 Subject: [PATCH 02/15] Update SettingsViewTrackers.swift --- .../SettingsView/SettingsSubViews/SettingsViewTrackers.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift index f90db74..0ff5bab 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift @@ -270,7 +270,7 @@ struct SettingsViewTrackers: View { SettingsToggleRow( icon: "arrow.triangle.2.circlepath", - title: NSLocalizedString("Sync TV shows progress", comment: ""), + title: NSLocalizedString("Sync shows/movies progress", comment: ""), isOn: $isSendTraktUpdates, showDivider: false ) From b47a888cc6e51a9b454c03e584b5ba68d1908f44 Mon Sep 17 00:00:00 2001 From: cranci <100066266+cranci1@users.noreply.github.com> Date: Sat, 14 Jun 2025 19:54:38 +0200 Subject: [PATCH 03/15] Update SettingsViewGeneral.swift --- .../SettingsView/SettingsSubViews/SettingsViewGeneral.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index 1c48ac8..8f9c15a 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -204,7 +204,8 @@ struct SettingsViewGeneral: View { title: NSLocalizedString("App Language", comment: ""), options: ["English", "Dutch"], optionToString: { $0 }, - selection: $settings.selectedLanguage + selection: $settings.selectedLanguage, + showDivider: false ) .onChange(of: settings.selectedLanguage) { _ in showRestartAlert = true From 583075abaa1c91109ba3ec353ae91cd3f3a4fb9d Mon Sep 17 00:00:00 2001 From: 50/50 <80717571+50n50@users.noreply.github.com> Date: Sun, 15 Jun 2025 08:37:38 +0200 Subject: [PATCH 04/15] Languages fix + Arabic + French + disable flip by Arabic (#193) --- Sora/SoraApp.swift | 1 + Sora/Views/DownloadView.swift | 1 + .../SettingsViewGeneral.swift | 2 +- Sora/Views/SettingsView/SettingsView.swift | 12 +- Sora/ar.lproj/Localizable.strings | 384 ++++++++++++++++++ Sora/en.lproj/Localizable.strings | 384 ++++++++++++++++++ Sora/fr.lproj/Localizable.strings | 384 ++++++++++++++++++ Sora/nl.lproj/Localizable.strings | 384 ++++++++++++++++++ Sulfur.xcodeproj/project.pbxproj | 90 +++- 9 files changed, 1636 insertions(+), 6 deletions(-) create mode 100644 Sora/ar.lproj/Localizable.strings create mode 100644 Sora/en.lproj/Localizable.strings create mode 100644 Sora/fr.lproj/Localizable.strings create mode 100644 Sora/nl.lproj/Localizable.strings diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index 560ba24..f83c741 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -38,6 +38,7 @@ struct SoraApp: App { ContentView() } } + .environment(\.layoutDirection, .leftToRight) .environmentObject(moduleManager) .environmentObject(settings) .environmentObject(libraryManager) diff --git a/Sora/Views/DownloadView.swift b/Sora/Views/DownloadView.swift index 8093104..6233474 100644 --- a/Sora/Views/DownloadView.swift +++ b/Sora/Views/DownloadView.swift @@ -1106,6 +1106,7 @@ struct EnhancedShowEpisodesView: View { } } .padding(.vertical) + .scrollViewBottomPadding() } .navigationTitle(NSLocalizedString("Episodes", comment: "")) .navigationBarTitleDisplayMode(.inline) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index 8f9c15a..ee0ebc9 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -202,7 +202,7 @@ struct SettingsViewGeneral: View { SettingsPickerRow( icon: "globe", title: NSLocalizedString("App Language", comment: ""), - options: ["English", "Dutch"], + options: ["English", "Dutch", "French", "Arabic"], optionToString: { $0 }, selection: $settings.selectedLanguage, showDivider: false diff --git a/Sora/Views/SettingsView/SettingsView.swift b/Sora/Views/SettingsView/SettingsView.swift index 51f191c..f9be26d 100644 --- a/Sora/Views/SettingsView/SettingsView.swift +++ b/Sora/Views/SettingsView/SettingsView.swift @@ -400,7 +400,17 @@ class Settings: ObservableObject { } func updateLanguage() { - let languageCode = selectedLanguage == "Dutch" ? "nl" : "en" + let languageCode: String + switch selectedLanguage { + case "Dutch": + languageCode = "nl" + case "French": + languageCode = "fr" + case "Arabic": + languageCode = "ar" + default: + languageCode = "en" + } UserDefaults.standard.set([languageCode], forKey: "AppleLanguages") UserDefaults.standard.synchronize() } diff --git a/Sora/ar.lproj/Localizable.strings b/Sora/ar.lproj/Localizable.strings new file mode 100644 index 0000000..3235d42 --- /dev/null +++ b/Sora/ar.lproj/Localizable.strings @@ -0,0 +1,384 @@ +/* General */ +"About" = "حول"; +"About Sora" = "حول Sora"; +"Active" = "نشط"; +"Active Downloads" = "التنزيلات النشطة"; +"Actively downloading media can be tracked from here." = "يمكن تتبع المحتوى الذي يتم تنزيله حاليًا من هنا."; +"Add Module" = "إضافة وحدة"; +"Adjust the number of media items per row in portrait and landscape modes." = "اضبط عدد عناصر المحتوى في كل صف في الوضعين الرأسي والأفقي."; +"Advanced" = "متقدم"; +"AKA Sulfur" = "يعرف أيضًا باسم Sulfur"; +"All Bookmarks" = "العناصر المحفوظة"; +"All Watching" = "الكل قيد المشاهدة"; +"Also known as Sulfur" = "يعرف أيضًا باسم Sulfur"; +"AniList" = "AniList"; +"AniList ID" = "معرّف AniList"; +"AniList Match" = "مطابقة AniList"; +"AniList.co" = "AniList.co"; +"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "يتم جمع بيانات مجهولة المصدر لتحسين التطبيق. لا يتم جمع أي معلومات شخصية. يمكن تعطيل هذا في أي وقت."; +"App Info" = "معلومات التطبيق"; +"App Language" = "لغة التطبيق"; +"App Storage" = "تخزين التطبيق"; +"Appearance" = "المظهر"; + +/* Alerts and Actions */ +"Are you sure you want to clear all cached data? This will help free up storage space." = "هل أنت متأكد من أنك تريد مسح جميع بيانات ذاكرة التخزين المؤقت؟ سيساعد هذا في تحرير مساحة التخزين."; +"Are you sure you want to delete '%@'?" = "هل أنت متأكد أنك تريد حذف '%@'؟"; +"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "هل أنت متأكد أنك تريد حذف جميع الحلقات البالغ عددها %1$d في '%2$@'؟"; +"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "هل أنت متأكد من أنك تريد حذف جميع الأصول التي تم تنزيلها؟ يمكنك اختيار مسح المكتبة فقط مع الاحتفاظ بالملفات التي تم تنزيلها للاستخدام في المستقبل."; +"Are you sure you want to erase all app data? This action cannot be undone." = "هل أنت متأكد أنك تريد محو جميع بيانات التطبيق؟ هذا الإجراء لا يمكن التراجع عنه."; + +/* Features */ +"Background Enabled" = "التفعيل في الخلفية"; +"Bookmark items for an easier access later." = "احفظ العناصر للوصول إليها بسهولة لاحقًا."; +"Bookmarks" = "العناصر المحفوظة"; +"Bottom Padding" = "الحشوة السفلية"; +"Cancel" = "إلغاء"; +"Cellular Quality" = "جودة بيانات الجوال"; +"Check out some community modules here!" = "اطلع على بعض وحدات المجتمع هنا!"; +"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality." = "اختر دقة الفيديو المفضلة لاتصالات WiFi وبيانات الجوال. الدقة الأعلى تستهلك المزيد من البيانات ولكنها توفر جودة أفضل."; +"Clear" = "مسح"; +"Clear All Downloads" = "مسح كل التنزيلات"; +"Clear Cache" = "مسح ذاكرة التخزين المؤقت"; +"Clear Library Only" = "مسح المكتبة فقط"; +"Clear Logs" = "مسح السجلات"; +"Click the plus button to add a module!" = "انقر على زر الإضافة لإضافة وحدة!"; +"Continue Watching" = "متابعة المشاهدة"; +"Continue Watching Episode %d" = "متابعة مشاهدة الحلقة %d"; +"Contributors" = "المساهمون"; +"Copied to Clipboard" = "تم النسخ إلى الحافظة"; +"Copy to Clipboard" = "نسخ إلى الحافظة"; +"Copy URL" = "نسخ الرابط"; + +/* Episodes */ +"%lld Episodes" = "%lld حلقات"; +"%lld of %lld" = "%1$lld من %2$lld"; +"%lld-%lld" = "%1$lld-%2$lld"; +"%lld%% seen" = "تمت مشاهدة %lld%%"; +"Episode %lld" = "الحلقة %lld"; +"Episodes" = "الحلقات"; +"Episodes might not be available yet or there could be an issue with the source." = "قد لا تكون الحلقات متاحة بعد أو قد تكون هناك مشكلة في المصدر."; +"Episodes Range" = "نطاق الحلقات"; + +/* System */ +"cranci1" = "cranci1"; +"Dark" = "داكن"; +"DATA & LOGS" = "البيانات والسجلات"; +"Debug" = "تصحيح الأخطاء"; +"Debugging and troubleshooting." = "تصحيح الأخطاء وإصلاحها."; + +/* Actions */ +"Delete" = "حذف"; +"Delete All" = "حذف الكل"; +"Delete All Downloads" = "حذف كل التنزيلات"; +"Delete All Episodes" = "حذف كل الحلقات"; +"Delete Download" = "حذف التنزيل"; +"Delete Episode" = "حذف الحلقة"; + +/* Player */ +"Double Tap to Seek" = "انقر نقرًا مزدوجًا للتقديم"; +"Double tapping the screen on it's sides will skip with the short tap setting." = "النقر المزدوج على جانبي الشاشة سيقوم بالتقديم حسب إعداد النقر القصير."; + +/* Downloads */ +"Download" = "تنزيل"; +"Download Episode" = "تنزيل الحلقة"; +"Download Summary" = "ملخص التنزيل"; +"Download This Episode" = "تنزيل هذه الحلقة"; +"Downloaded" = "تم التنزيل"; +"Downloaded Shows" = "العروض المنزّلة"; +"Downloading" = "جاري التنزيل"; +"Downloads" = "التنزيلات"; + +/* Settings */ +"Enable Analytics" = "تفعيل التحليلات"; +"Enable Subtitles" = "تفعيل الترجمة"; + +/* Data Management */ +"Erase" = "محو"; +"Erase all App Data" = "محو كل بيانات التطبيق"; +"Erase App Data" = "محو بيانات التطبيق"; + +/* Errors */ +"Error" = "خطأ"; +"Error Fetching Results" = "خطأ في جلب النتائج"; +"Errors and critical issues." = "الأخطاء والمشاكل الحرجة."; +"Failed to load contributors" = "فشل تحميل المساهمين"; + +/* Features */ +"Fetch Episode metadata" = "جلب بيانات الحلقة الوصفية"; +"Files Downloaded" = "الملفات المنزّلة"; +"Font Size" = "حجم الخط"; + +/* Interface */ +"Force Landscape" = "فرض الوضع الأفقي"; +"General" = "عام"; +"General events and activities." = "الأحداث والأنشطة العامة."; +"General Preferences" = "التفضيلات العامة"; +"Hide Splash Screen" = "إخفاء شاشة البداية"; +"HLS video downloading." = "تنزيل فيديو HLS."; +"Hold Speed" = "سرعة الضغط المطول"; + +/* Info */ +"Info" = "معلومات"; +"INFOS" = "معلومات"; +"Installed Modules" = "الوحدات المثبتة"; +"Interface" = "الواجهة"; + +/* Social */ +"Join the Discord" = "انضم إلى ديسكورد"; + +/* Layout */ +"Landscape Columns" = "أعمدة الوضع الأفقي"; +"Language" = "اللغة"; +"LESS" = "أقل"; + +/* Library */ +"Library" = "المكتبة"; +"License (GPLv3.0)" = "الرخصة (GPLv3.0)"; +"Light" = "فاتح"; + +/* Loading States */ +"Loading Episode %lld..." = "جاري تحميل الحلقة %lld..."; +"Loading logs..." = "جاري تحميل السجلات..."; +"Loading module information..." = "جاري تحميل معلومات الوحدة..."; +"Loading Stream" = "جاري تحميل البث"; + +/* Logging */ +"Log Debug Info" = "تسجيل معلومات تصحيح الأخطاء"; +"Log Filters" = "مرشحات السجل"; +"Log In with AniList" = "تسجيل الدخول باستخدام AniList"; +"Log In with Trakt" = "تسجيل الدخول باستخدام Trakt"; +"Log Out from AniList" = "تسجيل الخروج من AniList"; +"Log Out from Trakt" = "تسجيل الخروج من Trakt"; +"Log Types" = "أنواع السجل"; +"Logged in as" = "تم تسجيل الدخول باسم"; +"Logged in as " = "تم تسجيل الدخول باسم "; + +/* Logs and Settings */ +"Logs" = "السجلات"; +"Long press Skip" = "ضغط مطول للتخطي"; +"MAIN" = "الرئيسية"; +"Main Developer" = "المطور الرئيسي"; +"MAIN SETTINGS" = "الإعدادات الرئيسية"; + +/* Media Actions */ +"Mark All Previous Watched" = "تمييز كل ما سبق كمشاهد"; +"Mark as Watched" = "تمييز كمشاهد"; +"Mark Episode as Watched" = "تمييز الحلقة كمشاهدة"; +"Mark Previous Episodes as Watched" = "تمييز الحلقات السابقة كمشاهدة"; +"Mark watched" = "تمييز كمشاهد"; +"Match with AniList" = "مطابقة مع AniList"; +"Match with TMDB" = "مطابقة مع TMDB"; +"Matched ID: %lld" = "المعرّف المطابق: %lld"; +"Matched with: %@" = "تمت المطابقة مع: %@"; +"Max Concurrent Downloads" = "الحد الأقصى للتنزيلات المتزامنة"; + +/* Media Interface */ +"Media Grid Layout" = "تخطيط شبكة المحتوى"; +"Media Player" = "مشغل المحتوى"; +"Media View" = "عرض المحتوى"; +"Metadata Provider" = "مزود البيانات الوصفية"; +"Metadata Providers Order" = "ترتيب مزودي البيانات الوصفية"; +"Module Removed" = "تمت إزالة الوحدة"; +"Modules" = "الوحدات"; + +/* Headers */ +"MODULES" = "الوحدات"; +"MORE" = "المزيد"; + +/* Status Messages */ +"No Active Downloads" = "لا توجد تنزيلات نشطة"; +"No AniList matches found" = "لم يتم العثور على مطابقات في AniList"; +"No Data Available" = "لا توجد بيانات متاحة"; +"No Downloads" = "لا توجد تنزيلات"; +"No episodes available" = "لا توجد حلقات متاحة"; +"No Episodes Available" = "لا توجد حلقات متاحة"; +"No items to continue watching." = "لا توجد عناصر لمتابعة مشاهدتها."; +"No matches found" = "لم يتم العثور على نتائج"; +"No Module Selected" = "لم يتم تحديد أي وحدة"; +"No Modules" = "لا توجد وحدات"; +"No Results Found" = "لم يتم العثور على نتائج"; +"No Search Results Found" = "لم يتم العثور على نتائج بحث"; +"Nothing to Continue Watching" = "لا شيء لمتابعة مشاهدته"; + +/* Notes and Messages */ +"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "ملاحظة: سيتم استبدال الوحدات فقط في حالة وجود سلسلة إصدار مختلفة داخل ملف JSON."; + +/* Actions */ +"OK" = "موافق"; +"Open Community Library" = "فتح مكتبة المجتمع"; + +/* External Services */ +"Open in AniList" = "فتح في AniList"; +"Original Poster" = "الملصق الأصلي"; + +/* Playback */ +"Paused" = "متوقف مؤقتًا"; +"Play" = "تشغيل"; +"Player" = "المشغل"; + +/* System Messages */ +"Please restart the app to apply the language change." = "يرجى إعادة تشغيل التطبيق لتطبيق تغيير اللغة."; +"Please select a module from settings" = "يرجى تحديد وحدة من الإعدادات"; + +/* Interface */ +"Portrait Columns" = "أعمدة الوضع الرأسي"; +"Progress bar Marker Color" = "لون علامة شريط التقدم"; +"Provider: %@" = "المزود: %@"; + +/* Queue */ +"Queue" = "قائمة الانتظار"; +"Queued" = "في قائمة الانتظار"; + +/* Content */ +"Recently watched content will appear here." = "سيظهر المحتوى الذي تمت مشاهدته مؤخرًا هنا."; + +/* Settings */ +"Refresh Modules on Launch" = "تحديث الوحدات عند التشغيل"; +"Refresh Storage Info" = "تحديث معلومات التخزين"; +"Remember Playback speed" = "تذكر سرعة التشغيل"; + +/* Actions */ +"Remove" = "إزالة"; +"Remove All Cache" = "إزالة كل ذاكرة التخزين المؤقت"; + +/* File Management */ +"Remove All Documents" = "إزالة كل المستندات"; +"Remove Documents" = "إزالة المستندات"; +"Remove Downloaded Media" = "إزالة المحتوى المنزل"; +"Remove Downloads" = "إزالة التنزيلات"; +"Remove from Bookmarks" = "إزالة من العناصر المحفوظة"; +"Remove Item" = "إزالة العنصر"; + +/* Support */ +"Report an Issue" = "الإبلاغ عن مشكلة"; + +/* Reset Options */ +"Reset" = "إعادة تعيين"; +"Reset AniList ID" = "إعادة تعيين معرّف AniList"; +"Reset Episode Progress" = "إعادة تعيين تقدم الحلقة"; +"Reset progress" = "إعادة تعيين التقدم"; +"Reset Progress" = "إعادة تعيين التقدم"; + +/* System */ +"Restart Required" = "إعادة التشغيل مطلوبة"; +"Running Sora %@ - cranci1" = "يعمل Sora %@ - بواسطة cranci1"; + +/* Actions */ +"Save" = "حفظ"; +"Search" = "بحث"; + +/* Search */ +"Search downloads" = "بحث في التنزيلات"; +"Search for something..." = "ابحث عن شيء ما..."; +"Search..." = "بحث..."; + +/* Content */ +"Season %d" = "الموسم %d"; +"Season %lld" = "الموسم %lld"; +"Segments Color" = "لون الأجزاء"; + +/* Modules */ +"Select Module" = "تحديد وحدة"; +"Set Custom AniList ID" = "تعيين معرّف AniList مخصص"; + +/* Interface */ +"Settings" = "الإعدادات"; +"Shadow" = "ظل"; +"Show More (%lld more characters)" = "عرض المزيد (%lld أحرف إضافية)"; +"Show PiP Button" = "إظهار زر صورة داخل صورة"; +"Show Skip 85s Button" = "إظهار زر تخطي 85 ثانية"; +"Show Skip Intro / Outro Buttons" = "إظهار أزرار تخطي المقدمة / الخاتمة"; +"Shows" = "العروض"; +"Size (%@)" = "الحجم (%@)"; +"Skip Settings" = "إعدادات التخطي"; + +/* Player Features */ +"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "بعض الميزات تقتصر على مشغل Sora والمشغل الافتراضي، مثل فرض الوضع الأفقي، وسرعة الضغط المطول، وزيادات التخطي الزمني المخصصة."; + +/* App Info */ +"Sora" = "Sora"; +"Sora %@ by cranci1" = "Sora %@ بواسطة cranci1"; +"Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." = "Sora و cranci1 غير تابعين لـ AniList أو Trakt بأي شكل من الأشكال.\n\nيرجى ملاحظة أن تحديثات التقدم قد لا تكون دقيقة بنسبة 100%."; +"Sora GitHub Repository" = "مستودع Sora على GitHub"; +"Sora/Sulfur will always remain free with no ADs!" = "سيظل Sora/Sulfur دائمًا مجانيًا وبدون إعلانات!"; + +/* Interface */ +"Sort" = "فرز"; +"Speed Settings" = "إعدادات السرعة"; + +/* Playback */ +"Start Watching" = "ابدأ المشاهدة"; +"Start Watching Episode %d" = "ابدأ مشاهدة الحلقة %d"; +"Storage Used" = "المساحة المستخدمة"; +"Stream" = "بث"; +"Streaming and video playback." = "البث وتشغيل الفيديو."; + +/* Subtitles */ +"Subtitle Color" = "لون الترجمة"; +"Subtitle Settings" = "إعدادات الترجمة"; + +/* Sync */ +"Sync anime progress" = "مزامنة تقدم الأنمي"; +"Sync TV shows progress" = "مزامنة تقدم المسلسلات"; + +/* System */ +"System" = "النظام"; + +/* Instructions */ +"Tap a title to override the current match." = "انقر على عنوان لتجاوز المطابقة الحالية."; +"Tap Skip" = "انقر للتخطي"; +"Tap to manage your modules" = "انقر لإدارة وحداتك"; +"Tap to select a module" = "انقر لتحديد وحدة"; + +/* App Information */ +"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "تساعد ذاكرة التخزين المؤقت للتطبيق في تحميل الصور بشكل أسرع.\n\nسيؤدي مسح مجلد المستندات إلى حذف جميع الوحدات التي تم تنزيلها.\n\nلا تقم بمحو بيانات التطبيق إلا إذا كنت تفهم العواقب — فقد يتسبب ذلك في تعطل التطبيق."; +"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "يتحكم نطاق الحلقات في عدد الحلقات التي تظهر في كل صفحة. يتم تجميع الحلقات في مجموعات (مثل 1-25، 26-50، وهكذا)، مما يتيح لك التنقل بينها بسهولة أكبر.\n\nبالنسبة لبيانات الحلقة الوصفية، فإنها تشير إلى الصورة المصغرة للحلقة وعنوانها، حيث يمكن أن تحتوي أحيانًا على حرق للأحداث."; +"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "قدمت الوحدة حلقة واحدة فقط، ومن المرجح أن يكون هذا فيلمًا، لذلك قررنا إنشاء شاشات منفصلة لهذه الحالات."; + +/* Interface */ +"Thumbnails Width" = "عرض الصور المصغرة"; +"TMDB Match" = "مطابقة TMDB"; +"Trackers" = "المتتبعات"; +"Trakt" = "Trakt"; +"Trakt.tv" = "Trakt.tv"; + +/* Search */ +"Try different keywords" = "جرب كلمات مفتاحية مختلفة"; +"Try different search terms" = "جرب مصطلحات بحث مختلفة"; + +/* Player Controls */ +"Two Finger Hold for Pause" = "اضغط بإصبعين للإيقاف المؤقت"; +"Unable to fetch matches. Please try again later." = "تعذر جلب المطابقات. يرجى المحاولة مرة أخرى لاحقًا."; +"Use TMDB Poster Image" = "استخدام صورة ملصق TMDB"; + +/* Version */ +"v%@" = "v%@"; +"Video Player" = "مشغل الفيديو"; + +/* Video Settings */ +"Video Quality Preferences" = "تفضيلات جودة الفيديو"; +"View All" = "عرض الكل"; +"Watched" = "تمت مشاهدته"; +"Why am I not seeing any episodes?" = "لماذا لا أرى أي حلقات؟"; +"WiFi Quality" = "جودة WiFi"; + +/* User Status */ +"You are not logged in" = "أنت غير مسجل الدخول"; +"You have no items saved." = "ليس لديك عناصر محفوظة."; +"Your downloaded episodes will appear here" = "ستظهر حلقاتك المنزّلة هنا"; +"Your recently watched content will appear here" = "سيظهر المحتوى الذي شاهدته مؤخرًا هنا"; + +/* Download Settings */ +"Download Settings" = "إعدادات التنزيل"; +"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "يتحكم الحد الأقصى للتنزيلات المتزامنة في عدد الحلقات التي يمكن تنزيلها في وقت واحد. قد تستهلك القيم الأعلى المزيد من النطاق الترددي وموارد الجهاز."; +"Quality" = "الجودة"; +"Max Concurrent Downloads" = "الحد الأقصى للتنزيلات المتزامنة"; +"Allow Cellular Downloads" = "السماح بالتنزيلات عبر بيانات الجوال"; +"Quality Information" = "معلومات الجودة"; + +/* Storage */ +"Storage Management" = "إدارة التخزين"; +"Storage Used" = "المساحة المستخدمة"; +"Library cleared successfully" = "تم مسح المكتبة بنجاح"; +"All downloads deleted successfully" = "تم حذف جميع التنزيلات بنجاح"; \ No newline at end of file diff --git a/Sora/en.lproj/Localizable.strings b/Sora/en.lproj/Localizable.strings new file mode 100644 index 0000000..19cd7cf --- /dev/null +++ b/Sora/en.lproj/Localizable.strings @@ -0,0 +1,384 @@ +/* General */ +"About" = "About"; +"About Sora" = "About Sora"; +"Active" = "Active"; +"Active Downloads" = "Active Downloads"; +"Actively downloading media can be tracked from here." = "Actively downloading media can be tracked from here."; +"Add Module" = "Add Module"; +"Adjust the number of media items per row in portrait and landscape modes." = "Adjust the number of media items per row in portrait and landscape modes."; +"Advanced" = "Advanced"; +"AKA Sulfur" = "AKA Sulfur"; +"All Bookmarks" = "All Bookmarks"; +"All Watching" = "All Watching"; +"Also known as Sulfur" = "Also known as Sulfur"; +"AniList" = "AniList"; +"AniList ID" = "AniList ID"; +"AniList Match" = "AniList Match"; +"AniList.co" = "AniList.co"; +"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time."; +"App Info" = "App Info"; +"App Language" = "App Language"; +"App Storage" = "App Storage"; +"Appearance" = "Appearance"; + +/* Alerts and Actions */ +"Are you sure you want to clear all cached data? This will help free up storage space." = "Are you sure you want to clear all cached data? This will help free up storage space."; +"Are you sure you want to delete '%@'?" = "Are you sure you want to delete '%@'?"; +"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Are you sure you want to delete all %1$d episodes in '%2$@'?"; +"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use."; +"Are you sure you want to erase all app data? This action cannot be undone." = "Are you sure you want to erase all app data? This action cannot be undone."; + +/* Features */ +"Background Enabled" = "Background Enabled"; +"Bookmark items for an easier access later." = "Bookmark items for an easier access later."; +"Bookmarks" = "Bookmarks"; +"Bottom Padding" = "Bottom Padding"; +"Cancel" = "Cancel"; +"Cellular Quality" = "Cellular Quality"; +"Check out some community modules here!" = "Check out some community modules here!"; +"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality." = "Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality."; +"Clear" = "Clear"; +"Clear All Downloads" = "Clear All Downloads"; +"Clear Cache" = "Clear Cache"; +"Clear Library Only" = "Clear Library Only"; +"Clear Logs" = "Clear Logs"; +"Click the plus button to add a module!" = "Click the plus button to add a module!"; +"Continue Watching" = "Continue Watching"; +"Continue Watching Episode %d" = "Continue Watching Episode %d"; +"Contributors" = "Contributors"; +"Copied to Clipboard" = "Copied to Clipboard"; +"Copy to Clipboard" = "Copy to Clipboard"; +"Copy URL" = "Copy URL"; + +/* Episodes */ +"%lld Episodes" = "%lld Episodes"; +"%lld of %lld" = "%1$lld of %2$lld"; +"%lld-%lld" = "%1$lld-%2$lld"; +"%lld%% seen" = "%lld%% seen"; +"Episode %lld" = "Episode %lld"; +"Episodes" = "Episodes"; +"Episodes might not be available yet or there could be an issue with the source." = "Episodes might not be available yet or there could be an issue with the source."; +"Episodes Range" = "Episodes Range"; + +/* System */ +"cranci1" = "cranci1"; +"Dark" = "Dark"; +"DATA & LOGS" = "DATA & LOGS"; +"Debug" = "Debug"; +"Debugging and troubleshooting." = "Debugging and troubleshooting."; + +/* Actions */ +"Delete" = "Delete"; +"Delete All" = "Delete All"; +"Delete All Downloads" = "Delete All Downloads"; +"Delete All Episodes" = "Delete All Episodes"; +"Delete Download" = "Delete Download"; +"Delete Episode" = "Delete Episode"; + +/* Player */ +"Double Tap to Seek" = "Double Tap to Seek"; +"Double tapping the screen on it's sides will skip with the short tap setting." = "Double tapping the screen on it's sides will skip with the short tap setting."; + +/* Downloads */ +"Download" = "Download"; +"Download Episode" = "Download Episode"; +"Download Summary" = "Download Summary"; +"Download This Episode" = "Download This Episode"; +"Downloaded" = "Downloaded"; +"Downloaded Shows" = "Downloaded Shows"; +"Downloading" = "Downloading"; +"Downloads" = "Downloads"; + +/* Settings */ +"Enable Analytics" = "Enable Analytics"; +"Enable Subtitles" = "Enable Subtitles"; + +/* Data Management */ +"Erase" = "Erase"; +"Erase all App Data" = "Erase all App Data"; +"Erase App Data" = "Erase App Data"; + +/* Errors */ +"Error" = "Error"; +"Error Fetching Results" = "Error Fetching Results"; +"Errors and critical issues." = "Errors and critical issues."; +"Failed to load contributors" = "Failed to load contributors"; + +/* Features */ +"Fetch Episode metadata" = "Fetch Episode metadata"; +"Files Downloaded" = "Files Downloaded"; +"Font Size" = "Font Size"; + +/* Interface */ +"Force Landscape" = "Force Landscape"; +"General" = "General"; +"General events and activities." = "General events and activities."; +"General Preferences" = "General Preferences"; +"Hide Splash Screen" = "Hide Splash Screen"; +"HLS video downloading." = "HLS video downloading."; +"Hold Speed" = "Hold Speed"; + +/* Info */ +"Info" = "Info"; +"INFOS" = "INFOS"; +"Installed Modules" = "Installed Modules"; +"Interface" = "Interface"; + +/* Social */ +"Join the Discord" = "Join the Discord"; + +/* Layout */ +"Landscape Columns" = "Landscape Columns"; +"Language" = "Language"; +"LESS" = "LESS"; + +/* Library */ +"Library" = "Library"; +"License (GPLv3.0)" = "License (GPLv3.0)"; +"Light" = "Light"; + +/* Loading States */ +"Loading Episode %lld..." = "Loading Episode %lld..."; +"Loading logs..." = "Loading logs..."; +"Loading module information..." = "Loading module information..."; +"Loading Stream" = "Loading Stream"; + +/* Logging */ +"Log Debug Info" = "Log Debug Info"; +"Log Filters" = "Log Filters"; +"Log In with AniList" = "Log In with AniList"; +"Log In with Trakt" = "Log In with Trakt"; +"Log Out from AniList" = "Log Out from AniList"; +"Log Out from Trakt" = "Log Out from Trakt"; +"Log Types" = "Log Types"; +"Logged in as" = "Logged in as"; +"Logged in as " = "Logged in as "; + +/* Logs and Settings */ +"Logs" = "Logs"; +"Long press Skip" = "Long press Skip"; +"MAIN" = "Main Settings"; +"Main Developer" = "Main Developer"; +"MAIN SETTINGS" = "MAIN SETTINGS"; + +/* Media Actions */ +"Mark All Previous Watched" = "Mark All Previous Watched"; +"Mark as Watched" = "Mark as Watched"; +"Mark Episode as Watched" = "Mark Episode as Watched"; +"Mark Previous Episodes as Watched" = "Mark Previous Episodes as Watched"; +"Mark watched" = "Mark watched"; +"Match with AniList" = "Match with AniList"; +"Match with TMDB" = "Match with TMDB"; +"Matched ID: %lld" = "Matched ID: %lld"; +"Matched with: %@" = "Matched with: %@"; +"Max Concurrent Downloads" = "Max Concurrent Downloads"; + +/* Media Interface */ +"Media Grid Layout" = "Media Grid Layout"; +"Media Player" = "Media Player"; +"Media View" = "Media View"; +"Metadata Provider" = "Metadata Provider"; +"Metadata Providers Order" = "Metadata Providers Order"; +"Module Removed" = "Module Removed"; +"Modules" = "Modules"; + +/* Headers */ +"MODULES" = "MODULES"; +"MORE" = "MORE"; + +/* Status Messages */ +"No Active Downloads" = "No Active Downloads"; +"No AniList matches found" = "No AniList matches found"; +"No Data Available" = "No Data Available"; +"No Downloads" = "No Downloads"; +"No episodes available" = "No episodes available"; +"No Episodes Available" = "No Episodes Available"; +"No items to continue watching." = "No items to continue watching."; +"No matches found" = "No matches found"; +"No Module Selected" = "No Module Selected"; +"No Modules" = "No Modules"; +"No Results Found" = "No Results Found"; +"No Search Results Found" = "No Search Results Found"; +"Nothing to Continue Watching" = "Nothing to Continue Watching"; + +/* Notes and Messages */ +"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Note that the modules will be replaced only if there is a different version string inside the JSON file."; + +/* Actions */ +"OK" = "OK"; +"Open Community Library" = "Open Community Library"; + +/* External Services */ +"Open in AniList" = "Open in AniList"; +"Original Poster" = "Original Poster"; + +/* Playback */ +"Paused" = "Paused"; +"Play" = "Play"; +"Player" = "Player"; + +/* System Messages */ +"Please restart the app to apply the language change." = "Please restart the app to apply the language change."; +"Please select a module from settings" = "Please select a module from settings"; + +/* Interface */ +"Portrait Columns" = "Portrait Columns"; +"Progress bar Marker Color" = "Progress bar Marker Color"; +"Provider: %@" = "Provider: %@"; + +/* Queue */ +"Queue" = "Queue"; +"Queued" = "Queued"; + +/* Content */ +"Recently watched content will appear here." = "Recently watched content will appear here."; + +/* Settings */ +"Refresh Modules on Launch" = "Refresh Modules on Launch"; +"Refresh Storage Info" = "Refresh Storage Info"; +"Remember Playback speed" = "Remember Playback speed"; + +/* Actions */ +"Remove" = "Remove"; +"Remove All Cache" = "Remove All Cache"; + +/* File Management */ +"Remove All Documents" = "Remove All Documents"; +"Remove Documents" = "Remove Documents"; +"Remove Downloaded Media" = "Remove Downloaded Media"; +"Remove Downloads" = "Remove Downloads"; +"Remove from Bookmarks" = "Remove from Bookmarks"; +"Remove Item" = "Remove Item"; + +/* Support */ +"Report an Issue" = "Report an Issue"; + +/* Reset Options */ +"Reset" = "Reset"; +"Reset AniList ID" = "Reset AniList ID"; +"Reset Episode Progress" = "Reset Episode Progress"; +"Reset progress" = "Reset progress"; +"Reset Progress" = "Reset Progress"; + +/* System */ +"Restart Required" = "Restart Required"; +"Running Sora %@ - cranci1" = "Running Sora %@ - cranci1"; + +/* Actions */ +"Save" = "Save"; +"Search" = "Search"; + +/* Search */ +"Search downloads" = "Search downloads"; +"Search for something..." = "Search for something..."; +"Search..." = "Search..."; + +/* Content */ +"Season %d" = "Season %d"; +"Season %lld" = "Season %lld"; +"Segments Color" = "Segments Color"; + +/* Modules */ +"Select Module" = "Select Module"; +"Set Custom AniList ID" = "Set Custom AniList ID"; + +/* Interface */ +"Settings" = "Settings"; +"Shadow" = "Shadow"; +"Show More (%lld more characters)" = "Show More (%lld more characters)"; +"Show PiP Button" = "Show PiP Button"; +"Show Skip 85s Button" = "Show Skip 85s Button"; +"Show Skip Intro / Outro Buttons" = "Show Skip Intro / Outro Buttons"; +"Shows" = "Shows"; +"Size (%@)" = "Size (%@)"; +"Skip Settings" = "Skip Settings"; + +/* Player Features */ +"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments."; + +/* App Info */ +"Sora" = "Sora"; +"Sora %@ by cranci1" = "Sora %@ by cranci1"; +"Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." = "Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate."; +"Sora GitHub Repository" = "Sora GitHub Repository"; +"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur will always remain free with no ADs!"; + +/* Interface */ +"Sort" = "Sort"; +"Speed Settings" = "Speed Settings"; + +/* Playback */ +"Start Watching" = "Start Watching"; +"Start Watching Episode %d" = "Start Watching Episode %d"; +"Storage Used" = "Storage Used"; +"Stream" = "Stream"; +"Streaming and video playback." = "Streaming and video playback."; + +/* Subtitles */ +"Subtitle Color" = "Subtitle Color"; +"Subtitle Settings" = "Subtitle Settings"; + +/* Sync */ +"Sync anime progress" = "Sync anime progress"; +"Sync TV shows progress" = "Sync TV shows progress"; + +/* System */ +"System" = "System"; + +/* Instructions */ +"Tap a title to override the current match." = "Tap a title to override the current match."; +"Tap Skip" = "Tap Skip"; +"Tap to manage your modules" = "Tap to manage your modules"; +"Tap to select a module" = "Tap to select a module"; + +/* App Information */ +"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction."; +"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers."; +"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases."; + +/* Interface */ +"Thumbnails Width" = "Thumbnails Width"; +"TMDB Match" = "TMDB Match"; +"Trackers" = "Trackers"; +"Trakt" = "Trakt"; +"Trakt.tv" = "Trakt.tv"; + +/* Search */ +"Try different keywords" = "Try different keywords"; +"Try different search terms" = "Try different search terms"; + +/* Player Controls */ +"Two Finger Hold for Pause" = "Two Finger Hold for Pause"; +"Unable to fetch matches. Please try again later." = "Unable to fetch matches. Please try again later."; +"Use TMDB Poster Image" = "Use TMDB Poster Image"; + +/* Version */ +"v%@" = "v%@"; +"Video Player" = "Video Player"; + +/* Video Settings */ +"Video Quality Preferences" = "Video Quality Preferences"; +"View All" = "View All"; +"Watched" = "Watched"; +"Why am I not seeing any episodes?" = "Why am I not seeing any episodes?"; +"WiFi Quality" = "WiFi Quality"; + +/* User Status */ +"You are not logged in" = "You are not logged in"; +"You have no items saved." = "You have no items saved."; +"Your downloaded episodes will appear here" = "Your downloaded episodes will appear here"; +"Your recently watched content will appear here" = "Your recently watched content will appear here"; + +/* Download Settings */ +"Download Settings" = "Download Settings"; +"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources."; +"Quality" = "Quality"; +"Max Concurrent Downloads" = "Max Concurrent Downloads"; +"Allow Cellular Downloads" = "Allow Cellular Downloads"; +"Quality Information" = "Quality Information"; + +/* Storage */ +"Storage Management" = "Storage Management"; +"Storage Used" = "Storage Used"; +"Library cleared successfully" = "Library cleared successfully"; +"All downloads deleted successfully" = "All downloads deleted successfully"; \ No newline at end of file diff --git a/Sora/fr.lproj/Localizable.strings b/Sora/fr.lproj/Localizable.strings new file mode 100644 index 0000000..2dae36f --- /dev/null +++ b/Sora/fr.lproj/Localizable.strings @@ -0,0 +1,384 @@ +/* General */ +"About" = "À propos"; +"About Sora" = "À propos de Sora"; +"Active" = "Actif"; +"Active Downloads" = "Téléchargements actifs"; +"Actively downloading media can be tracked from here." = "Les médias en cours de téléchargement peuvent être suivis ici."; +"Add Module" = "Ajouter un module"; +"Adjust the number of media items per row in portrait and landscape modes." = "Ajustez le nombre d'éléments multimédias par ligne en mode portrait et paysage."; +"Advanced" = "Avancé"; +"AKA Sulfur" = "Aussi connu sous le nom de Sulfur"; +"All Bookmarks" = "Tous les favoris"; +"All Watching" = "Tout ce que je regarde"; +"Also known as Sulfur" = "Aussi connu sous le nom de Sulfur"; +"AniList" = "AniList"; +"AniList ID" = "ID AniList"; +"AniList Match" = "Correspondance AniList"; +"AniList.co" = "AniList.co"; +"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Des données anonymes sont collectées pour améliorer l'application. Aucune information personnelle n'est collectée. Ceci peut être désactivé à tout moment."; +"App Info" = "Infos sur l'application"; +"App Language" = "Langue de l'application"; +"App Storage" = "Stockage de l'application"; +"Appearance" = "Apparence"; + +/* Alerts and Actions */ +"Are you sure you want to clear all cached data? This will help free up storage space." = "Voulez-vous vraiment vider toutes les données en cache ? Cela aidera à libérer de l'espace de stockage."; +"Are you sure you want to delete '%@'?" = "Voulez-vous vraiment supprimer '%@' ?"; +"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Voulez-vous vraiment supprimer les %1$d épisodes de '%2$@' ?"; +"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Voulez-vous vraiment supprimer tous les fichiers téléchargés ? Vous pouvez choisir de vider uniquement la bibliothèque tout en conservant les fichiers téléchargés pour une utilisation future."; +"Are you sure you want to erase all app data? This action cannot be undone." = "Voulez-vous vraiment effacer toutes les données de l'application ? Cette action est irréversible."; + +/* Features */ +"Background Enabled" = "Activé en arrière-plan"; +"Bookmark items for an easier access later." = "Mettez des éléments en favoris pour un accès plus facile plus tard."; +"Bookmarks" = "Favoris"; +"Bottom Padding" = "Marge intérieure inférieure"; +"Cancel" = "Annuler"; +"Cellular Quality" = "Qualité cellulaire"; +"Check out some community modules here!" = "Découvrez quelques modules communautaires ici !"; +"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality." = "Choisissez la résolution vidéo préférée pour les connexions WiFi et cellulaires. Les résolutions plus élevées utilisent plus de données mais offrent une meilleure qualité."; +"Clear" = "Vider"; +"Clear All Downloads" = "Vider tous les téléchargements"; +"Clear Cache" = "Vider le cache"; +"Clear Library Only" = "Vider la bibliothèque uniquement"; +"Clear Logs" = "Vider les journaux"; +"Click the plus button to add a module!" = "Cliquez sur le bouton plus pour ajouter un module !"; +"Continue Watching" = "Reprendre la lecture"; +"Continue Watching Episode %d" = "Reprendre l'épisode %d"; +"Contributors" = "Contributeurs"; +"Copied to Clipboard" = "Copié dans le presse-papiers"; +"Copy to Clipboard" = "Copier dans le presse-papiers"; +"Copy URL" = "Copier l'URL"; + +/* Episodes */ +"%lld Episodes" = "%lld épisodes"; +"%lld of %lld" = "%1$lld sur %2$lld"; +"%lld-%lld" = "%1$lld-%2$lld"; +"%lld%% seen" = "%lld%% vus"; +"Episode %lld" = "Épisode %lld"; +"Episodes" = "Épisodes"; +"Episodes might not be available yet or there could be an issue with the source." = "Les épisodes ne sont peut-être pas encore disponibles ou il y a un problème avec la source."; +"Episodes Range" = "Plage d'épisodes"; + +/* System */ +"cranci1" = "cranci1"; +"Dark" = "Sombre"; +"DATA & LOGS" = "DONNÉES & JOURNEAUX"; +"Debug" = "Débogage"; +"Debugging and troubleshooting." = "Débogage et résolution de problèmes."; + +/* Actions */ +"Delete" = "Supprimer"; +"Delete All" = "Tout supprimer"; +"Delete All Downloads" = "Supprimer tous les téléchargements"; +"Delete All Episodes" = "Supprimer tous les épisodes"; +"Delete Download" = "Supprimer le téléchargement"; +"Delete Episode" = "Supprimer l'épisode"; + +/* Player */ +"Double Tap to Seek" = "Touchez deux fois pour avancer"; +"Double tapping the screen on it's sides will skip with the short tap setting." = "Appuyer deux fois sur les côtés de l'écran permet de sauter avec le réglage de pression courte."; + +/* Downloads */ +"Download" = "Télécharger"; +"Download Episode" = "Télécharger l'épisode"; +"Download Summary" = "Résumé du téléchargement"; +"Download This Episode" = "Télécharger cet épisode"; +"Downloaded" = "Téléchargé"; +"Downloaded Shows" = "Séries téléchargées"; +"Downloading" = "Téléchargement en cours"; +"Downloads" = "Téléchargements"; + +/* Settings */ +"Enable Analytics" = "Activer les analyses"; +"Enable Subtitles" = "Activer les sous-titres"; + +/* Data Management */ +"Erase" = "Effacer"; +"Erase all App Data" = "Effacer toutes les données de l'app"; +"Erase App Data" = "Effacer les données de l'app"; + +/* Errors */ +"Error" = "Erreur"; +"Error Fetching Results" = "Erreur lors de la récupération des résultats"; +"Errors and critical issues." = "Erreurs et problèmes critiques."; +"Failed to load contributors" = "Échec du chargement des contributeurs"; + +/* Features */ +"Fetch Episode metadata" = "Récupérer les métadonnées de l'épisode"; +"Files Downloaded" = "Fichiers téléchargés"; +"Font Size" = "Taille de la police"; + +/* Interface */ +"Force Landscape" = "Forcer le mode paysage"; +"General" = "Général"; +"General events and activities." = "Événements et activités générales."; +"General Preferences" = "Préférences générales"; +"Hide Splash Screen" = "Masquer l'écran de démarrage"; +"HLS video downloading." = "Téléchargement de vidéos HLS."; +"Hold Speed" = "Vitesse de maintien"; + +/* Info */ +"Info" = "Infos"; +"INFOS" = "INFOS"; +"Installed Modules" = "Modules installés"; +"Interface" = "Interface"; + +/* Social */ +"Join the Discord" = "Rejoindre le Discord"; + +/* Layout */ +"Landscape Columns" = "Colonnes en paysage"; +"Language" = "Langue"; +"LESS" = "MOINS"; + +/* Library */ +"Library" = "Bibliothèque"; +"License (GPLv3.0)" = "Licence (GPLv3.0)"; +"Light" = "Clair"; + +/* Loading States */ +"Loading Episode %lld..." = "Chargement de l'épisode %lld..."; +"Loading logs..." = "Chargement des journaux..."; +"Loading module information..." = "Chargement des informations du module..."; +"Loading Stream" = "Chargement du flux"; + +/* Logging */ +"Log Debug Info" = "Journaliser les infos de débogage"; +"Log Filters" = "Filtres de journal"; +"Log In with AniList" = "Se connecter avec AniList"; +"Log In with Trakt" = "Se connecter avec Trakt"; +"Log Out from AniList" = "Se déconnecter d'AniList"; +"Log Out from Trakt" = "Se déconnecter de Trakt"; +"Log Types" = "Types de journaux"; +"Logged in as" = "Connecté en tant que"; +"Logged in as " = "Connecté en tant que "; + +/* Logs and Settings */ +"Logs" = "Journaux"; +"Long press Skip" = "Pression longue pour sauter"; +"MAIN" = "PRINCIPAL"; +"Main Developer" = "Développeur principal"; +"MAIN SETTINGS" = "RÉGLAGES PRINCIPAUX"; + +/* Media Actions */ +"Mark All Previous Watched" = "Marquer tout comme vu"; +"Mark as Watched" = "Marquer comme vu"; +"Mark Episode as Watched" = "Marquer l'épisode comme vu"; +"Mark Previous Episodes as Watched" = "Marquer les épisodes précédents comme vus"; +"Mark watched" = "Marquer comme vu"; +"Match with AniList" = "Associer avec AniList"; +"Match with TMDB" = "Associer avec TMDB"; +"Matched ID: %lld" = "ID associé : %lld"; +"Matched with: %@" = "Associé avec : %@"; +"Max Concurrent Downloads" = "Téléchargements simultanés max"; + +/* Media Interface */ +"Media Grid Layout" = "Mise en page de la grille média"; +"Media Player" = "Lecteur multimédia"; +"Media View" = "Vue multimédia"; +"Metadata Provider" = "Fournisseur de métadonnées"; +"Metadata Providers Order" = "Ordre des fournisseurs de métadonnées"; +"Module Removed" = "Module supprimé"; +"Modules" = "Modules"; + +/* Headers */ +"MODULES" = "MODULES"; +"MORE" = "PLUS"; + +/* Status Messages */ +"No Active Downloads" = "Aucun téléchargement actif"; +"No AniList matches found" = "Aucune correspondance AniList trouvée"; +"No Data Available" = "Aucune donnée disponible"; +"No Downloads" = "Aucun téléchargement"; +"No episodes available" = "Aucun épisode disponible"; +"No Episodes Available" = "Aucun épisode disponible"; +"No items to continue watching." = "Aucun élément à reprendre."; +"No matches found" = "Aucune correspondance trouvée"; +"No Module Selected" = "Aucun module sélectionné"; +"No Modules" = "Aucun module"; +"No Results Found" = "Aucun résultat trouvé"; +"No Search Results Found" = "Aucun résultat de recherche trouvé"; +"Nothing to Continue Watching" = "Rien à reprendre"; + +/* Notes and Messages */ +"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Notez que les modules ne seront remplacés que s'il existe une chaîne de version différente dans le fichier JSON."; + +/* Actions */ +"OK" = "OK"; +"Open Community Library" = "Ouvrir la bibliothèque communautaire"; + +/* External Services */ +"Open in AniList" = "Ouvrir dans AniList"; +"Original Poster" = "Affiche originale"; + +/* Playback */ +"Paused" = "En pause"; +"Play" = "Lecture"; +"Player" = "Lecteur"; + +/* System Messages */ +"Please restart the app to apply the language change." = "Veuillez redémarrer l'application pour appliquer le changement de langue."; +"Please select a module from settings" = "Veuillez sélectionner un module dans les paramètres"; + +/* Interface */ +"Portrait Columns" = "Colonnes en portrait"; +"Progress bar Marker Color" = "Couleur du marqueur de la barre de progression"; +"Provider: %@" = "Fournisseur : %@"; + +/* Queue */ +"Queue" = "File d'attente"; +"Queued" = "En attente"; + +/* Content */ +"Recently watched content will appear here." = "Le contenu récemment visionné apparaîtra ici."; + +/* Settings */ +"Refresh Modules on Launch" = "Actualiser les modules au lancement"; +"Refresh Storage Info" = "Actualiser les infos de stockage"; +"Remember Playback speed" = "Mémoriser la vitesse de lecture"; + +/* Actions */ +"Remove" = "Supprimer"; +"Remove All Cache" = "Supprimer tout le cache"; + +/* File Management */ +"Remove All Documents" = "Supprimer tous les documents"; +"Remove Documents" = "Supprimer les documents"; +"Remove Downloaded Media" = "Supprimer les médias téléchargés"; +"Remove Downloads" = "Supprimer les téléchargements"; +"Remove from Bookmarks" = "Supprimer des favoris"; +"Remove Item" = "Supprimer l'élément"; + +/* Support */ +"Report an Issue" = "Signaler un problème"; + +/* Reset Options */ +"Reset" = "Réinitialiser"; +"Reset AniList ID" = "Réinitialiser l'ID AniList"; +"Reset Episode Progress" = "Réinitialiser la progression de l'épisode"; +"Reset progress" = "Réinitialiser la progression"; +"Reset Progress" = "Réinitialiser la progression"; + +/* System */ +"Restart Required" = "Redémarrage requis"; +"Running Sora %@ - cranci1" = "Sora %@ en cours d'exécution - cranci1"; + +/* Actions */ +"Save" = "Enregistrer"; +"Search" = "Rechercher"; + +/* Search */ +"Search downloads" = "Rechercher dans les téléchargements"; +"Search for something..." = "Rechercher quelque chose..."; +"Search..." = "Rechercher..."; + +/* Content */ +"Season %d" = "Saison %d"; +"Season %lld" = "Saison %lld"; +"Segments Color" = "Couleur des segments"; + +/* Modules */ +"Select Module" = "Sélectionner un module"; +"Set Custom AniList ID" = "Définir un ID AniList personnalisé"; + +/* Interface */ +"Settings" = "Paramètres"; +"Shadow" = "Ombre"; +"Show More (%lld more characters)" = "Afficher plus (%lld caractères de plus)"; +"Show PiP Button" = "Afficher le bouton PiP"; +"Show Skip 85s Button" = "Afficher le bouton Sauter 85s"; +"Show Skip Intro / Outro Buttons" = "Afficher les boutons Sauter l'intro / l'outro"; +"Shows" = "Séries"; +"Size (%@)" = "Taille (%@)"; +"Skip Settings" = "Réglages de saut"; + +/* Player Features */ +"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Certaines fonctionnalités sont limitées au lecteur Sora et par défaut, telles que Forcer Paysage, Vitesse par pression longue et les incréments de saut de temps personnalisés."; + +/* App Info */ +"Sora" = "Sora"; +"Sora %@ by cranci1" = "Sora %@ par cranci1"; +"Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." = "Sora et cranci1 ne sont en aucun cas affiliés à AniList ou Trakt.\n\nNotez également que les mises à jour de progression peuvent ne pas être précises à 100%."; +"Sora GitHub Repository" = "Dépôt GitHub de Sora"; +"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur restera toujours gratuit et sans publicités !"; + +/* Interface */ +"Sort" = "Trier"; +"Speed Settings" = "Réglages de vitesse"; + +/* Playback */ +"Start Watching" = "Commencer à regarder"; +"Start Watching Episode %d" = "Commencer l'épisode %d"; +"Storage Used" = "Stockage utilisé"; +"Stream" = "Flux"; +"Streaming and video playback." = "Streaming et lecture vidéo."; + +/* Subtitles */ +"Subtitle Color" = "Couleur des sous-titres"; +"Subtitle Settings" = "Réglages des sous-titres"; + +/* Sync */ +"Sync anime progress" = "Synchroniser la progression des animes"; +"Sync TV shows progress" = "Synchroniser la progression des séries TV"; + +/* System */ +"System" = "Système"; + +/* Instructions */ +"Tap a title to override the current match." = "Appuyez sur un titre pour remplacer la correspondance actuelle."; +"Tap Skip" = "Appuyer pour sauter"; +"Tap to manage your modules" = "Appuyez pour gérer vos modules"; +"Tap to select a module" = "Appuyez pour sélectionner un module"; + +/* App Information */ +"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "Le cache de l'application permet de charger les images plus rapidement.\n\nLa suppression du dossier Documents effacera tous les modules téléchargés.\n\nN'effacez pas les données de l'application à moins de comprendre les conséquences — cela pourrait entraîner un dysfonctionnement de l'application."; +"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "La plage d'épisodes contrôle le nombre d'épisodes qui apparaissent sur chaque page. Les épisodes sont regroupés en séries (comme 1–25, 26–50, etc.), ce qui vous permet de naviguer plus facilement.\n\nPour les métadonnées d'épisode, cela fait référence à la miniature et au titre de l'épisode, car ils peuvent parfois contenir des spoilers."; +"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Le module n'a fourni qu'un seul épisode, il s'agit très probablement d'un film, nous avons donc décidé de créer des écrans séparés pour ces cas."; + +/* Interface */ +"Thumbnails Width" = "Largeur des miniatures"; +"TMDB Match" = "Correspondance TMDB"; +"Trackers" = "Trackers"; +"Trakt" = "Trakt"; +"Trakt.tv" = "Trakt.tv"; + +/* Search */ +"Try different keywords" = "Essayez différents mots-clés"; +"Try different search terms" = "Essayez différents termes de recherche"; + +/* Player Controls */ +"Two Finger Hold for Pause" = "Maintenir avec deux doigts pour mettre en pause"; +"Unable to fetch matches. Please try again later." = "Impossible de récupérer les correspondances. Veuillez réessayer plus tard."; +"Use TMDB Poster Image" = "Utiliser l'image de l'affiche TMDB"; + +/* Version */ +"v%@" = "v%@"; +"Video Player" = "Lecteur vidéo"; + +/* Video Settings */ +"Video Quality Preferences" = "Préférences de qualité vidéo"; +"View All" = "Voir tout"; +"Watched" = "Vu"; +"Why am I not seeing any episodes?" = "Pourquoi ne vois-je aucun épisode ?"; +"WiFi Quality" = "Qualité WiFi"; + +/* User Status */ +"You are not logged in" = "Vous n'êtes pas connecté"; +"You have no items saved." = "Vous n'avez aucun élément enregistré."; +"Your downloaded episodes will appear here" = "Vos épisodes téléchargés apparaîtront ici"; +"Your recently watched content will appear here" = "Votre contenu récemment visionné apparaîtra ici"; + +/* Download Settings */ +"Download Settings" = "Paramètres de téléchargement"; +"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Le nombre maximum de téléchargements simultanés contrôle le nombre d'épisodes pouvant être téléchargés en même temps. Des valeurs plus élevées peuvent utiliser plus de bande passante et de ressources de l'appareil."; +"Quality" = "Qualité"; +"Max Concurrent Downloads" = "Téléchargements simultanés max"; +"Allow Cellular Downloads" = "Autoriser les téléchargements cellulaires"; +"Quality Information" = "Informations sur la qualité"; + +/* Storage */ +"Storage Management" = "Gestion du stockage"; +"Storage Used" = "Stockage utilisé"; +"Library cleared successfully" = "Bibliothèque vidée avec succès"; +"All downloads deleted successfully" = "Tous les téléchargements ont été supprimés avec succès"; \ No newline at end of file diff --git a/Sora/nl.lproj/Localizable.strings b/Sora/nl.lproj/Localizable.strings new file mode 100644 index 0000000..b14a0d4 --- /dev/null +++ b/Sora/nl.lproj/Localizable.strings @@ -0,0 +1,384 @@ +/* General */ +"About" = "Over"; +"About Sora" = "Over Sora"; +"Active" = "Actief"; +"Active Downloads" = "Actieve Downloads"; +"Actively downloading media can be tracked from here." = "Actief downloaden van media kan hier worden gevolgd."; +"Add Module" = "Module Toevoegen"; +"Adjust the number of media items per row in portrait and landscape modes." = "Pas het aantal media-items per rij aan in staande en liggende modus."; +"Advanced" = "Geavanceerd"; +"AKA Sulfur" = "AKA Sulfur"; +"All Bookmarks" = "Alle Bladwijzers"; +"All Prev" = "Alle vorige"; +"All Watching" = "Alles Wat Ik Kijk"; +"AniList" = "AniList"; +"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Anonieme gegevens worden verzameld om de app te verbeteren. Er worden geen persoonlijke gegevens verzameld. Dit kan op elk moment worden uitgeschakeld."; +"App Info" = "App Info"; +"App Language" = "App Taal"; +"App Storage" = "App Opslag"; +"Appearance" = "Uiterlijk"; + +/* Alerts and Actions */ +"Are you sure you want to clear all cached data? This will help free up storage space." = "Weet je zeker dat je alle gecachte gegevens wilt wissen? Dit helpt opslagruimte vrij te maken."; +"Are you sure you want to delete '%@'?" = "Weet je zeker dat je '%@' wilt verwijderen?"; +"Are you sure you want to delete all %1$lld episodes in '%2$@'?" = "Weet je zeker dat je alle %1$lld afleveringen in '%2$@' wilt verwijderen?"; +"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Weet je zeker dat je alle gedownloade bestanden wilt verwijderen? Je kunt ervoor kiezen om alleen de bibliotheek te wissen terwijl je de gedownloade bestanden voor later gebruik bewaart."; +"Are you sure you want to erase all app data? This action cannot be undone." = "Weet je zeker dat je alle app-gegevens wilt wissen? Deze actie kan niet ongedaan worden gemaakt."; +"Are you sure you want to remove all downloaded media files (.mov, .mp4, .pkg)?" = "Weet je zeker dat je alle gedownloade mediabestanden (.mov, .mp4, .pkg) wilt verwijderen?"; +"Are you sure you want to remove all files in the Documents folder?" = "Weet je zeker dat je alle bestanden in de map Documenten wilt verwijderen?"; + +/* Features */ +"Author" = "Auteur"; +"Background Enabled" = "Achtergrond Ingeschakeld"; +"Bookmark items for an easier access later." = "Bladwijzer items voor eenvoudigere toegang later."; +"Bookmarks" = "Bladwijzers"; +"Bottom Padding" = "Onderste Padding"; +"Cancel" = "Annuleren"; +"Cellular Quality" = "Mobiele Kwaliteit"; +"Check out some community modules here!" = "Bekijk hier enkele community modules!"; +"Choose preferred video resolution for WiFi and cellular connections." = "Kies de gewenste videoresolutie voor WiFi en mobiele verbindingen."; +"Clear" = "Wissen"; +"Clear All Downloads" = "Alle Downloads Wissen"; +"Clear Cache" = "Wis Cache"; +"Clear Library Only" = "Alleen Bibliotheek Wissen"; +"Clear Logs" = "Wis Logs"; +"Click the plus button to add a module!" = "Klik op de plus-knop om een module toe te voegen!"; +"Continue Watching" = "Verder Kijken"; +"Continue Watching Episode %d" = "Verder Kijken Aflevering %d"; +"Contributors" = "Bijdragers"; +"Copied to Clipboard" = "Gekopieerd naar Klembord"; +"Copy to Clipboard" = "Kopiëren naar Klembord"; +"Copy URL" = "Kopieer URL"; + +/* Episodes */ +"%lld Episodes" = "%lld Afleveringen"; +"%lld of %lld" = "%1$lld van %2$lld"; +"%lld-%lld" = "%1$lld-%2$lld"; +"%lld%% seen" = "%lld%% gezien"; +"Episode %lld" = "Aflevering %lld"; +"Episodes" = "Afleveringen"; +"Episodes might not be available yet or there could be an issue with the source." = "Afleveringen zijn mogelijk nog niet beschikbaar of er is een probleem met de bron."; +"Episodes Range" = "Afleveringen Bereik"; + +/* System */ +"cranci1" = "cranci1"; +"Dark" = "Donker"; +"DATA & LOGS" = "DATA & LOGS"; +"Debug" = "Debug"; +"Debugging and troubleshooting." = "Debuggen en probleemoplossing."; + +/* Actions */ +"Delete" = "Verwijderen"; +"Delete All" = "Alles Wissen"; +"Delete All Downloads" = "Alle Downloads Verwijderen"; +"Delete All Episodes" = "Alle Afleveringen Wissen"; +"Delete Download" = "Downloads Wissen"; +"Delete Episode" = "Afleveringen Wissen"; + +/* Player */ +"Double Tap to Seek" = "Dubbel Tikken om te Zoeken"; +"Double tapping the screen on it's sides will skip with the short tap setting." = "Dubbel tikken op de zijkanten van het scherm zal overslaan met de korte tik instelling."; + +/* Downloads */ +"Download" = "Downloaden"; +"Download Episode" = "Aflevering Downloaden"; +"Download Summary" = "Download Samenvatting"; +"Download This Episode" = "Download Deze Aflevering"; +"Downloaded" = "Gedownload"; +"Downloaded Shows" = "Gedownloade Series"; +"Downloading" = "Downloaden"; +"Downloads" = "Downloads"; + +/* Settings */ +"Enable Analytics" = "Analytics Inschakelen"; +"Enable Subtitles" = "Ondertiteling Inschakelen"; + +/* Data Management */ +"Erase" = "Verwijden"; +"Erase all App Data" = "Wis Alle App Data"; +"Erase App Data" = "Verwijder App Data"; + +/* Errors */ +"Error" = "Fout"; +"Error Fetching Results" = "Fout bij Ophalen Resultaten"; +"Errors and critical issues." = "Fouten en kritieke problemen."; +"Failed to load contributors" = "Laden van bijdragers mislukt"; + +/* Features */ +"Fetch Episode metadata" = "Haal Aflevering Metadata op"; +"Files Downloaded" = "Gedownloade Bestanden"; +"Font Size" = "Lettergrootte"; + +/* Interface */ +"Force Landscape" = "Forceer Landschap"; +"General" = "Algemeen"; +"General events and activities." = "Algemene gebeurtenissen en activiteiten."; +"General Preferences" = "Algemene Voorkeuren"; +"Hide Splash Screen" = "Splash Screen Verbergen"; +"HLS video downloading." = "HLS video downloaden."; +"Hold Speed" = "Vasthouden Snelheid"; + +/* Info */ +"Info" = "Info"; +"INFOS" = "INFO"; +"Installed Modules" = "Geïnstalleerde Modules"; +"Interface" = "Interface"; + +/* Social */ +"Join the Discord" = "Word lid van de Discord"; + +/* Layout */ +"Landscape Columns" = "Liggende Kolommen"; +"Language" = "Taal"; +"LESS" = "MINDER"; + +/* Library */ +"Library" = "Bibliotheek"; +"License (GPLv3.0)" = "Licentie (GPLv3.0)"; +"Light" = "Licht"; + +/* Loading States */ +"Loading Episode %lld..." = "Aflevering %lld laden..."; +"Loading logs..." = "Logboeken laden..."; +"Loading module information..." = "Module-informatie laden..."; +"Loading Stream" = "Stream Laden"; + +/* Logging */ +"Log Debug Info" = "Debug Info Loggen"; +"Log Filters" = "Log Filters"; +"Log In with AniList" = "Inloggen met AniList"; +"Log In with Trakt" = "Inloggen met Trakt"; +"Log Out from AniList" = "Uitloggen van AniList"; +"Log Out from Trakt" = "Uitloggen van Trakt"; +"Log Types" = "Logboek Types"; +"Logged in as" = "Ingelogd als"; +"Logged in as " = "Ingelogd als "; + +/* Logs and Settings */ +"Logs" = "Logboeken"; +"Long press Skip" = "Lang Drukken Overslaan"; +"MAIN" = "Hoofdinstellingen"; +"Main Developer" = "Hoofdontwikkelaar"; +"MAIN SETTINGS" = "HOOFDINSTELLINGEN"; + +/* Media Actions */ +"Mark All Previous Watched" = "Markeer alles als gezien"; +"Mark as Watched" = "Markeer als gezien"; +"Mark Episode as Watched" = "Markeer aflevering als gezien"; +"Mark Previous Episodes as Watched" = "Markeer vorige afleveringen als gezien"; +"Mark watched" = "Markeer als gezien"; +"Match with AniList" = "Match met AniList"; +"Match with TMDB" = "Match met TMDB"; +"Matched ID: %lld" = "Gematchte ID: %lld"; +"Matched with: %@" = "Match met: %@"; +"Max Concurrent Downloads" = "Maximaal gelijktijdige downloads"; + +/* Media Interface */ +"Media Grid Layout" = "Media Raster Layout"; +"Media Player" = "Media Speler"; +"Media View" = "Mediaweergave"; +"Metadata Provider" = "Metadata Provider"; +"Metadata Providers Order" = "Metadata Providers Volgorde"; +"Module Removed" = "Module Verwijderd"; +"Modules" = "Modules"; + +/* Headers */ +"MODULES" = "MODULES"; +"MORE" = "MEER"; + +/* Status Messages */ +"No Active Downloads" = "Geen Actieve Downloads"; +"No AniList matches found" = "Geen AniList overeenkomsten gevonden"; +"No Data Available" = "Geen Gegevens Beschikbaar"; +"No Downloads" = "Geen Downloads"; +"No episodes available" = "Geen afleveringen beschikbaar"; +"No Episodes Available" = "Geen Afleveringen Beschikbaar"; +"No items to continue watching." = "Geen items om verder te kijken."; +"No matches found" = "Geen overeenkomsten gevonden"; +"No Module Selected" = "Geen Module Geselecteerd"; +"No Modules" = "Geen Modules"; +"No Results Found" = "Geen Resultaten Gevonden"; +"No Search Results Found" = "Geen Zoekresultaten Gevonden"; +"Nothing to Continue Watching" = "Niets om Verder te Kijken"; + +/* Notes and Messages */ +"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Let op: de modules worden alleen vervangen als er een andere versiestring in het JSON-bestand staat."; + +/* Actions */ +"OK" = "OK"; +"Open Community Library" = "Open Community Bibliotheek"; + +/* External Services */ +"Open in AniList" = "Openen in AniList"; +"Original Poster" = "Originele Poster"; + +/* Playback */ +"Paused" = "Gepauzeerd"; +"Play" = "Afspelen"; +"Player" = "Speler"; + +/* System Messages */ +"Please restart the app to apply the language change." = "Herstart de app om de taalwijziging toe te passen."; +"Please select a module from settings" = "Selecteer een module uit de instellingen"; + +/* Interface */ +"Portrait Columns" = "Staande Kolommen"; +"Progress bar Marker Color" = "Voortgangsbalk Markeerkleur"; +"Provider: %@" = "Provider: %@"; + +/* Queue */ +"Queue" = "Wachtrij"; +"Queued" = "In Wachtrij"; + +/* Content */ +"Recently watched content will appear here." = "Recent bekeken inhoud verschijnt hier."; + +/* Settings */ +"Refresh Modules on Launch" = "Ververs Modules bij Opstarten"; +"Refresh Storage Info" = "Opslaginformatie Vernieuwen"; +"Remember Playback speed" = "Onthoud Afspeelsnelheid"; + +/* Actions */ +"Remove" = "Verwijderen"; +"Remove All Cache" = "Verwijder Alle Cache"; + +/* File Management */ +"Remove All Documents" = "Verwijder Alle Documenten"; +"Remove Documents" = "Documenten Verwijderen"; +"Remove Downloaded Media" = "Gedownloade Media Verwijderen"; +"Remove Downloads" = "Verwijder Downloads"; +"Remove from Bookmarks" = "Verwijderen uit Bladwijzers"; +"Remove Item" = "Item Verwijderen"; + +/* Support */ +"Report an Issue" = "Rapporteer een Probleem"; + +/* Reset Options */ +"Reset" = "Resetten"; +"Reset AniList ID" = "AniList ID Resetten"; +"Reset Episode Progress" = "Afleveringsvoortgang Resetten"; +"Reset progress" = "Voortgang resetten"; +"Reset Progress" = "Voortgang Resetten"; + +/* System */ +"Restart Required" = "Herstart Vereist"; +"Running Sora %@ - cranci1" = "Sora %@ draait - cranci1"; + +/* Actions */ +"Save" = "Opslaan"; +"Search" = "Zoeken"; + +/* Search */ +"Search downloads" = "Downloads zoeken"; +"Search for something..." = "Zoek naar iets..."; +"Search..." = "Zoeken..."; + +/* Content */ +"Season %d" = "Seizoen %d"; +"Season %lld" = "Seizoen %lld"; +"Segments Color" = "Segmenten Kleur"; + +/* Modules */ +"Select Module" = "Module Selecteren"; +"Set Custom AniList ID" = "Aangepaste AniList ID Instellen"; + +/* Interface */ +"Settings" = "Instellingen"; +"Shadow" = "Schaduw"; +"Show More (%lld more characters)" = "Meer Tonen (%lld meer tekens)"; +"Show PiP Button" = "Toon PiP Knop"; +"Show Skip 85s Button" = "Toon Overslaan 85s Knop"; +"Show Skip Intro / Outro Buttons" = "Toon Overslaan Intro / Outro Knoppen"; +"Shows" = "Series"; +"Size (%@)" = "Grootte (%@)"; +"Skip Settings" = "Overslaan Instellingen"; + +/* Player Features */ +"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Sommige functies zijn beperkt tot de Sora en Standaard speler, zoals ForceLandscape, holdSpeed en aangepaste tijd overslaan stappen."; + +/* App Info */ +"Sora" = "Sora"; +"Sora %@ by cranci1" = "Sora %@ door cranci1"; +"Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." = "Sora en cranci1 zijn op geen enkele manier verbonden met AniList of Trakt.\n\nHoud er ook rekening mee dat voortgangsupdates mogelijk niet 100% nauwkeurig zijn."; +"Sora GitHub Repository" = "Sora GitHub Repository"; +"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur blijft altijd gratis zonder advertenties!"; + +/* Interface */ +"Sort" = "Sorteren"; +"Speed Settings" = "Snelheidsinstellingen"; + +/* Playback */ +"Start Watching" = "Start met Kijken"; +"Start Watching Episode %d" = "Start met Kijken Aflevering %d"; +"Storage Used" = "Gebruikte Opslag"; +"Stream" = "Stream"; +"Streaming and video playback." = "Streaming en video afspelen."; + +/* Subtitles */ +"Subtitle Color" = "Ondertitelingskleur"; +"Subtitle Settings" = "Ondertitelingsinstellingen"; + +/* Sync */ +"Sync anime progress" = "Synchroniseer anime voortgang"; +"Sync TV shows progress" = "Synchroniseer TV series voortgang"; + +/* System */ +"System" = "Systeem"; + +/* Instructions */ +"Tap a title to override the current match." = "Tik op een titel om de huidige match te overschrijven."; +"Tap Skip" = "Tik Overslaan"; +"Tap to manage your modules" = "Tik om je modules te beheren"; +"Tap to select a module" = "Tik om een module te selecteren"; + +/* App Information */ +"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "De app cache helpt de app om afbeeldingen sneller te laden.\n\nHet wissen van de Documents map zal alle gedownloade modules verwijderen.\n\nWis de App Data niet tenzij je de gevolgen begrijpt — het kan ervoor zorgen dat de app niet meer goed werkt."; +"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Het afleveringen bereik bepaalt hoeveel afleveringen er op elke pagina verschijnen. Afleveringen worden gegroepeerd in sets (zoals 1-25, 26-50, enzovoort), waardoor je er gemakkelijker doorheen kunt navigeren.\n\nVoor aflevering metadata verwijst dit naar de aflevering miniatuur en titel, aangezien deze soms spoilers kunnen bevatten."; +"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "De module heeft slechts één aflevering geleverd, dit is waarschijnlijk een film, daarom hebben we aparte schermen gemaakt voor deze gevallen."; + +/* Interface */ +"Thumbnails Width" = "Miniatuur Breedte"; +"TMDB Match" = "TMDB Match"; +"Trackers" = "Trackers"; +"Trakt" = "Trakt"; +"Trakt.tv" = "Trakt.tv"; + +/* Search */ +"Try different keywords" = "Probeer andere zoekwoorden"; +"Try different search terms" = "Probeer andere zoektermen"; + +/* Player Controls */ +"Two Finger Hold for Pause" = "Twee Vingers Vasthouden voor Pauze"; +"Unable to fetch matches. Please try again later." = "Kan geen matches ophalen. Probeer het later opnieuw."; +"Use TMDB Poster Image" = "TMDB Poster Afbeelding Gebruiken"; + +/* Version */ +"v%@" = "v%@"; +"Video Player" = "Videospeler"; + +/* Video Settings */ +"Video Quality Preferences" = "Video Kwaliteit Voorkeuren"; +"View All" = "Alles Bekijken"; +"Watched" = "Bekeken"; +"Why am I not seeing any episodes?" = "Waarom zie ik geen afleveringen?"; +"WiFi Quality" = "WiFi Kwaliteit"; + +/* User Status */ +"You are not logged in" = "Je bent niet ingelogd"; +"You have no items saved." = "Je hebt geen items opgeslagen."; +"Your downloaded episodes will appear here" = "Je gedownloade afleveringen verschijnen hier"; +"Your recently watched content will appear here" = "Je recent bekeken inhoud verschijnt hier"; + +/* Download Settings */ +"Download Settings" = "Download Instellingen"; +"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Maximum gelijktijdige downloads bepaalt hoeveel afleveringen tegelijk kunnen worden gedownload. Hogere waarden kunnen meer bandbreedte en apparaatbronnen gebruiken."; +"Quality" = "Kwaliteit"; +"Max Concurrent Downloads" = "Maximum Gelijktijdige Downloads"; +"Allow Cellular Downloads" = "Downloads via Mobiel Netwerk Toestaan"; +"Quality Information" = "Kwaliteitsinformatie"; + +/* Storage */ +"Storage Management" = "Opslagbeheer"; +"Storage Used" = "Gebruikte Opslag"; +"Library cleared successfully" = "Bibliotheek succesvol gewist"; +"All downloads deleted successfully" = "Alle downloads succesvol verwijderd"; \ No newline at end of file diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index ba5a9f6..fb50560 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -17,6 +17,10 @@ 0457C59E2DE78267000AFBD9 /* BookmarkLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457C59B2DE78267000AFBD9 /* BookmarkLink.swift */; }; 0457C59F2DE78267000AFBD9 /* BookmarkGridItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457C5992DE78267000AFBD9 /* BookmarkGridItemView.swift */; }; 0457C5A12DE78385000AFBD9 /* BookmarksDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457C5A02DE78385000AFBD9 /* BookmarksDetailView.swift */; }; + 0488FA952DFDE724007575E1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0488FA902DFDE724007575E1 /* Localizable.strings */; }; + 0488FA962DFDE724007575E1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0488FA932DFDE724007575E1 /* Localizable.strings */; }; + 0488FA9A2DFDF380007575E1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0488FA982DFDF380007575E1 /* Localizable.strings */; }; + 0488FA9E2DFDF3BB007575E1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0488FA9C2DFDF3BB007575E1 /* Localizable.strings */; }; 04CD76DB2DE20F2200733536 /* AllWatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04CD76DA2DE20F2200733536 /* AllWatching.swift */; }; 04EAC39A2DF9E0DB00BBD483 /* SplashScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04EAC3992DF9E0DB00BBD483 /* SplashScreenView.swift */; }; 04F08EDC2DE10BF3006B29D9 /* ProgressiveBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F08EDB2DE10BEC006B29D9 /* ProgressiveBlurView.swift */; }; @@ -36,7 +40,6 @@ 132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1242D9995F900A0140B /* JSController-Search.swift */; }; 13367ECC2DF70698009CB33F /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = 13367ECB2DF70698009CB33F /* Nuke */; }; 13367ECE2DF70698009CB33F /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 13367ECD2DF70698009CB33F /* NukeUI */; }; - 13367ED02DF70819009CB33F /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 13367ECF2DF70819009CB33F /* Localizable.xcstrings */; }; 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; }; 133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; }; 133D7C722D2BE2520075467E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C712D2BE2520075467E /* Assets.xcassets */; }; @@ -112,6 +115,10 @@ 0457C59A2DE78267000AFBD9 /* BookmarkGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkGridView.swift; sourceTree = ""; }; 0457C59B2DE78267000AFBD9 /* BookmarkLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkLink.swift; sourceTree = ""; }; 0457C5A02DE78385000AFBD9 /* BookmarksDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDetailView.swift; sourceTree = ""; }; + 0488FA8F2DFDE724007575E1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Localizable.strings; sourceTree = ""; }; + 0488FA922DFDE724007575E1 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = Localizable.strings; sourceTree = ""; }; + 0488FA992DFDF380007575E1 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Localizable.strings; sourceTree = ""; }; + 0488FA9D2DFDF3BB007575E1 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = Localizable.strings; sourceTree = ""; }; 04CD76DA2DE20F2200733536 /* AllWatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllWatching.swift; sourceTree = ""; }; 04EAC3992DF9E0DB00BBD483 /* SplashScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenView.swift; sourceTree = ""; }; 04F08EDB2DE10BEC006B29D9 /* ProgressiveBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressiveBlurView.swift; sourceTree = ""; }; @@ -130,7 +137,6 @@ 132AF1202D99951700A0140B /* JSController-Streams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Streams.swift"; sourceTree = ""; }; 132AF1222D9995C300A0140B /* JSController-Details.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Details.swift"; sourceTree = ""; }; 132AF1242D9995F900A0140B /* JSController-Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Search.swift"; sourceTree = ""; }; - 13367ECF2DF70819009CB33F /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 133D7C6A2D2BE2500075467E /* Sulfur.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sulfur.app; sourceTree = BUILT_PRODUCTS_DIR; }; 133D7C6D2D2BE2500075467E /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = ""; }; 133D7C6F2D2BE2500075467E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -241,6 +247,38 @@ path = BookmarkComponents; sourceTree = ""; }; + 0488FA912DFDE724007575E1 /* en.lproj */ = { + isa = PBXGroup; + children = ( + 0488FA902DFDE724007575E1 /* Localizable.strings */, + ); + path = en.lproj; + sourceTree = ""; + }; + 0488FA942DFDE724007575E1 /* nl.lproj */ = { + isa = PBXGroup; + children = ( + 0488FA932DFDE724007575E1 /* Localizable.strings */, + ); + path = nl.lproj; + sourceTree = ""; + }; + 0488FA972DFDF334007575E1 /* fr.lproj */ = { + isa = PBXGroup; + children = ( + 0488FA982DFDF380007575E1 /* Localizable.strings */, + ); + path = fr.lproj; + sourceTree = ""; + }; + 0488FA9B2DFDF385007575E1 /* ar.lproj */ = { + isa = PBXGroup; + children = ( + 0488FA9C2DFDF3BB007575E1 /* Localizable.strings */, + ); + path = ar.lproj; + sourceTree = ""; + }; 04F08EDA2DE10BE3006B29D9 /* ProgressiveBlurView */ = { isa = PBXGroup; children = ( @@ -330,6 +368,10 @@ 133D7C6C2D2BE2500075467E /* Sora */ = { isa = PBXGroup; children = ( + 0488FA9B2DFDF385007575E1 /* ar.lproj */, + 0488FA972DFDF334007575E1 /* fr.lproj */, + 0488FA912DFDE724007575E1 /* en.lproj */, + 0488FA942DFDE724007575E1 /* nl.lproj */, 130C6BF82D53A4C200DC1432 /* Sora.entitlements */, 13DC0C412D2EC9BA00D0F966 /* Info.plist */, 13103E802D589D6C000F0673 /* Tracking Services */, @@ -339,7 +381,6 @@ 133D7C6F2D2BE2500075467E /* ContentView.swift */, 133D7C712D2BE2520075467E /* Assets.xcassets */, 133D7C732D2BE2520075467E /* Preview Content */, - 13367ECF2DF70819009CB33F /* Localizable.xcstrings */, ); path = Sora; sourceTree = ""; @@ -676,6 +717,9 @@ knownRegions = ( en, Base, + nl, + fr, + ar, ); mainGroup = 133D7C612D2BE2500075467E; packageReferences = ( @@ -698,9 +742,12 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0488FA9A2DFDF380007575E1 /* Localizable.strings in Resources */, + 0488FA9E2DFDF3BB007575E1 /* Localizable.strings in Resources */, 133D7C752D2BE2520075467E /* Preview Assets.xcassets in Resources */, 133D7C722D2BE2520075467E /* Assets.xcassets in Resources */, - 13367ED02DF70819009CB33F /* Localizable.xcstrings in Resources */, + 0488FA952DFDE724007575E1 /* Localizable.strings in Resources */, + 0488FA962DFDE724007575E1 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -800,6 +847,41 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXVariantGroup section */ + 0488FA902DFDE724007575E1 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 0488FA8F2DFDE724007575E1 /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + 0488FA932DFDE724007575E1 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 0488FA922DFDE724007575E1 /* nl */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + 0488FA982DFDF380007575E1 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 0488FA992DFDF380007575E1 /* fr */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + 0488FA9C2DFDF3BB007575E1 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 0488FA9D2DFDF3BB007575E1 /* ar */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ 133D7C762D2BE2520075467E /* Debug */ = { isa = XCBuildConfiguration; From ef4b18f622c5b5a39bbd624b48e9b7fa7305d826 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Sun, 15 Jun 2025 10:21:22 +0200 Subject: [PATCH 05/15] fixed tmp folder issues --- Sora/SoraApp.swift | 19 ++++- .../Downloads/JSController+Downloader.swift | 1 - .../SettingsSubViews/SettingsViewData.swift | 73 +------------------ 3 files changed, 20 insertions(+), 73 deletions(-) diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index f83c741..5b33256 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -19,6 +19,7 @@ struct SoraApp: App { if let userAccentColor = UserDefaults.standard.color(forKey: "accentColor") { UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = userAccentColor } + clearTmpFolder() TraktToken.checkAuthenticationStatus { isAuthenticated in if isAuthenticated { @@ -103,4 +104,20 @@ struct SoraApp: App { break } } -} \ No newline at end of file + + private func clearTmpFolder() { + let fileManager = FileManager.default + let tmpDirectory = NSTemporaryDirectory() + + do { + let tmpURL = URL(fileURLWithPath: tmpDirectory) + let tmpContents = try fileManager.contentsOfDirectory(at: tmpURL, includingPropertiesForKeys: nil) + + for url in tmpContents { + try fileManager.removeItem(at: url) + } + } catch { + Logger.shared.log("Failed to clear tmp folder: \(error.localizedDescription)", type: "Error") + } + } +} diff --git a/Sora/Utils/JSLoader/Downloads/JSController+Downloader.swift b/Sora/Utils/JSLoader/Downloads/JSController+Downloader.swift index 621152c..9ebe998 100644 --- a/Sora/Utils/JSLoader/Downloads/JSController+Downloader.swift +++ b/Sora/Utils/JSLoader/Downloads/JSController+Downloader.swift @@ -9,7 +9,6 @@ import Foundation import SwiftUI import AVFoundation - struct DownloadRequest { let url: URL let headers: [String: String] diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift index 15e3b6e..13c7685 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift @@ -141,10 +141,9 @@ struct SettingsViewData: View { @State private var isCalculatingSize: Bool = false @State private var cacheSize: Int64 = 0 @State private var documentsSize: Int64 = 0 - @State private var downloadsSize: Int64 = 0 enum ActiveAlert { - case eraseData, removeDocs, removeDownloads, clearCache + case eraseData, removeDocs, clearCache } @State private var activeAlert: ActiveAlert = .eraseData @@ -154,7 +153,7 @@ struct SettingsViewData: View { VStack(spacing: 24) { SettingsSection( title: NSLocalizedString("App Storage", comment: ""), - footer: NSLocalizedString("The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction.", comment: "") + footer: NSLocalizedString("The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nErasing the App Data will clears all your settings and data of the app.", comment: "") ) { VStack(spacing: 0) { SettingsButtonRow( @@ -169,18 +168,6 @@ struct SettingsViewData: View { Divider().padding(.horizontal, 16) - SettingsButtonRow( - icon: "film", - title: NSLocalizedString("Remove Downloads", comment: ""), - subtitle: formatSize(downloadsSize), - action: { - activeAlert = .removeDownloads - showAlert = true - } - ) - - Divider().padding(.horizontal, 16) - SettingsButtonRow( icon: "doc.text", title: NSLocalizedString("Remove All Documents", comment: ""), @@ -209,7 +196,6 @@ struct SettingsViewData: View { .onAppear { calculateCacheSize() updateSizes() - calculateDownloadsSize() } .alert(isPresented: $showAlert) { switch activeAlert { @@ -231,15 +217,6 @@ struct SettingsViewData: View { }, secondaryButton: .cancel() ) - case .removeDownloads: - return Alert( - title: Text(NSLocalizedString("Remove Downloaded Media", comment: "")), - message: Text(NSLocalizedString("Are you sure you want to remove all downloaded media files (.mov, .mp4, .pkg)? This action cannot be undone.", comment: "")), - primaryButton: .destructive(Text(NSLocalizedString("Remove", comment: ""))) { - removeDownloadedMedia() - }, - secondaryButton: .cancel() - ) case .clearCache: return Alert( title: Text(NSLocalizedString("Clear Cache", comment: "")), @@ -286,17 +263,6 @@ struct SettingsViewData: View { } } - func calculateDownloadsSize() { - DispatchQueue.global(qos: .background).async { - if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - let size = calculateMediaFilesSize(in: documentsURL) - DispatchQueue.main.async { - self.downloadsSize = size - } - } - } - } - func calculateMediaFilesSize(in directory: URL) -> Int64 { let fileManager = FileManager.default var totalSize: Int64 = 0 @@ -337,47 +303,12 @@ struct SettingsViewData: View { Logger.shared.log("Cache cleared successfully!", type: "General") calculateCacheSize() updateSizes() - calculateDownloadsSize() } } catch { Logger.shared.log("Failed to clear cache.", type: "Error") } } - func removeDownloadedMedia() { - let fileManager = FileManager.default - let mediaExtensions = [".mov", ".mp4", ".pkg"] - - if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { - removeMediaFiles(in: documentsURL, extensions: mediaExtensions) - Logger.shared.log("Downloaded media files removed", type: "General") - updateSizes() - calculateDownloadsSize() - } - } - - func removeMediaFiles(in directory: URL, extensions: [String]) { - let fileManager = FileManager.default - - do { - let contents = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: [.isDirectoryKey]) - for url in contents { - let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey]) - if resourceValues.isDirectory == true { - removeMediaFiles(in: url, extensions: extensions) - } else { - let fileExtension = ".\(url.pathExtension.lowercased())" - if extensions.contains(fileExtension) { - try fileManager.removeItem(at: url) - Logger.shared.log("Removed media file: \(url.lastPathComponent)", type: "General") - } - } - } - } catch { - Logger.shared.log("Error removing media files in \(directory.path): \(error)", type: "Error") - } - } - func removeAllFilesInDocuments() { let fileManager = FileManager.default if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { From a271858f77b7737b250a9fc32e6a0766ee49ff50 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Sun, 15 Jun 2025 10:26:48 +0200 Subject: [PATCH 06/15] =?UTF-8?q?=F0=9F=98=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sora/Views/MediaInfoView/MediaInfoView.swift | 6 ------ .../SettingsView/SettingsSubViews/SettingsViewData.swift | 1 - Sora/Views/SettingsView/SettingsView.swift | 2 +- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 66155ba..f2920de 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -1280,18 +1280,12 @@ struct MediaInfoView: View { self.tmdbType = type tmdbSuccess = true Logger.shared.log("Successfully fetched TMDB ID: \(id) (type: \(type.rawValue))", type: "Debug") - - if self.activeProvider != "TMDB" { - self.fetchTMDBPosterImageAndSet() - } } else { Logger.shared.log("Failed to fetch TMDB ID", type: "Debug") } checkCompletion() } } - - fetchAniListIDForSync() } private func fetchItemID(byTitle title: String, completion: @escaping (Result) -> Void) { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift index 13c7685..8b33b11 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift @@ -362,4 +362,3 @@ struct SettingsViewData: View { return formatter.string(fromByteCount: bytes) } } - diff --git a/Sora/Views/SettingsView/SettingsView.swift b/Sora/Views/SettingsView/SettingsView.swift index f9be26d..722ebef 100644 --- a/Sora/Views/SettingsView/SettingsView.swift +++ b/Sora/Views/SettingsView/SettingsView.swift @@ -174,7 +174,7 @@ struct SettingsView: View { Divider().padding(.horizontal, 16) NavigationLink(destination: SettingsViewDownloads()) { - SettingsNavigationRow(icon: "arrow.down.circle", titleKey: "Download") + SettingsNavigationRow(icon: "arrow.down.circle", titleKey: "Downloads") } Divider().padding(.horizontal, 16) From 634bda6a9489b66fca28682d9f71ce3fe555c1a1 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Sun, 15 Jun 2025 10:36:36 +0200 Subject: [PATCH 07/15] yeah --- .../SettingsSubViews/SettingsViewData.swift | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift index 8b33b11..a6f67ad 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift @@ -263,31 +263,6 @@ struct SettingsViewData: View { } } - func calculateMediaFilesSize(in directory: URL) -> Int64 { - let fileManager = FileManager.default - var totalSize: Int64 = 0 - let mediaExtensions = [".mov", ".mp4", ".pkg"] - - do { - let contents = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey]) - for url in contents { - let resourceValues = try url.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey]) - if resourceValues.isDirectory == true { - totalSize += calculateMediaFilesSize(in: url) - } else { - let fileExtension = url.pathExtension.lowercased() - if mediaExtensions.contains(".\(fileExtension)") { - totalSize += Int64(resourceValues.fileSize ?? 0) - } - } - } - } catch { - Logger.shared.log("Error calculating media files size: \(error)", type: "Error") - } - - return totalSize - } - func clearAllCaches() { clearCache() } From 0ca54ea38d1502dcbd4843b98b70cd5485163bec Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Sun, 15 Jun 2025 10:50:35 +0200 Subject: [PATCH 08/15] ok well lets test --- .../ContinueWatchingItem.swift | 0 .../ContinueWatchingManager.swift | 0 .../Components/Double+Extension.swift | 0 .../Components/MusicProgressSlider.swift | 0 .../Components/VolumeSlider.swift | 0 .../CustomPlayer/CustomPlayer.swift | 0 .../Helpers/SubtitleSettingsManager.swift | 0 .../Helpers/VTTSubtitlesLoader.swift | 0 .../NormalPlayer}/NormalPlayer.swift | 0 .../NormalPlayer}/VideoPlayer.swift | 87 +++++++++++++++++++ .../SharePlay/SharePlayCoordinator.swift | 78 +++++++++++++++++ .../SharePlay/SharePlayManager.swift | 77 ++++++++++++++++ .../SharePlay/VideoWatchingActivity.swift | 68 +++++++++++++++ Sora/Sora.entitlements | 4 + Sulfur.xcodeproj/project.pbxproj | 46 ++++++++-- 15 files changed, 351 insertions(+), 9 deletions(-) rename Sora/{Utils => MediaUtils}/ContinueWatching/ContinueWatchingItem.swift (100%) rename Sora/{Utils => MediaUtils}/ContinueWatching/ContinueWatchingManager.swift (100%) rename Sora/{Utils/MediaPlayer => MediaUtils}/CustomPlayer/Components/Double+Extension.swift (100%) rename Sora/{Utils/MediaPlayer => MediaUtils}/CustomPlayer/Components/MusicProgressSlider.swift (100%) rename Sora/{Utils/MediaPlayer => MediaUtils}/CustomPlayer/Components/VolumeSlider.swift (100%) rename Sora/{Utils/MediaPlayer => MediaUtils}/CustomPlayer/CustomPlayer.swift (100%) rename Sora/{Utils/MediaPlayer => MediaUtils}/CustomPlayer/Helpers/SubtitleSettingsManager.swift (100%) rename Sora/{Utils/MediaPlayer => MediaUtils}/CustomPlayer/Helpers/VTTSubtitlesLoader.swift (100%) rename Sora/{Utils/MediaPlayer => MediaUtils/NormalPlayer}/NormalPlayer.swift (100%) rename Sora/{Utils/MediaPlayer => MediaUtils/NormalPlayer}/VideoPlayer.swift (79%) create mode 100644 Sora/MediaUtils/SharePlay/SharePlayCoordinator.swift create mode 100644 Sora/MediaUtils/SharePlay/SharePlayManager.swift create mode 100644 Sora/MediaUtils/SharePlay/VideoWatchingActivity.swift diff --git a/Sora/Utils/ContinueWatching/ContinueWatchingItem.swift b/Sora/MediaUtils/ContinueWatching/ContinueWatchingItem.swift similarity index 100% rename from Sora/Utils/ContinueWatching/ContinueWatchingItem.swift rename to Sora/MediaUtils/ContinueWatching/ContinueWatchingItem.swift diff --git a/Sora/Utils/ContinueWatching/ContinueWatchingManager.swift b/Sora/MediaUtils/ContinueWatching/ContinueWatchingManager.swift similarity index 100% rename from Sora/Utils/ContinueWatching/ContinueWatchingManager.swift rename to Sora/MediaUtils/ContinueWatching/ContinueWatchingManager.swift diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift b/Sora/MediaUtils/CustomPlayer/Components/Double+Extension.swift similarity index 100% rename from Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift rename to Sora/MediaUtils/CustomPlayer/Components/Double+Extension.swift diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift b/Sora/MediaUtils/CustomPlayer/Components/MusicProgressSlider.swift similarity index 100% rename from Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift rename to Sora/MediaUtils/CustomPlayer/Components/MusicProgressSlider.swift diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/VolumeSlider.swift b/Sora/MediaUtils/CustomPlayer/Components/VolumeSlider.swift similarity index 100% rename from Sora/Utils/MediaPlayer/CustomPlayer/Components/VolumeSlider.swift rename to Sora/MediaUtils/CustomPlayer/Components/VolumeSlider.swift diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift similarity index 100% rename from Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift rename to Sora/MediaUtils/CustomPlayer/CustomPlayer.swift diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/SubtitleSettingsManager.swift b/Sora/MediaUtils/CustomPlayer/Helpers/SubtitleSettingsManager.swift similarity index 100% rename from Sora/Utils/MediaPlayer/CustomPlayer/Helpers/SubtitleSettingsManager.swift rename to Sora/MediaUtils/CustomPlayer/Helpers/SubtitleSettingsManager.swift diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift b/Sora/MediaUtils/CustomPlayer/Helpers/VTTSubtitlesLoader.swift similarity index 100% rename from Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift rename to Sora/MediaUtils/CustomPlayer/Helpers/VTTSubtitlesLoader.swift diff --git a/Sora/Utils/MediaPlayer/NormalPlayer.swift b/Sora/MediaUtils/NormalPlayer/NormalPlayer.swift similarity index 100% rename from Sora/Utils/MediaPlayer/NormalPlayer.swift rename to Sora/MediaUtils/NormalPlayer/NormalPlayer.swift diff --git a/Sora/Utils/MediaPlayer/VideoPlayer.swift b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift similarity index 79% rename from Sora/Utils/MediaPlayer/VideoPlayer.swift rename to Sora/MediaUtils/NormalPlayer/VideoPlayer.swift index 518c778..c7e036c 100644 --- a/Sora/Utils/MediaPlayer/VideoPlayer.swift +++ b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift @@ -7,6 +7,8 @@ import UIKit import AVKit +import Combine +import GroupActivities class VideoPlayerViewController: UIViewController { let module: ScrapingModule @@ -29,6 +31,9 @@ class VideoPlayerViewController: UIViewController { var subtitlesLoader: VTTSubtitlesLoader? var subtitleLabel: UILabel? + private var sharePlayCoordinator: SharePlayCoordinator? + private var subscriptions = Set() + private var aniListUpdateSent = false private var aniListUpdatedSuccessfully = false private var traktUpdateSent = false @@ -40,6 +45,7 @@ class VideoPlayerViewController: UIViewController { if UserDefaults.standard.object(forKey: "subtitlesEnabled") == nil { UserDefaults.standard.set(true, forKey: "subtitlesEnabled") } + setupSharePlay() } required init?(coder: NSCoder) { @@ -129,6 +135,10 @@ class VideoPlayerViewController: UIViewController { if !subtitles.isEmpty && UserDefaults.standard.bool(forKey: "subtitlesEnabled") { setupSubtitles() } + + // Configure SharePlay after player setup + setupSharePlayButton(in: playerViewController) + configureSharePlayForPlayer() } addPeriodicTimeObserver(fullURL: fullUrl) @@ -275,6 +285,79 @@ class VideoPlayerViewController: UIViewController { } } + @MainActor + private func setupSharePlay() { + sharePlayCoordinator = SharePlayCoordinator() + sharePlayCoordinator?.configureGroupSession() + + if let playerViewController = playerViewController { + setupSharePlayButton(in: playerViewController) + } + } + + private func setupSharePlayButton(in playerViewController: NormalPlayer) { + // WIP + } + + @MainActor + private func startSharePlay() { + guard let streamUrl = streamUrl else { return } + + Task { + var episodeImageData: Data? + if !episodeImageUrl.isEmpty, let imageUrl = URL(string: episodeImageUrl) { + episodeImageData = try? await URLSession.shared.data(from: imageUrl).0 + } + + let activity = VideoWatchingActivity( + mediaTitle: mediaTitle, + episodeNumber: episodeNumber, + streamUrl: streamUrl, + subtitles: subtitles, + aniListID: aniListID, + fullUrl: fullUrl, + headers: headers, + episodeImageUrl: episodeImageUrl, + episodeImageData: episodeImageData, + totalEpisodes: totalEpisodes, + tmdbID: tmdbID, + isMovie: isMovie, + seasonNumber: seasonNumber + ) + + await sharePlayCoordinator?.startSharePlay(with: activity) + } + } + + private func configureSharePlayForPlayer() { + guard let player = player else { return } + sharePlayCoordinator?.coordinatePlayback(with: player) + } + + @MainActor + func presentSharePlayInvitation() { + guard let streamUrl = streamUrl else { + Logger.shared.log("Cannot start SharePlay: Stream URL is nil", type: "Error") + return + } + + SharePlayManager.shared.presentSharePlayInvitation( + from: self, + mediaTitle: mediaTitle, + episodeNumber: episodeNumber, + streamUrl: streamUrl, + subtitles: subtitles, + aniListID: aniListID, + fullUrl: fullUrl, + headers: headers, + episodeImageUrl: episodeImageUrl, + totalEpisodes: totalEpisodes, + tmdbID: tmdbID, + isMovie: isMovie, + seasonNumber: seasonNumber + ) + } + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if UserDefaults.standard.bool(forKey: "alwaysLandscape") { return .landscape @@ -299,5 +382,9 @@ class VideoPlayerViewController: UIViewController { subtitleLabel?.removeFromSuperview() subtitleLabel = nil subtitlesLoader = nil + + sharePlayCoordinator?.leaveGroupSession() + sharePlayCoordinator = nil + subscriptions.removeAll() } } diff --git a/Sora/MediaUtils/SharePlay/SharePlayCoordinator.swift b/Sora/MediaUtils/SharePlay/SharePlayCoordinator.swift new file mode 100644 index 0000000..dea9d61 --- /dev/null +++ b/Sora/MediaUtils/SharePlay/SharePlayCoordinator.swift @@ -0,0 +1,78 @@ +// +// SharePlayCoordinator.swift +// Sora +// +// Created by Francesco on 15/06/25. +// + +import Combine +import Foundation +import AVFoundation +import GroupActivities + +@MainActor +class SharePlayCoordinator: ObservableObject { + private var subscriptions = Set() + private var groupSession: GroupSession? + + @Published var isEligibleForGroupSession = false + @Published var groupSessionState: GroupSession.State = .waiting + + private var playbackCoordinator: AVPlayerPlaybackCoordinator? + + func configureGroupSession() { + Task { + for await session in VideoWatchingActivity.sessions() { + await configureGroupSession(session) + } + } + } + + private func configureGroupSession(_ groupSession: GroupSession) async { + self.groupSession = groupSession + + groupSession.$state + .receive(on: DispatchQueue.main) + .assign(to: &$groupSessionState) + + groupSession.$activeParticipants + .receive(on: DispatchQueue.main) + .sink { participants in + Logger.shared.log("Active participants: \(participants.count)", type: "SharePlay") + } + .store(in: &subscriptions) + + groupSession.join() + } + + func startSharePlay(with activity: VideoWatchingActivity) async { + do { + _ = try await activity.activate() + Logger.shared.log("SharePlay activity activated successfully", type: "SharePlay") + } catch { + Logger.shared.log("Failed to activate SharePlay: \(error.localizedDescription)", type: "Error") + } + } + + func coordinatePlayback(with player: AVPlayer) { + guard let groupSession = groupSession else { return } + + playbackCoordinator = player.playbackCoordinator + playbackCoordinator?.coordinateWithSession(groupSession) + + Logger.shared.log("Playback coordination established", type: "SharePlay") + } + + nonisolated func leaveGroupSession() { + Task { @MainActor in + self.groupSession?.leave() + self.playbackCoordinator = nil + Logger.shared.log("Left SharePlay session", type: "SharePlay") + } + } + + deinit { + subscriptions.removeAll() + playbackCoordinator = nil + } +} diff --git a/Sora/MediaUtils/SharePlay/SharePlayManager.swift b/Sora/MediaUtils/SharePlay/SharePlayManager.swift new file mode 100644 index 0000000..415510c --- /dev/null +++ b/Sora/MediaUtils/SharePlay/SharePlayManager.swift @@ -0,0 +1,77 @@ +// +// SharePlayManager.swift +// Sora +// +// Created by Francesco on 15/06/25. +// + +import UIKit +import Foundation +import GroupActivities + +class SharePlayManager { + static let shared = SharePlayManager() + + private init() {} + + func isSharePlayAvailable() -> Bool { + return true + } + + func presentSharePlayInvitation(from viewController: UIViewController, + mediaTitle: String, + episodeNumber: Int, + streamUrl: String, + subtitles: String = "", + aniListID: Int = 0, + fullUrl: String, + headers: [String: String]? = nil, + episodeImageUrl: String = "", + totalEpisodes: Int = 0, + tmdbID: Int? = nil, + isMovie: Bool = false, + seasonNumber: Int = 1) { + + Task { @MainActor in + var episodeImageData: Data? + if !episodeImageUrl.isEmpty, let imageUrl = URL(string: episodeImageUrl) { + do { + episodeImageData = try await URLSession.shared.data(from: imageUrl).0 + } catch { + Logger.shared.log("Failed to load episode image for SharePlay: \(error.localizedDescription)", type: "Error") + } + } + + let activity = VideoWatchingActivity( + mediaTitle: mediaTitle, + episodeNumber: episodeNumber, + streamUrl: streamUrl, + subtitles: subtitles, + aniListID: aniListID, + fullUrl: fullUrl, + headers: headers, + episodeImageUrl: episodeImageUrl, + episodeImageData: episodeImageData, + totalEpisodes: totalEpisodes, + tmdbID: tmdbID, + isMovie: isMovie, + seasonNumber: seasonNumber + ) + + do { + _ = try await activity.activate() + Logger.shared.log("SharePlay invitation sent successfully", type: "SharePlay") + } catch { + Logger.shared.log("Failed to send SharePlay invitation: \(error.localizedDescription)", type: "Error") + + let alert = UIAlertController( + title: "SharePlay Unavailable", + message: "SharePlay is not available right now. Make sure you're connected to FaceTime or have SharePlay enabled in Control Center.", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + viewController.present(alert, animated: true) + } + } + } +} diff --git a/Sora/MediaUtils/SharePlay/VideoWatchingActivity.swift b/Sora/MediaUtils/SharePlay/VideoWatchingActivity.swift new file mode 100644 index 0000000..f055604 --- /dev/null +++ b/Sora/MediaUtils/SharePlay/VideoWatchingActivity.swift @@ -0,0 +1,68 @@ +// +// VideoWatchingActivity.swift +// Sora +// +// Created by Francesco on 15/06/25. +// + +import UIKit +import Foundation +import GroupActivities + +struct VideoWatchingActivity: GroupActivity { + var metadata: GroupActivityMetadata { + var metadata = GroupActivityMetadata() + metadata.title = mediaTitle + metadata.subtitle = "Episode \(episodeNumber)" + + if let imageData = episodeImageData, + let uiImage = UIImage(data: imageData) { + metadata.previewImage = uiImage.cgImage + } + + metadata.type = .watchTogether + return metadata + } + + let mediaTitle: String + let episodeNumber: Int + let streamUrl: String + let subtitles: String + let aniListID: Int + let fullUrl: String + let headers: [String: String]? + let episodeImageUrl: String + let episodeImageData: Data? + let totalEpisodes: Int + let tmdbID: Int? + let isMovie: Bool + let seasonNumber: Int + + init(mediaTitle: String, + episodeNumber: Int, + streamUrl: String, + subtitles: String = "", + aniListID: Int = 0, + fullUrl: String, + headers: [String: String]? = nil, + episodeImageUrl: String = "", + episodeImageData: Data? = nil, + totalEpisodes: Int = 0, + tmdbID: Int? = nil, + isMovie: Bool = false, + seasonNumber: Int = 1) { + self.mediaTitle = mediaTitle + self.episodeNumber = episodeNumber + self.streamUrl = streamUrl + self.subtitles = subtitles + self.aniListID = aniListID + self.fullUrl = fullUrl + self.headers = headers + self.episodeImageUrl = episodeImageUrl + self.episodeImageData = episodeImageData + self.totalEpisodes = totalEpisodes + self.tmdbID = tmdbID + self.isMovie = isMovie + self.seasonNumber = seasonNumber + } +} diff --git a/Sora/Sora.entitlements b/Sora/Sora.entitlements index b14b07a..4c847eb 100644 --- a/Sora/Sora.entitlements +++ b/Sora/Sora.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.group-session + + com.apple.developer.group-session.video + com.apple.security.app-sandbox com.apple.security.assets.movies.read-write diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index fb50560..5f2a3d8 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -40,6 +40,9 @@ 132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1242D9995F900A0140B /* JSController-Search.swift */; }; 13367ECC2DF70698009CB33F /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = 13367ECB2DF70698009CB33F /* Nuke */; }; 13367ECE2DF70698009CB33F /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 13367ECD2DF70698009CB33F /* NukeUI */; }; + 133CF6A62DFEBE9000BD13F9 /* VideoWatchingActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133CF6A32DFEBE8F00BD13F9 /* VideoWatchingActivity.swift */; }; + 133CF6A72DFEBE9000BD13F9 /* SharePlayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133CF6A42DFEBE8F00BD13F9 /* SharePlayManager.swift */; }; + 133CF6A82DFEBE9000BD13F9 /* SharePlayCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133CF6A52DFEBE9000BD13F9 /* SharePlayCoordinator.swift */; }; 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; }; 133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; }; 133D7C722D2BE2520075467E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C712D2BE2520075467E /* Assets.xcassets */; }; @@ -137,6 +140,9 @@ 132AF1202D99951700A0140B /* JSController-Streams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Streams.swift"; sourceTree = ""; }; 132AF1222D9995C300A0140B /* JSController-Details.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Details.swift"; sourceTree = ""; }; 132AF1242D9995F900A0140B /* JSController-Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Search.swift"; sourceTree = ""; }; + 133CF6A32DFEBE8F00BD13F9 /* VideoWatchingActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoWatchingActivity.swift; sourceTree = ""; }; + 133CF6A42DFEBE8F00BD13F9 /* SharePlayManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharePlayManager.swift; sourceTree = ""; }; + 133CF6A52DFEBE9000BD13F9 /* SharePlayCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharePlayCoordinator.swift; sourceTree = ""; }; 133D7C6A2D2BE2500075467E /* Sulfur.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sulfur.app; sourceTree = BUILT_PRODUCTS_DIR; }; 133D7C6D2D2BE2500075467E /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = ""; }; 133D7C6F2D2BE2500075467E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -349,6 +355,27 @@ path = Analytics; sourceTree = ""; }; + 133CF6A22DFEBE8100BD13F9 /* SharePlay */ = { + isa = PBXGroup; + children = ( + 133CF6A42DFEBE8F00BD13F9 /* SharePlayManager.swift */, + 133CF6A52DFEBE9000BD13F9 /* SharePlayCoordinator.swift */, + 133CF6A32DFEBE8F00BD13F9 /* VideoWatchingActivity.swift */, + ); + path = SharePlay; + sourceTree = ""; + }; + 133CF6A92DFEBEAB00BD13F9 /* MediaUtils */ = { + isa = PBXGroup; + children = ( + 133CF6A22DFEBE8100BD13F9 /* SharePlay */, + 13DC0C442D302C6A00D0F966 /* NormalPlayer */, + 13EA2BD02D32D97400C1EBD7 /* CustomPlayer */, + 13C0E5E82D5F85DD00E7F619 /* ContinueWatching */, + ); + path = MediaUtils; + sourceTree = ""; + }; 133D7C612D2BE2500075467E = { isa = PBXGroup; children = ( @@ -368,15 +395,16 @@ 133D7C6C2D2BE2500075467E /* Sora */ = { isa = PBXGroup; children = ( - 0488FA9B2DFDF385007575E1 /* ar.lproj */, - 0488FA972DFDF334007575E1 /* fr.lproj */, - 0488FA912DFDE724007575E1 /* en.lproj */, 0488FA942DFDE724007575E1 /* nl.lproj */, + 0488FA912DFDE724007575E1 /* en.lproj */, + 0488FA972DFDF334007575E1 /* fr.lproj */, + 0488FA9B2DFDF385007575E1 /* ar.lproj */, 130C6BF82D53A4C200DC1432 /* Sora.entitlements */, 13DC0C412D2EC9BA00D0F966 /* Info.plist */, 13103E802D589D6C000F0673 /* Tracking Services */, - 133D7C852D2BE2640075467E /* Utils */, + 133CF6A92DFEBEAB00BD13F9 /* MediaUtils */, 133D7C7B2D2BE2630075467E /* Views */, + 133D7C852D2BE2640075467E /* Utils */, 133D7C6D2D2BE2500075467E /* SoraApp.swift */, 133D7C6F2D2BE2500075467E /* ContentView.swift */, 133D7C712D2BE2520075467E /* Assets.xcassets */, @@ -446,10 +474,8 @@ 133D7C8A2D2BE2640075467E /* JSLoader */, 1327FBA52D758CEA00FC6689 /* Analytics */, 133D7C862D2BE2640075467E /* Extensions */, - 13DC0C442D302C6A00D0F966 /* MediaPlayer */, 13103E8C2D58E037000F0673 /* SkeletonCells */, 72443C832DC8046500A61321 /* DownloadUtils */, - 13C0E5E82D5F85DD00E7F619 /* ContinueWatching */, ); path = Utils; sourceTree = ""; @@ -593,14 +619,13 @@ path = Auth; sourceTree = ""; }; - 13DC0C442D302C6A00D0F966 /* MediaPlayer */ = { + 13DC0C442D302C6A00D0F966 /* NormalPlayer */ = { isa = PBXGroup; children = ( - 13EA2BD02D32D97400C1EBD7 /* CustomPlayer */, 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */, 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */, ); - path = MediaPlayer; + path = NormalPlayer; sourceTree = ""; }; 13E62FBF2DABC3A20007E259 /* Trakt */ = { @@ -806,6 +831,7 @@ 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */, 722248662DCBC13E00CABE2D /* JSController-Downloads.swift in Sources */, 133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */, + 133CF6A62DFEBE9000BD13F9 /* VideoWatchingActivity.swift in Sources */, 13DB468E2D90093A008CBC03 /* Anilist-Token.swift in Sources */, 1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */, 722248632DCBAA4700CABE2D /* JSController-HeaderManager.swift in Sources */, @@ -813,6 +839,7 @@ 133D7C922D2BE2640075467E /* URLSession.swift in Sources */, 0457C5A12DE78385000AFBD9 /* BookmarksDetailView.swift in Sources */, 133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */, + 133CF6A82DFEBE9000BD13F9 /* SharePlayCoordinator.swift in Sources */, 13E62FC22DABC5830007E259 /* Trakt-Login.swift in Sources */, 133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */, 13E62FC42DABC58C0007E259 /* Trakt-Token.swift in Sources */, @@ -840,6 +867,7 @@ 13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */, 13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */, 0457C59D2DE78267000AFBD9 /* BookmarkGridView.swift in Sources */, + 133CF6A72DFEBE9000BD13F9 /* SharePlayManager.swift in Sources */, 0457C59E2DE78267000AFBD9 /* BookmarkLink.swift in Sources */, 0457C59F2DE78267000AFBD9 /* BookmarkGridItemView.swift in Sources */, ); From 086ddf951b34f444993a99b35b098fc2e34218fa Mon Sep 17 00:00:00 2001 From: cranci <100066266+cranci1@users.noreply.github.com> Date: Sun, 15 Jun 2025 11:15:32 +0200 Subject: [PATCH 09/15] Update Sora.entitlements --- Sora/Sora.entitlements | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sora/Sora.entitlements b/Sora/Sora.entitlements index 4c847eb..b8c70c4 100644 --- a/Sora/Sora.entitlements +++ b/Sora/Sora.entitlements @@ -4,8 +4,6 @@ com.apple.developer.group-session - com.apple.developer.group-session.video - com.apple.security.app-sandbox com.apple.security.assets.movies.read-write From cc6a24d65c1a14ef28a949a3f0e655f50bc0e50c Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:30:42 +0200 Subject: [PATCH 10/15] fixed fetch metadata when off --- .../MediaInfoView/EpisodeCell/EpisodeCell.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index f8024d1..76bb07c 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -794,6 +794,14 @@ private extension EpisodeCell { private extension EpisodeCell { func fetchAnimeEpisodeDetails() { + if let fetchMeta = UserDefaults.standard.object(forKey: "fetchEpisodeMetadata"), + !(fetchMeta as? Bool ?? true) { + DispatchQueue.main.async { + self.isLoading = false + self.retryAttempts = 0 + } + return + } guard let url = URL(string: "https://api.ani.zip/mappings?anilist_id=\(itemID)") else { isLoading = false Logger.shared.log("Invalid URL for itemID: \(itemID)", type: "Error") @@ -870,6 +878,13 @@ private extension EpisodeCell { } func fetchTMDBEpisodeImage() { + if let fetchMeta = UserDefaults.standard.object(forKey: "fetchEpisodeMetadata"), + !(fetchMeta as? Bool ?? true) { + DispatchQueue.main.async { + self.isLoading = false + } + return + } guard let tmdbID = tmdbID, let season = seasonNumber else { return } let episodeNum = episodeID + 1 From a4899f11966a9186d1c5bfb81224a3541178fb59 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:39:28 +0200 Subject: [PATCH 11/15] test GroupActivity --- .../MediaUtils/NormalPlayer/VideoPlayer.swift | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift index c7e036c..c5f3e27 100644 --- a/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift +++ b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift @@ -33,6 +33,7 @@ class VideoPlayerViewController: UIViewController { private var sharePlayCoordinator: SharePlayCoordinator? private var subscriptions = Set() + private var groupSessionObserver: AnyCancellable? private var aniListUpdateSent = false private var aniListUpdatedSuccessfully = false @@ -53,7 +54,7 @@ class VideoPlayerViewController: UIViewController { } private func setupSubtitles() { - guard !subtitles.isEmpty, UserDefaults.standard.bool(forKey: "subtitlesEnabled"), let subtitleURL = URL(string: subtitles) else { + guard !subtitles.isEmpty, UserDefaults.standard.bool(forKey: "subtitlesEnabled"), let _ = URL(string: subtitles) else { return } @@ -151,6 +152,29 @@ class VideoPlayerViewController: UIViewController { } else { self.player?.play() } + + observeGroupSession() + } + + private func observeGroupSession() { + groupSessionObserver = nil + Task { [weak self] in + guard let self = self else { return } + for await session in VideoWatchingActivity.sessions() { + await self.handleIncomingGroupSession(session) + } + } + } + + @MainActor + private func handleIncomingGroupSession(_ session: GroupSession) async { + if sharePlayCoordinator == nil { + sharePlayCoordinator = SharePlayCoordinator() + } + sharePlayCoordinator?.configureGroupSession() + if let player = self.player { + sharePlayCoordinator?.coordinatePlayback(with: player) + } } override func viewDidAppear(_ animated: Bool) { @@ -386,5 +410,6 @@ class VideoPlayerViewController: UIViewController { sharePlayCoordinator?.leaveGroupSession() sharePlayCoordinator = nil subscriptions.removeAll() + groupSessionObserver = nil } } From 019989df4ff35adee0fd2325db1e35a3243a8500 Mon Sep 17 00:00:00 2001 From: 50/50 <80717571+50n50@users.noreply.github.com> Date: Sun, 15 Jun 2025 20:29:48 +0200 Subject: [PATCH 12/15] Fixes (Check description) (#196) --- .../EpisodeCell/EpisodeCell.swift | 2 +- Sora/Views/MediaInfoView/MediaInfoView.swift | 5 +- .../SettingsSubViews/SettingsViewAbout.swift | 106 +++++ .../SettingsViewDownloads.swift | 2 +- .../SettingsViewGeneral.swift | 31 +- .../SettingsSubViews/SettingsViewPlayer.swift | 4 +- Sora/Views/SettingsView/SettingsView.swift | 8 + Sora/ar.lproj/Localizable.strings | 14 +- Sora/cz.lproj/Localizable.strings | 408 ++++++++++++++++++ Sora/en.lproj/Localizable.strings | 10 +- Sora/es.lproj/Localizable.strings | 407 +++++++++++++++++ Sora/fr.lproj/Localizable.strings | 10 +- Sora/nl.lproj/Localizable.strings | 10 +- Sora/nn.lproj/Localizable.strings | 384 +++++++++++++++++ Sora/ru.lproj/Localizable.strings | 408 ++++++++++++++++++ Sulfur.xcodeproj/project.pbxproj | 90 +++- 16 files changed, 1876 insertions(+), 23 deletions(-) create mode 100644 Sora/cz.lproj/Localizable.strings create mode 100644 Sora/es.lproj/Localizable.strings create mode 100644 Sora/nn.lproj/Localizable.strings create mode 100644 Sora/ru.lproj/Localizable.strings diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index 76bb07c..c074a62 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -359,7 +359,7 @@ private extension EpisodeCell { swipeOffset = newOffset } } else if isShowingActions { - swipeOffset = max(proposedOffset, -maxSwipe) + swipeOffset = min(max(proposedOffset, -maxSwipe), maxSwipe * 0.2) } } else if !hasSignificantHorizontalMovement { dragState = .inactive diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index f2920de..0f0b7ff 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -203,7 +203,10 @@ struct MediaInfoView: View { private var navigationOverlay: some View { VStack { HStack { - Button(action: { dismiss() }) { + Button(action: { + tabBarController.showTabBar() + dismiss() + }) { Image(systemName: "chevron.left") .font(.system(size: 24)) .foregroundColor(.primary) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift index 9a909ae..2982a74 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift @@ -130,6 +130,10 @@ struct SettingsViewAbout: View { SettingsSection(title: "Contributors") { ContributorsView() } + + SettingsSection(title: "Translators") { + TranslatorsView() + } } .padding(.vertical, 20) } @@ -247,6 +251,7 @@ struct ContributorView: View { .foregroundColor( contributor.login == "IBH-RAD" ? Color(hexTwo: "#41127b") : contributor.login == "50n50" ? Color(hexTwo: "#fa4860") : + contributor.login == "CiroHoodLove" ? Color(hexTwo: "#940101") : .accentColor ) @@ -260,6 +265,107 @@ struct ContributorView: View { } } +struct TranslatorsView: View { + struct Translator: Identifiable { + let id: Int + let login: String + let avatarUrl: String + let language: String + } + + private let translators: [Translator] = [ + Translator( + id: 1, + login: "paul", + avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/54b3198dfb900837a9b8a7ec0b791add_webp.png?raw=true", + language: "Dutch" + ), + Translator( + id: 2, + login: "cranci", + avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/28ac8bfaa250788579af747d8fb7f827_webp.png?raw=true", + language: "Italian" + ), + Translator( + id: 3, + login: "ibro", + avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/05cd4f3508f99ba0a4ae2d0985c2f68c_webp.png?raw=true", + language: "Russian - Czech" + ), + Translator( + id: 4, + login: "Ciro", + avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/4accfc2fcfa436165febe4cad18de978_webp.png?raw=true", + language: "Arabic - French" + ), + Translator( + id: 5, + login: "storm", + avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/a6cc97f87d356523820461fd761fc3e1_webp.png?raw=true", + language: "Norwegian - Swedish" + ), + Translator( + id: 6, + login: "VastSector0", + avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/bd8bccb82e0393b767bb705c4dc07113_webp.png?raw=true", + language: "Spanish" + ), + Translator( + id: 7, + login: "Seiike", + avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/ca512dc4ce1f0997fd44503dce0a0fc8_webp.png?raw=true", + language: "Slovak" + ), + Translator( + id: 8, + login: "Cufiy", + avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/y1wwm0ed_png.png?raw=true", + language: "German" + ) + ] + + var body: some View { + ForEach(translators) { translator in + Button(action: { + if let url = URL(string: "https://github.com/\(translator.login)") { + UIApplication.shared.open(url) + } + }) { + HStack { + LazyImage(url: URL(string: translator.avatarUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .frame(width: 40, height: 40) + .clipShape(Circle()) + } else { + ProgressView() + .frame(width: 40, height: 40) + } + } + VStack(alignment: .leading, spacing: 2) { + Text(translator.login) + .font(.headline) + .foregroundColor(.accentColor) + Text(translator.language) + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + Image(systemName: "safari") + .foregroundColor(.accentColor) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + if translator.id != translators.last?.id { + Divider() + .padding(.horizontal, 16) + } + } + } +} + struct Contributor: Identifiable, Decodable { let id: Int let login: String diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewDownloads.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewDownloads.swift index ab9be27..684466b 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewDownloads.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewDownloads.swift @@ -169,7 +169,7 @@ struct SettingsViewDownloads: View { ) { SettingsPickerRow( icon: "4k.tv", - title: String(localized: "Quality"), + title: NSLocalizedString("Maximum Quality Available", comment: "Label for the download quality picker, meaning the highest quality that can be selected."), options: DownloadQualityPreference.allCases.map { $0.rawValue }, optionToString: { $0 }, selection: $downloadQuality diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index ee0ebc9..1109491 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -202,8 +202,29 @@ struct SettingsViewGeneral: View { SettingsPickerRow( icon: "globe", title: NSLocalizedString("App Language", comment: ""), - options: ["English", "Dutch", "French", "Arabic"], - optionToString: { $0 }, + options: [ + "English", + "Arabic", + "Czech", + "Dutch", + "French", + "Norsk", + "Russian", + "Spanish" + ], + optionToString: { lang in + switch lang { + case "English": return "English" + case "Dutch": return "Nederlands" + case "French": return "Français" + case "Arabic": return "العربية" + case "Czech": return "Čeština" + case "Spanish": return "Español" + case "Russian": return "Русский" + case "Norsk": return "Norsk" + default: return lang + } + }, selection: $settings.selectedLanguage, showDivider: false ) @@ -333,9 +354,9 @@ struct SettingsViewGeneral: View { .scrollViewBottomPadding() .alert(isPresented: $showRestartAlert) { Alert( - title: Text(NSLocalizedString("Restart Required", comment: "")), - message: Text(NSLocalizedString("Please restart the app to apply the language change.", comment: "")), - dismissButton: .default(Text("OK")) + title: Text(verbatim: "Restart Required"), + message: Text(verbatim: "Please restart the app to apply the language change."), + dismissButton: .default(Text(verbatim: "OK")) ) } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index e5f182e..501128a 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -265,8 +265,8 @@ struct SettingsViewPlayer: View { ) } SettingsSection( - title: String(localized: "Video Quality Preferences"), - footer: String(localized: "Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player.") + title: NSLocalizedString("Video Quality Preferences", comment: ""), + footer: NSLocalizedString("Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player.", comment: "Footer explaining video quality settings for translators.") ) { SettingsPickerRow( icon: "wifi", diff --git a/Sora/Views/SettingsView/SettingsView.swift b/Sora/Views/SettingsView/SettingsView.swift index 722ebef..9c01e25 100644 --- a/Sora/Views/SettingsView/SettingsView.swift +++ b/Sora/Views/SettingsView/SettingsView.swift @@ -408,6 +408,14 @@ class Settings: ObservableObject { languageCode = "fr" case "Arabic": languageCode = "ar" + case "Czech": + languageCode = "cs" + case "Spanish": + languageCode = "es" + case "Russian": + languageCode = "ru" + case "Norsk": + languageCode = "nn" default: languageCode = "en" } diff --git a/Sora/ar.lproj/Localizable.strings b/Sora/ar.lproj/Localizable.strings index 3235d42..1f3036a 100644 --- a/Sora/ar.lproj/Localizable.strings +++ b/Sora/ar.lproj/Localizable.strings @@ -36,7 +36,7 @@ "Cancel" = "إلغاء"; "Cellular Quality" = "جودة بيانات الجوال"; "Check out some community modules here!" = "اطلع على بعض وحدات المجتمع هنا!"; -"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality." = "اختر دقة الفيديو المفضلة لاتصالات WiFi وبيانات الجوال. الدقة الأعلى تستهلك المزيد من البيانات ولكنها توفر جودة أفضل."; +"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "اختر دقة الفيديو المفضلة لاتصالات WiFi وبيانات الجوال. الدقة الأعلى تستهلك المزيد من البيانات ولكنها توفر جودة أفضل. إذا لم تكن الجودة الدقيقة متاحة، سيتم اختيار الخيار الأقرب تلقائيًا.\n\nملاحظة: ليست كل مصادر الفيديو والمشغلات تدعم اختيار الجودة. تعمل هذه الميزة بشكل أفضل مع تدفقات HLS باستخدام مشغل Sora."; "Clear" = "مسح"; "Clear All Downloads" = "مسح كل التنزيلات"; "Clear Cache" = "مسح ذاكرة التخزين المؤقت"; @@ -113,7 +113,7 @@ "Force Landscape" = "فرض الوضع الأفقي"; "General" = "عام"; "General events and activities." = "الأحداث والأنشطة العامة."; -"General Preferences" = "التفضيلات العامة"; +"General Preferences" = "الإعدادات العامة"; "Hide Splash Screen" = "إخفاء شاشة البداية"; "HLS video downloading." = "تنزيل فيديو HLS."; "Hold Speed" = "سرعة الضغط المطول"; @@ -339,7 +339,7 @@ /* Interface */ "Thumbnails Width" = "عرض الصور المصغرة"; "TMDB Match" = "مطابقة TMDB"; -"Trackers" = "المتتبعات"; +"Trackers" = "منصات المتابعة"; "Trakt" = "Trakt"; "Trakt.tv" = "Trakt.tv"; @@ -381,4 +381,10 @@ "Storage Management" = "إدارة التخزين"; "Storage Used" = "المساحة المستخدمة"; "Library cleared successfully" = "تم مسح المكتبة بنجاح"; -"All downloads deleted successfully" = "تم حذف جميع التنزيلات بنجاح"; \ No newline at end of file +"All downloads deleted successfully" = "تم حذف جميع التنزيلات بنجاح"; + +/* New additions */ +"Recent searches" = "عمليات البحث الأخيرة"; +"me frfr" = "me frfr"; +"Data" = "البيانات"; +"Maximum Quality Available" = "أعلى جودة متاحة"; diff --git a/Sora/cz.lproj/Localizable.strings b/Sora/cz.lproj/Localizable.strings new file mode 100644 index 0000000..12a1591 --- /dev/null +++ b/Sora/cz.lproj/Localizable.strings @@ -0,0 +1,408 @@ +/* General */ +"About" = "O aplikaci"; +"About Sora" = "O Sora"; +"Active" = "Aktivní"; +"Active Downloads" = "Aktivní stahování"; +"Actively downloading media can be tracked from here." = "Aktivně stahovaná média lze sledovat zde."; +"Add Module" = "Přidat modul"; +"Adjust the number of media items per row in portrait and landscape modes." = "Upravte počet položek médií na řádek v režimu na výšku a na šířku."; +"Advanced" = "Pokročilé"; +"AKA Sulfur" = "Známý jako Sulfur"; +"All Bookmarks" = "Všechny záložky"; +"All Watching" = "Vše sledované"; +"Also known as Sulfur" = "Také známý jako Sulfur"; +"AniList" = "AniList"; +"AniList ID" = "AniList ID"; +"AniList Match" = "Shoda AniList"; +"AniList.co" = "AniList.co"; +"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Anonymní data jsou shromažďována za účelem vylepšení aplikace. Žádné osobní údaje nejsou shromažďovány. Toto lze kdykoli vypnout."; +"App Info" = "Informace o aplikaci"; +"App Language" = "Jazyk aplikace"; +"App Storage" = "Úložiště aplikace"; +"Appearance" = "Vzhled"; + +/* Alerts and Actions */ +"Are you sure you want to clear all cached data? This will help free up storage space." = "Opravdu chcete vymazat všechna data z cache? Toto pomůže uvolnit úložný prostor."; +"Are you sure you want to delete '%@'?" = "Opravdu chcete smazat '%@'?"; +"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Opravdu chcete smazat všech %1$d epizod v '%2$@'?"; +"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Opravdu chcete smazat všechny stažené soubory? Můžete zvolit vymazání pouze knihovny při zachování stažených souborů pro budoucí použití."; +"Are you sure you want to erase all app data? This action cannot be undone." = "Opravdu chcete vymazat všechna data aplikace? Tuto akci nelze vrátit zpět."; + +/* Features */ +"Background Enabled" = "Pozadí povoleno"; +"Bookmark items for an easier access later." = "Uložte položky do záložek pro snadnější přístup později."; +"Bookmarks" = "Záložky"; +"Bottom Padding" = "Spodní odsazení"; +"Cancel" = "Zrušit"; +"Cellular Quality" = "Kvalita na mobilních datech"; +"Check out some community modules here!" = "Podívejte se na některé komunitní moduly zde!"; +"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Vyberte preferované rozlišení videa pro WiFi a mobilní připojení. Vyšší rozlišení používají více dat, ale poskytují lepší kvalitu. Pokud není k dispozici přesná kvalita, bude automaticky vybrána nejbližší možnost.\n\nPoznámka: Ne všechny video zdroje a přehrávače podporují výběr kvality. Tato funkce funguje nejlépe s HLS streamy pomocí přehrávače Sora."; +"Clear" = "Vymazat"; +"Clear All Downloads" = "Vymazat všechna stahování"; +"Clear Cache" = "Vymazat cache"; +"Clear Library Only" = "Vymazat pouze knihovnu"; +"Clear Logs" = "Vymazat logy"; +"Click the plus button to add a module!" = "Klikněte na tlačítko plus pro přidání modulu!"; +"Continue Watching" = "Pokračovat ve sledování"; +"Continue Watching Episode %d" = "Pokračovat ve sledování epizody %d"; +"Contributors" = "Přispěvatelé"; +"Copied to Clipboard" = "Zkopírováno do schránky"; +"Copy to Clipboard" = "Kopírovat do schránky"; +"Copy URL" = "Kopírovat URL"; + +/* Episodes */ +"%lld Episodes" = "%lld epizod"; +"%lld of %lld" = "%lld z %lld"; +"%lld-%lld" = "%lld-%lld"; +"%lld%% seen" = "%lld%% shlédnuto"; +"Episode %lld" = "Epizoda %lld"; +"Episodes" = "Epizody"; +"Episodes might not be available yet or there could be an issue with the source." = "Epizody možná ještě nejsou dostupné nebo může být problém se zdrojem."; +"Episodes Range" = "Rozsah epizod"; + +/* System */ +"cranci1" = "cranci1"; +"Dark" = "Tmavý"; +"DATA & LOGS" = "DATA & LOGY"; +"Debug" = "Ladění"; +"Debugging and troubleshooting." = "Ladění a řešení problémů."; + +/* Actions */ +"Delete" = "Smazat"; +"Delete All" = "Smazat vše"; +"Delete All Downloads" = "Smazat všechna stahování"; +"Delete All Episodes" = "Smazat všechny epizody"; +"Delete Download" = "Smazat stahování"; +"Delete Episode" = "Smazat epizodu"; + +/* Player */ +"Double Tap to Seek" = "Dvojitý dotyk pro posun"; +"Double tapping the screen on it's sides will skip with the short tap setting." = "Dvojitý dotyk na strany obrazovky přeskočí s nastavením krátkého dotyku."; + +/* Downloads */ +"Download" = "Stáhnout"; +"Download Episode" = "Stáhnout epizodu"; +"Download Summary" = "Přehled stahování"; +"Download This Episode" = "Stáhnout tuto epizodu"; +"Downloaded" = "Staženo"; +"Downloaded Shows" = "Stažené seriály"; +"Downloading" = "Stahování"; +"Downloads" = "Stahování"; + +/* Settings */ +"Enable Analytics" = "Povolit analytiku"; +"Enable Subtitles" = "Povolit titulky"; + +/* Data Management */ +"Erase" = "Vymazat"; +"Erase all App Data" = "Vymazat všechna data aplikace"; +"Erase App Data" = "Vymazat data aplikace"; + +/* Errors */ +"Error" = "Chyba"; +"Error Fetching Results" = "Chyba při načítání výsledků"; +"Errors and critical issues." = "Chyby a kritické problémy."; +"Failed to load contributors" = "Nepodařilo se načíst přispěvatele"; + +/* Features */ +"Fetch Episode metadata" = "Načíst metadata epizody"; +"Files Downloaded" = "Stažené soubory"; +"Font Size" = "Velikost písma"; + +/* Interface */ +"Force Landscape" = "Vynutit na šířku"; +"General" = "Obecné"; +"General events and activities." = "Obecné události a aktivity."; +"General Preferences" = "Obecné předvolby"; +"Hide Splash Screen" = "Skrýt úvodní obrazovku (Splash Screen)"; +"HLS video downloading." = "Stahování HLS videa."; +"Hold Speed" = "Rychlost při podržení"; + +/* Info */ +"Info" = "Informace"; +"INFOS" = "INFORMACE"; +"Installed Modules" = "Nainstalované moduly"; +"Interface" = "Rozhraní"; + +/* Social */ +"Join the Discord" = "Připojit se na Discord"; + +/* Layout */ +"Landscape Columns" = "Sloupce na šířku"; +"Language" = "Jazyk"; +"LESS" = "MÉNĚ"; + +/* Library */ +"Library" = "Knihovna"; +"License (GPLv3.0)" = "Licence (GPLv3.0)"; +"Light" = "Světlý"; + +/* Loading States */ +"Loading Episode %lld..." = "Načítá se epizoda %lld..."; +"Loading logs..." = "Načítají se logy..."; +"Loading module information..." = "Načítají se informace o modulu..."; +"Loading Stream" = "Načítá se stream"; + +/* Logging */ +"Log Debug Info" = "Logovat ladicí informace"; +"Log Filters" = "Logovat filtry"; +"Log In with AniList" = "Přihlásit se pomocí AniList"; +"Log In with Trakt" = "Přihlásit se pomocí Trakt"; +"Log Out from AniList" = "Odhlásit se z AniList"; +"Log Out from Trakt" = "Odhlásit se z Trakt"; +"Log Types" = "Logovat typy"; +"Logged in as" = "Přihlášen jako"; +"Logged in as " = "Přihlášen jako "; + +/* Logs and Settings */ +"Logs" = "Logy"; +"Long press Skip" = "Dlouhý stisk pro přeskočení"; +"MAIN" = "HLAVNÍ"; +"Main Developer" = "Hlavní vývojář"; +"MAIN SETTINGS" = "HLAVNÍ NASTAVENÍ"; + +/* Media Actions */ +"Mark All Previous Watched" = "Označit všechny předchozí jako shlédnuté"; +"Mark as Watched" = "Označit jako shlédnuté"; +"Mark Episode as Watched" = "Označit epizodu jako shlédnutou"; +"Mark Previous Episodes as Watched" = "Označit předchozí epizody jako shlédnuté"; +"Mark watched" = "Označit jako shlédnuté"; +"Match with AniList" = "Párovat s AniList"; +"Match with TMDB" = "Párovat s TMDB"; +"Matched ID: %lld" = "Spárované ID: %lld"; +"Matched with: %@" = "Spárováno s: %@"; +"Max Concurrent Downloads" = "Max současných stahování"; + +/* Media Interface */ +"Media Grid Layout" = "Rozložení mřížky médií"; +"Media Player" = "Přehrávač médií"; +"Media View" = "Zobrazení médií"; +"Metadata Provider" = "Poskytovatel metadat"; +"Metadata Providers Order" = "Pořadí poskytovatelů metadat"; +"Module Removed" = "Modul odstraněn"; +"Modules" = "Moduly"; + +/* Headers */ +"MODULES" = "MODULY"; +"MORE" = "VÍCE"; + +/* Status Messages */ +"No Active Downloads" = "Žádná aktivní stahování"; +"No AniList matches found" = "Nenalezeny žádné shody z AniListu"; +"No Data Available" = "Žádná data k dispozici"; +"No Downloads" = "Žádná stahování"; +"No episodes available" = "Žádné epizody k dispozici"; +"No Episodes Available" = "Žádné epizody k dispozici"; +"No items to continue watching." = "Žádné položky k pokračování ve sledování."; +"No matches found" = "Nenalezeny žádné shody"; +"No Module Selected" = "Žádný modul nevybrán"; +"No Modules" = "Žádné moduly"; +"No Results Found" = "Nenalezeny žádné výsledky"; +"No Search Results Found" = "Nenalezeny žádné výsledky vyhledávání"; +"Nothing to Continue Watching" = "Nic k pokračování ve sledování"; + +/* Notes and Messages */ +"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Poznámka: moduly budou nahrazeny pouze pokud je v JSON souboru jiný řetězec verze."; + +/* Actions */ +"OK" = "OK"; +"Open Community Library" = "Otevřít komunitní knihovnu"; + +/* External Services */ +"Open in AniList" = "Otevřít v AniList"; +"Original Poster" = "Původní plakát"; + +/* Playback */ +"Paused" = "Pozastaveno"; +"Play" = "Přehrát"; +"Player" = "Přehrávač"; + +/* System Messages */ +"Please restart the app to apply the language change." = "Prosím restartujte aplikaci pro použití změny jazyka."; +"Please select a module from settings" = "Prosím vyberte modul v nastavení"; + +/* Interface */ +"Portrait Columns" = "Sloupce na výšku"; +"Progress bar Marker Color" = "Barva značky v progres baru"; +"Provider: %@" = "Poskytovatel: %@"; + +/* Queue */ +"Queue" = "Fronta"; +"Queued" = "Ve frontě"; + +/* Content */ +"Recently watched content will appear here." = "Nedávno sledovaný obsah se zobrazí zde."; + +/* Settings */ +"Refresh Modules on Launch" = "Obnovit moduly při spuštění aplikace"; +"Refresh Storage Info" = "Obnovit informace o úložišti"; +"Remember Playback speed" = "Zapamatovat rychlost přehrávání"; + +/* Actions */ +"Remove" = "Odebrat"; +"Remove All Cache" = "Odebrat veškerou cache paměť"; + +/* File Management */ +"Remove All Documents" = "Odebrat všechny dokumenty"; +"Remove Documents" = "Odebrat dokumenty"; +"Remove Downloaded Media" = "Odebrat stažená média"; +"Remove Downloads" = "Odebrat stahování"; +"Remove from Bookmarks" = "Odebrat ze záložek"; +"Remove Item" = "Odebrat položku"; + +/* Support */ +"Report an Issue" = "Hlásit problém"; + +/* Reset Options */ +"Reset" = "Resetovat"; +"Reset AniList ID" = "Resetovat AniList ID"; +"Reset Episode Progress" = "Resetovat progres epizody"; +"Reset progress" = "Resetovat progres"; +"Reset Progress" = "Resetovat progres"; + +/* System */ +"Restart Required" = "Vyžadován restart"; +"Running Sora %@ - cranci1" = "Spuštěna Sora %@ - cranci1"; + +/* Actions */ +"Save" = "Uložit"; +"Search" = "Hledat"; + +/* Search */ +"Search downloads" = "Hledat ve stahováních"; +"Search for something..." = "Hledat něco..."; +"Search..." = "Hledat..."; + +/* Content */ +"Season %d" = "Sezóna %d"; +"Season %lld" = "Sezóna %lld"; +"Segments Color" = "Barva segmentů"; + +/* Modules */ +"Select Module" = "Vybrat modul"; +"Set Custom AniList ID" = "Nastavit vlastní AniList ID"; + +/* Interface */ +"Settings" = "Nastavení"; +"Shadow" = "Stín"; +"Show More (%lld more characters)" = "Zobrazit více (%lld dalších znaků)"; +"Show PiP Button" = "Zobrazit PiP tlačítko"; +"Show Skip 85s Button" = "Zobrazit tlačítko přeskočení 85s"; +"Show Skip Intro / Outro Buttons" = "Zobrazit tlačítka přeskočení intro/outro"; +"Shows" = "Seriály"; +"Size (%@)" = "Velikost (%@)"; +"Skip Settings" = "Nastavení přeskakování"; + +/* Player Features */ +"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Některé funkce jsou omezeny na přehrávač Sora a výchozí, jako je vynucení orientace na šířku (ForceLandscape), rychlost při podržení (holdSpeed) a vlastní časové skoky."; + +/* App Info */ +"Sora" = "Sora"; +"Sora %@ by cranci1" = "Sora %@ od cranci1"; +"Sora and cranci1 are not affiliated with AniList or Trakt in any way. + +Also note that progress updates may not be 100% accurate." = "Sora a cranci1 nejsou nijak spojeni s AniList nebo Trakt. + +Také si všimněte, že aktualizace progresu nemusí být 100% přesné."; +"Sora GitHub Repository" = "Sora GitHub repozitář"; +"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur vždy zůstane zdarma bez reklam!"; + +/* Interface */ +"Sort" = "Seřadit"; +"Speed Settings" = "Nastavení rychlosti"; + +/* Playback */ +"Start Watching" = "Začít sledovat"; +"Start Watching Episode %d" = "Začít sledovat epizodu %d"; +"Storage Used" = "Využité úložiště"; +"Stream" = "Stream"; +"Streaming and video playback." = "Streamování a přehrávání videa."; + +/* Subtitles */ +"Subtitle Color" = "Barva titulků"; +"Subtitle Settings" = "Nastavení titulků"; + +/* Sync */ +"Sync anime progress" = "Synchronizovat progres anime"; +"Sync TV shows progress" = "Synchronizovat progres TV seriálů"; + +/* System */ +"System" = "Systém"; + +/* Instructions */ +"Tap a title to override the current match." = "Dotkněte se názvu pro přepsání aktuální shody."; +"Tap Skip" = "Klepnutím přeskočit"; +"Tap to manage your modules" = "Dotkněte se pro správu vašich modulů"; +"Tap to select a module" = "Dotkněte se pro výběr modulu"; + +/* App Information */ +"The app cache helps the app load images faster. + +Clearing the Documents folder will delete all downloaded modules. + +Do not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "Cache aplikace pomáhá aplikaci načítat obrázky rychleji. + +Vymazání složky Dokumenty smaže všechny stažené moduly. + +Nemažte Data aplikace, pokud nerozumíte důsledkům — může to způsobit selhání aplikace."; +"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily. + +For episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Rozsah epizod určuje, kolik epizod se zobrazí na každé stránce. Epizody jsou seskupeny do bloků (jako 1–25, 26–50, atd.), což vám umožňuje procházet je snadněji. + +Metadata epizody se týkají náhledu a názvu epizody, které mohou někdy obsahovat spoilery."; +"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Modul poskytl pouze jednu epizodu, toto je pravděpodobně film, takže jsme se rozhodli vytvořit samostatné obrazovky pro tyto případy."; + +/* Interface */ +"Thumbnails Width" = "Šířka náhledů"; +"TMDB Match" = "TMDB shoda"; +"Trackers" = "Trackery"; +"Trakt" = "Trakt"; +"Trakt.tv" = "Trakt.tv"; + +/* Search */ +"Try different keywords" = "Zkuste jiná klíčová slova"; +"Try different search terms" = "Zkuste jiné vyhledávací termíny"; + +/* Player Controls */ +"Two Finger Hold for Pause" = "Držení dvěma prsty pro pozastavení"; +"Unable to fetch matches. Please try again later." = "Nelze načíst shody. Prosím zkuste to později."; +"Use TMDB Poster Image" = "Použít plakát z TMDB"; + +/* Version */ +"v%@" = "v%@"; +"Video Player" = "Přehrávač videa"; + +/* Video Settings */ +"Video Quality Preferences" = "Předvolby kvality videa"; +"View All" = "Zobrazit vše"; +"Watched" = "Shlédnuto"; +"Why am I not seeing any episodes?" = "Proč nevidím žádné epizody?"; +"WiFi Quality" = "WiFi kvalita"; + +/* User Status */ +"You are not logged in" = "Nejste přihlášeni"; +"You have no items saved." = "Nemáte uložené žádné položky."; +"Your downloaded episodes will appear here" = "Vaše stažené epizody se zobrazí zde"; +"Your recently watched content will appear here" = "Váš nedávno sledovaný obsah se zobrazí zde"; + +/* Download Settings */ +"Download Settings" = "Nastavení stahování"; +"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Max současných stahování určuje, kolik epizod se může stahovat současně. Vyšší hodnoty mohou používat více šířky pásma a zdrojů zařízení."; +"Quality" = "Kvalita"; +"Max Concurrent Downloads" = "Max současných stahování"; +"Allow Cellular Downloads" = "Povolit stahování přes mobilní síť"; +"Quality Information" = "Informace o kvalitě"; + +/* Storage */ +"Storage Management" = "Správa úložiště"; +"Storage Used" = "Využité úložiště"; +"Library cleared successfully" = "Knihovna úspěšně vymazána"; +"All downloads deleted successfully" = "Všechna stahování úspěšně smazána"; + +/* Recent searches */ +"Recent searches" = "Nedávná hledání"; +"me frfr" = "já frfr"; +"Data" = "Data"; + +/* New string */ +"Maximum Quality Available" = "Maximální dostupná kvalita"; \ No newline at end of file diff --git a/Sora/en.lproj/Localizable.strings b/Sora/en.lproj/Localizable.strings index 19cd7cf..2e83aae 100644 --- a/Sora/en.lproj/Localizable.strings +++ b/Sora/en.lproj/Localizable.strings @@ -36,7 +36,7 @@ "Cancel" = "Cancel"; "Cellular Quality" = "Cellular Quality"; "Check out some community modules here!" = "Check out some community modules here!"; -"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality." = "Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality."; +"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player."; "Clear" = "Clear"; "Clear All Downloads" = "Clear All Downloads"; "Clear Cache" = "Clear Cache"; @@ -376,9 +376,15 @@ "Max Concurrent Downloads" = "Max Concurrent Downloads"; "Allow Cellular Downloads" = "Allow Cellular Downloads"; "Quality Information" = "Quality Information"; +"Maximum Quality Available" = "Maximum Quality Available"; /* Storage */ "Storage Management" = "Storage Management"; "Storage Used" = "Storage Used"; "Library cleared successfully" = "Library cleared successfully"; -"All downloads deleted successfully" = "All downloads deleted successfully"; \ No newline at end of file +"All downloads deleted successfully" = "All downloads deleted successfully"; + +/* New additions */ +"Recent searches" = "Recent searches"; +"me frfr" = "me frfr"; +"Data" = "Data"; diff --git a/Sora/es.lproj/Localizable.strings b/Sora/es.lproj/Localizable.strings new file mode 100644 index 0000000..d4f1f09 --- /dev/null +++ b/Sora/es.lproj/Localizable.strings @@ -0,0 +1,407 @@ +/* General */ +"About" = "Acerca de"; +"About Sora" = "Acerca de Sora"; +"Active" = "Activo"; +"Active Downloads" = "Descargas activas"; +"Actively downloading media can be tracked from here." = "El contenido multimedia que se está descargando activamente se puede seguir desde aquí."; +"Add Module" = "Añadir módulo"; +"Adjust the number of media items per row in portrait and landscape modes." = "Ajusta el número de elementos multimedia por fila en modo vertical y horizontal."; +"Advanced" = "Avanzado"; +"AKA Sulfur" = "También conocido como Sulfur"; +"All Bookmarks" = "Todos los marcadores"; +"All Watching" = "Todo lo que estás viendo"; +"Also known as Sulfur" = "También conocido como Sulfur"; +"AniList" = "AniList"; +"AniList ID" = "ID de AniList"; +"AniList Match" = "Coincidencia de AniList"; +"AniList.co" = "AniList.co"; +"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Se recopilan datos anónimos para mejorar la aplicación. No se recopila información personal. Esto puede deshabilitarse en cualquier momento."; +"App Info" = "Información de la aplicación"; +"App Language" = "Idioma de la aplicación"; +"App Storage" = "Almacenamiento de la aplicación"; +"Appearance" = "Apariencia"; + +/* Alerts and Actions */ +"Are you sure you want to clear all cached data? This will help free up storage space." = "¿Estás seguro/a de que quieres borrar todos los datos en caché? Esto ayudará a liberar espacio de almacenamiento."; +"Are you sure you want to delete '%@'?" = "¿Estás seguro/a de que quieres eliminar '%@'?"; +"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "¿Estás seguro/a de que quieres eliminar todos los %1$d episodios en '%2$@'?"; +"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "¿Estás seguro/a de que quieres eliminar todos los archivos descargados? Puedes optar por borrar solo la biblioteca y conservar los archivos descargados para uso futuro."; +"Are you sure you want to erase all app data? This action cannot be undone." = "¿Estás seguro/a de que quieres borrar todos los datos de la aplicación? Esta acción no se puede deshacer."; + +/* Features */ +"Background Enabled" = "Fondo habilitado"; +"Bookmark items for an easier access later." = "Guarda los elementos en marcadores para facilitar el acceso más adelante."; +"Bookmarks" = "Marcadores"; +"Bottom Padding" = "Espacio inferior"; +"Cancel" = "Cancelar"; +"Cellular Quality" = "Calidad con datos móviles"; +"Check out some community modules here!" = "¡Echa un vistazo a algunos módulos de la comunidad aquí!"; +"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Elige la resolución de video preferida para conexiones WiFi y de datos móviles. Las resoluciones más altas usan más datos pero brindan mejor calidad. Si la calidad exacta no está disponible, se seleccionará automáticamente la opción más cercana.\n\nNota: No todas las fuentes de video y reproductores admiten la selección de calidad. Esta función funciona mejor con transmisiones HLS usando el reproductor Sora."; +"Clear" = "Borrar"; +"Clear All Downloads" = "Borrar todas las descargas"; +"Clear Cache" = "Borrar caché"; +"Clear Library Only" = "Borrar solo la biblioteca"; +"Clear Logs" = "Borrar registros"; +"Click the plus button to add a module!" = "¡Haz clic en el botón de más para añadir un módulo!"; +"Continue Watching" = "Continuar viendo"; +"Continue Watching Episode %d" = "Continuar viendo el episodio %d"; +"Contributors" = "Colaboradores"; +"Copied to Clipboard" = "Copiado al portapapeles"; +"Copy to Clipboard" = "Copiar al portapapeles"; +"Copy URL" = "Copiar enlace"; + +/* Episodes */ +"%lld Episodes" = "%lld Episodios"; +"%lld of %lld" = "%lld de %lld"; +"%lld-%lld" = "%lld-%lld"; +"%lld%% seen" = "%lld%% visto(s)"; +"Episode %lld" = "Episodio %lld"; +"Episodes" = "Episodios"; +"Episodes might not be available yet or there could be an issue with the source." = "Es posible que los episodios aún no estén disponibles o que haya un problema con la fuente."; +"Episodes Range" = "Rango de episodios"; + +/* System */ +"cranci1" = "cranci1"; +"Dark" = "Oscuro"; +"DATA & LOGS" = "DATOS Y REGISTROS"; +"Debug" = "Depurar"; +"Debugging and troubleshooting." = "Depuración y resolución de problemas."; + +/* Actions */ +"Delete" = "Eliminar"; +"Delete All" = "Eliminar todo"; +"Delete All Downloads" = "Eliminar todas las descargas"; +"Delete All Episodes" = "Eliminar todos los episodios"; +"Delete Download" = "Eliminar descarga"; +"Delete Episode" = "Eliminar episodio"; + +/* Player */ +"Double Tap to Seek" = "Toca dos veces para buscar"; +"Double tapping the screen on it's sides will skip with the short tap setting." = "Al tocar dos veces la pantalla en sus lados, se saltará con la configuración de toque corto."; + +/* Downloads */ +"Download" = "Descargar"; +"Download Episode" = "Descargar episodio"; +"Download Summary" = "Resumen de descarga"; +"Download This Episode" = "Descargar este episodio"; +"Downloaded" = "Descargado"; +"Downloaded Shows" = "Programas descargados"; +"Downloading" = "Descargando"; +"Downloads" = "Descargas"; + +/* Settings */ +"Enable Analytics" = "Habilitar análisis"; +"Enable Subtitles" = "Habilitar subtítulos"; + +/* Data Management */ +"Erase" = "Borrar"; +"Erase all App Data" = "Borrar todos los datos de la aplicación"; +"Erase App Data" = "Borrar datos de la aplicación"; + +/* Errors */ +"Error" = "Error"; +"Error Fetching Results" = "Error al obtener resultados"; +"Errors and critical issues." = "Errores y problemas críticos."; +"Failed to load contributors" = "No se pudieron cargar los colaboradores"; + +/* Features */ +"Fetch Episode metadata" = "Obtener metadatos del episodio"; +"Files Downloaded" = "Archivos descargados"; +"Font Size" = "Tamaño de fuente"; + +/* Interface */ +"Force Landscape" = "Forzar horizontal"; +"General" = "General"; +"General events and activities." = "Eventos y actividades generales."; +"General Preferences" = "Preferencias generales"; +"Hide Splash Screen" = "Ocultar pantalla de inicio"; +"HLS video downloading." = "Descarga de video HLS."; +"Hold Speed" = "Velocidad de retención"; + +/* Info */ +"Info" = "Información"; +"INFOS" = "INFORMACIÓN"; +"Installed Modules" = "Módulos instalados"; +"Interface" = "Interfaz"; + +/* Social */ +"Join the Discord" = "Unirse a Discord"; + +/* Layout */ +"Landscape Columns" = "Columnas horizontales"; +"Language" = "Idioma"; +"LESS" = "MENOS"; + +/* Library */ +"Library" = "Biblioteca"; +"License (GPLv3.0)" = "Licencia (GPLv3.0)"; +"Light" = "Claro"; + +/* Loading States */ +"Loading Episode %lld..." = "Cargando episodio %lld..."; +"Loading logs..." = "Cargando registros..."; +"Loading module information..." = "Cargando información del módulo..."; +"Loading Stream" = "Cargando transmisión"; + +/* Logging */ +"Log Debug Info" = "Registrar información de depuración"; +"Log Filters" = "Filtros de registro"; +"Log In with AniList" = "Iniciar sesión con AniList"; +"Log In with Trakt" = "Iniciar sesión con Trakt"; +"Log Out from AniList" = "Cerrar sesión de AniList"; +"Log Out from Trakt" = "Cerrar sesión de Trakt"; +"Log Types" = "Tipos de registro"; +"Logged in as" = "Sesión iniciada como"; +"Logged in as " = "Sesión iniciada como "; + +/* Logs and Settings */ +"Logs" = "Registros"; +"Long press Skip" = "Mantener presionado para saltar"; +"MAIN" = "PRINCIPAL"; +"Main Developer" = "Desarrollador principal"; +"MAIN SETTINGS" = "AJUSTES PRINCIPALES"; + +/* Media Actions */ +"Mark All Previous Watched" = "Marcar todos los anteriores como vistos"; +"Mark as Watched" = "Marcar como visto"; +"Mark Episode as Watched" = "Marcar episodio como visto"; +"Mark Previous Episodes as Watched" = "Marcar episodios anteriores como vistos"; +"Mark watched" = "Marcar como visto"; +"Match with AniList" = "Coincidir con AniList"; +"Match with TMDB" = "Coincidir con TMDB"; +"Matched ID: %lld" = "ID coincidente: %lld"; +"Matched with: %@" = "Coincidido con: %@"; +"Max Concurrent Downloads" = "Máx. descargas simultáneas"; + +/* Media Interface */ +"Media Grid Layout" = "Diseño de cuadrícula multimedia"; +"Media Player" = "Reproductor multimedia"; +"Media View" = "Vista multimedia"; +"Metadata Provider" = "Proveedor de metadatos"; +"Metadata Providers Order" = "Orden de proveedores de metadatos"; +"Module Removed" = "Módulo eliminado"; +"Modules" = "Módulos"; + +/* Headers */ +"MODULES" = "MÓDULOS"; +"MORE" = "MÁS"; + +/* Status Messages */ +"No Active Downloads" = "No hay descargas activas"; +"No AniList matches found" = "No se encontraron coincidencias en AniList"; +"No Data Available" = "No hay datos disponibles"; +"No Downloads" = "No hay descargas"; +"No episodes available" = "No hay episodios disponibles"; +"No Episodes Available" = "No hay episodios disponibles"; +"No items to continue watching." = "No hay elementos para seguir viendo."; +"No matches found" = "No se encontraron coincidencias"; +"No Module Selected" = "Ningún módulo seleccionado"; +"No Modules" = "No hay módulos"; +"No Results Found" = "No se encontraron resultados"; +"No Search Results Found" = "No se encontraron resultados de búsqueda"; +"Nothing to Continue Watching" = "Nada para continuar viendo"; + +/* Notes and Messages */ +"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Ten en cuenta que los módulos se reemplazarán solo si hay una cadena de versión diferente dentro del archivo JSON."; + +/* Actions */ +"OK" = "OK"; +"Open Community Library" = "Abrir biblioteca de la comunidad"; + +/* External Services */ +"Open in AniList" = "Abrir en AniList"; +"Original Poster" = "Póster original"; + +/* Playback */ +"Paused" = "Pausado"; +"Play" = "Reproducir"; +"Player" = "Reproductor"; + +/* System Messages */ +"Please restart the app to apply the language change." = "Por favor, reinicia la aplicación para aplicar el cambio de idioma."; +"Please select a module from settings" = "Por favor, selecciona un módulo desde la configuración"; + +/* Interface */ +"Portrait Columns" = "Columnas verticales"; +"Progress bar Marker Color" = "Color del marcador de la barra de progreso"; +"Provider: %@" = "Proveedor: %@"; + +/* Queue */ +"Queue" = "Cola"; +"Queued" = "En cola"; + +/* Content */ +"Recently watched content will appear here." = "El contenido visto recientemente aparecerá aquí."; + +/* Settings */ +"Refresh Modules on Launch" = "Actualizar módulos al iniciar"; +"Refresh Storage Info" = "Actualizar información de almacenamiento"; +"Remember Playback speed" = "Recordar velocidad de reproducción"; + +/* Actions */ +"Remove" = "Eliminar"; +"Remove All Cache" = "Eliminar toda la caché"; + +/* File Management */ +"Remove All Documents" = "Eliminar todos los documentos"; +"Remove Documents" = "Eliminar documentos"; +"Remove Downloaded Media" = "Eliminar contenido multimedia descargado"; +"Remove Downloads" = "Eliminar descargas"; +"Remove from Bookmarks" = "Eliminar de marcadores"; +"Remove Item" = "Eliminar elemento"; + +/* Support */ +"Report an Issue" = "Reportar un problema"; + +/* Reset Options */ +"Reset" = "Restablecer"; +"Reset AniList ID" = "Restablecer ID de AniList"; +"Reset Episode Progress" = "Restablecer progreso del episodio"; +"Reset progress" = "Restablecer progreso"; +"Reset Progress" = "Restablecer progreso"; + +/* System */ +"Restart Required" = "Reinicio requerido"; +"Running Sora %@ - cranci1" = "Ejecutando Sora %@ - cranci1"; + +/* Actions */ +"Save" = "Guardar"; +"Search" = "Buscar"; + +/* Search */ +"Search downloads" = "Buscar descargas"; +"Search for something..." = "Buscar algo..."; +"Search..." = "Buscar..."; + +/* Content */ +"Season %d" = "Temporada %d"; +"Season %lld" = "Temporada %lld"; +"Segments Color" = "Color de segmentos"; + +/* Modules */ +"Select Module" = "Seleccionar módulo"; +"Set Custom AniList ID" = "Establecer ID de AniList personalizado"; + +/* Interface */ +"Settings" = "Ajustes"; +"Shadow" = "Sombra"; +"Show More (%lld more characters)" = "Mostrar más (%lld caracteres más)"; +"Show PiP Button" = "Mostrar botón PiP"; +"Show Skip 85s Button" = "Mostrar botón Saltar 85s"; +"Show Skip Intro / Outro Buttons" = "Mostrar botones Saltar introducción/final"; +"Shows" = "Programas"; +"Size (%@)" = "Tamaño (%@)"; +"Skip Settings" = "Configuración de saltar"; + +/* Player Features */ +"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Algunas funciones están limitadas al reproductor Sora y Default, como Forzar horizontal, velocidad de retención e incrementos de tiempo de salto personalizados."; + +/* App Info */ +"Sora" = "Sora"; +"Sora %@ by cranci1" = "Sora %@ por cranci1"; +"Sora and cranci1 are not affiliated with AniList or Trakt in any way. + +Also note that progress updates may not be 100% accurate." = "Sora y cranci1 no están afiliados a AniList ni a Trakt de ninguna manera. + +Ten en cuenta también que las actualizaciones de progreso pueden no ser 100% precisas."; +"Sora GitHub Repository" = "Repositorio de Sora en GitHub"; +"Sora/Sulfur will always remain free with no ADs!" = "¡Sora/Sulfur siempre será gratis y sin anuncios!"; + +/* Interface */ +"Sort" = "Ordenar"; +"Speed Settings" = "Configuración de velocidad"; + +/* Playback */ +"Start Watching" = "Empezar a ver"; +"Start Watching Episode %d" = "Empezar a ver episodio %d"; +"Storage Used" = "Almacenamiento usado"; +"Stream" = "Transmisión"; +"Streaming and video playback." = "Transmisión y reproducción de video."; + +/* Subtitles */ +"Subtitle Color" = "Color de subtítulos"; +"Subtitle Settings" = "Configuración de subtítulos"; + +/* Sync */ +"Sync anime progress" = "Sincronizar progreso de anime"; +"Sync TV shows progress" = "Sincronizar progreso de programas de TV"; + +/* System */ +"System" = "Sistema"; + +/* Instructions */ +"Tap a title to override the current match." = "Toca un título para anular la coincidencia actual."; +"Tap Skip" = "Tocar para saltar"; +"Tap to manage your modules" = "Toca para administrar tus módulos"; +"Tap to select a module" = "Toca para seleccionar un módulo"; + +/* App Information */ +"The app cache helps the app load images faster. + +Clearing the Documents folder will delete all downloaded modules. + +Do not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "La caché de la aplicación ayuda a que la aplicación cargue las imágenes más rápido. + +Al borrar la carpeta Documentos, se eliminarán todos los módulos descargados. + +No borres los datos de la aplicación a menos que comprendas las consecuencias; podría hacer que la aplicación funcione mal."; +"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily. + +For episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "El rango de episodios controla cuántos episodios aparecen en cada página. Los episodios se agrupan en conjuntos (como 1-25, 26-50, etc.), lo que te permite navegar por ellos más fácilmente. + +Para los metadatos del episodio, se refiere a la miniatura y el título del episodio, ya que a veces puede contener spoilers."; +"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "El módulo proporcionó solo un episodio, esto es muy probable que sea una película, por lo que decidimos hacer pantallas separadas para estos casos."; + +/* Interface */ +"Thumbnails Width" = "Ancho de miniaturas"; +"TMDB Match" = "Coincidencia de TMDB"; +"Trackers" = "Rastreadores"; +"Trakt" = "Trakt"; +"Trakt.tv" = "Trakt.tv"; + +/* Search */ +"Try different keywords" = "Prueba con diferentes palabras clave"; +"Try different search terms" = "Prueba con diferentes términos de búsqueda"; + +/* Player Controls */ +"Two Finger Hold for Pause" = "Mantén presionado con dos dedos para pausar"; +"Unable to fetch matches. Please try again later." = "No se pudieron obtener las coincidencias. Por favor, inténtalo de nuevo más tarde."; +"Use TMDB Poster Image" = "Usar imagen de póster de TMDB"; + +/* Version */ +"v%@" = "v%@"; +"Video Player" = "Reproductor de video"; + +/* Video Settings */ +"Video Quality Preferences" = "Preferencias de calidad de video"; +"View All" = "Ver todo"; +"Watched" = "Visto"; +"Why am I not seeing any episodes?" = "¿Por qué no veo ningún episodio?"; +"WiFi Quality" = "Calidad de WiFi"; + +/* User Status */ +"You are not logged in" = "No has iniciado sesión"; +"You have no items saved." = "No tienes elementos guardados."; +"Your downloaded episodes will appear here" = "Tus episodios descargados aparecerán aquí"; +"Your recently watched content will appear here" = "Tu contenido visto recientemente aparecerá aquí"; + +/* Download Settings */ +"Download Settings" = "Configuración de descarga"; +"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "El número máximo de descargas simultáneas controla cuántos episodios se pueden descargar al mismo tiempo. Valores más altos pueden usar más ancho de banda y recursos del dispositivo."; +"Quality" = "Calidad"; +"Max Concurrent Downloads" = "Máx. descargas simultáneas"; +"Allow Cellular Downloads" = "Permitir descargas con datos móviles"; +"Quality Information" = "Información de calidad"; + +/* Storage */ +"Storage Management" = "Gestión de almacenamiento"; +"Storage Used" = "Almacenamiento usado"; +"Library cleared successfully" = "Biblioteca borrada con éxito"; +"All downloads deleted successfully" = "Todas las descargas eliminadas con éxito"; + +/* New localizations */ +"Recent searches" = "Búsquedas recientes"; +"me frfr" = "yo frfr"; +"Data" = "Datos"; +"Maximum Quality Available" = "Calidad máxima disponible"; + diff --git a/Sora/fr.lproj/Localizable.strings b/Sora/fr.lproj/Localizable.strings index 2dae36f..6fe1728 100644 --- a/Sora/fr.lproj/Localizable.strings +++ b/Sora/fr.lproj/Localizable.strings @@ -36,7 +36,7 @@ "Cancel" = "Annuler"; "Cellular Quality" = "Qualité cellulaire"; "Check out some community modules here!" = "Découvrez quelques modules communautaires ici !"; -"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality." = "Choisissez la résolution vidéo préférée pour les connexions WiFi et cellulaires. Les résolutions plus élevées utilisent plus de données mais offrent une meilleure qualité."; +"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Choisissez la résolution vidéo préférée pour les connexions WiFi et cellulaires. Les résolutions plus élevées utilisent plus de données mais offrent une meilleure qualité. Si la qualité exacte n'est pas disponible, l'option la plus proche sera sélectionnée automatiquement.\n\nRemarque : toutes les sources vidéo et tous les lecteurs ne prennent pas en charge la sélection de la qualité. Cette fonctionnalité fonctionne mieux avec les flux HLS utilisant le lecteur Sora."; "Clear" = "Vider"; "Clear All Downloads" = "Vider tous les téléchargements"; "Clear Cache" = "Vider le cache"; @@ -381,4 +381,10 @@ "Storage Management" = "Gestion du stockage"; "Storage Used" = "Stockage utilisé"; "Library cleared successfully" = "Bibliothèque vidée avec succès"; -"All downloads deleted successfully" = "Tous les téléchargements ont été supprimés avec succès"; \ No newline at end of file +"All downloads deleted successfully" = "Tous les téléchargements ont été supprimés avec succès"; + +/* New additions */ +"Recent searches" = "Recherches récentes"; +"me frfr" = "moi frfr"; +"Data" = "Données"; +"Maximum Quality Available" = "Qualité maximale disponible"; diff --git a/Sora/nl.lproj/Localizable.strings b/Sora/nl.lproj/Localizable.strings index b14a0d4..8b61971 100644 --- a/Sora/nl.lproj/Localizable.strings +++ b/Sora/nl.lproj/Localizable.strings @@ -36,7 +36,7 @@ "Cancel" = "Annuleren"; "Cellular Quality" = "Mobiele Kwaliteit"; "Check out some community modules here!" = "Bekijk hier enkele community modules!"; -"Choose preferred video resolution for WiFi and cellular connections." = "Kies de gewenste videoresolutie voor WiFi en mobiele verbindingen."; +"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Kies de gewenste videoresolutie voor WiFi- en mobiele verbindingen. Hogere resoluties gebruiken meer data maar bieden een betere kwaliteit. Als de exacte kwaliteit niet beschikbaar is, wordt automatisch de dichtstbijzijnde optie geselecteerd.\n\nLet op: Niet alle videobronnen en spelers ondersteunen kwaliteitsselectie. Deze functie werkt het beste met HLS-streams via de Sora-speler."; "Clear" = "Wissen"; "Clear All Downloads" = "Alle Downloads Wissen"; "Clear Cache" = "Wis Cache"; @@ -381,4 +381,10 @@ "Storage Management" = "Opslagbeheer"; "Storage Used" = "Gebruikte Opslag"; "Library cleared successfully" = "Bibliotheek succesvol gewist"; -"All downloads deleted successfully" = "Alle downloads succesvol verwijderd"; \ No newline at end of file +"All downloads deleted successfully" = "Alle downloads succesvol verwijderd"; + +/* New additions */ +"Recent searches" = "Recente zoekopdrachten"; +"me frfr" = "ik frfr"; +"Data" = "Gegevens"; +"Maximum Quality Available" = "Maximale beschikbare kwaliteit"; diff --git a/Sora/nn.lproj/Localizable.strings b/Sora/nn.lproj/Localizable.strings new file mode 100644 index 0000000..4876fd0 --- /dev/null +++ b/Sora/nn.lproj/Localizable.strings @@ -0,0 +1,384 @@ +/* General */ +"About" = "Om oss"; +"About Sora" = "Om Sora"; +"Active" = "Aktiv"; +"Active Downloads" = "Aktive nedlastinger"; +"Actively downloading media can be tracked from here." = "Aktive nedlastninger av media kan spores her."; +"Add Module" = "Legg til Modul"; +"Adjust the number of media items per row in portrait and landscape modes." = "Juster antall mediaelementer per rad i portrett- og landskapsmodus."; +"Advanced" = "Avansert"; +"AKA Sulfur" = "AKA Sulfur"; +"All Bookmarks" = "Alle bokmerker"; +"All Watching" = "Alt du ser på"; +"Also known as Sulfur" = "Også kjent som Sulfur"; +"AniList" = "AniList"; +"AniList ID" = "AniList-ID"; +"AniList Match" = "AniList-treff"; +"AniList.co" = "AniList.co"; +"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Anonyme data samles inn for å forbedre appen. Ingen personlig informasjon samles inn. Dette kan deaktiveres når som helst."; +"App Info" = "App Informasjon"; +"App Language" = "App Språk"; +"App Storage" = "App Lagring"; +"Appearance" = "Utseende"; + +/* Alerts and Actions */ +"Are you sure you want to clear all cached data? This will help free up storage space." = "Er du sikker på at du vil slette alle bufrede data? Dette vil hjelpe med å frigjøre lagringsplass."; +"Are you sure you want to delete '%@'?" = "Er du sikker på at du vil slette '%@'?"; +"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Er du sikker på at du vil slette alle %1$d episodene i '%2$@'?"; +"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Er du sikker på at du vil slette alle nedlastede filer? Du kan velge å kun tømme biblioteket og beholde de nedlastede filene til senere bruk."; +"Are you sure you want to erase all app data? This action cannot be undone." = "Er du sikker på at du vil slette alle appens data? Denne handlingen kan ikke angres."; + +/* Features */ +"Background Enabled" = "Bakgrunn Aktivert"; +"Bookmark items for an easier access later." = "Bokmerk elementer for enklere tilgang senere."; +"Bookmarks" = "Bokmerker"; +"Bottom Padding" = "Bunnutfylling"; +"Cancel" = "Avbryt"; +"Cellular Quality" = "Mobilnettkvalitet"; +"Check out some community modules here!" = "Sjekk ut noen fellesskapsmoduler her!"; +"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality." = "Velg foretrukken videooppløsning for WiFi og mobilnett. Høyere oppløsninger bruker mer data, men gir bedre kvalitet."; +"Clear" = "Tøm"; +"Clear All Downloads" = "Tøm alle nedlastinger"; +"Clear Cache" = "Tøm buffer"; +"Clear Library Only" = "Tøm kun bibliotek"; +"Clear Logs" = "Tøm logger"; +"Click the plus button to add a module!" = "Klikk på pluss-knappen for å legge til en modul!"; +"Continue Watching" = "Fortsett å se"; +"Continue Watching Episode %d" = "Fortsett å se Episode %d"; +"Contributors" = "Prosjektdeltaker"; +"Copied to Clipboard" = "Kopiert til Utklippstavlen"; +"Copy to Clipboard" = "Kopier til Utklippstavlen"; +"Copy URL" = "Kopier URL"; + +/* Episodes */ +"%lld Episodes" = "%lld Episoder"; +"%lld of %lld" = "%lld av %lld"; +"%lld-%lld" = "%lld-%lld"; +"%lld%% seen" = "%lld%% sett"; +"Episode %lld" = "Episode %lld"; +"Episodes" = "Episoder"; +"Episodes might not be available yet or there could be an issue with the source." = "Det er mulig at episodene ikke er tilgjengelige ennå, eller at det er en feil med kilden."; +"Episodes Range" = "Episoderrekkevidde"; + +/* System */ +"cranci1" = "cranci1"; +"Dark" = "Mørk"; +"DATA & LOGS" = "DATA & LOGGER"; +"Debug" = "Feilsøking"; +"Debugging and troubleshooting." = "Feilsøking og debugging."; + +/* Actions */ +"Delete" = "Slett"; +"Delete All" = "Slett alle"; +"Delete All Downloads" = "Slett Alle Nedlastinger"; +"Delete All Episodes" = "Slett Alle Episoder"; +"Delete Download" = "Slett Nedlasting"; +"Delete Episode" = "Slett Episode"; + +/* Player */ +"Double Tap to Seek" = "Dobbeltklikk for å Søke"; +"Double tapping the screen on it's sides will skip with the short tap setting." = "Ved å dobbeltklikke på sidene av skjermen vil spilleren hoppe som definert i kortklikk-innstillingen."; + +/* Downloads */ +"Download" = "Last ned"; +"Download Episode" = "Last ned Episode"; +"Download Summary" = "Nedlastingssammendrag"; +"Download This Episode" = "Last ned Denne Episoden"; +"Downloaded" = "Nedlastet"; +"Downloaded Shows" = "Nedlastede Serier"; +"Downloading" = "Laster ned"; +"Downloads" = "Nedlastinger"; + +/* Settings */ +"Enable Analytics" = "Aktiver Analyser"; +"Enable Subtitles" = "Aktiver Undertekster"; + +/* Data Management */ +"Erase" = "Slett"; +"Erase all App Data" = "Slett alle App Data"; +"Erase App Data" = "Slett App Data"; + +/* Errors */ +"Error" = "Feil"; +"Error Fetching Results" = "Feil ved henting av resultater"; +"Errors and critical issues." = "Feil og kritiske problemer."; +"Failed to load contributors" = "Kunne ikke laste prosjektdeltakere"; + +/* Features */ +"Fetch Episode metadata" = "Hent Episode metadata"; +"Files Downloaded" = "Nedlastede Filer"; +"Font Size" = "Skriftstørrelse"; + +/* Interface */ +"Force Landscape" = "Tving Landskapsmodus"; +"General" = "Generelt"; +"General events and activities." = "Generelle hendelser og aktiviteter."; +"General Preferences" = "Generelle Instillinger"; +"Hide Splash Screen" = "Skjul Velkomstskjerm"; +"HLS video downloading." = "HLS videonedlasting."; +"Hold Speed" = "Midlertidig Holdehastighet"; + +/* Info */ +"Info" = "Info"; +"INFOS" = "INFO"; +"Installed Modules" = "Installerte moduler"; +"Interface" = "Grensesnitt"; + +/* Social */ +"Join the Discord" = "Bli med i vår Discord"; + +/* Layout */ +"Landscape Columns" = "Kolonner i Landskapsmodus"; +"Language" = "Språk"; +"LESS" = "MINDRE"; + +/* Library */ +"Library" = "Bibliotek"; +"License (GPLv3.0)" = "Lisens (GPLv3.0)"; +"Light" = "Lys"; + +/* Loading States */ +"Loading Episode %lld..." = "Laster Episode %lld..."; +"Loading logs..." = "Laster logger..."; +"Loading module information..." = "Laster modulinformasjon..."; +"Loading Stream" = "Laster Videostrøm"; + +/* Logging */ +"Log Debug Info" = "Logg Feilsøkingsinfo"; +"Log Filters" = "Loggfiltre"; +"Log In with AniList" = "Logg inn med AniList"; +"Log In with Trakt" = "Logg inn med Trakt"; +"Log Out from AniList" = "Logg ut fra AniList"; +"Log Out from Trakt" = "Logg ut fra Trakt"; +"Log Types" = "Loggtyper"; +"Logged in as" = "Logget inn som"; +"Logged in as " = "Logget inn som "; + +/* Logs and Settings */ +"Logs" = "Logger"; +"Long press Skip" = "Langt trykk Skip"; +"MAIN" = "HOVED"; +"Main Developer" = "Hovedutvikler"; +"MAIN SETTINGS" = "HOVEDINNSTILLINGER"; + +/* Media Actions */ +"Mark All Previous Watched" = "Merk Alle Tidligere som Sett"; +"Mark as Watched" = "Merk som Sett"; +"Mark Episode as Watched" = "Merk Episode som Sett"; +"Mark Previous Episodes as Watched" = "Merk Tidligere Episoder som Sett"; +"Mark watched" = "Merk som Sett"; +"Match with AniList" = "Match med AniList"; +"Match with TMDB" = "Match med TMDB"; +"Matched ID: %lld" = "Matchet ID: %lld"; +"Matched with: %@" = "Matchet med: %@"; +"Max Concurrent Downloads" = "Maks Antall Parallele Nedlastinger"; + +/* Media Interface */ +"Media Grid Layout" = "Medierutenettlayout"; +"Media Player" = "Mediaspiller"; +"Media View" = "Mediavisning"; +"Metadata Provider" = "Metadata Leverandør"; +"Metadata Providers Order" = "Metadata Leverandørs Rekkefølge"; +"Module Removed" = "Modul Fjernet"; +"Modules" = "Moduler"; + +/* Headers */ +"MODULES" = "MODULER"; +"MORE" = "MER"; + +/* Status Messages */ +"No Active Downloads" = "Ingen Aktive Nedlastinger"; +"No AniList matches found" = "Ingen AniList-treff funnet"; +"No Data Available" = "Ingen Data Tilgjengelig"; +"No Downloads" = "Ingen Nedlastinger"; +"No episodes available" = "Ingen episoder tilgjengelig"; +"No Episodes Available" = "Ingen Episoder Tilgjengelig"; +"No items to continue watching." = "Ingen elementer å fortsette å se på."; +"No matches found" = "Ingen treff funnet"; +"No Module Selected" = "Ingen Modul Valgt"; +"No Modules" = "Ingen Moduler"; +"No Results Found" = "Ingen Resultater Funnet"; +"No Search Results Found" = "Ingen Søkeresultater Funnet"; +"Nothing to Continue Watching" = "Ingenting å fortsette å se på"; + +/* Notes and Messages */ +"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Merk at modulene erstattes kun hvis det er en annen versjonsstreng i JSON-filen."; + +/* Actions */ +"OK" = "OK"; +"Open Community Library" = "Åpne Fellesskapsbibliotek"; + +/* External Services */ +"Open in AniList" = "Åpne i AniList"; +"Original Poster" = "Originalplakat"; + +/* Playback */ +"Paused" = "Pauset"; +"Play" = "Spill av"; +"Player" = "Spiller"; + +/* System Messages */ +"Please restart the app to apply the language change." = "Appen må startes på nytt for å aktivere språkendringen."; +"Please select a module from settings" = "Velg en modul fra innstillinger"; + +/* Interface */ +"Portrait Columns" = "Kolonner i Portrettmodus"; +"Progress bar Marker Color" = "Farge på Progresjonslinje"; +"Provider: %@" = "Leverandør: %@"; + +/* Queue */ +"Queue" = "Kø"; +"Queued" = "I kø"; + +/* Content */ +"Recently watched content will appear here." = "Nylig sett innhold vil vises her."; + +/* Settings */ +"Refresh Modules on Launch" = "Oppdater Moduler ved Oppstart"; +"Refresh Storage Info" = "Oppdater Lagringsinformasjon"; +"Remember Playback speed" = "Husk Avspillingshastighet"; + +/* Actions */ +"Remove" = "Fjern"; +"Remove All Cache" = "Fjern Alle Buffer"; + +/* File Management */ +"Remove All Documents" = "Fjern Alle Dokumenter"; +"Remove Documents" = "Fjern Dokumenter"; +"Remove Downloaded Media" = "Fjern Nedlastet Media"; +"Remove Downloads" = "Fjern Nedlastinger"; +"Remove from Bookmarks" = "Fjern fra Bokmerker"; +"Remove Item" = "Fjern Element"; + +/* Support */ +"Report an Issue" = "Rapporter et Problem"; + +/* Reset Options */ +"Reset" = "Tilbakestill"; +"Reset AniList ID" = "Tilbakestill AniList-ID"; +"Reset Episode Progress" = "Tilbakestill Episodeprogresjon"; +"Reset progress" = "Tilbakestill Progresjon"; +"Reset Progress" = "Tilbakestill Progresjon"; + +/* System */ +"Restart Required" = "Omstart Kreves"; +"Running Sora %@ - cranci1" = "Kjører Sora %@ - cranci1"; + +/* Actions */ +"Save" = "Lagre"; +"Search" = "Søk"; + +/* Search */ +"Search downloads" = "Søk i nedlastinger"; +"Search for something..." = "Søk etter noe..."; +"Search..." = "Søk..."; + +/* Content */ +"Season %d" = "Sesong %d"; +"Season %lld" = "Sesong %lld"; +"Segments Color" = "Segmentfarge"; + +/* Modules */ +"Select Module" = "Velg Modul"; +"Set Custom AniList ID" = "Sett Egendefinert AniList-ID"; + +/* Interface */ +"Settings" = "Innstillinger"; +"Shadow" = "Skygge"; +"Show More (%lld more characters)" = "Vis mer (%lld flere tegn)"; +"Show PiP Button" = "Vis PiP Knapp"; +"Show Skip 85s Button" = "Vis Hopp 85s Knapp"; +"Show Skip Intro / Outro Buttons" = "Vis hopp over Intro / Outro Knapper"; +"Shows" = "Serier"; +"Size (%@)" = "Størrelse (%@)"; +"Skip Settings" = "Tidshopp Innstillinger"; + +/* Player Features */ +"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Noen funksjoner er begrenset til Sora og standardavspiller, som Tving Landskapsmodus, Midlertidig Holdehastighet og tilpassede tidshopp."; + +/* App Info */ +"Sora" = "Sora"; +"Sora %@ by cranci1" = "Sora %@ av cranci1"; +"Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." = "Sora og cranci1 er ikke tilknyttet AniList eller Trakt på noen måte.\n\nVær også oppmerksom på at progresjonsoppdateringer ikke nødvendigvis er 100% nøyaktige."; +"Sora GitHub Repository" = "Sora GitHub Kodelager"; +"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur vil alltid være gratis og uten reklame!"; + +/* Interface */ +"Sort" = "Sorter"; +"Speed Settings" = "Hastighetsinnstillinger"; + +/* Playback */ +"Start Watching" = "Start å se"; +"Start Watching Episode %d" = "Start å se Episode %d"; +"Storage Used" = "Brukt Lagring"; +"Stream" = "Strøm"; +"Streaming and video playback." = "Strømming og videoavspilling."; + +/* Subtitles */ +"Subtitle Color" = "Undertekstfarge"; +"Subtitle Settings" = "Undertekstinnstillinger"; + +/* Sync */ +"Sync anime progress" = "Synkroniser anime progresjon"; +"Sync TV shows progress" = "Synkroniser TV-serie progresjon"; + +/* System */ +"System" = "System"; + +/* Instructions */ +"Tap a title to override the current match." = "Trykk på en tittel for å overstyre gjeldende treff."; +"Tap Skip" = "Trykk for å hoppe Fram / Tilbake"; +"Tap to manage your modules" = "Trykk for å administrere modulene dine"; +"Tap to select a module" = "Trykk for å velge en modul"; + +/* App Information */ +"The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "Appens buffer hjelper appen med å laste bilder raskere.\n\nÅ tømme dokumentmappen vil slette alle nedlastede moduler.\n\nIkke slett App Lagring med mindre du forstår konsekvensene — det kan føre til at appen ikke fungerer som den skal."; +"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Episoderekkevidden styrer hvor mange episoder som vises på hver side. Episoder er gruppert i sett (som 1–25, 26–50, osv.), slik at du enklere kan navigere gjennom dem.\n\nEpisode-metadata refererer til episodens miniatyrbilde og tittel, da det noen ganger kan inneholde spoilers."; +"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Modulen leverte kun en episode, dette er mest sannsynlig en film, så vi bestemte oss for å lage separate sider for disse tilfellene."; + +/* Interface */ +"Thumbnails Width" = "Miniatyrbildebredde"; +"TMDB Match" = "TMDB Treff"; +"Trackers" = "Sporere"; +"Trakt" = "Trakt"; +"Trakt.tv" = "Trakt.tv"; + +/* Search */ +"Try different keywords" = "Prøv andre nøkkelord"; +"Try different search terms" = "Prøv andre søkeord"; + +/* Player Controls */ +"Two Finger Hold for Pause" = "Hold to fingre for pause"; +"Unable to fetch matches. Please try again later." = "Kunne ikke finne noen treff. Vennligst prøv igjen senere."; +"Use TMDB Poster Image" = "Bruk TMDB Plakatbilde"; + +/* Version */ +"v%@" = "v%@"; +"Video Player" = "Videospiller"; + +/* Video Settings */ +"Video Quality Preferences" = "Videokvalitetspreferanser"; +"View All" = "Se alle"; +"Watched" = "Sett"; +"Why am I not seeing any episodes?" = "Hvorfor ser jeg ingen episoder?"; +"WiFi Quality" = "WiFi Kvalitet"; + +/* User Status */ +"You are not logged in" = "Du er ikke logget inn"; +"You have no items saved." = "Du har ingen lagrede elementer."; +"Your downloaded episodes will appear here" = "Dine nedlastede episoder vil vises her"; +"Your recently watched content will appear here" = "Ditt nylig sette innhold vil vises her"; + +/* Download Settings */ +"Download Settings" = "Nedlastingsinnstillinger"; +"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Maks antall parallele nedlastinger styrer hvor mange episoder som kan lastes ned samtidig. Høyere verdier kan bruke mer båndbredde og enhetsressurser."; +"Quality" = "Kvalitet"; +"Max Concurrent Downloads" = "Maks Antall Parallele Nedlastinger"; +"Allow Cellular Downloads" = "Tillat Nedlastinger over Mobilnett"; +"Quality Information" = "Kvalitetsinformasjon"; + +/* Storage */ +"Storage Management" = "Lagringsadministrasjon"; +"Storage Used" = "Brukt Lagring"; +"Library cleared successfully" = "Bibliotek tømt"; +"All downloads deleted successfully" = "Alle nedlastinger slettet"; \ No newline at end of file diff --git a/Sora/ru.lproj/Localizable.strings b/Sora/ru.lproj/Localizable.strings new file mode 100644 index 0000000..178e7c0 --- /dev/null +++ b/Sora/ru.lproj/Localizable.strings @@ -0,0 +1,408 @@ +/* General */ +"About" = "О программе"; +"About Sora" = "О Sora"; +"Active" = "Активные"; +"Active Downloads" = "Активные загрузки"; +"Actively downloading media can be tracked from here." = "Активно загружаемые медиафайлы можно отслеживать отсюда."; +"Add Module" = "Добавить модуль"; +"Adjust the number of media items per row in portrait and landscape modes." = "Настройте количество медиаэлементов в строке в портретном и альбомном режимах."; +"Advanced" = "Расширенные"; +"AKA Sulfur" = "Также известен как Sulfur"; +"All Bookmarks" = "Все закладки"; +"All Watching" = "Все просматриваемое"; +"Also known as Sulfur" = "Также известен как Sulfur"; +"AniList" = "AniList"; +"AniList ID" = "AniList ID"; +"AniList Match" = "Совпадение AniList"; +"AniList.co" = "AniList.co"; +"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Анонимные данные собираются для улучшения приложения. Личная информация не собирается. Это можно отключить в любое время."; +"App Info" = "Информация о приложении"; +"App Language" = "Язык приложения"; +"App Storage" = "Хранилище приложения"; +"Appearance" = "Внешний вид"; + +/* Alerts and Actions */ +"Are you sure you want to clear all cached data? This will help free up storage space." = "Вы уверены, что хотите очистить все кэшированные данные? Это поможет освободить место в хранилище."; +"Are you sure you want to delete '%@'?" = "Вы уверены, что хотите удалить '%@'?"; +"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Вы уверены, что хотите удалить все %1$d эпизодов в '%2$@'?"; +"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Вы уверены, что хотите удалить все загруженные файлы? Вы можете выбрать очистку только библиотеки, сохранив загруженные файлы для будущего использования."; +"Are you sure you want to erase all app data? This action cannot be undone." = "Вы уверены, что хотите стереть все данные приложения? Это действие нельзя отменить."; + +/* Features */ +"Background Enabled" = "Фон включен"; +"Bookmark items for an easier access later." = "Добавляйте элементы в закладки для более легкого доступа позже."; +"Bookmarks" = "Закладки"; +"Bottom Padding" = "Отступ снизу"; +"Cancel" = "Отмена"; +"Cellular Quality" = "Качество мобильной сети"; +"Check out some community modules here!" = "Посмотрите модули сообщества здесь!"; +"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Выберите предпочтительное разрешение видео для WiFi и мобильных соединений. Более высокие разрешения используют больше данных, но обеспечивают лучшее качество. Если точное качество недоступно, ближайший вариант будет выбран автоматически.\n\nПримечание: Не все источники видео и плееры поддерживают выбор качества. Эта функция работает лучше всего с HLS потоками, использующими плеер Sora."; +"Clear" = "Очистить"; +"Clear All Downloads" = "Очистить все загрузки"; +"Clear Cache" = "Очистить кэш"; +"Clear Library Only" = "Очистить только библиотеку"; +"Clear Logs" = "Очистить логи"; +"Click the plus button to add a module!" = "Нажмите кнопку плюс, чтобы добавить модуль!"; +"Continue Watching" = "Продолжить просмотр"; +"Continue Watching Episode %d" = "Продолжить просмотр эпизода %d"; +"Contributors" = "Участники разработки"; +"Copied to Clipboard" = "Скопировано в буфер обмена"; +"Copy to Clipboard" = "Скопировать в буфер обмена"; +"Copy URL" = "Скопировать URL"; + +/* Episodes */ +"%lld Episodes" = "%lld эпизодов"; +"%lld of %lld" = "%lld из %lld"; +"%lld-%lld" = "%lld-%lld"; +"%lld%% seen" = "%lld%% просмотрено"; +"Episode %lld" = "Эпизод %lld"; +"Episodes" = "Эпизоды"; +"Episodes might not be available yet or there could be an issue with the source." = "Эпизоды могут быть еще недоступны или могут быть проблемы с источником."; +"Episodes Range" = "Диапазон эпизодов"; + +/* System */ +"cranci1" = "cranci1"; +"Dark" = "Темная"; +"DATA & LOGS" = "ДАННЫЕ И ЛОГИ"; +"Debug" = "Отладка"; +"Debugging and troubleshooting." = "Отладка и устранение неполадок."; + +/* Actions */ +"Delete" = "Удалить"; +"Delete All" = "Удалить все"; +"Delete All Downloads" = "Удалить все загрузки"; +"Delete All Episodes" = "Удалить все эпизоды"; +"Delete Download" = "Удалить загрузку"; +"Delete Episode" = "Удалить эпизод"; + +/* Player */ +"Double Tap to Seek" = "Двойное касание для перемотки"; +"Double tapping the screen on it's sides will skip with the short tap setting." = "Двойное касание по бокам экрана будет перематывать в соответствии с настройкой короткого касания."; + +/* Downloads */ +"Download" = "Загрузить"; +"Download Episode" = "Загрузить эпизод"; +"Download Summary" = "Сводка загрузок"; +"Download This Episode" = "Загрузить этот эпизод"; +"Downloaded" = "Загружено"; +"Downloaded Shows" = "Загруженные шоу"; +"Downloading" = "Загружается"; +"Downloads" = "Загрузки"; + +/* Settings */ +"Enable Analytics" = "Включить аналитику"; +"Enable Subtitles" = "Включить субтитры"; + +/* Data Management */ +"Erase" = "Стереть"; +"Erase all App Data" = "Стереть все данные приложения"; +"Erase App Data" = "Стереть данные приложения"; + +/* Errors */ +"Error" = "Ошибка"; +"Error Fetching Results" = "Ошибка получения результатов"; +"Errors and critical issues." = "Ошибки и критические проблемы."; +"Failed to load contributors" = "Не удалось загрузить участников"; + +/* Features */ +"Fetch Episode metadata" = "Получить метаданные эпизода"; +"Files Downloaded" = "Файлы загружены"; +"Font Size" = "Размер шрифта"; + +/* Interface */ +"Force Landscape" = "Принудительный альбомный режим"; +"General" = "Общие"; +"General events and activities." = "Общие события и действия."; +"General Preferences" = "Общие настройки"; +"Hide Splash Screen" = "Скрыть заставку"; +"HLS video downloading." = "Загрузка HLS видео."; +"Hold Speed" = "Скорость при удержании"; + +/* Info */ +"Info" = "Информация"; +"INFOS" = "ИНФОРМАЦИЯ"; +"Installed Modules" = "Установленные модули"; +"Interface" = "Интерфейс"; + +/* Social */ +"Join the Discord" = "Присоединиться к Discord"; + +/* Layout */ +"Landscape Columns" = "Столбцы в альбомном режиме"; +"Language" = "Язык"; +"LESS" = "МЕНЬШЕ"; + +/* Library */ +"Library" = "Библиотека"; +"License (GPLv3.0)" = "Лицензия (GPLv3.0)"; +"Light" = "Светлая"; + +/* Loading States */ +"Loading Episode %lld..." = "Загрузка эпизода %lld..."; +"Loading logs..." = "Загрузка логов..."; +"Loading module information..." = "Загрузка информации о модуле..."; +"Loading Stream" = "Загрузка потока"; + +/* Logging */ +"Log Debug Info" = "Записывать отладочную информацию"; +"Log Filters" = "Записывать фильтры"; +"Log In with AniList" = "Войти с AniList"; +"Log In with Trakt" = "Войти с Trakt"; +"Log Out from AniList" = "Выйти из AniList"; +"Log Out from Trakt" = "Выйти из Trakt"; +"Log Types" = "Записывать Типы"; +"Logged in as" = "Вошли как"; +"Logged in as " = "Вошли как "; + +/* Logs and Settings */ +"Logs" = "Логи"; +"Long press Skip" = "Долгое нажатие для пропуска"; +"MAIN" = "ОСНОВНОЕ"; +"Main Developer" = "Главный разработчик"; +"MAIN SETTINGS" = "ОСНОВНЫЕ НАСТРОЙКИ"; + +/* Media Actions */ +"Mark All Previous Watched" = "Отметить все предыдущие как просмотренные"; +"Mark as Watched" = "Отметить как просмотренное"; +"Mark Episode as Watched" = "Отметить эпизод как просмотренный"; +"Mark Previous Episodes as Watched" = "Отметить предыдущие эпизоды как просмотренные"; +"Mark watched" = "Отметить просмотренным"; +"Match with AniList" = "Сопоставить с AniList"; +"Match with TMDB" = "Сопоставить с TMDB"; +"Matched ID: %lld" = "Сопоставленный ID: %lld"; +"Matched with: %@" = "Сопоставлено с: %@"; +"Max Concurrent Downloads" = "Максимальное количество одновременных загрузок"; + +/* Media Interface */ +"Media Grid Layout" = "Макет сетки медиа"; +"Media Player" = "Медиаплеер"; +"Media View" = "Просмотр медиа"; +"Metadata Provider" = "Поставщик метаданных"; +"Metadata Providers Order" = "Порядок поставщиков метаданных"; +"Module Removed" = "Модуль удален"; +"Modules" = "Модули"; + +/* Headers */ +"MODULES" = "МОДУЛИ"; +"MORE" = "БОЛЬШЕ"; + +/* Status Messages */ +"No Active Downloads" = "Нет активных загрузок"; +"No AniList matches found" = "Совпадений AniList не найдено"; +"No Data Available" = "Данные недоступны"; +"No Downloads" = "Нет загрузок"; +"No episodes available" = "Эпизоды недоступны"; +"No Episodes Available" = "Эпизоды недоступны"; +"No items to continue watching." = "Нет элементов для продолжения просмотра."; +"No matches found" = "Совпадений не найдено"; +"No Module Selected" = "Модуль не выбран"; +"No Modules" = "Нет модулей"; +"No Results Found" = "Результаты не найдены"; +"No Search Results Found" = "Результаты поиска не найдены"; +"Nothing to Continue Watching" = "Нечего продолжать смотреть"; + +/* Notes and Messages */ +"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Обратите внимание, что модули будут заменены только в том случае, если в JSON-файле есть другая строка версии."; + +/* Actions */ +"OK" = "ОК"; +"Open Community Library" = "Открыть библиотеку сообщества"; + +/* External Services */ +"Open in AniList" = "Открыть в AniList"; +"Original Poster" = "Оригинальный постер"; + +/* Playback */ +"Paused" = "Приостановлено"; +"Play" = "Воспроизвести"; +"Player" = "Плеер"; + +/* System Messages */ +"Please restart the app to apply the language change." = "Пожалуйста, перезапустите приложение, чтобы применить изменение языка."; +"Please select a module from settings" = "Пожалуйста, выберите модуль в настройках"; + +/* Interface */ +"Portrait Columns" = "Столбцы в портретном режиме"; +"Progress bar Marker Color" = "Цвет маркера полосы прогресса"; +"Provider: %@" = "Поставщик: %@"; + +/* Queue */ +"Queue" = "Очередь"; +"Queued" = "В очереди"; + +/* Content */ +"Recently watched content will appear here." = "Недавно просмотренный контент будет отображаться здесь."; + +/* Settings */ +"Refresh Modules on Launch" = "Обновлять модули при запуске"; +"Refresh Storage Info" = "Обновить информацию о хранилище"; +"Remember Playback speed" = "Запомнить скорость воспроизведения"; + +/* Actions */ +"Remove" = "Удалить"; +"Remove All Cache" = "Удалить весь кэш"; + +/* File Management */ +"Remove All Documents" = "Удалить все документы"; +"Remove Documents" = "Удалить документы"; +"Remove Downloaded Media" = "Удалить загруженные медиафайлы"; +"Remove Downloads" = "Удалить загрузки"; +"Remove from Bookmarks" = "Удалить из закладок"; +"Remove Item" = "Удалить элемент"; + +/* Support */ +"Report an Issue" = "Сообщить о проблеме"; + +/* Reset Options */ +"Reset" = "Сбросить"; +"Reset AniList ID" = "Сбросить AniList ID"; +"Reset Episode Progress" = "Сбросить прогресс эпизода"; +"Reset progress" = "Сбросить прогресс"; +"Reset Progress" = "Сбросить прогресс"; + +/* System */ +"Restart Required" = "Требуется перезапуск"; +"Running Sora %@ - cranci1" = "Запущена Sora %@ - cranci1"; + +/* Actions */ +"Save" = "Сохранить"; +"Search" = "Поиск"; + +/* Search */ +"Search downloads" = "Поиск загрузок"; +"Search for something..." = "Поиск чего-либо..."; +"Search..." = "Поиск..."; + +/* Content */ +"Season %d" = "Сезон %d"; +"Season %lld" = "Сезон %lld"; +"Segments Color" = "Цвет сегментов"; + +/* Modules */ +"Select Module" = "Выбрать модуль"; +"Set Custom AniList ID" = "Установить пользовательский AniList ID"; + +/* Interface */ +"Settings" = "Настройки"; +"Shadow" = "Тень"; +"Show More (%lld more characters)" = "Показать больше (еще %lld символов)"; +"Show PiP Button" = "Показать кнопку PiP"; +"Show Skip 85s Button" = "Показать кнопку пропуска 85с"; +"Show Skip Intro / Outro Buttons" = "Показать кнопки пропуска интро/аутро"; +"Shows" = "Шоу"; +"Size (%@)" = "Размер (%@)"; +"Skip Settings" = "Настройки перемотки"; + +/* Player Features */ +"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Некоторые функции ограничены плеерами Sora и Default, такие как принудительный альбомный режим, скорость при удержании и пользовательские интервалы перемотки времени."; + +/* App Info */ +"Sora" = "Sora"; +"Sora %@ by cranci1" = "Sora %@ от cranci1"; +"Sora and cranci1 are not affiliated with AniList or Trakt in any way. + +Also note that progress updates may not be 100% accurate." = "Sora и cranci1 никак не связаны с AniList или Trakt. + +Также обратите внимание, что обновления прогресса могут быть не на 100% точными."; +"Sora GitHub Repository" = "Репозиторий Sora на GitHub"; +"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur всегда останется бесплатной без рекламы!"; + +/* Interface */ +"Sort" = "Сортировка"; +"Speed Settings" = "Настройки скорости"; + +/* Playback */ +"Start Watching" = "Начать просмотр"; +"Start Watching Episode %d" = "Начать просмотр эпизода %d"; +"Storage Used" = "Использовано хранилища"; +"Stream" = "Поток"; +"Streaming and video playback." = "Потоковая передача и воспроизведение видео."; + +/* Subtitles */ +"Subtitle Color" = "Цвет субтитров"; +"Subtitle Settings" = "Настройки субтитров"; + +/* Sync */ +"Sync anime progress" = "Синхронизировать прогресс аниме"; +"Sync TV shows progress" = "Синхронизировать прогресс ТВ-шоу"; + +/* System */ +"System" = "Система"; + +/* Instructions */ +"Tap a title to override the current match." = "Нажмите на название, чтобы переопределить текущее совпадение."; +"Tap Skip" = "Пропуск нажатием"; +"Tap to manage your modules" = "Нажмите, чтобы управлять модулями"; +"Tap to select a module" = "Нажмите, чтобы выбрать модуль"; + +/* App Information */ +"The app cache helps the app load images faster. + +Clearing the Documents folder will delete all downloaded modules. + +Do not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "Кэш приложения помогает приложению загружать изображения быстрее. + +Очистка папки документов удалит все загруженные модули. + +Не стирайте данные приложения, если не понимаете последствий — это может привести к сбоям в работе приложения."; +"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily. + +For episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Диапазон эпизодов определяет, сколько эпизодов отображается на каждой странице. Эпизоды группируются в наборы (например, 1–25, 26–50 и т. д.), что упрощает навигацию. + +Для метаданных эпизода это относится к миниатюре и названию эпизода, поскольку иногда они могут содержать спойлеры."; +"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Модуль предоставил только один эпизод, это скорее всего фильм, поэтому мы решили сделать отдельные экраны для таких случаев."; + +/* Interface */ +"Thumbnails Width" = "Ширина миниатюр"; +"TMDB Match" = "Совпадение TMDB"; +"Trackers" = "Трекеры"; +"Trakt" = "Trakt"; +"Trakt.tv" = "Trakt.tv"; + +/* Search */ +"Try different keywords" = "Попробуйте другие ключевые слова"; +"Try different search terms" = "Попробуйте другие поисковые термины"; + +/* Player Controls */ +"Two Finger Hold for Pause" = "Удержание двумя пальцами для паузы"; +"Unable to fetch matches. Please try again later." = "Не удается получить совпадения. Пожалуйста, попробуйте позже."; +"Use TMDB Poster Image" = "Использовать изображение постера TMDB"; + +/* Version */ +"v%@" = "v%@"; +"Video Player" = "Видеоплеер"; + +/* Video Settings */ +"Video Quality Preferences" = "Настройки качества видео"; +"View All" = "Просмотреть все"; +"Watched" = "Просмотрено"; +"Why am I not seeing any episodes?" = "Почему я не вижу эпизодов?"; +"WiFi Quality" = "Качество WiFi"; + +/* User Status */ +"You are not logged in" = "Вы не вошли в"; +"You have no items saved." = "У вас нет сохраненных элементов."; +"Your downloaded episodes will appear here" = "Ваши загруженные эпизоды будут отображаться здесь"; +"Your recently watched content will appear here" = "Ваш недавно просмотренный контент будет отображаться здесь"; + +/* Download Settings */ +"Download Settings" = "Настройки загрузки"; +"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Максимальное количество одновременных загрузок контролирует, сколько эпизодов могут загружаться одновременно. Более высокие значения могут использовать больше пропускной способности и ресурсов устройства."; +"Quality" = "Качество"; +"Max Concurrent Downloads" = "Максимальное количество одновременных загрузок"; +"Allow Cellular Downloads" = "Разрешить загрузки по мобильной сети"; +"Quality Information" = "Информация о качестве"; + +/* Storage */ +"Storage Management" = "Управление хранилищем"; +"Storage Used" = "Использовано хранилища"; +"Library cleared successfully" = "Библиотека успешно очищена"; +"All downloads deleted successfully" = "Все загрузки успешно удалены"; + +/* Recent searches */ +"Recent searches" = "Недавние поиски"; +"me frfr" = "я фрфр"; +"Data" = "Данные"; + +/* New string */ +"Maximum Quality Available" = "Максимальное доступное качество"; \ No newline at end of file diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 5f2a3d8..93abb53 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -12,6 +12,9 @@ 0402DA142DE7B5EC003BB42C /* SearchResultsGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0402DA102DE7B5EC003BB42C /* SearchResultsGrid.swift */; }; 0402DA152DE7B5EC003BB42C /* SearchComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0402DA0F2DE7B5EC003BB42C /* SearchComponents.swift */; }; 0402DA172DE7B7B8003BB42C /* SearchViewComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0402DA162DE7B7B8003BB42C /* SearchViewComponents.swift */; }; + 0409FE872DFF0870000DB00C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0409FE852DFF0870000DB00C /* Localizable.strings */; }; + 0409FE882DFF0870000DB00C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0409FE822DFF0870000DB00C /* Localizable.strings */; }; + 0409FE8C2DFF2886000DB00C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0409FE8A2DFF2886000DB00C /* Localizable.strings */; }; 0457C5972DE7712A000AFBD9 /* DeviceScaleModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457C5942DE7712A000AFBD9 /* DeviceScaleModifier.swift */; }; 0457C59D2DE78267000AFBD9 /* BookmarkGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457C59A2DE78267000AFBD9 /* BookmarkGridView.swift */; }; 0457C59E2DE78267000AFBD9 /* BookmarkLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457C59B2DE78267000AFBD9 /* BookmarkLink.swift */; }; @@ -21,6 +24,7 @@ 0488FA962DFDE724007575E1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0488FA932DFDE724007575E1 /* Localizable.strings */; }; 0488FA9A2DFDF380007575E1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0488FA982DFDF380007575E1 /* Localizable.strings */; }; 0488FA9E2DFDF3BB007575E1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0488FA9C2DFDF3BB007575E1 /* Localizable.strings */; }; + 04A1B73C2DFF39EB0064688A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 04A1B73A2DFF39EB0064688A /* Localizable.strings */; }; 04CD76DB2DE20F2200733536 /* AllWatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04CD76DA2DE20F2200733536 /* AllWatching.swift */; }; 04EAC39A2DF9E0DB00BBD483 /* SplashScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04EAC3992DF9E0DB00BBD483 /* SplashScreenView.swift */; }; 04F08EDC2DE10BF3006B29D9 /* ProgressiveBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F08EDB2DE10BEC006B29D9 /* ProgressiveBlurView.swift */; }; @@ -113,6 +117,9 @@ 0402DA102DE7B5EC003BB42C /* SearchResultsGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsGrid.swift; sourceTree = ""; }; 0402DA112DE7B5EC003BB42C /* SearchStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchStateView.swift; sourceTree = ""; }; 0402DA162DE7B7B8003BB42C /* SearchViewComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewComponents.swift; sourceTree = ""; }; + 0409FE812DFF0870000DB00C /* cz */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cz; path = Localizable.strings; sourceTree = ""; }; + 0409FE842DFF0870000DB00C /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = Localizable.strings; sourceTree = ""; }; + 0409FE892DFF2886000DB00C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = Localizable.strings; sourceTree = ""; }; 0457C5942DE7712A000AFBD9 /* DeviceScaleModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceScaleModifier.swift; sourceTree = ""; }; 0457C5992DE78267000AFBD9 /* BookmarkGridItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkGridItemView.swift; sourceTree = ""; }; 0457C59A2DE78267000AFBD9 /* BookmarkGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkGridView.swift; sourceTree = ""; }; @@ -122,6 +129,7 @@ 0488FA922DFDE724007575E1 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = Localizable.strings; sourceTree = ""; }; 0488FA992DFDF380007575E1 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Localizable.strings; sourceTree = ""; }; 0488FA9D2DFDF3BB007575E1 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = Localizable.strings; sourceTree = ""; }; + 04A1B7392DFF39EB0064688A /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nn; path = Localizable.strings; sourceTree = ""; }; 04CD76DA2DE20F2200733536 /* AllWatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllWatching.swift; sourceTree = ""; }; 04EAC3992DF9E0DB00BBD483 /* SplashScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenView.swift; sourceTree = ""; }; 04F08EDB2DE10BEC006B29D9 /* ProgressiveBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressiveBlurView.swift; sourceTree = ""; }; @@ -234,6 +242,30 @@ path = SearchView; sourceTree = ""; }; + 0409FE832DFF0870000DB00C /* cz.lproj */ = { + isa = PBXGroup; + children = ( + 0409FE822DFF0870000DB00C /* Localizable.strings */, + ); + path = cz.lproj; + sourceTree = ""; + }; + 0409FE862DFF0870000DB00C /* es.lproj */ = { + isa = PBXGroup; + children = ( + 0409FE852DFF0870000DB00C /* Localizable.strings */, + ); + path = es.lproj; + sourceTree = ""; + }; + 0409FE8B2DFF2886000DB00C /* ru.lproj */ = { + isa = PBXGroup; + children = ( + 0409FE8A2DFF2886000DB00C /* Localizable.strings */, + ); + path = ru.lproj; + sourceTree = ""; + }; 0457C5962DE7712A000AFBD9 /* ViewModifiers */ = { isa = PBXGroup; children = ( @@ -285,6 +317,14 @@ path = ar.lproj; sourceTree = ""; }; + 04A1B73B2DFF39EB0064688A /* nn.lproj */ = { + isa = PBXGroup; + children = ( + 04A1B73A2DFF39EB0064688A /* Localizable.strings */, + ); + path = nn.lproj; + sourceTree = ""; + }; 04F08EDA2DE10BE3006B29D9 /* ProgressiveBlurView */ = { isa = PBXGroup; children = ( @@ -395,10 +435,14 @@ 133D7C6C2D2BE2500075467E /* Sora */ = { isa = PBXGroup; children = ( - 0488FA942DFDE724007575E1 /* nl.lproj */, - 0488FA912DFDE724007575E1 /* en.lproj */, - 0488FA972DFDF334007575E1 /* fr.lproj */, + 04A1B73B2DFF39EB0064688A /* nn.lproj */, + 0409FE8B2DFF2886000DB00C /* ru.lproj */, + 0409FE832DFF0870000DB00C /* cz.lproj */, + 0409FE862DFF0870000DB00C /* es.lproj */, 0488FA9B2DFDF385007575E1 /* ar.lproj */, + 0488FA972DFDF334007575E1 /* fr.lproj */, + 0488FA912DFDE724007575E1 /* en.lproj */, + 0488FA942DFDE724007575E1 /* nl.lproj */, 130C6BF82D53A4C200DC1432 /* Sora.entitlements */, 13DC0C412D2EC9BA00D0F966 /* Info.plist */, 13103E802D589D6C000F0673 /* Tracking Services */, @@ -745,6 +789,10 @@ nl, fr, ar, + cz, + es, + ru, + nn, ); mainGroup = 133D7C612D2BE2500075467E; packageReferences = ( @@ -769,6 +817,10 @@ files = ( 0488FA9A2DFDF380007575E1 /* Localizable.strings in Resources */, 0488FA9E2DFDF3BB007575E1 /* Localizable.strings in Resources */, + 0409FE872DFF0870000DB00C /* Localizable.strings in Resources */, + 0409FE882DFF0870000DB00C /* Localizable.strings in Resources */, + 0409FE8C2DFF2886000DB00C /* Localizable.strings in Resources */, + 04A1B73C2DFF39EB0064688A /* Localizable.strings in Resources */, 133D7C752D2BE2520075467E /* Preview Assets.xcassets in Resources */, 133D7C722D2BE2520075467E /* Assets.xcassets in Resources */, 0488FA952DFDE724007575E1 /* Localizable.strings in Resources */, @@ -876,6 +928,30 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ + 0409FE822DFF0870000DB00C /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 0409FE812DFF0870000DB00C /* cz */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + 0409FE852DFF0870000DB00C /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 0409FE842DFF0870000DB00C /* es */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + 0409FE8A2DFF2886000DB00C /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 0409FE892DFF2886000DB00C /* ru */, + ); + name = Localizable.strings; + sourceTree = ""; + }; 0488FA902DFDE724007575E1 /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( @@ -908,6 +984,14 @@ name = Localizable.strings; sourceTree = ""; }; + 04A1B73A2DFF39EB0064688A /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 04A1B7392DFF39EB0064688A /* nn */, + ); + name = Localizable.strings; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ From 5ef63160364a64e67b71c0fe7d719a677f3aa2fe Mon Sep 17 00:00:00 2001 From: 50/50 <80717571+50n50@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:33:38 +0200 Subject: [PATCH 13/15] IBH bug report fixes (Read description) (#197) --- .../AccentColor.colorset/Contents.json | 12 +- Sora/Utils/Drops/DropManager.swift | 4 +- .../Modules/ModuleAdditionSettingsView.swift | 2 +- Sora/Views/DownloadView.swift | 10 +- Sora/Views/LibraryView/AllWatching.swift | 2 +- .../BookmarkComponents/BookmarkGridView.swift | 2 +- .../BookmarksDetailView.swift | 2 +- Sora/Views/LibraryView/LibraryView.swift | 2 +- .../AnilistMatchPopupView.swift | 2 +- .../CustomMatching/TMDBMatchPopupView.swift | 2 +- Sora/Views/MediaInfoView/MediaInfoView.swift | 2 +- Sora/Views/SearchView/SearchResultsGrid.swift | 137 ++++++++++++------ Sora/Views/SearchView/SearchView.swift | 2 +- .../SettingsSubViews/SettingsViewAbout.swift | 2 +- .../SettingsSubViews/SettingsViewData.swift | 3 +- .../SettingsViewDownloads.swift | 6 +- .../SettingsViewGeneral.swift | 5 +- .../SettingsSubViews/SettingsViewLogger.swift | 2 +- .../SettingsViewLoggerFilter.swift | 2 +- .../SettingsSubViews/SettingsViewModule.swift | 2 +- .../SettingsSubViews/SettingsViewPlayer.swift | 2 +- .../SettingsViewTrackers.swift | 2 +- Sora/Views/SettingsView/SettingsView.swift | 19 +-- 23 files changed, 139 insertions(+), 87 deletions(-) diff --git a/Sora/Assets.xcassets/AccentColor.colorset/Contents.json b/Sora/Assets.xcassets/AccentColor.colorset/Contents.json index 0425637..d890719 100644 --- a/Sora/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Sora/Assets.xcassets/AccentColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "blue" : "0.000", + "green" : "0.000", + "red" : "0.000" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.000", - "green" : "0.000", - "red" : "0.000" + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" } }, "idiom" : "universal" diff --git a/Sora/Utils/Drops/DropManager.swift b/Sora/Utils/Drops/DropManager.swift index 9170158..a05138d 100644 --- a/Sora/Utils/Drops/DropManager.swift +++ b/Sora/Utils/Drops/DropManager.swift @@ -7,6 +7,7 @@ import Drops import UIKit +import SwiftUI class DropManager { static let shared = DropManager() @@ -59,7 +60,8 @@ class DropManager { } func info(_ message: String, duration: TimeInterval = 2.0) { - let icon = UIImage(systemName: "info.circle.fill")?.withTintColor(.blue, renderingMode: .alwaysOriginal) + let accentColor = UIColor(Color.accentColor) + let icon = UIImage(systemName: "info.circle.fill")?.withTintColor(accentColor, renderingMode: .alwaysOriginal) showDrop(title: "Info", subtitle: message, duration: duration, icon: icon) } diff --git a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift index c05c734..2398ca2 100644 --- a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift +++ b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift @@ -41,7 +41,7 @@ struct ModuleAdditionSettingsView: View { } .padding(.bottom, 8) - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 24) { if let metadata = moduleMetadata { VStack(spacing: 0) { diff --git a/Sora/Views/DownloadView.swift b/Sora/Views/DownloadView.swift index 6233474..020603c 100644 --- a/Sora/Views/DownloadView.swift +++ b/Sora/Views/DownloadView.swift @@ -79,7 +79,7 @@ struct DownloadView: View { if jsController.activeDownloads.isEmpty && jsController.downloadQueue.isEmpty { emptyActiveDownloadsView } else { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 20) { if !jsController.downloadQueue.isEmpty { DownloadSectionView( @@ -109,7 +109,7 @@ struct DownloadView: View { if filteredAndSortedAssets.isEmpty { emptyDownloadsView } else { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 20) { DownloadSummaryCard( totalShows: groupedAssets.count, @@ -888,7 +888,7 @@ struct EnhancedDownloadGroupCard: View { .foregroundStyle(.primary) HStack(spacing: 16) { - Label("\(group.assetCount)", systemImage: "play.rectangle") + Label("\(group.assetCount) \(group.assetCount == 1 ? "Episode" : "Episodes")", systemImage: "play.rectangle") .font(.subheadline) .foregroundStyle(.secondary) @@ -956,7 +956,7 @@ struct EnhancedShowEpisodesView: View { } var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 24) { VStack(spacing: 20) { HStack(alignment: .top, spacing: 20) { @@ -997,7 +997,7 @@ struct EnhancedShowEpisodesView: View { HStack { Image(systemName: "play.rectangle.fill") .foregroundColor(.accentColor) - Text("\(group.assetCount) Episodes") + Text("\(group.assetCount) \(group.assetCount == 1 ? "Episode" : "Episodes")") .font(.headline) .foregroundColor(.secondary) } diff --git a/Sora/Views/LibraryView/AllWatching.swift b/Sora/Views/LibraryView/AllWatching.swift index 0921bdf..6e56855 100644 --- a/Sora/Views/LibraryView/AllWatching.swift +++ b/Sora/Views/LibraryView/AllWatching.swift @@ -105,7 +105,7 @@ struct AllWatchingView: View { .padding(.horizontal) .padding(.top) - ScrollView { + ScrollView(showsIndicators: false) { LazyVStack(spacing: 12) { ForEach(sortedItems) { item in FullWidthContinueWatchingCell( diff --git a/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridView.swift b/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridView.swift index 12289c6..e3321cf 100644 --- a/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridView.swift +++ b/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridView.swift @@ -16,7 +16,7 @@ struct BookmarkGridView: View { ] var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { LazyVGrid(columns: columns, spacing: 16) { ForEach(bookmarks) { bookmark in BookmarkGridItemView( diff --git a/Sora/Views/LibraryView/BookmarkComponents/BookmarksDetailView.swift b/Sora/Views/LibraryView/BookmarkComponents/BookmarksDetailView.swift index e9a0dcd..94109d9 100644 --- a/Sora/Views/LibraryView/BookmarkComponents/BookmarksDetailView.swift +++ b/Sora/Views/LibraryView/BookmarkComponents/BookmarksDetailView.swift @@ -109,7 +109,7 @@ private struct BookmarksDetailGrid: View { let moduleManager: ModuleManager private let columns = [GridItem(.adaptive(minimum: 150))] var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { LazyVGrid(columns: columns, spacing: 16) { ForEach(bookmarks) { bookmark in BookmarksDetailGridCell(bookmark: bookmark, moduleManager: moduleManager) diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 66e282a..bad055c 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -56,7 +56,7 @@ struct LibraryView: View { var body: some View { NavigationView { ZStack { - ScrollView { + ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 20) { Text("Library") .font(.largeTitle) diff --git a/Sora/Views/MediaInfoView/CustomMatching/AnilistMatchPopupView.swift b/Sora/Views/MediaInfoView/CustomMatching/AnilistMatchPopupView.swift index 8e05e38..35e996a 100644 --- a/Sora/Views/MediaInfoView/CustomMatching/AnilistMatchPopupView.swift +++ b/Sora/Views/MediaInfoView/CustomMatching/AnilistMatchPopupView.swift @@ -29,7 +29,7 @@ struct AnilistMatchPopupView: View { var body: some View { NavigationView { - ScrollView { + ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 4) { Text("".uppercased()) .font(.footnote) diff --git a/Sora/Views/MediaInfoView/CustomMatching/TMDBMatchPopupView.swift b/Sora/Views/MediaInfoView/CustomMatching/TMDBMatchPopupView.swift index d34bc69..251abe8 100644 --- a/Sora/Views/MediaInfoView/CustomMatching/TMDBMatchPopupView.swift +++ b/Sora/Views/MediaInfoView/CustomMatching/TMDBMatchPopupView.swift @@ -38,7 +38,7 @@ struct TMDBMatchPopupView: View { var body: some View { NavigationView { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 0) { if isLoading { ProgressView() diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 0f0b7ff..3f864da 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -226,7 +226,7 @@ struct MediaInfoView: View { @ViewBuilder private var mainScrollView: some View { - ScrollView { + ScrollView(showsIndicators: false) { ZStack(alignment: .top) { heroImageSection contentContainer diff --git a/Sora/Views/SearchView/SearchResultsGrid.swift b/Sora/Views/SearchView/SearchResultsGrid.swift index 78b93cf..a3a90c1 100644 --- a/Sora/Views/SearchView/SearchResultsGrid.swift +++ b/Sora/Views/SearchView/SearchResultsGrid.swift @@ -12,6 +12,10 @@ struct SearchResultsGrid: View { @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 @Environment(\.verticalSizeClass) var verticalSizeClass + @EnvironmentObject private var libraryManager: LibraryManager + @EnvironmentObject private var moduleManager: ModuleManager + @State private var showBookmarkToast: Bool = false + @State private var toastMessage: String = "" let items: [SearchItem] let columns: [GridItem] @@ -28,57 +32,100 @@ struct SearchResultsGrid: View { } var body: some View { - LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: columnsCount), spacing: 12) { - ForEach(items) { item in - NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: selectedModule)) { - ZStack { - LazyImage(url: URL(string: item.imageUrl)) { state in - if let uiImage = state.imageContainer?.image { - Image(uiImage: uiImage) - .resizable() - .aspectRatio(0.72, contentMode: .fill) - .frame(width: cellWidth, height: cellWidth * 1.5) - .cornerRadius(12) - .clipped() - } else { - Rectangle() - .fill(.tertiary) - .frame(width: cellWidth, height: cellWidth * 1.5) - .cornerRadius(12) - .clipped() + ZStack { + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: columnsCount), spacing: 12) { + ForEach(items) { item in + NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: selectedModule)) { + ZStack { + LazyImage(url: URL(string: item.imageUrl)) { state in + if let uiImage = state.imageContainer?.image { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(0.72, contentMode: .fill) + .frame(width: cellWidth, height: cellWidth * 1.5) + .cornerRadius(12) + .clipped() + } else { + Rectangle() + .fill(.tertiary) + .frame(width: cellWidth, height: cellWidth * 1.5) + .cornerRadius(12) + .clipped() + } } - } - - VStack { - Spacer() - HStack { - Text(item.title) - .lineLimit(2) - .foregroundColor(.white) - .multilineTextAlignment(.leading) + + VStack { Spacer() - } - .padding(12) - .background( - LinearGradient( - colors: [ - .black.opacity(0.7), - .black.opacity(0.0) - ], - startPoint: .bottom, - endPoint: .top + HStack { + Text(item.title) + .lineLimit(2) + .foregroundColor(.white) + .multilineTextAlignment(.leading) + Spacer() + } + .padding(12) + .background( + LinearGradient( + colors: [ + .black.opacity(0.7), + .black.opacity(0.0) + ], + startPoint: .bottom, + endPoint: .top + ) + .shadow(color: .black, radius: 4, x: 0, y: 2) ) - .shadow(color: .black, radius: 4, x: 0, y: 2) - ) + } + .frame(width: cellWidth) } - .frame(width: cellWidth) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .padding(4) } - .clipShape(RoundedRectangle(cornerRadius: 12)) - .padding(4) - }.id(item.href) + .id(item.href) + .contextMenu { + Button(action: { + let isBookmarked = libraryManager.isBookmarked(href: item.href, moduleName: selectedModule.metadata.sourceName) + libraryManager.toggleBookmark( + title: item.title, + imageUrl: item.imageUrl, + href: item.href, + moduleId: selectedModule.id.uuidString, + moduleName: selectedModule.metadata.sourceName + ) + DropManager.shared.showDrop( + title: isBookmarked ? "Removed from Bookmarks" : "Bookmarked!", + subtitle: item.title, + duration: 1.0, + icon: UIImage(systemName: isBookmarked ? "bookmark.slash" : "bookmark.fill") + ) + }) { + Label(libraryManager.isBookmarked(href: item.href, moduleName: selectedModule.metadata.sourceName) ? "Remove Bookmark" : "Bookmark", + systemImage: libraryManager.isBookmarked(href: item.href, moduleName: selectedModule.metadata.sourceName) ? "bookmark.slash" : "bookmark") + } + } + } + } + .padding(.top) + .padding() + + if showBookmarkToast { + VStack { + Spacer() + Text(toastMessage) + .font(.headline) + .padding() + .background(Color.black.opacity(0.8)) + .foregroundColor(.white) + .cornerRadius(12) + .padding(.bottom, 40) + .transition(.move(edge: .bottom).combined(with: .opacity)) + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { + withAnimation { showBookmarkToast = false } + } + } } } - .padding(.top) - .padding() } } diff --git a/Sora/Views/SearchView/SearchView.swift b/Sora/Views/SearchView/SearchView.swift index 622968b..a3cbfca 100644 --- a/Sora/Views/SearchView/SearchView.swift +++ b/Sora/Views/SearchView/SearchView.swift @@ -93,7 +93,7 @@ struct SearchView: View { .padding(.horizontal, 20) .padding(.top, 20) - ScrollView { + ScrollView(showsIndicators: false) { SearchContent( selectedModule: selectedModule, searchQuery: searchQuery, diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift index 2982a74..9192206 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift @@ -60,7 +60,7 @@ fileprivate struct SettingsSection: View { struct SettingsViewAbout: View { var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 24) { SettingsSection(title: "App Info", footer: "Sora/Sulfur will always remain free with no ads!") { HStack(alignment: .center, spacing: 16) { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift index a6f67ad..3f7ac87 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift @@ -149,7 +149,7 @@ struct SettingsViewData: View { @State private var activeAlert: ActiveAlert = .eraseData var body: some View { - return ScrollView { + return ScrollView(showsIndicators: false) { VStack(spacing: 24) { SettingsSection( title: NSLocalizedString("App Storage", comment: ""), @@ -191,6 +191,7 @@ struct SettingsViewData: View { } } } + .padding(.vertical, 20) .scrollViewBottomPadding() .navigationTitle(NSLocalizedString("App Data", comment: "")) .onAppear { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewDownloads.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewDownloads.swift index 684466b..122143b 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewDownloads.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewDownloads.swift @@ -161,7 +161,7 @@ struct SettingsViewDownloads: View { @State private var isCalculating: Bool = false var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 24) { SettingsSection( title: String(localized: "Download Settings"), @@ -309,9 +309,9 @@ struct SettingsViewDownloads: View { } } .padding(.vertical, 20) + .scrollViewBottomPadding() + .navigationTitle(String(localized: "Downloads")) } - .navigationTitle(String(localized: "Downloads")) - .scrollViewBottomPadding() .alert(String(localized: "Delete All Downloads"), isPresented: $showClearConfirmation) { Button(String(localized: "Cancel"), role: .cancel) { } Button(String(localized: "Delete All"), role: .destructive) { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index 1109491..04ebe21 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -173,7 +173,7 @@ struct SettingsViewGeneral: View { @State private var showRestartAlert = false var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 24) { SettingsSection(title: NSLocalizedString("Interface", comment: "")) { SettingsPickerRow( @@ -347,8 +347,9 @@ struct SettingsViewGeneral: View { ) } } - .navigationTitle("General") + .padding(.vertical, 20) .scrollViewBottomPadding() + .navigationTitle("General") } .navigationTitle(NSLocalizedString("General", comment: "")) .scrollViewBottomPadding() diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift index ddb935b..e140412 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift @@ -74,7 +74,7 @@ struct SettingsViewLogger: View { } var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 24) { SettingsSection(title: NSLocalizedString("Logs", comment: "")) { if isLoading { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift index 3b6e3c7..b3e85e8 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift @@ -177,7 +177,7 @@ struct SettingsViewLoggerFilter: View { } var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 24) { SettingsSection(title: NSLocalizedString("Log Types", comment: "")) { ForEach($viewModel.filters) { $filter in diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift index 9e03433..b8ac26b 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift @@ -160,7 +160,7 @@ struct SettingsViewModule: View { @State private var showLibrary = false var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 24) { if moduleManager.modules.isEmpty { SettingsSection(title: NSLocalizedString("Modules", comment: "")) { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index 501128a..468c17f 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -212,7 +212,7 @@ struct SettingsViewPlayer: View { private let qualityOptions = VideoQualityPreference.allCases.map { $0.rawValue } var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 24) { SettingsSection( title: NSLocalizedString("Media Player", comment: ""), diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift index 0ff5bab..65c489e 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift @@ -115,7 +115,7 @@ struct SettingsViewTrackers: View { @State private var isTraktLoading: Bool = false var body: some View { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 24) { SettingsSection(title: NSLocalizedString("AniList", comment: "")) { VStack(spacing: 0) { diff --git a/Sora/Views/SettingsView/SettingsView.swift b/Sora/Views/SettingsView/SettingsView.swift index 9c01e25..e50f899 100644 --- a/Sora/Views/SettingsView/SettingsView.swift +++ b/Sora/Views/SettingsView/SettingsView.swift @@ -131,10 +131,11 @@ struct SettingsView: View { @Environment(\.colorScheme) var colorScheme @StateObject var settings = Settings() @EnvironmentObject var moduleManager: ModuleManager + @State private var isNavigationActive = false var body: some View { NavigationView { - ScrollView { + ScrollView(showsIndicators: false) { VStack(spacing: 24) { Text("Settings") .font(.largeTitle) @@ -150,7 +151,7 @@ struct SettingsView: View { .foregroundStyle(.gray) .padding(.horizontal, 20) - NavigationLink(destination: SettingsViewModule()) { + NavigationLink(destination: SettingsViewModule().navigationBarBackButtonHidden(false)) { ModulePreviewRow() } .padding(.horizontal, 20) @@ -163,22 +164,22 @@ struct SettingsView: View { .padding(.horizontal, 20) VStack(spacing: 0) { - NavigationLink(destination: SettingsViewGeneral()) { + NavigationLink(destination: SettingsViewGeneral().navigationBarBackButtonHidden(false)) { SettingsNavigationRow(icon: "gearshape", titleKey: "General Preferences") } Divider().padding(.horizontal, 16) - NavigationLink(destination: SettingsViewPlayer()) { + NavigationLink(destination: SettingsViewPlayer().navigationBarBackButtonHidden(false)) { SettingsNavigationRow(icon: "play.circle", titleKey: "Video Player") } Divider().padding(.horizontal, 16) - NavigationLink(destination: SettingsViewDownloads()) { + NavigationLink(destination: SettingsViewDownloads().navigationBarBackButtonHidden(false)) { SettingsNavigationRow(icon: "arrow.down.circle", titleKey: "Downloads") } Divider().padding(.horizontal, 16) - NavigationLink(destination: SettingsViewTrackers()) { + NavigationLink(destination: SettingsViewTrackers().navigationBarBackButtonHidden(false)) { SettingsNavigationRow(icon: "square.stack.3d.up", titleKey: "Trackers") } } @@ -208,12 +209,12 @@ struct SettingsView: View { .padding(.horizontal, 20) VStack(spacing: 0) { - NavigationLink(destination: SettingsViewData()) { + NavigationLink(destination: SettingsViewData().navigationBarBackButtonHidden(false)) { SettingsNavigationRow(icon: "folder", titleKey: "Data") } Divider().padding(.horizontal, 16) - NavigationLink(destination: SettingsViewLogger()) { + NavigationLink(destination: SettingsViewLogger().navigationBarBackButtonHidden(false)) { SettingsNavigationRow(icon: "doc.text", titleKey: "Logs") } } @@ -243,7 +244,7 @@ struct SettingsView: View { .padding(.horizontal, 20) VStack(spacing: 0) { - NavigationLink(destination: SettingsViewAbout()) { + NavigationLink(destination: SettingsViewAbout().navigationBarBackButtonHidden(false)) { SettingsNavigationRow(icon: "info.circle", titleKey: "About Sora") } Divider().padding(.horizontal, 16) From 718f5b4a75580cf93fc32ee373b92181c38cb037 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Mon, 16 Jun 2025 09:45:37 +0200 Subject: [PATCH 14/15] Organized sttufs + maybe testflight build now --- .../ar.lproj/Localizable.strings | 0 .../cz.lproj/Localizable.strings | 0 .../en.lproj/Localizable.strings | 0 .../es.lproj/Localizable.strings | 0 .../fr.lproj/Localizable.strings | 0 .../nl.lproj/Localizable.strings | 0 .../nn.lproj/Localizable.strings | 0 .../ru.lproj/Localizable.strings | 0 .../AniList/Auth/Anilist-Login.swift | 0 .../AniList/Auth/Anilist-Token.swift | 0 .../Mutations/AniListPushUpdates.swift | 0 .../TMDB/TMDB-FetchID.swift | 0 .../Trakt/Auth/Trakt-Login.swift | 0 .../Trakt/Auth/Trakt-Token.swift | 0 .../Trakt/Mutations/TraktPushUpdates.swift | 0 Sulfur.xcodeproj/project.pbxproj | 74 ++++++++++--------- 16 files changed, 41 insertions(+), 33 deletions(-) rename Sora/{ => Localization}/ar.lproj/Localizable.strings (100%) rename Sora/{ => Localization}/cz.lproj/Localizable.strings (100%) rename Sora/{ => Localization}/en.lproj/Localizable.strings (100%) rename Sora/{ => Localization}/es.lproj/Localizable.strings (100%) rename Sora/{ => Localization}/fr.lproj/Localizable.strings (100%) rename Sora/{ => Localization}/nl.lproj/Localizable.strings (100%) rename Sora/{ => Localization}/nn.lproj/Localizable.strings (100%) rename Sora/{ => Localization}/ru.lproj/Localizable.strings (100%) rename Sora/{Tracking Services => Tracking & Metadata}/AniList/Auth/Anilist-Login.swift (100%) rename Sora/{Tracking Services => Tracking & Metadata}/AniList/Auth/Anilist-Token.swift (100%) rename Sora/{Tracking Services => Tracking & Metadata}/AniList/Mutations/AniListPushUpdates.swift (100%) rename Sora/{Tracking Services => Tracking & Metadata}/TMDB/TMDB-FetchID.swift (100%) rename Sora/{Tracking Services => Tracking & Metadata}/Trakt/Auth/Trakt-Login.swift (100%) rename Sora/{Tracking Services => Tracking & Metadata}/Trakt/Auth/Trakt-Token.swift (100%) rename Sora/{Tracking Services => Tracking & Metadata}/Trakt/Mutations/TraktPushUpdates.swift (100%) diff --git a/Sora/ar.lproj/Localizable.strings b/Sora/Localization/ar.lproj/Localizable.strings similarity index 100% rename from Sora/ar.lproj/Localizable.strings rename to Sora/Localization/ar.lproj/Localizable.strings diff --git a/Sora/cz.lproj/Localizable.strings b/Sora/Localization/cz.lproj/Localizable.strings similarity index 100% rename from Sora/cz.lproj/Localizable.strings rename to Sora/Localization/cz.lproj/Localizable.strings diff --git a/Sora/en.lproj/Localizable.strings b/Sora/Localization/en.lproj/Localizable.strings similarity index 100% rename from Sora/en.lproj/Localizable.strings rename to Sora/Localization/en.lproj/Localizable.strings diff --git a/Sora/es.lproj/Localizable.strings b/Sora/Localization/es.lproj/Localizable.strings similarity index 100% rename from Sora/es.lproj/Localizable.strings rename to Sora/Localization/es.lproj/Localizable.strings diff --git a/Sora/fr.lproj/Localizable.strings b/Sora/Localization/fr.lproj/Localizable.strings similarity index 100% rename from Sora/fr.lproj/Localizable.strings rename to Sora/Localization/fr.lproj/Localizable.strings diff --git a/Sora/nl.lproj/Localizable.strings b/Sora/Localization/nl.lproj/Localizable.strings similarity index 100% rename from Sora/nl.lproj/Localizable.strings rename to Sora/Localization/nl.lproj/Localizable.strings diff --git a/Sora/nn.lproj/Localizable.strings b/Sora/Localization/nn.lproj/Localizable.strings similarity index 100% rename from Sora/nn.lproj/Localizable.strings rename to Sora/Localization/nn.lproj/Localizable.strings diff --git a/Sora/ru.lproj/Localizable.strings b/Sora/Localization/ru.lproj/Localizable.strings similarity index 100% rename from Sora/ru.lproj/Localizable.strings rename to Sora/Localization/ru.lproj/Localizable.strings diff --git a/Sora/Tracking Services/AniList/Auth/Anilist-Login.swift b/Sora/Tracking & Metadata/AniList/Auth/Anilist-Login.swift similarity index 100% rename from Sora/Tracking Services/AniList/Auth/Anilist-Login.swift rename to Sora/Tracking & Metadata/AniList/Auth/Anilist-Login.swift diff --git a/Sora/Tracking Services/AniList/Auth/Anilist-Token.swift b/Sora/Tracking & Metadata/AniList/Auth/Anilist-Token.swift similarity index 100% rename from Sora/Tracking Services/AniList/Auth/Anilist-Token.swift rename to Sora/Tracking & Metadata/AniList/Auth/Anilist-Token.swift diff --git a/Sora/Tracking Services/AniList/Mutations/AniListPushUpdates.swift b/Sora/Tracking & Metadata/AniList/Mutations/AniListPushUpdates.swift similarity index 100% rename from Sora/Tracking Services/AniList/Mutations/AniListPushUpdates.swift rename to Sora/Tracking & Metadata/AniList/Mutations/AniListPushUpdates.swift diff --git a/Sora/Tracking Services/TMDB/TMDB-FetchID.swift b/Sora/Tracking & Metadata/TMDB/TMDB-FetchID.swift similarity index 100% rename from Sora/Tracking Services/TMDB/TMDB-FetchID.swift rename to Sora/Tracking & Metadata/TMDB/TMDB-FetchID.swift diff --git a/Sora/Tracking Services/Trakt/Auth/Trakt-Login.swift b/Sora/Tracking & Metadata/Trakt/Auth/Trakt-Login.swift similarity index 100% rename from Sora/Tracking Services/Trakt/Auth/Trakt-Login.swift rename to Sora/Tracking & Metadata/Trakt/Auth/Trakt-Login.swift diff --git a/Sora/Tracking Services/Trakt/Auth/Trakt-Token.swift b/Sora/Tracking & Metadata/Trakt/Auth/Trakt-Token.swift similarity index 100% rename from Sora/Tracking Services/Trakt/Auth/Trakt-Token.swift rename to Sora/Tracking & Metadata/Trakt/Auth/Trakt-Token.swift diff --git a/Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift b/Sora/Tracking & Metadata/Trakt/Mutations/TraktPushUpdates.swift similarity index 100% rename from Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift rename to Sora/Tracking & Metadata/Trakt/Mutations/TraktPushUpdates.swift diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 93abb53..4b9be87 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ 133D7C932D2BE2640075467E /* Modules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C892D2BE2640075467E /* Modules.swift */; }; 133D7C942D2BE2640075467E /* JSController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C8B2D2BE2640075467E /* JSController.swift */; }; 133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133F55BA2D33B55100E08EEA /* LibraryManager.swift */; }; + 13530BDF2E0002790048B7DE /* SoraCore in Frameworks */ = {isa = PBXBuildFile; productRef = 13530BDE2E0002790048B7DE /* SoraCore */; }; 1359ED142D76F49900C13034 /* finTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1359ED132D76F49900C13034 /* finTopView.swift */; }; 135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */; }; 13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13637B892DE0EA1100BDA2FC /* UserDefaults.swift */; }; @@ -69,7 +70,6 @@ 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; }; 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; }; 138FE1D02DECA00D00936D81 /* TMDB-FetchID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138FE1CF2DECA00D00936D81 /* TMDB-FetchID.swift */; }; - 138FF5642DFB17FF00083087 /* SoraCore in Frameworks */ = {isa = PBXBuildFile; productRef = 138FF5632DFB17FF00083087 /* SoraCore */; }; 1398FB3F2DE4E161004D3F5F /* SettingsViewAbout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1398FB3E2DE4E161004D3F5F /* SettingsViewAbout.swift */; }; 139935662D468C450065CEFF /* ModuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 139935652D468C450065CEFF /* ModuleManager.swift */; }; 1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */; }; @@ -221,7 +221,7 @@ files = ( 13367ECC2DF70698009CB33F /* Nuke in Frameworks */, 13637B902DE0ECD200BDA2FC /* Drops in Frameworks */, - 138FF5642DFB17FF00083087 /* SoraCore in Frameworks */, + 13530BDF2E0002790048B7DE /* SoraCore in Frameworks */, 13637B932DE0ECDB00BDA2FC /* MarqueeLabel in Frameworks */, 13367ECE2DF70698009CB33F /* NukeUI in Frameworks */, ); @@ -359,14 +359,14 @@ path = Sora/Utils/WebAuthentication; sourceTree = SOURCE_ROOT; }; - 13103E802D589D6C000F0673 /* Tracking Services */ = { + 13103E802D589D6C000F0673 /* Tracking & Metadata */ = { isa = PBXGroup; children = ( - 138FE1CE2DEC9FFA00936D81 /* TMDB */, 13E62FBF2DABC3A20007E259 /* Trakt */, + 138FE1CE2DEC9FFA00936D81 /* TMDB */, 13103E812D589D77000F0673 /* AniList */, ); - path = "Tracking Services"; + path = "Tracking & Metadata"; sourceTree = ""; }; 13103E812D589D77000F0673 /* AniList */ = { @@ -435,23 +435,16 @@ 133D7C6C2D2BE2500075467E /* Sora */ = { isa = PBXGroup; children = ( - 04A1B73B2DFF39EB0064688A /* nn.lproj */, - 0409FE8B2DFF2886000DB00C /* ru.lproj */, - 0409FE832DFF0870000DB00C /* cz.lproj */, - 0409FE862DFF0870000DB00C /* es.lproj */, - 0488FA9B2DFDF385007575E1 /* ar.lproj */, - 0488FA972DFDF334007575E1 /* fr.lproj */, - 0488FA912DFDE724007575E1 /* en.lproj */, - 0488FA942DFDE724007575E1 /* nl.lproj */, - 130C6BF82D53A4C200DC1432 /* Sora.entitlements */, - 13DC0C412D2EC9BA00D0F966 /* Info.plist */, - 13103E802D589D6C000F0673 /* Tracking Services */, + 13103E802D589D6C000F0673 /* Tracking & Metadata */, + 13530BE02E00028E0048B7DE /* Localization */, 133CF6A92DFEBEAB00BD13F9 /* MediaUtils */, 133D7C7B2D2BE2630075467E /* Views */, 133D7C852D2BE2640075467E /* Utils */, + 13DC0C412D2EC9BA00D0F966 /* Info.plist */, 133D7C6D2D2BE2500075467E /* SoraApp.swift */, - 133D7C6F2D2BE2500075467E /* ContentView.swift */, 133D7C712D2BE2520075467E /* Assets.xcassets */, + 133D7C6F2D2BE2500075467E /* ContentView.swift */, + 130C6BF82D53A4C200DC1432 /* Sora.entitlements */, 133D7C732D2BE2520075467E /* Preview Content */, ); path = Sora; @@ -507,11 +500,11 @@ 133D7C852D2BE2640075467E /* Utils */ = { isa = PBXGroup; children = ( + 04F08EDA2DE10BE3006B29D9 /* ProgressiveBlurView */, 130326B42DF979A300AEF610 /* WebAuthentication */, 0457C5962DE7712A000AFBD9 /* ViewModifiers */, 04F08EE02DE10C22006B29D9 /* Models */, 04F08EDD2DE10C05006B29D9 /* TabBar */, - 04F08EDA2DE10BE3006B29D9 /* ProgressiveBlurView */, 13D842532D45266900EBBFA6 /* Drops */, 1399FAD12D3AB33D00E97C31 /* Logger */, 133D7C882D2BE2640075467E /* Modules */, @@ -586,6 +579,21 @@ path = Downloads; sourceTree = ""; }; + 13530BE02E00028E0048B7DE /* Localization */ = { + isa = PBXGroup; + children = ( + 04A1B73B2DFF39EB0064688A /* nn.lproj */, + 0409FE8B2DFF2886000DB00C /* ru.lproj */, + 0409FE832DFF0870000DB00C /* cz.lproj */, + 0409FE862DFF0870000DB00C /* es.lproj */, + 0488FA9B2DFDF385007575E1 /* ar.lproj */, + 0488FA972DFDF334007575E1 /* fr.lproj */, + 0488FA912DFDE724007575E1 /* en.lproj */, + 0488FA942DFDE724007575E1 /* nl.lproj */, + ); + path = Localization; + sourceTree = ""; + }; 1384DCDF2D89BE870094797A /* Helpers */ = { isa = PBXGroup; children = ( @@ -758,7 +766,7 @@ 13637B922DE0ECDB00BDA2FC /* MarqueeLabel */, 13367ECB2DF70698009CB33F /* Nuke */, 13367ECD2DF70698009CB33F /* NukeUI */, - 138FF5632DFB17FF00083087 /* SoraCore */, + 13530BDE2E0002790048B7DE /* SoraCore */, ); productName = Sora; productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */; @@ -799,7 +807,7 @@ 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */, 13637B912DE0ECDB00BDA2FC /* XCRemoteSwiftPackageReference "MarqueeLabel" */, 13367ECA2DF70698009CB33F /* XCRemoteSwiftPackageReference "Nuke" */, - 138FF5622DFB17FF00083087 /* XCRemoteSwiftPackageReference "SoraCore" */, + 13530BDD2E0002790048B7DE /* XCRemoteSwiftPackageReference "SoraCore" */, ); productRefGroup = 133D7C6B2D2BE2500075467E /* Products */; projectDirPath = ""; @@ -1247,6 +1255,14 @@ kind = branch; }; }; + 13530BDD2E0002790048B7DE /* XCRemoteSwiftPackageReference "SoraCore" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/cranci1/SoraCore"; + requirement = { + branch = main; + kind = branch; + }; + }; 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/omaralbeik/Drops.git"; @@ -1263,14 +1279,6 @@ kind = branch; }; }; - 138FF5622DFB17FF00083087 /* XCRemoteSwiftPackageReference "SoraCore" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/cranci1/SoraCore"; - requirement = { - branch = main; - kind = branch; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1284,6 +1292,11 @@ package = 13367ECA2DF70698009CB33F /* XCRemoteSwiftPackageReference "Nuke" */; productName = NukeUI; }; + 13530BDE2E0002790048B7DE /* SoraCore */ = { + isa = XCSwiftPackageProductDependency; + package = 13530BDD2E0002790048B7DE /* XCRemoteSwiftPackageReference "SoraCore" */; + productName = SoraCore; + }; 13637B8F2DE0ECD200BDA2FC /* Drops */ = { isa = XCSwiftPackageProductDependency; package = 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */; @@ -1294,11 +1307,6 @@ package = 13637B912DE0ECDB00BDA2FC /* XCRemoteSwiftPackageReference "MarqueeLabel" */; productName = MarqueeLabel; }; - 138FF5632DFB17FF00083087 /* SoraCore */ = { - isa = XCSwiftPackageProductDependency; - package = 138FF5622DFB17FF00083087 /* XCRemoteSwiftPackageReference "SoraCore" */; - productName = SoraCore; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 133D7C622D2BE2500075467E /* Project object */; From 04fc467cb4f01b66936468df5e6a3b6aa6e955fd Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Mon, 16 Jun 2025 10:33:34 +0200 Subject: [PATCH 15/15] not 100% sure byt maybe it works now --- .../NormalPlayer/NormalPlayer.swift | 22 ++ .../MediaUtils/NormalPlayer/VideoPlayer.swift | 228 ++++++------------ .../SharePlay/SharePlayCoordinator.swift | 78 ------ .../SharePlay/SharePlayManager.swift | 77 ------ .../SharePlay/VideoWatchingActivity.swift | 35 +-- Sulfur.xcodeproj/project.pbxproj | 8 - 6 files changed, 106 insertions(+), 342 deletions(-) delete mode 100644 Sora/MediaUtils/SharePlay/SharePlayCoordinator.swift delete mode 100644 Sora/MediaUtils/SharePlay/SharePlayManager.swift diff --git a/Sora/MediaUtils/NormalPlayer/NormalPlayer.swift b/Sora/MediaUtils/NormalPlayer/NormalPlayer.swift index da9e9c3..ed4f53d 100644 --- a/Sora/MediaUtils/NormalPlayer/NormalPlayer.swift +++ b/Sora/MediaUtils/NormalPlayer/NormalPlayer.swift @@ -6,15 +6,37 @@ // import AVKit +import GroupActivities class NormalPlayer: AVPlayerViewController { private var originalRate: Float = 1.0 private var holdGesture: UILongPressGestureRecognizer? + var onSharePlayRequested: (() -> Void)? + override func viewDidLoad() { super.viewDidLoad() setupHoldGesture() setupAudioSession() + setupSharePlayButton() + } + + private func setupSharePlayButton() { + let sharePlayItem = UIBarButtonItem( + image: UIImage(systemName: "shareplay"), + style: .plain, + target: self, + action: #selector(sharePlayButtonTapped) + ) + sharePlayItem.tintColor = .white + + if responds(to: Selector(("setCustomControlItems:"))) { + setValue([sharePlayItem], forKey: "customControlItems") + } + } + + @objc private func sharePlayButtonTapped() { + onSharePlayRequested?() } private func setupHoldGesture() { diff --git a/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift index c5f3e27..e963f03 100644 --- a/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift +++ b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift @@ -28,12 +28,9 @@ class VideoPlayerViewController: UIViewController { var episodeNumber: Int = 0 var episodeImageUrl: String = "" var mediaTitle: String = "" - var subtitlesLoader: VTTSubtitlesLoader? - var subtitleLabel: UILabel? - private var sharePlayCoordinator: SharePlayCoordinator? + private var groupSession: GroupSession? private var subscriptions = Set() - private var groupSessionObserver: AnyCancellable? private var aniListUpdateSent = false private var aniListUpdatedSuccessfully = false @@ -46,60 +43,12 @@ class VideoPlayerViewController: UIViewController { if UserDefaults.standard.object(forKey: "subtitlesEnabled") == nil { UserDefaults.standard.set(true, forKey: "subtitlesEnabled") } - setupSharePlay() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func setupSubtitles() { - guard !subtitles.isEmpty, UserDefaults.standard.bool(forKey: "subtitlesEnabled"), let _ = URL(string: subtitles) else { - return - } - - subtitlesLoader = VTTSubtitlesLoader() - setupSubtitleLabel() - - subtitlesLoader?.load(from: subtitles) - - let interval = CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in - self?.updateSubtitles(at: time.seconds) - } - } - - private func setupSubtitleLabel() { - let label = UILabel() - label.numberOfLines = 0 - label.textAlignment = .center - label.textColor = .white - label.font = .systemFont(ofSize: 16, weight: .medium) - label.layer.shadowColor = UIColor.black.cgColor - label.layer.shadowOffset = CGSize(width: 1, height: 1) - label.layer.shadowOpacity = 0.8 - label.layer.shadowRadius = 2 - - guard let playerView = playerViewController?.view else { return } - playerView.addSubview(label) - label.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - label.leadingAnchor.constraint(equalTo: playerView.leadingAnchor, constant: 16), - label.trailingAnchor.constraint(equalTo: playerView.trailingAnchor, constant: -16), - label.bottomAnchor.constraint(equalTo: playerView.bottomAnchor, constant: -32) - ]) - - self.subtitleLabel = label - } - - private func updateSubtitles(at time: Double) { - let currentSubtitle = subtitlesLoader?.cues.first { cue in - time >= cue.startTime && time <= cue.endTime - } - subtitleLabel?.text = currentSubtitle?.text ?? "" - } - override func viewDidLoad() { super.viewDidLoad() @@ -133,13 +82,11 @@ class VideoPlayerViewController: UIViewController { view.addSubview(playerViewController.view) playerViewController.didMove(toParent: self) - if !subtitles.isEmpty && UserDefaults.standard.bool(forKey: "subtitlesEnabled") { - setupSubtitles() + playerViewController.onSharePlayRequested = { [weak self] in + Task { @MainActor in + await self?.startSharePlay() + } } - - // Configure SharePlay after player setup - setupSharePlayButton(in: playerViewController) - configureSharePlayForPlayer() } addPeriodicTimeObserver(fullURL: fullUrl) @@ -153,27 +100,86 @@ class VideoPlayerViewController: UIViewController { self.player?.play() } - observeGroupSession() + configureGroupSession() } - private func observeGroupSession() { - groupSessionObserver = nil - Task { [weak self] in - guard let self = self else { return } - for await session in VideoWatchingActivity.sessions() { - await self.handleIncomingGroupSession(session) + private func configureGroupSession() { + Task { + for await groupSession in VideoWatchingActivity.sessions() { + await configureGroupSession(groupSession) } } } @MainActor - private func handleIncomingGroupSession(_ session: GroupSession) async { - if sharePlayCoordinator == nil { - sharePlayCoordinator = SharePlayCoordinator() + private func configureGroupSession(_ groupSession: GroupSession) async { + self.groupSession = groupSession + + groupSession.$state + .receive(on: DispatchQueue.main) + .sink { [weak self] state in + switch state { + case .joined: + self?.coordinatePlayback() + case .invalidated: + self?.groupSession = nil + default: + break + } + } + .store(in: &subscriptions) + + groupSession.join() + } + + private func coordinatePlayback() { + guard let player = player, let groupSession = groupSession else { return } + + player.playbackCoordinator.coordinateWithSession(groupSession) + } + + @MainActor + func startSharePlay() async { + guard let streamUrl = streamUrl else { return } + + var episodeImageData: Data? + if !episodeImageUrl.isEmpty, let imageUrl = URL(string: episodeImageUrl) { + do { + episodeImageData = try await URLSession.shared.data(from: imageUrl).0 + } catch { + Logger.shared.log("Failed to load episode image: \(error)", type: "Error") + } } - sharePlayCoordinator?.configureGroupSession() - if let player = self.player { - sharePlayCoordinator?.coordinatePlayback(with: player) + + let activity = VideoWatchingActivity( + mediaTitle: mediaTitle, + episodeNumber: episodeNumber, + streamUrl: streamUrl, + subtitles: subtitles, + aniListID: aniListID, + fullUrl: fullUrl, + headers: headers, + episodeImageUrl: episodeImageUrl, + episodeImageData: episodeImageData, + totalEpisodes: totalEpisodes, + tmdbID: tmdbID, + isMovie: isMovie, + seasonNumber: seasonNumber + ) + + do { + _ = try await activity.activate() + Logger.shared.log("SharePlay session started successfully", type: "SharePlay") + } catch { + Logger.shared.log("Failed to start SharePlay: \(error)", type: "Error") + + let alert = UIAlertController( + title: "SharePlay Unavailable", + message: "SharePlay is not available right now. Make sure you're connected to FaceTime or have SharePlay enabled in Control Center.", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + present(alert, animated: true) } } @@ -309,79 +315,6 @@ class VideoPlayerViewController: UIViewController { } } - @MainActor - private func setupSharePlay() { - sharePlayCoordinator = SharePlayCoordinator() - sharePlayCoordinator?.configureGroupSession() - - if let playerViewController = playerViewController { - setupSharePlayButton(in: playerViewController) - } - } - - private func setupSharePlayButton(in playerViewController: NormalPlayer) { - // WIP - } - - @MainActor - private func startSharePlay() { - guard let streamUrl = streamUrl else { return } - - Task { - var episodeImageData: Data? - if !episodeImageUrl.isEmpty, let imageUrl = URL(string: episodeImageUrl) { - episodeImageData = try? await URLSession.shared.data(from: imageUrl).0 - } - - let activity = VideoWatchingActivity( - mediaTitle: mediaTitle, - episodeNumber: episodeNumber, - streamUrl: streamUrl, - subtitles: subtitles, - aniListID: aniListID, - fullUrl: fullUrl, - headers: headers, - episodeImageUrl: episodeImageUrl, - episodeImageData: episodeImageData, - totalEpisodes: totalEpisodes, - tmdbID: tmdbID, - isMovie: isMovie, - seasonNumber: seasonNumber - ) - - await sharePlayCoordinator?.startSharePlay(with: activity) - } - } - - private func configureSharePlayForPlayer() { - guard let player = player else { return } - sharePlayCoordinator?.coordinatePlayback(with: player) - } - - @MainActor - func presentSharePlayInvitation() { - guard let streamUrl = streamUrl else { - Logger.shared.log("Cannot start SharePlay: Stream URL is nil", type: "Error") - return - } - - SharePlayManager.shared.presentSharePlayInvitation( - from: self, - mediaTitle: mediaTitle, - episodeNumber: episodeNumber, - streamUrl: streamUrl, - subtitles: subtitles, - aniListID: aniListID, - fullUrl: fullUrl, - headers: headers, - episodeImageUrl: episodeImageUrl, - totalEpisodes: totalEpisodes, - tmdbID: tmdbID, - isMovie: isMovie, - seasonNumber: seasonNumber - ) - } - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if UserDefaults.standard.bool(forKey: "alwaysLandscape") { return .landscape @@ -403,13 +336,8 @@ class VideoPlayerViewController: UIViewController { if let timeObserverToken = timeObserverToken { player?.removeTimeObserver(timeObserverToken) } - subtitleLabel?.removeFromSuperview() - subtitleLabel = nil - subtitlesLoader = nil - sharePlayCoordinator?.leaveGroupSession() - sharePlayCoordinator = nil + groupSession?.leave() subscriptions.removeAll() - groupSessionObserver = nil } } diff --git a/Sora/MediaUtils/SharePlay/SharePlayCoordinator.swift b/Sora/MediaUtils/SharePlay/SharePlayCoordinator.swift deleted file mode 100644 index dea9d61..0000000 --- a/Sora/MediaUtils/SharePlay/SharePlayCoordinator.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// SharePlayCoordinator.swift -// Sora -// -// Created by Francesco on 15/06/25. -// - -import Combine -import Foundation -import AVFoundation -import GroupActivities - -@MainActor -class SharePlayCoordinator: ObservableObject { - private var subscriptions = Set() - private var groupSession: GroupSession? - - @Published var isEligibleForGroupSession = false - @Published var groupSessionState: GroupSession.State = .waiting - - private var playbackCoordinator: AVPlayerPlaybackCoordinator? - - func configureGroupSession() { - Task { - for await session in VideoWatchingActivity.sessions() { - await configureGroupSession(session) - } - } - } - - private func configureGroupSession(_ groupSession: GroupSession) async { - self.groupSession = groupSession - - groupSession.$state - .receive(on: DispatchQueue.main) - .assign(to: &$groupSessionState) - - groupSession.$activeParticipants - .receive(on: DispatchQueue.main) - .sink { participants in - Logger.shared.log("Active participants: \(participants.count)", type: "SharePlay") - } - .store(in: &subscriptions) - - groupSession.join() - } - - func startSharePlay(with activity: VideoWatchingActivity) async { - do { - _ = try await activity.activate() - Logger.shared.log("SharePlay activity activated successfully", type: "SharePlay") - } catch { - Logger.shared.log("Failed to activate SharePlay: \(error.localizedDescription)", type: "Error") - } - } - - func coordinatePlayback(with player: AVPlayer) { - guard let groupSession = groupSession else { return } - - playbackCoordinator = player.playbackCoordinator - playbackCoordinator?.coordinateWithSession(groupSession) - - Logger.shared.log("Playback coordination established", type: "SharePlay") - } - - nonisolated func leaveGroupSession() { - Task { @MainActor in - self.groupSession?.leave() - self.playbackCoordinator = nil - Logger.shared.log("Left SharePlay session", type: "SharePlay") - } - } - - deinit { - subscriptions.removeAll() - playbackCoordinator = nil - } -} diff --git a/Sora/MediaUtils/SharePlay/SharePlayManager.swift b/Sora/MediaUtils/SharePlay/SharePlayManager.swift deleted file mode 100644 index 415510c..0000000 --- a/Sora/MediaUtils/SharePlay/SharePlayManager.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// SharePlayManager.swift -// Sora -// -// Created by Francesco on 15/06/25. -// - -import UIKit -import Foundation -import GroupActivities - -class SharePlayManager { - static let shared = SharePlayManager() - - private init() {} - - func isSharePlayAvailable() -> Bool { - return true - } - - func presentSharePlayInvitation(from viewController: UIViewController, - mediaTitle: String, - episodeNumber: Int, - streamUrl: String, - subtitles: String = "", - aniListID: Int = 0, - fullUrl: String, - headers: [String: String]? = nil, - episodeImageUrl: String = "", - totalEpisodes: Int = 0, - tmdbID: Int? = nil, - isMovie: Bool = false, - seasonNumber: Int = 1) { - - Task { @MainActor in - var episodeImageData: Data? - if !episodeImageUrl.isEmpty, let imageUrl = URL(string: episodeImageUrl) { - do { - episodeImageData = try await URLSession.shared.data(from: imageUrl).0 - } catch { - Logger.shared.log("Failed to load episode image for SharePlay: \(error.localizedDescription)", type: "Error") - } - } - - let activity = VideoWatchingActivity( - mediaTitle: mediaTitle, - episodeNumber: episodeNumber, - streamUrl: streamUrl, - subtitles: subtitles, - aniListID: aniListID, - fullUrl: fullUrl, - headers: headers, - episodeImageUrl: episodeImageUrl, - episodeImageData: episodeImageData, - totalEpisodes: totalEpisodes, - tmdbID: tmdbID, - isMovie: isMovie, - seasonNumber: seasonNumber - ) - - do { - _ = try await activity.activate() - Logger.shared.log("SharePlay invitation sent successfully", type: "SharePlay") - } catch { - Logger.shared.log("Failed to send SharePlay invitation: \(error.localizedDescription)", type: "Error") - - let alert = UIAlertController( - title: "SharePlay Unavailable", - message: "SharePlay is not available right now. Make sure you're connected to FaceTime or have SharePlay enabled in Control Center.", - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "OK", style: .default)) - viewController.present(alert, animated: true) - } - } - } -} diff --git a/Sora/MediaUtils/SharePlay/VideoWatchingActivity.swift b/Sora/MediaUtils/SharePlay/VideoWatchingActivity.swift index f055604..df64851 100644 --- a/Sora/MediaUtils/SharePlay/VideoWatchingActivity.swift +++ b/Sora/MediaUtils/SharePlay/VideoWatchingActivity.swift @@ -13,7 +13,12 @@ struct VideoWatchingActivity: GroupActivity { var metadata: GroupActivityMetadata { var metadata = GroupActivityMetadata() metadata.title = mediaTitle - metadata.subtitle = "Episode \(episodeNumber)" + + if isMovie { + metadata.subtitle = "Movie" + } else { + metadata.subtitle = "Episode \(episodeNumber)" + } if let imageData = episodeImageData, let uiImage = UIImage(data: imageData) { @@ -37,32 +42,4 @@ struct VideoWatchingActivity: GroupActivity { let tmdbID: Int? let isMovie: Bool let seasonNumber: Int - - init(mediaTitle: String, - episodeNumber: Int, - streamUrl: String, - subtitles: String = "", - aniListID: Int = 0, - fullUrl: String, - headers: [String: String]? = nil, - episodeImageUrl: String = "", - episodeImageData: Data? = nil, - totalEpisodes: Int = 0, - tmdbID: Int? = nil, - isMovie: Bool = false, - seasonNumber: Int = 1) { - self.mediaTitle = mediaTitle - self.episodeNumber = episodeNumber - self.streamUrl = streamUrl - self.subtitles = subtitles - self.aniListID = aniListID - self.fullUrl = fullUrl - self.headers = headers - self.episodeImageUrl = episodeImageUrl - self.episodeImageData = episodeImageData - self.totalEpisodes = totalEpisodes - self.tmdbID = tmdbID - self.isMovie = isMovie - self.seasonNumber = seasonNumber - } } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 4b9be87..c3488b9 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -45,8 +45,6 @@ 13367ECC2DF70698009CB33F /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = 13367ECB2DF70698009CB33F /* Nuke */; }; 13367ECE2DF70698009CB33F /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 13367ECD2DF70698009CB33F /* NukeUI */; }; 133CF6A62DFEBE9000BD13F9 /* VideoWatchingActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133CF6A32DFEBE8F00BD13F9 /* VideoWatchingActivity.swift */; }; - 133CF6A72DFEBE9000BD13F9 /* SharePlayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133CF6A42DFEBE8F00BD13F9 /* SharePlayManager.swift */; }; - 133CF6A82DFEBE9000BD13F9 /* SharePlayCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133CF6A52DFEBE9000BD13F9 /* SharePlayCoordinator.swift */; }; 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; }; 133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; }; 133D7C722D2BE2520075467E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C712D2BE2520075467E /* Assets.xcassets */; }; @@ -149,8 +147,6 @@ 132AF1222D9995C300A0140B /* JSController-Details.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Details.swift"; sourceTree = ""; }; 132AF1242D9995F900A0140B /* JSController-Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Search.swift"; sourceTree = ""; }; 133CF6A32DFEBE8F00BD13F9 /* VideoWatchingActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoWatchingActivity.swift; sourceTree = ""; }; - 133CF6A42DFEBE8F00BD13F9 /* SharePlayManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharePlayManager.swift; sourceTree = ""; }; - 133CF6A52DFEBE9000BD13F9 /* SharePlayCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharePlayCoordinator.swift; sourceTree = ""; }; 133D7C6A2D2BE2500075467E /* Sulfur.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sulfur.app; sourceTree = BUILT_PRODUCTS_DIR; }; 133D7C6D2D2BE2500075467E /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = ""; }; 133D7C6F2D2BE2500075467E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -398,8 +394,6 @@ 133CF6A22DFEBE8100BD13F9 /* SharePlay */ = { isa = PBXGroup; children = ( - 133CF6A42DFEBE8F00BD13F9 /* SharePlayManager.swift */, - 133CF6A52DFEBE9000BD13F9 /* SharePlayCoordinator.swift */, 133CF6A32DFEBE8F00BD13F9 /* VideoWatchingActivity.swift */, ); path = SharePlay; @@ -899,7 +893,6 @@ 133D7C922D2BE2640075467E /* URLSession.swift in Sources */, 0457C5A12DE78385000AFBD9 /* BookmarksDetailView.swift in Sources */, 133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */, - 133CF6A82DFEBE9000BD13F9 /* SharePlayCoordinator.swift in Sources */, 13E62FC22DABC5830007E259 /* Trakt-Login.swift in Sources */, 133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */, 13E62FC42DABC58C0007E259 /* Trakt-Token.swift in Sources */, @@ -927,7 +920,6 @@ 13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */, 13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */, 0457C59D2DE78267000AFBD9 /* BookmarkGridView.swift in Sources */, - 133CF6A72DFEBE9000BD13F9 /* SharePlayManager.swift in Sources */, 0457C59E2DE78267000AFBD9 /* BookmarkLink.swift in Sources */, 0457C59F2DE78267000AFBD9 /* BookmarkGridItemView.swift in Sources */, );