diff --git a/Sora/Info.plist b/Sora/Info.plist index ba6c434..dc64431 100644 --- a/Sora/Info.plist +++ b/Sora/Info.plist @@ -6,6 +6,25 @@ $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleDevelopmentRegion + en + CFBundleLocalizations + + en + ar + bos + cs + nl + fr + de + it + kk + nn + ru + sk + es + sv + CFBundleURLTypes diff --git a/Sora/Localizable.xcstrings b/Sora/Localizable.xcstrings deleted file mode 100644 index e15ac25..0000000 --- a/Sora/Localizable.xcstrings +++ /dev/null @@ -1,3907 +0,0 @@ -{ - "sourceLanguage" : "en", - "strings" : { - "" : { - - }, - "%lld" : { - - }, - "%lld Episodes" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Afleveringen" - } - } - } - }, - "%lld of %lld" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$lld of %2$lld" - } - } - } - }, - "%lld-%lld" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$lld-%2$lld" - } - } - } - }, - "%lld%%" : { - - }, - "%lld%% seen" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld%% gezien" - } - } - } - }, - "•" : { - - }, - "About" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "About" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Over" - } - } - } - }, - "About Sora" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "About Sora" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Over Sora" - } - } - } - }, - "Active" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Active" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Actief" - } - } - } - }, - "Active Downloads" : { - - }, - "Actively downloading media can be tracked from here." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Actively downloading media can be tracked from here." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Actief downloaden van media kan hier worden gevolgd." - } - } - } - }, - "Add Module" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Add Module" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Module Toevoegen" - } - } - } - }, - "Adjust the number of media items per row in portrait and landscape modes." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Adjust the number of media items per row in portrait and landscape modes." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Pas het aantal media-items per rij aan in staande en liggende modus." - } - } - } - }, - "Advanced" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Advanced" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Geavanceerd" - } - } - } - }, - "AKA Sulfur" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "AKA Sulfur" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "AKA Sulfur" - } - } - } - }, - "All Bookmarks" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "All Bookmarks" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Alle Bladwijzers" - } - } - } - }, - "All Prev" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alle vorige" - } - } - } - }, - "All Watching" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "All Watching" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Alles Wat Ik Kijk" - } - } - } - }, - "Also known as Sulfur" : { - - }, - "AniList" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "AniList" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "AniList" - } - } - } - }, - "AniList ID" : { - - }, - "AniList Match" : { - - }, - "AniList.co" : { - - }, - "Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Anonieme gegevens worden verzameld om de app te verbeteren. Er worden geen persoonlijke gegevens verzameld. Dit kan op elk moment worden uitgeschakeld." - } - } - } - }, - "App Data" : { - - }, - "App Info" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "App Info" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "App Info" - } - } - } - }, - "App Language" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "App Language" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "App Taal" - } - } - } - }, - "App Storage" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "App Storage" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "App Opslag" - } - } - } - }, - "Appearance" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Appearance" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Uiterlijk" - } - } - } - }, - "Are you sure you want to clear all cached data? This will help free up storage space." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Are you sure you want to clear all cached data? This will help free up storage space." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Weet je zeker dat je alle gecachte gegevens wilt wissen? Dit helpt opslagruimte vrij te maken." - } - } - } - }, - "Are you sure you want to delete '%@'?" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Are you sure you want to delete '%@'?" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Weet je zeker dat je '%@' wilt verwijderen?" - } - } - } - }, - "Are you sure you want to delete all %d episodes in '%@'?" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Are you sure you want to delete all %1$d episodes in '%2$@'?" - } - } - } - }, - "Are you sure you want to delete all %lld episodes in '%@'?" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Are you sure you want to delete all %1$lld episodes in '%2$@'?" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Weet je zeker dat je alle %1$lld afleveringen in '%2$@' wilt verwijderen?" - } - } - } - }, - "Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Weet je zeker dat je alle gedownloade bestanden wilt verwijderen? Je kunt ervoor kiezen om alleen de bibliotheek te wissen terwijl je de gedownloade bestanden voor later gebruik bewaart." - } - } - } - }, - "Are you sure you want to erase all app data? This action cannot be undone." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Are you sure you want to erase all app data? This action cannot be undone." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Weet je zeker dat je alle app-gegevens wilt wissen? Deze actie kan niet ongedaan worden gemaakt." - } - } - } - }, - "Are you sure you want to remove all downloaded media files (.mov, .mp4, .pkg)? This action cannot be undone." : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weet je zeker dat je alle gedownloade mediabestanden (.mov, .mp4, .pkg) wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.\n" - } - } - } - }, - "Are you sure you want to remove all files in the Documents folder? This will remove all modules." : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weet je zeker dat je alle bestanden in de map Documenten wilt verwijderen? Dit zal alle modules verwijderen.\n" - } - } - } - }, - "Author" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auteur\n" - } - } - } - }, - "Background Enabled" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Background Enabled" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Achtergrond Ingeschakeld" - } - } - } - }, - "Bookmark items for an easier access later." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Bookmark items for an easier access later." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Bladwijzer items voor eenvoudigere toegang later." - } - } - } - }, - "Bookmarks" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Bookmarks" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Bladwijzers" - } - } - } - }, - "Bottom Padding" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Bottom Padding" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Onderste Padding" - } - } - } - }, - "Cancel" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Cancel" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Annuleren" - } - } - } - }, - "Cellular Quality" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Cellular Quality" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Mobiele Kwaliteit" - } - } - } - }, - "Check out some community modules here!" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Check out some community modules here!" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Bekijk hier enkele community modules!" - } - } - } - }, - "Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Kies de gewenste videoresolutie voor WiFi en mobiele verbindingen. Hogere resoluties gebruiken meer data maar bieden betere kwaliteit. Als de exacte kwaliteit niet beschikbaar is, wordt automatisch de dichtstbijzijnde optie geselecteerd.\n\nLet op: Niet alle videobronnen en spelers ondersteunen kwaliteitsselectie. Deze functie werkt het beste met HLS-streams met de Sora-speler." - } - } - } - }, - "Clear" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Clear" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Wissen" - } - } - } - }, - "Clear All Downloads" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wis Alle Downloads" - } - } - } - }, - "Clear Cache" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wis Cache" - } - } - } - }, - "Clear Library Only" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alleen bibliotheek wissen\n" - } - } - } - }, - "Clear Logs" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wis Logs" - } - } - } - }, - "Click the plus button to add a module!" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Click the plus button to add a module!" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Klik op de plus-knop om een module toe te voegen!" - } - } - } - }, - "Continue Watching" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Continue Watching" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verder Kijken" - } - } - } - }, - "Continue Watching Episode %d" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Continue Watching Episode %d" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verder Kijken Aflevering %d" - } - } - } - }, - "Contributors" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Contributors" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Bijdragers" - } - } - } - }, - "Copied to Clipboard" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Copied to Clipboard" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gekopieerd naar Klembord" - } - } - } - }, - "Copy to Clipboard" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Copy to Clipboard" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Kopiëren naar Klembord" - } - } - } - }, - "Copy URL" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Copy URL" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "URL Kopiëren" - } - } - } - }, - "cranci1" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "cranci1" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "cranci1" - } - } - } - }, - "Dark" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Dark" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Donker" - } - } - } - }, - "DATA & LOGS" : { - - }, - "Debug" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Debug" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Debug" - } - } - } - }, - "Debugging and troubleshooting." : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Debugging and troubleshooting." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Debuggen en probleemoplossing." - } - } - } - }, - "Delete" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Delete" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Verwijderen" - } - } - } - }, - "Delete All" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alles Wissen" - } - } - } - }, - "Delete All Downloads" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alle Downloads Wissen" - } - } - } - }, - "Delete All Episodes" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alle Afleveringen Wissen" - } - } - } - }, - "Delete Download" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Downloads Wissen" - } - } - } - }, - "Delete Episode" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afleveringen Wissen" - } - } - } - }, - "Double Tap to Seek" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Double Tap to Seek" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Dubbel Tikken om te Zoeken" - } - } - } - }, - "Double tapping the screen on it's sides will skip with the short tap setting." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Double tapping the screen on it's sides will skip with the short tap setting." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Dubbel tikken op de zijkanten van het scherm zal overslaan met de korte tik instelling." - } - } - } - }, - "Download" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Download" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Downloaden" - } - } - } - }, - "Download Episode" : { - "extractionState" : "stale", - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aflevering Downloaden" - } - } - } - }, - "Download Summary" : { - - }, - "Download This Episode" : { - - }, - "Downloaded" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Downloaded" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Gedownload" - } - } - } - }, - "Downloaded Shows" : { - - }, - "Downloading" : { - - }, - "Downloads" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Downloads" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Downloads" - } - } - } - }, - "Enable Analytics" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Enable Analytics" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Analytics Inschakelen" - } - } - } - }, - "Enable Subtitles" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Enable Subtitles" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Ondertiteling Inschakelen" - } - } - } - }, - "Enter the AniList ID for this media" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Enter the AniList ID for this media" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Voer de AniList ID in voor deze media" - } - } - } - }, - "Enter the AniList ID for this series" : { - - }, - "Episode %lld" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Episode %lld" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Aflevering %lld" - } - } - } - }, - "Episodes" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Episodes" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afleveringen" - } - } - } - }, - "Episodes might not be available yet or there could be an issue with the source." : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Episodes might not be available yet or there could be an issue with the source." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afleveringen zijn mogelijk nog niet beschikbaar of er is een probleem met de bron." - } - } - } - }, - "Episodes Range" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Episodes Range" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Afleveringen Bereik" - } - } - } - }, - "Erase" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verwijden" - } - } - } - }, - "Erase all App Data" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erase all App Data" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wis Alle App Data" - } - } - } - }, - "Erase App Data" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verwijder App Data" - } - } - } - }, - "Error" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Error" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Fout" - } - } - } - }, - "Error Fetching Results" : { - - }, - "Errors and critical issues." : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Errors and critical issues." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fouten en kritieke problemen." - } - } - } - }, - "Failed to load contributors" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Failed to load contributors" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Laden van bijdragers mislukt" - } - } - } - }, - "Fetch Episode metadata" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Fetch Episode metadata" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Haal Aflevering Metadata op" - } - } - } - }, - "Files Downloaded" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Files Downloaded" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Gedownloade Bestanden" - } - } - } - }, - "Font Size" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Font Size" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Lettergrootte" - } - } - } - }, - "Force Landscape" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Force Landscape" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Forceer Landschap" - } - } - } - }, - "General" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "General" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Algemeen" - } - } - } - }, - "General events and activities." : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "General events and activities." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Algemene gebeurtenissen en activiteiten." - } - } - } - }, - "General Preferences" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "General Preferences" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Algemene Voorkeuren" - } - } - } - }, - "Hide Splash Screen" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Hide Splash Screen" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Splash Screen Verbergen" - } - } - } - }, - "HLS video downloading." : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "HLS video downloading." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "HLS video downloaden." - } - } - } - }, - "Hold Speed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Hold Speed" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Vasthouden Snelheid" - } - } - } - }, - "Info" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Info" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Info" - } - } - } - }, - "INFOS" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "INFOS" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "INFO" - } - } - } - }, - "Installed Modules" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Installed Modules" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geïnstalleerde Modules" - } - } - } - }, - "Interface" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Interface" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Interface" - } - } - } - }, - "Join the Discord" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Join the Discord" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Word lid van de Discord" - } - } - } - }, - "Landscape Columns" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Landscape Columns" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Liggende Kolommen" - } - } - } - }, - "Language" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Language" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Taal" - } - } - } - }, - "LESS" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "LESS" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "MINDER" - } - } - } - }, - "Library" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Library" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Bibliotheek" - } - } - } - }, - "License (GPLv3.0)" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "License (GPLv3.0)" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Licentie (GPLv3.0)" - } - } - } - }, - "Light" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Light" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Licht" - } - } - } - }, - "Loading Episode %lld..." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Loading Episode %lld..." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Aflevering %lld laden..." - } - } - } - }, - "Loading logs..." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Loading logs..." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Logboeken laden..." - } - } - } - }, - "Loading module information..." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Loading module information..." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Module-informatie laden..." - } - } - } - }, - "Loading Stream" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Loading Stream" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Stream Laden" - } - } - } - }, - "Log Debug Info" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Log Debug Info" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Debug Info Loggen" - } - } - } - }, - "Log Filters" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Log Filters" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Log Filters" - } - } - } - }, - "Log In with AniList" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Log In with AniList" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Inloggen met AniList" - } - } - } - }, - "Log In with Trakt" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Log In with Trakt" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Inloggen met Trakt" - } - } - } - }, - "Log Out from AniList" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Log Out from AniList" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Uitloggen van AniList" - } - } - } - }, - "Log Out from Trakt" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Log Out from Trakt" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Uitloggen van Trakt" - } - } - } - }, - "Log Types" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Log Types" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Logboek Types" - } - } - } - }, - "Logged in as" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Logged in as" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingelogd als" - } - } - } - }, - "Logged in as " : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Logged in as " - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Ingelogd als " - } - } - } - }, - "Logs" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Logs" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Logboeken" - } - } - } - }, - "Long press Skip" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Long press Skip" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Lang Drukken Overslaan" - } - } - } - }, - "MAIN" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Main Settings" - } - } - } - }, - "Main Developer" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Main Developer" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Hoofdontwikkelaar" - } - } - } - }, - "MAIN SETTINGS" : { - - }, - "Mark All Previous Watched" : { - "extractionState" : "stale", - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Markeer alles als gezien\n" - } - } - } - }, - "Mark as Watched" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Markeer als gezien" - } - } - } - }, - "Mark Episode as Watched" : { - - }, - "Mark Previous Episodes as Watched" : { - - }, - "Mark watched" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Markeer als gezien" - } - } - } - }, - "Match with AniList" : { - - }, - "Match with TMDB" : { - - }, - "Matched ID: %lld" : { - - }, - "Matched with: %@" : { - "extractionState" : "stale", - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Match met: %@" - } - } - } - }, - "Max Concurrent Downloads" : { - "localizations" : { - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maximaal gelijktijdige downloads\n" - } - } - } - }, - "me frfr" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "me frfr" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "me frfr" - } - } - } - }, - "Media Grid Layout" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Media Grid Layout" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Media Raster Layout" - } - } - } - }, - "Media Player" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Media Player" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Media Speler" - } - } - } - }, - "Media View" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Media View" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Mediaweergave" - } - } - } - }, - "Metadata Provider" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Metadata Provider" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Metadata Provider" - } - } - } - }, - "Metadata Providers Order" : { - - }, - "Module Removed" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Module Removed" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Module Verwijderd" - } - } - } - }, - "Modules" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Modules" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Modules" - } - } - } - }, - "MODULES" : { - - }, - "MORE" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "MORE" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "MEER" - } - } - } - }, - "No Active Downloads" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "No Active Downloads" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Geen Actieve Downloads" - } - } - } - }, - "No AniList matches found" : { - - }, - "No Data Available" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "No Data Available" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Geen Gegevens Beschikbaar" - } - } - } - }, - "No Downloads" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "No Downloads" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Geen Downloads" - } - } - } - }, - "No episodes available" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "No episodes available" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Geen afleveringen beschikbaar" - } - } - } - }, - "No Episodes Available" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No Episodes Available" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geen Afleveringen Beschikbaar" - } - } - } - }, - "No items to continue watching." : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "No items to continue watching." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Geen items om verder te kijken." - } - } - } - }, - "No matches found" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "No matches found" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Geen overeenkomsten gevonden" - } - } - } - }, - "No Module Selected" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "No Module Selected" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Geen Module Geselecteerd" - } - } - } - }, - "No Modules" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "No Modules" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Geen Modules" - } - } - } - }, - "No Results Found" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "No Results Found" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Geen Resultaten Gevonden" - } - } - } - }, - "No Search Results Found" : { - - }, - "Note that the modules will be replaced only if there is a different version string inside the JSON file." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Note that the modules will be replaced only if there is a different version string inside the JSON file." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Let op: de modules worden alleen vervangen als er een andere versiestring in het JSON-bestand staat." - } - } - } - }, - "Nothing to Continue Watching" : { - - }, - "OK" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "OK" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "OK" - } - } - } - }, - "Open Community Library" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Open Community Library" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Open Community Bibliotheek" - } - } - } - }, - "Open in AniList" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Open in AniList" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Openen in AniList" - } - } - } - }, - "Original Poster" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Original Poster" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Originele Poster" - } - } - } - }, - "Paused" : { - - }, - "Play" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Play" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Afspelen" - } - } - } - }, - "Player" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Player" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Speler" - } - } - } - }, - "Please restart the app to apply the language change." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Please restart the app to apply the language change." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Herstart de app om de taalwijziging toe te passen." - } - } - } - }, - "Please select a module from settings" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Please select a module from settings" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Selecteer een module uit de instellingen" - } - } - } - }, - "Portrait Columns" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Portrait Columns" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Staande Kolommen" - } - } - } - }, - "Progress bar Marker Color" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Progress bar Marker Color" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Voortgangsbalk Markeerkleur" - } - } - } - }, - "Provider: %@" : { - - }, - "Queue" : { - - }, - "Queued" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Queued" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "In Wachtrij" - } - } - } - }, - "Recently watched content will appear here." : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Recently watched content will appear here." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Recent bekeken inhoud verschijnt hier." - } - } - } - }, - "Refresh Modules on Launch" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Refresh Modules on Launch" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Ververs Modules bij Opstarten" - } - } - } - }, - "Refresh Storage Info" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Refresh Storage Info" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Opslaginformatie Vernieuwen" - } - } - } - }, - "Remember Playback speed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Remember Playback speed" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Onthoud Afspeelsnelheid" - } - } - } - }, - "Remove" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Remove" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Verwijderen" - } - } - } - }, - "Remove All Cache" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remove All Cache" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verwijder Alle Cache" - } - } - } - }, - "Remove All Documents" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remove All Documents" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verwijder Alle Documenten" - } - } - } - }, - "Remove Documents" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Remove Documents" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Documenten Verwijderen" - } - } - } - }, - "Remove Downloaded Media" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Remove Downloaded Media" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Gedownloade Media Verwijderen" - } - } - } - }, - "Remove Downloads" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remove Downloads" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verwijder Downloads" - } - } - } - }, - "Remove from Bookmarks" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Remove from Bookmarks" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Verwijderen uit Bladwijzers" - } - } - } - }, - "Remove Item" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Remove Item" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Item Verwijderen" - } - } - } - }, - "Report an Issue" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Report an Issue" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rapporteer een Probleem" - } - } - } - }, - "Reset" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Reset" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Resetten" - } - } - } - }, - "Reset AniList ID" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Reset AniList ID" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "AniList ID Resetten" - } - } - } - }, - "Reset Episode Progress" : { - - }, - "Reset progress" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reset progress" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Voortgang resetten" - } - } - } - }, - "Reset Progress" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Reset Progress" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Voortgang Resetten" - } - } - } - }, - "Restart Required" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Restart Required" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Herstart Vereist" - } - } - } - }, - "Running Sora %@ - cranci1" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Running Sora %@ - cranci1" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Sora %@ draait - cranci1" - } - } - } - }, - "Save" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Save" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Opslaan" - } - } - } - }, - "Search" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Search" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Zoeken" - } - } - } - }, - "Search downloads" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Search downloads" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Downloads zoeken" - } - } - } - }, - "Search for something..." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Search for something..." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Zoek naar iets..." - } - } - } - }, - "Search..." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Search..." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Zoeken..." - } - } - } - }, - "Season %d" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Season %d" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seizoen %d" - } - } - } - }, - "Season %lld" : { - - }, - "Segments Color" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Segments Color" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Segmenten Kleur" - } - } - } - }, - "Select Module" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Select Module" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Module Selecteren" - } - } - } - }, - "Set Custom AniList ID" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Set Custom AniList ID" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Aangepaste AniList ID Instellen" - } - } - } - }, - "Settings" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Settings" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Instellingen" - } - } - } - }, - "Shadow" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Shadow" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Schaduw" - } - } - } - }, - "Show More (%lld more characters)" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Show More (%lld more characters)" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Meer Tonen (%lld meer tekens)" - } - } - } - }, - "Show PiP Button" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Show PiP Button" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Toon PiP Knop" - } - } - } - }, - "Show Skip 85s Button" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Show Skip 85s Button" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Toon Overslaan 85s Knop" - } - } - } - }, - "Show Skip Intro / Outro Buttons" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Show Skip Intro / Outro Buttons" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Toon Overslaan Intro / Outro Knoppen" - } - } - } - }, - "Shows" : { - - }, - "Size (%@)" : { - - }, - "Skip Settings" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Skip Settings" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Overslaan Instellingen" - } - } - } - }, - "Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Sommige functies zijn beperkt tot de Sora en Standaard speler, zoals ForceLandscape, holdSpeed en aangepaste tijd overslaan stappen." - } - } - } - }, - "Sora" : { - - }, - "Sora %@ by cranci1" : { - - }, - "Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sora en cranci1 zijn op geen enkele manier verbonden met AniList of Trakt.\n\nHoud er ook rekening mee dat voortgangsupdates mogelijk niet 100% nauwkeurig zijn." - } - } - } - }, - "Sora GitHub Repository" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sora GitHub Repository" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sora GitHub Repository" - } - } - } - }, - "Sora/Sulfur will always remain free with no ADs!" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Sora/Sulfur will always remain free with no ADs!" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Sora/Sulfur blijft altijd gratis zonder advertenties!" - } - } - } - }, - "Sort" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Sort" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Sorteren" - } - } - } - }, - "Speed Settings" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Speed Settings" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Snelheidsinstellingen" - } - } - } - }, - "Start Watching" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Start Watching" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Start met Kijken" - } - } - } - }, - "Start Watching Episode %d" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Start Watching Episode %d" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Start met Kijken Aflevering %d" - } - } - } - }, - "Storage Used" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Storage Used" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Gebruikte Opslag" - } - } - } - }, - "Stream" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stream" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stream" - } - } - } - }, - "Streaming and video playback." : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Streaming and video playback." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Streaming en video afspelen." - } - } - } - }, - "Subtitle Color" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Subtitle Color" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Ondertitelingskleur" - } - } - } - }, - "Subtitle Settings" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Subtitle Settings" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Ondertitelingsinstellingen" - } - } - } - }, - "Sync anime progress" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sync anime progress" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Synchroniseer anime voortgang" - } - } - } - }, - "Sync TV shows progress" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sync TV shows progress" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Synchroniseer TV series voortgang" - } - } - } - }, - "System" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "System" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Systeem" - } - } - } - }, - "Tap a title to override the current match." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Tap a title to override the current match." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Tik op een titel om de huidige match te overschrijven." - } - } - } - }, - "Tap Skip" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Tap Skip" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Tik Overslaan" - } - } - } - }, - "Tap to manage your modules" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Tap to manage your modules" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Tik om je modules te beheren" - } - } - } - }, - "Tap to select a module" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Tap to select a module" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Tik om een module te selecteren" - } - } - } - }, - "The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "De app cache helpt de app om afbeeldingen sneller te laden.\n\nHet wissen van de Documents map zal alle gedownloade modules verwijderen.\n\nWis de App Data niet tenzij je de gevolgen begrijpt — het kan ervoor zorgen dat de app niet meer goed werkt." - } - } - } - }, - "The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Het afleveringen bereik bepaalt hoeveel afleveringen er op elke pagina verschijnen. Afleveringen worden gegroepeerd in sets (zoals 1-25, 26-50, enzovoort), waardoor je er gemakkelijker doorheen kunt navigeren.\n\nVoor aflevering metadata verwijst dit naar de aflevering miniatuur en titel, aangezien deze soms spoilers kunnen bevatten." - } - } - } - }, - "The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "De module heeft slechts één aflevering geleverd, dit is waarschijnlijk een film, daarom hebben we aparte schermen gemaakt voor deze gevallen." - } - } - } - }, - "Thumbnails Width" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Thumbnails Width" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Miniatuur Breedte" - } - } - } - }, - "TMDB Match" : { - - }, - "Trackers" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Trackers" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Trackers" - } - } - } - }, - "Trakt" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trakt" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trakt" - } - } - } - }, - "Trakt.tv" : { - - }, - "Try different keywords" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Try different keywords" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Probeer andere zoekwoorden" - } - } - } - }, - "Try different search terms" : { - - }, - "Two Finger Hold for Pause" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Two Finger Hold for Pause" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Twee Vingers Vasthouden voor Pauze" - } - } - } - }, - "Unable to fetch matches. Please try again later." : { - - }, - "Use TMDB Poster Image" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Use TMDB Poster Image" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "TMDB Poster Afbeelding Gebruiken" - } - } - } - }, - "v%@" : { - - }, - "Video Player" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Video Player" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Videospeler" - } - } - } - }, - "Video Quality Preferences" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Video Quality Preferences" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Video Kwaliteit Voorkeuren" - } - } - } - }, - "View All" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "View All" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Alles Bekijken" - } - } - } - }, - "Watched" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Watched" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Bekeken" - } - } - } - }, - "Why am I not seeing any episodes?" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Why am I not seeing any episodes?" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Waarom zie ik geen afleveringen?" - } - } - } - }, - "WiFi Quality" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "WiFi Quality" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "WiFi Kwaliteit" - } - } - } - }, - "You are not logged in" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You are not logged in" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Je bent niet ingelogd" - } - } - } - }, - "You have no items saved." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "You have no items saved." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Je hebt geen items opgeslagen." - } - } - } - }, - "Your downloaded episodes will appear here" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Your downloaded episodes will appear here" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Je gedownloade afleveringen verschijnen hier" - } - } - } - }, - "Your recently watched content will appear here" : { - - }, - "Download Settings" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Download Settings" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Download Instellingen" - } - } - } - }, - "Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Maximum gelijktijdige downloads bepaalt hoeveel afleveringen tegelijk kunnen worden gedownload. Hogere waarden kunnen meer bandbreedte en apparaatbronnen gebruiken." - } - } - } - }, - "Quality" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Quality" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Kwaliteit" - } - } - } - }, - "Max Concurrent Downloads" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Max Concurrent Downloads" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Maximum Gelijktijdige Downloads" - } - } - } - }, - "Allow Cellular Downloads" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Allow Cellular Downloads" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Downloads via Mobiel Netwerk Toestaan" - } - } - } - }, - "Quality Information" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Quality Information" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Kwaliteitsinformatie" - } - } - } - }, - "Storage Management" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Storage Management" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Opslagbeheer" - } - } - } - }, - "Storage Used" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Storage Used" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Gebruikte Opslag" - } - } - } - }, - "Files Downloaded" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Files Downloaded" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Gedownloade Bestanden" - } - } - } - }, - "Refresh Storage Info" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Refresh Storage Info" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Opslaginformatie Vernieuwen" - } - } - } - }, - "Clear All Downloads" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Clear All Downloads" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Alle Downloads Wissen" - } - } - } - }, - "Delete All Downloads" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Delete All Downloads" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Alle Downloads Verwijderen" - } - } - } - }, - "Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Weet je zeker dat je alle gedownloade bestanden wilt verwijderen? Je kunt ervoor kiezen om alleen de bibliotheek te wissen terwijl je de gedownloade bestanden voor later gebruik bewaart." - } - } - } - }, - "Clear Library Only" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Clear Library Only" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Alleen Bibliotheek Wissen" - } - } - } - }, - "Library cleared successfully" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Library cleared successfully" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Bibliotheek succesvol gewist" - } - } - } - }, - "All downloads deleted successfully" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "All downloads deleted successfully" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Alle downloads succesvol verwijderd" - } - } - } - }, - "Downloads" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Downloads" - } - }, - "nl" : { - "stringUnit" : { - "state" : "new", - "value" : "Downloads" - } - } - } - } - }, - "version" : "1.0" -} \ No newline at end of file diff --git a/Sora/Localization/en.lproj/Localizable.strings b/Sora/Localization/en.lproj/Localizable.strings index 4f55cbb..303ef28 100644 --- a/Sora/Localization/en.lproj/Localizable.strings +++ b/Sora/Localization/en.lproj/Localizable.strings @@ -339,7 +339,6 @@ "The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction."; "The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers."; "The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases."; - /* Interface */ "Thumbnails Width" = "Thumbnails Width"; "TMDB Match" = "TMDB Match"; @@ -398,3 +397,4 @@ "Recent searches" = "Recent searches"; "me frfr" = "me frfr"; "Data" = "Data"; + diff --git a/Sora/Localization/it.lproj/Localizable.strings b/Sora/Localization/it.lproj/Localizable.strings new file mode 100644 index 0000000..df2e8d0 --- /dev/null +++ b/Sora/Localization/it.lproj/Localizable.strings @@ -0,0 +1,330 @@ +/* General */ +"About" = "informazioni"; +"About Sora" = "Informazioni su Sora"; +"Active" = "Attivi"; +"Active Downloads" = "Download Attivi"; +"Actively downloading media can be tracked from here." = "I Download Attivi si possono trovare qui"; +"Add Module" = "Aggiungi Modulo"; +"Adjust the number of media items per row in portrait and landscape modes." = "Imposta il numero dei media per riga in vista orizzontale e in vista verticale"; +"Advanced" = "Avanzate"; +"AKA Sulfur" = "Aka Sulfur"; +"All Bookmarks" = "Tutti i Preferiti"; +"All Watching" = "Tutti i Guardati"; +"Also known as Sulfur" = "Conosciuta anche come Sulfur"; +"AniList" = "AniList"; +"AniList ID" = "AniList ID"; +"AniList Match" = "AniList Match"; +"AniList.co" = "AniList.co"; +"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Vengono raccolti dati anonimi per il miglioramento dell'App, Nessun Dato Personale viene Registrato, Questa opzione può essere disabilitata in ogni momento"; +"App Info" = "Informazioni App"; +"App Language" = "Lingua App"; +"App Storage" = "Memoria Utilizzata dall'app"; +"Appearance" = "Aspetto"; +/* Alerts and Actions */ +"Are you sure you want to clear all cached data? This will help free up storage space." = "Sei sicuro di voler rimuovere tutti i dati presenti nella cache? Questo aiuterà a liberare spazio"; +"Are you sure you want to delete '%@'?" = "Sei sicuro di volere rimuovere '%@'?"; +"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Sei sicuro di voler eliminare tutti %1$d episodi in '%2$@'?"; +"Are you sure you want to delete all downloaded assets? You can choose to clear only the library while preserving the downloaded files for future use." = "Siete sicuri di voler eliminare tutte le risorse scaricate? È possibile scegliere di cancellare solo la libreria, conservando i file scaricati per un uso futuro."; +"Are you sure you want to erase all app data? This action cannot be undone." = "Sei sicuro di voler eliminare tutti i dati dell'app, questa azione non può essere cancellata"; +/* Features */ +"Background Enabled" = "Background Abilitato"; +"Bookmark items for an easier access later." = "Imposta Preferiti per un accesso Più veloce dopo"; +"Bookmarks" = "Preferiti"; +"Bottom Padding" = "Distanza Sottotitoli"; +"Cancel" = "Cancella"; +"Cellular Quality" = "Qualità Dati Cellulare"; +"Check out some community modules here!" = "Cerca i moduli della community qui!"; +"Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player." = "Seleziona la Risoluzione video preferita per WiFi e Dati Mobili. Una Risoluzione più alta consuma più dati ma fornisce una qualità migliore. Se la stessa qualità non é disponibile la soluzione più vicina verrà scelta automaticamente \n\nNota:Non tutte le fonti e player supportano la scelta della qualità video.Questa Feature funziona meglio usando il Player integrato di Sora"; +"Clear" = "Pulisci"; +"Clear All Downloads" = "Pulisci Tutti i Download"; +"Clear Cache" = "Pulisci Cache"; +"Clear Library Only" = "Pulisci Solo La Libreria"; +"Clear Logs" = "Pulisci Log"; +"Click the plus button to add a module!" = "Premi il più per aggiungere un modulo"; +"Continue Watching" = "Continua a Guardare"; +"Continue Watching Episode %d" = "Continua a guardare %d"; +"Contributors" = "Collaboratori"; +"Copied to Clipboard" = "Copiato Negli Appunti"; +"Copy to Clipboard" = "Copia negli Appunti"; +"Copy URL" = "Copia URL"; +/* Episodes */ +"%lld Episodes" = "%lld Episodi"; +"%lld of %lld" = "%lld di %lld"; +"%lld-%lld" = "%lld-%lld"; +"%lld%% seen" = "%lld%% visti"; +"Episode %lld" = "Episodio %lld"; +"Episodes" = "Episodi"; +"Episodes might not be available yet or there could be an issue with the source." = "Gli episodi potrebbero non essere ancora disponibili o potrebbe esserci un errore con la fonte"; +"Episodes Range" = "Range Episodi"; +/* System */ +"cranci1" = "cranc1"; +"Dark" = "Dark"; +"DATA & LOGS" = "DATA & LOGS"; +"Debug" = "Debug"; +"Debugging and troubleshooting." = "Debugging e Risoluzione Problemi"; +/* Actions */ +"Delete" = "Elimina"; +"Delete All" = "Elimina Tutto"; +"Delete All Downloads" = "Elimina Tutti I Download"; +"Delete All Episodes" = "Elimina Tutti Gli Episodi"; +"Delete Download" = "Elimina Download"; +"Delete Episode" = "Elimina Episodio"; +/* Player */ +"Double Tap to Seek" = "Doppio Tap Per (Seek)"; +"Double tapping the screen on it's sides will skip with the short tap setting." = "Se l'opzione per saltare é abilitata, con questo premendo nei lati due volte é possibile skippare"; +/* Downloads */ +"Download" = "Scarica"; +"Download Episode" = "Scarica Episodio"; +"Download Summary" = "Scarica Riassunto"; +"Download This Episode" = "Scarica questo Episodio"; +"Downloaded" = "Scaricato"; +"Downloaded Shows" = "Serie Scaricate"; +"Downloading" = "Download in Corso"; +"Downloads" = "Scaricati"; +/* Settings */ +"Enable Analytics" = "Attiva Analytics"; +"Enable Subtitles" = "Attiva Sottotitoli"; +/* Data Management */ +"Erase" = "Cancella"; +"Erase all App Data" = "Cancella Tutti i Dati dell'App"; +"Erase App Data" = "Cancella Dati App"; +/* Errors */ +"Error" = "Errore"; +"Error Fetching Results" = "Errore nella ricerca dei risultati"; +"Errors and critical issues." = "Errori e Problemi Critici"; +"Failed to load contributors" = "impossibile caricare i collaboratori"; +/* Features */ +"Fetch Episode metadata" = "Cercando i Metadata"; +"Files Downloaded" = "File Scaricati"; +"Font Size" = "Dimensione Carattere"; +/* Interface */ +"Force Landscape" = "Forza Vista Orizzontale"; +"General" = "Generali"; +"General events and activities." = "Eventi e Attività Generali"; +"General Preferences" = "Preferenze Generali"; +"Hide Splash Screen" = "Nascondi Splash Screen"; +"HLS video downloading." = "Scaricamento Video HLS"; +"Hold Speed" = "Mantieni per Velocizzare"; +/* Info */ +"Info" = "Info"; +"INFOS" = "INFORMAZIONI"; +"Installed Modules" = "Moduli Installati"; +"Interface" = "Interfaccia"; +/* Social */ +"Join the Discord" = "Entra Nel Nostro Discord!"; +/* Layout */ +"Landscape Columns" = "Colonne in Vista Orizzontale"; +"Language" = "Lingua"; +"LESS" = "Mostra Meno"; +/* Library */ +"Library" = "Libreria"; +"License (GPLv3.0)" = "Licenza (GPLv3.0)"; +"Light" = "Tema Chiaro"; +/* Loading States */ +"Loading Episode %lld..." = "Caricando Gli Episodi %lld..."; +"Loading logs..." = "Caricando i Logs"; +"Loading module information..." = "Caricando le Informazioni del modulo"; +"Loading Stream" = "Caricando lo Stream"; +/* Logging */ +"Log Debug Info" = "Info Log Debug"; +"Log Filters" = "Filtri Log"; +"Log In with AniList" = "Accedi a AniList"; +"Log In with Trakt" = "Accedi a Trakt"; +"Log Out from AniList" = "Esci da AniList"; +"Log Out from Trakt" = "Esci da Trakt"; +"Log Types" = "Tipo Log"; +"Logged in as" = "Accesso Effettuato come"; +"Logged in as " = "Accesso Effettuato come"; +/* Logs and Settings */ +"Logs" = "Logs"; +"Long press Skip" = "Tener premuto per Skippare"; +"MAIN" = "PRINCIPALE"; +"Main Developer" = "Sviluppatore Principale"; +"MAIN SETTINGS" = "Impostazioni principali"; +/* Media Actions */ +"Mark All Previous Watched" = "Segna Tutti I Precedenti Come visti"; +"Mark as Watched" = "Segna Come Visto"; +"Mark Episode as Watched" = "Segna Episodio come Visto"; +"Mark Previous Episodes as Watched" = "Segna episodi precedenti come visti"; +"Mark watched" = "Segna Come Completato"; +"Match with AniList" = "Match with AniList"; +"Match with TMDB" = "Match with TMDB"; +"Matched ID: %lld" = "Matched ID: %lld"; +"Matched with: %@" = "Matched with:%@"; +"Max Concurrent Downloads" = "Download massimi in Contemporanea"; +/* Media Interface */ +"Media Grid Layout" = "Layout della Griglia dei Media"; +"Media Player" = "Media Player"; +"Media View" = "Vista media"; +"Metadata Provider" = "Provider Metadata"; +"Metadata Providers Order" = "Ordine dei Provider Metadata"; +"Module Removed" = "Modulo Rimosso"; +"Modules" = "Moduli"; +/* Headers */ +"MODULES" = "MODULI"; +"MORE" = "PIÙ"; +/* Status Messages */ +"No Active Downloads" = "Nessun Download Attivo"; +"No AniList matches found" = "Nessun"; +"No Data Available" = "Nessun Dato Disponibile"; +"No Downloads" = "Nessun Download"; +"No episodes available" = "Nessun Episodio Disponibile"; +"No Episodes Available" = "Nessun Episodio Disponibile"; +"No items to continue watching." = "Nessun Contenuto da continuare a guardare"; +"No matches found" = "Nessun Elemento Trovato"; +"No Module Selected" = "Nessun Modulo Selezionato"; +"No Modules" = "Nessun Modulo"; +"No Results Found" = "Nessun Risultato Trovato"; +"No Search Results Found" = "Nessun risultato di ricerca trovato"; +"Nothing to Continue Watching" = "Nulla da Continuare a Guardare"; +/* Notes and Messages */ +"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Si noti che i moduli saranno sostituiti solo se c'è una stringa di versione diversa all'interno del file JSON"; +/* Actions */ +"OK" = "OK"; +"Open Community Library" = "Apri Libreria Community"; +/* External Services */ +"Open in AniList" = "Apri in AniList"; +"Original Poster" = "Poster Originale"; +/* Playback */ +"Paused" = "Pausa"; +"Play" = "Play"; +"Player" = "Player"; +/* System Messages */ +"Please restart the app to apply the language change." = "Riavvia l'app per effettuare i cambiamenti"; +"Please select a module from settings" = "Seleziona un Modulo dalle impostazioni"; +/* Interface */ +"Portrait Columns" = "Colonne in Vista Verticale"; +"Progress bar Marker Color" = "Colore Barra di Progresso"; +"Provider: %@" = "Provider: %@"; +/* Queue */ +"Queue" = "Coda"; +"Queued" = "In Coda"; +/* Content */ +"Recently watched content will appear here." = "I Contenuti Precedentemente Visti Appariranno qui"; +/* Settings */ +"Refresh Modules on Launch" = "Aggiorna i Moduli al Lancio"; +"Refresh Storage Info" = "Aggiorna informazioni storage"; +"Remember Playback speed" = "Ricorda Velocità Playback"; +/* Actions */ +"Remove" = "Rimuovi"; +"Remove All Cache" = "Pulisci Cache"; +/* File Management */ +"Remove All Documents" = "Rimuovi Tutti i Documenti"; +"Remove Documents" = "Rimuovi Documenti"; +"Remove Downloaded Media" = "Rimuovi Media Scaricati"; +"Remove Downloads" = "Rimuovi Scaricati"; +"Remove from Bookmarks" = "Rimuovi dai Preferiti"; +"Remove Item" = "Rimuovi Elemento"; +/* Support */ +"Report an Issue" = "Segnala un Problema"; +/* Reset Options */ +"Reset" = "Reimposta"; +"Reset AniList ID" = "Reimposta AniList ID"; +"Reset Episode Progress" = "Reimposta"; +"Reset progress" = "Reimposta Progressi"; +"Reset Progress" = "Reimposta Progressi"; +/* System */ +"Restart Required" = "Riavvio Richiesto"; +"Running Sora %@ - cranci1" = "Running Sora %@ - cranc1"; +/* Actions */ +"Save" = "Salva"; +"Search" = "Cerca"; +/* Search */ +"Search downloads" = "Cerca Download"; +"Search for something..." = "Cerca Qualcosa..."; +"Search..." = "Cerca..."; +/* Content */ +"Season %d" = "Stagione %d"; +"Season %lld" = "Stagione %lld"; +"Segments Color" = "Colore Segmenti"; +/* Modules */ +"Select Module" = "Seleziona Modulo"; +"Set Custom AniList ID" = "Imposta Custom AniList ID"; +/* Interface */ +"Settings" = "Impostazioni"; +"Shadow" = "Ombra"; +"Show More (%lld more characters)" = "Mostra Di Più (%lld più Caratteri)"; +"Show PiP Button" = "Mostra Bottone PiP"; +"Show Skip 85s Button" = "Mostra Skip 85s"; +"Show Skip Intro / Outro Buttons" = "Mostra Bottoni Skip Intro/Outro"; +"Shows" = "Serie"; +"Size (%@)" = "Dimensione (%@)"; +"Skip Settings" = "Salta Impostazioni"; +/* Player Features */ +"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Qualche Features sono limitate a Sora e al Default Player, Come Forza Visualizzazione Orizzontale, Mantieni per Velocizzare e Skip di Tempo Custom"; +/* App Info */ +"Sora" = "Sora"; +"Sora %@ by cranci1" = "Sora %@ by cranci1"; +"Sora and cranci1 are not affiliated with AniList or Trakt in any way. Also note that progress updates may not be 100% accurate." = "Sora e Cranc1 non sono affiliati in nessun modo con AniList o Trakt."; +"Sora GitHub Repository" = "Repository GitHub di Sora"; +"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur rimarrà sempre gratis e senza pubblicità"; +/* Interface */ +"Sort" = "Filtra"; +"Speed Settings" = "Impostazioni velocità"; +/* Playback */ +"Start Watching" = "Inizia a Guardare"; +"Start Watching Episode %d" = "Guarda Episodio %d"; +"Storage Used" = "Memoria Usata"; +"Stream" = "Stream"; +"Streaming and video playback." = "Streaming e VideoPlayBack"; +/* Subtitles */ +"Subtitle Color" = "Colore Sottotitoli"; +"Subtitle Settings" = "Impostazioni Sottotitoli"; +/* Sync */ +"Sync anime progress" = "Sincronizza Progressi Anime"; +"Sync TV shows progress" = "Sincronizza Progressi Serie Tv"; +/* System */ +"System" = "Sistema"; +/* Instructions */ +"Tap a title to override the current match." = "Premi per Sovrascrivere il"; +"Tap Skip" = "Premi per Saltare"; +"Tap to manage your modules" = "Premi Per Gestire i Tuoi Moduli"; +"Tap to select a module" = "Premi per Selezionare un Modulo"; +/* App Information */ +"The app cache helps the app load images faster. Clearing the Documents folder will delete all downloaded modules. Do not erase App Data unless you understand the consequences — it may cause the app to malfunction." = "La Cache dell'App serve a caricare le immagini più velocemente, pulire la cartella dei documenti eliminerà tutti i moduli scaricati. Non cancellare i Dati dell'App senza aver capito le conseguenze- potrebbe causare malfunzionamenti all'app"; +"The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1–25, 26–50, and so on), allowing you to navigate through them more easily. For episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." = "Il range degli episodi controlla quanti episodi appaiono in ogni pagina.Gli episodi sono raggruppati in set (tipo 1-25,26-50 e così via), permettendo di accedere ad essi più facilmente"; +"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = ""; +/* Interface */ +"Thumbnails Width" = "Grandezza Miniature"; +"TMDB Match" = "TMDB Match"; +"Trackers" = "Trackers"; +"Trakt" = "Trakt"; +"Trakt.tv" = "Trakt.tv"; +/* Search */ +"Try different keywords" = "Prova diverse Parole Chiave"; +"Try different search terms" = "Prova diversi Termini di Ricerca"; +/* Player Controls */ +"Two Finger Hold for Pause" = "Mantieni con Due Dita Per Mettere in Pausa"; +"Unable to fetch matches. Please try again later." = "Impossibile trovare i match. Riprovare più tardi."; +"Use TMDB Poster Image" = "Usa i poster di IMDB"; +/* Version */ +"v%@" = "v%@"; +"Video Player" = "Video Player"; +/* Video Settings */ +"Video Quality Preferences" = "Preferenze qualità video"; +"View All" = "Vedi Tutti"; +"Watched" = "Guardati"; +"Why am I not seeing any episodes?" = "Perché non sto vedendo nessun episodio?"; +"WiFi Quality" = "Qualità WiFi"; +/* User Status */ +"You are not logged in" = "Non sei Loggato"; +"You have no items saved." = "Non hai Elementi Salvati"; +"Your downloaded episodes will appear here" = "I Tuoi Download appariranno Qui"; +"Your recently watched content will appear here" = "I Contenuti Guardati Recentemente Appariranno Qui"; +/* Download Settings */ +"Download Settings" = "Impostazioni Download"; +"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Il valore massimo di download contemporanei controlla il numero di episodi che possono essere scaricati simultaneamente. Valori più alti possono utilizzare più larghezza di banda e risorse del dispositivo."; +"Quality" = "Qualità"; +"Max Concurrent Downloads" = "Download Massimi in Contemporanea"; +"Allow Cellular Downloads" = "Permetti il download con Connessione Dati"; +"Quality Information" = "Informazioni Qualità"; +/* Storage */ +"Storage Management" = "Gestione Memoria"; +"Storage Used" = "Memoria Usata"; +"Library cleared successfully" = "Libreria Pulita con Successo"; +"All downloads deleted successfully" = "Tutti i Download Sono Stati Eliminati Correttamente"; +/* New additions */ +"Recent searches" = "Ricerche Recenti"; +"me frfr" = "me frfr"; +"Data" = "Dati"; +"Maximum Quality Available" = "Qualità Massima Disponibile"; diff --git a/Sora/Utils/JSLoader/JSController-Details.swift b/Sora/Utils/JSLoader/JSController-Details.swift index 51c0b8d..9c79f0f 100644 --- a/Sora/Utils/JSLoader/JSController-Details.swift +++ b/Sora/Utils/JSLoader/JSController-Details.swift @@ -65,24 +65,25 @@ extension JSController { func fetchDetailsJS(url: String, completion: @escaping ([MediaItem], [EpisodeLink]) -> Void) { guard let url = URL(string: url) else { + Logger.shared.log("Invalid URL in fetchDetailsJS: \(url)", type: "Error") completion([], []) return } if let exception = context.exception { - Logger.shared.log("JavaScript exception: \(exception)",type: "Error") + Logger.shared.log("JavaScript exception: \(exception)", type: "Error") completion([], []) return } guard let extractDetailsFunction = context.objectForKeyedSubscript("extractDetails") else { - Logger.shared.log("No JavaScript function extractDetails found",type: "Error") + Logger.shared.log("No JavaScript function extractDetails found", type: "Error") completion([], []) return } guard let extractEpisodesFunction = context.objectForKeyedSubscript("extractEpisodes") else { - Logger.shared.log("No JavaScript function extractEpisodes found",type: "Error") + Logger.shared.log("No JavaScript function extractEpisodes found", type: "Error") completion([], []) return } @@ -95,13 +96,13 @@ extension JSController { dispatchGroup.enter() let promiseValueDetails = extractDetailsFunction.call(withArguments: [url.absoluteString]) guard let promiseDetails = promiseValueDetails else { - Logger.shared.log("extractDetails did not return a Promise",type: "Error") + Logger.shared.log("extractDetails did not return a Promise", type: "Error") + dispatchGroup.leave() completion([], []) return } let thenBlockDetails: @convention(block) (JSValue) -> Void = { result in - Logger.shared.log(result.toString(),type: "Debug") if let jsonOfDetails = result.toString(), let dataDetails = jsonOfDetails.data(using: .utf8) { do { @@ -114,19 +115,19 @@ extension JSController { ) } } else { - Logger.shared.log("Failed to parse JSON of extractDetails",type: "Error") + Logger.shared.log("Failed to parse JSON of extractDetails", type: "Error") } } catch { - Logger.shared.log("JSON parsing error of extract details: \(error)",type: "Error") + Logger.shared.log("JSON parsing error of extract details: \(error)", type: "Error") } } else { - Logger.shared.log("Result is not a string of extractDetails",type: "Error") + Logger.shared.log("Result is not a string of extractDetails", type: "Error") } dispatchGroup.leave() } let catchBlockDetails: @convention(block) (JSValue) -> Void = { error in - Logger.shared.log("Promise rejected of extractDetails: \(String(describing: error.toString()))",type: "Error") + Logger.shared.log("Promise rejected of extractDetails: \(String(describing: error.toString()))", type: "Error") dispatchGroup.leave() } @@ -138,14 +139,23 @@ extension JSController { dispatchGroup.enter() let promiseValueEpisodes = extractEpisodesFunction.call(withArguments: [url.absoluteString]) + + let timeoutWorkItem = DispatchWorkItem { + Logger.shared.log("Timeout for extractEpisodes", type: "Warning") + dispatchGroup.leave() + } + DispatchQueue.main.asyncAfter(deadline: .now() + 5.0, execute: timeoutWorkItem) + guard let promiseEpisodes = promiseValueEpisodes else { - Logger.shared.log("extractEpisodes did not return a Promise",type: "Error") + Logger.shared.log("extractEpisodes did not return a Promise", type: "Error") + timeoutWorkItem.cancel() + dispatchGroup.leave() completion([], []) return } let thenBlockEpisodes: @convention(block) (JSValue) -> Void = { result in - Logger.shared.log(result.toString(),type: "Debug") + timeoutWorkItem.cancel() if let jsonOfEpisodes = result.toString(), let dataEpisodes = jsonOfEpisodes.data(using: .utf8) { do { @@ -159,19 +169,20 @@ extension JSController { ) } } else { - Logger.shared.log("Failed to parse JSON of extractEpisodes",type: "Error") + Logger.shared.log("Failed to parse JSON of extractEpisodes", type: "Error") } } catch { - Logger.shared.log("JSON parsing error of extractEpisodes: \(error)",type: "Error") + Logger.shared.log("JSON parsing error of extractEpisodes: \(error)", type: "Error") } } else { - Logger.shared.log("Result is not a string of extractEpisodes",type: "Error") + Logger.shared.log("Result is not a string of extractEpisodes", type: "Error") } dispatchGroup.leave() } let catchBlockEpisodes: @convention(block) (JSValue) -> Void = { error in - Logger.shared.log("Promise rejected of extractEpisodes: \(String(describing: error.toString()))",type: "Error") + timeoutWorkItem.cancel() + Logger.shared.log("Promise rejected of extractEpisodes: \(String(describing: error.toString()))", type: "Error") dispatchGroup.leave() } diff --git a/Sora/Utils/JSLoader/JSController-Novel.swift b/Sora/Utils/JSLoader/JSController-Novel.swift new file mode 100644 index 0000000..6b01c11 --- /dev/null +++ b/Sora/Utils/JSLoader/JSController-Novel.swift @@ -0,0 +1,187 @@ +// +// JSController-Novel.swift +// Sora +// +// Created by paul on 20/06/25. +// + +import Foundation +import JavaScriptCore + +enum JSError: Error { + case moduleNotFound + case invalidResponse + case emptyContent + case redirectError + case jsException(String) + + var localizedDescription: String { + switch self { + case .moduleNotFound: + return "Module not found" + case .invalidResponse: + return "Invalid response from server" + case .emptyContent: + return "No content received" + case .redirectError: + return "Redirect error occurred" + case .jsException(let message): + return "JavaScript error: \(message)" + } + } +} + +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 { + throw JSError.moduleNotFound + } + + return await withCheckedContinuation { (continuation: CheckedContinuation<[[String: Any]], Never>) in + DispatchQueue.main.async { [weak self] in + guard let self = self else { + continuation.resume(returning: []) + return + } + guard let extractChaptersFunction = self.context.objectForKeyedSubscript("extractChapters") else { + Logger.shared.log("extractChapters: function not found", type: "Error") + continuation.resume(returning: []) + return + } + let result = extractChaptersFunction.call(withArguments: [href]) + if result?.isUndefined == true || result == nil { + Logger.shared.log("extractChapters: result is undefined or nil", type: "Error") + continuation.resume(returning: []) + return + } + if let result = result, result.hasProperty("then") { + let group = DispatchGroup() + group.enter() + var chaptersArr: [[String: Any]] = [] + let thenBlock: @convention(block) (JSValue) -> Void = { jsValue in + Logger.shared.log("extractChapters thenBlock: \(jsValue)", type: "Debug") + if let arr = jsValue.toArray() as? [[String: Any]] { + Logger.shared.log("extractChapters: parsed as array, count = \(arr.count)", type: "Debug") + chaptersArr = arr + } else if let jsonString = jsValue.toString(), let data = jsonString.data(using: .utf8) { + do { + if let arr = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] { + Logger.shared.log("extractChapters: parsed as JSON string, count = \(arr.count)", type: "Debug") + chaptersArr = arr + } else { + Logger.shared.log("extractChapters: JSON string did not parse to array", type: "Error") + } + } catch { + Logger.shared.log("JSON parsing error of extractChapters: \(error)", type: "Error") + } + } else { + Logger.shared.log("extractChapters: could not parse result", type: "Error") + } + group.leave() + } + let catchBlock: @convention(block) (JSValue) -> Void = { jsValue in + Logger.shared.log("extractChapters catchBlock: \(jsValue)", type: "Error") + group.leave() + } + result.invokeMethod("then", withArguments: [thenBlock]) + result.invokeMethod("catch", withArguments: [catchBlock]) + group.notify(queue: .main) { + continuation.resume(returning: chaptersArr) + } + } else { + if let arr = result?.toArray() as? [[String: Any]] { + Logger.shared.log("extractChapters: direct array, count = \(arr.count)", type: "Debug") + continuation.resume(returning: arr) + } else if let jsonString = result?.toString(), let data = jsonString.data(using: .utf8) { + do { + if let arr = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] { + Logger.shared.log("extractChapters: direct JSON string, count = \(arr.count)", type: "Debug") + continuation.resume(returning: arr) + } else { + Logger.shared.log("extractChapters: direct JSON string did not parse to array", type: "Error") + continuation.resume(returning: []) + } + } catch { + Logger.shared.log("JSON parsing error of extractChapters: \(error)", type: "Error") + continuation.resume(returning: []) + } + } else { + Logger.shared.log("extractChapters: could not parse direct result", type: "Error") + continuation.resume(returning: []) + } + } + } + } + } + + @MainActor + func extractText(moduleId: String, href: String) async throws -> String { + guard let module = ModuleManager().modules.first(where: { $0.id.uuidString == moduleId }) else { + throw JSError.moduleNotFound + } + + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + DispatchQueue.main.async { [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 let exception = self.context.exception { + Logger.shared.log("Error extracting text: \(exception)", type: "Error") + } + + if let result = result, result.hasProperty("then") { + let group = DispatchGroup() + group.enter() + var extractedText = "" + var extractError: Error? = nil + + let thenBlock: @convention(block) (JSValue) -> Void = { jsValue in + Logger.shared.log("extractText thenBlock: received value", type: "Debug") + if let text = jsValue.toString(), !text.isEmpty { + Logger.shared.log("extractText: successfully extracted text", type: "Debug") + extractedText = text + } else { + extractError = JSError.emptyContent + } + group.leave() + } + + let catchBlock: @convention(block) (JSValue) -> Void = { jsValue in + Logger.shared.log("extractText catchBlock: \(jsValue)", type: "Error") + if extractedText.isEmpty { + extractError = JSError.jsException(jsValue.toString() ?? "Unknown error") + } + group.leave() + } + + result.invokeMethod("then", withArguments: [thenBlock]) + result.invokeMethod("catch", withArguments: [catchBlock]) + + group.notify(queue: .main) { + if !extractedText.isEmpty { + continuation.resume(returning: extractedText) + } else if let error = extractError { + continuation.resume(throwing: error) + } else { + continuation.resume(throwing: JSError.emptyContent) + } + } + } 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) + } + } + } + } + } +} \ No newline at end of file diff --git a/Sora/Utils/JSLoader/JSController.swift b/Sora/Utils/JSLoader/JSController.swift index 163eae9..4cc7f60 100644 --- a/Sora/Utils/JSLoader/JSController.swift +++ b/Sora/Utils/JSLoader/JSController.swift @@ -42,6 +42,35 @@ class JSController: NSObject, ObservableObject { func setupContext() { context.setupJavaScriptEnvironment() + // Inject async Promise bridge for extractChapters with debug logging + let asyncChaptersHelper = """ + function extractChaptersWithCallback(href, callback) { + try { + console.log('[JS] extractChaptersWithCallback called with href:', href); + var result = extractChapters(href); + if (result && typeof result.then === 'function') { + result.then(function(arr) { + console.log('[JS] extractChaptersWithCallback Promise resolved, arr.length:', arr && arr.length); + callback(arr); + }).catch(function(e) { + console.log('[JS] extractChaptersWithCallback Promise rejected:', e); + callback([]); + }); + } else { + console.log('[JS] extractChaptersWithCallback result is not a Promise:', result); + callback(result); + } + } catch (e) { + console.log('[JS] extractChaptersWithCallback threw:', e); + callback([]); + } + } + """ + context.evaluateScript(asyncChaptersHelper) + // Print JS exceptions to Xcode console + context.exceptionHandler = { context, exception in + print("[JS Exception]", exception?.toString() ?? "unknown") + } setupDownloadSession() } diff --git a/Sora/Utils/Modules/Modules.swift b/Sora/Utils/Modules/Modules.swift index 7debdbb..f52f0e2 100644 --- a/Sora/Utils/Modules/Modules.swift +++ b/Sora/Utils/Modules/Modules.swift @@ -24,6 +24,7 @@ struct ModuleMetadata: Codable, Hashable { let multiStream: Bool? let multiSubs: Bool? let type: String? + let novel: Bool? struct Author: Codable, Hashable { let name: String diff --git a/Sora/Utils/TabBar/TabBar.swift b/Sora/Utils/TabBar/TabBar.swift index 148f1c5..8b93d24 100644 --- a/Sora/Utils/TabBar/TabBar.swift +++ b/Sora/Utils/TabBar/TabBar.swift @@ -90,7 +90,7 @@ struct TabBar: View { .stroke( LinearGradient( gradient: Gradient(stops: [ - .init(color: Color.accentColor.opacity(gradientOpacity), location: 0), + .init(color: Color.accentColor.opacity(0.25), location: 0), .init(color: Color.accentColor.opacity(0), location: 1) ]), startPoint: .top, @@ -162,7 +162,7 @@ struct TabBar: View { .strokeBorder( LinearGradient( gradient: Gradient(stops: [ - .init(color: Color.accentColor.opacity(gradientOpacity), location: 0), + .init(color: Color.accentColor.opacity(0.25), location: 0), .init(color: Color.accentColor.opacity(0), location: 1) ]), startPoint: .top, diff --git a/Sora/Views/DownloadView.swift b/Sora/Views/DownloadView.swift index c997f4f..5e36154 100644 --- a/Sora/Views/DownloadView.swift +++ b/Sora/Views/DownloadView.swift @@ -11,6 +11,7 @@ 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 @@ -70,6 +71,9 @@ struct DownloadView: View { Text(String(format: NSLocalizedString("Are you sure you want to delete '%@'?", comment: ""), asset.episodeDisplayName)) } } + .onAppear { + tabBarController.showTabBar() + } } .deviceScaled() .navigationViewStyle(StackNavigationViewStyle()) @@ -232,7 +236,8 @@ struct DownloadView: View { softsub: nil, multiStream: nil, multiSubs: nil, - type: nil + type: nil, + novel: false ) let dummyModule = ScrapingModule( @@ -241,7 +246,6 @@ struct DownloadView: View { metadataUrl: "" ) - // Always use CustomMediaPlayerViewController for consistency let customPlayer = CustomMediaPlayerViewController( module: dummyModule, urlString: asset.localURL.absoluteString, @@ -1026,7 +1030,6 @@ struct EnhancedShowEpisodesView: View { } .onAppear { tabBarController.hideTabBar() - // Enable swipe-to-go-back gesture if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let window = windowScene.windows.first, let navigationController = window.rootViewController?.children.first as? UINavigationController { diff --git a/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridItemView.swift b/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridItemView.swift index e833918..3c49eba 100644 --- a/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridItemView.swift +++ b/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridItemView.swift @@ -12,6 +12,10 @@ struct BookmarkGridItemView: View { let item: LibraryItem let module: Module + var isNovel: Bool { + module.metadata.novel ?? false + } + var body: some View { ZStack { LazyImage(url: URL(string: item.imageUrl)) { state in @@ -30,7 +34,7 @@ struct BookmarkGridItemView: View { } } .overlay( - ZStack { + ZStack(alignment: .bottomTrailing) { Circle() .fill(Color.black.opacity(0.5)) .frame(width: 28, height: 28) @@ -49,6 +53,13 @@ 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) } .padding(8), alignment: .topLeading diff --git a/Sora/Views/LibraryView/BookmarkComponents/CollectionDetailView.swift b/Sora/Views/LibraryView/BookmarkComponents/CollectionDetailView.swift index 620c05c..c256090 100644 --- a/Sora/Views/LibraryView/BookmarkComponents/CollectionDetailView.swift +++ b/Sora/Views/LibraryView/BookmarkComponents/CollectionDetailView.swift @@ -12,6 +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 @@ -282,6 +283,7 @@ struct CollectionDetailView: View { navigationController.interactivePopGestureRecognizer?.isEnabled = true navigationController.interactivePopGestureRecognizer?.delegate = nil } + tabBarController.showTabBar() } } } \ No newline at end of file diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 1f0844e..d5a84b1 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -12,6 +12,7 @@ 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 @@ -139,6 +140,7 @@ struct LibraryView: View { .deviceScaled() .onAppear { fetchContinueWatching() + tabBarController.showTabBar() } .onChange(of: scenePhase) { newPhase in if newPhase == .active { diff --git a/Sora/Views/MediaInfoView/ChapterCell/ChapterCell.swift b/Sora/Views/MediaInfoView/ChapterCell/ChapterCell.swift new file mode 100644 index 0000000..ccfa006 --- /dev/null +++ b/Sora/Views/MediaInfoView/ChapterCell/ChapterCell.swift @@ -0,0 +1,78 @@ +// +// ChapterCell.swift +// Sora +// +// Created by paul on 20/06/25. +// + +import SwiftUI + +struct ChapterCell: View { + let chapterNumber: String + let chapterTitle: String + let isCurrentChapter: Bool + + var body: some View { + HStack(alignment: .center, spacing: 12) { + VStack(alignment: .leading, spacing: 3) { + HStack(alignment: .center, spacing: 6) { + Text("Chapter \(chapterNumber)") + .font(.system(size: 17, weight: .semibold)) + .foregroundColor(.primary) + .lineLimit(1) + + if isCurrentChapter { + Text("Current") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.blue) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(Color.blue.opacity(0.18)) + ) + } + Spacer(minLength: 0) + } + Text(chapterTitle) + .font(.system(size: 15)) + .foregroundColor(.secondary) + .lineLimit(2) + } + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 14) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color(UIColor.systemBackground)) + .overlay( + RoundedRectangle(cornerRadius: 16) + .fill(Color.accentColor.opacity(0.08)) + ) + ) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke( + LinearGradient( + gradient: Gradient(stops: [ + .init(color: Color.accentColor.opacity(0.35), location: 0), + .init(color: Color.accentColor.opacity(0), location: 1) + ]), + startPoint: .top, + endPoint: .bottom + ), + lineWidth: 1.2 + ) + ) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } +} + +#Preview { + ChapterCell( + chapterNumber: "1", + chapterTitle: "Chapter 1: The Beginning", + isCurrentChapter: true + ) +} \ No newline at end of file diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 23ded14..471bece 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -28,6 +28,7 @@ struct MediaInfoView: View { @State private var synopsis: String = "" @State private var airdate: String = "" @State private var episodeLinks: [EpisodeLink] = [] + @State private var chapters: [[String: Any]] = [] @State private var itemID: Int? @State private var tmdbID: Int? @State private var tmdbType: TMDBFetcher.MediaType? = nil @@ -119,29 +120,37 @@ struct MediaInfoView: View { return isCompactLayout ? 20 : 16 } - private var startWatchingText: String { - let indices = finishedAndUnfinishedIndices() - let finished = indices.finished - let unfinished = indices.unfinished - - if episodeLinks.count == 1 { - if let _ = unfinished { - return NSLocalizedString("Continue Watching", comment: "") + private var startActionText: String { + if module.metadata.novel == true { + let lastReadChapter = UserDefaults.standard.string(forKey: "lastReadChapter") + if let lastRead = lastReadChapter, chapters.contains(where: { $0["href"] as! String == lastRead }) { + return NSLocalizedString("Continue Reading", comment: "") } + return NSLocalizedString("Start Reading", comment: "") + } else { + let indices = finishedAndUnfinishedIndices() + let finished = indices.finished + let unfinished = indices.unfinished + + if episodeLinks.count == 1 { + if let _ = unfinished { + return NSLocalizedString("Continue Watching", comment: "") + } + return NSLocalizedString("Start Watching", comment: "") + } + + if let finishedIndex = finished, finishedIndex < episodeLinks.count - 1 { + let nextEp = episodeLinks[finishedIndex + 1] + return String(format: NSLocalizedString("Start Watching Episode %d", comment: ""), nextEp.number) + } + + if let unfinishedIndex = unfinished { + let currentEp = episodeLinks[unfinishedIndex] + return String(format: NSLocalizedString("Continue Watching Episode %d", comment: ""), currentEp.number) + } + return NSLocalizedString("Start Watching", comment: "") } - - if let finishedIndex = finished, finishedIndex < episodeLinks.count - 1 { - let nextEp = episodeLinks[finishedIndex + 1] - return String(format: NSLocalizedString("Start Watching Episode %d", comment: ""), nextEp.number) - } - - if let unfinishedIndex = unfinished { - let currentEp = episodeLinks[unfinishedIndex] - return String(format: NSLocalizedString("Continue Watching Episode %d", comment: ""), currentEp.number) - } - - return NSLocalizedString("Start Watching", comment: "") } private var singleEpisodeWatchText: String { @@ -154,6 +163,14 @@ struct MediaInfoView: View { return NSLocalizedString("Mark watched", comment: "") } + @State private var selectedChapterRange: Range = { + let size = UserDefaults.standard.integer(forKey: "chapterChunkSize") + let chunk = size == 0 ? 100 : size + return 0.. 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 let href = chapter["href"] as? String, + let number = chapter["number"] as? Int, + let title = chapter["title"] as? String { + NavigationLink( + destination: ReaderView( + moduleId: module.id.uuidString, + chapterHref: href, + chapterTitle: title + ) + ) { + ChapterCell( + chapterNumber: String(number), + chapterTitle: title, + isCurrentChapter: UserDefaults.standard.string(forKey: "lastReadChapter") == href + ) + } + .buttonStyle(PlainButtonStyle()) + } + } + } + } + } + + @ViewBuilder + private var noContentSection: some View { VStack(spacing: 8) { - Image(systemName: "tv.slash") + Image(systemName: module.metadata.novel == true ? "book.slash" : "tv.slash") .font(.system(size: 48)) .foregroundColor(.secondary) - Text(NSLocalizedString("No Episodes Available", comment: "")) + Text(module.metadata.novel == true ? NSLocalizedString("No Chapters Available", comment: "") : NSLocalizedString("No Episodes Available", comment: "")) .font(.title2) .fontWeight(.semibold) .foregroundColor(.primary) - Text(NSLocalizedString("Episodes might not be available yet or there could be an issue with the source.", comment: "")) + Text(module.metadata.novel == true ? NSLocalizedString("Chapters might not be available yet or there could be an issue with the source.", comment: "") : NSLocalizedString("Episodes might not be available yet or there could be an issue with the source.", comment: "")) .font(.body) - .lineLimit(0) .foregroundColor(.secondary) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) .padding(.horizontal) } .padding(.vertical, 50) @@ -676,7 +790,7 @@ struct MediaInfoView: View { .sheet(isPresented: $isMatchingPresented) { AnilistMatchPopupView(seriesTitle: title) { id, matched in handleAniListMatch(selectedID: id) - matchedTitle = matched // ← now in scope + matchedTitle = matched fetchMetadataIDIfNeeded() } } @@ -684,7 +798,7 @@ struct MediaInfoView: View { TMDBMatchPopupView(seriesTitle: title) { id, type, matched in tmdbID = id tmdbType = type - matchedTitle = matched // ← now in scope + matchedTitle = matched fetchMetadataIDIfNeeded() } } @@ -755,37 +869,105 @@ struct MediaInfoView: View { } private func setupInitialData() async { - guard !hasFetched else { return } - - let savedCustomID = UserDefaults.standard.integer(forKey: "custom_anilist_id_\(href)") - if savedCustomID != 0 { customAniListID = savedCustomID } - - if let savedPoster = UserDefaults.standard.string(forKey: "tmdbPosterURL_\(href)") { - imageUrl = savedPoster + do { + Logger.shared.log("setupInitialData: module.metadata.novel = \(String(describing: module.metadata.novel))", 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") + ) + } + let jsContent = try? moduleManager.getModuleContent(module) + if let jsContent = jsContent { + jsController.loadScript(jsContent) + } + + await withTaskGroup(of: Void.self) { group in + var chaptersLoaded = false + var detailsLoaded = false + let timeout: TimeInterval = 8.0 + let start = Date() + + group.addTask { + let fetchedChapters = try? await JSController.shared.extractChapters(moduleId: module.id.uuidString, href: href) + DispatchQueue.main.async { + if let fetchedChapters = fetchedChapters { + Logger.shared.log("setupInitialData: fetchedChapters count = \(fetchedChapters.count)", type: "Debug") + Logger.shared.log("setupInitialData: fetchedChapters = \(fetchedChapters)", type: "Debug") + self.chapters = fetchedChapters + } + chaptersLoaded = true + } + } + group.addTask { + await withCheckedContinuation { continuation in + self.fetchDetails() + var checkDetails: (() -> Void)? + checkDetails = { + if !(self.synopsis.isEmpty && self.aliases.isEmpty && self.airdate.isEmpty) { + detailsLoaded = true + continuation.resume() + } else if Date().timeIntervalSince(start) > timeout { + detailsLoaded = true + continuation.resume() + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + checkDetails?() + } + } + } + checkDetails?() + } + } + group.addTask { + try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) + DispatchQueue.main.async { + chaptersLoaded = true + detailsLoaded = true + } + } + while !(chaptersLoaded && detailsLoaded) { + try? await Task.sleep(nanoseconds: 100_000_000) + } + } + DispatchQueue.main.async { + self.hasFetched = true + self.isLoading = false + } + } else { + let savedCustomID = UserDefaults.standard.integer(forKey: "custom_anilist_id_\(href)") + if savedCustomID != 0 { customAniListID = savedCustomID } + 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") + ) + fetchDetails() + if savedCustomID != 0 { + itemID = savedCustomID + activeProvider = "AniList" + UserDefaults.standard.set("AniList", forKey: "metadataProviders") + } else { + fetchMetadataIDIfNeeded() + } + hasFetched = true + AnalyticsManager.shared.sendEvent( + event: "MediaInfoView", + additionalData: ["title": title] + ) + } + } catch { + isError = true + isLoading = false + Logger.shared.log("Error loading media info: \(error)", type: "Error") } - - 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 - activeProvider = "AniList" - UserDefaults.standard.set("AniList", forKey: "metadataProviders") - } else { - fetchMetadataIDIfNeeded() - } - - hasFetched = true - AnalyticsManager.shared.sendEvent( - event: "MediaInfoView", - additionalData: ["title": title] - ) } private func cancelCurrentFetch() { @@ -1169,6 +1351,7 @@ struct MediaInfoView: View { func fetchDetails() { + Logger.shared.log("fetchDetails: called", type: "Debug") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { Task { do { @@ -1176,27 +1359,85 @@ struct MediaInfoView: View { jsController.loadScript(jsContent) if module.metadata.asyncJS == true { jsController.fetchDetailsJS(url: href) { items, episodes in - if let item = items.first { + Logger.shared.log("fetchDetails: items = \(items)", type: "Debug") + Logger.shared.log("fetchDetails: episodes = \(episodes)", type: "Debug") + + if let mediaItems = items as? [MediaItem], let item = mediaItems.first { self.synopsis = item.description self.aliases = item.aliases self.airdate = item.airdate + } else if let str = items as? String { + if let data = str.data(using: .utf8), + let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]], + let dict = arr.first { + self.synopsis = dict["description"] as? String ?? "" + self.aliases = dict["aliases"] as? String ?? "" + self.airdate = dict["airdate"] as? String ?? "" + } + } else if let dict = items as? [String: Any] { + self.synopsis = dict["description"] as? String ?? "" + self.aliases = dict["aliases"] as? String ?? "" + self.airdate = dict["airdate"] as? String ?? "" + } else if let arr = items as? [[String: Any]], let dict = arr.first { + self.synopsis = dict["description"] as? String ?? "" + self.aliases = dict["aliases"] as? String ?? "" + self.airdate = dict["airdate"] as? String ?? "" + } else { + Logger.shared.log("Failed to process items of type: \(type(of: items))", type: "Error") + } + + if self.module.metadata.novel ?? false { + Logger.shared.log("fetchDetails: (novel) chapters count = \(self.chapters.count)", type: "Debug") + self.isLoading = false + self.isRefetching = false + } else { + Logger.shared.log("fetchDetails: (episodes) episodes count = \(episodes.count)", type: "Debug") + self.episodeLinks = episodes + self.restoreSelectionState() + self.isLoading = false + self.isRefetching = false } - self.episodeLinks = episodes - self.restoreSelectionState() - self.isLoading = false - self.isRefetching = false } } else { jsController.fetchDetails(url: href) { items, episodes in - if let item = items.first { + Logger.shared.log("fetchDetails: items = \(items)", type: "Debug") + Logger.shared.log("fetchDetails: episodes = \(episodes)", type: "Debug") + + if let mediaItems = items as? [MediaItem], let item = mediaItems.first { self.synopsis = item.description self.aliases = item.aliases self.airdate = item.airdate + } else if let str = items as? String { + if let data = str.data(using: .utf8), + let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]], + let dict = arr.first { + self.synopsis = dict["description"] as? String ?? "" + self.aliases = dict["aliases"] as? String ?? "" + self.airdate = dict["airdate"] as? String ?? "" + } + } else if let dict = items as? [String: Any] { + self.synopsis = dict["description"] as? String ?? "" + self.aliases = dict["aliases"] as? String ?? "" + self.airdate = dict["airdate"] as? String ?? "" + } else if let arr = items as? [[String: Any]], let dict = arr.first { + self.synopsis = dict["description"] as? String ?? "" + self.aliases = dict["aliases"] as? String ?? "" + self.airdate = dict["airdate"] as? String ?? "" + } else { + Logger.shared.log("Failed to process items of type: \(type(of: items))", type: "Error") + } + + if self.module.metadata.novel ?? false { + Logger.shared.log("fetchDetails: (novel) chapters count = \(self.chapters.count)", type: "Debug") + self.isLoading = false + self.isRefetching = false + } else { + Logger.shared.log("fetchDetails: (episodes) episodes count = \(episodes.count)", type: "Debug") + self.episodeLinks = episodes + self.restoreSelectionState() + self.isLoading = false + self.isRefetching = false } - self.episodeLinks = episodes - self.restoreSelectionState() - self.isLoading = false - self.isRefetching = false } } } catch { @@ -1941,4 +2182,15 @@ struct MediaInfoView: View { findTopViewController.findViewController(rootVC).present(alert, animated: true) } } + + private func generateChapterRanges() -> [Range] { + let chunkSize = chapterChunkSize + let totalChapters = chapters.count + var ranges: [Range] = [] + for i in stride(from: 0, to: totalChapters, by: chunkSize) { + let end = min(i + chunkSize, totalChapters) + ranges.append(i.. CGFloat? { + if let value = object(forKey: defaultName) as? NSNumber { + return CGFloat(value.doubleValue) + } + return nil + } + + func set(_ value: CGFloat, forKey defaultName: String) { + set(NSNumber(value: Double(value)), forKey: defaultName) + } +} + +struct ReaderView: View { + let moduleId: String + let chapterHref: String + let chapterTitle: String + + @State private var htmlContent: String = "" + @State private var isLoading: Bool = true + @State private var error: Error? + @State private var isHeaderVisible: Bool = true + @State private var fontSize: CGFloat = 16 + @State private var selectedFont: String = "-apple-system" + @State private var fontWeight: String = "normal" + @State private var isAutoScrolling: Bool = false + @State private var autoScrollSpeed: Double = 1.0 + @State private var autoScrollTimer: Timer? + @State private var selectedColorPreset: Int = 0 + @State private var isSettingsExpanded: Bool = false + @State private var textAlignment: String = "left" + @State private var lineSpacing: CGFloat = 1.6 + @State private var margin: CGFloat = 4 + @Environment(\.dismiss) private var dismiss + @EnvironmentObject var tabBarController: TabBarController + + private let fontOptions = [ + ("-apple-system", "System"), + ("Georgia", "Georgia"), + ("Times New Roman", "Times"), + ("Helvetica", "Helvetica"), + ("Charter", "Charter"), + ("New York", "New York") + ] + private let weightOptions = [ + ("300", "Light"), + ("normal", "Regular"), + ("600", "Semibold"), + ("bold", "Bold") + ] + + private let alignmentOptions = [ + ("left", "Left", "text.alignleft"), + ("center", "Center", "text.aligncenter"), + ("right", "Right", "text.alignright"), + ("justify", "Justify", "text.justify") + ] + + private let colorPresets = [ + (name: "Pure", background: "#ffffff", text: "#000000"), + (name: "Warm", background: "#f9f1e4", text: "#4f321c"), + (name: "Slate", background: "#49494d", text: "#d7d7d8"), + (name: "Off-Black", background: "#121212", text: "#EAEAEA"), + (name: "Dark", background: "#000000", text: "#ffffff") + ] + + private var currentTheme: (background: Color, text: Color) { + let preset = colorPresets[selectedColorPreset] + return ( + background: Color(hex: preset.background), + text: Color(hex: preset.text) + ) + } + + init(moduleId: String, chapterHref: String, chapterTitle: String) { + self.moduleId = moduleId + self.chapterHref = chapterHref + self.chapterTitle = chapterTitle + + _fontSize = State(initialValue: UserDefaults.standard.cgFloat(forKey: "readerFontSize") ?? 16) + _selectedFont = State(initialValue: UserDefaults.standard.string(forKey: "readerFontFamily") ?? "-apple-system") + _fontWeight = State(initialValue: UserDefaults.standard.string(forKey: "readerFontWeight") ?? "normal") + _selectedColorPreset = State(initialValue: UserDefaults.standard.integer(forKey: "readerColorPreset")) + _textAlignment = State(initialValue: UserDefaults.standard.string(forKey: "readerTextAlignment") ?? "left") + _lineSpacing = State(initialValue: UserDefaults.standard.cgFloat(forKey: "readerLineSpacing") ?? 1.6) + _margin = State(initialValue: UserDefaults.standard.cgFloat(forKey: "readerMargin") ?? 4) + } + + var body: some View { + ZStack(alignment: .bottom) { + currentTheme.background.ignoresSafeArea() + + if isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: currentTheme.text)) + .onDisappear { + stopAutoScroll() + } + } else if let error = error { + VStack { + Text("Error loading chapter") + .font(.headline) + .foregroundColor(currentTheme.text) + Text(error.localizedDescription) + .font(.subheadline) + .foregroundColor(currentTheme.text.opacity(0.7)) + } + } else { + ZStack { + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + withAnimation(.easeInOut(duration: 0.6)) { + isHeaderVisible.toggle() + if !isHeaderVisible { + isSettingsExpanded = false + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + HTMLView( + htmlContent: htmlContent, + fontSize: fontSize, + fontFamily: selectedFont, + fontWeight: fontWeight, + textAlignment: textAlignment, + lineSpacing: lineSpacing, + margin: margin, + isAutoScrolling: $isAutoScrolling, + autoScrollSpeed: autoScrollSpeed, + colorPreset: colorPresets[selectedColorPreset] + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.horizontal) + .simultaneousGesture(TapGesture().onEnded { + withAnimation(.easeInOut(duration: 0.6)) { + isHeaderVisible.toggle() + if !isHeaderVisible { + isSettingsExpanded = false + } + } + }) + } + .padding(.top, isHeaderVisible ? 0 : (UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)) + } + + headerView + .opacity(isHeaderVisible ? 1 : 0) + .offset(y: isHeaderVisible ? 0 : -100) + .allowsHitTesting(isHeaderVisible) + .animation(.easeInOut(duration: 0.6), value: isHeaderVisible) + .zIndex(1) + + if isHeaderVisible { + footerView + .transition(.move(edge: .bottom)) + .zIndex(2) + } + } + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + .ignoresSafeArea() + .onAppear { + tabBarController.hideTabBar() + UserDefaults.standard.set(chapterHref, forKey: "lastReadChapter") + } + .task { + do { + let content = try await JSController.shared.extractText(moduleId: moduleId, href: chapterHref) + if !content.isEmpty { + htmlContent = content + isLoading = false + } else { + throw JSError.invalidResponse + } + } catch { + self.error = error + isLoading = false + } + } + } + + private func stopAutoScroll() { + autoScrollTimer?.invalidate() + autoScrollTimer = nil + isAutoScrolling = false + } + + private var headerView: some View { + VStack { + ZStack(alignment: .top) { + // Base header content + HStack { + Button(action: { + dismiss() + }) { + Image(systemName: "chevron.left") + .font(.system(size: 20, weight: .bold)) + .foregroundColor(currentTheme.text) + .padding(12) + .background(currentTheme.background.opacity(0.8)) + .clipShape(Circle()) + .circularGradientOutline() + } + .padding(.leading) + + Text(chapterTitle) + .font(.headline) + .foregroundColor(currentTheme.text) + .lineLimit(1) + .truncationMode(.tail) + + Spacer() + + Color.clear + .frame(width: 44, height: 44) + .padding(.trailing) + } + .opacity(isHeaderVisible ? 1 : 0) + .offset(y: isHeaderVisible ? 0 : -100) + .animation(.easeInOut(duration: 0.6), value: isHeaderVisible) + .contentShape(Rectangle()) + .onTapGesture { + withAnimation(.easeInOut(duration: 0.6)) { + isHeaderVisible = false + isSettingsExpanded = false + } + } + + HStack { + Spacer() + ZStack(alignment: .topTrailing) { + Button(action: { + withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) { + isSettingsExpanded.toggle() + } + }) { + Image(systemName: "ellipsis") + .font(.system(size: 18, weight: .bold)) + .foregroundColor(currentTheme.text) + .padding(12) + .background(currentTheme.background.opacity(0.8)) + .clipShape(Circle()) + .circularGradientOutline() + .rotationEffect(.degrees(isSettingsExpanded ? 90 : 0)) + } + .opacity(isHeaderVisible ? 1 : 0) + .offset(y: isHeaderVisible ? 0 : -100) + .animation(.easeInOut(duration: 0.6), value: isHeaderVisible) + + if isSettingsExpanded { + VStack(spacing: 8) { + Menu { + VStack { + Text("Font Size: \(Int(fontSize))pt") + .font(.headline) + .padding(.bottom, 8) + + Slider(value: Binding( + get: { fontSize }, + set: { newValue in + fontSize = newValue + UserDefaults.standard.set(newValue, forKey: "readerFontSize") + } + ), in: 12...32, step: 1) { + Text("Font Size") + } + .padding(.horizontal) + } + .padding() + } label: { + settingsButtonLabel(icon: "textformat.size") + } + + Menu { + ForEach(fontOptions, id: \.0) { font in + Button(action: { + selectedFont = font.0 + UserDefaults.standard.set(font.0, forKey: "readerFontFamily") + }) { + HStack { + Text(font.1) + .font(.custom(font.0, size: 16)) + Spacer() + if selectedFont == font.0 { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + } + } + } + } label: { + settingsButtonLabel(icon: "textformat.characters") + } + + Menu { + ForEach(weightOptions, id: \.0) { weight in + Button(action: { + fontWeight = weight.0 + UserDefaults.standard.set(weight.0, forKey: "readerFontWeight") + }) { + HStack { + Text(weight.1) + .fontWeight(weight.0 == "300" ? .light : + weight.0 == "normal" ? .regular : + weight.0 == "600" ? .semibold : .bold) + Spacer() + if fontWeight == weight.0 { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + } + } + } + } label: { + settingsButtonLabel(icon: "bold") + } + + Menu { + ForEach(0.. some View { + Image(systemName: icon) + .font(.system(size: 16, weight: .bold)) + .foregroundColor(currentTheme.text) + .padding(10) + .background(currentTheme.background.opacity(0.8)) + .clipShape(Circle()) + .circularGradientOutline() + } +} + +struct ColorPreviewCircle: View { + let backgroundColor: String + let textColor: String + + var body: some View { + ZStack { + Circle() + .fill( + LinearGradient( + colors: [ + Color(hex: backgroundColor), + Color(hex: textColor) + ], + startPoint: .leading, + endPoint: .trailing + ) + ) + } + } +} + +struct HTMLView: UIViewRepresentable { + let htmlContent: String + let fontSize: CGFloat + let fontFamily: String + let fontWeight: String + let textAlignment: String + let lineSpacing: CGFloat + let margin: CGFloat + @Binding var isAutoScrolling: Bool + let autoScrollSpeed: Double + let colorPreset: (name: String, background: String, text: String) + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + var parent: HTMLView + var scrollTimer: Timer? + var lastHtmlContent: String = "" + var lastFontSize: CGFloat = 0 + var lastFontFamily: String = "" + var lastFontWeight: String = "" + var lastTextAlignment: String = "" + var lastLineSpacing: CGFloat = 0 + var lastMargin: CGFloat = 0 + var lastColorPreset: String = "" + + init(_ parent: HTMLView) { + self.parent = parent + } + + func startAutoScroll(webView: WKWebView) { + stopAutoScroll() + + scrollTimer = Timer.scheduledTimer(withTimeInterval: 0.016, repeats: true) { _ in // 60fps for smoother scrolling + let scrollAmount = self.parent.autoScrollSpeed * 0.5 // Reduced increment for smoother scrolling + + webView.evaluateJavaScript("window.scrollBy(0, \(scrollAmount));") { _, error in + if let error = error { + print("Scroll error: \(error)") + } + } + + webView.evaluateJavaScript("(window.pageYOffset + window.innerHeight) >= document.body.scrollHeight") { result, _ in + if let isAtBottom = result as? Bool, isAtBottom { + DispatchQueue.main.async { + self.parent.isAutoScrolling = false + } + } + } + } + } + + func stopAutoScroll() { + scrollTimer?.invalidate() + scrollTimer = nil + } + } + + func makeUIView(context: Context) -> WKWebView { + let webView = WKWebView() + webView.backgroundColor = .clear + webView.isOpaque = false + webView.scrollView.backgroundColor = .clear + + webView.scrollView.showsHorizontalScrollIndicator = false + webView.scrollView.bounces = false + webView.scrollView.alwaysBounceHorizontal = false + webView.scrollView.contentInsetAdjustmentBehavior = .never + + return webView + } + + func updateUIView(_ webView: WKWebView, context: Context) { + let coordinator = context.coordinator + + if isAutoScrolling { + coordinator.startAutoScroll(webView: webView) + } else { + coordinator.stopAutoScroll() + } + + guard !htmlContent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + return + } + + let contentChanged = coordinator.lastHtmlContent != htmlContent + let fontSizeChanged = coordinator.lastFontSize != fontSize + let fontFamilyChanged = coordinator.lastFontFamily != fontFamily + let fontWeightChanged = coordinator.lastFontWeight != fontWeight + let alignmentChanged = coordinator.lastTextAlignment != textAlignment + let lineSpacingChanged = coordinator.lastLineSpacing != lineSpacing + let marginChanged = coordinator.lastMargin != margin + let colorChanged = coordinator.lastColorPreset != colorPreset.name + + if contentChanged || fontSizeChanged || fontFamilyChanged || fontWeightChanged || + alignmentChanged || lineSpacingChanged || marginChanged || colorChanged { + let htmlTemplate = """ + + + + + + + + \(htmlContent) + + + """ + + Logger.shared.log("Loading HTML content into WebView", type: "Debug") + webView.loadHTMLString(htmlTemplate, baseURL: nil) + + coordinator.lastHtmlContent = htmlContent + coordinator.lastFontSize = fontSize + coordinator.lastFontFamily = fontFamily + coordinator.lastFontWeight = fontWeight + coordinator.lastTextAlignment = textAlignment + coordinator.lastLineSpacing = lineSpacing + coordinator.lastMargin = margin + coordinator.lastColorPreset = colorPreset.name + } + } +} diff --git a/Sora/Views/SearchView/SearchView.swift b/Sora/Views/SearchView/SearchView.swift index a3cbfca..2668c62 100644 --- a/Sora/Views/SearchView/SearchView.swift +++ b/Sora/Views/SearchView/SearchView.swift @@ -23,6 +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 @@ -141,6 +142,7 @@ struct SearchView: View { if !searchQuery.isEmpty { performSearch() } + tabBarController.showTabBar() } .onChange(of: selectedModuleId) { _ in if !searchQuery.isEmpty { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift index 5bc1242..94375e6 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift @@ -288,8 +288,8 @@ struct TranslatorsView: View { ), Translator( id: 3, - login: "cranci", - avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/28ac8bfaa250788579af747d8fb7f827_webp.png?raw=true", + login: "simplymox", + avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/9131174855bd67fc445206e888505a6a_webp.png?raw=true", language: "Italian" ), Translator( diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index cb7bdcb..47cd59b 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -224,6 +224,7 @@ struct SettingsViewGeneral: View { "Dutch", "French", "German", + "Italian", "Kazakh", "Norsk", "Russian", @@ -246,6 +247,7 @@ struct SettingsViewGeneral: View { case "Norsk": return "Norsk" case "Kazakh": return "Қазақша" case "Swedish": return "Svenska" + case "Italian": return "Italiano" default: return lang } }, diff --git a/Sora/Views/SettingsView/SettingsView.swift b/Sora/Views/SettingsView/SettingsView.swift index 9a7c6c4..c36c2d1 100644 --- a/Sora/Views/SettingsView/SettingsView.swift +++ b/Sora/Views/SettingsView/SettingsView.swift @@ -131,6 +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 { @@ -144,7 +145,6 @@ struct SettingsView: View { .padding(.horizontal, 20) .padding(.top, 16) - // Modules Section at the top VStack(alignment: .leading, spacing: 4) { Text("MODULES") .font(.footnote) @@ -330,6 +330,7 @@ struct SettingsView: View { } .onAppear { settings.updateAccentColor(currentColorScheme: colorScheme) + tabBarController.showTabBar() } } } @@ -427,6 +428,8 @@ class Settings: ObservableObject { languageCode = "kk" case "Swedish": languageCode = "sv" + case "Italian": + languageCode = "it" default: languageCode = "en" } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 6f8acfb..102fb60 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -19,6 +19,9 @@ 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 */; }; + 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 */; }; 0457C5972DE7712A000AFBD9 /* DeviceScaleModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457C5942DE7712A000AFBD9 /* DeviceScaleModifier.swift */; }; 0457C59D2DE78267000AFBD9 /* BookmarkGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457C59A2DE78267000AFBD9 /* BookmarkGridView.swift */; }; 0457C59E2DE78267000AFBD9 /* BookmarkLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457C59B2DE78267000AFBD9 /* BookmarkLink.swift */; }; @@ -34,6 +37,7 @@ 04AD07122E0360CD00EB74C1 /* CollectionPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD07112E0360CD00EB74C1 /* CollectionPickerView.swift */; }; 04AD07162E03704700EB74C1 /* BookmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AD07152E03704700EB74C1 /* BookmarkCell.swift */; }; 04CD76DB2DE20F2200733536 /* AllWatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04CD76DA2DE20F2200733536 /* AllWatching.swift */; }; + 04E00C9F2E09F5920056124A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 04E00C9D2E09F5920056124A /* Localizable.strings */; }; 04EAC39A2DF9E0DB00BBD483 /* SplashScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04EAC3992DF9E0DB00BBD483 /* SplashScreenView.swift */; }; 04F08EDC2DE10BF3006B29D9 /* ProgressiveBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F08EDB2DE10BEC006B29D9 /* ProgressiveBlurView.swift */; }; 04F08EDF2DE10C1D006B29D9 /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F08EDE2DE10C1A006B29D9 /* TabBar.swift */; }; @@ -130,6 +134,9 @@ 041069802E00C71000A157BB /* kk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kk; path = Localizable.strings; sourceTree = ""; }; 041261012E00D14F00D05B47 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = Localizable.strings; sourceTree = ""; }; 0452339E2E02149C002EA23C /* bos */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bos; path = Localizable.strings; sourceTree = ""; }; + 04536F702E04BA3B00A11248 /* JSController-Novel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-Novel.swift"; sourceTree = ""; }; + 04536F722E04BA5600A11248 /* ReaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderView.swift; sourceTree = ""; }; + 04536F752E04BA6900A11248 /* ChapterCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterCell.swift; sourceTree = ""; }; 0457C5942DE7712A000AFBD9 /* DeviceScaleModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceScaleModifier.swift; sourceTree = ""; }; 0457C5992DE78267000AFBD9 /* BookmarkGridItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkGridItemView.swift; sourceTree = ""; }; 0457C59A2DE78267000AFBD9 /* BookmarkGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkGridView.swift; sourceTree = ""; }; @@ -145,6 +152,7 @@ 04AD07112E0360CD00EB74C1 /* CollectionPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionPickerView.swift; sourceTree = ""; }; 04AD07152E03704700EB74C1 /* BookmarkCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkCell.swift; sourceTree = ""; }; 04CD76DA2DE20F2200733536 /* AllWatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllWatching.swift; sourceTree = ""; }; + 04E00C9E2E09F5920056124A /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = Localizable.strings; sourceTree = ""; }; 04EAC3992DF9E0DB00BBD483 /* SplashScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenView.swift; sourceTree = ""; }; 04F08EDB2DE10BEC006B29D9 /* ProgressiveBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressiveBlurView.swift; sourceTree = ""; }; 04F08EDE2DE10C1A006B29D9 /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = ""; }; @@ -317,6 +325,22 @@ path = bos.lproj; sourceTree = ""; }; + 04536F732E04BA5600A11248 /* ReaderView */ = { + isa = PBXGroup; + children = ( + 04536F722E04BA5600A11248 /* ReaderView.swift */, + ); + path = ReaderView; + sourceTree = ""; + }; + 04536F762E04BA6900A11248 /* ChapterCell */ = { + isa = PBXGroup; + children = ( + 04536F752E04BA6900A11248 /* ChapterCell.swift */, + ); + path = ChapterCell; + sourceTree = ""; + }; 0457C5962DE7712A000AFBD9 /* ViewModifiers */ = { isa = PBXGroup; children = ( @@ -380,6 +404,14 @@ path = nn.lproj; sourceTree = ""; }; + 04E00C9A2E09E96B0056124A /* it.lproj */ = { + isa = PBXGroup; + children = ( + 04E00C9D2E09F5920056124A /* Localizable.strings */, + ); + path = it.lproj; + sourceTree = ""; + }; 04F08EDA2DE10BE3006B29D9 /* ProgressiveBlurView */ = { isa = PBXGroup; children = ( @@ -514,6 +546,7 @@ 133D7C7B2D2BE2630075467E /* Views */ = { isa = PBXGroup; children = ( + 04536F732E04BA5600A11248 /* ReaderView */, 04EAC3992DF9E0DB00BBD483 /* SplashScreenView.swift */, 72443C7C2DC8036500A61321 /* DownloadView.swift */, 0402DA122DE7B5EC003BB42C /* SearchView */, @@ -527,6 +560,7 @@ 133D7C7F2D2BE2630075467E /* MediaInfoView */ = { isa = PBXGroup; children = ( + 04536F762E04BA6900A11248 /* ChapterCell */, 1E0435F02DFCB86800FF6808 /* CustomMatching */, 138AA1B52D2D66EC0021F9DF /* EpisodeCell */, 133D7C802D2BE2630075467E /* MediaInfoView.swift */, @@ -600,6 +634,7 @@ 133D7C8A2D2BE2640075467E /* JSLoader */ = { isa = PBXGroup; children = ( + 04536F702E04BA3B00A11248 /* JSController-Novel.swift */, 134A387B2DE4B5B90041B687 /* Downloads */, 133D7C8B2D2BE2640075467E /* JSController.swift */, 132AF1202D99951700A0140B /* JSController-Streams.swift */, @@ -634,6 +669,7 @@ 13530BE02E00028E0048B7DE /* Localization */ = { isa = PBXGroup; children = ( + 04E00C9A2E09E96B0056124A /* it.lproj */, 0452339C2E021491002EA23C /* bos.lproj */, 041261032E00D14F00D05B47 /* sv.lproj */, 041069822E00C71000A157BB /* kk.lproj */, @@ -854,7 +890,6 @@ nl, fr, ar, - cz, es, ru, nn, @@ -865,6 +900,7 @@ bos, bs, cs, + it, ); mainGroup = 133D7C612D2BE2500075467E; packageReferences = ( @@ -897,6 +933,7 @@ 0409FE8C2DFF2886000DB00C /* Localizable.strings in Resources */, 04A1B73C2DFF39EB0064688A /* Localizable.strings in Resources */, 133D7C752D2BE2520075467E /* Preview Assets.xcassets in Resources */, + 04E00C9F2E09F5920056124A /* Localizable.strings in Resources */, 041069832E00C71000A157BB /* Localizable.strings in Resources */, 133D7C722D2BE2520075467E /* Assets.xcassets in Resources */, 0488FA952DFDE724007575E1 /* Localizable.strings in Resources */, @@ -912,16 +949,19 @@ buildActionMask = 2147483647; files = ( 135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */, + 04536F742E04BA5600A11248 /* ReaderView.swift in Sources */, 131270172DC13A010093AA9C /* DownloadManager.swift in Sources */, 1EDA48F42DFAC374002A4EC3 /* TMDBMatchPopupView.swift in Sources */, 1327FBA72D758CEA00FC6689 /* Analytics.swift in Sources */, 13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */, 1359ED142D76F49900C13034 /* finTopView.swift in Sources */, 1399FAD62D3AB3DB00E97C31 /* Logger.swift in Sources */, + 04536F772E04BA6900A11248 /* ChapterCell.swift in Sources */, 13B7F4C12D58FFDD0045714A /* Shimmer.swift in Sources */, 139935662D468C450065CEFF /* ModuleManager.swift in Sources */, 133D7C902D2BE2640075467E /* SettingsView.swift in Sources */, 132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */, + 04536F712E04BA3B00A11248 /* JSController-Novel.swift in Sources */, 1EF5C3A92DB988E40032BF07 /* CommunityLib.swift in Sources */, 13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */, 04F08EDC2DE10BF3006B29D9 /* ProgressiveBlurView.swift in Sources */, @@ -1109,6 +1149,14 @@ name = Localizable.strings; sourceTree = ""; }; + 04E00C9D2E09F5920056124A /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 04E00C9E2E09F5920056124A /* it */, + ); + name = Localizable.strings; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */