mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
Type Shit (Future reference) (#203)
This commit is contained in:
parent
6b9ca1b6c8
commit
cdcac3fab7
14 changed files with 1038 additions and 264 deletions
406
Sora/Localization/bos.lproj/Localizable.strings
Normal file
406
Sora/Localization/bos.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
/* General */
|
||||
"About" = "O aplikaciji";
|
||||
"About Sora" = "O Sora aplikaciji";
|
||||
"Active" = "Aktivno";
|
||||
"Active Downloads" = "Aktivna preuzimanja";
|
||||
"Actively downloading media can be tracked from here." = "Mediji koji se trenutno preuzimaju mogu se pratiti odavde.";
|
||||
"Add Module" = "Dodaj modul";
|
||||
"Adjust the number of media items per row in portrait and landscape modes." = "Podesi broj medijskih stavki po redu u portretnom i pejzažnom načinu.";
|
||||
"Advanced" = "Napredno";
|
||||
"AKA Sulfur" = "Također poznat kao Sulfur";
|
||||
"All Bookmarks" = "Sve zabilješke";
|
||||
"All Watching" = "Sve što gledam";
|
||||
"Also known as Sulfur" = "Također poznat kao Sulfur";
|
||||
"AniList" = "AniList";
|
||||
"AniList ID" = "AniList ID";
|
||||
"AniList Match" = "AniList poklapanje";
|
||||
"AniList.co" = "AniList.co";
|
||||
"Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." = "Anonimni podaci se prikupljaju za poboljšanje aplikacije. Lične informacije se ne prikupljaju. Ovo se može onemogućiti u bilo kojem trenutku.";
|
||||
"App Info" = "Informacije o aplikaciji";
|
||||
"App Language" = "Jezik aplikacije";
|
||||
"App Storage" = "Spremište aplikacije";
|
||||
"Appearance" = "Izgled";
|
||||
|
||||
/* Alerts and Actions */
|
||||
"Are you sure you want to clear all cached data? This will help free up storage space." = "Jeste li sigurni da želite obrisati sve keširane podatke? Ovo će pomoći oslobađanju prostora za spremanje.";
|
||||
"Are you sure you want to delete '%@'?" = "Jeste li sigurni da želite obrisati '%@'?";
|
||||
"Are you sure you want to delete all %1$d episodes in '%2$@'?" = "Jeste li sigurni da želite obrisati sve %1$d epizode u '%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." = "Jeste li sigurni da želite obrisati sve preuzete sadržaje? Možete odabrati da obrišete samo biblioteku zadržavajući preuzete datoteke za buduću upotrebu.";
|
||||
"Are you sure you want to erase all app data? This action cannot be undone." = "Jeste li sigurni da želite obrisati sve podatke aplikacije? Ova radnja se ne može poništiti.";
|
||||
|
||||
/* Features */
|
||||
"Background Enabled" = "Pozadina omogućena";
|
||||
"Bookmark items for an easier access later." = "Zabilježite stavke za lakši pristup kasnije.";
|
||||
"Bookmarks" = "Zabilješke";
|
||||
"Bottom Padding" = "Donja margina";
|
||||
"Cancel" = "Otkaži";
|
||||
"Cellular Quality" = "Kvaliteta mobilne mreže";
|
||||
"Check out some community modules here!" = "Pogledajte neke module zajednice ovdje!";
|
||||
"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." = "Odaberite željenu rezoluciju videa za WiFi i mobilne konekcije. Veće rezolucije koriste više podataka ali pružaju bolji kvalitet. Ako točna kvaliteta nije dostupna, najbliža opcija će biti automatski odabrana.\n\nNapomena: Nisu svi video izvori i playeri podržavaju odabir kvalitete. Ova funkcija najbolje radi s HLS streamovima koristeći Sora player.";
|
||||
"Clear" = "Obriši";
|
||||
"Clear All Downloads" = "Obriši sva preuzimanja";
|
||||
"Clear Cache" = "Obriši keš";
|
||||
"Clear Library Only" = "Obriši samo biblioteku";
|
||||
"Clear Logs" = "Obriši logове";
|
||||
"Click the plus button to add a module!" = "Kliknite plus dugme da dodate modul!";
|
||||
"Continue Watching" = "Nastavi gledanje";
|
||||
"Continue Watching Episode %d" = "Nastavi gledanje epizode %d";
|
||||
"Contributors" = "Saradnici";
|
||||
"Copied to Clipboard" = "Kopirano u međuspremnik";
|
||||
"Copy to Clipboard" = "Kopiraj u međuspremnik";
|
||||
"Copy URL" = "Kopiraj URL";
|
||||
|
||||
/* Episodes */
|
||||
"%lld Episodes" = "%lld epizoda";
|
||||
"%lld of %lld" = "%lld od %lld";
|
||||
"%lld-%lld" = "%lld-%lld";
|
||||
"%lld%% seen" = "%lld%% pogledano";
|
||||
"Episode %lld" = "Epizoda %lld";
|
||||
"Episodes" = "Epizode";
|
||||
"Episodes might not be available yet or there could be an issue with the source." = "Epizode možda još nisu dostupne ili možda postoji problem s izvorom.";
|
||||
"Episodes Range" = "Raspon epizoda";
|
||||
|
||||
/* System */
|
||||
"cranci1" = "cranci1";
|
||||
"Dark" = "Tamno";
|
||||
"DATA & LOGS" = "PODACI I LOGOVI";
|
||||
"Debug" = "Otkrivanje grešaka";
|
||||
"Debugging and troubleshooting." = "Otkrivanje i rješavanje problema.";
|
||||
|
||||
/* Actions */
|
||||
"Delete" = "Obriši";
|
||||
"Delete All" = "Obriši sve";
|
||||
"Delete All Downloads" = "Obriši sva preuzimanja";
|
||||
"Delete All Episodes" = "Obriši sve epizode";
|
||||
"Delete Download" = "Obriši preuzimanje";
|
||||
"Delete Episode" = "Obriši epizodu";
|
||||
|
||||
/* Player */
|
||||
"Double Tap to Seek" = "Dvostruki dodir za traženje";
|
||||
"Double tapping the screen on it's sides will skip with the short tap setting." = "Dvostruki dodir ekrana sa strane će preskočiti s kratkim dodirom.";
|
||||
|
||||
/* Downloads */
|
||||
"Download" = "Preuzmi";
|
||||
"Download Episode" = "Preuzmi epizodu";
|
||||
"Download Summary" = "Sažetak preuzimanja";
|
||||
"Download This Episode" = "Preuzmi ovu epizodu";
|
||||
"Downloaded" = "Preuzeto";
|
||||
"Downloaded Shows" = "Preuzete serije";
|
||||
"Downloading" = "Preuzimanje";
|
||||
"Downloads" = "Preuzimanja";
|
||||
|
||||
/* Settings */
|
||||
"Enable Analytics" = "Omogući analitiku";
|
||||
"Enable Subtitles" = "Omogući titlove";
|
||||
|
||||
/* Data Management */
|
||||
"Erase" = "Obriši";
|
||||
"Erase all App Data" = "Obriši sve podatke aplikacije";
|
||||
"Erase App Data" = "Obriši podatke aplikacije";
|
||||
|
||||
/* Errors */
|
||||
"Error" = "Greška";
|
||||
"Error Fetching Results" = "Greška pri dohvaćanju rezultata";
|
||||
"Errors and critical issues." = "Greške i kritični problemi.";
|
||||
"Failed to load contributors" = "Neuspješno učitavanje saradnika";
|
||||
|
||||
/* Features */
|
||||
"Fetch Episode metadata" = "Dohvati metapodatke epizode";
|
||||
"Files Downloaded" = "Datoteke preuzete";
|
||||
"Font Size" = "Veličina fonta";
|
||||
|
||||
/* Interface */
|
||||
"Force Landscape" = "Forsiraj pejzažni način";
|
||||
"General" = "Općenito";
|
||||
"General events and activities." = "Općeniti događaji i aktivnosti.";
|
||||
"General Preferences" = "Općenite postavke";
|
||||
"Hide Splash Screen" = "Sakrij početni ekran";
|
||||
"HLS video downloading." = "HLS video preuzimanje.";
|
||||
"Hold Speed" = "Brzina držanja";
|
||||
|
||||
/* Info */
|
||||
"Info" = "Informacije";
|
||||
"INFOS" = "INFORMACIJE";
|
||||
"Installed Modules" = "Instalirani moduli";
|
||||
"Interface" = "Sučelje";
|
||||
|
||||
/* Social */
|
||||
"Join the Discord" = "Pridruži se Discordu";
|
||||
|
||||
/* Layout */
|
||||
"Landscape Columns" = "Stupci u pejzažnom načinu";
|
||||
"Language" = "Jezik";
|
||||
"LESS" = "MANJE";
|
||||
|
||||
/* Library */
|
||||
"Library" = "Biblioteka";
|
||||
"License (GPLv3.0)" = "Licenca (GPLv3.0)";
|
||||
"Light" = "Svijetlo";
|
||||
|
||||
/* Loading States */
|
||||
"Loading Episode %lld..." = "Učitavam epizodu %lld...";
|
||||
"Loading logs..." = "Učitavam logove...";
|
||||
"Loading module information..." = "Učitavam informacije o modulu...";
|
||||
"Loading Stream" = "Učitavam stream";
|
||||
|
||||
/* Logging */
|
||||
"Log Debug Info" = "Logiraj debug informacije";
|
||||
"Log Filters" = "Filtri logova";
|
||||
"Log In with AniList" = "Prijavite se s AniList";
|
||||
"Log In with Trakt" = "Prijavite se s Trakt";
|
||||
"Log Out from AniList" = "Odjavite se s AniList";
|
||||
"Log Out from Trakt" = "Odjavite se s Trakt";
|
||||
"Log Types" = "Tipovi logova";
|
||||
"Logged in as" = "Prijavljen kao";
|
||||
"Logged in as " = "Prijavljen kao ";
|
||||
|
||||
/* Logs and Settings */
|
||||
"Logs" = "Logovi";
|
||||
"Long press Skip" = "Dugi pritisak za preskačanje";
|
||||
"MAIN" = "GLAVNO";
|
||||
"Main Developer" = "Glavni developer";
|
||||
"MAIN SETTINGS" = "GLAVNE POSTAVKE";
|
||||
|
||||
/* Media Actions */
|
||||
"Mark All Previous Watched" = "Označi sve prethodne kao pogledane";
|
||||
"Mark as Watched" = "Označi kao pogledano";
|
||||
"Mark Episode as Watched" = "Označi epizodu kao pogledanu";
|
||||
"Mark Previous Episodes as Watched" = "Označi prethodne epizode kao pogledane";
|
||||
"Mark watched" = "Označi pogledano";
|
||||
"Match with AniList" = "Poklopi s AniList";
|
||||
"Match with TMDB" = "Poklopi s TMDB";
|
||||
"Matched ID: %lld" = "Poklopljeni ID: %lld";
|
||||
"Matched with: %@" = "Poklopljeno s: %@";
|
||||
"Max Concurrent Downloads" = "Maksimalno istovremenih preuzimanja";
|
||||
|
||||
/* Media Interface */
|
||||
"Media Grid Layout" = "Mrežni raspored medija";
|
||||
"Media Player" = "Medijski player";
|
||||
"Media View" = "Pregled medija";
|
||||
"Metadata Provider" = "Pružatelj metapodataka";
|
||||
"Metadata Providers Order" = "Redoslijed pružatelja metapodataka";
|
||||
"Module Removed" = "Modul uklonjen";
|
||||
"Modules" = "Moduli";
|
||||
|
||||
/* Headers */
|
||||
"MODULES" = "MODULI";
|
||||
"MORE" = "VIŠE";
|
||||
|
||||
/* Status Messages */
|
||||
"No Active Downloads" = "Nema aktivnih preuzimanja";
|
||||
"No AniList matches found" = "Nisu pronađena AniList poklapanja";
|
||||
"No Data Available" = "Nema dostupnih podataka";
|
||||
"No Downloads" = "Nema preuzimanja";
|
||||
"No episodes available" = "Nema dostupnih epizoda";
|
||||
"No Episodes Available" = "Nema dostupnih epizoda";
|
||||
"No items to continue watching." = "Nema stavki za nastavak gledanja.";
|
||||
"No matches found" = "Nisu pronađena poklapanja";
|
||||
"No Module Selected" = "Nije odabran modul";
|
||||
"No Modules" = "Nema modula";
|
||||
"No Results Found" = "Nisu pronađeni rezultati";
|
||||
"No Search Results Found" = "Nisu pronađeni rezultati pretrage";
|
||||
"Nothing to Continue Watching" = "Nema ništa za nastavak gledanja";
|
||||
|
||||
/* Notes and Messages */
|
||||
"Note that the modules will be replaced only if there is a different version string inside the JSON file." = "Imajte na umu da će moduli biti zamijenjeni samo ako postoji drugačiji string verzije u JSON datoteci.";
|
||||
|
||||
/* Actions */
|
||||
"OK" = "U redu";
|
||||
"Open Community Library" = "Otvori biblioteku zajednice";
|
||||
|
||||
/* External Services */
|
||||
"Open in AniList" = "Otvori u AniList";
|
||||
"Original Poster" = "Originalni poster";
|
||||
|
||||
/* Playback */
|
||||
"Paused" = "Pauzirano";
|
||||
"Play" = "Reproduciraj";
|
||||
"Player" = "Player";
|
||||
|
||||
/* System Messages */
|
||||
"Please restart the app to apply the language change." = "Molimo restartajte aplikaciju da primijenite promjenu jezika.";
|
||||
"Please select a module from settings" = "Molimo odaberite modul iz postavki";
|
||||
|
||||
/* Interface */
|
||||
"Portrait Columns" = "Stupci u portretnom načinu";
|
||||
"Progress bar Marker Color" = "Boja oznake progress bara";
|
||||
"Provider: %@" = "Pružatelj: %@";
|
||||
|
||||
/* Queue */
|
||||
"Queue" = "Red čekanja";
|
||||
"Queued" = "U redu čekanja";
|
||||
|
||||
/* Content */
|
||||
"Recently watched content will appear here." = "Nedavno pogledani sadržaj će se pojaviti ovdje.";
|
||||
|
||||
/* Settings */
|
||||
"Refresh Modules on Launch" = "Osvježi module pri pokretanju";
|
||||
"Refresh Storage Info" = "Osvježi informacije o spremištu";
|
||||
"Remember Playback speed" = "Zapamti brzinu reprodukcije";
|
||||
|
||||
/* Actions */
|
||||
"Remove" = "Ukloni";
|
||||
"Remove All Cache" = "Ukloni sav keš";
|
||||
|
||||
/* File Management */
|
||||
"Remove All Documents" = "Ukloni sve dokumente";
|
||||
"Remove Documents" = "Ukloni dokumente";
|
||||
"Remove Downloaded Media" = "Ukloni preuzete medije";
|
||||
"Remove Downloads" = "Ukloni preuzimanja";
|
||||
"Remove from Bookmarks" = "Ukloni iz zabilježaka";
|
||||
"Remove Item" = "Ukloni stavku";
|
||||
|
||||
/* Support */
|
||||
"Report an Issue" = "Prijavite problem";
|
||||
|
||||
/* Reset Options */
|
||||
"Reset" = "Resetiraj";
|
||||
"Reset AniList ID" = "Resetiraj AniList ID";
|
||||
"Reset Episode Progress" = "Resetiraj napredak epizode";
|
||||
"Reset progress" = "Resetiraj napredak";
|
||||
"Reset Progress" = "Resetiraj napredak";
|
||||
|
||||
/* System */
|
||||
"Restart Required" = "Potreban restart";
|
||||
"Running Sora %@ - cranci1" = "Pokrenut Sora %@ - cranci1";
|
||||
|
||||
/* Actions */
|
||||
"Save" = "Spremi";
|
||||
"Search" = "Pretraži";
|
||||
|
||||
/* Search */
|
||||
"Search downloads" = "Pretraži preuzimanja";
|
||||
"Search for something..." = "Pretraži nešto...";
|
||||
"Search..." = "Pretraži...";
|
||||
|
||||
/* Content */
|
||||
"Season %d" = "Sezona %d";
|
||||
"Season %lld" = "Sezona %lld";
|
||||
"Segments Color" = "Boja segmenata";
|
||||
|
||||
/* Modules */
|
||||
"Select Module" = "Odaberi modul";
|
||||
"Set Custom AniList ID" = "Postavi prilagođeni AniList ID";
|
||||
|
||||
/* Interface */
|
||||
"Settings" = "Postavke";
|
||||
"Shadow" = "Sjena";
|
||||
"Show More (%lld more characters)" = "Prikaži više (%lld više znakova)";
|
||||
"Show PiP Button" = "Prikaži PiP dugme";
|
||||
"Show Skip 85s Button" = "Prikaži dugme za preskakanje 85s";
|
||||
"Show Skip Intro / Outro Buttons" = "Prikaži dugmad za preskakanje intro / outro";
|
||||
"Shows" = "Serije";
|
||||
"Size (%@)" = "Veličina (%@)";
|
||||
"Skip Settings" = "Postavke preskakanja";
|
||||
|
||||
/* Player Features */
|
||||
"Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." = "Neke funkcije su ograničene na Sora i zadani player, kao što su ForceLandscape, holdSpeed i prilagođeni vremenski intervali preskakanja.";
|
||||
|
||||
/* App Info */
|
||||
"Sora" = "Sora";
|
||||
"Sora %@ by cranci1" = "Sora %@ od cranci1";
|
||||
"Sora and cranci1 are not affiliated with AniList or Trakt in any way.
|
||||
|
||||
Also note that progress updates may not be 100% accurate." = "Sora i cranci1 nisu povezani s AniList ili Trakt na bilo koji način.
|
||||
|
||||
Također imajte na umu da ažuriranja napretka možda neće biti 100% točna.";
|
||||
"Sora GitHub Repository" = "Sora GitHub repozitorij";
|
||||
"Sora/Sulfur will always remain free with no ADs!" = "Sora/Sulfur će uvijek ostati besplatan bez reklama!";
|
||||
|
||||
/* Interface */
|
||||
"Sort" = "Sortiraj";
|
||||
"Speed Settings" = "Postavke brzine";
|
||||
|
||||
/* Playback */
|
||||
"Start Watching" = "Počni gledati";
|
||||
"Start Watching Episode %d" = "Počni gledati epizodu %d";
|
||||
"Storage Used" = "Iskorišten prostor za spremanje";
|
||||
"Stream" = "Stream";
|
||||
"Streaming and video playback." = "Streaming i reprodukcija videa.";
|
||||
|
||||
/* Subtitles */
|
||||
"Subtitle Color" = "Boja titlova";
|
||||
"Subtitle Settings" = "Postavke titlova";
|
||||
|
||||
/* Sync */
|
||||
"Sync anime progress" = "Sinhroniziraj napredak animea";
|
||||
"Sync TV shows progress" = "Sinhroniziraj napredak TV serija";
|
||||
|
||||
/* System */
|
||||
"System" = "Sistem";
|
||||
|
||||
/* Instructions */
|
||||
"Tap a title to override the current match." = "Dodirnite naslov da pregazite trenutno poklapanje.";
|
||||
"Tap Skip" = "Dodirnite za preskakanje";
|
||||
"Tap to manage your modules" = "Dodirnite za upravljanje modulima";
|
||||
"Tap to select a module" = "Dodirnite da odaberete modul";
|
||||
|
||||
/* 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." = "Keš aplikacije pomaže aplikaciji da učita slike brže.
|
||||
|
||||
Brisanje mape Dokumenti će obrisati sve preuzete module.
|
||||
|
||||
Ne brišite podatke aplikacije osim ako ne razumijete posljedice — može dovesti do kvarova aplikacije.";
|
||||
"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." = "Raspon epizoda kontroliše koliko epizoda se pojavljuje na svakoj stranici. Epizode su grupisane u setove (kao 1–25, 26–50, i tako dalje), omogućavajući vam lakše navigiranje kroz njih.
|
||||
|
||||
Za metapodatke epizode, odnosi se na sličicu i naslov epizode, jer ponekad mogu sadržavati spojlere.";
|
||||
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." = "Modul je pružio samo jednu epizodu, ovo je najvjerojatniji film, pa smo odlučili napraviti zasebne ekrane za ove slučajeve.";
|
||||
|
||||
/* Interface */
|
||||
"Thumbnails Width" = "Širina sličica";
|
||||
"TMDB Match" = "TMDB poklapanje";
|
||||
"Trackers" = "Pratitelji";
|
||||
"Trakt" = "Trakt";
|
||||
"Trakt.tv" = "Trakt.tv";
|
||||
|
||||
/* Search */
|
||||
"Try different keywords" = "Pokušajte s drugačijim ključnim riječima";
|
||||
"Try different search terms" = "Pokušajte s drugačijim pojmovima za pretragu";
|
||||
|
||||
/* Player Controls */
|
||||
"Two Finger Hold for Pause" = "Držanje dva prsta za pauzu";
|
||||
"Unable to fetch matches. Please try again later." = "Nije moguće dohvatiti poklapanja. Molimo pokušajte ponovo kasnije.";
|
||||
"Use TMDB Poster Image" = "Koristi TMDB poster sliku";
|
||||
|
||||
/* Version */
|
||||
"v%@" = "v%@";
|
||||
"Video Player" = "Video player";
|
||||
|
||||
/* Video Settings */
|
||||
"Video Quality Preferences" = "Postavke kvalitete videa";
|
||||
"View All" = "Prikaži sve";
|
||||
"Watched" = "Pogledano";
|
||||
"Why am I not seeing any episodes?" = "Zašto ne vidim epizode?";
|
||||
"WiFi Quality" = "WiFi kvaliteta";
|
||||
|
||||
/* User Status */
|
||||
"You are not logged in" = "Niste prijavljeni";
|
||||
"You have no items saved." = "Nemate spremljenih stavki.";
|
||||
"Your downloaded episodes will appear here" = "Vaše preuzete epizode će se pojaviti ovdje";
|
||||
"Your recently watched content will appear here" = "Vaš nedavno pogledani sadržaj će se pojaviti ovdje";
|
||||
|
||||
/* Download Settings */
|
||||
"Download Settings" = "Postavke preuzimanja";
|
||||
"Max concurrent downloads controls how many episodes can download simultaneously. Higher values may use more bandwidth and device resources." = "Maksimalno istovremenih preuzimanja kontroliše koliko epizoda se može preuzeti istovremeno. Veće vrijednosti mogu koristiti više propusnog opsega i resursa uređaja.";
|
||||
"Quality" = "Kvaliteta";
|
||||
"Max Concurrent Downloads" = "Maksimalno istovremenih preuzimanja";
|
||||
"Allow Cellular Downloads" = "Dozvoli preuzimanja preko mobilne mreže";
|
||||
"Quality Information" = "Informacije o kvaliteti";
|
||||
|
||||
/* Storage */
|
||||
"Storage Management" = "Upravljanje spremištem";
|
||||
"Storage Used" = "Iskorišten prostor";
|
||||
"Library cleared successfully" = "Biblioteka uspješno obrisana";
|
||||
"All downloads deleted successfully" = "Sva preuzimanja uspješno obrisana";
|
||||
|
||||
/* New additions */
|
||||
"Recent searches" = "Nedavne pretrage";
|
||||
"me frfr" = "ja stvarno";
|
||||
"Data" = "Podaci";
|
||||
"Maximum Quality Available" = "Maksimalna dostupna kvaliteta";
|
||||
|
|
@ -202,6 +202,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
private var backgroundToken: Any?
|
||||
private var foregroundToken: Any?
|
||||
|
||||
private var timeBatteryContainer: UIView?
|
||||
private var timeLabel: UILabel?
|
||||
private var batteryLabel: UILabel?
|
||||
private var timeUpdateTimer: Timer?
|
||||
|
||||
init(module: ScrapingModule,
|
||||
urlString: String,
|
||||
fullUrl: String,
|
||||
|
|
@ -311,6 +316,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
updateSkipButtonsVisibility()
|
||||
setupHoldSpeedIndicator()
|
||||
setupPipIfSupported()
|
||||
setupTimeBatteryIndicator()
|
||||
|
||||
view.bringSubviewToFront(subtitleStackView)
|
||||
subtitleStackView.isHidden = !SubtitleSettingsManager.shared.settings.enabled
|
||||
|
|
@ -454,28 +460,23 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
|
||||
deinit {
|
||||
if let timeUpdateTimer = timeUpdateTimer {
|
||||
timeUpdateTimer.invalidate()
|
||||
}
|
||||
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
UIDevice.current.isBatteryMonitoringEnabled = false
|
||||
|
||||
inactivityTimer?.invalidate()
|
||||
inactivityTimer = nil
|
||||
updateTimer?.invalidate()
|
||||
updateTimer = nil
|
||||
lockButtonTimer?.invalidate()
|
||||
lockButtonTimer = nil
|
||||
dimButtonTimer?.invalidate()
|
||||
dimButtonTimer = nil
|
||||
|
||||
playerRateObserver?.invalidate()
|
||||
playerRateObserver = nil
|
||||
loadedTimeRangesObservation?.invalidate()
|
||||
loadedTimeRangesObservation = nil
|
||||
playerTimeControlStatusObserver?.invalidate()
|
||||
playerTimeControlStatusObserver = nil
|
||||
volumeObserver?.invalidate()
|
||||
volumeObserver = nil
|
||||
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
if let token = timeObserverToken {
|
||||
player?.removeTimeObserver(token)
|
||||
timeObserverToken = nil
|
||||
}
|
||||
|
||||
// Remove observer from player item if it exists
|
||||
|
|
@ -2828,6 +2829,95 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
@objc private func handleEscape() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func setupTimeBatteryIndicator() {
|
||||
// Create container
|
||||
let container = UIView()
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.backgroundColor = .clear
|
||||
container.layer.cornerRadius = 8
|
||||
controlsContainerView.addSubview(container)
|
||||
self.timeBatteryContainer = container
|
||||
|
||||
// Create time label
|
||||
let timeLabel = UILabel()
|
||||
timeLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
timeLabel.textColor = .white
|
||||
timeLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
timeLabel.textAlignment = .center
|
||||
container.addSubview(timeLabel)
|
||||
self.timeLabel = timeLabel
|
||||
|
||||
// Create separator
|
||||
let separator = UIView()
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
separator.backgroundColor = .white.withAlphaComponent(0.5)
|
||||
container.addSubview(separator)
|
||||
|
||||
// Create battery label
|
||||
let batteryLabel = UILabel()
|
||||
batteryLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
batteryLabel.textColor = .white
|
||||
batteryLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
batteryLabel.textAlignment = .center
|
||||
container.addSubview(batteryLabel)
|
||||
self.batteryLabel = batteryLabel
|
||||
|
||||
// Setup constraints
|
||||
NSLayoutConstraint.activate([
|
||||
container.centerXAnchor.constraint(equalTo: controlsContainerView.centerXAnchor),
|
||||
container.topAnchor.constraint(equalTo: sliderHostingController?.view.bottomAnchor ?? controlsContainerView.bottomAnchor, constant: 2),
|
||||
container.heightAnchor.constraint(equalToConstant: 20),
|
||||
|
||||
timeLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12),
|
||||
timeLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor),
|
||||
timeLabel.widthAnchor.constraint(equalToConstant: 50),
|
||||
|
||||
separator.leadingAnchor.constraint(equalTo: timeLabel.trailingAnchor, constant: 8),
|
||||
separator.centerYAnchor.constraint(equalTo: container.centerYAnchor),
|
||||
separator.widthAnchor.constraint(equalToConstant: 1),
|
||||
separator.heightAnchor.constraint(equalToConstant: 12),
|
||||
|
||||
batteryLabel.leadingAnchor.constraint(equalTo: separator.trailingAnchor, constant: 8),
|
||||
batteryLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -12),
|
||||
batteryLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor),
|
||||
batteryLabel.widthAnchor.constraint(equalToConstant: 50)
|
||||
])
|
||||
|
||||
// Start time updates
|
||||
updateTime()
|
||||
timeUpdateTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||||
self?.updateTime()
|
||||
}
|
||||
|
||||
// Setup battery monitoring
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
updateBatteryLevel()
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(batteryLevelDidChange),
|
||||
name: UIDevice.batteryLevelDidChangeNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
private func updateTime() {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm"
|
||||
timeLabel?.text = formatter.string(from: Date())
|
||||
}
|
||||
|
||||
@objc private func batteryLevelDidChange() {
|
||||
updateBatteryLevel()
|
||||
}
|
||||
|
||||
private func updateBatteryLevel() {
|
||||
let batteryLevel = UIDevice.current.batteryLevel
|
||||
if batteryLevel >= 0 {
|
||||
let percentage = Int(batteryLevel * 100)
|
||||
batteryLabel?.text = "\(percentage)%"
|
||||
} else {
|
||||
batteryLabel?.text = "N/A"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GradientOverlayButton: UIButton {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ struct TabBar: View {
|
|||
.stroke(
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: Color.accentColor.opacity(0.25), location: 0),
|
||||
.init(color: Color.accentColor.opacity(gradientOpacity), location: 0),
|
||||
.init(color: Color.accentColor.opacity(0), location: 1)
|
||||
]),
|
||||
startPoint: .top,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/*
|
||||
struct DeviceScaleModifier: ViewModifier {
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
|
|
@ -29,13 +29,13 @@ struct DeviceScaleModifier: ViewModifier {
|
|||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
struct DeviceScaleModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content // does nothing for now
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
|
||||
extension View {
|
||||
|
|
|
|||
|
|
@ -692,122 +692,148 @@ struct EnhancedActiveDownloadCard: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 16) {
|
||||
Group {
|
||||
if let imageURL = download.imageURL {
|
||||
LazyImage(url: imageURL) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.tertiary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(.tertiary)
|
||||
.overlay(
|
||||
Image(systemName: "photo")
|
||||
.foregroundStyle(.secondary)
|
||||
)
|
||||
}
|
||||
}
|
||||
.frame(width: 64, height: 64)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(download.title ?? download.originalURL.lastPathComponent)
|
||||
.font(.headline)
|
||||
.fontWeight(.medium)
|
||||
.lineLimit(2)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
VStack(spacing: 6) {
|
||||
HStack {
|
||||
if download.queueStatus == .queued {
|
||||
Text(NSLocalizedString("Queued", comment: ""))
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.orange)
|
||||
} else {
|
||||
Text("\(Int(currentProgress * 100))%")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Circle()
|
||||
.fill(statusColor)
|
||||
.frame(width: 6, height: 6)
|
||||
|
||||
Text(statusText)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if download.queueStatus == .queued {
|
||||
ProgressView()
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .orange))
|
||||
.scaleEffect(y: 0.8)
|
||||
HStack(spacing: 14) {
|
||||
// Thumbnail
|
||||
Group {
|
||||
if let imageURL = download.imageURL {
|
||||
LazyImage(url: imageURL) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.clipped()
|
||||
} else {
|
||||
ProgressView(value: currentProgress)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: currentProgress >= 1.0 ? .green : .accentColor))
|
||||
.scaleEffect(y: 0.8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
if download.queueStatus == .queued {
|
||||
Button(action: cancelDownload) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
} else {
|
||||
Button(action: toggleDownload) {
|
||||
Image(systemName: taskState == .running ? "pause.circle.fill" : "play.circle.fill")
|
||||
.font(.title3)
|
||||
.foregroundStyle(taskState == .running ? .orange : .accentColor)
|
||||
}
|
||||
|
||||
Button(action: cancelDownload) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.red)
|
||||
Rectangle().fill(Color(white: 0.2))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Rectangle().fill(Color(white: 0.2))
|
||||
.overlay(
|
||||
Image(systemName: "photo")
|
||||
.foregroundColor(.gray)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.frame(width: 56, height: 56)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
|
||||
if download != download {
|
||||
Divider()
|
||||
.padding(.horizontal, 16)
|
||||
// Center VStack
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(download.title ?? download.originalURL.lastPathComponent)
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
|
||||
HStack {
|
||||
Text("\(Int(currentProgress * 100))%")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
HStack(spacing: 6) {
|
||||
Circle()
|
||||
.fill(Color.green)
|
||||
.frame(width: 10, height: 10)
|
||||
Text(statusText)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(Color(white: 0.7))
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
ProgressView(value: currentProgress)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: Color(white: 0.7)))
|
||||
.frame(height: 4)
|
||||
.cornerRadius(2)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
// Right VStack (buttons)
|
||||
VStack(spacing: 12) {
|
||||
Button(action: cancelDownload) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.red)
|
||||
.opacity(0.85)
|
||||
.frame(width: 40, height: 40)
|
||||
.overlay(
|
||||
Circle().stroke(
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: Color.white.opacity(0.25), location: 0),
|
||||
.init(color: Color.white.opacity(0), location: 1)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
),
|
||||
lineWidth: 2
|
||||
)
|
||||
)
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 20, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
Button(action: toggleDownload) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.yellow)
|
||||
.opacity(0.85)
|
||||
.frame(width: 40, height: 40)
|
||||
.overlay(
|
||||
Circle().stroke(
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: Color.white.opacity(0.25), location: 0),
|
||||
.init(color: Color.white.opacity(0), location: 1)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
),
|
||||
lineWidth: 2
|
||||
)
|
||||
)
|
||||
Image(systemName: taskState == .running ? "pause.fill" : "play.fill")
|
||||
.font(.system(size: 20, weight: .bold))
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(Color(UIColor.systemBackground))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.stroke(
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: Color.accentColor.opacity(0.25), location: 0),
|
||||
.init(color: Color.accentColor.opacity(0), location: 1)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
),
|
||||
lineWidth: 0.5
|
||||
)
|
||||
)
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadProgressChanged"))) { _ in
|
||||
updateProgress()
|
||||
}
|
||||
}
|
||||
|
||||
private var statusColor: Color {
|
||||
if download.queueStatus == .queued {
|
||||
return .orange
|
||||
} else if taskState == .running {
|
||||
return .green
|
||||
} else {
|
||||
return .orange
|
||||
}
|
||||
}
|
||||
|
||||
private var statusText: String {
|
||||
if download.queueStatus == .queued {
|
||||
return NSLocalizedString("Queued", comment: "")
|
||||
|
|
@ -901,9 +927,12 @@ struct EnhancedDownloadGroupCard: View {
|
|||
.foregroundStyle(.primary)
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Label("\(group.assetCount) \(group.assetCount == 1 ? "Episode" : "Episodes")", systemImage: "play.rectangle")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "play.rectangle")
|
||||
Text("\(group.assetCount) \(group.assetCount == 1 ? "Episode" : "Episodes")")
|
||||
}
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Label(formatFileSize(group.totalFileSize), systemImage: "internaldrive")
|
||||
.font(.subheadline)
|
||||
|
|
@ -1162,7 +1191,21 @@ struct EnhancedShowEpisodesView: View {
|
|||
.padding(.horizontal, 20)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 25)
|
||||
.fill(Color.red.opacity(0.1))
|
||||
.fill(Color.red.opacity(0.18))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 25)
|
||||
.stroke(
|
||||
LinearGradient(
|
||||
gradient: Gradient(stops: [
|
||||
.init(color: Color.red.opacity(0.25), location: 0),
|
||||
.init(color: Color.red.opacity(0), location: 1)
|
||||
]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
),
|
||||
lineWidth: 1.5
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ struct AllBookmarks: View {
|
|||
@State private var searchText: String = ""
|
||||
@State private var isSearchActive: Bool = false
|
||||
@State private var sortOption: SortOption = .title
|
||||
@State private var isSelecting: Bool = false
|
||||
@State private var selectedBookmarks: Set<LibraryItem.ID> = []
|
||||
|
||||
enum SortOption: String, CaseIterable {
|
||||
case title = "Title"
|
||||
|
|
@ -121,6 +123,35 @@ struct AllBookmarks: View {
|
|||
)
|
||||
.circularGradientOutlineTwo()
|
||||
}
|
||||
Button(action: {
|
||||
if isSelecting {
|
||||
// If trash icon tapped
|
||||
if !selectedBookmarks.isEmpty {
|
||||
for id in selectedBookmarks {
|
||||
if let item = libraryManager.bookmarks.first(where: { $0.id == id }) {
|
||||
libraryManager.removeBookmark(item: item)
|
||||
}
|
||||
}
|
||||
selectedBookmarks.removeAll()
|
||||
}
|
||||
isSelecting = false
|
||||
} else {
|
||||
isSelecting = true
|
||||
}
|
||||
}) {
|
||||
Image(systemName: isSelecting ? "trash" : "checkmark.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(isSelecting ? .red : .accentColor)
|
||||
.padding(10)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.shadow(color: .accentColor.opacity(0.2), radius: 2)
|
||||
)
|
||||
.circularGradientOutlineTwo()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
|
@ -176,7 +207,9 @@ struct AllBookmarks: View {
|
|||
}
|
||||
BookmarkGridView(
|
||||
bookmarks: filteredAndSortedBookmarks,
|
||||
moduleManager: moduleManager
|
||||
moduleManager: moduleManager,
|
||||
isSelecting: isSelecting,
|
||||
selectedBookmarks: $selectedBookmarks
|
||||
)
|
||||
.withGridPadding()
|
||||
Spacer()
|
||||
|
|
@ -199,6 +232,7 @@ struct AllBookmarks: View {
|
|||
struct BookmarkCell: View {
|
||||
let bookmark: LibraryItem
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
|
||||
var body: some View {
|
||||
if let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) {
|
||||
|
|
@ -265,6 +299,13 @@ struct BookmarkCell: View {
|
|||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.padding(4)
|
||||
.contextMenu {
|
||||
Button(role: .destructive, action: {
|
||||
libraryManager.removeBookmark(item: bookmark)
|
||||
}) {
|
||||
Label("Remove from Bookmarks", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ struct AllWatchingView: View {
|
|||
@State private var sortOption: SortOption = .dateAdded
|
||||
@State private var searchText: String = ""
|
||||
@State private var isSearchActive: Bool = false
|
||||
@State private var isSelecting: Bool = false
|
||||
@State private var selectedItems: Set<ContinueWatchingItem.ID> = []
|
||||
|
||||
enum SortOption: String, CaseIterable {
|
||||
case dateAdded = "Recently Added"
|
||||
|
|
@ -133,6 +135,36 @@ struct AllWatchingView: View {
|
|||
)
|
||||
.circularGradientOutline()
|
||||
}
|
||||
Button(action: {
|
||||
if isSelecting {
|
||||
// If trash icon tapped
|
||||
if !selectedItems.isEmpty {
|
||||
for id in selectedItems {
|
||||
if let item = continueWatchingItems.first(where: { $0.id == id }) {
|
||||
ContinueWatchingManager.shared.remove(item: item)
|
||||
}
|
||||
}
|
||||
selectedItems.removeAll()
|
||||
loadContinueWatchingItems()
|
||||
}
|
||||
isSelecting = false
|
||||
} else {
|
||||
isSelecting = true
|
||||
}
|
||||
}) {
|
||||
Image(systemName: isSelecting ? "trash" : "checkmark.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(isSelecting ? .red : .accentColor)
|
||||
.padding(10)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.shadow(color: .accentColor.opacity(0.2), radius: 2)
|
||||
)
|
||||
.circularGradientOutline()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
|
@ -198,7 +230,9 @@ struct AllWatchingView: View {
|
|||
},
|
||||
removeItem: {
|
||||
removeItem(item: item)
|
||||
}
|
||||
},
|
||||
isSelecting: isSelecting,
|
||||
selectedItems: $selectedItems
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -245,140 +279,86 @@ struct FullWidthContinueWatchingCell: View {
|
|||
let item: ContinueWatchingItem
|
||||
var markAsWatched: () -> Void
|
||||
var removeItem: () -> Void
|
||||
var isSelecting: Bool
|
||||
var selectedItems: Binding<Set<ContinueWatchingItem.ID>>
|
||||
|
||||
@State private var currentProgress: Double = 0.0
|
||||
|
||||
var isSelected: Bool {
|
||||
selectedItems.wrappedValue.contains(item.id)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
if UserDefaults.standard.string(forKey: "externalPlayer") == "Default" {
|
||||
let videoPlayerViewController = VideoPlayerViewController(module: item.module)
|
||||
videoPlayerViewController.streamUrl = item.streamUrl
|
||||
videoPlayerViewController.fullUrl = item.fullUrl
|
||||
videoPlayerViewController.episodeImageUrl = item.imageUrl
|
||||
videoPlayerViewController.episodeNumber = item.episodeNumber
|
||||
videoPlayerViewController.mediaTitle = item.mediaTitle
|
||||
videoPlayerViewController.subtitles = item.subtitles ?? ""
|
||||
videoPlayerViewController.aniListID = item.aniListID ?? 0
|
||||
videoPlayerViewController.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
|
||||
}
|
||||
} else {
|
||||
let customMediaPlayer = CustomMediaPlayerViewController(
|
||||
module: item.module,
|
||||
urlString: item.streamUrl,
|
||||
fullUrl: item.fullUrl,
|
||||
title: item.mediaTitle,
|
||||
episodeNumber: item.episodeNumber,
|
||||
onWatchNext: { },
|
||||
subtitlesURL: item.subtitles,
|
||||
aniListID: item.aniListID ?? 0,
|
||||
totalEpisodes: item.totalEpisodes,
|
||||
episodeImageUrl: item.imageUrl,
|
||||
headers: item.headers ?? nil
|
||||
)
|
||||
customMediaPlayer.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
LazyImage(url: URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
Group {
|
||||
if isSelecting {
|
||||
Button(action: {
|
||||
if isSelected {
|
||||
selectedItems.wrappedValue.remove(item.id)
|
||||
} else {
|
||||
selectedItems.wrappedValue.insert(item.id)
|
||||
}
|
||||
}) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
cellContent
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: geometry.size.width, height: 157.03)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(height: 157.03)
|
||||
.shimmering()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.black)
|
||||
.background(Color.white.clipShape(Circle()).opacity(0.8))
|
||||
.offset(x: -8, y: 8)
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
ProgressiveBlurView()
|
||||
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Spacer()
|
||||
Text(item.mediaTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
|
||||
HStack {
|
||||
Text("Episode \(item.episodeNumber)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(Int(item.progress * 100))% seen")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
.black.opacity(0.7),
|
||||
.black.opacity(0.0)
|
||||
],
|
||||
startPoint: .bottom,
|
||||
endPoint: .top
|
||||
)
|
||||
.clipped()
|
||||
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
|
||||
.shadow(color: .black.opacity(0.3), radius: 3, x: 0, y: 1)
|
||||
)
|
||||
},
|
||||
alignment: .bottom
|
||||
)
|
||||
.overlay(
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.black.opacity(0.5))
|
||||
.frame(width: 28, height: 28)
|
||||
.overlay(
|
||||
LazyImage(url: URL(string: item.module.metadata.iconUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Button(action: {
|
||||
if UserDefaults.standard.string(forKey: "externalPlayer") == "Default" {
|
||||
let videoPlayerViewController = VideoPlayerViewController(module: item.module)
|
||||
videoPlayerViewController.streamUrl = item.streamUrl
|
||||
videoPlayerViewController.fullUrl = item.fullUrl
|
||||
videoPlayerViewController.episodeImageUrl = item.imageUrl
|
||||
videoPlayerViewController.episodeNumber = item.episodeNumber
|
||||
videoPlayerViewController.mediaTitle = item.mediaTitle
|
||||
videoPlayerViewController.subtitles = item.subtitles ?? ""
|
||||
videoPlayerViewController.aniListID = item.aniListID ?? 0
|
||||
videoPlayerViewController.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
|
||||
}
|
||||
.padding(8),
|
||||
alignment: .topLeading
|
||||
)
|
||||
} else {
|
||||
let customMediaPlayer = CustomMediaPlayerViewController(
|
||||
module: item.module,
|
||||
urlString: item.streamUrl,
|
||||
fullUrl: item.fullUrl,
|
||||
title: item.mediaTitle,
|
||||
episodeNumber: item.episodeNumber,
|
||||
onWatchNext: { },
|
||||
subtitlesURL: item.subtitles,
|
||||
aniListID: item.aniListID ?? 0,
|
||||
totalEpisodes: item.totalEpisodes,
|
||||
episodeImageUrl: item.imageUrl,
|
||||
headers: item.headers ?? nil
|
||||
)
|
||||
customMediaPlayer.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
cellContent
|
||||
}
|
||||
}
|
||||
.frame(height: 157.03)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: { markAsWatched() }) {
|
||||
Label("Mark as Watched", systemImage: "checkmark.circle")
|
||||
}
|
||||
Button(role: .destructive, action: { removeItem() }) {
|
||||
Label("Remove Item", systemImage: "trash")
|
||||
Label("Remove from Continue Watching", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
|
|
@ -389,6 +369,94 @@ struct FullWidthContinueWatchingCell: View {
|
|||
}
|
||||
}
|
||||
|
||||
private var cellContent: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
LazyImage(url: URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: geometry.size.width, height: 157.03)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(height: 157.03)
|
||||
.shimmering()
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
ProgressiveBlurView()
|
||||
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Spacer()
|
||||
Text(item.mediaTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
|
||||
HStack {
|
||||
Text("Episode \(item.episodeNumber)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(Int(item.progress * 100))% seen")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
.black.opacity(0.7),
|
||||
.black.opacity(0.0)
|
||||
],
|
||||
startPoint: .bottom,
|
||||
endPoint: .top
|
||||
)
|
||||
.clipped()
|
||||
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
|
||||
.shadow(color: .black.opacity(0.3), radius: 3, x: 0, y: 1)
|
||||
)
|
||||
},
|
||||
alignment: .bottom
|
||||
)
|
||||
.overlay(
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.black.opacity(0.5))
|
||||
.frame(width: 28, height: 28)
|
||||
.overlay(
|
||||
LazyImage(url: URL(string: item.module.metadata.iconUrl)) { state in
|
||||
if let uiImage = state.imageContainer?.image {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 32, height: 32)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(8),
|
||||
alignment: .topLeading
|
||||
)
|
||||
}
|
||||
}
|
||||
.frame(height: 157.03)
|
||||
}
|
||||
|
||||
private func updateProgress() {
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(item.fullUrl)")
|
||||
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(item.fullUrl)")
|
||||
|
|
|
|||
|
|
@ -10,14 +10,42 @@ import SwiftUI
|
|||
struct BookmarkGridItemView: View {
|
||||
let bookmark: LibraryItem
|
||||
let moduleManager: ModuleManager
|
||||
let isSelecting: Bool
|
||||
@Binding var selectedBookmarks: Set<LibraryItem.ID>
|
||||
|
||||
var isSelected: Bool {
|
||||
selectedBookmarks.contains(bookmark.id)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) {
|
||||
BookmarkLink(
|
||||
bookmark: bookmark,
|
||||
module: module
|
||||
)
|
||||
if isSelecting {
|
||||
Button(action: {
|
||||
if isSelected {
|
||||
selectedBookmarks.remove(bookmark.id)
|
||||
} else {
|
||||
selectedBookmarks.insert(bookmark.id)
|
||||
}
|
||||
}) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
BookmarkCell(bookmark: bookmark)
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.accentColor)
|
||||
.background(Color.white.clipShape(Circle()).opacity(0.8))
|
||||
.offset(x: -8, y: 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BookmarkLink(
|
||||
bookmark: bookmark,
|
||||
module: module
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import SwiftUI
|
|||
struct BookmarkGridView: View {
|
||||
let bookmarks: [LibraryItem]
|
||||
let moduleManager: ModuleManager
|
||||
let isSelecting: Bool
|
||||
@Binding var selectedBookmarks: Set<LibraryItem.ID>
|
||||
|
||||
private let columns = [
|
||||
GridItem(.adaptive(minimum: 150))
|
||||
|
|
@ -21,7 +23,9 @@ struct BookmarkGridView: View {
|
|||
ForEach(bookmarks) { bookmark in
|
||||
BookmarkGridItemView(
|
||||
bookmark: bookmark,
|
||||
moduleManager: moduleManager
|
||||
moduleManager: moduleManager,
|
||||
isSelecting: isSelecting,
|
||||
selectedBookmarks: $selectedBookmarks
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ struct BookmarksDetailView: View {
|
|||
@State private var sortOption: SortOption = .dateAdded
|
||||
@State private var searchText: String = ""
|
||||
@State private var isSearchActive: Bool = false
|
||||
@State private var isSelecting: Bool = false
|
||||
@State private var selectedBookmarks: Set<LibraryItem.ID> = []
|
||||
|
||||
enum SortOption: String, CaseIterable {
|
||||
case dateAdded = "Date Added"
|
||||
|
|
@ -108,6 +110,35 @@ struct BookmarksDetailView: View {
|
|||
)
|
||||
.circularGradientOutline()
|
||||
}
|
||||
Button(action: {
|
||||
if isSelecting {
|
||||
// If trash icon tapped
|
||||
if !selectedBookmarks.isEmpty {
|
||||
for id in selectedBookmarks {
|
||||
if let item = bookmarks.first(where: { $0.id == id }) {
|
||||
libraryManager.removeBookmark(item: item)
|
||||
}
|
||||
}
|
||||
selectedBookmarks.removeAll()
|
||||
}
|
||||
isSelecting = false
|
||||
} else {
|
||||
isSelecting = true
|
||||
}
|
||||
}) {
|
||||
Image(systemName: isSelecting ? "trash" : "checkmark.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(isSelecting ? .red : .accentColor)
|
||||
.padding(10)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.shadow(color: .accentColor.opacity(0.2), radius: 2)
|
||||
)
|
||||
.circularGradientOutline()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
|
@ -163,7 +194,9 @@ struct BookmarksDetailView: View {
|
|||
}
|
||||
BookmarksDetailGrid(
|
||||
bookmarks: filteredAndSortedBookmarks,
|
||||
moduleManager: moduleManager
|
||||
moduleManager: moduleManager,
|
||||
isSelecting: isSelecting,
|
||||
selectedBookmarks: $selectedBookmarks
|
||||
)
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
|
|
@ -212,12 +245,14 @@ private struct SortMenu: View {
|
|||
private struct BookmarksDetailGrid: View {
|
||||
let bookmarks: [LibraryItem]
|
||||
let moduleManager: ModuleManager
|
||||
let isSelecting: Bool
|
||||
@Binding var selectedBookmarks: Set<LibraryItem.ID>
|
||||
private let columns = [GridItem(.adaptive(minimum: 150))]
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
LazyVGrid(columns: columns, spacing: 16) {
|
||||
ForEach(bookmarks) { bookmark in
|
||||
BookmarksDetailGridCell(bookmark: bookmark, moduleManager: moduleManager)
|
||||
BookmarksDetailGridCell(bookmark: bookmark, moduleManager: moduleManager, isSelecting: isSelecting, selectedBookmarks: $selectedBookmarks)
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
|
|
@ -230,15 +265,44 @@ private struct BookmarksDetailGrid: View {
|
|||
private struct BookmarksDetailGridCell: View {
|
||||
let bookmark: LibraryItem
|
||||
let moduleManager: ModuleManager
|
||||
let isSelecting: Bool
|
||||
@Binding var selectedBookmarks: Set<LibraryItem.ID>
|
||||
|
||||
var isSelected: Bool {
|
||||
selectedBookmarks.contains(bookmark.id)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) {
|
||||
NavigationLink(destination: MediaInfoView(
|
||||
title: bookmark.title,
|
||||
imageUrl: bookmark.imageUrl,
|
||||
href: bookmark.href,
|
||||
module: module
|
||||
)) {
|
||||
BookmarkCell(bookmark: bookmark)
|
||||
if isSelecting {
|
||||
Button(action: {
|
||||
if isSelected {
|
||||
selectedBookmarks.remove(bookmark.id)
|
||||
} else {
|
||||
selectedBookmarks.insert(bookmark.id)
|
||||
}
|
||||
}) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
BookmarkCell(bookmark: bookmark)
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.black)
|
||||
.background(Color.white.clipShape(Circle()).opacity(0.8))
|
||||
.offset(x: -8, y: 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NavigationLink(destination: MediaInfoView(
|
||||
title: bookmark.title,
|
||||
imageUrl: bookmark.imageUrl,
|
||||
href: bookmark.href,
|
||||
module: module
|
||||
)) {
|
||||
BookmarkCell(bookmark: bookmark)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -276,48 +276,54 @@ struct TranslatorsView: View {
|
|||
private let translators: [Translator] = [
|
||||
Translator(
|
||||
id: 1,
|
||||
login: "paul",
|
||||
login: "paul",
|
||||
avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/54b3198dfb900837a9b8a7ec0b791add_webp.png?raw=true",
|
||||
language: "Dutch"
|
||||
),
|
||||
Translator(
|
||||
id: 2,
|
||||
login: "Utopia",
|
||||
avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/2b3b696895d5b7e708e3e5efaad62411_webp.png?raw=true",
|
||||
language: "Bosnian"
|
||||
),
|
||||
Translator(
|
||||
id: 3,
|
||||
login: "cranci",
|
||||
avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/28ac8bfaa250788579af747d8fb7f827_webp.png?raw=true",
|
||||
language: "Italian"
|
||||
),
|
||||
Translator(
|
||||
id: 3,
|
||||
id: 4,
|
||||
login: "ibro",
|
||||
avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/05cd4f3508f99ba0a4ae2d0985c2f68c_webp.png?raw=true",
|
||||
language: "Russian, Czech, Kazakh"
|
||||
),
|
||||
Translator(
|
||||
id: 4,
|
||||
id: 5,
|
||||
login: "Ciro",
|
||||
avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/4accfc2fcfa436165febe4cad18de978_webp.png?raw=true",
|
||||
language: "Arabic, French"
|
||||
),
|
||||
Translator(
|
||||
id: 5,
|
||||
id: 6,
|
||||
login: "storm",
|
||||
avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/a6cc97f87d356523820461fd761fc3e1_webp.png?raw=true",
|
||||
language: "Norwegian, Swedish"
|
||||
),
|
||||
Translator(
|
||||
id: 6,
|
||||
id: 7,
|
||||
login: "VastSector0",
|
||||
avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/bd8bccb82e0393b767bb705c4dc07113_webp.png?raw=true",
|
||||
language: "Spanish"
|
||||
),
|
||||
Translator(
|
||||
id: 7,
|
||||
id: 8,
|
||||
login: "Seiike",
|
||||
avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/ca512dc4ce1f0997fd44503dce0a0fc8_webp.png?raw=true",
|
||||
language: "Slovak"
|
||||
),
|
||||
Translator(
|
||||
id: 8,
|
||||
id: 9,
|
||||
login: "Cufiy",
|
||||
avatarUrl: "https://github.com/50n50/assets/blob/main/pfps/y1wwm0ed_png.png?raw=true",
|
||||
language: "German"
|
||||
|
|
|
|||
|
|
@ -205,6 +205,7 @@ struct SettingsViewGeneral: View {
|
|||
options: [
|
||||
"English",
|
||||
"Arabic",
|
||||
"Bosnian",
|
||||
"Czech",
|
||||
"Dutch",
|
||||
"French",
|
||||
|
|
@ -223,6 +224,7 @@ struct SettingsViewGeneral: View {
|
|||
case "French": return "Français"
|
||||
case "German": return "Deutsch"
|
||||
case "Arabic": return "العربية"
|
||||
case "Bosnian": return "Bosanski"
|
||||
case "Czech": return "Čeština"
|
||||
case "Slovak": return "Slovenčina"
|
||||
case "Spanish": return "Español"
|
||||
|
|
|
|||
|
|
@ -411,6 +411,8 @@ class Settings: ObservableObject {
|
|||
languageCode = "de"
|
||||
case "Arabic":
|
||||
languageCode = "ar"
|
||||
case "Bosnian":
|
||||
languageCode = "bos"
|
||||
case "Czech":
|
||||
languageCode = "cs"
|
||||
case "Slovak":
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@
|
|||
0410697B2E00ABE900A157BB /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
041069802E00C71000A157BB /* kk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kk; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
041261012E00D14F00D05B47 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
0452339E2E02149C002EA23C /* bos */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bos; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
0457C5942DE7712A000AFBD9 /* DeviceScaleModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceScaleModifier.swift; sourceTree = "<group>"; };
|
||||
0457C5992DE78267000AFBD9 /* BookmarkGridItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkGridItemView.swift; sourceTree = "<group>"; };
|
||||
0457C59A2DE78267000AFBD9 /* BookmarkGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkGridView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -302,6 +303,14 @@
|
|||
path = sv.lproj;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0452339C2E021491002EA23C /* bos.lproj */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0452339D2E02149C002EA23C /* Localizable.strings */,
|
||||
);
|
||||
path = bos.lproj;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0457C5962DE7712A000AFBD9 /* ViewModifiers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -616,6 +625,7 @@
|
|||
13530BE02E00028E0048B7DE /* Localization */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0452339C2E021491002EA23C /* bos.lproj */,
|
||||
041261032E00D14F00D05B47 /* sv.lproj */,
|
||||
041069822E00C71000A157BB /* kk.lproj */,
|
||||
0410697A2E00ABE900A157BB /* de.lproj */,
|
||||
|
|
@ -843,6 +853,8 @@
|
|||
sk,
|
||||
kk,
|
||||
sv,
|
||||
bos,
|
||||
bs,
|
||||
);
|
||||
mainGroup = 133D7C612D2BE2500075467E;
|
||||
packageReferences = (
|
||||
|
|
@ -1036,6 +1048,14 @@
|
|||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0452339D2E02149C002EA23C /* Localizable.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
0452339E2E02149C002EA23C /* bos */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0488FA902DFDE724007575E1 /* Localizable.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
|
|
|
|||
Loading…
Reference in a new issue