mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
Minor bug fixes (#211)
* Modified season selector location * Increase back button opacity * Fix drop when exiting reader * Fix tab bar appearing in reader * Next chapter button * removed old commentary * Fix collection image not updating after module removal * Fix next chapter button size * Modified small bookmark type indicator * Align season selector * Continue reading, not fully done yet tho * fixed continue reading issues + added some stuff * correct resetting * pretty continue reading cells :3 * Test building * Fixed continue reading by caching * inshallah only build issue * Fixed chapter number for continue reading * Fix tab bar not appearing in search * Added github and discord icon * fix next chapter button * disable locking for dim * two finger tap to pause * 4 hours to fix this, from 8 pm till now * fix that bichass dim button * Fix downloadview * more tab bar fixes * smoother search bar * time till done indicator * someone stop me * fix bounce scroll * Fixed most of the localizations * back up system (experimental) * fuck main actor * fix reader crash when no network --------- Co-authored-by: cranci <100066266+cranci1@users.noreply.github.com>
This commit is contained in:
parent
92343aee2b
commit
0dac0566dd
49 changed files with 4719 additions and 610 deletions
21
Sora/Assets.xcassets/Discord Icon.imageset/Contents.json
vendored
Normal file
21
Sora/Assets.xcassets/Discord Icon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Discord Icon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Sora/Assets.xcassets/Discord Icon.imageset/Discord Icon.png
vendored
Normal file
BIN
Sora/Assets.xcassets/Discord Icon.imageset/Discord Icon.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
21
Sora/Assets.xcassets/Github Icon.imageset/Contents.json
vendored
Normal file
21
Sora/Assets.xcassets/Github Icon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Github Icon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Sora/Assets.xcassets/Github Icon.imageset/Github Icon.png
vendored
Normal file
BIN
Sora/Assets.xcassets/Github Icon.imageset/Github Icon.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -18,10 +18,13 @@ struct ContentView_Previews: PreviewProvider {
|
|||
|
||||
struct ContentView: View {
|
||||
@AppStorage("useNativeTabBar") private var useNativeTabBar: Bool = false
|
||||
@StateObject private var tabBarController = TabBarController()
|
||||
@State var selectedTab: Int = 0
|
||||
@State var lastTab: Int = 0
|
||||
@State private var searchQuery: String = ""
|
||||
@State private var shouldShowTabBar: Bool = true
|
||||
@State private var tabBarOffset: CGFloat = 0
|
||||
@State private var tabBarVisible: Bool = true
|
||||
@State private var lastHideTime: Date = Date()
|
||||
|
||||
let tabs: [TabItem] = [
|
||||
TabItem(icon: "square.stack", title: NSLocalizedString("LibraryTab", comment: "")),
|
||||
|
|
@ -50,25 +53,72 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
.searchable(text: $searchQuery)
|
||||
.environmentObject(tabBarController)
|
||||
} else {
|
||||
ZStack(alignment: .bottom) {
|
||||
Group {
|
||||
tabView(for: selectedTab)
|
||||
}
|
||||
.environmentObject(tabBarController)
|
||||
.onPreferenceChange(TabBarVisibilityKey.self) { shouldShowTabBar = $0 }
|
||||
|
||||
TabBar(
|
||||
tabs: tabs,
|
||||
selectedTab: $selectedTab,
|
||||
lastTab: $lastTab,
|
||||
searchQuery: $searchQuery,
|
||||
controller: tabBarController
|
||||
)
|
||||
if shouldShowTabBar {
|
||||
TabBar(
|
||||
tabs: tabs,
|
||||
selectedTab: $selectedTab
|
||||
)
|
||||
.opacity(shouldShowTabBar && tabBarVisible ? 1 : 0)
|
||||
.offset(y: tabBarVisible ? 0 : 120)
|
||||
.animation(.spring(response: 0.15, dampingFraction: 0.7), value: tabBarVisible)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
.padding(.bottom, -20)
|
||||
.onAppear {
|
||||
setupNotificationObservers()
|
||||
}
|
||||
.onDisappear {
|
||||
removeNotificationObservers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupNotificationObservers() {
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .hideTabBar,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { _ in
|
||||
lastHideTime = Date()
|
||||
tabBarVisible = false
|
||||
Logger.shared.log("Tab bar hidden", type: "Debug")
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .showTabBar,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { _ in
|
||||
let timeSinceHide = Date().timeIntervalSince(lastHideTime)
|
||||
if timeSinceHide > 0.2 {
|
||||
tabBarVisible = true
|
||||
Logger.shared.log("Tab bar shown after \(timeSinceHide) seconds", type: "Debug")
|
||||
} else {
|
||||
Logger.shared.log("Tab bar show request ignored, only \(timeSinceHide) seconds since hide", type: "Debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removeNotificationObservers() {
|
||||
NotificationCenter.default.removeObserver(self, name: .hideTabBar, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: .showTabBar, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct TabBarVisibilityKey: PreferenceKey {
|
||||
static var defaultValue: Bool = true
|
||||
static func reduce(value: inout Bool, nextValue: () -> Bool) {
|
||||
value = nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -388,3 +388,105 @@
|
|||
"me frfr" = "me frfr";
|
||||
"Data" = "البيانات";
|
||||
"Maximum Quality Available" = "أعلى جودة متاحة";
|
||||
|
||||
/* New additions */
|
||||
"DownloadCountFormat" = "%d من %d";
|
||||
"Error loading chapter" = "حدث خطأ أثناء تحميل الفصل";
|
||||
"Font Size: %dpt" = "حجم الخط: %d نقطة";
|
||||
"Line Spacing: %.1f" = "تباعد الأسطر: %.1f";
|
||||
"Line Spacing" = "تباعد الأسطر";
|
||||
"Margin: %dpx" = "الهامش: %d بكسل";
|
||||
"Margin" = "الهامش";
|
||||
"Auto Scroll Speed" = "سرعة التمرير التلقائي";
|
||||
"Speed" = "السرعة";
|
||||
"Speed: %.1fx" = "السرعة: %.1fx";
|
||||
"Matched %@: %@" = "%@: %@ متطابق";
|
||||
"Enter the AniList ID for this series" = "أدخل معرف AniList لهذه السلسلة";
|
||||
|
||||
/* New additions */
|
||||
"Create Collection" = "إنشاء مجموعة";
|
||||
"Collection Name" = "اسم المجموعة";
|
||||
"Rename Collection" = "إعادة تسمية المجموعة";
|
||||
"Rename" = "إعادة تسمية";
|
||||
"All Reading" = "كل القراءة";
|
||||
"Recently Added" = "أضيفت مؤخراً";
|
||||
"Novel Title" = "عنوان الرواية";
|
||||
"Read Progress" = "تقدم القراءة";
|
||||
"Date Created" = "تاريخ الإنشاء";
|
||||
"Name" = "الاسم";
|
||||
"Item Count" = "عدد العناصر";
|
||||
"Date Added" = "تاريخ الإضافة";
|
||||
"Title" = "العنوان";
|
||||
"Source" = "المصدر";
|
||||
"Search reading..." = "ابحث في القراءة...";
|
||||
"Search collections..." = "ابحث في المجموعات...";
|
||||
"Search bookmarks..." = "ابحث في الإشارات المرجعية...";
|
||||
"%d items" = "%d عناصر";
|
||||
"Fetching Data" = "جاري جلب البيانات";
|
||||
"Please wait while fetching." = "يرجى الانتظار أثناء الجلب.";
|
||||
"Start Reading" = "ابدأ القراءة";
|
||||
"Chapters" = "الفصول";
|
||||
"Completed" = "مكتمل";
|
||||
"Drag to reorder" = "اسحب لإعادة الترتيب";
|
||||
"Drag to reorder sections" = "اسحب لإعادة ترتيب الأقسام";
|
||||
"Library View" = "عرض المكتبة";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "خصص الأقسام المعروضة في مكتبتك. يمكنك إعادة ترتيب الأقسام أو تعطيلها بالكامل.";
|
||||
"Library Sections Order" = "ترتيب أقسام المكتبة";
|
||||
"Completion Percentage" = "نسبة الإكمال";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "بعض الميزات محدودة في مشغل Sora والمشغل الافتراضي فقط، مثل الوضع الأفقي الإجباري، سرعة التثبيت، وزيادات تخطي الوقت المخصصة.\n\nإعداد نسبة الإكمال يحدد عند أي نقطة قبل نهاية الفيديو سيتم اعتبار العمل مكتمل في AniList وTrakt.";
|
||||
"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." = "ذاكرة التخزين المؤقت تساعد التطبيق على تحميل الصور بشكل أسرع.\n\nمسح مجلد المستندات سيحذف جميع الوحدات التي تم تنزيلها.\n\nمسح بيانات التطبيق سيحذف جميع إعداداتك وبياناتك.";
|
||||
"Translators" = "المترجمون";
|
||||
"Paste URL" = "الصق الرابط";
|
||||
|
||||
/* New additions */
|
||||
"Series Title" = "عنوان السلسلة";
|
||||
"Content Source" = "مصدر المحتوى";
|
||||
"Watch Progress" = "تقدم المشاهدة";
|
||||
"Nothing to Continue Reading" = "لا شيء لمتابعة القراءة";
|
||||
"Your recently read novels will appear here" = "ستظهر الروايات التي قرأتها مؤخرًا هنا";
|
||||
"No Bookmarks" = "لا توجد إشارات مرجعية";
|
||||
"Add bookmarks to this collection" = "أضف إشارات مرجعية إلى هذه المجموعة";
|
||||
"items" = "عناصر";
|
||||
"All Watching" = "كل المشاهدة";
|
||||
"No Reading History" = "لا يوجد سجل قراءة";
|
||||
"Books you're reading will appear here" = "ستظهر الكتب التي تقرأها هنا";
|
||||
"Create Collection" = "إنشاء مجموعة";
|
||||
"Collection Name" = "اسم المجموعة";
|
||||
"Rename Collection" = "إعادة تسمية المجموعة";
|
||||
"Rename" = "إعادة تسمية";
|
||||
"Novel Title" = "عنوان الرواية";
|
||||
"Read Progress" = "تقدم القراءة";
|
||||
"Date Created" = "تاريخ الإنشاء";
|
||||
"Name" = "الاسم";
|
||||
"Item Count" = "عدد العناصر";
|
||||
"Date Added" = "تاريخ الإضافة";
|
||||
"Title" = "العنوان";
|
||||
"Source" = "المصدر";
|
||||
"Search reading..." = "ابحث في القراءة...";
|
||||
"Search collections..." = "ابحث في المجموعات...";
|
||||
"Search bookmarks..." = "ابحث في الإشارات المرجعية...";
|
||||
"%d items" = "%d عناصر";
|
||||
"Fetching Data" = "جاري جلب البيانات";
|
||||
"Please wait while fetching." = "يرجى الانتظار أثناء الجلب.";
|
||||
"Start Reading" = "ابدأ القراءة";
|
||||
"Chapters" = "الفصول";
|
||||
"Completed" = "مكتمل";
|
||||
"Drag to reorder" = "اسحب لإعادة الترتيب";
|
||||
"Drag to reorder sections" = "اسحب لإعادة ترتيب الأقسام";
|
||||
"Library View" = "عرض المكتبة";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "خصص الأقسام المعروضة في مكتبتك. يمكنك إعادة ترتيب الأقسام أو تعطيلها بالكامل.";
|
||||
"Library Sections Order" = "ترتيب أقسام المكتبة";
|
||||
"Completion Percentage" = "نسبة الإكمال";
|
||||
"Translators" = "المترجمون";
|
||||
"Paste URL" = "الصق الرابط";
|
||||
|
||||
/* New additions */
|
||||
"Collections" = "المجموعات";
|
||||
"Continue Reading" = "متابعة القراءة";
|
||||
|
||||
/* New additions */
|
||||
"Backup & Restore" = "النسخ الاحتياطي والاستعادة";
|
||||
"Export Backup" = "تصدير النسخة الاحتياطية";
|
||||
"Import Backup" = "استيراد النسخة الاحتياطية";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "تنبيه: هذه الميزة لا تزال تجريبية. يرجى التحقق من بياناتك بعد التصدير/الاستيراد.";
|
||||
"Backup" = "نسخة احتياطية";
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
"Clear All Downloads" = "Obriši sva preuzimanja";
|
||||
"Clear Cache" = "Obriši keš";
|
||||
"Clear Library Only" = "Obriši samo biblioteku";
|
||||
"Clear Logs" = "Obriši logове";
|
||||
"Clear Logs" = "Obriši logove";
|
||||
"Click the plus button to add a module!" = "Kliknite plus dugme da dodate modul!";
|
||||
"Continue Watching" = "Nastavi gledanje";
|
||||
"Continue Watching Episode %d" = "Nastavi gledanje epizode %d";
|
||||
|
|
@ -404,3 +404,107 @@ Za metapodatke epizode, odnosi se na sličicu i naslov epizode, jer ponekad mogu
|
|||
"me frfr" = "ja stvarno";
|
||||
"Data" = "Podaci";
|
||||
"Maximum Quality Available" = "Maksimalna dostupna kvaliteta";
|
||||
|
||||
/* Additional translations */
|
||||
"DownloadCountFormat" = "%d od %d";
|
||||
"Error loading chapter" = "Greška pri učitavanju poglavlja";
|
||||
"Font Size: %dpt" = "Veličina fonta: %dpt";
|
||||
"Line Spacing: %.1f" = "Razmak između redova: %.1f";
|
||||
"Line Spacing" = "Razmak između redova";
|
||||
"Margin: %dpx" = "Margina: %dpx";
|
||||
"Margin" = "Margina";
|
||||
"Auto Scroll Speed" = "Brzina automatskog pomicanja";
|
||||
"Speed" = "Brzina";
|
||||
"Speed: %.1fx" = "Brzina: %.1fx";
|
||||
"Matched %@: %@" = "Poklapanje %@: %@";
|
||||
"Enter the AniList ID for this series" = "Unesite AniList ID za ovu seriju";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Kreiraj kolekciju";
|
||||
"Collection Name" = "Naziv kolekcije";
|
||||
"Rename Collection" = "Preimenuj kolekciju";
|
||||
"Rename" = "Preimenuj";
|
||||
"All Reading" = "Sva čitanja";
|
||||
"Recently Added" = "Nedavno dodano";
|
||||
"Novel Title" = "Naslov romana";
|
||||
"Read Progress" = "Napredak čitanja";
|
||||
"Date Created" = "Datum kreiranja";
|
||||
"Name" = "Naziv";
|
||||
"Item Count" = "Broj stavki";
|
||||
"Date Added" = "Datum dodavanja";
|
||||
"Title" = "Naslov";
|
||||
"Source" = "Izvor";
|
||||
"Search reading..." = "Pretraži čitanje...";
|
||||
"Search collections..." = "Pretraži kolekcije...";
|
||||
"Search bookmarks..." = "Pretraži oznake...";
|
||||
"%d items" = "%d stavki";
|
||||
"Fetching Data" = "Preuzimanje podataka";
|
||||
"Please wait while fetching." = "Molimo sačekajte dok se preuzima.";
|
||||
"Start Reading" = "Započni čitanje";
|
||||
"Chapters" = "Poglavlja";
|
||||
"Completed" = "Završeno";
|
||||
"Drag to reorder" = "Povuci za promjenu redoslijeda";
|
||||
"Drag to reorder sections" = "Povuci za promjenu redoslijeda sekcija";
|
||||
"Library View" = "Prikaz biblioteke";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Prilagodite sekcije prikazane u vašoj biblioteci. Možete ih preurediti ili potpuno onemogućiti.";
|
||||
"Library Sections Order" = "Redoslijed sekcija biblioteke";
|
||||
"Completion Percentage" = "Procenat završetka";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Neke funkcije su ograničene na Sora i zadani player, kao što su prisilni pejzaž, držanje brzine i prilagođeni intervali preskakanja.\n\nPostavka procenta završetka određuje u kojoj tački prije kraja videa će aplikacija označiti kao završeno na AniList i Trakt.";
|
||||
"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." = "Keš aplikacije pomaže bržem učitavanju slika.\n\nBrisanje Documents foldera će ukloniti sve preuzete module.\n\nBrisanje podataka aplikacije briše sve vaše postavke i podatke.";
|
||||
"Translators" = "Prevoditelji";
|
||||
"Paste URL" = "Zalijepi URL";
|
||||
|
||||
/* New additions */
|
||||
"Series Title" = "Naslov serije";
|
||||
"Content Source" = "Izvor sadržaja";
|
||||
"Watch Progress" = "Napredak gledanja";
|
||||
"Recent searches" = "Nedavne pretrage";
|
||||
"All Reading" = "Sve što čitam";
|
||||
"Nothing to Continue Reading" = "Nema ništa za nastaviti čitanje";
|
||||
"Your recently read novels will appear here" = "Vaši nedavno pročitani romani će se pojaviti ovdje";
|
||||
"No Bookmarks" = "Nema zabilješki";
|
||||
"Add bookmarks to this collection" = "Dodajte zabilješke u ovu kolekciju";
|
||||
"items" = "stavke";
|
||||
"All Watching" = "Sve što gledam";
|
||||
"No Reading History" = "Nema historije čitanja";
|
||||
"Books you're reading will appear here" = "Knjige koje čitate će se pojaviti ovdje";
|
||||
"Create Collection" = "Kreiraj kolekciju";
|
||||
"Collection Name" = "Naziv kolekcije";
|
||||
"Rename Collection" = "Preimenuj kolekciju";
|
||||
"Rename" = "Preimenuj";
|
||||
"Novel Title" = "Naslov romana";
|
||||
"Read Progress" = "Napredak čitanja";
|
||||
"Date Created" = "Datum kreiranja";
|
||||
"Name" = "Ime";
|
||||
"Item Count" = "Broj stavki";
|
||||
"Date Added" = "Datum dodavanja";
|
||||
"Title" = "Naslov";
|
||||
"Source" = "Izvor";
|
||||
"Search reading..." = "Pretraži čitanje...";
|
||||
"Search collections..." = "Pretraži kolekcije...";
|
||||
"Search bookmarks..." = "Pretraži zabilješke...";
|
||||
"%d items" = "%d stavki";
|
||||
"Fetching Data" = "Dohvatanje podataka";
|
||||
"Please wait while fetching." = "Molimo sačekajte dok se podaci dohvaćaju.";
|
||||
"Start Reading" = "Započni čitanje";
|
||||
"Chapters" = "Poglavlja";
|
||||
"Completed" = "Završeno";
|
||||
"Drag to reorder" = "Povucite za promjenu redoslijeda";
|
||||
"Drag to reorder sections" = "Povucite za promjenu redoslijeda sekcija";
|
||||
"Library View" = "Prikaz biblioteke";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Prilagodite sekcije prikazane u vašoj biblioteci. Možete promijeniti redoslijed ili ih potpuno onemogućiti.";
|
||||
"Library Sections Order" = "Redoslijed sekcija biblioteke";
|
||||
"Completion Percentage" = "Procenat završetka";
|
||||
"Translators" = "Prevodioci";
|
||||
"Paste URL" = "Zalijepi URL";
|
||||
|
||||
/* New additions */
|
||||
"Collections" = "Kolekcije";
|
||||
"Continue Reading" = "Nastavi čitanje";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Sigurnosna kopija i vraćanje";
|
||||
"Export Backup" = "Izvezi sigurnosnu kopiju";
|
||||
"Import Backup" = "Uvezi sigurnosnu kopiju";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Napomena: Ova funkcija je još uvijek eksperimentalna. Molimo provjerite svoje podatke nakon izvoza/uvoza.";
|
||||
"Backup" = "Sigurnosna kopija";
|
||||
|
|
|
|||
|
|
@ -405,4 +405,107 @@ Metadata epizody se týkají náhledu a názvu epizody, které mohou někdy obsa
|
|||
"Data" = "Data";
|
||||
|
||||
/* New string */
|
||||
"Maximum Quality Available" = "Maximální dostupná kvalita";
|
||||
"Maximum Quality Available" = "Maximální dostupná kvalita";
|
||||
|
||||
/* Additional translations */
|
||||
"DownloadCountFormat" = "%d z %d";
|
||||
"Error loading chapter" = "Chyba při načítání kapitoly";
|
||||
"Font Size: %dpt" = "Velikost písma: %dpt";
|
||||
"Line Spacing: %.1f" = "Řádkování: %.1f";
|
||||
"Line Spacing" = "Řádkování";
|
||||
"Margin: %dpx" = "Okraj: %dpx";
|
||||
"Margin" = "Okraj";
|
||||
"Auto Scroll Speed" = "Rychlost automatického posunu";
|
||||
"Speed" = "Rychlost";
|
||||
"Speed: %.1fx" = "Rychlost: %.1fx";
|
||||
"Matched %@: %@" = "Shoda %@: %@";
|
||||
"Enter the AniList ID for this series" = "Zadejte AniList ID pro tuto sérii";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Vytvořit kolekci";
|
||||
"Collection Name" = "Název kolekce";
|
||||
"Rename Collection" = "Přejmenovat kolekci";
|
||||
"Rename" = "Přejmenovat";
|
||||
"All Reading" = "Všechny knihy";
|
||||
"Recently Added" = "Nedávno přidáno";
|
||||
"Novel Title" = "Název románu";
|
||||
"Read Progress" = "Postup čtení";
|
||||
"Date Created" = "Datum vytvoření";
|
||||
"Name" = "Název";
|
||||
"Item Count" = "Počet položek";
|
||||
"Date Added" = "Datum přidání";
|
||||
"Title" = "Titul";
|
||||
"Source" = "Zdroj";
|
||||
"Search reading..." = "Hledat v knihách...";
|
||||
"Search collections..." = "Hledat v kolekcích...";
|
||||
"Search bookmarks..." = "Hledat v záložkách...";
|
||||
"%d items" = "%d položek";
|
||||
"Fetching Data" = "Načítání dat";
|
||||
"Please wait while fetching." = "Počkejte prosím během načítání.";
|
||||
"Start Reading" = "Začít číst";
|
||||
"Chapters" = "Kapitoly";
|
||||
"Completed" = "Dokončeno";
|
||||
"Drag to reorder" = "Přetáhněte pro změnu pořadí";
|
||||
"Drag to reorder sections" = "Přetáhněte pro změnu pořadí sekcí";
|
||||
"Library View" = "Zobrazení knihovny";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Přizpůsobte sekce zobrazené ve vaší knihovně. Můžete je přeuspořádat nebo zcela vypnout.";
|
||||
"Library Sections Order" = "Pořadí sekcí knihovny";
|
||||
"Completion Percentage" = "Procento dokončení";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Některé funkce jsou omezeny na Sora a výchozí přehrávač, například vynucená krajina, podržení rychlosti a vlastní intervaly přeskočení.\n\nNastavení procenta dokončení určuje, v jakém bodě před koncem videa bude aplikace označovat jako dokončené na AniList a Trakt.";
|
||||
"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." = "Mezipaměť aplikace pomáhá rychlejšímu načítání obrázků.\n\nVymazání složky Documents odstraní všechny stažené moduly.\n\nVymazání dat aplikace smaže všechna vaše nastavení a data.";
|
||||
"Translators" = "Překladatelé";
|
||||
"Paste URL" = "Vložit URL";
|
||||
|
||||
/* New localizations */
|
||||
"Series Title" = "Název série";
|
||||
"Content Source" = "Zdroj obsahu";
|
||||
"Watch Progress" = "Průběh sledování";
|
||||
"All Reading" = "Vše ke čtení";
|
||||
"Nothing to Continue Reading" = "Nic k pokračování ve čtení";
|
||||
"Your recently read novels will appear here" = "Vaše nedávno čtené romány se zobrazí zde";
|
||||
"No Bookmarks" = "Žádné záložky";
|
||||
"Add bookmarks to this collection" = "Přidejte záložky do této kolekce";
|
||||
"items" = "položky";
|
||||
"All Watching" = "Vše ke sledování";
|
||||
"No Reading History" = "Žádná historie čtení";
|
||||
"Books you're reading will appear here" = "Knihy, které čtete, se zobrazí zde";
|
||||
"Create Collection" = "Vytvořit kolekci";
|
||||
"Collection Name" = "Název kolekce";
|
||||
"Rename Collection" = "Přejmenovat kolekci";
|
||||
"Rename" = "Přejmenovat";
|
||||
"Novel Title" = "Název románu";
|
||||
"Read Progress" = "Průběh čtení";
|
||||
"Date Created" = "Datum vytvoření";
|
||||
"Name" = "Jméno";
|
||||
"Item Count" = "Počet položek";
|
||||
"Date Added" = "Datum přidání";
|
||||
"Title" = "Název";
|
||||
"Source" = "Zdroj";
|
||||
"Search reading..." = "Hledat ve čtení...";
|
||||
"Search collections..." = "Hledat v kolekcích...";
|
||||
"Search bookmarks..." = "Hledat v záložkách...";
|
||||
"%d items" = "%d položek";
|
||||
"Fetching Data" = "Načítání dat";
|
||||
"Please wait while fetching." = "Počkejte prosím, načítají se data.";
|
||||
"Start Reading" = "Začít číst";
|
||||
"Chapters" = "Kapitoly";
|
||||
"Completed" = "Dokončeno";
|
||||
"Drag to reorder" = "Přetáhněte pro změnu pořadí";
|
||||
"Drag to reorder sections" = "Přetáhněte pro změnu pořadí sekcí";
|
||||
"Library View" = "Zobrazení knihovny";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Přizpůsobte sekce zobrazené ve vaší knihovně. Můžete změnit jejich pořadí nebo je úplně vypnout.";
|
||||
"Library Sections Order" = "Pořadí sekcí knihovny";
|
||||
"Completion Percentage" = "Procento dokončení";
|
||||
"Translators" = "Překladatelé";
|
||||
"Paste URL" = "Vložit URL";
|
||||
|
||||
/* New localizations */
|
||||
"Collections" = "Kolekce";
|
||||
"Continue Reading" = "Pokračovat ve čtení";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Zálohování a obnovení";
|
||||
"Export Backup" = "Exportovat zálohu";
|
||||
"Import Backup" = "Importovat zálohu";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Upozornění: Tato funkce je stále experimentální. Po exportu/importu si prosím zkontrolujte svá data.";
|
||||
"Backup" = "Záloha";
|
||||
|
|
@ -398,3 +398,102 @@
|
|||
"me frfr" = "Ich, ohne Witz";
|
||||
"Data" = "Daten";
|
||||
"Maximum Quality Available" = "Maximal verfügbare Qualität";
|
||||
|
||||
"DownloadCountFormat" = "%d von %d";
|
||||
"Error loading chapter" = "Fehler beim Laden des Kapitels";
|
||||
"Font Size: %dpt" = "Schriftgröße: %dpt";
|
||||
"Line Spacing: %.1f" = "Zeilenabstand: %.1f";
|
||||
"Line Spacing" = "Zeilenabstand";
|
||||
"Margin: %dpx" = "Rand: %dpx";
|
||||
"Margin" = "Rand";
|
||||
"Auto Scroll Speed" = "Automatische Scroll-Geschwindigkeit";
|
||||
"Speed" = "Geschwindigkeit";
|
||||
"Speed: %.1fx" = "Geschwindigkeit: %.1fx";
|
||||
"Matched %@: %@" = "Abgeglichen %@: %@";
|
||||
"Enter the AniList ID for this series" = "Geben Sie die AniList-ID für diese Serie ein";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Sammlung erstellen";
|
||||
"Collection Name" = "Sammlungsname";
|
||||
"Rename Collection" = "Sammlung umbenennen";
|
||||
"Rename" = "Umbenennen";
|
||||
"All Reading" = "Alles Lesen";
|
||||
"Recently Added" = "Kürzlich hinzugefügt";
|
||||
"Novel Title" = "Roman Titel";
|
||||
"Read Progress" = "Lesefortschritt";
|
||||
"Date Created" = "Erstellungsdatum";
|
||||
"Name" = "Name";
|
||||
"Item Count" = "Anzahl der Elemente";
|
||||
"Date Added" = "Hinzugefügt am";
|
||||
"Title" = "Titel";
|
||||
"Source" = "Quelle";
|
||||
"Search reading..." = "Lesen durchsuchen...";
|
||||
"Search collections..." = "Sammlungen durchsuchen...";
|
||||
"Search bookmarks..." = "Lesezeichen durchsuchen...";
|
||||
"%d items" = "%d Elemente";
|
||||
"Fetching Data" = "Daten werden abgerufen";
|
||||
"Please wait while fetching." = "Bitte warten Sie während des Abrufs.";
|
||||
"Start Reading" = "Lesen starten";
|
||||
"Chapters" = "Kapitel";
|
||||
"Completed" = "Abgeschlossen";
|
||||
"Drag to reorder" = "Ziehen zum Neuordnen";
|
||||
"Drag to reorder sections" = "Ziehen zum Neuordnen der Abschnitte";
|
||||
"Library View" = "Bibliotheksansicht";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Passen Sie die in Ihrer Bibliothek angezeigten Abschnitte an. Sie können Abschnitte neu anordnen oder vollständig deaktivieren.";
|
||||
"Library Sections Order" = "Reihenfolge der Bibliotheksabschnitte";
|
||||
"Completion Percentage" = "Abschlussprozentsatz";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Einige Funktionen sind nur im Sora- und Standard-Player verfügbar, wie z.B. erzwungene Querformatansicht, Haltegeschwindigkeit und benutzerdefinierte Zeitsprünge.\n\nDie Einstellung des Abschlussprozentsatzes bestimmt, ab welchem Punkt vor dem Ende eines Videos die App es als abgeschlossen auf AniList und Trakt markiert.";
|
||||
"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." = "Der App-Cache hilft, Bilder schneller zu laden.\n\nDas Löschen des Dokumente-Ordners entfernt alle heruntergeladenen Module.\n\nDas Löschen der App-Daten entfernt alle Ihre Einstellungen und Daten.";
|
||||
"Translators" = "Übersetzer";
|
||||
"Paste URL" = "URL einfügen";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Serientitel";
|
||||
"Content Source" = "Inhaltsquelle";
|
||||
"Watch Progress" = "Fortschritt ansehen";
|
||||
"All Reading" = "Alles Lesen";
|
||||
"Nothing to Continue Reading" = "Nichts zum Weiterlesen";
|
||||
"Your recently read novels will appear here" = "Ihre zuletzt gelesenen Romane erscheinen hier";
|
||||
"No Bookmarks" = "Keine Lesezeichen";
|
||||
"Add bookmarks to this collection" = "Fügen Sie dieser Sammlung Lesezeichen hinzu";
|
||||
"items" = "Elemente";
|
||||
"All Watching" = "Alles Ansehen";
|
||||
"No Reading History" = "Kein Leseverlauf";
|
||||
"Books you're reading will appear here" = "Bücher, die Sie lesen, erscheinen hier";
|
||||
"Create Collection" = "Sammlung erstellen";
|
||||
"Collection Name" = "Sammlungsname";
|
||||
"Rename Collection" = "Sammlung umbenennen";
|
||||
"Rename" = "Umbenennen";
|
||||
"Novel Title" = "Roman Titel";
|
||||
"Read Progress" = "Lesefortschritt";
|
||||
"Date Created" = "Erstellungsdatum";
|
||||
"Name" = "Name";
|
||||
"Item Count" = "Anzahl der Elemente";
|
||||
"Date Added" = "Hinzugefügt am";
|
||||
"Title" = "Titel";
|
||||
"Source" = "Quelle";
|
||||
"Search reading..." = "Lesen durchsuchen...";
|
||||
"Search collections..." = "Sammlungen durchsuchen...";
|
||||
"Search bookmarks..." = "Lesezeichen durchsuchen...";
|
||||
"%d items" = "%d Elemente";
|
||||
"Fetching Data" = "Daten werden abgerufen";
|
||||
"Please wait while fetching." = "Bitte warten Sie während des Abrufs.";
|
||||
"Start Reading" = "Lesen starten";
|
||||
"Chapters" = "Kapitel";
|
||||
"Completed" = "Abgeschlossen";
|
||||
"Drag to reorder" = "Ziehen zum Neuordnen";
|
||||
"Drag to reorder sections" = "Ziehen zum Neuordnen der Abschnitte";
|
||||
"Library View" = "Bibliotheksansicht";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Passen Sie die in Ihrer Bibliothek angezeigten Abschnitte an. Sie können Abschnitte neu anordnen oder vollständig deaktivieren.";
|
||||
"Library Sections Order" = "Reihenfolge der Bibliotheksabschnitte";
|
||||
"Completion Percentage" = "Abschlussprozentsatz";
|
||||
"Translators" = "Übersetzer";
|
||||
"Paste URL" = "URL einfügen";
|
||||
|
||||
"Continue Reading" = "Weiterlesen";
|
||||
|
||||
"Backup & Restore" = "Sichern & Wiederherstellen";
|
||||
"Export Backup" = "Backup exportieren";
|
||||
"Import Backup" = "Backup importieren";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Hinweis: Diese Funktion ist noch experimentell. Bitte überprüfe deine Daten nach dem Export/Import.";
|
||||
"Backup" = "Backup";
|
||||
|
|
|
|||
|
|
@ -396,5 +396,66 @@
|
|||
/* New additions */
|
||||
"Recent searches" = "Recent searches";
|
||||
"me frfr" = "me frfr";
|
||||
"Data" = "Data";
|
||||
"Data" = "Data";
|
||||
"All Reading" = "All Reading";
|
||||
"No Reading History" = "No Reading History";
|
||||
"Books you're reading will appear here" = "Books you're reading will appear here";
|
||||
"All Watching" = "All Watching";
|
||||
"Continue Reading" = "Continue Reading";
|
||||
"Nothing to Continue Reading" = "Nothing to Continue Reading";
|
||||
"Your recently read novels will appear here" = "Your recently read novels will appear here";
|
||||
"No Bookmarks" = "No Bookmarks";
|
||||
"Add bookmarks to this collection" = "Add bookmarks to this collection";
|
||||
"items" = "items";
|
||||
"Chapter %d" = "Chapter %d";
|
||||
"Episode %d" = "Episode %d";
|
||||
"%d%%" = "%d%%";
|
||||
"%d%% seen" = "%d%% seen";
|
||||
"DownloadCountFormat" = "%d of %d";
|
||||
"Error loading chapter" = "Error loading chapter";
|
||||
"Font Size: %dpt" = "Font Size: %dpt";
|
||||
"Line Spacing: %.1f" = "Line Spacing: %.1f";
|
||||
"Line Spacing" = "Line Spacing";
|
||||
"Margin: %dpx" = "Margin: %dpx";
|
||||
"Margin" = "Margin";
|
||||
"Auto Scroll Speed" = "Auto Scroll Speed";
|
||||
"Speed" = "Speed";
|
||||
"Speed: %.1fx" = "Speed: %.1fx";
|
||||
"Matched %@: %@" = "Matched %@: %@";
|
||||
"Enter the AniList ID for this series" = "Enter the AniList ID for this series";
|
||||
|
||||
/* New additions */
|
||||
"Create Collection" = "Create Collection";
|
||||
"Collection Name" = "Collection Name";
|
||||
"Rename Collection" = "Rename Collection";
|
||||
"Rename" = "Rename";
|
||||
"All Reading" = "All Reading";
|
||||
"Recently Added" = "Recently Added";
|
||||
"Novel Title" = "Novel Title";
|
||||
"Read Progress" = "Read Progress";
|
||||
"Date Created" = "Date Created";
|
||||
"Name" = "Name";
|
||||
"Item Count" = "Item Count";
|
||||
"Date Added" = "Date Added";
|
||||
"Title" = "Title";
|
||||
"Source" = "Source";
|
||||
"Search reading..." = "Search reading...";
|
||||
"Search collections..." = "Search collections...";
|
||||
"Search bookmarks..." = "Search bookmarks...";
|
||||
"%d items" = "%d items";
|
||||
"Fetching Data" = "Fetching Data";
|
||||
"Please wait while fetching." = "Please wait while fetching.";
|
||||
"Start Reading" = "Start Reading";
|
||||
"Chapters" = "Chapters";
|
||||
"Completed" = "Completed";
|
||||
"Drag to reorder" = "Drag to reorder";
|
||||
"Drag to reorder sections" = "Drag to reorder sections";
|
||||
"Library View" = "Library View";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Customize the sections shown in your library. You can reorder sections or disable them completely.";
|
||||
"Library Sections Order" = "Library Sections Order";
|
||||
"Completion Percentage" = "Completion Percentage";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt.";
|
||||
"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." = "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.";
|
||||
"Translators" = "Translators";
|
||||
"Paste URL" = "Paste URL";
|
||||
|
||||
|
|
|
|||
|
|
@ -404,4 +404,103 @@ Para los metadatos del episodio, se refiere a la miniatura y el título del epis
|
|||
"me frfr" = "yo frfr";
|
||||
"Data" = "Datos";
|
||||
"Maximum Quality Available" = "Calidad máxima disponible";
|
||||
"DownloadCountFormat" = "%d de %d";
|
||||
"Error loading chapter" = "Error al cargar el capítulo";
|
||||
"Font Size: %dpt" = "Tamaño de fuente: %dpt";
|
||||
"Line Spacing: %.1f" = "Espaciado de línea: %.1f";
|
||||
"Line Spacing" = "Espaciado de línea";
|
||||
"Margin: %dpx" = "Margen: %dpx";
|
||||
"Margin" = "Margen";
|
||||
"Auto Scroll Speed" = "Velocidad de desplazamiento automático";
|
||||
"Speed" = "Velocidad";
|
||||
"Speed: %.1fx" = "Velocidad: %.1fx";
|
||||
"Matched %@: %@" = "Coincidencia %@: %@";
|
||||
"Enter the AniList ID for this series" = "Introduce el ID de AniList para esta serie";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Crear colección";
|
||||
"Collection Name" = "Nombre de la colección";
|
||||
"Rename Collection" = "Renombrar colección";
|
||||
"Rename" = "Renombrar";
|
||||
"All Reading" = "Todas las lecturas";
|
||||
"Recently Added" = "Añadido recientemente";
|
||||
"Novel Title" = "Título de la novela";
|
||||
"Read Progress" = "Progreso de lectura";
|
||||
"Date Created" = "Fecha de creación";
|
||||
"Name" = "Nombre";
|
||||
"Item Count" = "Cantidad de elementos";
|
||||
"Date Added" = "Fecha de añadido";
|
||||
"Title" = "Título";
|
||||
"Source" = "Fuente";
|
||||
"Search reading..." = "Buscar en lecturas...";
|
||||
"Search collections..." = "Buscar en colecciones...";
|
||||
"Search bookmarks..." = "Buscar en marcadores...";
|
||||
"%d items" = "%d elementos";
|
||||
"Fetching Data" = "Obteniendo datos";
|
||||
"Please wait while fetching." = "Por favor, espere mientras se obtienen los datos.";
|
||||
"Start Reading" = "Comenzar a leer";
|
||||
"Chapters" = "Capítulos";
|
||||
"Completed" = "Completado";
|
||||
"Drag to reorder" = "Arrastrar para reordenar";
|
||||
"Drag to reorder sections" = "Arrastrar para reordenar secciones";
|
||||
"Library View" = "Vista de biblioteca";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Personaliza las secciones que se muestran en tu biblioteca. Puedes reordenarlas o desactivarlas completamente.";
|
||||
"Library Sections Order" = "Orden de secciones de la biblioteca";
|
||||
"Completion Percentage" = "Porcentaje de finalización";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Algunas funciones están limitadas al reproductor Sora y al predeterminado, como el modo horizontal forzado, la velocidad de retención y los saltos de tiempo personalizados.\n\nEl ajuste del porcentaje de finalización determina en qué punto antes del final de un vídeo la app lo marcará como completado en AniList y Trakt.";
|
||||
"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." = "La caché de la app ayuda a cargar imágenes más rápido.\n\nBorrar la carpeta Documentos eliminará todos los módulos descargados.\n\nBorrar los datos de la app eliminará todos tus ajustes y datos.";
|
||||
"Translators" = "Traductores";
|
||||
"Paste URL" = "Pegar URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Título de la serie";
|
||||
"Content Source" = "Fuente de contenido";
|
||||
"Watch Progress" = "Progreso de visualización";
|
||||
"All Reading" = "Todo lo que lees";
|
||||
"Nothing to Continue Reading" = "Nada para continuar leyendo";
|
||||
"Your recently read novels will appear here" = "Tus novelas leídas recientemente aparecerán aquí";
|
||||
"No Bookmarks" = "Sin marcadores";
|
||||
"Add bookmarks to this collection" = "Agrega marcadores a esta colección";
|
||||
"items" = "elementos";
|
||||
"All Watching" = "Todo lo que ves";
|
||||
"No Reading History" = "Sin historial de lectura";
|
||||
"Books you're reading will appear here" = "Los libros que estás leyendo aparecerán aquí";
|
||||
"Create Collection" = "Crear colección";
|
||||
"Collection Name" = "Nombre de la colección";
|
||||
"Rename Collection" = "Renombrar colección";
|
||||
"Rename" = "Renombrar";
|
||||
"Novel Title" = "Título de la novela";
|
||||
"Read Progress" = "Progreso de lectura";
|
||||
"Date Created" = "Fecha de creación";
|
||||
"Name" = "Nombre";
|
||||
"Item Count" = "Cantidad de elementos";
|
||||
"Date Added" = "Fecha de agregado";
|
||||
"Title" = "Título";
|
||||
"Source" = "Fuente";
|
||||
"Search reading..." = "Buscar en lecturas...";
|
||||
"Search collections..." = "Buscar en colecciones...";
|
||||
"Search bookmarks..." = "Buscar en marcadores...";
|
||||
"%d items" = "%d elementos";
|
||||
"Fetching Data" = "Obteniendo datos";
|
||||
"Please wait while fetching." = "Por favor, espera mientras se obtienen los datos.";
|
||||
"Start Reading" = "Comenzar a leer";
|
||||
"Chapters" = "Capítulos";
|
||||
"Completed" = "Completado";
|
||||
"Drag to reorder" = "Arrastra para reordenar";
|
||||
"Drag to reorder sections" = "Arrastra para reordenar secciones";
|
||||
"Library View" = "Vista de biblioteca";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Personaliza las secciones que se muestran en tu biblioteca. Puedes reordenar secciones o deshabilitarlas completamente.";
|
||||
"Library Sections Order" = "Orden de secciones de la biblioteca";
|
||||
"Completion Percentage" = "Porcentaje de finalización";
|
||||
"Translators" = "Traductores";
|
||||
"Paste URL" = "Pegar URL";
|
||||
|
||||
"Collections" = "Colecciones";
|
||||
"Continue Reading" = "Continuar leyendo";
|
||||
|
||||
"Backup & Restore" = "Copia de seguridad y restaurar";
|
||||
"Export Backup" = "Exportar copia de seguridad";
|
||||
"Import Backup" = "Importar copia de seguridad";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Aviso: Esta función aún es experimental. Por favor, verifica tus datos después de exportar/importar.";
|
||||
"Backup" = "Copia de seguridad";
|
||||
|
||||
|
|
|
|||
|
|
@ -388,3 +388,107 @@
|
|||
"me frfr" = "moi frfr";
|
||||
"Data" = "Données";
|
||||
"Maximum Quality Available" = "Qualité maximale disponible";
|
||||
|
||||
/* Additional translations */
|
||||
"DownloadCountFormat" = "%d sur %d";
|
||||
"Error loading chapter" = "Erreur lors du chargement du chapitre";
|
||||
"Font Size: %dpt" = "Taille de police : %dpt";
|
||||
"Line Spacing: %.1f" = "Interligne : %.1f";
|
||||
"Line Spacing" = "Interligne";
|
||||
"Margin: %dpx" = "Marge : %dpx";
|
||||
"Margin" = "Marge";
|
||||
"Auto Scroll Speed" = "Vitesse de défilement automatique";
|
||||
"Speed" = "Vitesse";
|
||||
"Speed: %.1fx" = "Vitesse : %.1fx";
|
||||
"Matched %@: %@" = "Correspondance %@ : %@";
|
||||
"Enter the AniList ID for this series" = "Entrez l'ID AniList pour cette série";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Créer une collection";
|
||||
"Collection Name" = "Nom de la collection";
|
||||
"Rename Collection" = "Renommer la collection";
|
||||
"Rename" = "Renommer";
|
||||
"All Reading" = "Toutes les lectures";
|
||||
"Recently Added" = "Ajouté récemment";
|
||||
"Novel Title" = "Titre du roman";
|
||||
"Read Progress" = "Progression de la lecture";
|
||||
"Date Created" = "Date de création";
|
||||
"Name" = "Nom";
|
||||
"Item Count" = "Nombre d'éléments";
|
||||
"Date Added" = "Date d'ajout";
|
||||
"Title" = "Titre";
|
||||
"Source" = "Source";
|
||||
"Search reading..." = "Rechercher dans les lectures...";
|
||||
"Search collections..." = "Rechercher dans les collections...";
|
||||
"Search bookmarks..." = "Rechercher dans les favoris...";
|
||||
"%d items" = "%d éléments";
|
||||
"Fetching Data" = "Récupération des données";
|
||||
"Please wait while fetching." = "Veuillez patienter pendant la récupération.";
|
||||
"Start Reading" = "Commencer la lecture";
|
||||
"Chapters" = "Chapitres";
|
||||
"Completed" = "Terminé";
|
||||
"Drag to reorder" = "Glisser pour réorganiser";
|
||||
"Drag to reorder sections" = "Glisser pour réorganiser les sections";
|
||||
"Library View" = "Vue de la bibliothèque";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Personnalisez les sections affichées dans votre bibliothèque. Vous pouvez réorganiser ou désactiver complètement les sections.";
|
||||
"Library Sections Order" = "Ordre des sections de la bibliothèque";
|
||||
"Completion Percentage" = "Pourcentage d'achèvement";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Certaines fonctionnalités sont limitées au lecteur Sora et au lecteur par défaut, comme le mode paysage forcé, la vitesse de maintien et les sauts de temps personnalisés.\n\nLe réglage du pourcentage d'achèvement détermine à quel moment avant la fin d'une vidéo l'application la marquera comme terminée sur AniList et Trakt.";
|
||||
"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." = "Le cache de l'application aide à charger les images plus rapidement.\n\nVider le dossier Documents supprimera tous les modules téléchargés.\n\nEffacer les données de l'application supprimera tous vos paramètres et données.";
|
||||
"Translators" = "Traducteurs";
|
||||
"Paste URL" = "Coller l'URL";
|
||||
|
||||
/* New additions */
|
||||
"Series Title" = "Titre de la série";
|
||||
"Content Source" = "Source du contenu";
|
||||
"Watch Progress" = "Progression de visionnage";
|
||||
"Recent searches" = "Recherches récentes";
|
||||
"All Reading" = "Tout ce que vous lisez";
|
||||
"Nothing to Continue Reading" = "Rien à continuer à lire";
|
||||
"Your recently read novels will appear here" = "Vos romans récemment lus apparaîtront ici";
|
||||
"No Bookmarks" = "Aucun favori";
|
||||
"Add bookmarks to this collection" = "Ajoutez des favoris à cette collection";
|
||||
"items" = "éléments";
|
||||
"All Watching" = "Tout ce que vous regardez";
|
||||
"No Reading History" = "Aucun historique de lecture";
|
||||
"Books you're reading will appear here" = "Les livres que vous lisez apparaîtront ici";
|
||||
"Create Collection" = "Créer une collection";
|
||||
"Collection Name" = "Nom de la collection";
|
||||
"Rename Collection" = "Renommer la collection";
|
||||
"Rename" = "Renommer";
|
||||
"Novel Title" = "Titre du roman";
|
||||
"Read Progress" = "Progression de lecture";
|
||||
"Date Created" = "Date de création";
|
||||
"Name" = "Nom";
|
||||
"Item Count" = "Nombre d'éléments";
|
||||
"Date Added" = "Date d'ajout";
|
||||
"Title" = "Titre";
|
||||
"Source" = "Source";
|
||||
"Search reading..." = "Rechercher dans les lectures...";
|
||||
"Search collections..." = "Rechercher dans les collections...";
|
||||
"Search bookmarks..." = "Rechercher dans les favoris...";
|
||||
"%d items" = "%d éléments";
|
||||
"Fetching Data" = "Récupération des données";
|
||||
"Please wait while fetching." = "Veuillez patienter pendant la récupération.";
|
||||
"Start Reading" = "Commencer la lecture";
|
||||
"Chapters" = "Chapitres";
|
||||
"Completed" = "Terminé";
|
||||
"Drag to reorder" = "Faites glisser pour réorganiser";
|
||||
"Drag to reorder sections" = "Faites glisser pour réorganiser les sections";
|
||||
"Library View" = "Vue de la bibliothèque";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Personnalisez les sections affichées dans votre bibliothèque. Vous pouvez réorganiser les sections ou les désactiver complètement.";
|
||||
"Library Sections Order" = "Ordre des sections de la bibliothèque";
|
||||
"Completion Percentage" = "Pourcentage d'achèvement";
|
||||
"Translators" = "Traducteurs";
|
||||
"Paste URL" = "Coller l'URL";
|
||||
|
||||
/* New additions */
|
||||
"Collections" = "Collections";
|
||||
"Continue Reading" = "Continuer la lecture";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Sauvegarde & Restauration";
|
||||
"Export Backup" = "Exporter la sauvegarde";
|
||||
"Import Backup" = "Importer la sauvegarde";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Remarque : Cette fonctionnalité est encore expérimentale. Veuillez vérifier vos données après l'export/import.";
|
||||
"Backup" = "Sauvegarde";
|
||||
|
|
|
|||
|
|
@ -328,3 +328,98 @@
|
|||
"me frfr" = "me frfr";
|
||||
"Data" = "Dati";
|
||||
"Maximum Quality Available" = "Qualità Massima Disponibile";
|
||||
"DownloadCountFormat" = "%d di %d";
|
||||
"Error loading chapter" = "Errore nel caricamento del capitolo";
|
||||
"Font Size: %dpt" = "Dimensione carattere: %dpt";
|
||||
"Line Spacing: %.1f" = "Interlinea: %.1f";
|
||||
"Line Spacing" = "Interlinea";
|
||||
"Margin: %dpx" = "Margine: %dpx";
|
||||
"Margin" = "Margine";
|
||||
"Auto Scroll Speed" = "Velocità di scorrimento automatico";
|
||||
"Speed" = "Velocità";
|
||||
"Speed: %.1fx" = "Velocità: %.1fx";
|
||||
"Matched %@: %@" = "Corrispondenza %@: %@";
|
||||
"Enter the AniList ID for this series" = "Inserisci l'ID AniList per questa serie";
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Crea raccolta";
|
||||
"Collection Name" = "Nome raccolta";
|
||||
"Rename Collection" = "Rinomina raccolta";
|
||||
"Rename" = "Rinomina";
|
||||
"All Reading" = "Tutte le letture";
|
||||
"Recently Added" = "Aggiunti di recente";
|
||||
"Novel Title" = "Titolo del romanzo";
|
||||
"Read Progress" = "Progresso lettura";
|
||||
"Date Created" = "Data di creazione";
|
||||
"Name" = "Nome";
|
||||
"Item Count" = "Numero di elementi";
|
||||
"Date Added" = "Data di aggiunta";
|
||||
"Title" = "Titolo";
|
||||
"Source" = "Fonte";
|
||||
"Search reading..." = "Cerca nelle letture...";
|
||||
"Search collections..." = "Cerca nelle raccolte...";
|
||||
"Search bookmarks..." = "Cerca nei segnalibri...";
|
||||
"%d items" = "%d elementi";
|
||||
"Fetching Data" = "Recupero dati";
|
||||
"Please wait while fetching." = "Attendere durante il recupero.";
|
||||
"Start Reading" = "Inizia a leggere";
|
||||
"Chapters" = "Capitoli";
|
||||
"Completed" = "Completato";
|
||||
"Drag to reorder" = "Trascina per riordinare";
|
||||
"Drag to reorder sections" = "Trascina per riordinare le sezioni";
|
||||
"Library View" = "Vista libreria";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Personalizza le sezioni mostrate nella tua libreria. Puoi riordinare le sezioni o disabilitarle completamente.";
|
||||
"Library Sections Order" = "Ordine delle sezioni della libreria";
|
||||
"Completion Percentage" = "Percentuale di completamento";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Alcune funzioni sono limitate al player Sora e a quello predefinito, come la forzatura del paesaggio, la velocità di mantenimento e gli intervalli di salto personalizzati.\n\nL'impostazione della percentuale di completamento determina in quale punto prima della fine di un video l'app lo segnerà come completato su AniList e Trakt.";
|
||||
"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." = "La cache dell'app aiuta a caricare le immagini più velocemente.\n\nCancellare la cartella Documenti eliminerà tutti i moduli scaricati.\n\nCancellare i dati dell'app eliminerà tutte le tue impostazioni e dati.";
|
||||
"Translators" = "Traduttori";
|
||||
"Paste URL" = "Incolla URL";
|
||||
"Series Title" = "Titolo della serie";
|
||||
"Content Source" = "Fonte del contenuto";
|
||||
"Watch Progress" = "Progresso di visione";
|
||||
"Recent searches" = "Ricerche recenti";
|
||||
"All Reading" = "Tutto ciò che leggi";
|
||||
"Nothing to Continue Reading" = "Niente da continuare a leggere";
|
||||
"Your recently read novels will appear here" = "I tuoi romanzi letti di recente appariranno qui";
|
||||
"No Bookmarks" = "Nessun segnalibro";
|
||||
"Add bookmarks to this collection" = "Aggiungi segnalibri a questa raccolta";
|
||||
"items" = "elementi";
|
||||
"All Watching" = "Tutto ciò che guardi";
|
||||
"No Reading History" = "Nessuna cronologia di lettura";
|
||||
"Books you're reading will appear here" = "I libri che stai leggendo appariranno qui";
|
||||
"Create Collection" = "Crea raccolta";
|
||||
"Collection Name" = "Nome raccolta";
|
||||
"Rename Collection" = "Rinomina raccolta";
|
||||
"Rename" = "Rinomina";
|
||||
"Novel Title" = "Titolo del romanzo";
|
||||
"Read Progress" = "Progresso di lettura";
|
||||
"Date Created" = "Data di creazione";
|
||||
"Name" = "Nome";
|
||||
"Item Count" = "Numero di elementi";
|
||||
"Date Added" = "Data di aggiunta";
|
||||
"Title" = "Titolo";
|
||||
"Source" = "Fonte";
|
||||
"Search reading..." = "Cerca nelle letture...";
|
||||
"Search collections..." = "Cerca nelle raccolte...";
|
||||
"Search bookmarks..." = "Cerca nei segnalibri...";
|
||||
"%d items" = "%d elementi";
|
||||
"Fetching Data" = "Recupero dati";
|
||||
"Please wait while fetching." = "Attendere durante il recupero.";
|
||||
"Start Reading" = "Inizia a leggere";
|
||||
"Chapters" = "Capitoli";
|
||||
"Completed" = "Completato";
|
||||
"Drag to reorder" = "Trascina per riordinare";
|
||||
"Drag to reorder sections" = "Trascina per riordinare le sezioni";
|
||||
"Library View" = "Vista libreria";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Personalizza le sezioni mostrate nella tua libreria. Puoi riordinare le sezioni o disabilitarle completamente.";
|
||||
"Library Sections Order" = "Ordine delle sezioni della libreria";
|
||||
"Completion Percentage" = "Percentuale di completamento";
|
||||
"Translators" = "Traduttori";
|
||||
"Paste URL" = "Incolla URL";
|
||||
"Collections" = "Raccolte";
|
||||
"Continue Reading" = "Continua a leggere";
|
||||
"Backup & Restore" = "Backup e ripristino";
|
||||
"Export Backup" = "Esporta backup";
|
||||
"Import Backup" = "Importa backup";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Nota: Questa funzione è ancora sperimentale. Si prega di ricontrollare i dati dopo esportazione/importazione.";
|
||||
"Backup" = "Backup";
|
||||
|
|
|
|||
|
|
@ -397,10 +397,110 @@ For episode metadata, it refers to the episode thumbnail and title, since someti
|
|||
"Storage Management" = "Қойма басқаруы";
|
||||
"Storage Used" = "Пайдаланылған қойма";
|
||||
"Library cleared successfully" = "Кітапхана сәтті тазартылды";
|
||||
"All downloads deleted successfully" = "Барлық жүктеулер сәтті жойылды";
|
||||
"All downloads deleted successfully" = "Барлық жүктеулер сәтті жойылды";
|
||||
|
||||
/* New additions */
|
||||
"Recent searches" = "Соңғы іздеулер";
|
||||
"me frfr" = "мен шынында";
|
||||
"Data" = "Деректер";
|
||||
"Maximum Quality Available" = "Қолжетімді максималды сапа";
|
||||
"Maximum Quality Available" = "Қолжетімді максималды сапа";
|
||||
"DownloadCountFormat" = "%d / %d";
|
||||
"Error loading chapter" = "Тарауды жүктеу қатесі";
|
||||
"Font Size: %dpt" = "Қаріп өлшемі: %dpt";
|
||||
"Line Spacing: %.1f" = "Жоларалық қашықтық: %.1f";
|
||||
"Line Spacing" = "Жоларалық қашықтық";
|
||||
"Margin: %dpx" = "Шеткі өріс: %dpx";
|
||||
"Margin" = "Шеткі өріс";
|
||||
"Auto Scroll Speed" = "Автоматты айналдыру жылдамдығы";
|
||||
"Speed" = "Жылдамдық";
|
||||
"Speed: %.1fx" = "Жылдамдық: %.1fx";
|
||||
"Matched %@: %@" = "Сәйкестік %@: %@";
|
||||
"Enter the AniList ID for this series" = "Осы серия үшін AniList ID енгізіңіз";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Жинақ құру";
|
||||
"Collection Name" = "Жинақ атауы";
|
||||
"Rename Collection" = "Жинақты қайта атау";
|
||||
"Rename" = "Қайта атау";
|
||||
"All Reading" = "Барлық оқу";
|
||||
"Recently Added" = "Жуырда қосылған";
|
||||
"Novel Title" = "Роман атауы";
|
||||
"Read Progress" = "Оқу барысы";
|
||||
"Date Created" = "Құрылған күні";
|
||||
"Name" = "Атауы";
|
||||
"Item Count" = "Элементтер саны";
|
||||
"Date Added" = "Қосылған күні";
|
||||
"Title" = "Тақырып";
|
||||
"Source" = "Дереккөз";
|
||||
"Search reading..." = "Оқуды іздеу...";
|
||||
"Search collections..." = "Жинақтарды іздеу...";
|
||||
"Search bookmarks..." = "Бетбелгілерді іздеу...";
|
||||
"%d items" = "%d элемент";
|
||||
"Fetching Data" = "Деректерді алу";
|
||||
"Please wait while fetching." = "Алу барысында күтіңіз.";
|
||||
"Start Reading" = "Оқуды бастау";
|
||||
"Chapters" = "Тараулар";
|
||||
"Completed" = "Аяқталды";
|
||||
"Drag to reorder" = "Ретін өзгерту үшін сүйреңіз";
|
||||
"Drag to reorder sections" = "Бөлімдердің ретін өзгерту үшін сүйреңіз";
|
||||
"Library View" = "Кітапхана көрінісі";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Кітапханаңызда көрсетілетін бөлімдерді баптаңыз. Бөлімдерді қайта реттеуге немесе толықтай өшіруге болады.";
|
||||
"Library Sections Order" = "Кітапхана бөлімдерінің реті";
|
||||
"Completion Percentage" = "Аяқталу пайызы";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Кейбір функциялар тек Sora және әдепкі ойнатқышта ғана қолжетімді, мысалы, ландшафтты мәжбүрлеу, жылдамдықты ұстау және уақытты өткізіп жіберу.\n\nАяқталу пайызы параметрі бейне соңына дейін қай жерде аяқталған деп белгіленетінін анықтайды (AniList және Trakt).";
|
||||
"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." = "Қолданба кэші суреттерді жылдам жүктеуге көмектеседі.\n\nDocuments қалтасын тазалау барлық жүктелген модульдерді өшіреді.\n\nҚолданба деректерін өшіру барлық баптауларыңызды және деректеріңізді өшіреді.";
|
||||
"Translators" = "Аудармашылар";
|
||||
"Paste URL" = "URL қою";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Серия атауы";
|
||||
"Content Source" = "Мазмұн көзі";
|
||||
"Watch Progress" = "Көру барысы";
|
||||
"Nothing to Continue Reading" = "Оқуды жалғастыруға ештеңе жоқ";
|
||||
"Your recently read novels will appear here" = "Соңғы оқылған романдар осында көрсетіледі";
|
||||
"No Bookmarks" = "Бетбелгілер жоқ";
|
||||
"Add bookmarks to this collection" = "Бұл жинаққа бетбелгілерді қосыңыз";
|
||||
"items" = "элементтер";
|
||||
"All Watching" = "Барлық көру";
|
||||
"No Reading History" = "Оқу тарихы жоқ";
|
||||
"Books you're reading will appear here" = "Сіз оқып жатқан кітаптар осында көрсетіледі";
|
||||
"Create Collection" = "Жинақ құру";
|
||||
"Collection Name" = "Жинақ атауы";
|
||||
"Rename Collection" = "Жинақты қайта атау";
|
||||
"Rename" = "Қайта атау";
|
||||
"Novel Title" = "Роман атауы";
|
||||
"Read Progress" = "Оқу барысы";
|
||||
"Date Created" = "Жасалған күні";
|
||||
"Name" = "Аты";
|
||||
"Item Count" = "Элементтер саны";
|
||||
"Date Added" = "Қосылған күні";
|
||||
"Title" = "Атауы";
|
||||
"Source" = "Дереккөз";
|
||||
"Search reading..." = "Оқуды іздеу...";
|
||||
"Search collections..." = "Жинақтарды іздеу...";
|
||||
"Search bookmarks..." = "Бетбелгілерді іздеу...";
|
||||
"%d items" = "%d элемент";
|
||||
"Fetching Data" = "Деректерді алу";
|
||||
"Please wait while fetching." = "Алу кезінде күтіңіз.";
|
||||
"Start Reading" = "Оқуды бастау";
|
||||
"Chapters" = "Тараулар";
|
||||
"Completed" = "Аяқталды";
|
||||
"Drag to reorder" = "Қайта реттеу үшін сүйреңіз";
|
||||
"Drag to reorder sections" = "Бөлімдерді қайта реттеу үшін сүйреңіз";
|
||||
"Library View" = "Кітапхана көрінісі";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Кітапханаңызда көрсетілетін бөлімдерді реттеңіз. Бөлімдерді қайта реттеуге немесе толықтай өшіруге болады.";
|
||||
"Library Sections Order" = "Кітапхана бөлімдерінің реті";
|
||||
"Completion Percentage" = "Аяқталу пайызы";
|
||||
"Translators" = "Аудармашылар";
|
||||
"Paste URL" = "URL қою";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Collections" = "Жинақтар";
|
||||
"Continue Reading" = "Оқуды жалғастыру";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Сақтық көшірме және қалпына келтіру";
|
||||
"Export Backup" = "Сақтық көшірмені экспорттау";
|
||||
"Import Backup" = "Сақтық көшірмені импорттау";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Ескерту: Бұл мүмкіндік әлі де тәжірибелік. Экспорт/импорттан кейін деректеріңізді тексеріңіз.";
|
||||
"Backup" = "Сақтық көшірме";
|
||||
|
|
@ -387,4 +387,102 @@
|
|||
"Recent searches" = "Recente zoekopdrachten";
|
||||
"me frfr" = "ik frfr";
|
||||
"Data" = "Gegevens";
|
||||
"Maximum Quality Available" = "Maximale beschikbare kwaliteit";
|
||||
"Maximum Quality Available" = "Maximale beschikbare kwaliteit";
|
||||
"DownloadCountFormat" = "%d van %d";
|
||||
"Error loading chapter" = "Fout bij het laden van hoofdstuk";
|
||||
"Font Size: %dpt" = "Lettergrootte: %dpt";
|
||||
"Line Spacing: %.1f" = "Regelafstand: %.1f";
|
||||
"Line Spacing" = "Regelafstand";
|
||||
"Margin: %dpx" = "Marge: %dpx";
|
||||
"Margin" = "Marge";
|
||||
"Auto Scroll Speed" = "Automatische scrollsnelheid";
|
||||
"Speed" = "Snelheid";
|
||||
"Speed: %.1fx" = "Snelheid: %.1fx";
|
||||
"Matched %@: %@" = "Overeenkomst %@: %@";
|
||||
"Enter the AniList ID for this series" = "Voer de AniList-ID voor deze serie in";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Collectie aanmaken";
|
||||
"Collection Name" = "Collectienaam";
|
||||
"Rename Collection" = "Collectie hernoemen";
|
||||
"Rename" = "Hernoemen";
|
||||
"All Reading" = "Alles wat je leest";
|
||||
"Recently Added" = "Recent toegevoegd";
|
||||
"Novel Title" = "Roman titel";
|
||||
"Read Progress" = "Leesvoortgang";
|
||||
"Date Created" = "Aanmaakdatum";
|
||||
"Name" = "Naam";
|
||||
"Item Count" = "Aantal items";
|
||||
"Date Added" = "Datum toegevoegd";
|
||||
"Title" = "Titel";
|
||||
"Source" = "Bron";
|
||||
"Search reading..." = "Zoek in lezen...";
|
||||
"Search collections..." = "Zoek in collecties...";
|
||||
"Search bookmarks..." = "Zoek in bladwijzers...";
|
||||
"%d items" = "%d items";
|
||||
"Fetching Data" = "Gegevens ophalen";
|
||||
"Please wait while fetching." = "Even geduld tijdens het ophalen.";
|
||||
"Start Reading" = "Begin met lezen";
|
||||
"Chapters" = "Hoofdstukken";
|
||||
"Completed" = "Voltooid";
|
||||
"Drag to reorder" = "Sleep om te herschikken";
|
||||
"Drag to reorder sections" = "Sleep om secties te herschikken";
|
||||
"Library View" = "Bibliotheekweergave";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Pas de secties aan die in je bibliotheek worden weergegeven. Je kunt secties herschikken of volledig uitschakelen.";
|
||||
"Library Sections Order" = "Volgorde van bibliotheeksecties";
|
||||
"Completion Percentage" = "Voltooiingspercentage";
|
||||
"Translators" = "Vertalers";
|
||||
"Paste URL" = "URL plakken";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Serietitel";
|
||||
"Content Source" = "Inhoudsbron";
|
||||
"Watch Progress" = "Kijkvoortgang";
|
||||
"Nothing to Continue Reading" = "Niets om verder te lezen";
|
||||
"Your recently read novels will appear here" = "Je recent gelezen romans verschijnen hier";
|
||||
"No Bookmarks" = "Geen bladwijzers";
|
||||
"Add bookmarks to this collection" = "Voeg bladwijzers toe aan deze collectie";
|
||||
"items" = "items";
|
||||
"All Watching" = "Alles wat je kijkt";
|
||||
"No Reading History" = "Geen leeshistorie";
|
||||
"Books you're reading will appear here" = "Boeken die je leest verschijnen hier";
|
||||
"Create Collection" = "Collectie aanmaken";
|
||||
"Collection Name" = "Collectienaam";
|
||||
"Rename Collection" = "Collectie hernoemen";
|
||||
"Rename" = "Hernoemen";
|
||||
"Novel Title" = "Roman titel";
|
||||
"Read Progress" = "Leesvoortgang";
|
||||
"Date Created" = "Aanmaakdatum";
|
||||
"Name" = "Naam";
|
||||
"Item Count" = "Aantal items";
|
||||
"Date Added" = "Datum toegevoegd";
|
||||
"Title" = "Titel";
|
||||
"Source" = "Bron";
|
||||
"Search reading..." = "Zoek in lezen...";
|
||||
"Search collections..." = "Zoek in collecties...";
|
||||
"Search bookmarks..." = "Zoek in bladwijzers...";
|
||||
"%d items" = "%d items";
|
||||
"Fetching Data" = "Gegevens ophalen";
|
||||
"Please wait while fetching." = "Even geduld tijdens het ophalen.";
|
||||
"Start Reading" = "Begin met lezen";
|
||||
"Chapters" = "Hoofdstukken";
|
||||
"Completed" = "Voltooid";
|
||||
"Drag to reorder" = "Sleep om te herschikken";
|
||||
"Drag to reorder sections" = "Sleep om secties te herschikken";
|
||||
"Library View" = "Bibliotheekweergave";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Pas de secties aan die in je bibliotheek worden weergegeven. Je kunt secties herschikken of volledig uitschakelen.";
|
||||
"Library Sections Order" = "Volgorde van bibliotheeksecties";
|
||||
"Completion Percentage" = "Voltooiingspercentage";
|
||||
"Translators" = "Vertalers";
|
||||
"Paste URL" = "URL plakken";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Collections" = "Collecties";
|
||||
"Continue Reading" = "Doorgaan met lezen";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Back-up & Herstellen";
|
||||
"Export Backup" = "Back-up exporteren";
|
||||
"Import Backup" = "Back-up importeren";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Let op: Deze functie is nog experimenteel. Controleer je gegevens na export/import.";
|
||||
"Backup" = "Back-up";
|
||||
|
|
|
|||
|
|
@ -381,4 +381,108 @@
|
|||
"Storage Management" = "Lagringsadministrasjon";
|
||||
"Storage Used" = "Brukt Lagring";
|
||||
"Library cleared successfully" = "Bibliotek tømt";
|
||||
"All downloads deleted successfully" = "Alle nedlastinger slettet";
|
||||
"All downloads deleted successfully" = "Alle nedlastinger slettet";
|
||||
|
||||
/* New keys from English localization */
|
||||
"DownloadCountFormat" = "%d av %d";
|
||||
"Error loading chapter" = "Feil ved lasting av kapittel";
|
||||
"Font Size: %dpt" = "Skriftstorleik: %dpt";
|
||||
"Line Spacing: %.1f" = "Linjeavstand: %.1f";
|
||||
"Line Spacing" = "Linjeavstand";
|
||||
"Margin: %dpx" = "Marg: %dpx";
|
||||
"Margin" = "Marg";
|
||||
"Auto Scroll Speed" = "Fart på automatisk rulling";
|
||||
"Speed" = "Fart";
|
||||
"Speed: %.1fx" = "Fart: %.1fx";
|
||||
"Matched %@: %@" = "Treff %@: %@";
|
||||
"Enter the AniList ID for this series" = "Skriv inn AniList-ID for denne serien";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Opprett samling";
|
||||
"Collection Name" = "Samlingens navn";
|
||||
"Rename Collection" = "Gi nytt navn til samling";
|
||||
"Rename" = "Gi nytt navn";
|
||||
"All Reading" = "All lesing";
|
||||
"Recently Added" = "Nylig lagt til";
|
||||
"Novel Title" = "Roman tittel";
|
||||
"Read Progress" = "Lesefremgang";
|
||||
"Date Created" = "Opprettelsesdato";
|
||||
"Name" = "Navn";
|
||||
"Item Count" = "Antall elementer";
|
||||
"Date Added" = "Dato lagt til";
|
||||
"Title" = "Tittel";
|
||||
"Source" = "Kilde";
|
||||
"Search reading..." = "Søk i lesing...";
|
||||
"Search collections..." = "Søk i samlinger...";
|
||||
"Search bookmarks..." = "Søk i bokmerker...";
|
||||
"%d items" = "%d elementer";
|
||||
"Fetching Data" = "Henter data";
|
||||
"Please wait while fetching." = "Vennligst vent mens det hentes.";
|
||||
"Start Reading" = "Start lesing";
|
||||
"Chapters" = "Kapitler";
|
||||
"Completed" = "Fullført";
|
||||
"Drag to reorder" = "Dra for å endre rekkefølge";
|
||||
"Drag to reorder sections" = "Dra for å endre rekkefølge på seksjoner";
|
||||
"Library View" = "Bibliotekvisning";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Tilpass seksjonene som vises i biblioteket ditt. Du kan endre rekkefølge eller deaktivere seksjoner helt.";
|
||||
"Library Sections Order" = "Rekkefølge på bibliotekseksjoner";
|
||||
"Completion Percentage" = "Fullføringsprosent";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Noen funksjoner er begrenset til Sora- og standardspilleren, som tvunget landskap, holdhastighet og tilpassede tidshopp.\n\nFullføringsprosenten bestemmer når før slutten av en video appen markerer den som fullført på AniList og Trakt.";
|
||||
"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." = "Appens cache hjelper med å laste inn bilder raskere.\n\nÅ tømme Dokumenter-mappen sletter alle nedlastede moduler.\n\nÅ slette appdata sletter alle innstillinger og data.";
|
||||
"Translators" = "Oversettere";
|
||||
"Paste URL" = "Lim inn URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Serietittel";
|
||||
"Content Source" = "Innhaldskjelde";
|
||||
"Watch Progress" = "Framdrift for vising";
|
||||
"Recent searches" = "Nylege søk";
|
||||
"All Reading" = "Alt du les";
|
||||
"Nothing to Continue Reading" = "Ingenting å fortsetje å lese";
|
||||
"Your recently read novels will appear here" = "Dine nyleg lesne romanar vil visast her";
|
||||
"No Bookmarks" = "Ingen bokmerke";
|
||||
"Add bookmarks to this collection" = "Legg til bokmerke i denne samlinga";
|
||||
"items" = "element";
|
||||
"All Watching" = "Alt du ser på";
|
||||
"No Reading History" = "Ingen leseloggar";
|
||||
"Books you're reading will appear here" = "Bøker du les vil visast her";
|
||||
"Create Collection" = "Opprett samling";
|
||||
"Collection Name" = "Samlingnamn";
|
||||
"Rename Collection" = "Endre namn på samling";
|
||||
"Rename" = "Endre namn";
|
||||
"Novel Title" = "Roman tittel";
|
||||
"Read Progress" = "Leseframdrift";
|
||||
"Date Created" = "Oppretta dato";
|
||||
"Name" = "Namn";
|
||||
"Item Count" = "Tal på element";
|
||||
"Date Added" = "Dato lagt til";
|
||||
"Title" = "Tittel";
|
||||
"Source" = "Kjelde";
|
||||
"Search reading..." = "Søk i lesing...";
|
||||
"Search collections..." = "Søk i samlingar...";
|
||||
"Search bookmarks..." = "Søk i bokmerke...";
|
||||
"%d items" = "%d element";
|
||||
"Fetching Data" = "Hentar data";
|
||||
"Please wait while fetching." = "Vent medan data vert henta.";
|
||||
"Start Reading" = "Start lesing";
|
||||
"Chapters" = "Kapittel";
|
||||
"Completed" = "Fullført";
|
||||
"Drag to reorder" = "Dra for å endre rekkefølgje";
|
||||
"Drag to reorder sections" = "Dra for å endre rekkefølgje på seksjonar";
|
||||
"Library View" = "Bibliotekvising";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Tilpass seksjonane som vert viste i biblioteket ditt. Du kan endre rekkefølgje eller slå dei heilt av.";
|
||||
"Library Sections Order" = "Rekkefølgje på bibliotekseksjonar";
|
||||
"Completion Percentage" = "Fullføringsprosent";
|
||||
"Translators" = "Omsetjarar";
|
||||
"Paste URL" = "Lim inn URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Collections" = "Samlingar";
|
||||
"Continue Reading" = "Hald fram med å lese";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Sikkerhetskopi og gjenoppretting";
|
||||
"Export Backup" = "Eksporter sikkerhetskopi";
|
||||
"Import Backup" = "Importer sikkerhetskopi";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Merk: Denne funksjonen er fortsatt eksperimentell. Vennligst dobbeltsjekk dataene dine etter eksport/import.";
|
||||
"Backup" = "Sikkerhetskopi";
|
||||
|
|
@ -405,4 +405,108 @@ For episode metadata, it refers to the episode thumbnail and title, since someti
|
|||
"Data" = "Данные";
|
||||
|
||||
/* New string */
|
||||
"Maximum Quality Available" = "Максимальное доступное качество";
|
||||
"Maximum Quality Available" = "Максимальное доступное качество";
|
||||
|
||||
/* Additional translations */
|
||||
"DownloadCountFormat" = "%d из %d";
|
||||
"Error loading chapter" = "Ошибка загрузки главы";
|
||||
"Font Size: %dpt" = "Размер шрифта: %dpt";
|
||||
"Line Spacing: %.1f" = "Межстрочный интервал: %.1f";
|
||||
"Line Spacing" = "Межстрочный интервал";
|
||||
"Margin: %dpx" = "Поле: %dpx";
|
||||
"Margin" = "Поле";
|
||||
"Auto Scroll Speed" = "Скорость автопрокрутки";
|
||||
"Speed" = "Скорость";
|
||||
"Speed: %.1fx" = "Скорость: %.1fx";
|
||||
"Matched %@: %@" = "Совпадение %@: %@";
|
||||
"Enter the AniList ID for this series" = "Введите AniList ID для этой серии";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Создать коллекцию";
|
||||
"Collection Name" = "Название коллекции";
|
||||
"Rename Collection" = "Переименовать коллекцию";
|
||||
"Rename" = "Переименовать";
|
||||
"All Reading" = "Все чтения";
|
||||
"Recently Added" = "Недавно добавленные";
|
||||
"Novel Title" = "Название романа";
|
||||
"Read Progress" = "Прогресс чтения";
|
||||
"Date Created" = "Дата создания";
|
||||
"Name" = "Имя";
|
||||
"Item Count" = "Количество элементов";
|
||||
"Date Added" = "Дата добавления";
|
||||
"Title" = "Заголовок";
|
||||
"Source" = "Источник";
|
||||
"Search reading..." = "Поиск по чтению...";
|
||||
"Search collections..." = "Поиск по коллекциям...";
|
||||
"Search bookmarks..." = "Поиск по закладкам...";
|
||||
"%d items" = "%d элементов";
|
||||
"Fetching Data" = "Получение данных";
|
||||
"Please wait while fetching." = "Пожалуйста, подождите, идет получение данных.";
|
||||
"Start Reading" = "Начать чтение";
|
||||
"Chapters" = "Главы";
|
||||
"Completed" = "Завершено";
|
||||
"Drag to reorder" = "Перетащите для изменения порядка";
|
||||
"Drag to reorder sections" = "Перетащите для изменения порядка разделов";
|
||||
"Library View" = "Вид библиотеки";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Настройте разделы, отображаемые в вашей библиотеке. Вы можете изменить порядок разделов или полностью их отключить.";
|
||||
"Library Sections Order" = "Порядок разделов библиотеки";
|
||||
"Completion Percentage" = "Процент завершения";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Некоторые функции доступны только в Sora и стандартном плеере, такие как принудительный ландшафт, удержание скорости и пользовательские интервалы пропуска.\n\nНастройка процента завершения определяет, в какой момент до конца видео приложение отметит его как завершенное на AniList и Trakt.";
|
||||
"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." = "Кэш приложения помогает быстрее загружать изображения.\n\nОчистка папки Documents удалит все загруженные модули.\n\nСтирание данных приложения удалит все ваши настройки и данные.";
|
||||
"Translators" = "Переводчики";
|
||||
"Paste URL" = "Вставить URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Название серии";
|
||||
"Content Source" = "Источник контента";
|
||||
"Watch Progress" = "Прогресс просмотра";
|
||||
"Recent searches" = "Недавние поиски";
|
||||
"All Reading" = "Всё для чтения";
|
||||
"Nothing to Continue Reading" = "Нечего продолжать читать";
|
||||
"Your recently read novels will appear here" = "Ваши недавно прочитанные романы появятся здесь";
|
||||
"No Bookmarks" = "Нет закладок";
|
||||
"Add bookmarks to this collection" = "Добавьте закладки в эту коллекцию";
|
||||
"items" = "элементы";
|
||||
"All Watching" = "Всё для просмотра";
|
||||
"No Reading History" = "Нет истории чтения";
|
||||
"Books you're reading will appear here" = "Книги, которые вы читаете, появятся здесь";
|
||||
"Create Collection" = "Создать коллекцию";
|
||||
"Collection Name" = "Название коллекции";
|
||||
"Rename Collection" = "Переименовать коллекцию";
|
||||
"Rename" = "Переименовать";
|
||||
"Novel Title" = "Название романа";
|
||||
"Read Progress" = "Прогресс чтения";
|
||||
"Date Created" = "Дата создания";
|
||||
"Name" = "Имя";
|
||||
"Item Count" = "Количество элементов";
|
||||
"Date Added" = "Дата добавления";
|
||||
"Title" = "Заголовок";
|
||||
"Source" = "Источник";
|
||||
"Search reading..." = "Поиск по чтению...";
|
||||
"Search collections..." = "Поиск по коллекциям...";
|
||||
"Search bookmarks..." = "Поиск по закладкам...";
|
||||
"%d items" = "%d элементов";
|
||||
"Fetching Data" = "Получение данных";
|
||||
"Please wait while fetching." = "Пожалуйста, подождите, идет получение данных.";
|
||||
"Start Reading" = "Начать чтение";
|
||||
"Chapters" = "Главы";
|
||||
"Completed" = "Завершено";
|
||||
"Drag to reorder" = "Перетащите для изменения порядка";
|
||||
"Drag to reorder sections" = "Перетащите для изменения порядка разделов";
|
||||
"Library View" = "Вид библиотеки";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Настройте разделы, отображаемые в вашей библиотеке. Вы можете изменить порядок разделов или полностью их отключить.";
|
||||
"Library Sections Order" = "Порядок разделов библиотеки";
|
||||
"Completion Percentage" = "Процент завершения";
|
||||
"Translators" = "Переводчики";
|
||||
"Paste URL" = "Вставить URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Collections" = "Коллекции";
|
||||
"Continue Reading" = "Продолжить чтение";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Резервное копирование и восстановление";
|
||||
"Export Backup" = "Экспорт резервной копии";
|
||||
"Import Backup" = "Импорт резервной копии";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Внимание: Эта функция все еще экспериментальная. Пожалуйста, проверьте свои данные после экспорта/импорта.";
|
||||
"Backup" = "Резервная копия";
|
||||
|
|
@ -404,3 +404,105 @@ For episode metadata, it refers to the episode thumbnail and title, since someti
|
|||
"me frfr" = "me frfr";
|
||||
"Data" = "Dáta";
|
||||
"Maximum Quality Available" = "Maximálna dostupná kvalita";
|
||||
|
||||
/* New additions */
|
||||
"DownloadCountFormat" = "%d z %d";
|
||||
"Error loading chapter" = "Chyba pri načítaní kapitoly";
|
||||
"Font Size: %dpt" = "Veľkosť písma: %dpt";
|
||||
"Line Spacing: %.1f" = "Riadkovanie: %.1f";
|
||||
"Line Spacing" = "Riadkovanie";
|
||||
"Margin: %dpx" = "Okraj: %dpx";
|
||||
"Margin" = "Okraj";
|
||||
"Auto Scroll Speed" = "Rýchlosť automatického posúvania";
|
||||
"Speed" = "Rýchlosť";
|
||||
"Speed: %.1fx" = "Rýchlosť: %.1fx";
|
||||
"Matched %@: %@" = "Zhoda %@: %@";
|
||||
"Enter the AniList ID for this series" = "Zadajte AniList ID pre túto sériu";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Vytvoriť kolekciu";
|
||||
"Collection Name" = "Názov kolekcie";
|
||||
"Rename Collection" = "Premenovať kolekciu";
|
||||
"Rename" = "Premenovať";
|
||||
"All Reading" = "Všetko na čítanie";
|
||||
"Recently Added" = "Nedávno pridané";
|
||||
"Novel Title" = "Názov románu";
|
||||
"Read Progress" = "Priebeh čítania";
|
||||
"Date Created" = "Dátum vytvorenia";
|
||||
"Name" = "Meno";
|
||||
"Item Count" = "Počet položiek";
|
||||
"Date Added" = "Dátum pridania";
|
||||
"Title" = "Názov";
|
||||
"Source" = "Zdroj";
|
||||
"Search reading..." = "Hľadať v čítaní...";
|
||||
"Search collections..." = "Hľadať v kolekciách...";
|
||||
"Search bookmarks..." = "Hľadať v záložkách...";
|
||||
"%d items" = "%d položiek";
|
||||
"Fetching Data" = "Načítavanie údajov";
|
||||
"Please wait while fetching." = "Počkajte, kým sa údaje načítajú.";
|
||||
"Start Reading" = "Začať čítať";
|
||||
"Chapters" = "Kapitoly";
|
||||
"Completed" = "Dokončené";
|
||||
"Drag to reorder" = "Potiahnite na zmenu poradia";
|
||||
"Drag to reorder sections" = "Potiahnite na zmenu poradia sekcií";
|
||||
"Library View" = "Zobrazenie knižnice";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Prispôsobte sekcie zobrazené vo vašej knižnici. Môžete zmeniť ich poradie alebo ich úplne vypnúť.";
|
||||
"Library Sections Order" = "Poradie sekcií knižnice";
|
||||
"Completion Percentage" = "Percento dokončenia";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Niektoré funkcie sú obmedzené na Sora a predvolený prehrávač, ako je vynútená krajina, podržanie rýchlosti a vlastné intervaly preskočenia.\n\nNastavenie percenta dokončenia určuje, v ktorom bode pred koncom videa aplikácia označí ako dokončené na AniList a Trakt.";
|
||||
"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." = "Vyrovnávacia pamäť aplikácie pomáha rýchlejšiemu načítaniu obrázkov.\n\nVymazanie priečinka Documents odstráni všetky stiahnuté moduly.\n\nVymazanie údajov aplikácie odstráni všetky vaše nastavenia a údaje.";
|
||||
"Translators" = "Prekladatelia";
|
||||
"Paste URL" = "Vložiť URL";
|
||||
|
||||
/* New additions */
|
||||
"Series Title" = "Názov série";
|
||||
"Content Source" = "Zdroj obsahu";
|
||||
"Watch Progress" = "Priebeh sledovania";
|
||||
"Nothing to Continue Reading" = "Nič na pokračovanie v čítaní";
|
||||
"Your recently read novels will appear here" = "Vaše nedávno čítané romány sa zobrazia tu";
|
||||
"No Bookmarks" = "Žiadne záložky";
|
||||
"Add bookmarks to this collection" = "Pridajte záložky do tejto kolekcie";
|
||||
"items" = "položky";
|
||||
"All Watching" = "Všetko na sledovanie";
|
||||
"No Reading History" = "Žiadna história čítania";
|
||||
"Books you're reading will appear here" = "Knihy, ktoré čítate, sa zobrazia tu";
|
||||
"Create Collection" = "Vytvoriť kolekciu";
|
||||
"Collection Name" = "Názov kolekcie";
|
||||
"Rename Collection" = "Premenovať kolekciu";
|
||||
"Rename" = "Premenovať";
|
||||
"Novel Title" = "Názov románu";
|
||||
"Read Progress" = "Priebeh čítania";
|
||||
"Date Created" = "Dátum vytvorenia";
|
||||
"Name" = "Meno";
|
||||
"Item Count" = "Počet položiek";
|
||||
"Date Added" = "Dátum pridania";
|
||||
"Title" = "Názov";
|
||||
"Source" = "Zdroj";
|
||||
"Search reading..." = "Hľadať v čítaní...";
|
||||
"Search collections..." = "Hľadať v kolekciách...";
|
||||
"Search bookmarks..." = "Hľadať v záložkách...";
|
||||
"%d items" = "%d položiek";
|
||||
"Fetching Data" = "Načítavanie údajov";
|
||||
"Please wait while fetching." = "Počkajte, kým sa údaje načítajú.";
|
||||
"Start Reading" = "Začať čítať";
|
||||
"Chapters" = "Kapitoly";
|
||||
"Completed" = "Dokončené";
|
||||
"Drag to reorder" = "Potiahnite na zmenu poradia";
|
||||
"Drag to reorder sections" = "Potiahnite na zmenu poradia sekcií";
|
||||
"Library View" = "Zobrazenie knižnice";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Prispôsobte sekcie zobrazené vo vašej knižnici. Môžete zmeniť ich poradie alebo ich úplne vypnúť.";
|
||||
"Library Sections Order" = "Poradie sekcií knižnice";
|
||||
"Completion Percentage" = "Percento dokončenia";
|
||||
"Translators" = "Prekladatelia";
|
||||
"Paste URL" = "Vložiť URL";
|
||||
|
||||
/* New additions */
|
||||
"Collections" = "Kolekcie";
|
||||
"Continue Reading" = "Pokračovať v čítaní";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Záloha a obnovenie";
|
||||
"Export Backup" = "Exportovať zálohu";
|
||||
"Import Backup" = "Importovať zálohu";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Upozornenie: Táto funkcia je stále experimentálna. Po exporte/importe si prosím skontrolujte svoje údaje.";
|
||||
"Backup" = "Záloha";
|
||||
|
|
|
|||
|
|
@ -381,4 +381,107 @@
|
|||
"Storage Management" = "Lagringshantering";
|
||||
"Storage Used" = "Använt Lagringsutrymme";
|
||||
"Library cleared successfully" = "Biblioteket rensat";
|
||||
"All downloads deleted successfully" = "Alla nedladdningar borttagna";
|
||||
"All downloads deleted successfully" = "Alla nedladdningar borttagna";
|
||||
|
||||
/* New keys from English localization */
|
||||
"DownloadCountFormat" = "%d av %d";
|
||||
"Error loading chapter" = "Fel vid inläsning av kapitel";
|
||||
"Font Size: %dpt" = "Teckenstorlek: %dpt";
|
||||
"Line Spacing: %.1f" = "Radavstånd: %.1f";
|
||||
"Line Spacing" = "Radavstånd";
|
||||
"Margin: %dpx" = "Marginal: %dpx";
|
||||
"Margin" = "Marginal";
|
||||
"Auto Scroll Speed" = "Automatisk rullningshastighet";
|
||||
"Speed" = "Hastighet";
|
||||
"Speed: %.1fx" = "Hastighet: %.1fx";
|
||||
"Matched %@: %@" = "Matchning %@: %@";
|
||||
"Enter the AniList ID for this series" = "Ange AniList-ID för denna serie";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Create Collection" = "Skapa samling";
|
||||
"Collection Name" = "Samlingens namn";
|
||||
"Rename Collection" = "Byt namn på samling";
|
||||
"Rename" = "Byt namn";
|
||||
"All Reading" = "All läsning";
|
||||
"Recently Added" = "Nyligen tillagda";
|
||||
"Novel Title" = "Romanens titel";
|
||||
"Read Progress" = "Läsningsframsteg";
|
||||
"Date Created" = "Skapad datum";
|
||||
"Name" = "Namn";
|
||||
"Item Count" = "Antal objekt";
|
||||
"Date Added" = "Datum tillagt";
|
||||
"Title" = "Titel";
|
||||
"Source" = "Källa";
|
||||
"Search reading..." = "Sök i läsning...";
|
||||
"Search collections..." = "Sök i samlingar...";
|
||||
"Search bookmarks..." = "Sök i bokmärken...";
|
||||
"%d items" = "%d objekt";
|
||||
"Fetching Data" = "Hämtar data";
|
||||
"Please wait while fetching." = "Vänligen vänta under hämtning.";
|
||||
"Start Reading" = "Börja läsa";
|
||||
"Chapters" = "Kapitel";
|
||||
"Completed" = "Slutförd";
|
||||
"Drag to reorder" = "Dra för att ändra ordning";
|
||||
"Drag to reorder sections" = "Dra för att ändra ordning på sektioner";
|
||||
"Library View" = "Biblioteksvisning";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Anpassa sektionerna som visas i ditt bibliotek. Du kan ändra ordning eller inaktivera sektioner helt.";
|
||||
"Library Sections Order" = "Ordning på bibliotekets sektioner";
|
||||
"Completion Percentage" = "Slutförandeprocent";
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.\n\nThe completion percentage setting determines at what point before the end of a video the app will mark it as completed on AniList and Trakt." = "Vissa funktioner är begränsade till Sora- och standardspelaren, såsom tvingat landskap, hållhastighet och anpassade tidshopp.\n\nInställningen för slutförandeprocent avgör vid vilken punkt före slutet av en video appen markerar den som slutförd på AniList och Trakt.";
|
||||
"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." = "Appens cache hjälper till att ladda bilder snabbare.\n\nAtt rensa mappen Dokument tar bort alla nedladdade moduler.\n\nAtt radera appdata tar bort alla dina inställningar och data.";
|
||||
"Translators" = "Översättare";
|
||||
"Paste URL" = "Klistra in URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Series Title" = "Serietitel";
|
||||
"Content Source" = "Innehållskälla";
|
||||
"Watch Progress" = "Tittarframsteg";
|
||||
"Recent searches" = "Senaste sökningar";
|
||||
"Nothing to Continue Reading" = "Inget att fortsätta läsa";
|
||||
"Your recently read novels will appear here" = "Dina nyligen lästa romaner visas här";
|
||||
"No Bookmarks" = "Inga bokmärken";
|
||||
"Add bookmarks to this collection" = "Lägg till bokmärken i denna samling";
|
||||
"items" = "objekt";
|
||||
"All Watching" = "Allt du tittar på";
|
||||
"No Reading History" = "Ingen läshistorik";
|
||||
"Books you're reading will appear here" = "Böcker du läser visas här";
|
||||
"Create Collection" = "Skapa samling";
|
||||
"Collection Name" = "Samlingens namn";
|
||||
"Rename Collection" = "Byt namn på samling";
|
||||
"Rename" = "Byt namn";
|
||||
"Novel Title" = "Roman titel";
|
||||
"Read Progress" = "Läsframsteg";
|
||||
"Date Created" = "Skapad datum";
|
||||
"Name" = "Namn";
|
||||
"Item Count" = "Antal objekt";
|
||||
"Date Added" = "Datum tillagt";
|
||||
"Title" = "Titel";
|
||||
"Source" = "Källa";
|
||||
"Search reading..." = "Sök i läsning...";
|
||||
"Search collections..." = "Sök i samlingar...";
|
||||
"Search bookmarks..." = "Sök i bokmärken...";
|
||||
"%d items" = "%d objekt";
|
||||
"Fetching Data" = "Hämtar data";
|
||||
"Please wait while fetching." = "Vänligen vänta medan data hämtas.";
|
||||
"Start Reading" = "Börja läsa";
|
||||
"Chapters" = "Kapitel";
|
||||
"Completed" = "Slutförd";
|
||||
"Drag to reorder" = "Dra för att ändra ordning";
|
||||
"Drag to reorder sections" = "Dra för att ändra ordning på sektioner";
|
||||
"Library View" = "Biblioteksvy";
|
||||
"Customize the sections shown in your library. You can reorder sections or disable them completely." = "Anpassa sektionerna som visas i ditt bibliotek. Du kan ändra ordning eller inaktivera dem helt.";
|
||||
"Library Sections Order" = "Bibliotekssektioners ordning";
|
||||
"Completion Percentage" = "Slutförandeprocent";
|
||||
"Translators" = "Översättare";
|
||||
"Paste URL" = "Klistra in URL";
|
||||
|
||||
/* Added missing localizations */
|
||||
"Collections" = "Samlingar";
|
||||
"Continue Reading" = "Fortsätt läsa";
|
||||
|
||||
/* Backup & Restore */
|
||||
"Backup & Restore" = "Säkerhetskopiera & Återställ";
|
||||
"Export Backup" = "Exportera säkerhetskopia";
|
||||
"Import Backup" = "Importera säkerhetskopia";
|
||||
"Notice: This feature is still experimental. Please double-check your data after import/export." = "Observera: Denna funktion är fortfarande experimentell. Kontrollera dina data efter export/import.";
|
||||
"Backup" = "Säkerhetskopia";
|
||||
48
Sora/MediaUtils/ContinueWatching/ContinueReadingItem.swift
Normal file
48
Sora/MediaUtils/ContinueWatching/ContinueReadingItem.swift
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// ContinueReadingItem.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 26/06/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ContinueReadingItem: Identifiable, Codable {
|
||||
let id: UUID
|
||||
let mediaTitle: String
|
||||
let chapterTitle: String
|
||||
let chapterNumber: Int
|
||||
let imageUrl: String
|
||||
let href: String
|
||||
let moduleId: String
|
||||
let progress: Double
|
||||
let totalChapters: Int
|
||||
let lastReadDate: Date
|
||||
let cachedHtml: String?
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
mediaTitle: String,
|
||||
chapterTitle: String,
|
||||
chapterNumber: Int,
|
||||
imageUrl: String,
|
||||
href: String,
|
||||
moduleId: String,
|
||||
progress: Double = 0.0,
|
||||
totalChapters: Int = 0,
|
||||
lastReadDate: Date = Date(),
|
||||
cachedHtml: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.mediaTitle = mediaTitle
|
||||
self.chapterTitle = chapterTitle
|
||||
self.chapterNumber = chapterNumber
|
||||
self.imageUrl = imageUrl
|
||||
self.href = href
|
||||
self.moduleId = moduleId
|
||||
self.progress = progress
|
||||
self.totalChapters = totalChapters
|
||||
self.lastReadDate = lastReadDate
|
||||
self.cachedHtml = cachedHtml
|
||||
}
|
||||
}
|
||||
275
Sora/MediaUtils/ContinueWatching/ContinueReadingManager.swift
Normal file
275
Sora/MediaUtils/ContinueWatching/ContinueReadingManager.swift
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
//
|
||||
// ContinueReadingManager.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 26/06/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ContinueReadingManager {
|
||||
static let shared = ContinueReadingManager()
|
||||
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let continueReadingKey = "continueReadingItems"
|
||||
|
||||
private init() {}
|
||||
|
||||
func extractTitleFromURL(_ url: String) -> String? {
|
||||
guard let url = URL(string: url) else { return nil }
|
||||
|
||||
let pathComponents = url.pathComponents
|
||||
|
||||
for (index, component) in pathComponents.enumerated() {
|
||||
if component == "book" || component == "novel" {
|
||||
if index + 1 < pathComponents.count {
|
||||
let bookTitle = pathComponents[index + 1]
|
||||
.replacingOccurrences(of: "-", with: " ")
|
||||
.replacingOccurrences(of: "_", with: " ")
|
||||
.capitalized
|
||||
|
||||
if !bookTitle.isEmpty {
|
||||
return bookTitle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchItems() -> [ContinueReadingItem] {
|
||||
guard let data = userDefaults.data(forKey: continueReadingKey) else {
|
||||
Logger.shared.log("No continue reading items found in UserDefaults", type: "Debug")
|
||||
return []
|
||||
}
|
||||
|
||||
do {
|
||||
let items = try JSONDecoder().decode([ContinueReadingItem].self, from: data)
|
||||
Logger.shared.log("Fetched \(items.count) continue reading items", type: "Debug")
|
||||
|
||||
for (index, item) in items.enumerated() {
|
||||
Logger.shared.log("Item \(index): \(item.mediaTitle), Image URL: \(item.imageUrl)", type: "Debug")
|
||||
}
|
||||
|
||||
return items.sorted(by: { $0.lastReadDate > $1.lastReadDate })
|
||||
} catch {
|
||||
Logger.shared.log("Error decoding continue reading items: \(error)", type: "Error")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func save(item: ContinueReadingItem, htmlContent: String? = nil) {
|
||||
var items = fetchItems()
|
||||
|
||||
items.removeAll { $0.href == item.href }
|
||||
|
||||
if item.progress >= 0.98 {
|
||||
userDefaults.set(item.progress, forKey: "readingProgress_\(item.href)")
|
||||
|
||||
do {
|
||||
let data = try JSONEncoder().encode(items)
|
||||
userDefaults.set(data, forKey: continueReadingKey)
|
||||
} catch {
|
||||
Logger.shared.log("Error encoding continue reading items: \(error)", type: "Error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var updatedItem = item
|
||||
if item.mediaTitle.contains("-") && item.mediaTitle.count >= 30 || item.mediaTitle.contains("Unknown") {
|
||||
if let betterTitle = extractTitleFromURL(item.href) {
|
||||
updatedItem = ContinueReadingItem(
|
||||
id: item.id,
|
||||
mediaTitle: betterTitle,
|
||||
chapterTitle: item.chapterTitle,
|
||||
chapterNumber: item.chapterNumber,
|
||||
imageUrl: item.imageUrl,
|
||||
href: item.href,
|
||||
moduleId: item.moduleId,
|
||||
progress: item.progress,
|
||||
totalChapters: item.totalChapters,
|
||||
lastReadDate: item.lastReadDate,
|
||||
cachedHtml: htmlContent ?? item.cachedHtml
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.shared.log("Incoming item image URL: \(updatedItem.imageUrl)", type: "Debug")
|
||||
|
||||
if updatedItem.imageUrl.isEmpty {
|
||||
let defaultImageUrl = "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/novel_cover.jpg"
|
||||
updatedItem = ContinueReadingItem(
|
||||
id: updatedItem.id,
|
||||
mediaTitle: updatedItem.mediaTitle,
|
||||
chapterTitle: updatedItem.chapterTitle,
|
||||
chapterNumber: updatedItem.chapterNumber,
|
||||
imageUrl: defaultImageUrl,
|
||||
href: updatedItem.href,
|
||||
moduleId: updatedItem.moduleId,
|
||||
progress: updatedItem.progress,
|
||||
totalChapters: updatedItem.totalChapters,
|
||||
lastReadDate: updatedItem.lastReadDate,
|
||||
cachedHtml: htmlContent ?? updatedItem.cachedHtml
|
||||
)
|
||||
Logger.shared.log("Using default image URL: \(defaultImageUrl)", type: "Debug")
|
||||
}
|
||||
|
||||
if !updatedItem.imageUrl.isEmpty {
|
||||
if URL(string: updatedItem.imageUrl) == nil {
|
||||
Logger.shared.log("Invalid image URL format: \(updatedItem.imageUrl)", type: "Warning")
|
||||
|
||||
if let encodedUrl = updatedItem.imageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
|
||||
let _ = URL(string: encodedUrl) {
|
||||
updatedItem = ContinueReadingItem(
|
||||
id: updatedItem.id,
|
||||
mediaTitle: updatedItem.mediaTitle,
|
||||
chapterTitle: updatedItem.chapterTitle,
|
||||
chapterNumber: updatedItem.chapterNumber,
|
||||
imageUrl: encodedUrl,
|
||||
href: updatedItem.href,
|
||||
moduleId: updatedItem.moduleId,
|
||||
progress: updatedItem.progress,
|
||||
totalChapters: updatedItem.totalChapters,
|
||||
lastReadDate: updatedItem.lastReadDate,
|
||||
cachedHtml: htmlContent ?? updatedItem.cachedHtml
|
||||
)
|
||||
Logger.shared.log("Fixed image URL with encoding: \(encodedUrl)", type: "Debug")
|
||||
} else {
|
||||
let defaultImageUrl = "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/novel_cover.jpg"
|
||||
updatedItem = ContinueReadingItem(
|
||||
id: updatedItem.id,
|
||||
mediaTitle: updatedItem.mediaTitle,
|
||||
chapterTitle: updatedItem.chapterTitle,
|
||||
chapterNumber: updatedItem.chapterNumber,
|
||||
imageUrl: defaultImageUrl,
|
||||
href: updatedItem.href,
|
||||
moduleId: updatedItem.moduleId,
|
||||
progress: updatedItem.progress,
|
||||
totalChapters: updatedItem.totalChapters,
|
||||
lastReadDate: updatedItem.lastReadDate,
|
||||
cachedHtml: htmlContent ?? updatedItem.cachedHtml
|
||||
)
|
||||
Logger.shared.log("Using default image URL after encoding failed: \(defaultImageUrl)", type: "Debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.shared.log("Saving item with image URL: \(updatedItem.imageUrl)", type: "Debug")
|
||||
|
||||
items.append(updatedItem)
|
||||
|
||||
if items.count > 20 {
|
||||
items = Array(items.sorted(by: { $0.lastReadDate > $1.lastReadDate }).prefix(20))
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try JSONEncoder().encode(items)
|
||||
userDefaults.set(data, forKey: continueReadingKey)
|
||||
Logger.shared.log("Successfully saved continue reading item", type: "Debug")
|
||||
} catch {
|
||||
Logger.shared.log("Error encoding continue reading items: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
func remove(item: ContinueReadingItem) {
|
||||
var items = fetchItems()
|
||||
items.removeAll { $0.id == item.id }
|
||||
|
||||
do {
|
||||
let data = try JSONEncoder().encode(items)
|
||||
userDefaults.set(data, forKey: continueReadingKey)
|
||||
} catch {
|
||||
Logger.shared.log("Error encoding continue reading items: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
func updateProgress(for href: String, progress: Double, htmlContent: String? = nil) {
|
||||
var items = fetchItems()
|
||||
if let index = items.firstIndex(where: { $0.href == href }) {
|
||||
let updatedItem = items[index]
|
||||
|
||||
if progress >= 0.98 {
|
||||
let cachedHtml = htmlContent ?? updatedItem.cachedHtml
|
||||
|
||||
if let html = cachedHtml, !html.isEmpty && !html.contains("undefined") && html.count > 50 {
|
||||
let completedChapterKey = "completedChapterHtml_\(href)"
|
||||
UserDefaults.standard.set(html, forKey: completedChapterKey)
|
||||
Logger.shared.log("Saved HTML content for completed chapter \(href)", type: "Debug")
|
||||
}
|
||||
|
||||
items.remove(at: index)
|
||||
userDefaults.set(progress, forKey: "readingProgress_\(href)")
|
||||
|
||||
do {
|
||||
let data = try JSONEncoder().encode(items)
|
||||
userDefaults.set(data, forKey: continueReadingKey)
|
||||
} catch {
|
||||
Logger.shared.log("Error encoding continue reading items: \(error)", type: "Error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var mediaTitle = updatedItem.mediaTitle
|
||||
if mediaTitle.contains("-") && mediaTitle.count >= 30 || mediaTitle.contains("Unknown") {
|
||||
if let betterTitle = extractTitleFromURL(href) {
|
||||
mediaTitle = betterTitle
|
||||
}
|
||||
}
|
||||
|
||||
let newItem = ContinueReadingItem(
|
||||
id: updatedItem.id,
|
||||
mediaTitle: mediaTitle,
|
||||
chapterTitle: updatedItem.chapterTitle,
|
||||
chapterNumber: updatedItem.chapterNumber,
|
||||
imageUrl: updatedItem.imageUrl,
|
||||
href: updatedItem.href,
|
||||
moduleId: updatedItem.moduleId,
|
||||
progress: progress,
|
||||
totalChapters: updatedItem.totalChapters,
|
||||
lastReadDate: Date(),
|
||||
cachedHtml: htmlContent ?? updatedItem.cachedHtml
|
||||
)
|
||||
|
||||
Logger.shared.log("Updating item with image URL: \(newItem.imageUrl)", type: "Debug")
|
||||
|
||||
items[index] = newItem
|
||||
|
||||
do {
|
||||
let data = try JSONEncoder().encode(items)
|
||||
userDefaults.set(data, forKey: continueReadingKey)
|
||||
} catch {
|
||||
Logger.shared.log("Error encoding continue reading items: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isChapterCompleted(href: String) -> Bool {
|
||||
let progress = UserDefaults.standard.double(forKey: "readingProgress_\(href)")
|
||||
if progress >= 0.98 {
|
||||
return true
|
||||
}
|
||||
|
||||
let items = fetchItems()
|
||||
if let item = items.first(where: { $0.href == href }) {
|
||||
return item.progress >= 0.98
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getCachedHtml(for href: String) -> String? {
|
||||
let completedChapterKey = "completedChapterHtml_\(href)"
|
||||
if let completedHtml = UserDefaults.standard.string(forKey: completedChapterKey),
|
||||
!completedHtml.isEmpty && !completedHtml.contains("undefined") && completedHtml.count > 50 {
|
||||
Logger.shared.log("Using cached HTML for completed chapter \(href)", type: "Debug")
|
||||
return completedHtml
|
||||
}
|
||||
|
||||
let items = fetchItems()
|
||||
if let item = items.first(where: { $0.href == href }) {
|
||||
return item.cachedHtml
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -184,7 +184,10 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
volumeSliderHostingView,
|
||||
pipButton,
|
||||
airplayButton,
|
||||
timeBatteryContainer
|
||||
timeBatteryContainer,
|
||||
endTimeIcon,
|
||||
endTimeLabel,
|
||||
endTimeSeparator
|
||||
].compactMap { $0 }
|
||||
|
||||
private var originalHiddenStates: [UIView: Bool] = [:]
|
||||
|
|
@ -207,6 +210,10 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
private var timeLabel: UILabel?
|
||||
private var batteryLabel: UILabel?
|
||||
private var timeUpdateTimer: Timer?
|
||||
private var endTimeLabel: UILabel?
|
||||
private var endTimeIcon: UIImageView?
|
||||
private var endTimeSeparator: UIView?
|
||||
private var isEndTimeVisible: Bool = false
|
||||
|
||||
init(module: ScrapingModule,
|
||||
urlString: String,
|
||||
|
|
@ -332,10 +339,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
self?.checkForHLSStream()
|
||||
}
|
||||
|
||||
if isHoldPauseEnabled {
|
||||
holdForPause()
|
||||
}
|
||||
|
||||
do {
|
||||
try audioSession.setActive(true)
|
||||
} catch {
|
||||
|
|
@ -394,6 +397,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
if let slider = hiddenVolumeView.subviews.first(where: { $0 is UISlider }) as? UISlider {
|
||||
systemVolumeSlider = slider
|
||||
}
|
||||
|
||||
let twoFingerTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTwoFingerTapPause(_:)))
|
||||
twoFingerTapGesture.numberOfTouchesRequired = 2
|
||||
twoFingerTapGesture.delegate = self
|
||||
view.addGestureRecognizer(twoFingerTapGesture)
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
|
|
@ -461,6 +469,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
NotificationCenter.default.removeObserver(self)
|
||||
UIDevice.current.isBatteryMonitoringEnabled = false
|
||||
|
||||
// Clean up end time related resources
|
||||
endTimeIcon?.removeFromSuperview()
|
||||
endTimeLabel?.removeFromSuperview()
|
||||
endTimeSeparator?.removeFromSuperview()
|
||||
|
||||
inactivityTimer?.invalidate()
|
||||
inactivityTimer = nil
|
||||
|
||||
|
|
@ -473,7 +486,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
player?.removeTimeObserver(token)
|
||||
}
|
||||
|
||||
// Remove observer from player item if it exists
|
||||
if let currentItem = player?.currentItem {
|
||||
currentItem.removeObserver(self, forKeyPath: "status", context: &playerItemKVOContext)
|
||||
}
|
||||
|
|
@ -548,7 +560,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
} else if keyPath == "loadedTimeRanges" {
|
||||
// Handle loaded time ranges if needed
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -593,13 +604,14 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toggleControls))
|
||||
view.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
func setupControls() {
|
||||
controlsContainerView = UIView()
|
||||
|
||||
controlsContainerView = PassthroughView()
|
||||
controlsContainerView.backgroundColor = UIColor.black.withAlphaComponent(0.0)
|
||||
view.addSubview(controlsContainerView)
|
||||
controlsContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
func setupControls() {
|
||||
NSLayoutConstraint.activate([
|
||||
controlsContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
controlsContainerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
||||
|
|
@ -610,6 +622,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
blackCoverView = UIView()
|
||||
blackCoverView.backgroundColor = UIColor.black.withAlphaComponent(0.4)
|
||||
blackCoverView.translatesAutoresizingMaskIntoConstraints = false
|
||||
blackCoverView.isUserInteractionEnabled = false
|
||||
controlsContainerView.insertSubview(blackCoverView, at: 0)
|
||||
NSLayoutConstraint.activate([
|
||||
blackCoverView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
|
|
@ -765,11 +778,10 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
])
|
||||
}
|
||||
|
||||
func holdForPause() {
|
||||
let holdForPauseGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleHoldForPause(_:)))
|
||||
holdForPauseGesture.minimumPressDuration = 1
|
||||
holdForPauseGesture.numberOfTouchesRequired = 2
|
||||
view.addGestureRecognizer(holdForPauseGesture)
|
||||
@objc private func handleTwoFingerTapPause(_ gesture: UITapGestureRecognizer) {
|
||||
if gesture.state == .ended {
|
||||
togglePlayPause()
|
||||
}
|
||||
}
|
||||
|
||||
func addInvisibleControlOverlays() {
|
||||
|
|
@ -1589,6 +1601,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
self.sliderViewModel.sliderValue = max(0, min(self.currentTimeVal, self.duration))
|
||||
}
|
||||
|
||||
// Update end time when current time changes
|
||||
self.updateEndTime()
|
||||
|
||||
self.updateSkipButtonsVisibility()
|
||||
|
||||
UserDefaults.standard.set(self.currentTimeVal, forKey: "lastPlayedTime_\(self.fullUrl)")
|
||||
|
|
@ -1746,6 +1761,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
return
|
||||
}
|
||||
|
||||
isControlsVisible.toggle()
|
||||
|
||||
if isDimmed {
|
||||
dimButton.isHidden = false
|
||||
dimButton.alpha = 1.0
|
||||
|
|
@ -1755,12 +1772,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
self?.dimButton.alpha = 0
|
||||
}
|
||||
}
|
||||
|
||||
updateSkipButtonsVisibility()
|
||||
return
|
||||
}
|
||||
|
||||
isControlsVisible.toggle()
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
let alpha: CGFloat = self.isControlsVisible ? 1.0 : 0.0
|
||||
self.controlsContainerView.alpha = alpha
|
||||
|
|
@ -1947,31 +1960,12 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600))
|
||||
}
|
||||
|
||||
@objc private func handleHoldForPause(_ gesture: UILongPressGestureRecognizer) {
|
||||
guard isHoldPauseEnabled else { return }
|
||||
|
||||
if gesture.state == .began {
|
||||
togglePlayPause()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func dimTapped() {
|
||||
isDimmed.toggle()
|
||||
isControlsVisible = !isDimmed
|
||||
dimButtonTimer?.invalidate()
|
||||
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.blackCoverView.alpha = self.isDimmed ? 1.0 : 0.4
|
||||
for v in self.controlsToHide { v.alpha = self.isDimmed ? 0 : 1 }
|
||||
self.dimButton.alpha = self.isDimmed ? 0 : 1
|
||||
self.lockButton.alpha = self.isDimmed ? 0 : 1
|
||||
|
||||
self.subtitleBottomToSafeAreaConstraint?.isActive = !self.isControlsVisible
|
||||
self.subtitleBottomToSliderConstraint?.isActive = self.isControlsVisible
|
||||
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
dimButtonToSlider.isActive = !isDimmed
|
||||
dimButtonToRight.isActive = isDimmed
|
||||
}
|
||||
|
|
@ -2131,7 +2125,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
|
||||
|
||||
private func parseM3U8(url: URL, completion: @escaping () -> Void) {
|
||||
// For local file URLs, use a simple data task without custom headers
|
||||
if url.scheme == "file" {
|
||||
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
|
||||
self?.processM3U8Data(data: data, url: url, completion: completion)
|
||||
|
|
@ -2139,7 +2132,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
return
|
||||
}
|
||||
|
||||
// For remote URLs, add HTTP headers
|
||||
var request = URLRequest(url: url)
|
||||
if let mydict = headers, !mydict.isEmpty {
|
||||
for (key,value) in mydict {
|
||||
|
|
@ -2243,12 +2235,10 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
|
||||
let asset: AVURLAsset
|
||||
|
||||
// Check if this is a local file URL
|
||||
if url.scheme == "file" {
|
||||
// For local files, don't add HTTP headers
|
||||
Logger.shared.log("Switching to local file: \(url.absoluteString)", type: "Debug")
|
||||
|
||||
// Check if file exists
|
||||
|
||||
if FileManager.default.fileExists(atPath: url.path) {
|
||||
Logger.shared.log("Local file exists for quality switch: \(url.path)", type: "Debug")
|
||||
} else {
|
||||
|
|
@ -2257,7 +2247,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
|
||||
asset = AVURLAsset(url: url)
|
||||
} else {
|
||||
// For remote URLs, add HTTP headers
|
||||
Logger.shared.log("Switching to remote URL: \(url.absoluteString)", type: "Debug")
|
||||
var request = URLRequest(url: url)
|
||||
if let mydict = headers, !mydict.isEmpty {
|
||||
|
|
@ -2275,10 +2264,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
|
||||
// Add observer for the new player item
|
||||
playerItem.addObserver(self, forKeyPath: "status", options: [.new], context: &playerItemKVOContext)
|
||||
|
||||
// Remove observer from old item if it exists
|
||||
if let currentItem = player.currentItem {
|
||||
currentItem.removeObserver(self, forKeyPath: "status", context: &playerItemKVOContext)
|
||||
}
|
||||
|
|
@ -2839,6 +2826,36 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
controlsContainerView.addSubview(container)
|
||||
self.timeBatteryContainer = container
|
||||
|
||||
// Add tap gesture to toggle end time visibility
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toggleEndTimeVisibility))
|
||||
container.addGestureRecognizer(tapGesture)
|
||||
container.isUserInteractionEnabled = true
|
||||
|
||||
// Add end time components (initially hidden)
|
||||
let endTimeIcon = UIImageView(image: UIImage(systemName: "timer"))
|
||||
endTimeIcon.translatesAutoresizingMaskIntoConstraints = false
|
||||
endTimeIcon.tintColor = .white
|
||||
endTimeIcon.contentMode = .scaleAspectFit
|
||||
endTimeIcon.alpha = 0
|
||||
container.addSubview(endTimeIcon)
|
||||
self.endTimeIcon = endTimeIcon
|
||||
|
||||
let endTimeLabel = UILabel()
|
||||
endTimeLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
endTimeLabel.textColor = .white
|
||||
endTimeLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
endTimeLabel.textAlignment = .center
|
||||
endTimeLabel.alpha = 0
|
||||
container.addSubview(endTimeLabel)
|
||||
self.endTimeLabel = endTimeLabel
|
||||
|
||||
let endTimeSeparator = UIView()
|
||||
endTimeSeparator.translatesAutoresizingMaskIntoConstraints = false
|
||||
endTimeSeparator.backgroundColor = .white.withAlphaComponent(0.5)
|
||||
endTimeSeparator.alpha = 0
|
||||
container.addSubview(endTimeSeparator)
|
||||
self.endTimeSeparator = endTimeSeparator
|
||||
|
||||
let timeLabel = UILabel()
|
||||
timeLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
timeLabel.textColor = .white
|
||||
|
|
@ -2860,12 +2877,28 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
container.addSubview(batteryLabel)
|
||||
self.batteryLabel = batteryLabel
|
||||
|
||||
let centerXConstraint = container.centerXAnchor.constraint(equalTo: controlsContainerView.centerXAnchor)
|
||||
centerXConstraint.isActive = true
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
container.centerXAnchor.constraint(equalTo: controlsContainerView.centerXAnchor),
|
||||
container.topAnchor.constraint(equalTo: sliderHostingController?.view.bottomAnchor ?? controlsContainerView.bottomAnchor, constant: 2),
|
||||
container.heightAnchor.constraint(equalToConstant: 20),
|
||||
|
||||
timeLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12),
|
||||
endTimeIcon.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12),
|
||||
endTimeIcon.centerYAnchor.constraint(equalTo: container.centerYAnchor),
|
||||
endTimeIcon.widthAnchor.constraint(equalToConstant: 12),
|
||||
endTimeIcon.heightAnchor.constraint(equalToConstant: 12),
|
||||
|
||||
endTimeLabel.leadingAnchor.constraint(equalTo: endTimeIcon.trailingAnchor, constant: 4),
|
||||
endTimeLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor),
|
||||
endTimeLabel.widthAnchor.constraint(equalToConstant: 50),
|
||||
|
||||
endTimeSeparator.leadingAnchor.constraint(equalTo: endTimeLabel.trailingAnchor, constant: 8),
|
||||
endTimeSeparator.centerYAnchor.constraint(equalTo: container.centerYAnchor),
|
||||
endTimeSeparator.widthAnchor.constraint(equalToConstant: 1),
|
||||
endTimeSeparator.heightAnchor.constraint(equalToConstant: 12),
|
||||
|
||||
timeLabel.leadingAnchor.constraint(equalTo: endTimeSeparator.trailingAnchor, constant: 8),
|
||||
timeLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor),
|
||||
timeLabel.widthAnchor.constraint(equalToConstant: 50),
|
||||
|
||||
|
|
@ -2880,9 +2913,14 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
batteryLabel.widthAnchor.constraint(equalToConstant: 50)
|
||||
])
|
||||
|
||||
isEndTimeVisible = UserDefaults.standard.bool(forKey: "showEndTime")
|
||||
updateEndTimeVisibility(animated: false)
|
||||
|
||||
updateTime()
|
||||
updateEndTime()
|
||||
timeUpdateTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||||
self?.updateTime()
|
||||
self?.updateEndTime()
|
||||
}
|
||||
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
|
|
@ -2890,12 +2928,62 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(batteryLevelDidChange), name: UIDevice.batteryLevelDidChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
@objc private func toggleEndTimeVisibility() {
|
||||
isEndTimeVisible.toggle()
|
||||
UserDefaults.standard.set(isEndTimeVisible, forKey: "showEndTime")
|
||||
updateEndTimeVisibility(animated: true)
|
||||
}
|
||||
|
||||
private func updateEndTimeVisibility(animated: Bool) {
|
||||
let alpha: CGFloat = isEndTimeVisible ? 1.0 : 0.0
|
||||
let offset: CGFloat = isEndTimeVisible ? 0 : -37
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.endTimeIcon?.alpha = alpha
|
||||
self.endTimeSeparator?.alpha = alpha
|
||||
self.endTimeLabel?.alpha = alpha
|
||||
|
||||
if let container = self.timeBatteryContainer {
|
||||
container.transform = CGAffineTransform(translationX: offset, y: 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.endTimeIcon?.alpha = alpha
|
||||
self.endTimeSeparator?.alpha = alpha
|
||||
self.endTimeLabel?.alpha = alpha
|
||||
|
||||
// 调整容器位置以保持居中
|
||||
if let container = self.timeBatteryContainer {
|
||||
container.transform = CGAffineTransform(translationX: offset, y: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTime() {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm"
|
||||
timeLabel?.text = formatter.string(from: Date())
|
||||
}
|
||||
|
||||
private func updateEndTime() {
|
||||
guard let player = player, duration > 0 else { return }
|
||||
|
||||
let currentSeconds = CMTimeGetSeconds(player.currentTime())
|
||||
let remainingSeconds = duration - currentSeconds
|
||||
|
||||
if remainingSeconds <= 0 {
|
||||
endTimeLabel?.text = "--:--"
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate end time by adding remaining seconds to current time
|
||||
let endTime = Date().addingTimeInterval(remainingSeconds)
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm"
|
||||
endTimeLabel?.text = formatter.string(from: endTime)
|
||||
}
|
||||
|
||||
@objc private func batteryLevelDidChange() {
|
||||
updateBatteryLevel()
|
||||
}
|
||||
|
|
@ -2962,9 +3050,28 @@ extension CustomMediaPlayerViewController: AVPictureInPictureControllerDelegate
|
|||
}
|
||||
}
|
||||
|
||||
extension CustomMediaPlayerViewController {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// yes? Like the plural of the famous american rapper ye? -IBHRAD
|
||||
// low taper fade the meme is massive -cranci
|
||||
// The mind is the source of good and evil, only you yourself can decide which you will bring yourself. -seiike
|
||||
// guys watch Clannad already - ibro
|
||||
// May the Divine Providence bestow its infinite mercy upon your soul, and may eternal grace find you beyond the shadows of this mortal realm. - paul, 15/11/2005 - 13/05/2023
|
||||
// this dumbass ↑ defo used gpt, ong he did bro
|
||||
// A view that passes through touches to views behind it unless the touch hits a subview
|
||||
// fuck yall stories, continue below this code
|
||||
|
||||
class PassthroughView: UIView {
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
for subview in subviews {
|
||||
if !subview.isHidden && subview.alpha > 0 && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension Notification.Name {
|
||||
static let iCloudSyncDidComplete = Notification.Name("iCloudSyncDidComplete")
|
||||
|
|
@ -13,4 +14,12 @@ extension Notification.Name {
|
|||
static let ContinueWatchingDidUpdate = Notification.Name("ContinueWatchingDidUpdate")
|
||||
static let DownloadManagerStatusUpdate = Notification.Name("DownloadManagerStatusUpdate")
|
||||
static let modulesSyncDidComplete = Notification.Name("modulesSyncDidComplete")
|
||||
static let moduleRemoved = Notification.Name("moduleRemoved")
|
||||
static let didReceiveNewModule = Notification.Name("didReceiveNewModule")
|
||||
static let didUpdateModules = Notification.Name("didUpdateModules")
|
||||
static let didUpdateDownloads = Notification.Name("didUpdateDownloads")
|
||||
static let didUpdateBookmarks = Notification.Name("didUpdateBookmarks")
|
||||
static let hideTabBar = Notification.Name("hideTabBar")
|
||||
static let showTabBar = Notification.Name("showTabBar")
|
||||
static let searchQueryChanged = Notification.Name("searchQueryChanged")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct ScrollViewBottomPadding: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ enum JSError: Error {
|
|||
extension JSController {
|
||||
@MainActor
|
||||
func extractChapters(moduleId: String, href: String) async throws -> [[String: Any]] {
|
||||
guard let module = ModuleManager().modules.first(where: { $0.id.uuidString == moduleId }) else {
|
||||
guard ModuleManager().modules.first(where: { $0.id.uuidString == moduleId }) != nil else {
|
||||
throw JSError.moduleNotFound
|
||||
}
|
||||
|
||||
|
|
@ -122,17 +122,59 @@ extension JSController {
|
|||
}
|
||||
|
||||
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<String, Error>) in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
guard let self = self else {
|
||||
continuation.resume(throwing: JSError.invalidResponse)
|
||||
return
|
||||
}
|
||||
|
||||
let function = self.context.objectForKeyedSubscript("extractText")
|
||||
let result = function?.call(withArguments: [href])
|
||||
if self.context.objectForKeyedSubscript("extractText") == nil {
|
||||
Logger.shared.log("extractText function not found, attempting to load module script", type: "Debug")
|
||||
do {
|
||||
let moduleContent = try ModuleManager().getModuleContent(module)
|
||||
self.loadScript(moduleContent)
|
||||
Logger.shared.log("Successfully loaded module script", type: "Debug")
|
||||
} catch {
|
||||
Logger.shared.log("Failed to load module script: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
guard let function = self.context.objectForKeyedSubscript("extractText") else {
|
||||
Logger.shared.log("extractText function not available after loading module script", type: "Error")
|
||||
|
||||
let task = Task<String, Error> {
|
||||
return try await self.fetchContentDirectly(from: href)
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
let content = try await task.value
|
||||
continuation.resume(returning: content)
|
||||
} catch {
|
||||
continuation.resume(throwing: JSError.invalidResponse)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let result = function.call(withArguments: [href])
|
||||
|
||||
if let exception = self.context.exception {
|
||||
Logger.shared.log("Error extracting text: \(exception)", type: "Error")
|
||||
|
||||
let task = Task<String, Error> {
|
||||
return try await self.fetchContentDirectly(from: href)
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
let content = try await task.value
|
||||
continuation.resume(returning: content)
|
||||
} catch {
|
||||
continuation.resume(throwing: JSError.jsException(exception.toString() ?? "Unknown JS error"))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let result = result, result.hasProperty("then") {
|
||||
|
|
@ -163,25 +205,116 @@ extension JSController {
|
|||
result.invokeMethod("then", withArguments: [thenBlock])
|
||||
result.invokeMethod("catch", withArguments: [catchBlock])
|
||||
|
||||
group.notify(queue: .main) {
|
||||
let notifyWorkItem = DispatchWorkItem {
|
||||
if !extractedText.isEmpty {
|
||||
continuation.resume(returning: extractedText)
|
||||
} else if let error = extractError {
|
||||
continuation.resume(throwing: error)
|
||||
} else if extractError != nil {
|
||||
let fetchTask = Task<String, Error> {
|
||||
return try await self.fetchContentDirectly(from: href)
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
let content = try await fetchTask.value
|
||||
continuation.resume(returning: content)
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
continuation.resume(throwing: JSError.emptyContent)
|
||||
let fetchTask = Task<String, Error> {
|
||||
return try await self.fetchContentDirectly(from: href)
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
let content = try await fetchTask.value
|
||||
continuation.resume(returning: content)
|
||||
} catch _ {
|
||||
continuation.resume(throwing: JSError.emptyContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .main, work: notifyWorkItem)
|
||||
} else {
|
||||
if let text = result?.toString(), !text.isEmpty {
|
||||
Logger.shared.log("extractText: direct string result", type: "Debug")
|
||||
continuation.resume(returning: text)
|
||||
} else {
|
||||
Logger.shared.log("extractText: could not parse direct result", type: "Error")
|
||||
continuation.resume(throwing: JSError.emptyContent)
|
||||
Logger.shared.log("extractText: could not parse direct result, trying direct fetch", type: "Error")
|
||||
let task = Task<String, Error> {
|
||||
return try await self.fetchContentDirectly(from: href)
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
let content = try await task.value
|
||||
continuation.resume(returning: content)
|
||||
} catch {
|
||||
continuation.resume(throwing: JSError.emptyContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async(execute: workItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchContentDirectly(from url: String) async throws -> String {
|
||||
guard let url = URL(string: url) else {
|
||||
throw JSError.invalidResponse
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
Logger.shared.log("Attempting direct fetch from: \(url.absoluteString)", type: "Debug")
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode) else {
|
||||
Logger.shared.log("Direct fetch failed with status code: \((response as? HTTPURLResponse)?.statusCode ?? -1)", type: "Error")
|
||||
throw JSError.invalidResponse
|
||||
}
|
||||
|
||||
guard let htmlString = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode response data", type: "Error")
|
||||
throw JSError.invalidResponse
|
||||
}
|
||||
|
||||
var content = ""
|
||||
|
||||
if let contentRange = htmlString.range(of: "<article", options: .caseInsensitive),
|
||||
let endRange = htmlString.range(of: "</article>", options: .caseInsensitive) {
|
||||
let startIndex = contentRange.lowerBound
|
||||
let endIndex = endRange.upperBound
|
||||
content = String(htmlString[startIndex..<endIndex])
|
||||
} else if let contentRange = htmlString.range(of: "<div class=\"chapter-content\"", options: .caseInsensitive),
|
||||
let endRange = htmlString.range(of: "</div>", options: .caseInsensitive, range: contentRange.upperBound..<htmlString.endIndex) {
|
||||
let startIndex = contentRange.lowerBound
|
||||
let endIndex = endRange.upperBound
|
||||
content = String(htmlString[startIndex..<endIndex])
|
||||
} else if let contentRange = htmlString.range(of: "<div class=\"content\"", options: .caseInsensitive),
|
||||
let endRange = htmlString.range(of: "</div>", options: .caseInsensitive, range: contentRange.upperBound..<htmlString.endIndex) {
|
||||
let startIndex = contentRange.lowerBound
|
||||
let endIndex = endRange.upperBound
|
||||
content = String(htmlString[startIndex..<endIndex])
|
||||
} else if let bodyRange = htmlString.range(of: "<body", options: .caseInsensitive),
|
||||
let endBodyRange = htmlString.range(of: "</body>", options: .caseInsensitive) {
|
||||
let startIndex = bodyRange.lowerBound
|
||||
let endIndex = endBodyRange.upperBound
|
||||
content = String(htmlString[startIndex..<endIndex])
|
||||
} else {
|
||||
content = htmlString
|
||||
}
|
||||
|
||||
Logger.shared.log("Direct fetch successful, content length: \(content.count)", type: "Debug")
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ private struct ModuleLink: Identifiable {
|
|||
|
||||
struct CommunityLibraryView: View {
|
||||
@EnvironmentObject var moduleManager: ModuleManager
|
||||
@EnvironmentObject var tabBarController: TabBarController
|
||||
|
||||
|
||||
@AppStorage("lastCommunityURL") private var inputURL: String = ""
|
||||
@State private var webURL: URL?
|
||||
|
|
@ -40,10 +40,6 @@ struct CommunityLibraryView: View {
|
|||
}
|
||||
.onAppear {
|
||||
loadURL()
|
||||
tabBarController.hideTabBar()
|
||||
}
|
||||
.onDisappear {
|
||||
tabBarController.showTabBar()
|
||||
}
|
||||
.sheet(item: $moduleLinkToAdd) { link in
|
||||
ModuleAdditionSettingsView(moduleUrl: link.url)
|
||||
|
|
|
|||
|
|
@ -196,6 +196,8 @@ class ModuleManager: ObservableObject {
|
|||
modules.removeAll { $0.id == module.id }
|
||||
saveModules()
|
||||
Logger.shared.log("Deleted module: \(module.metadata.sourceName)")
|
||||
|
||||
NotificationCenter.default.post(name: .moduleRemoved, object: module.id.uuidString)
|
||||
}
|
||||
|
||||
func getModuleContent(_ module: ScrapingModule) throws -> String {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
|
||||
extension Color {
|
||||
|
|
@ -36,15 +37,14 @@ extension Color {
|
|||
|
||||
|
||||
struct TabBar: View {
|
||||
let tabs: [TabItem]
|
||||
var tabs: [TabItem]
|
||||
@Binding var selectedTab: Int
|
||||
@Binding var lastTab: Int
|
||||
@State var showSearch: Bool = false
|
||||
@State var searchLocked: Bool = false
|
||||
@FocusState var keyboardFocus: Bool
|
||||
@State var keyboardHidden: Bool = true
|
||||
@Binding var searchQuery: String
|
||||
@ObservedObject var controller: TabBarController
|
||||
@State private var lastTab: Int = 0
|
||||
@State private var showSearch: Bool = false
|
||||
@State private var searchQuery: String = ""
|
||||
@FocusState private var keyboardFocus: Bool
|
||||
@State private var keyboardHidden: Bool = true
|
||||
@State private var searchLocked: Bool = false
|
||||
|
||||
@State private var keyboardHeight: CGFloat = 0
|
||||
|
||||
|
|
@ -57,15 +57,6 @@ struct TabBar: View {
|
|||
|
||||
@Namespace private var animation
|
||||
|
||||
|
||||
func slideDown() {
|
||||
controller.hideTabBar()
|
||||
}
|
||||
|
||||
func slideUp() {
|
||||
controller.showTabBar()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if showSearch && keyboardHidden {
|
||||
|
|
@ -124,6 +115,14 @@ struct TabBar: View {
|
|||
keyboardHidden = !newValue
|
||||
}
|
||||
}
|
||||
.onChange(of: searchQuery) { newValue in
|
||||
// 发送通知,传递搜索查询
|
||||
NotificationCenter.default.post(
|
||||
name: .searchQueryChanged,
|
||||
object: nil,
|
||||
userInfo: ["searchQuery": newValue]
|
||||
)
|
||||
}
|
||||
.onDisappear {
|
||||
keyboardFocus = false
|
||||
}
|
||||
|
|
@ -180,12 +179,10 @@ struct TabBar: View {
|
|||
.padding(.horizontal, -20)
|
||||
.padding(.bottom, -100)
|
||||
.padding(.top, -10)
|
||||
.opacity(controller.isHidden ? 0 : 1) // Animate opacity
|
||||
.animation(.easeInOut(duration: 0.15), value: controller.isHidden)
|
||||
}
|
||||
.offset(y: controller.isHidden ? 120 : (keyboardFocus ? -keyboardHeight + 36 : 0))
|
||||
.offset(y: keyboardFocus ? -keyboardHeight + 40 : 0)
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: keyboardHeight)
|
||||
.animation(.easeInOut(duration: 0.15), value: controller.isHidden)
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: keyboardFocus)
|
||||
.onChange(of: keyboardHeight) { newValue in
|
||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
|
||||
}
|
||||
|
|
@ -201,6 +198,10 @@ struct TabBar: View {
|
|||
keyboardHeight = 0
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
//
|
||||
// TabBarController.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Mac on 28/05/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
class TabBarController: ObservableObject {
|
||||
@Published var isHidden = false
|
||||
|
||||
func hideTabBar() {
|
||||
withAnimation(.easeInOut(duration: 0.15)) {
|
||||
isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
func showTabBar() {
|
||||
withAnimation(.easeInOut(duration: 0.10)) {
|
||||
isHidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import SwiftUI
|
|||
|
||||
struct DownloadView: View {
|
||||
@EnvironmentObject var jsController: JSController
|
||||
@EnvironmentObject var tabBarController: TabBarController
|
||||
@State private var searchText = ""
|
||||
@State private var selectedTab = 0
|
||||
@State private var sortOption: SortOption = .newest
|
||||
|
|
@ -71,9 +70,6 @@ struct DownloadView: View {
|
|||
Text(String(format: NSLocalizedString("Are you sure you want to delete '%@'?", comment: ""), asset.episodeDisplayName))
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
tabBarController.showTabBar()
|
||||
}
|
||||
}
|
||||
.deviceScaled()
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
|
|
@ -989,7 +985,6 @@ struct EnhancedShowEpisodesView: View {
|
|||
@State private var showDeleteAllAlert = false
|
||||
@State private var assetToDelete: DownloadedAsset?
|
||||
@EnvironmentObject var jsController: JSController
|
||||
@EnvironmentObject var tabBarController: TabBarController
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
|
|
@ -1029,16 +1024,18 @@ struct EnhancedShowEpisodesView: View {
|
|||
navigationOverlay
|
||||
}
|
||||
.onAppear {
|
||||
tabBarController.hideTabBar()
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first,
|
||||
let navigationController = window.rootViewController?.children.first as? UINavigationController {
|
||||
navigationController.interactivePopGestureRecognizer?.isEnabled = true
|
||||
navigationController.interactivePopGestureRecognizer?.delegate = nil
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: .hideTabBar, object: nil)
|
||||
}
|
||||
.onDisappear {
|
||||
tabBarController.showTabBar()
|
||||
NotificationCenter.default.post(name: .showTabBar, object: nil)
|
||||
UIScrollView.appearance().bounces = true
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
}
|
||||
|
|
@ -1048,7 +1045,6 @@ struct EnhancedShowEpisodesView: View {
|
|||
VStack {
|
||||
HStack {
|
||||
Button(action: {
|
||||
tabBarController.showTabBar()
|
||||
dismiss()
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
|
|
|
|||
448
Sora/Views/LibraryView/AllReading.swift
Normal file
448
Sora/Views/LibraryView/AllReading.swift
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
//
|
||||
// AllReading.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 26/06/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
|
||||
struct AllReadingView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
|
||||
@State private var continueReadingItems: [ContinueReadingItem] = []
|
||||
@State private var isRefreshing: Bool = false
|
||||
@State private var sortOption: SortOption = .dateAdded
|
||||
@State private var searchText: String = ""
|
||||
@State private var isSearchActive: Bool = false
|
||||
@State private var isSelecting: Bool = false
|
||||
@State private var selectedItems: Set<ContinueReadingItem.ID> = []
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
enum SortOption: String, CaseIterable {
|
||||
case dateAdded = "Recently Added"
|
||||
case title = "Novel Title"
|
||||
case progress = "Read Progress"
|
||||
}
|
||||
|
||||
var filteredAndSortedItems: [ContinueReadingItem] {
|
||||
let filtered = searchText.isEmpty ? continueReadingItems : continueReadingItems.filter { item in
|
||||
item.mediaTitle.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
switch sortOption {
|
||||
case .dateAdded:
|
||||
return filtered.sorted { $0.lastReadDate > $1.lastReadDate }
|
||||
case .title:
|
||||
return filtered.sorted { $0.mediaTitle.lowercased() < $1.mediaTitle.lowercased() }
|
||||
case .progress:
|
||||
return filtered.sorted { $0.progress > $1.progress }
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 24))
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}) {
|
||||
Text(LocalizedStringKey("All Reading"))
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Button(action: {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
isSearchActive.toggle()
|
||||
}
|
||||
if !isSearchActive {
|
||||
searchText = ""
|
||||
}
|
||||
}) {
|
||||
Image(systemName: isSearchActive ? "xmark.circle.fill" : "magnifyingglass")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(10)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.shadow(color: .accentColor.opacity(0.2), radius: 2)
|
||||
)
|
||||
.circularGradientOutline()
|
||||
}
|
||||
Menu {
|
||||
ForEach(SortOption.allCases, id: \.self) { option in
|
||||
Button {
|
||||
sortOption = option
|
||||
} label: {
|
||||
HStack {
|
||||
Text(NSLocalizedString(option.rawValue, comment: ""))
|
||||
if option == sortOption {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "line.3.horizontal.decrease.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(10)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.shadow(color: .accentColor.opacity(0.2), radius: 2)
|
||||
)
|
||||
.circularGradientOutline()
|
||||
}
|
||||
Button(action: {
|
||||
if isSelecting {
|
||||
if !selectedItems.isEmpty {
|
||||
for id in selectedItems {
|
||||
if let item = continueReadingItems.first(where: { $0.id == id }) {
|
||||
ContinueReadingManager.shared.remove(item: item)
|
||||
}
|
||||
}
|
||||
selectedItems.removeAll()
|
||||
fetchContinueReading()
|
||||
}
|
||||
isSelecting = false
|
||||
} else {
|
||||
isSelecting = true
|
||||
}
|
||||
}) {
|
||||
Image(systemName: isSelecting ? "trash" : "checkmark.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(isSelecting ? .red : .accentColor)
|
||||
.padding(10)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.shadow(color: .accentColor.opacity(0.2), radius: 2)
|
||||
)
|
||||
.circularGradientOutline()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top)
|
||||
|
||||
if isSearchActive {
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(.secondary)
|
||||
TextField(LocalizedStringKey("Search reading..."), text: $searchText)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
.foregroundColor(.primary)
|
||||
if !searchText.isEmpty {
|
||||
Button(action: {
|
||||
searchText = ""
|
||||
}) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(.ultraThinMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.strokeBorder(
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: Color.accentColor.opacity(0.25), location: 0),
|
||||
.init(color: Color.accentColor.opacity(0), location: 1)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
),
|
||||
lineWidth: 1.5
|
||||
)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 8)
|
||||
.transition(.asymmetric(
|
||||
insertion: .move(edge: .top).combined(with: .opacity),
|
||||
removal: .move(edge: .top).combined(with: .opacity)
|
||||
))
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 12) {
|
||||
if filteredAndSortedItems.isEmpty {
|
||||
emptyStateView
|
||||
} else {
|
||||
ForEach(filteredAndSortedItems) { item in
|
||||
FullWidthContinueReadingCell(
|
||||
item: item,
|
||||
markAsRead: {
|
||||
markContinueReadingItemAsRead(item: item)
|
||||
},
|
||||
removeItem: {
|
||||
removeContinueReadingItem(item: item)
|
||||
},
|
||||
isSelecting: isSelecting,
|
||||
selectedItems: $selectedItems
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.scrollViewBottomPadding()
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onAppear {
|
||||
fetchContinueReading()
|
||||
|
||||
NotificationCenter.default.post(name: .showTabBar, object: nil)
|
||||
}
|
||||
.onChange(of: scenePhase) { newPhase in
|
||||
if newPhase == .active {
|
||||
fetchContinueReading()
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
isRefreshing = true
|
||||
fetchContinueReading()
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyStateView: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "book.closed")
|
||||
.font(.system(size: 50))
|
||||
.foregroundColor(.gray)
|
||||
|
||||
Text("No Reading History")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Text("Books you're reading will appear here")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 60)
|
||||
}
|
||||
|
||||
private func fetchContinueReading() {
|
||||
continueReadingItems = ContinueReadingManager.shared.fetchItems()
|
||||
|
||||
for (index, item) in continueReadingItems.enumerated() {
|
||||
print("Reading item \(index): Title: \(item.mediaTitle), Image URL: \(item.imageUrl)")
|
||||
}
|
||||
}
|
||||
|
||||
private func markContinueReadingItemAsRead(item: ContinueReadingItem) {
|
||||
UserDefaults.standard.set(1.0, forKey: "readingProgress_\(item.href)")
|
||||
ContinueReadingManager.shared.updateProgress(for: item.href, progress: 1.0)
|
||||
fetchContinueReading()
|
||||
}
|
||||
|
||||
private func removeContinueReadingItem(item: ContinueReadingItem) {
|
||||
ContinueReadingManager.shared.remove(item: item)
|
||||
fetchContinueReading()
|
||||
}
|
||||
}
|
||||
|
||||
struct FullWidthContinueReadingCell: View {
|
||||
let item: ContinueReadingItem
|
||||
var markAsRead: () -> Void
|
||||
var removeItem: () -> Void
|
||||
var isSelecting: Bool
|
||||
var selectedItems: Binding<Set<ContinueReadingItem.ID>>
|
||||
|
||||
var isSelected: Bool {
|
||||
selectedItems.wrappedValue.contains(item.id)
|
||||
}
|
||||
|
||||
private var imageURL: URL {
|
||||
print("Processing image URL: \(item.imageUrl)")
|
||||
|
||||
if !item.imageUrl.isEmpty {
|
||||
if let url = URL(string: item.imageUrl) {
|
||||
print("Valid URL: \(url)")
|
||||
return url
|
||||
}
|
||||
|
||||
if let encodedUrlString = item.imageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
|
||||
let url = URL(string: encodedUrlString) {
|
||||
print("Using encoded URL: \(encodedUrlString)")
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
print("Using fallback URL")
|
||||
return URL(string: "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png")!
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var body: some View {
|
||||
Group {
|
||||
if isSelecting {
|
||||
Button(action: {
|
||||
if isSelected {
|
||||
selectedItems.wrappedValue.remove(item.id)
|
||||
} else {
|
||||
selectedItems.wrappedValue.insert(item.id)
|
||||
}
|
||||
}) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
cellContent
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.black)
|
||||
.background(Color.white.clipShape(Circle()).opacity(0.8))
|
||||
.offset(x: -8, y: 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NavigationLink(destination: ReaderView(
|
||||
moduleId: item.moduleId,
|
||||
chapterHref: item.href,
|
||||
chapterTitle: item.chapterTitle,
|
||||
mediaTitle: item.mediaTitle,
|
||||
chapterNumber: item.chapterNumber
|
||||
)) {
|
||||
cellContent
|
||||
}
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
UserDefaults.standard.set(true, forKey: "navigatingToReaderView")
|
||||
})
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: { markAsRead() }) {
|
||||
Label("Mark as Read", systemImage: "checkmark.circle")
|
||||
}
|
||||
Button(role: .destructive, action: { removeItem() }) {
|
||||
Label("Remove from Continue Reading", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private var cellContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
LazyImage(url: imageURL) { state in
|
||||
if let image = state.imageContainer?.image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: geometry.size.width, height: 157.03)
|
||||
.blur(radius: 3)
|
||||
.opacity(0.7)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: geometry.size.width, height: 157.03)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
print("Background image loading: \(imageURL)")
|
||||
}
|
||||
|
||||
Rectangle()
|
||||
.fill(LinearGradient(
|
||||
gradient: Gradient(colors: [Color.black.opacity(0.7), Color.black.opacity(0.4)]),
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
))
|
||||
.frame(width: geometry.size.width, height: 157.03)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("\(Int(item.progress * 100))%")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.white)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal, 8)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.cornerRadius(4)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Chapter \(item.chapterNumber)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
|
||||
Text(item.mediaTitle)
|
||||
.font(.headline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.frame(width: geometry.size.width * 0.6, alignment: .leading)
|
||||
|
||||
LazyImage(url: imageURL) { state in
|
||||
if let image = state.imageContainer?.image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: geometry.size.width * 0.4, height: 157.03)
|
||||
.clipped()
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: geometry.size.width * 0.4, height: 157.03)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
print("Right image loading: \(imageURL)")
|
||||
}
|
||||
.frame(width: geometry.size.width * 0.4, height: 157.03)
|
||||
}
|
||||
}
|
||||
.frame(height: 157.03)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color.gray.opacity(0.3), lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
.frame(height: 157.03)
|
||||
}
|
||||
}
|
||||
|
|
@ -113,7 +113,7 @@ struct AllWatchingView: View {
|
|||
sortOption = option
|
||||
} label: {
|
||||
HStack {
|
||||
Text(option.rawValue)
|
||||
Text(NSLocalizedString(option.rawValue, comment: ""))
|
||||
if option == sortOption {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
|
|
|
|||
|
|
@ -53,13 +53,19 @@ struct BookmarkGridItemView: View {
|
|||
}
|
||||
}
|
||||
)
|
||||
// Book/TV icon overlay, bottom right of module icon
|
||||
Image(systemName: isNovel ? "book.fill" : "tv.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 14, height: 14)
|
||||
.foregroundColor(.accentColor)
|
||||
.offset(x: 6, y: 6)
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.8))
|
||||
.shadow(color: .accentColor.opacity(0.2), radius: 2)
|
||||
.frame(width: 20, height: 20)
|
||||
Image(systemName: isNovel ? "book.fill" : "tv.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 10, height: 10)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.circularGradientOutline()
|
||||
.offset(x: 6, y: 6)
|
||||
}
|
||||
.padding(8),
|
||||
alignment: .topLeading
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ struct BookmarksDetailView: View {
|
|||
.foregroundColor(.primary)
|
||||
}
|
||||
Button(action: { dismiss() }) {
|
||||
Text("Collections")
|
||||
Text(LocalizedStringKey("Collections"))
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.primary)
|
||||
|
|
@ -90,7 +90,7 @@ struct BookmarksDetailView: View {
|
|||
sortOption = option
|
||||
} label: {
|
||||
HStack {
|
||||
Text(option.rawValue)
|
||||
Text(NSLocalizedString(option.rawValue, comment: ""))
|
||||
if option == sortOption {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
|
|
@ -168,7 +168,7 @@ struct BookmarksDetailView: View {
|
|||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(.secondary)
|
||||
TextField("Search collections...", text: $searchText)
|
||||
TextField(LocalizedStringKey("Search collections..."), text: $searchText)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
.foregroundColor(.primary)
|
||||
if !searchText.isEmpty {
|
||||
|
|
@ -215,9 +215,9 @@ struct BookmarksDetailView: View {
|
|||
Image(systemName: "folder")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("No Collections")
|
||||
Text(LocalizedStringKey("No Collections"))
|
||||
.font(.headline)
|
||||
Text("Create a collection to organize your bookmarks")
|
||||
Text(LocalizedStringKey("Create a collection to organize your bookmarks"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
|
@ -254,7 +254,7 @@ struct BookmarksDetailView: View {
|
|||
)
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Rename") {
|
||||
Button(LocalizedStringKey("Rename")) {
|
||||
collectionToRename = collection
|
||||
renameCollectionName = collection.name
|
||||
isShowingRenamePrompt = true
|
||||
|
|
@ -262,7 +262,7 @@ struct BookmarksDetailView: View {
|
|||
Button(role: .destructive) {
|
||||
libraryManager.deleteCollection(id: collection.id)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
Label(LocalizedStringKey("Delete"), systemImage: "trash")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -270,7 +270,7 @@ struct BookmarksDetailView: View {
|
|||
BookmarkCollectionGridCell(collection: collection, width: 162, height: 162)
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Rename") {
|
||||
Button(LocalizedStringKey("Rename")) {
|
||||
collectionToRename = collection
|
||||
renameCollectionName = collection.name
|
||||
isShowingRenamePrompt = true
|
||||
|
|
@ -278,7 +278,7 @@ struct BookmarksDetailView: View {
|
|||
Button(role: .destructive) {
|
||||
libraryManager.deleteCollection(id: collection.id)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
Label(LocalizedStringKey("Delete"), systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -292,30 +292,30 @@ struct BookmarksDetailView: View {
|
|||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.alert("Create Collection", isPresented: $isShowingCreateCollection) {
|
||||
TextField("Collection Name", text: $newCollectionName)
|
||||
Button("Cancel", role: .cancel) {
|
||||
.alert(LocalizedStringKey("Create Collection"), isPresented: $isShowingCreateCollection) {
|
||||
TextField(LocalizedStringKey("Collection Name"), text: $newCollectionName)
|
||||
Button(LocalizedStringKey("Cancel"), role: .cancel) {
|
||||
newCollectionName = ""
|
||||
}
|
||||
Button("Create") {
|
||||
Button(LocalizedStringKey("Create")) {
|
||||
if !newCollectionName.isEmpty {
|
||||
libraryManager.createCollection(name: newCollectionName)
|
||||
newCollectionName = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Rename Collection", isPresented: $isShowingRenamePrompt, presenting: collectionToRename) { collection in
|
||||
TextField("Collection Name", text: $renameCollectionName)
|
||||
Button("Cancel", role: .cancel) {
|
||||
.alert(LocalizedStringKey("Rename Collection"), isPresented: $isShowingRenamePrompt, presenting: collectionToRename) { collection in
|
||||
TextField(LocalizedStringKey("Collection Name"), text: $renameCollectionName)
|
||||
Button(LocalizedStringKey("Cancel"), role: .cancel) {
|
||||
collectionToRename = nil
|
||||
renameCollectionName = ""
|
||||
}
|
||||
Button("Rename") {
|
||||
if !renameCollectionName.isEmpty {
|
||||
Button(LocalizedStringKey("Rename")) {
|
||||
if let collection = collectionToRename, !renameCollectionName.isEmpty {
|
||||
libraryManager.renameCollection(id: collection.id, newName: renameCollectionName)
|
||||
collectionToRename = nil
|
||||
renameCollectionName = ""
|
||||
}
|
||||
collectionToRename = nil
|
||||
renameCollectionName = ""
|
||||
}
|
||||
} message: { _ in EmptyView() }
|
||||
.onAppear {
|
||||
|
|
@ -338,7 +338,7 @@ private struct SortMenu: View {
|
|||
sortOption = option
|
||||
} label: {
|
||||
HStack {
|
||||
Text(option.rawValue)
|
||||
Text(NSLocalizedString(option.rawValue, comment: ""))
|
||||
if option == sortOption {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ struct CollectionDetailView: View {
|
|||
@Environment(\.dismiss) private var dismiss
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
@EnvironmentObject private var tabBarController: TabBarController
|
||||
|
||||
|
||||
let collection: BookmarkCollection
|
||||
@State private var sortOption: SortOption = .dateAdded
|
||||
|
|
@ -20,6 +20,7 @@ struct CollectionDetailView: View {
|
|||
@State private var isSearchActive: Bool = false
|
||||
@State private var isSelecting: Bool = false
|
||||
@State private var selectedBookmarks: Set<LibraryItem.ID> = []
|
||||
@State private var isActive: Bool = false
|
||||
|
||||
enum SortOption: String, CaseIterable {
|
||||
case dateAdded = "Date Added"
|
||||
|
|
@ -28,10 +29,15 @@ struct CollectionDetailView: View {
|
|||
}
|
||||
|
||||
private var filteredAndSortedBookmarks: [LibraryItem] {
|
||||
let filtered = searchText.isEmpty ? collection.bookmarks : collection.bookmarks.filter { item in
|
||||
let validBookmarks = collection.bookmarks.filter { bookmark in
|
||||
moduleManager.modules.contains { $0.id.uuidString == bookmark.moduleId }
|
||||
}
|
||||
|
||||
let filtered = searchText.isEmpty ? validBookmarks : validBookmarks.filter { item in
|
||||
item.title.localizedCaseInsensitiveContains(searchText) ||
|
||||
item.moduleName.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
|
||||
switch sortOption {
|
||||
case .dateAdded:
|
||||
return filtered
|
||||
|
|
@ -92,7 +98,7 @@ struct CollectionDetailView: View {
|
|||
sortOption = option
|
||||
} label: {
|
||||
HStack {
|
||||
Text(option.rawValue)
|
||||
Text(NSLocalizedString(option.rawValue, comment: ""))
|
||||
if option == sortOption {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
|
|
@ -118,7 +124,7 @@ struct CollectionDetailView: View {
|
|||
if isSelecting {
|
||||
if !selectedBookmarks.isEmpty {
|
||||
for id in selectedBookmarks {
|
||||
if let item = collection.bookmarks.first(where: { $0.id == id }) {
|
||||
if collection.bookmarks.contains(where: { $0.id == id }) {
|
||||
libraryManager.removeBookmarkFromCollection(bookmarkId: id, collectionId: collection.id)
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +162,7 @@ struct CollectionDetailView: View {
|
|||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(.secondary)
|
||||
TextField("Search bookmarks...", text: $searchText)
|
||||
TextField(LocalizedStringKey("Search bookmarks..."), text: $searchText)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
.foregroundColor(.primary)
|
||||
if !searchText.isEmpty {
|
||||
|
|
@ -258,6 +264,7 @@ struct CollectionDetailView: View {
|
|||
)) {
|
||||
BookmarkGridItemView(item: bookmark, module: module)
|
||||
}
|
||||
.isDetailLink(true)
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
libraryManager.removeBookmarkFromCollection(bookmarkId: bookmark.id, collectionId: collection.id)
|
||||
|
|
@ -277,13 +284,41 @@ struct CollectionDetailView: View {
|
|||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onAppear {
|
||||
isActive = true
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first,
|
||||
let navigationController = window.rootViewController?.children.first as? UINavigationController {
|
||||
navigationController.interactivePopGestureRecognizer?.isEnabled = true
|
||||
navigationController.interactivePopGestureRecognizer?.delegate = nil
|
||||
}
|
||||
tabBarController.showTabBar()
|
||||
|
||||
let isMediaInfoActive = UserDefaults.standard.bool(forKey: "isMediaInfoActive")
|
||||
let isReaderActive = UserDefaults.standard.bool(forKey: "isReaderActive")
|
||||
if !isMediaInfoActive && !isReaderActive {
|
||||
NotificationCenter.default.post(name: .showTabBar, object: nil)
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
|
||||
let isMediaInfoActive = UserDefaults.standard.bool(forKey: "isMediaInfoActive")
|
||||
let isReaderActive = UserDefaults.standard.bool(forKey: "isReaderActive")
|
||||
if !isMediaInfoActive && !isReaderActive {
|
||||
NotificationCenter.default.post(name: .showTabBar, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()) { _ in
|
||||
let isMediaInfoActive = UserDefaults.standard.bool(forKey: "isMediaInfoActive")
|
||||
let isReaderActive = UserDefaults.standard.bool(forKey: "isReaderActive")
|
||||
if isActive && !isMediaInfoActive && !isReaderActive {
|
||||
NotificationCenter.default.post(name: .showTabBar, object: nil)
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
|
||||
isActive = true
|
||||
let isMediaInfoActive = UserDefaults.standard.bool(forKey: "isMediaInfoActive")
|
||||
let isReaderActive = UserDefaults.standard.bool(forKey: "isReaderActive")
|
||||
if !isMediaInfoActive && !isReaderActive {
|
||||
NotificationCenter.default.post(name: .showTabBar, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ struct CollectionPickerView: View {
|
|||
if isShowingNewCollectionField {
|
||||
Section {
|
||||
HStack {
|
||||
TextField("Collection name", text: $newCollectionName)
|
||||
Button("Create") {
|
||||
TextField(LocalizedStringKey("Collection name"), text: $newCollectionName)
|
||||
Button(LocalizedStringKey("Create")) {
|
||||
if !newCollectionName.isEmpty {
|
||||
libraryManager.createCollection(name: newCollectionName)
|
||||
if let newCollection = libraryManager.collections.first(where: { $0.name == newCollectionName }) {
|
||||
|
|
@ -52,11 +52,11 @@ struct CollectionPickerView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Add to Collection")
|
||||
.navigationTitle(LocalizedStringKey("Add to Collection"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
Button(LocalizedStringKey("Cancel")) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
181
Sora/Views/LibraryView/ContinueReadingSection.swift
Normal file
181
Sora/Views/LibraryView/ContinueReadingSection.swift
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
//
|
||||
// ContinueReadingSection.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 26/06/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
|
||||
struct ContinueReadingSection: View {
|
||||
@Binding var items: [ContinueReadingItem]
|
||||
var markAsRead: (ContinueReadingItem) -> Void
|
||||
var removeItem: (ContinueReadingItem) -> Void
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 12) {
|
||||
ForEach(Array(items.prefix(5))) { item in
|
||||
ContinueReadingCell(item: item, markAsRead: {
|
||||
markAsRead(item)
|
||||
}, removeItem: {
|
||||
removeItem(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.frame(height: 157.03)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContinueReadingCell: View {
|
||||
let item: ContinueReadingItem
|
||||
var markAsRead: () -> Void
|
||||
var removeItem: () -> Void
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@State private var imageLoadError: Bool = false
|
||||
|
||||
private var imageURL: URL {
|
||||
print("Processing image URL in ContinueReadingCell: \(item.imageUrl)")
|
||||
|
||||
if !item.imageUrl.isEmpty {
|
||||
if let url = URL(string: item.imageUrl) {
|
||||
print("Valid direct URL: \(url)")
|
||||
return url
|
||||
}
|
||||
|
||||
if let encodedUrlString = item.imageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
|
||||
let url = URL(string: encodedUrlString) {
|
||||
print("Using encoded URL: \(encodedUrlString)")
|
||||
return url
|
||||
}
|
||||
|
||||
if item.imageUrl.hasPrefix("http://") {
|
||||
let httpsUrl = "https://" + item.imageUrl.dropFirst(7)
|
||||
if let url = URL(string: httpsUrl) {
|
||||
print("Using https URL: \(httpsUrl)")
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print("Using fallback URL")
|
||||
return URL(string: "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png")!
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(destination: ReaderView(
|
||||
moduleId: item.moduleId,
|
||||
chapterHref: item.href,
|
||||
chapterTitle: item.chapterTitle,
|
||||
chapters: [],
|
||||
mediaTitle: item.mediaTitle,
|
||||
chapterNumber: item.chapterNumber
|
||||
)) {
|
||||
ZStack {
|
||||
LazyImage(url: imageURL) { state in
|
||||
if let image = state.imageContainer?.image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 280, height: 157.03)
|
||||
.blur(radius: 3)
|
||||
.opacity(0.7)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 280, height: 157.03)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
print("Background image loading: \(imageURL)")
|
||||
}
|
||||
|
||||
Rectangle()
|
||||
.fill(LinearGradient(
|
||||
gradient: Gradient(colors: [Color.black.opacity(0.7), Color.black.opacity(0.4)]),
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
))
|
||||
.frame(width: 280, height: 157.03)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("\(Int(item.progress * 100))%")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.white)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal, 8)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.cornerRadius(4)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Chapter \(item.chapterNumber)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
|
||||
Text(item.mediaTitle)
|
||||
.font(.headline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.frame(width: 170, alignment: .leading)
|
||||
|
||||
LazyImage(url: imageURL) { state in
|
||||
if let image = state.imageContainer?.image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 110, height: 157.03)
|
||||
.clipped()
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 110, height: 157.03)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
print("Right image loading: \(imageURL)")
|
||||
}
|
||||
.onDisappear {
|
||||
print("Right image disappeared")
|
||||
}
|
||||
.frame(width: 110, height: 157.03)
|
||||
}
|
||||
}
|
||||
.frame(width: 280, height: 157.03)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color.gray.opacity(0.3), lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
markAsRead()
|
||||
}) {
|
||||
Label("Mark as Read", systemImage: "checkmark.circle")
|
||||
}
|
||||
Button(role: .destructive, action: {
|
||||
removeItem()
|
||||
}) {
|
||||
Label("Remove Item", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
print("ContinueReadingCell appeared for: \(item.mediaTitle)")
|
||||
print("Image URL: \(item.imageUrl)")
|
||||
print("Chapter: \(item.chapterNumber)")
|
||||
print("Progress: \(item.progress)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
import Nuke
|
||||
|
||||
struct BookmarkCollection: Codable, Identifiable {
|
||||
let id: UUID
|
||||
|
|
@ -55,6 +57,7 @@ class LibraryManager: ObservableObject {
|
|||
loadCollections()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleiCloudSync), name: .iCloudSyncDidComplete, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleModuleRemoval), name: .moduleRemoved, object: nil)
|
||||
}
|
||||
|
||||
@objc private func handleiCloudSync() {
|
||||
|
|
@ -63,6 +66,30 @@ class LibraryManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func handleModuleRemoval(_ notification: Notification) {
|
||||
if let moduleId = notification.object as? String {
|
||||
cleanupBookmarksForModule(moduleId: moduleId)
|
||||
}
|
||||
}
|
||||
|
||||
private func cleanupBookmarksForModule(moduleId: String) {
|
||||
var didChange = false
|
||||
|
||||
for (collectionIndex, collection) in collections.enumerated() {
|
||||
let originalCount = collection.bookmarks.count
|
||||
collections[collectionIndex].bookmarks.removeAll { $0.moduleId == moduleId }
|
||||
|
||||
if collections[collectionIndex].bookmarks.count != originalCount {
|
||||
didChange = true
|
||||
}
|
||||
}
|
||||
|
||||
if didChange {
|
||||
ImagePipeline.shared.cache.removeAll()
|
||||
saveCollections()
|
||||
}
|
||||
}
|
||||
|
||||
private func migrateOldBookmarks() {
|
||||
guard let data = UserDefaults.standard.data(forKey: oldBookmarksKey) else {
|
||||
return
|
||||
|
|
@ -71,16 +98,13 @@ class LibraryManager: ObservableObject {
|
|||
do {
|
||||
let oldBookmarks = try JSONDecoder().decode([LibraryItem].self, from: data)
|
||||
if !oldBookmarks.isEmpty {
|
||||
// Check if "Old Bookmarks" collection already exists
|
||||
if let existingIndex = collections.firstIndex(where: { $0.name == "Old Bookmarks" }) {
|
||||
// Add new bookmarks to existing collection, avoiding duplicates
|
||||
for bookmark in oldBookmarks {
|
||||
if !collections[existingIndex].bookmarks.contains(where: { $0.href == bookmark.href }) {
|
||||
collections[existingIndex].bookmarks.insert(bookmark, at: 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create new "Old Bookmarks" collection
|
||||
let oldCollection = BookmarkCollection(name: "Old Bookmarks", bookmarks: oldBookmarks)
|
||||
collections.append(oldCollection)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,19 +12,34 @@ import SwiftUI
|
|||
struct LibraryView: View {
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
@EnvironmentObject var tabBarController: TabBarController
|
||||
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
|
||||
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4
|
||||
@AppStorage("librarySectionsOrderData") private var librarySectionsOrderData: Data = {
|
||||
try! JSONEncoder().encode(["continueWatching", "continueReading", "collections"])
|
||||
}()
|
||||
@AppStorage("disabledLibrarySectionsData") private var disabledLibrarySectionsData: Data = {
|
||||
try! JSONEncoder().encode([String]())
|
||||
}()
|
||||
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
|
||||
@State private var continueWatchingItems: [ContinueWatchingItem] = []
|
||||
@State private var continueReadingItems: [ContinueReadingItem] = []
|
||||
@State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
|
||||
@State private var selectedTab: Int = 0
|
||||
|
||||
private var librarySectionsOrder: [String] {
|
||||
(try? JSONDecoder().decode([String].self, from: librarySectionsOrderData)) ?? ["continueWatching", "continueReading", "collections"]
|
||||
}
|
||||
|
||||
private var disabledLibrarySections: [String] {
|
||||
(try? JSONDecoder().decode([String].self, from: disabledLibrarySectionsData)) ?? []
|
||||
}
|
||||
|
||||
private let columns = [
|
||||
GridItem(.adaptive(minimum: 150), spacing: 12)
|
||||
]
|
||||
|
|
@ -56,81 +71,26 @@ struct LibraryView: View {
|
|||
ZStack {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text("Library")
|
||||
Text(LocalizedStringKey("Library"))
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
HStack {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "play.fill")
|
||||
.font(.subheadline)
|
||||
Text("Continue Watching")
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
NavigationLink(destination: AllWatchingView()) {
|
||||
Text("View All")
|
||||
.font(.subheadline)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.gray.opacity(0.2))
|
||||
.cornerRadius(15)
|
||||
.gradientOutline()
|
||||
|
||||
ForEach(librarySectionsOrder, id: \.self) { section in
|
||||
if !disabledLibrarySections.contains(section) {
|
||||
switch section {
|
||||
case "continueWatching":
|
||||
continueWatchingSection
|
||||
case "continueReading":
|
||||
continueReadingSection
|
||||
case "collections":
|
||||
collectionsSection
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
if continueWatchingItems.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "play.circle")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("Nothing to Continue Watching")
|
||||
.font(.headline)
|
||||
Text("Your recently watched content will appear here")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
ContinueWatchingSection(items: $continueWatchingItems, markAsWatched: {
|
||||
item in
|
||||
markContinueWatchingItemAsWatched(item: item)
|
||||
}, removeItem: {
|
||||
item in
|
||||
removeContinueWatchingItem(item: item)
|
||||
})
|
||||
}
|
||||
|
||||
HStack {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "folder.fill")
|
||||
.font(.subheadline)
|
||||
Text("Collections")
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
NavigationLink(destination: BookmarksDetailView()) {
|
||||
Text("View All")
|
||||
.font(.subheadline)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.gray.opacity(0.2))
|
||||
.cornerRadius(15)
|
||||
.gradientOutline()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
BookmarksSection()
|
||||
|
||||
Spacer().frame(height: 100)
|
||||
}
|
||||
|
|
@ -140,16 +100,160 @@ struct LibraryView: View {
|
|||
.deviceScaled()
|
||||
.onAppear {
|
||||
fetchContinueWatching()
|
||||
tabBarController.showTabBar()
|
||||
fetchContinueReading()
|
||||
|
||||
NotificationCenter.default.post(name: .showTabBar, object: nil)
|
||||
}
|
||||
.onChange(of: scenePhase) { newPhase in
|
||||
if newPhase == .active {
|
||||
fetchContinueWatching()
|
||||
fetchContinueReading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Section Views
|
||||
|
||||
private var continueWatchingSection: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "play.fill")
|
||||
.font(.subheadline)
|
||||
Text(LocalizedStringKey("Continue Watching"))
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
NavigationLink(destination: AllWatchingView()) {
|
||||
Text(LocalizedStringKey("View All"))
|
||||
.font(.subheadline)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.gray.opacity(0.2))
|
||||
.cornerRadius(15)
|
||||
.gradientOutline()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
if continueWatchingItems.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "play.circle")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text(LocalizedStringKey("Nothing to Continue Watching"))
|
||||
.font(.headline)
|
||||
Text(LocalizedStringKey("Your recently watched content will appear here"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
ContinueWatchingSection(items: $continueWatchingItems, markAsWatched: {
|
||||
item in
|
||||
markContinueWatchingItemAsWatched(item: item)
|
||||
}, removeItem: {
|
||||
item in
|
||||
removeContinueWatchingItem(item: item)
|
||||
})
|
||||
}
|
||||
|
||||
Spacer().frame(height: 20)
|
||||
}
|
||||
}
|
||||
|
||||
private var continueReadingSection: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "book.fill")
|
||||
.font(.subheadline)
|
||||
Text(LocalizedStringKey("Continue Reading"))
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
NavigationLink(destination: AllReadingView()) {
|
||||
Text(LocalizedStringKey("View All"))
|
||||
.font(.subheadline)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.gray.opacity(0.2))
|
||||
.cornerRadius(15)
|
||||
.gradientOutline()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
if continueReadingItems.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "book.closed")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text(LocalizedStringKey("Nothing to Continue Reading"))
|
||||
.font(.headline)
|
||||
Text(LocalizedStringKey("Your recently read novels will appear here"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
ContinueReadingSection(items: $continueReadingItems, markAsRead: {
|
||||
item in
|
||||
markContinueReadingItemAsRead(item: item)
|
||||
}, removeItem: {
|
||||
item in
|
||||
removeContinueReadingItem(item: item)
|
||||
})
|
||||
}
|
||||
|
||||
Spacer().frame(height: 20)
|
||||
}
|
||||
}
|
||||
|
||||
private var collectionsSection: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "folder.fill")
|
||||
.font(.subheadline)
|
||||
Text(LocalizedStringKey("Collections"))
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
NavigationLink(destination: BookmarksDetailView()) {
|
||||
Text(LocalizedStringKey("View All"))
|
||||
.font(.subheadline)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.gray.opacity(0.2))
|
||||
.cornerRadius(15)
|
||||
.gradientOutline()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
BookmarksSection()
|
||||
|
||||
Spacer().frame(height: 20)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchContinueWatching() {
|
||||
|
|
@ -174,6 +278,30 @@ struct LibraryView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func fetchContinueReading() {
|
||||
continueReadingItems = ContinueReadingManager.shared.fetchItems()
|
||||
Logger.shared.log("Fetched \(continueReadingItems.count) continue reading items", type: "Debug")
|
||||
|
||||
if !continueReadingItems.isEmpty {
|
||||
for (index, item) in continueReadingItems.enumerated() {
|
||||
Logger.shared.log("Reading item \(index): \(item.mediaTitle), chapter \(item.chapterNumber), progress \(item.progress)", type: "Debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func markContinueReadingItemAsRead(item: ContinueReadingItem) {
|
||||
UserDefaults.standard.set(1.0, forKey: "readingProgress_\(item.href)")
|
||||
ContinueReadingManager.shared.updateProgress(for: item.href, progress: 1.0)
|
||||
fetchContinueReading()
|
||||
}
|
||||
|
||||
private func removeContinueReadingItem(item: ContinueReadingItem) {
|
||||
ContinueReadingManager.shared.remove(item: item)
|
||||
continueReadingItems.removeAll {
|
||||
$0.id == item.id
|
||||
}
|
||||
}
|
||||
|
||||
private func updateOrientation() {
|
||||
DispatchQueue.main.async {
|
||||
isLandscape = UIDevice.current.orientation.isLandscape
|
||||
|
|
|
|||
|
|
@ -11,6 +11,28 @@ struct ChapterCell: View {
|
|||
let chapterNumber: String
|
||||
let chapterTitle: String
|
||||
let isCurrentChapter: Bool
|
||||
var progress: Double = 0.0
|
||||
var href: String = ""
|
||||
|
||||
private var progressText: String {
|
||||
if progress >= 0.98 {
|
||||
return "Completed"
|
||||
} else if progress > 0 {
|
||||
return "\(Int(progress * 100))%"
|
||||
} else {
|
||||
return "New"
|
||||
}
|
||||
}
|
||||
|
||||
private var progressColor: Color {
|
||||
if progress >= 0.98 {
|
||||
return .green
|
||||
} else if progress > 0 {
|
||||
return .blue
|
||||
} else {
|
||||
return .secondary
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
|
|
@ -21,15 +43,15 @@ struct ChapterCell: View {
|
|||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
|
||||
if isCurrentChapter {
|
||||
Text("Current")
|
||||
if progress > 0 {
|
||||
Text(progressText)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(.blue)
|
||||
.foregroundColor(progressColor)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 3)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(Color.blue.opacity(0.18))
|
||||
.fill(progressColor.opacity(0.18))
|
||||
)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
|
|
@ -38,6 +60,13 @@ struct ChapterCell: View {
|
|||
.font(.system(size: 15))
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(2)
|
||||
|
||||
if progress > 0 && progress < 0.98 {
|
||||
ProgressView(value: progress)
|
||||
.progressViewStyle(LinearProgressViewStyle())
|
||||
.frame(height: 3)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
|
@ -70,9 +99,27 @@ struct ChapterCell: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
ChapterCell(
|
||||
chapterNumber: "1",
|
||||
chapterTitle: "Chapter 1: The Beginning",
|
||||
isCurrentChapter: true
|
||||
)
|
||||
VStack(spacing: 16) {
|
||||
ChapterCell(
|
||||
chapterNumber: "1",
|
||||
chapterTitle: "Chapter 1: The Beginning",
|
||||
isCurrentChapter: false,
|
||||
progress: 0.0
|
||||
)
|
||||
|
||||
ChapterCell(
|
||||
chapterNumber: "2",
|
||||
chapterTitle: "Chapter 2: The Journey",
|
||||
isCurrentChapter: false,
|
||||
progress: 0.45
|
||||
)
|
||||
|
||||
ChapterCell(
|
||||
chapterNumber: "3",
|
||||
chapterTitle: "Chapter 3: The Conclusion",
|
||||
isCurrentChapter: false,
|
||||
progress: 1.0
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ struct MediaInfoView: View {
|
|||
@ObservedObject private var jsController = JSController.shared
|
||||
@EnvironmentObject var moduleManager: ModuleManager
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
@EnvironmentObject var tabBarController: TabBarController
|
||||
@ObservedObject private var navigator = ChapterNavigator.shared
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
|
@ -185,6 +185,16 @@ struct MediaInfoView: View {
|
|||
.ignoresSafeArea(.container, edges: .top)
|
||||
.onAppear {
|
||||
setupViewOnAppear()
|
||||
NotificationCenter.default.post(name: .hideTabBar, object: nil)
|
||||
UserDefaults.standard.set(true, forKey: "isMediaInfoActive")
|
||||
// swipe back
|
||||
/*
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first,
|
||||
let navigationController = window.rootViewController?.children.first as? UINavigationController {
|
||||
navigationController.interactivePopGestureRecognizer?.isEnabled = false
|
||||
}
|
||||
*/
|
||||
}
|
||||
.onChange(of: selectedRange) { newValue in
|
||||
UserDefaults.standard.set(newValue.lowerBound, forKey: selectedRangeKey)
|
||||
|
|
@ -196,9 +206,10 @@ struct MediaInfoView: View {
|
|||
UserDefaults.standard.set(newValue.lowerBound, forKey: selectedChapterRangeKey)
|
||||
}
|
||||
.onDisappear {
|
||||
tabBarController.showTabBar()
|
||||
currentFetchTask?.cancel()
|
||||
activeFetchID = nil
|
||||
UserDefaults.standard.set(false, forKey: "isMediaInfoActive")
|
||||
UIScrollView.appearance().bounces = true
|
||||
}
|
||||
.task {
|
||||
await setupInitialData()
|
||||
|
|
@ -229,14 +240,13 @@ struct MediaInfoView: View {
|
|||
VStack {
|
||||
HStack {
|
||||
Button(action: {
|
||||
tabBarController.showTabBar()
|
||||
dismiss()
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 24))
|
||||
.foregroundColor(.primary)
|
||||
.padding(12)
|
||||
.background(Color.gray.opacity(0.2))
|
||||
.background(Color(.systemBackground).opacity(0.8))
|
||||
.clipShape(Circle())
|
||||
.circularGradientOutline()
|
||||
}
|
||||
|
|
@ -305,7 +315,7 @@ struct MediaInfoView: View {
|
|||
|
||||
if !aliases.isEmpty && !(module.metadata.novel ?? false) {
|
||||
Text(aliases)
|
||||
.font(.system(size: 14))
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
|
|
@ -354,7 +364,7 @@ struct MediaInfoView: View {
|
|||
Image(systemName: "calendar")
|
||||
.foregroundColor(.accentColor)
|
||||
Text(airdate)
|
||||
.font(.system(size: 14))
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundColor(.accentColor)
|
||||
Spacer()
|
||||
}
|
||||
|
|
@ -390,7 +400,7 @@ struct MediaInfoView: View {
|
|||
private var synopsisSection: some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(synopsis)
|
||||
.font(.system(size: 16))
|
||||
.font(.system(size: 16, weight: .bold))
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(showFullSynopsis ? nil : 3)
|
||||
.animation(nil, value: showFullSynopsis)
|
||||
|
|
@ -418,7 +428,7 @@ struct MediaInfoView: View {
|
|||
Image(systemName: "play.fill")
|
||||
.foregroundColor(colorScheme == .dark ? .black : .white)
|
||||
Text(startActionText)
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.font(.system(size: 16, weight: .bold))
|
||||
.foregroundColor(colorScheme == .dark ? .black : .white)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
|
@ -530,10 +540,69 @@ struct MediaInfoView: View {
|
|||
if episodeLinks.count != 1 {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
episodesSectionHeader
|
||||
if isGroupedBySeasons || episodeLinks.count > episodeChunkSize {
|
||||
HStack(spacing: 8) {
|
||||
if isGroupedBySeasons {
|
||||
seasonSelectorStyled
|
||||
}
|
||||
Spacer()
|
||||
if episodeLinks.count > episodeChunkSize {
|
||||
rangeSelectorStyled
|
||||
.padding(.trailing, 4)
|
||||
}
|
||||
}
|
||||
.padding(.top, -8)
|
||||
}
|
||||
episodeListSection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var seasonSelectorStyled: some View {
|
||||
let seasons = groupedEpisodes()
|
||||
if seasons.count > 1 {
|
||||
Menu {
|
||||
ForEach(0..<seasons.count, id: \..self) { index in
|
||||
Button(action: { selectedSeason = index }) {
|
||||
Text(String(format: NSLocalizedString("Season %d", comment: ""), index + 1))
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
Text("Season \(selectedSeason + 1)")
|
||||
.font(.system(size: 15, weight: .bold))
|
||||
.foregroundColor(.accentColor)
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.system(size: 13, weight: .bold))
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var rangeSelectorStyled: some View {
|
||||
Menu {
|
||||
ForEach(generateRanges(), id: \..self) { range in
|
||||
Button(action: { selectedRange = range }) {
|
||||
Text("\(range.lowerBound + 1)-\(range.upperBound)")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
Text("\(selectedRange.lowerBound + 1)-\(selectedRange.upperBound)")
|
||||
.font(.system(size: 15, weight: .bold))
|
||||
.foregroundColor(.accentColor)
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.system(size: 13, weight: .bold))
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var episodesSectionHeader: some View {
|
||||
|
|
@ -544,8 +613,6 @@ struct MediaInfoView: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
episodeNavigationSection
|
||||
|
||||
HStack(spacing: 4) {
|
||||
sourceButton
|
||||
menuButton
|
||||
|
|
@ -553,52 +620,6 @@ struct MediaInfoView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var episodeNavigationSection: some View {
|
||||
Group {
|
||||
if !isGroupedBySeasons && episodeLinks.count <= episodeChunkSize {
|
||||
EmptyView()
|
||||
} else if !isGroupedBySeasons && episodeLinks.count > episodeChunkSize {
|
||||
rangeSelectionMenu
|
||||
} else if isGroupedBySeasons {
|
||||
seasonSelectionMenu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var rangeSelectionMenu: some View {
|
||||
Menu {
|
||||
ForEach(generateRanges(), id: \.self) { range in
|
||||
Button(action: { selectedRange = range }) {
|
||||
Text("\(range.lowerBound + 1)-\(range.upperBound)")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("\(selectedRange.lowerBound + 1)-\(selectedRange.upperBound)")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var seasonSelectionMenu: some View {
|
||||
let seasons = groupedEpisodes()
|
||||
if seasons.count > 1 {
|
||||
Menu {
|
||||
ForEach(0..<seasons.count, id: \.self) { index in
|
||||
Button(action: { selectedSeason = index }) {
|
||||
Text(String(format: NSLocalizedString("Season %d", comment: ""), index + 1))
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("Season \(selectedSeason + 1)")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var episodeListSection: some View {
|
||||
Group {
|
||||
|
|
@ -678,43 +699,37 @@ struct MediaInfoView: View {
|
|||
Image(systemName: "calendar")
|
||||
.foregroundColor(.accentColor)
|
||||
Text(airdate)
|
||||
.font(.system(size: 14))
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundColor(.accentColor)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
if !aliases.isEmpty {
|
||||
Text(aliases)
|
||||
.font(.system(size: 14))
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text(NSLocalizedString("Chapters", comment: ""))
|
||||
.font(.system(size: 22, weight: .bold))
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
if chapters.count > chapterChunkSize {
|
||||
Menu {
|
||||
ForEach(generateChapterRanges(), id: \..self) { range in
|
||||
Button(action: { selectedChapterRange = range }) {
|
||||
Text("\(range.lowerBound + 1)-\(range.upperBound)")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("\(selectedChapterRange.lowerBound + 1)-\(selectedChapterRange.upperBound)")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
sourceButton
|
||||
menuButton
|
||||
}
|
||||
}
|
||||
LazyVStack(spacing: 15) {
|
||||
ForEach(chapters.indices.filter { selectedChapterRange.contains($0) }, id: \..self) { i in
|
||||
let chapter = chapters[i]
|
||||
if chapters.count > chapterChunkSize {
|
||||
HStack {
|
||||
Spacer()
|
||||
chapterRangeSelectorStyled
|
||||
}
|
||||
.padding(.bottom, 0)
|
||||
}
|
||||
LazyVStack(spacing: 15) {
|
||||
ForEach(chapters.indices.filter { selectedChapterRange.contains($0) }, id: \..self) { i in
|
||||
let chapter = chapters[i]
|
||||
let _ = refreshTrigger
|
||||
if let href = chapter["href"] as? String,
|
||||
let number = chapter["number"] as? Int,
|
||||
let title = chapter["title"] as? String {
|
||||
|
|
@ -722,21 +737,83 @@ struct MediaInfoView: View {
|
|||
destination: ReaderView(
|
||||
moduleId: module.id.uuidString,
|
||||
chapterHref: href,
|
||||
chapterTitle: title
|
||||
chapterTitle: title,
|
||||
chapters: chapters,
|
||||
mediaTitle: self.title,
|
||||
chapterNumber: number
|
||||
)
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
ChapterNavigator.shared.currentChapter = nil
|
||||
}
|
||||
}
|
||||
) {
|
||||
ChapterCell(
|
||||
chapterNumber: String(number),
|
||||
chapterTitle: title,
|
||||
isCurrentChapter: UserDefaults.standard.string(forKey: "lastReadChapter") == href
|
||||
isCurrentChapter: false,
|
||||
progress: UserDefaults.standard.double(forKey: "readingProgress_\(href)"),
|
||||
href: href
|
||||
)
|
||||
}
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
UserDefaults.standard.set(true, forKey: "navigatingToReaderView")
|
||||
ChapterNavigator.shared.currentChapter = (
|
||||
moduleId: module.id.uuidString,
|
||||
href: href,
|
||||
title: title,
|
||||
chapters: chapters,
|
||||
mediaTitle: self.title,
|
||||
chapterNumber: number
|
||||
)
|
||||
})
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
markChapterAsRead(href: href, number: number)
|
||||
}) {
|
||||
Label("Mark as Read", systemImage: "checkmark.circle")
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
resetChapterProgress(href: href)
|
||||
}) {
|
||||
Label("Reset Progress", systemImage: "arrow.counterclockwise")
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
markAllPreviousChaptersAsRead(currentNumber: number)
|
||||
}) {
|
||||
Label("Mark Previous as Read", systemImage: "checkmark.circle.badge.plus")
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var chapterRangeSelectorStyled: some View {
|
||||
Menu {
|
||||
ForEach(generateChapterRanges(), id: \..self) { range in
|
||||
Button(action: { selectedChapterRange = range }) {
|
||||
Text("\(range.lowerBound + 1)-\(range.upperBound)")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
Text("\(selectedChapterRange.lowerBound + 1)-\(selectedChapterRange.upperBound)")
|
||||
.font(.system(size: 15, weight: .bold))
|
||||
.foregroundColor(.accentColor)
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.system(size: 13, weight: .bold))
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var noContentSection: some View {
|
||||
|
|
@ -859,7 +936,6 @@ struct MediaInfoView: View {
|
|||
|
||||
private func setupViewOnAppear() {
|
||||
buttonRefreshTrigger.toggle()
|
||||
tabBarController.hideTabBar()
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first,
|
||||
|
|
@ -872,14 +948,22 @@ struct MediaInfoView: View {
|
|||
private func setupInitialData() async {
|
||||
do {
|
||||
Logger.shared.log("setupInitialData: module.metadata.novel = \(String(describing: module.metadata.novel))", type: "Debug")
|
||||
|
||||
UserDefaults.standard.set(imageUrl, forKey: "mediaInfoImageUrl_\(module.id.uuidString)")
|
||||
Logger.shared.log("Saved MediaInfoView image URL: \(imageUrl) for module \(module.id.uuidString)", type: "Debug")
|
||||
|
||||
|
||||
|
||||
if module.metadata.novel == true {
|
||||
DispatchQueue.main.async {
|
||||
DropManager.shared.showDrop(
|
||||
title: "Fetching Data",
|
||||
subtitle: "Please wait while fetching.",
|
||||
duration: 0.5,
|
||||
icon: UIImage(systemName: "arrow.triangle.2.circlepath")
|
||||
)
|
||||
if !hasFetched {
|
||||
DispatchQueue.main.async {
|
||||
DropManager.shared.showDrop(
|
||||
title: "Fetching Data",
|
||||
subtitle: "Please wait while fetching.",
|
||||
duration: 0.5,
|
||||
icon: UIImage(systemName: "arrow.triangle.2.circlepath")
|
||||
)
|
||||
}
|
||||
}
|
||||
let jsContent = try? moduleManager.getModuleContent(module)
|
||||
if let jsContent = jsContent {
|
||||
|
|
@ -949,12 +1033,14 @@ struct MediaInfoView: View {
|
|||
if let savedPoster = UserDefaults.standard.string(forKey: "tmdbPosterURL_\(href)") {
|
||||
imageUrl = savedPoster
|
||||
}
|
||||
DropManager.shared.showDrop(
|
||||
title: "Fetching Data",
|
||||
subtitle: "Please wait while fetching.",
|
||||
duration: 0.5,
|
||||
icon: UIImage(systemName: "arrow.triangle.2.circlepath")
|
||||
)
|
||||
if !hasFetched {
|
||||
DropManager.shared.showDrop(
|
||||
title: "Fetching Data",
|
||||
subtitle: "Please wait while fetching.",
|
||||
duration: 0.5,
|
||||
icon: UIImage(systemName: "arrow.triangle.2.circlepath")
|
||||
)
|
||||
}
|
||||
fetchDetails()
|
||||
if savedCustomID != 0 {
|
||||
itemID = savedCustomID
|
||||
|
|
@ -969,10 +1055,10 @@ struct MediaInfoView: View {
|
|||
additionalData: ["title": title]
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
} catch let loadError {
|
||||
isError = true
|
||||
isLoading = false
|
||||
Logger.shared.log("Error loading media info: \(error)", type: "Error")
|
||||
Logger.shared.log("Error loading media info: \(loadError)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2199,4 +2285,74 @@ struct MediaInfoView: View {
|
|||
}
|
||||
return ranges
|
||||
}
|
||||
|
||||
private func markChapterAsRead(href: String, number: Int) {
|
||||
UserDefaults.standard.set(1.0, forKey: "readingProgress_\(href)")
|
||||
|
||||
UserDefaults.standard.set(1.0, forKey: "scrollPosition_\(href)")
|
||||
|
||||
ContinueReadingManager.shared.updateProgress(for: href, progress: 1.0)
|
||||
|
||||
DropManager.shared.showDrop(
|
||||
title: "Chapter \(number) Marked as Read",
|
||||
subtitle: "",
|
||||
duration: 1.0,
|
||||
icon: UIImage(systemName: "checkmark.circle.fill")
|
||||
)
|
||||
refreshTrigger.toggle()
|
||||
}
|
||||
|
||||
private func resetChapterProgress(href: String) {
|
||||
UserDefaults.standard.set(0.0, forKey: "readingProgress_\(href)")
|
||||
|
||||
UserDefaults.standard.removeObject(forKey: "scrollPosition_\(href)")
|
||||
|
||||
ContinueReadingManager.shared.updateProgress(for: href, progress: 0.0)
|
||||
|
||||
DropManager.shared.showDrop(
|
||||
title: "Progress Reset",
|
||||
subtitle: "",
|
||||
duration: 1.0,
|
||||
icon: UIImage(systemName: "arrow.counterclockwise")
|
||||
)
|
||||
refreshTrigger.toggle()
|
||||
}
|
||||
|
||||
private func markAllPreviousChaptersAsRead(currentNumber: Int) {
|
||||
let userDefaults = UserDefaults.standard
|
||||
var markedCount = 0
|
||||
|
||||
for chapter in chapters {
|
||||
if let number = chapter["number"] as? Int,
|
||||
let href = chapter["href"] as? String {
|
||||
if number < currentNumber {
|
||||
userDefaults.set(1.0, forKey: "readingProgress_\(href)")
|
||||
|
||||
userDefaults.set(1.0, forKey: "scrollPosition_\(href)")
|
||||
|
||||
ContinueReadingManager.shared.updateProgress(for: href, progress: 1.0)
|
||||
markedCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userDefaults.synchronize()
|
||||
|
||||
DropManager.shared.showDrop(
|
||||
title: "Marked \(markedCount) Chapters as Read",
|
||||
subtitle: "",
|
||||
duration: 1.0,
|
||||
icon: UIImage(systemName: "checkmark.circle.fill")
|
||||
)
|
||||
|
||||
refreshTrigger.toggle()
|
||||
}
|
||||
|
||||
private func simultaneousGesture(for item: NavigationLink<some View, some View>) -> some View {
|
||||
item.simultaneousGesture(TapGesture().onEnded {
|
||||
UserDefaults.standard.set(true, forKey: "navigatingToReaderView")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -14,6 +14,7 @@ struct SearchResultsGrid: View {
|
|||
@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 = ""
|
||||
|
||||
|
|
@ -35,7 +36,12 @@ struct SearchResultsGrid: View {
|
|||
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)) {
|
||||
NavigationLink(destination:
|
||||
MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: selectedModule)
|
||||
.onDisappear {
|
||||
|
||||
}
|
||||
) {
|
||||
ZStack {
|
||||
LazyImage(url: URL(string: item.imageUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
|
|
@ -81,6 +87,7 @@ struct SearchResultsGrid: View {
|
|||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.padding(4)
|
||||
}
|
||||
.isDetailLink(true)
|
||||
.id(item.href)
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ struct SearchView: View {
|
|||
|
||||
@StateObject private var jsController = JSController.shared
|
||||
@EnvironmentObject var moduleManager: ModuleManager
|
||||
@EnvironmentObject var tabBarController: TabBarController
|
||||
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||
|
||||
@Binding public var searchQuery: String
|
||||
|
|
@ -38,6 +38,7 @@ struct SearchView: View {
|
|||
@State private var isSearchFieldFocused = false
|
||||
@State private var saveDebounceTimer: Timer?
|
||||
@State private var searchDebounceTimer: Timer?
|
||||
@State private var isActive: Bool = false
|
||||
|
||||
init(searchQuery: Binding<String>) {
|
||||
self._searchQuery = searchQuery
|
||||
|
|
@ -75,7 +76,7 @@ struct SearchView: View {
|
|||
private var mainContent: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("Search")
|
||||
Text(LocalizedStringKey("Search"))
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
|
||||
|
|
@ -138,11 +139,51 @@ struct SearchView: View {
|
|||
}
|
||||
}
|
||||
.onAppear {
|
||||
isActive = true
|
||||
loadSearchHistory()
|
||||
if !searchQuery.isEmpty {
|
||||
performSearch()
|
||||
}
|
||||
tabBarController.showTabBar()
|
||||
let isMediaInfoActive = UserDefaults.standard.bool(forKey: "isMediaInfoActive")
|
||||
let isReaderActive = UserDefaults.standard.bool(forKey: "isReaderActive")
|
||||
if !isMediaInfoActive && !isReaderActive {
|
||||
NotificationCenter.default.post(name: .showTabBar, object: nil)
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
|
||||
let isMediaInfoActive = UserDefaults.standard.bool(forKey: "isMediaInfoActive")
|
||||
let isReaderActive = UserDefaults.standard.bool(forKey: "isReaderActive")
|
||||
if !isMediaInfoActive && !isReaderActive {
|
||||
NotificationCenter.default.post(name: .showTabBar, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .searchQueryChanged,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { notification in
|
||||
if let query = notification.userInfo?["searchQuery"] as? String {
|
||||
searchQuery = query
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
NotificationCenter.default.removeObserver(self, name: .searchQueryChanged, object: nil)
|
||||
}
|
||||
.onReceive(Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()) { _ in
|
||||
let isMediaInfoActive = UserDefaults.standard.bool(forKey: "isMediaInfoActive")
|
||||
let isReaderActive = UserDefaults.standard.bool(forKey: "isReaderActive")
|
||||
if isActive && !isMediaInfoActive && !isReaderActive {
|
||||
NotificationCenter.default.post(name: .showTabBar, object: nil)
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
|
||||
isActive = true
|
||||
let isMediaInfoActive = UserDefaults.standard.bool(forKey: "isMediaInfoActive")
|
||||
let isReaderActive = UserDefaults.standard.bool(forKey: "isReaderActive")
|
||||
if !isMediaInfoActive && !isReaderActive {
|
||||
NotificationCenter.default.post(name: .showTabBar, object: nil)
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedModuleId) { _ in
|
||||
if !searchQuery.isEmpty {
|
||||
|
|
@ -302,6 +343,7 @@ struct SearchView: View {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
struct SearchBar: View {
|
||||
@State private var debounceTimer: Timer?
|
||||
@Binding var text: String
|
||||
|
|
@ -310,7 +352,7 @@ struct SearchBar: View {
|
|||
|
||||
var body: some View {
|
||||
HStack {
|
||||
TextField("Search...", text: $text, onEditingChanged: { isEditing in
|
||||
TextField(LocalizedStringKey("Search..."), text: $text, onEditingChanged: { isEditing in
|
||||
isFocused = isEditing
|
||||
}, onCommit: onSearchButtonClicked)
|
||||
.padding(7)
|
||||
|
|
|
|||
|
|
@ -78,10 +78,10 @@ struct SettingsViewAbout: View {
|
|||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Sora")
|
||||
Text(LocalizedStringKey("Sora"))
|
||||
.font(.title)
|
||||
.bold()
|
||||
Text("Also known as Sulfur")
|
||||
Text(LocalizedStringKey("Also known as Sulfur"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
|
@ -111,10 +111,10 @@ struct SettingsViewAbout: View {
|
|||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("cranci1")
|
||||
Text(LocalizedStringKey("cranci1"))
|
||||
.font(.headline)
|
||||
.foregroundColor(.indigo)
|
||||
Text("me frfr")
|
||||
Text(LocalizedStringKey("me frfr"))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
|
@ -137,7 +137,7 @@ struct SettingsViewAbout: View {
|
|||
}
|
||||
.padding(.vertical, 20)
|
||||
}
|
||||
.navigationTitle("About")
|
||||
.navigationTitle(LocalizedStringKey("About"))
|
||||
.scrollViewBottomPadding()
|
||||
}
|
||||
}
|
||||
|
|
@ -157,7 +157,7 @@ struct ContributorsView: View {
|
|||
}
|
||||
.padding(.vertical, 12)
|
||||
} else if error != nil {
|
||||
Text("Failed to load contributors")
|
||||
Text(LocalizedStringKey("Failed to load contributors"))
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.vertical, 12)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,261 @@
|
|||
//
|
||||
// SettingsViewBackup.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by paul on 29/06/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
fileprivate struct SettingsSection<Content: View>: View {
|
||||
let title: String
|
||||
let footer: String?
|
||||
let content: Content
|
||||
|
||||
init(title: String, footer: String? = nil, @ViewBuilder content: () -> Content) {
|
||||
self.title = title
|
||||
self.footer = footer
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title.uppercased())
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
content
|
||||
}
|
||||
.background(.ultraThinMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.strokeBorder(
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: Color.accentColor.opacity(0.3), location: 0),
|
||||
.init(color: Color.accentColor.opacity(0), location: 1)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
),
|
||||
lineWidth: 0.5
|
||||
)
|
||||
)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
if let footer = footer {
|
||||
Text(footer)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct SettingsActionRow: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let action: () -> Void
|
||||
var showDivider: Bool = true
|
||||
var color: Color = .accentColor
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack {
|
||||
Image(systemName: icon)
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundStyle(color)
|
||||
Text(title)
|
||||
.foregroundStyle(color)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.background(Color.clear)
|
||||
.contentShape(Rectangle())
|
||||
.overlay(
|
||||
VStack {
|
||||
if showDivider {
|
||||
Divider().padding(.leading, 56)
|
||||
}
|
||||
}, alignment: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsViewBackup: View {
|
||||
@State private var showExporter = false
|
||||
@State private var showImporter = false
|
||||
@State private var exportURL: URL?
|
||||
@State private var showAlert = false
|
||||
@State private var alertMessage = ""
|
||||
@State private var exportData: Data? = nil
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 24) {
|
||||
SettingsSection(
|
||||
title: NSLocalizedString("Backup & Restore", comment: "Settings section title for backup and restore"),
|
||||
footer: NSLocalizedString("Notice: This feature is still experimental. Please double-check your data after import/export.", comment: "Footer notice for experimental backup/restore feature")
|
||||
) {
|
||||
SettingsActionRow(
|
||||
icon: "arrow.up.doc",
|
||||
title: NSLocalizedString("Export Backup", comment: "Export backup button title"),
|
||||
action: {
|
||||
exportData = generateBackupData()
|
||||
showExporter = true
|
||||
},
|
||||
showDivider: true
|
||||
)
|
||||
SettingsActionRow(
|
||||
icon: "arrow.down.doc",
|
||||
title: NSLocalizedString("Import Backup", comment: "Import backup button title"),
|
||||
action: {
|
||||
showImporter = true
|
||||
},
|
||||
showDivider: false
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("Backup & Restore", comment: "Navigation title for backup and restore view"))
|
||||
.fileExporter(
|
||||
isPresented: $showExporter,
|
||||
document: BackupDocument(data: exportData ?? Data()),
|
||||
contentType: .json,
|
||||
defaultFilename: exportFilename()
|
||||
) { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
alertMessage = "Exported to \(url.lastPathComponent)"
|
||||
showAlert = true
|
||||
case .failure(let error):
|
||||
alertMessage = "Export failed: \(error.localizedDescription)"
|
||||
showAlert = true
|
||||
}
|
||||
}
|
||||
.fileImporter(
|
||||
isPresented: $showImporter,
|
||||
allowedContentTypes: [.json]
|
||||
) { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
var success = false
|
||||
if url.startAccessingSecurityScopedResource() {
|
||||
defer { url.stopAccessingSecurityScopedResource() }
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
try restoreBackupData(data)
|
||||
alertMessage = "Import successful!"
|
||||
success = true
|
||||
} catch {
|
||||
alertMessage = "Import failed: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
alertMessage = "Import failed: Could not access file."
|
||||
}
|
||||
showAlert = true
|
||||
case .failure(let error):
|
||||
alertMessage = "Import failed: \(error.localizedDescription)"
|
||||
showAlert = true
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showAlert) {
|
||||
Alert(title: Text(NSLocalizedString("Backup", comment: "Alert title for backup actions")), message: Text(alertMessage), dismissButton: .default(Text("OK")))
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func generateBackupData() -> Data? {
|
||||
let continueWatching = ContinueWatchingManager.shared.fetchItems()
|
||||
let continueReading = ContinueReadingManager.shared.fetchItems()
|
||||
let collections = (try? JSONDecoder().decode([BookmarkCollection].self, from: UserDefaults.standard.data(forKey: "bookmarkCollections") ?? Data())) ?? []
|
||||
let searchHistory = UserDefaults.standard.stringArray(forKey: "searchHistory") ?? []
|
||||
let modules = ModuleManager().modules
|
||||
|
||||
let backup: [String: Any] = [
|
||||
"continueWatching": continueWatching.map { try? $0.toDictionary() },
|
||||
"continueReading": continueReading.map { try? $0.toDictionary() },
|
||||
"collections": collections.map { try? $0.toDictionary() },
|
||||
"searchHistory": searchHistory,
|
||||
"modules": modules.map { try? $0.toDictionary() }
|
||||
]
|
||||
|
||||
return try? JSONSerialization.data(withJSONObject: backup, options: .prettyPrinted)
|
||||
}
|
||||
|
||||
private func restoreBackupData(_ data: Data) throws {
|
||||
guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
|
||||
throw NSError(domain: "restoreBackupData", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid backup format"])
|
||||
}
|
||||
if let cwArr = json["continueWatching"] as? NSArray {
|
||||
let cwData = try JSONSerialization.data(withJSONObject: cwArr, options: [])
|
||||
UserDefaults.standard.set(cwData, forKey: "continueWatchingItems")
|
||||
}
|
||||
if let crArr = json["continueReading"] as? NSArray {
|
||||
let crData = try JSONSerialization.data(withJSONObject: crArr, options: [])
|
||||
UserDefaults.standard.set(crData, forKey: "continueReadingItems")
|
||||
}
|
||||
if let colArr = json["collections"] as? NSArray {
|
||||
let colData = try JSONSerialization.data(withJSONObject: colArr, options: [])
|
||||
UserDefaults.standard.set(colData, forKey: "bookmarkCollections")
|
||||
}
|
||||
if let shArr = json["searchHistory"] as? [String] {
|
||||
UserDefaults.standard.set(shArr, forKey: "searchHistory")
|
||||
}
|
||||
if let modArr = json["modules"] as? NSArray {
|
||||
let modData = try JSONSerialization.data(withJSONObject: modArr, options: [])
|
||||
let fileManager = FileManager.default
|
||||
let docs = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let modulesURL = docs.appendingPathComponent("modules.json")
|
||||
try modData.write(to: modulesURL)
|
||||
}
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
|
||||
|
||||
private func exportFilename() -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
|
||||
let dateString = formatter.string(from: Date())
|
||||
return "SoraBackup_\(dateString).json"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Encodable {
|
||||
func toDictionary() throws -> [String: Any] {
|
||||
let data = try JSONEncoder().encode(self)
|
||||
let json = try JSONSerialization.jsonObject(with: data, options: [])
|
||||
guard let dict = json as? [String: Any] else {
|
||||
throw NSError(domain: "toDictionary", code: 0, userInfo: nil)
|
||||
}
|
||||
return dict
|
||||
}
|
||||
}
|
||||
|
||||
struct BackupDocument: FileDocument {
|
||||
static var readableContentTypes: [UTType] { [.json] }
|
||||
var data: Data
|
||||
|
||||
init(data: Data) {
|
||||
self.data = data
|
||||
}
|
||||
init(configuration: ReadConfiguration) throws {
|
||||
self.data = configuration.file.regularFileContents ?? Data()
|
||||
}
|
||||
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
|
||||
return .init(regularFileWithContents: data)
|
||||
}
|
||||
}
|
||||
|
|
@ -155,18 +155,35 @@ struct SettingsViewGeneral: View {
|
|||
@AppStorage("analyticsEnabled") private var analyticsEnabled: Bool = false
|
||||
@AppStorage("hideSplashScreen") private var hideSplashScreenEnable: Bool = false
|
||||
@AppStorage("useNativeTabBar") private var useNativeTabBar: Bool = false
|
||||
@AppStorage("metadataProvidersOrder") private var metadataProvidersOrderData: Data = {
|
||||
@AppStorage("metadataProvidersOrderData") private var metadataProvidersOrderData: Data = {
|
||||
try! JSONEncoder().encode(["TMDB","AniList"])
|
||||
}()
|
||||
@AppStorage("tmdbImageWidth") private var TMDBimageWidht: String = "original"
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
|
||||
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4
|
||||
@AppStorage("metadataProviders") private var metadataProviders: String = "TMDB"
|
||||
@AppStorage("librarySectionsOrderData") private var librarySectionsOrderData: Data = {
|
||||
try! JSONEncoder().encode(["continueWatching", "continueReading", "collections"])
|
||||
}()
|
||||
@AppStorage("disabledLibrarySectionsData") private var disabledLibrarySectionsData: Data = {
|
||||
try! JSONEncoder().encode([String]())
|
||||
}()
|
||||
|
||||
private var metadataProvidersOrder: [String] {
|
||||
get { (try? JSONDecoder().decode([String].self, from: metadataProvidersOrderData)) ?? ["AniList","TMDB"] }
|
||||
set { metadataProvidersOrderData = try! JSONEncoder().encode(newValue) }
|
||||
}
|
||||
|
||||
private var librarySectionsOrder: [String] {
|
||||
get { (try? JSONDecoder().decode([String].self, from: librarySectionsOrderData)) ?? ["continueWatching", "continueReading", "collections"] }
|
||||
set { librarySectionsOrderData = try! JSONEncoder().encode(newValue) }
|
||||
}
|
||||
|
||||
private var disabledLibrarySections: [String] {
|
||||
get { (try? JSONDecoder().decode([String].self, from: disabledLibrarySectionsData)) ?? [] }
|
||||
set { disabledLibrarySectionsData = try! JSONEncoder().encode(newValue) }
|
||||
}
|
||||
|
||||
private let TMDBimageWidhtList = ["300", "500", "780", "1280", "original"]
|
||||
private let sortOrderOptions = ["Ascending", "Descending"]
|
||||
private let metadataProvidersList = ["TMDB", "AniList"]
|
||||
|
|
@ -317,9 +334,15 @@ struct SettingsViewGeneral: View {
|
|||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.frame(height: CGFloat(metadataProvidersOrder.count * 48))
|
||||
.frame(height: CGFloat(metadataProvidersOrder.count * 65))
|
||||
.background(Color.clear)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(NSLocalizedString("Drag to reorder", comment: ""))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
.environment(\.editMode, .constant(.active))
|
||||
}
|
||||
|
|
@ -369,6 +392,70 @@ struct SettingsViewGeneral: View {
|
|||
showDivider: false
|
||||
)
|
||||
}
|
||||
|
||||
SettingsSection(
|
||||
title: NSLocalizedString("Library View", comment: ""),
|
||||
footer: NSLocalizedString("Customize the sections shown in your library. You can reorder sections or disable them completely.", comment: "")
|
||||
) {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Image(systemName: "arrow.up.arrow.down")
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text(NSLocalizedString("Library Sections Order", comment: ""))
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
|
||||
List {
|
||||
ForEach(Array(librarySectionsOrder.enumerated()), id: \.element) { index, section in
|
||||
HStack {
|
||||
Text("\(index + 1)")
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundStyle(.gray)
|
||||
|
||||
Image(systemName: sectionIcon(for: section))
|
||||
.frame(width: 24, height: 24)
|
||||
|
||||
Text(sectionName(for: section))
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("", isOn: toggleBinding(for: section))
|
||||
.labelsHidden()
|
||||
.tint(.accentColor.opacity(0.7))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.visible)
|
||||
.listRowSeparatorTint(.gray.opacity(0.3))
|
||||
.listRowInsets(EdgeInsets())
|
||||
}
|
||||
.onMove { from, to in
|
||||
var arr = librarySectionsOrder
|
||||
arr.move(fromOffsets: from, toOffset: to)
|
||||
librarySectionsOrderData = try! JSONEncoder().encode(arr)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.frame(height: CGFloat(librarySectionsOrder.count * 70))
|
||||
.background(Color.clear)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(NSLocalizedString("Drag to reorder sections", comment: ""))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
.environment(\.editMode, .constant(.active))
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
}
|
||||
|
|
@ -382,4 +469,47 @@ struct SettingsViewGeneral: View {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func sectionName(for section: String) -> String {
|
||||
switch section {
|
||||
case "continueWatching":
|
||||
return NSLocalizedString("Continue Watching", comment: "")
|
||||
case "continueReading":
|
||||
return NSLocalizedString("Continue Reading", comment: "")
|
||||
case "collections":
|
||||
return NSLocalizedString("Collections", comment: "")
|
||||
default:
|
||||
return section
|
||||
}
|
||||
}
|
||||
|
||||
private func sectionIcon(for section: String) -> String {
|
||||
switch section {
|
||||
case "continueWatching":
|
||||
return "play.fill"
|
||||
case "continueReading":
|
||||
return "book.fill"
|
||||
case "collections":
|
||||
return "folder.fill"
|
||||
default:
|
||||
return "questionmark"
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleBinding(for section: String) -> Binding<Bool> {
|
||||
return Binding(
|
||||
get: { !self.disabledLibrarySections.contains(section) },
|
||||
set: { isEnabled in
|
||||
var sections = self.disabledLibrarySections
|
||||
if isEnabled {
|
||||
sections.removeAll { $0 == section }
|
||||
} else {
|
||||
if !sections.contains(section) {
|
||||
sections.append(section)
|
||||
}
|
||||
}
|
||||
self.disabledLibrarySectionsData = try! JSONEncoder().encode(sections)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ struct SettingsView: View {
|
|||
@Environment(\.colorScheme) var colorScheme
|
||||
@StateObject var settings = Settings()
|
||||
@EnvironmentObject var moduleManager: ModuleManager
|
||||
@EnvironmentObject var tabBarController: TabBarController
|
||||
|
||||
@State private var isNavigationActive = false
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -177,11 +177,6 @@ struct SettingsView: View {
|
|||
NavigationLink(destination: SettingsViewDownloads().navigationBarBackButtonHidden(false)) {
|
||||
SettingsNavigationRow(icon: "arrow.down.circle", titleKey: "Downloads")
|
||||
}
|
||||
Divider().padding(.horizontal, 16)
|
||||
|
||||
NavigationLink(destination: SettingsViewTrackers().navigationBarBackButtonHidden(false)) {
|
||||
SettingsNavigationRow(icon: "square.stack.3d.up", titleKey: "Trackers")
|
||||
}
|
||||
}
|
||||
.background(.ultraThinMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
|
|
@ -217,6 +212,11 @@ struct SettingsView: View {
|
|||
NavigationLink(destination: SettingsViewLogger().navigationBarBackButtonHidden(false)) {
|
||||
SettingsNavigationRow(icon: "doc.text", titleKey: "Logs")
|
||||
}
|
||||
Divider().padding(.horizontal, 16)
|
||||
|
||||
NavigationLink(destination: SettingsViewBackup().navigationBarBackButtonHidden(false)) {
|
||||
SettingsNavigationRow(icon: "arrow.triangle.2.circlepath", titleKey: NSLocalizedString("Backup & Restore", comment: "Settings navigation row for backup and restore"))
|
||||
}
|
||||
}
|
||||
.background(.ultraThinMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
|
|
@ -250,28 +250,52 @@ struct SettingsView: View {
|
|||
Divider().padding(.horizontal, 16)
|
||||
|
||||
Link(destination: URL(string: "https://github.com/cranci1/Sora")!) {
|
||||
SettingsNavigationRow(
|
||||
icon: "chevron.left.forwardslash.chevron.right",
|
||||
titleKey: "Sora GitHub Repository",
|
||||
isExternal: true,
|
||||
textColor: .gray
|
||||
)
|
||||
HStack {
|
||||
Image("Github Icon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
.padding(.leading, 2)
|
||||
.padding(.trailing, 4)
|
||||
|
||||
Text(NSLocalizedString("Sora GitHub Repository", comment: ""))
|
||||
.foregroundStyle(.gray)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "safari")
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
Divider().padding(.horizontal, 16)
|
||||
|
||||
Link(destination: URL(string: "https://discord.gg/x7hppDWFDZ")!) {
|
||||
SettingsNavigationRow(
|
||||
icon: "bubble.left.and.bubble.right",
|
||||
titleKey: "Join the Discord",
|
||||
isExternal: true,
|
||||
textColor: .gray
|
||||
)
|
||||
HStack {
|
||||
Image("Discord Icon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
.padding(.leading, 2)
|
||||
.padding(.trailing, 4)
|
||||
|
||||
Text(NSLocalizedString("Join the Discord", comment: ""))
|
||||
.foregroundStyle(.gray)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "safari")
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
Divider().padding(.horizontal, 16)
|
||||
|
||||
Link(destination: URL(string: "https://github.com/cranci1/Sora/issues")!) {
|
||||
SettingsNavigationRow(
|
||||
icon: "exclamationmark.circle",
|
||||
icon: "exclamationmark.circle.fill",
|
||||
titleKey: "Report an Issue",
|
||||
isExternal: true,
|
||||
textColor: .gray
|
||||
|
|
@ -281,7 +305,7 @@ struct SettingsView: View {
|
|||
|
||||
Link(destination: URL(string: "https://github.com/cranci1/Sora/blob/dev/LICENSE")!) {
|
||||
SettingsNavigationRow(
|
||||
icon: "doc.text",
|
||||
icon: "doc.text.fill",
|
||||
titleKey: "License (GPLv3.0)",
|
||||
isExternal: true,
|
||||
textColor: .gray
|
||||
|
|
@ -330,7 +354,6 @@ struct SettingsView: View {
|
|||
}
|
||||
.onAppear {
|
||||
settings.updateAccentColor(currentColorScheme: colorScheme)
|
||||
tabBarController.showTabBar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0402DA0E2DE7AA01003BB42C /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0402DA0D2DE7A9FC003BB42C /* TabBarController.swift */; };
|
||||
0402DA132DE7B5EC003BB42C /* SearchStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0402DA112DE7B5EC003BB42C /* SearchStateView.swift */; };
|
||||
0402DA142DE7B5EC003BB42C /* SearchResultsGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0402DA102DE7B5EC003BB42C /* SearchResultsGrid.swift */; };
|
||||
0402DA152DE7B5EC003BB42C /* SearchComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0402DA0F2DE7B5EC003BB42C /* SearchComponents.swift */; };
|
||||
|
|
@ -19,6 +18,7 @@
|
|||
0410697F2E00ABE900A157BB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0410697C2E00ABE900A157BB /* Localizable.strings */; };
|
||||
041069832E00C71000A157BB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 041069812E00C71000A157BB /* Localizable.strings */; };
|
||||
041261042E00D14F00D05B47 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 041261022E00D14F00D05B47 /* Localizable.strings */; };
|
||||
041E9D722E11D71F0025F150 /* SettingsViewBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041E9D712E11D71F0025F150 /* SettingsViewBackup.swift */; };
|
||||
04536F712E04BA3B00A11248 /* JSController-Novel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04536F702E04BA3B00A11248 /* JSController-Novel.swift */; };
|
||||
04536F742E04BA5600A11248 /* ReaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04536F722E04BA5600A11248 /* ReaderView.swift */; };
|
||||
04536F772E04BA6900A11248 /* ChapterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04536F752E04BA6900A11248 /* ChapterCell.swift */; };
|
||||
|
|
@ -27,6 +27,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 */; };
|
||||
047F170A2E0C93E10081B5FB /* AllReading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047F17082E0C93E10081B5FB /* AllReading.swift */; };
|
||||
047F170B2E0C93E10081B5FB /* ContinueReadingSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047F17092E0C93E10081B5FB /* ContinueReadingSection.swift */; };
|
||||
047F170E2E0C93F30081B5FB /* ContinueReadingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047F170C2E0C93F30081B5FB /* ContinueReadingItem.swift */; };
|
||||
047F170F2E0C93F30081B5FB /* ContinueReadingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047F170D2E0C93F30081B5FB /* ContinueReadingManager.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 */; };
|
||||
|
|
@ -121,7 +125,6 @@
|
|||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0402DA0D2DE7A9FC003BB42C /* TabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = "<group>"; };
|
||||
0402DA0F2DE7B5EC003BB42C /* SearchComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchComponents.swift; sourceTree = "<group>"; };
|
||||
0402DA102DE7B5EC003BB42C /* SearchResultsGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsGrid.swift; sourceTree = "<group>"; };
|
||||
0402DA112DE7B5EC003BB42C /* SearchStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchStateView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -133,6 +136,7 @@
|
|||
0410697B2E00ABE900A157BB /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
041069802E00C71000A157BB /* kk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kk; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
041261012E00D14F00D05B47 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
041E9D712E11D71F0025F150 /* SettingsViewBackup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewBackup.swift; sourceTree = "<group>"; };
|
||||
0452339E2E02149C002EA23C /* bos */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bos; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
04536F702E04BA3B00A11248 /* JSController-Novel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Novel.swift"; sourceTree = "<group>"; };
|
||||
04536F722E04BA5600A11248 /* ReaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -142,6 +146,10 @@
|
|||
0457C59A2DE78267000AFBD9 /* BookmarkGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkGridView.swift; sourceTree = "<group>"; };
|
||||
0457C59B2DE78267000AFBD9 /* BookmarkLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkLink.swift; sourceTree = "<group>"; };
|
||||
0457C5A02DE78385000AFBD9 /* BookmarksDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDetailView.swift; sourceTree = "<group>"; };
|
||||
047F17082E0C93E10081B5FB /* AllReading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllReading.swift; sourceTree = "<group>"; };
|
||||
047F17092E0C93E10081B5FB /* ContinueReadingSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueReadingSection.swift; sourceTree = "<group>"; };
|
||||
047F170C2E0C93F30081B5FB /* ContinueReadingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueReadingItem.swift; sourceTree = "<group>"; };
|
||||
047F170D2E0C93F30081B5FB /* ContinueReadingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueReadingManager.swift; sourceTree = "<group>"; };
|
||||
0488FA8F2DFDE724007575E1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
0488FA922DFDE724007575E1 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
0488FA992DFDF380007575E1 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
|
|
@ -423,7 +431,6 @@
|
|||
04F08EDD2DE10C05006B29D9 /* TabBar */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0402DA0D2DE7A9FC003BB42C /* TabBarController.swift */,
|
||||
04F08EDE2DE10C1A006B29D9 /* TabBar.swift */,
|
||||
);
|
||||
path = TabBar;
|
||||
|
|
@ -562,6 +569,7 @@
|
|||
133D7C832D2BE2630075467E /* SettingsSubViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
041E9D712E11D71F0025F150 /* SettingsViewBackup.swift */,
|
||||
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */,
|
||||
1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */,
|
||||
133D7C842D2BE2630075467E /* SettingsViewModule.swift */,
|
||||
|
|
@ -638,6 +646,8 @@
|
|||
133F55B92D33B53E00E08EEA /* LibraryView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
047F17082E0C93E10081B5FB /* AllReading.swift */,
|
||||
047F17092E0C93E10081B5FB /* ContinueReadingSection.swift */,
|
||||
0457C59C2DE78267000AFBD9 /* BookmarkComponents */,
|
||||
04CD76DA2DE20F2200733536 /* AllWatching.swift */,
|
||||
133F55BA2D33B55100E08EEA /* LibraryManager.swift */,
|
||||
|
|
@ -740,6 +750,8 @@
|
|||
13C0E5E82D5F85DD00E7F619 /* ContinueWatching */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
047F170C2E0C93F30081B5FB /* ContinueReadingItem.swift */,
|
||||
047F170D2E0C93F30081B5FB /* ContinueReadingManager.swift */,
|
||||
13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */,
|
||||
13C0E5EB2D5F85F800E7F619 /* ContinueWatchingItem.swift */,
|
||||
);
|
||||
|
|
@ -947,6 +959,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
041E9D722E11D71F0025F150 /* SettingsViewBackup.swift in Sources */,
|
||||
135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */,
|
||||
04536F742E04BA5600A11248 /* ReaderView.swift in Sources */,
|
||||
131270172DC13A010093AA9C /* DownloadManager.swift in Sources */,
|
||||
|
|
@ -966,13 +979,14 @@
|
|||
04F08EDC2DE10BF3006B29D9 /* ProgressiveBlurView.swift in Sources */,
|
||||
1E26E9E72DA9577900B9DC02 /* VolumeSlider.swift in Sources */,
|
||||
136BBE802DB1038000906B5E /* Notification+Name.swift in Sources */,
|
||||
047F170E2E0C93F30081B5FB /* ContinueReadingItem.swift in Sources */,
|
||||
047F170F2E0C93F30081B5FB /* ContinueReadingManager.swift in Sources */,
|
||||
13DB46902D900A38008CBC03 /* URL.swift in Sources */,
|
||||
138FE1D02DECA00D00936D81 /* TMDB-FetchID.swift in Sources */,
|
||||
04AD07162E03704700EB74C1 /* BookmarkCell.swift in Sources */,
|
||||
130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */,
|
||||
0402DA172DE7B7B8003BB42C /* SearchViewComponents.swift in Sources */,
|
||||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */,
|
||||
0402DA0E2DE7AA01003BB42C /* TabBarController.swift in Sources */,
|
||||
1E47859B2DEBC5960095BF2F /* AnilistMatchPopupView.swift in Sources */,
|
||||
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */,
|
||||
133D7C932D2BE2640075467E /* Modules.swift in Sources */,
|
||||
|
|
@ -992,6 +1006,8 @@
|
|||
132AF1212D99951700A0140B /* JSController-Streams.swift in Sources */,
|
||||
131845F92D47C62D00CA7A54 /* SettingsViewGeneral.swift in Sources */,
|
||||
04CD76DB2DE20F2200733536 /* AllWatching.swift in Sources */,
|
||||
047F170A2E0C93E10081B5FB /* AllReading.swift in Sources */,
|
||||
047F170B2E0C93E10081B5FB /* ContinueReadingSection.swift in Sources */,
|
||||
13103E8E2D58E04A000F0673 /* SkeletonCell.swift in Sources */,
|
||||
13D842552D45267500EBBFA6 /* DropManager.swift in Sources */,
|
||||
13103E8B2D58E028000F0673 /* View.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue