Update AllWatching.swift (#204)

This commit is contained in:
cranci 2025-06-17 23:58:28 +02:00 committed by GitHub
parent 082a6b2b83
commit 1bce1e1c5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1039 additions and 264 deletions

View 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 125, 2650, 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 125, 2650, 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";

View file

@ -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 {

View file

@ -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,

View file

@ -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 {

View file

@ -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
)
)
)
}
}

View file

@ -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")
}
}
}
}
}

View file

@ -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
)
}
}
@ -241,144 +275,91 @@ struct AllWatchingView: View {
}
}
@MainActor
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 +370,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)")

View file

@ -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
)
}
}
}
}

View file

@ -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
)
}
}

View file

@ -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)
}
}
}
}

View file

@ -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"

View file

@ -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"

View file

@ -411,6 +411,8 @@ class Settings: ObservableObject {
languageCode = "de"
case "Arabic":
languageCode = "ar"
case "Bosnian":
languageCode = "bos"
case "Czech":
languageCode = "cs"
case "Slovak":

View file

@ -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 = (