From cdcac3fab73fa4862c5391627350f346a8d7dbdd Mon Sep 17 00:00:00 2001 From: 50/50 <80717571+50n50@users.noreply.github.com> Date: Tue, 17 Jun 2025 23:49:27 +0200 Subject: [PATCH 1/9] Type Shit (Future reference) (#203) --- .../bos.lproj/Localizable.strings | 406 ++++++++++++++++++ .../CustomPlayer/CustomPlayer.swift | 114 ++++- Sora/Utils/TabBar/TabBar.swift | 2 +- .../ViewModifiers/DeviceScaleModifier.swift | 6 +- Sora/Views/DownloadView.swift | 257 ++++++----- Sora/Views/LibraryView/AllBookmarks.swift | 43 +- Sora/Views/LibraryView/AllWatching.swift | 306 ++++++++----- .../BookmarkGridItemView.swift | 36 +- .../BookmarkComponents/BookmarkGridView.swift | 6 +- .../BookmarksDetailView.swift | 82 +++- .../SettingsSubViews/SettingsViewAbout.swift | 20 +- .../SettingsViewGeneral.swift | 2 + Sora/Views/SettingsView/SettingsView.swift | 2 + Sulfur.xcodeproj/project.pbxproj | 20 + 14 files changed, 1038 insertions(+), 264 deletions(-) create mode 100644 Sora/Localization/bos.lproj/Localizable.strings diff --git a/Sora/Localization/bos.lproj/Localizable.strings b/Sora/Localization/bos.lproj/Localizable.strings new file mode 100644 index 0000000..7840131 --- /dev/null +++ b/Sora/Localization/bos.lproj/Localizable.strings @@ -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"; diff --git a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift index b8dadba..789b378 100644 --- a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift +++ b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift @@ -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 { diff --git a/Sora/Utils/TabBar/TabBar.swift b/Sora/Utils/TabBar/TabBar.swift index ef3758c..a646e58 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(0.25), location: 0), + .init(color: Color.accentColor.opacity(gradientOpacity), location: 0), .init(color: Color.accentColor.opacity(0), location: 1) ]), startPoint: .top, diff --git a/Sora/Utils/ViewModifiers/DeviceScaleModifier.swift b/Sora/Utils/ViewModifiers/DeviceScaleModifier.swift index 1667d6f..27abe3d 100644 --- a/Sora/Utils/ViewModifiers/DeviceScaleModifier.swift +++ b/Sora/Utils/ViewModifiers/DeviceScaleModifier.swift @@ -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 { diff --git a/Sora/Views/DownloadView.swift b/Sora/Views/DownloadView.swift index b93978e..c997f4f 100644 --- a/Sora/Views/DownloadView.swift +++ b/Sora/Views/DownloadView.swift @@ -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 + ) + ) ) } } diff --git a/Sora/Views/LibraryView/AllBookmarks.swift b/Sora/Views/LibraryView/AllBookmarks.swift index ab054f1..fa9aeda 100644 --- a/Sora/Views/LibraryView/AllBookmarks.swift +++ b/Sora/Views/LibraryView/AllBookmarks.swift @@ -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 = [] 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") + } + } } } } diff --git a/Sora/Views/LibraryView/AllWatching.swift b/Sora/Views/LibraryView/AllWatching.swift index 0da6cd7..14682c7 100644 --- a/Sora/Views/LibraryView/AllWatching.swift +++ b/Sora/Views/LibraryView/AllWatching.swift @@ -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 = [] 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> @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)") diff --git a/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridItemView.swift b/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridItemView.swift index 35a4747..d6085f1 100644 --- a/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridItemView.swift +++ b/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridItemView.swift @@ -10,14 +10,42 @@ import SwiftUI struct BookmarkGridItemView: View { let bookmark: LibraryItem let moduleManager: ModuleManager + let isSelecting: Bool + @Binding var selectedBookmarks: Set + + 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 + ) + } } } } diff --git a/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridView.swift b/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridView.swift index e3321cf..3d792a7 100644 --- a/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridView.swift +++ b/Sora/Views/LibraryView/BookmarkComponents/BookmarkGridView.swift @@ -10,6 +10,8 @@ import SwiftUI struct BookmarkGridView: View { let bookmarks: [LibraryItem] let moduleManager: ModuleManager + let isSelecting: Bool + @Binding var selectedBookmarks: Set 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 ) } } diff --git a/Sora/Views/LibraryView/BookmarkComponents/BookmarksDetailView.swift b/Sora/Views/LibraryView/BookmarkComponents/BookmarksDetailView.swift index aa24d71..07985f2 100644 --- a/Sora/Views/LibraryView/BookmarkComponents/BookmarksDetailView.swift +++ b/Sora/Views/LibraryView/BookmarkComponents/BookmarksDetailView.swift @@ -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 = [] 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 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 + + 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) + } } } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift index c15bf31..5bc1242 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAbout.swift @@ -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" diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index caca95b..f525648 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -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" diff --git a/Sora/Views/SettingsView/SettingsView.swift b/Sora/Views/SettingsView/SettingsView.swift index e9753ba..9a7c6c4 100644 --- a/Sora/Views/SettingsView/SettingsView.swift +++ b/Sora/Views/SettingsView/SettingsView.swift @@ -411,6 +411,8 @@ class Settings: ObservableObject { languageCode = "de" case "Arabic": languageCode = "ar" + case "Bosnian": + languageCode = "bos" case "Czech": languageCode = "cs" case "Slovak": diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index e7dabb1..0b4230f 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -126,6 +126,7 @@ 0410697B2E00ABE900A157BB /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = Localizable.strings; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -302,6 +303,14 @@ path = sv.lproj; sourceTree = ""; }; + 0452339C2E021491002EA23C /* bos.lproj */ = { + isa = PBXGroup; + children = ( + 0452339D2E02149C002EA23C /* Localizable.strings */, + ); + path = bos.lproj; + sourceTree = ""; + }; 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 = ""; }; + 0452339D2E02149C002EA23C /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 0452339E2E02149C002EA23C /* bos */, + ); + name = Localizable.strings; + sourceTree = ""; + }; 0488FA902DFDE724007575E1 /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( From e3fda69df3940c8a8024848ea32a00f6d9e05d9a Mon Sep 17 00:00:00 2001 From: cranci <100066266+cranci1@users.noreply.github.com> Date: Tue, 17 Jun 2025 23:55:41 +0200 Subject: [PATCH 2/9] Update AllWatching.swift --- Sora/Views/LibraryView/AllWatching.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sora/Views/LibraryView/AllWatching.swift b/Sora/Views/LibraryView/AllWatching.swift index 14682c7..558facd 100644 --- a/Sora/Views/LibraryView/AllWatching.swift +++ b/Sora/Views/LibraryView/AllWatching.swift @@ -275,6 +275,7 @@ struct AllWatchingView: View { } } +@MainActor struct FullWidthContinueWatchingCell: View { let item: ContinueWatchingItem var markAsWatched: () -> Void From baa42e643c84df9507313e548d03256b841e02fe Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:52:14 +0200 Subject: [PATCH 3/9] ok just some fixes i guess --- Sora/MediaUtils/CustomPlayer/CustomPlayer.swift | 1 + Sora/MediaUtils/NormalPlayer/VideoPlayer.swift | 3 +-- Sora/Utils/Extensions/URLSession.swift | 2 +- Sora/Utils/Modules/ModuleAdditionSettingsView.swift | 2 +- Sora/Views/LibraryView/AllBookmarks.swift | 1 - Sora/Views/LibraryView/AllWatching.swift | 9 +++++++-- Sora/Views/MediaInfoView/MediaInfoView.swift | 3 ++- .../SettingsSubViews/SettingsViewPlayer.swift | 4 ++-- 8 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift index 789b378..131d0cc 100644 --- a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift +++ b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift @@ -1592,6 +1592,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele self.updateSkipButtonsVisibility() + Logger.shared.log(fullUrl, type: "Stream") UserDefaults.standard.set(self.currentTimeVal, forKey: "lastPlayedTime_\(self.fullUrl)") UserDefaults.standard.set(self.duration, forKey: "totalTime_\(self.fullUrl)") diff --git a/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift index 4810e4a..141d38c 100644 --- a/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift +++ b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift @@ -90,6 +90,7 @@ class VideoPlayerViewController: UIViewController { } addPeriodicTimeObserver(fullURL: fullUrl) + Logger.shared.log(fullUrl, type: "Stream") let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)") if lastPlayedTime > 0 { let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1) @@ -208,7 +209,6 @@ class VideoPlayerViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - // Only start normal playback if not launched from SharePlay if !isLaunchedFromSharePlay { player?.play() setInitialPlayerRate() @@ -217,7 +217,6 @@ class VideoPlayerViewController: UIViewController { await checkForFaceTimeAndPromptSharePlay() } } else { - // For SharePlay launches, the playback will be coordinated setInitialPlayerRate() } } diff --git a/Sora/Utils/Extensions/URLSession.swift b/Sora/Utils/Extensions/URLSession.swift index d1cacb7..63beafd 100644 --- a/Sora/Utils/Extensions/URLSession.swift +++ b/Sora/Utils/Extensions/URLSession.swift @@ -5,8 +5,8 @@ // Created by Francesco on 05/01/25. // -import Foundation import Network +import Foundation class FetchDelegate: NSObject, URLSessionTaskDelegate { private let allowRedirects: Bool diff --git a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift index 2398ca2..0c75830 100644 --- a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift +++ b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift @@ -87,7 +87,7 @@ struct ModuleAdditionSettingsView: View { .fill(Color(.systemGray5)) } } - .frame(width: 32, height: 32) + .frame(width: 40, height: 40) .clipShape(Circle()) .shadow( color: colorScheme == .dark diff --git a/Sora/Views/LibraryView/AllBookmarks.swift b/Sora/Views/LibraryView/AllBookmarks.swift index fa9aeda..cb4fde4 100644 --- a/Sora/Views/LibraryView/AllBookmarks.swift +++ b/Sora/Views/LibraryView/AllBookmarks.swift @@ -125,7 +125,6 @@ struct AllBookmarks: View { } 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 }) { diff --git a/Sora/Views/LibraryView/AllWatching.swift b/Sora/Views/LibraryView/AllWatching.swift index 558facd..c60a711 100644 --- a/Sora/Views/LibraryView/AllWatching.swift +++ b/Sora/Views/LibraryView/AllWatching.swift @@ -266,12 +266,17 @@ struct AllWatchingView: View { UserDefaults.standard.set(99999999.0, forKey: key) UserDefaults.standard.set(99999999.0, forKey: totalKey) ContinueWatchingManager.shared.remove(item: item) - loadContinueWatchingItems() + + DispatchQueue.main.async { + loadContinueWatchingItems() + } } private func removeItem(item: ContinueWatchingItem) { ContinueWatchingManager.shared.remove(item: item) - loadContinueWatchingItems() + DispatchQueue.main.async { + loadContinueWatchingItems() + } } } diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 6546621..a4d053d 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -1508,7 +1508,7 @@ struct MediaInfoView: View { alert.addAction(UIAlertAction(title: title, style: .default) { _ in guard self.activeFetchID == fetchID else { return } - self.playStream(url: streamUrl, fullURL: href, subtitles: subtitles, headers: headers, fetchID: fetchID) + self.playStream(url: streamUrl, fullURL: fullURL, subtitles: subtitles, headers: headers, fetchID: fetchID) }) streamIndex += 1 @@ -1528,6 +1528,7 @@ struct MediaInfoView: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { guard self.activeFetchID == fetchID else { return } + Logger.shared.log(fullURL, type: "Stream") let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Sora" var scheme: String? diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index 468c17f..6aba3dc 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -378,7 +378,7 @@ struct SubtitleSettingsSection: View { icon: "captions.bubble", title: NSLocalizedString("Enable Subtitles", comment: ""), isOn: $subtitlesEnabled, - showDivider: false + showDivider: true ) .onChange(of: subtitlesEnabled) { newValue in SubtitleSettingsManager.shared.update { settings in @@ -454,4 +454,4 @@ struct SubtitleSettingsSection: View { } } } -} \ No newline at end of file +} From 7957601814da3c76fedc42aea0743f59fd771d65 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:02:15 +0200 Subject: [PATCH 4/9] =?UTF-8?q?seiike=20cant=20even=20add=20a=20shadow=20?= =?UTF-8?q?=F0=9F=98=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sora/MediaUtils/CustomPlayer/CustomPlayer.swift | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift index 131d0cc..865f8d0 100644 --- a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift +++ b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift @@ -1357,6 +1357,12 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele airplayButton.setContentCompressionResistancePriority(.required, for: .horizontal) controlsContainerView.addSubview(airplayButton) + airplayButton.layer.shadowColor = UIColor.black.cgColor + airplayButton.layer.shadowOffset = CGSize(width: 0, height: 2) + airplayButton.layer.shadowOpacity = 0.6 + airplayButton.layer.shadowRadius = 4 + airplayButton.layer.masksToBounds = false + guard AVPictureInPictureController.isPictureInPictureSupported() else { return } @@ -1368,7 +1374,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele pipController = AVPictureInPictureController(playerLayer: pipPlayerLayer) pipController?.delegate = self - let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .medium) let Image = UIImage(systemName: "pip", withConfiguration: config) pipButton = UIButton(type: .system) @@ -1416,11 +1421,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele menuButton.isHidden = true } - dismissButton.layer.shadowColor = UIColor.black.cgColor - dismissButton.layer.shadowOffset = CGSize(width: 0, height: 2) - dismissButton.layer.shadowOpacity = 0.6 - dismissButton.layer.shadowRadius = 4 - dismissButton.layer.masksToBounds = false + menuButton.layer.shadowColor = UIColor.black.cgColor + menuButton.layer.shadowOffset = CGSize(width: 0, height: 2) + menuButton.layer.shadowOpacity = 0.6 + menuButton.layer.shadowRadius = 4 + menuButton.layer.masksToBounds = false controlsContainerView.addSubview(menuButton) menuButton.translatesAutoresizingMaskIntoConstraints = false From 4704f27e30e31cff7ffa63ee8b31e75d3b2adf65 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:03:31 +0200 Subject: [PATCH 5/9] removed debug things --- Sora/MediaUtils/CustomPlayer/CustomPlayer.swift | 1 - Sora/MediaUtils/NormalPlayer/VideoPlayer.swift | 1 - Sora/Views/MediaInfoView/MediaInfoView.swift | 1 - 3 files changed, 3 deletions(-) diff --git a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift index 865f8d0..f65614c 100644 --- a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift +++ b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift @@ -1597,7 +1597,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele self.updateSkipButtonsVisibility() - Logger.shared.log(fullUrl, type: "Stream") UserDefaults.standard.set(self.currentTimeVal, forKey: "lastPlayedTime_\(self.fullUrl)") UserDefaults.standard.set(self.duration, forKey: "totalTime_\(self.fullUrl)") diff --git a/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift index 141d38c..0bbb821 100644 --- a/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift +++ b/Sora/MediaUtils/NormalPlayer/VideoPlayer.swift @@ -90,7 +90,6 @@ class VideoPlayerViewController: UIViewController { } addPeriodicTimeObserver(fullURL: fullUrl) - Logger.shared.log(fullUrl, type: "Stream") let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)") if lastPlayedTime > 0 { let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1) diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index a4d053d..e665c3f 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -1528,7 +1528,6 @@ struct MediaInfoView: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { guard self.activeFetchID == fetchID else { return } - Logger.shared.log(fullURL, type: "Stream") let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Sora" var scheme: String? From b686c452cd0ea61542f40cb88b8db2e55945cf90 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:06:26 +0200 Subject: [PATCH 6/9] =?UTF-8?q?paul=20+=20seiike=20issues=20=F0=9F=98=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SettingsView/SettingsSubViews/SettingsViewGeneral.swift | 5 ----- Sora/Views/SplashScreenView.swift | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index f525648..68b2f96 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -275,9 +275,6 @@ struct SettingsViewGeneral: View { .padding(.horizontal, 16) .padding(.vertical, 12) - Divider() - .padding(.horizontal, 16) - List { ForEach(Array(metadataProvidersOrder.enumerated()), id: \.element) { index, provider in HStack { @@ -358,8 +355,6 @@ struct SettingsViewGeneral: View { } } .padding(.vertical, 20) - .scrollViewBottomPadding() - .navigationTitle("General") } .navigationTitle(NSLocalizedString("General", comment: "")) .scrollViewBottomPadding() diff --git a/Sora/Views/SplashScreenView.swift b/Sora/Views/SplashScreenView.swift index af37f9f..1e1c9e0 100644 --- a/Sora/Views/SplashScreenView.swift +++ b/Sora/Views/SplashScreenView.swift @@ -30,7 +30,7 @@ struct SplashScreenView: View { isAnimating = true } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { withAnimation(.easeOut(duration: 0.5)) { showMainApp = true } From 9b610a3b168c1450df90cd7acac70113a7eb7705 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:10:19 +0200 Subject: [PATCH 7/9] fixed module addition view --- .../Modules/ModuleAdditionSettingsView.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift index 0c75830..7f4acb7 100644 --- a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift +++ b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift @@ -18,6 +18,13 @@ struct ModuleAdditionSettingsView: View { @State private var errorMessage: String? var moduleUrl: String + private var moduleAlreadyExists: Bool { + if let metadata = moduleMetadata { + return moduleManager.modules.contains(where: { $0.metadata.sourceName == metadata.sourceName }) + } + return false + } + var body: some View { ZStack { LinearGradient( @@ -194,10 +201,11 @@ struct ModuleAdditionSettingsView: View { Button(action: addModule) { HStack { Image(systemName: "plus.circle.fill") - Text("Add Module") + .foregroundColor(colorScheme == .dark ? .black : .white) + Text(moduleAlreadyExists ? "Module already added" : "Add Module") } .font(.headline) - .foregroundColor(Color.accentColor) + .foregroundColor(colorScheme == .dark ? .black : .white) .frame(maxWidth: .infinity) .padding(.vertical, 14) .background( @@ -219,8 +227,8 @@ struct ModuleAdditionSettingsView: View { ) .padding(.horizontal, 20) } - .disabled(isLoading || moduleMetadata == nil) - .opacity(isLoading ? 0.6 : 1) + .disabled(isLoading || moduleMetadata == nil || moduleAlreadyExists) + .opacity(isLoading || moduleAlreadyExists ? 0.6 : 1) Button(action: { presentationMode.wrappedValue.dismiss() }) { Text("Cancel") From c0e94cbe5a696ee8bdc13aff16631cc14b3bbeb1 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:26:31 +0200 Subject: [PATCH 8/9] fixed battery indicator not hiding --- .../CustomPlayer/CustomPlayer.swift | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift index f65614c..21d86bb 100644 --- a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift +++ b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift @@ -183,7 +183,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele watchNextButton, volumeSliderHostingView, pipButton, - airplayButton + airplayButton, + timeBatteryContainer ].compactMap { $0 } private var originalHiddenStates: [UIView: Bool] = [:] @@ -237,13 +238,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } let asset: AVURLAsset - - // Check if this is a local file URL if url.scheme == "file" { - // For local files, don't add HTTP headers Logger.shared.log("Loading local file: \(url.absoluteString)", type: "Debug") - // Check if file exists if FileManager.default.fileExists(atPath: url.path) { Logger.shared.log("Local file exists at path: \(url.path)", type: "Debug") } else { @@ -252,7 +249,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele asset = AVURLAsset(url: url) } else { - // For remote URLs, add HTTP headers Logger.shared.log("Loading remote URL: \(url.absoluteString)", type: "Debug") var request = URLRequest(url: url) if let mydict = headers, !mydict.isEmpty { @@ -271,8 +267,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele let playerItem = AVPlayerItem(asset: asset) self.player = AVPlayer(playerItem: playerItem) - - // Add error observation playerItem.addObserver(self, forKeyPath: "status", options: [.new], context: &playerItemKVOContext) Logger.shared.log("Created AVPlayerItem with status: \(playerItem.status.rawValue)", type: "Debug") @@ -1396,7 +1390,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele pipButton.widthAnchor.constraint(equalToConstant: 44), pipButton.heightAnchor.constraint(equalToConstant: 44), airplayButton.centerYAnchor.constraint(equalTo: pipButton.centerYAnchor), - airplayButton.trailingAnchor.constraint(equalTo: pipButton.leadingAnchor, constant: -6), + airplayButton.trailingAnchor.constraint(equalTo: pipButton.leadingAnchor, constant: -4), airplayButton.widthAnchor.constraint(equalToConstant: 44), airplayButton.heightAnchor.constraint(equalToConstant: 44) ]) @@ -2836,7 +2830,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } private func setupTimeBatteryIndicator() { - // Create container let container = UIView() container.translatesAutoresizingMaskIntoConstraints = false container.backgroundColor = .clear @@ -2844,7 +2837,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele controlsContainerView.addSubview(container) self.timeBatteryContainer = container - // Create time label let timeLabel = UILabel() timeLabel.translatesAutoresizingMaskIntoConstraints = false timeLabel.textColor = .white @@ -2853,13 +2845,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele 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 @@ -2868,7 +2858,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele 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), @@ -2889,19 +2878,14 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele 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) + NotificationCenter.default.addObserver(self, selector: #selector(batteryLevelDidChange), name: UIDevice.batteryLevelDidChangeNotification, object: nil) } private func updateTime() { From 14ebc82fc68800e89edb3e4054a9e60556f0310b Mon Sep 17 00:00:00 2001 From: 50/50 <80717571+50n50@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:32:22 +0200 Subject: [PATCH 9/9] Quick Czech fix (#205) --- .../Localization/{cz.lproj => cs.lproj}/Localizable.strings | 0 Sulfur.xcodeproj/project.pbxproj | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename Sora/Localization/{cz.lproj => cs.lproj}/Localizable.strings (100%) diff --git a/Sora/Localization/cz.lproj/Localizable.strings b/Sora/Localization/cs.lproj/Localizable.strings similarity index 100% rename from Sora/Localization/cz.lproj/Localizable.strings rename to Sora/Localization/cs.lproj/Localizable.strings diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 0b4230f..7a8529b 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -247,12 +247,12 @@ path = SearchView; sourceTree = ""; }; - 0409FE832DFF0870000DB00C /* cz.lproj */ = { + 0409FE832DFF0870000DB00C /* cs.lproj */ = { isa = PBXGroup; children = ( 0409FE822DFF0870000DB00C /* Localizable.strings */, ); - path = cz.lproj; + path = cs.lproj; sourceTree = ""; }; 0409FE862DFF0870000DB00C /* es.lproj */ = { @@ -632,7 +632,7 @@ 0410697D2E00ABE900A157BB /* sk.lproj */, 04A1B73B2DFF39EB0064688A /* nn.lproj */, 0409FE8B2DFF2886000DB00C /* ru.lproj */, - 0409FE832DFF0870000DB00C /* cz.lproj */, + 0409FE832DFF0870000DB00C /* cs.lproj */, 0409FE862DFF0870000DB00C /* es.lproj */, 0488FA9B2DFDF385007575E1 /* ar.lproj */, 0488FA972DFDF334007575E1 /* fr.lproj */,