merge it fg (#184)
Some checks are pending
Build and Release / Build IPA (push) Waiting to run
Build and Release / Build Mac Catalyst (push) Waiting to run

This commit is contained in:
50/50 2025-06-12 21:54:58 +02:00 committed by GitHub
parent 35c4f35f5e
commit 375fe1806b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 3689 additions and 584 deletions

File diff suppressed because it is too large Load diff

View file

@ -57,16 +57,16 @@ struct DownloadView: View {
} }
.animation(.easeInOut(duration: 0.2), value: selectedTab) .animation(.easeInOut(duration: 0.2), value: selectedTab)
.navigationBarHidden(true) .navigationBarHidden(true)
.alert("Delete Download", isPresented: $showDeleteAlert) { .alert(NSLocalizedString("Delete Download", comment: ""), isPresented: $showDeleteAlert) {
Button("Delete", role: .destructive) { Button(NSLocalizedString("Delete", comment: ""), role: .destructive) {
if let asset = assetToDelete { if let asset = assetToDelete {
jsController.deleteAsset(asset) jsController.deleteAsset(asset)
} }
} }
Button("Cancel", role: .cancel) {} Button(NSLocalizedString("Cancel", comment: ""), role: .cancel) {}
} message: { } message: {
if let asset = assetToDelete { if let asset = assetToDelete {
Text("Are you sure you want to delete '\(asset.episodeDisplayName)'?") Text(String(format: NSLocalizedString("Are you sure you want to delete '%@'?", comment: ""), asset.episodeDisplayName))
} }
} }
} }
@ -83,7 +83,7 @@ struct DownloadView: View {
VStack(spacing: 20) { VStack(spacing: 20) {
if !jsController.downloadQueue.isEmpty { if !jsController.downloadQueue.isEmpty {
DownloadSectionView( DownloadSectionView(
title: "Queue", title: NSLocalizedString("Queue", comment: ""),
icon: "clock.fill", icon: "clock.fill",
downloads: jsController.downloadQueue downloads: jsController.downloadQueue
) )
@ -91,7 +91,7 @@ struct DownloadView: View {
if !jsController.activeDownloads.isEmpty { if !jsController.activeDownloads.isEmpty {
DownloadSectionView( DownloadSectionView(
title: "Active Downloads", title: NSLocalizedString("Active Downloads", comment: ""),
icon: "arrow.down.circle.fill", icon: "arrow.down.circle.fill",
downloads: jsController.activeDownloads downloads: jsController.activeDownloads
) )
@ -140,12 +140,12 @@ struct DownloadView: View {
.foregroundStyle(.tertiary) .foregroundStyle(.tertiary)
VStack(spacing: 8) { VStack(spacing: 8) {
Text("No Active Downloads") Text(NSLocalizedString("No Active Downloads", comment: ""))
.font(.title2) .font(.title2)
.fontWeight(.medium) .fontWeight(.medium)
.foregroundStyle(.primary) .foregroundStyle(.primary)
Text("Your active downloads will appear here.") Text(NSLocalizedString("Actively downloading media can be tracked from here.", comment: ""))
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@ -162,12 +162,12 @@ struct DownloadView: View {
.foregroundStyle(.tertiary) .foregroundStyle(.tertiary)
VStack(spacing: 8) { VStack(spacing: 8) {
Text("No Downloads") Text(NSLocalizedString("No Downloads", comment: ""))
.font(.title2) .font(.title2)
.fontWeight(.medium) .fontWeight(.medium)
.foregroundStyle(.primary) .foregroundStyle(.primary)
Text("Your downloaded content will appear here") Text(NSLocalizedString("Your downloaded episodes will appear here", comment: ""))
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@ -274,7 +274,7 @@ struct CustomDownloadHeader: View {
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack { HStack {
Text("Downloads") Text(NSLocalizedString("Downloads", comment: ""))
.font(.largeTitle) .font(.largeTitle)
.fontWeight(.bold) .fontWeight(.bold)
.foregroundStyle(.primary) .foregroundStyle(.primary)
@ -348,7 +348,7 @@ struct CustomDownloadHeader: View {
.frame(width: 18, height: 18) .frame(width: 18, height: 18)
.foregroundColor(.secondary) .foregroundColor(.secondary)
TextField("Search downloads", text: $searchText) TextField(NSLocalizedString("Search downloads", comment: ""), text: $searchText)
.textFieldStyle(PlainTextFieldStyle()) .textFieldStyle(PlainTextFieldStyle())
.foregroundColor(.primary) .foregroundColor(.primary)
@ -394,14 +394,14 @@ struct CustomDownloadHeader: View {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
TabButton( TabButton(
title: "Active", title: NSLocalizedString("Active", comment: ""),
icon: "arrow.down.circle", icon: "arrow.down.circle",
isSelected: selectedTab == 0, isSelected: selectedTab == 0,
action: { selectedTab = 0 } action: { selectedTab = 0 }
) )
TabButton( TabButton(
title: "Downloaded", title: NSLocalizedString("Downloaded", comment: ""),
icon: "checkmark.circle", icon: "checkmark.circle",
isSelected: selectedTab == 1, isSelected: selectedTab == 1,
action: { selectedTab = 1 } action: { selectedTab = 1 }
@ -526,7 +526,7 @@ struct DownloadSummaryCard: View {
HStack { HStack {
Image(systemName: "chart.bar.fill") Image(systemName: "chart.bar.fill")
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
Text("Download Summary".uppercased()) Text(NSLocalizedString("Download Summary", comment: "").uppercased())
.font(.footnote) .font(.footnote)
.fontWeight(.medium) .fontWeight(.medium)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -538,7 +538,7 @@ struct DownloadSummaryCard: View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 20) { HStack(spacing: 20) {
SummaryItem( SummaryItem(
title: "Shows", title: NSLocalizedString("Shows", comment: ""),
value: "\(totalShows)", value: "\(totalShows)",
icon: "tv.fill" icon: "tv.fill"
) )
@ -546,7 +546,7 @@ struct DownloadSummaryCard: View {
Divider().frame(height: 32) Divider().frame(height: 32)
SummaryItem( SummaryItem(
title: "Episodes", title: NSLocalizedString("Episodes", comment: ""),
value: "\(totalEpisodes)", value: "\(totalEpisodes)",
icon: "play.rectangle.fill" icon: "play.rectangle.fill"
) )
@ -559,7 +559,7 @@ struct DownloadSummaryCard: View {
let sizeUnit = components.dropFirst().first.map(String.init) ?? "" let sizeUnit = components.dropFirst().first.map(String.init) ?? ""
SummaryItem( SummaryItem(
title: "Size (\(sizeUnit))", title: String(format: NSLocalizedString("Size (%@)", comment: ""), sizeUnit),
value: sizeValue, value: sizeValue,
icon: "internaldrive.fill" icon: "internaldrive.fill"
) )
@ -630,7 +630,7 @@ struct DownloadedSection: View {
HStack { HStack {
Image(systemName: "folder.fill") Image(systemName: "folder.fill")
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
Text("Downloaded Shows".uppercased()) Text(NSLocalizedString("Downloaded Shows", comment: "").uppercased())
.font(.footnote) .font(.footnote)
.fontWeight(.medium) .fontWeight(.medium)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -715,7 +715,7 @@ struct EnhancedActiveDownloadCard: View {
VStack(spacing: 6) { VStack(spacing: 6) {
HStack { HStack {
if download.queueStatus == .queued { if download.queueStatus == .queued {
Text("Queued") Text(NSLocalizedString("Queued", comment: ""))
.font(.caption) .font(.caption)
.fontWeight(.medium) .fontWeight(.medium)
.foregroundStyle(.orange) .foregroundStyle(.orange)
@ -797,11 +797,11 @@ struct EnhancedActiveDownloadCard: View {
private var statusText: String { private var statusText: String {
if download.queueStatus == .queued { if download.queueStatus == .queued {
return "Queued" return NSLocalizedString("Queued", comment: "")
} else if taskState == .running { } else if taskState == .running {
return "Downloading" return NSLocalizedString("Downloading", comment: "")
} else { } else {
return "Paused" return NSLocalizedString("Paused", comment: "")
} }
} }
@ -1026,7 +1026,7 @@ struct EnhancedShowEpisodesView: View {
HStack { HStack {
Image(systemName: "list.bullet.rectangle") Image(systemName: "list.bullet.rectangle")
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
Text("Episodes".uppercased()) Text(NSLocalizedString("Episodes", comment: "").uppercased())
.font(.footnote) .font(.footnote)
.fontWeight(.medium) .fontWeight(.medium)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -1051,7 +1051,7 @@ struct EnhancedShowEpisodesView: View {
} label: { } label: {
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: episodeSortOption.systemImage) Image(systemName: episodeSortOption.systemImage)
Text("Sort") Text(NSLocalizedString("Sort", comment: ""))
} }
.font(.subheadline) .font(.subheadline)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
@ -1062,7 +1062,7 @@ struct EnhancedShowEpisodesView: View {
}) { }) {
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: "trash") Image(systemName: "trash")
Text("Delete All") Text(NSLocalizedString("Delete All", comment: ""))
} }
.font(.subheadline) .font(.subheadline)
.foregroundColor(.red) .foregroundColor(.red)
@ -1073,7 +1073,7 @@ struct EnhancedShowEpisodesView: View {
// Episodes List // Episodes List
if group.assets.isEmpty { if group.assets.isEmpty {
Text("No episodes available") Text(NSLocalizedString("No episodes available", comment: ""))
.foregroundColor(.secondary) .foregroundColor(.secondary)
.italic() .italic()
.padding(40) .padding(40)
@ -1086,7 +1086,7 @@ struct EnhancedShowEpisodesView: View {
) )
.contextMenu { .contextMenu {
Button(action: { onPlay(asset) }) { Button(action: { onPlay(asset) }) {
Label("Play", systemImage: "play.fill") Label(NSLocalizedString("Play", comment: ""), systemImage: "play.fill")
} }
.disabled(!asset.fileExists) .disabled(!asset.fileExists)
@ -1094,7 +1094,7 @@ struct EnhancedShowEpisodesView: View {
assetToDelete = asset assetToDelete = asset
showDeleteAlert = true showDeleteAlert = true
}) { }) {
Label("Delete", systemImage: "trash") Label(NSLocalizedString("Delete", comment: ""), systemImage: "trash")
} }
} }
.onTapGesture { .onTapGesture {
@ -1107,27 +1107,27 @@ struct EnhancedShowEpisodesView: View {
} }
.padding(.vertical) .padding(.vertical)
} }
.navigationTitle("Episodes") .navigationTitle(NSLocalizedString("Episodes", comment: ""))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.alert("Delete Episode", isPresented: $showDeleteAlert) { .alert(NSLocalizedString("Delete Episode", comment: ""), isPresented: $showDeleteAlert) {
Button("Cancel", role: .cancel) { } Button(NSLocalizedString("Cancel", comment: ""), role: .cancel) { }
Button("Delete", role: .destructive) { Button(NSLocalizedString("Delete", comment: ""), role: .destructive) {
if let asset = assetToDelete { if let asset = assetToDelete {
onDelete(asset) onDelete(asset)
} }
} }
} message: { } message: {
if let asset = assetToDelete { if let asset = assetToDelete {
Text("Are you sure you want to delete '\(asset.episodeDisplayName)'?") Text(String(format: NSLocalizedString("Are you sure you want to delete '%@'?", comment: ""), asset.episodeDisplayName))
} }
} }
.alert("Delete All Episodes", isPresented: $showDeleteAllAlert) { .alert(NSLocalizedString("Delete All Episodes", comment: ""), isPresented: $showDeleteAllAlert) {
Button("Cancel", role: .cancel) { } Button(NSLocalizedString("Cancel", comment: ""), role: .cancel) { }
Button("Delete All", role: .destructive) { Button(NSLocalizedString("Delete All", comment: ""), role: .destructive) {
deleteAllAssets() deleteAllAssets()
} }
} message: { } message: {
Text("Are you sure you want to delete all \(group.assetCount) episodes in '\(group.title)'?") Text(String(format: NSLocalizedString("Are you sure you want to delete all %d episodes in '%@'?", comment: ""), group.assetCount, group.title))
} }
} }

View file

@ -126,24 +126,33 @@ struct MediaInfoView: View {
if episodeLinks.count == 1 { if episodeLinks.count == 1 {
if let _ = unfinished { if let _ = unfinished {
return "Continue Watching" return NSLocalizedString("Continue Watching", comment: "")
} }
return "Start Watching" return NSLocalizedString("Start Watching", comment: "")
} }
if let finishedIndex = finished, finishedIndex < episodeLinks.count - 1 { if let finishedIndex = finished, finishedIndex < episodeLinks.count - 1 {
let nextEp = episodeLinks[finishedIndex + 1] let nextEp = episodeLinks[finishedIndex + 1]
return "Start Watching Episode \(nextEp.number)" return String(format: NSLocalizedString("Start Watching Episode %d", comment: ""), nextEp.number)
} }
if let unfinishedIndex = unfinished { if let unfinishedIndex = unfinished {
let currentEp = episodeLinks[unfinishedIndex] let currentEp = episodeLinks[unfinishedIndex]
return "Continue Watching Episode \(currentEp.number)" return String(format: NSLocalizedString("Continue Watching Episode %d", comment: ""), currentEp.number)
} }
return "Start Watching" return NSLocalizedString("Start Watching", comment: "")
} }
private var singleEpisodeWatchText: String {
if let ep = episodeLinks.first {
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
return progress <= 0.9 ? NSLocalizedString("Mark watched", comment: "") : NSLocalizedString("Reset progress", comment: "")
}
return NSLocalizedString("Mark watched", comment: "")
}
var body: some View { var body: some View {
ZStack { ZStack {
@ -336,7 +345,7 @@ struct MediaInfoView: View {
.lineLimit(showFullSynopsis ? nil : 3) .lineLimit(showFullSynopsis ? nil : 3)
.animation(nil, value: showFullSynopsis) .animation(nil, value: showFullSynopsis)
Text(showFullSynopsis ? "LESS" : "MORE") Text(showFullSynopsis ? NSLocalizedString("LESS", comment: "") : NSLocalizedString("MORE", comment: ""))
.font(.system(size: 16, weight: .bold)) .font(.system(size: 16, weight: .bold))
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.animation(.easeInOut(duration: 0.3), value: showFullSynopsis) .animation(.easeInOut(duration: 0.3), value: showFullSynopsis)
@ -405,7 +414,7 @@ struct MediaInfoView: View {
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: "arrow.down.circle") Image(systemName: "arrow.down.circle")
.foregroundColor(.primary) .foregroundColor(.primary)
Text("Download") Text(NSLocalizedString("Download", comment: ""))
.font(.system(size: 14, weight: .medium)) .font(.system(size: 14, weight: .medium))
.foregroundColor(.primary) .foregroundColor(.primary)
} }
@ -420,13 +429,13 @@ struct MediaInfoView: View {
} }
VStack(spacing: 4) { VStack(spacing: 4) {
Text("Why am I not seeing any episodes?") Text(NSLocalizedString("Why am I not seeing any episodes?", comment: ""))
.font(.caption) .font(.caption)
.bold() .bold()
.foregroundColor(.gray) .foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
Text("The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases.") Text(NSLocalizedString("The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases.", comment: ""))
.font(.caption) .font(.caption)
.foregroundColor(.gray) .foregroundColor(.gray)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
@ -451,16 +460,6 @@ struct MediaInfoView: View {
return "checkmark.circle" return "checkmark.circle"
} }
private var singleEpisodeWatchText: String {
if let ep = episodeLinks.first {
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
return progress <= 0.9 ? "Mark watched" : "Reset progress"
}
return "Mark watched"
}
@ViewBuilder @ViewBuilder
private var episodesSection: some View { private var episodesSection: some View {
if episodeLinks.count != 1 { if episodeLinks.count != 1 {
@ -474,7 +473,7 @@ struct MediaInfoView: View {
@ViewBuilder @ViewBuilder
private var episodesSectionHeader: some View { private var episodesSectionHeader: some View {
HStack { HStack {
Text("Episodes") Text(NSLocalizedString("Episodes", comment: ""))
.font(.system(size: 22, weight: .bold)) .font(.system(size: 22, weight: .bold))
.foregroundColor(.primary) .foregroundColor(.primary)
@ -524,7 +523,7 @@ struct MediaInfoView: View {
Menu { Menu {
ForEach(0..<seasons.count, id: \.self) { index in ForEach(0..<seasons.count, id: \.self) { index in
Button(action: { selectedSeason = index }) { Button(action: { selectedSeason = index }) {
Text("Season \(index + 1)") Text(String(format: NSLocalizedString("Season %d", comment: ""), index + 1))
} }
} }
} label: { } label: {
@ -612,12 +611,12 @@ struct MediaInfoView: View {
.font(.system(size: 48)) .font(.system(size: 48))
.foregroundColor(.secondary) .foregroundColor(.secondary)
Text("No Episodes Available") Text(NSLocalizedString("No Episodes Available", comment: ""))
.font(.title2) .font(.title2)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(.primary) .foregroundColor(.primary)
Text("Episodes might not be available yet or there could be an issue with the source.") Text(NSLocalizedString("Episodes might not be available yet or there could be an issue with the source.", comment: ""))
.font(.body) .font(.body)
.lineLimit(0) .lineLimit(0)
.foregroundColor(.secondary) .foregroundColor(.secondary)

View file

@ -153,13 +153,13 @@ struct SettingsViewData: View {
return ScrollView { return ScrollView {
VStack(spacing: 24) { VStack(spacing: 24) {
SettingsSection( SettingsSection(
title: "App Storage", title: NSLocalizedString("App Storage", comment: ""),
footer: "The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction." footer: NSLocalizedString("The app cache helps the app load images faster.\n\nClearing the Documents folder will delete all downloaded modules.\n\nDo not erase App Data unless you understand the consequences — it may cause the app to malfunction.", comment: "")
) { ) {
VStack(spacing: 0) { VStack(spacing: 0) {
SettingsButtonRow( SettingsButtonRow(
icon: "trash", icon: "trash",
title: "Remove All Cache", title: NSLocalizedString("Remove All Cache", comment: ""),
subtitle: cacheSizeText, subtitle: cacheSizeText,
action: { action: {
activeAlert = .clearCache activeAlert = .clearCache
@ -171,7 +171,7 @@ struct SettingsViewData: View {
SettingsButtonRow( SettingsButtonRow(
icon: "film", icon: "film",
title: "Remove Downloads", title: NSLocalizedString("Remove Downloads", comment: ""),
subtitle: formatSize(downloadsSize), subtitle: formatSize(downloadsSize),
action: { action: {
activeAlert = .removeDownloads activeAlert = .removeDownloads
@ -183,7 +183,7 @@ struct SettingsViewData: View {
SettingsButtonRow( SettingsButtonRow(
icon: "doc.text", icon: "doc.text",
title: "Remove All Documents", title: NSLocalizedString("Remove All Documents", comment: ""),
subtitle: formatSize(documentsSize), subtitle: formatSize(documentsSize),
action: { action: {
activeAlert = .removeDocs activeAlert = .removeDocs
@ -195,7 +195,7 @@ struct SettingsViewData: View {
SettingsButtonRow( SettingsButtonRow(
icon: "exclamationmark.triangle", icon: "exclamationmark.triangle",
title: "Erase all App Data", title: NSLocalizedString("Erase all App Data", comment: ""),
action: { action: {
activeAlert = .eraseData activeAlert = .eraseData
showAlert = true showAlert = true
@ -205,7 +205,7 @@ struct SettingsViewData: View {
} }
} }
.scrollViewBottomPadding() .scrollViewBottomPadding()
.navigationTitle("App Data") .navigationTitle(NSLocalizedString("App Data", comment: ""))
.onAppear { .onAppear {
calculateCacheSize() calculateCacheSize()
updateSizes() updateSizes()
@ -215,36 +215,36 @@ struct SettingsViewData: View {
switch activeAlert { switch activeAlert {
case .eraseData: case .eraseData:
return Alert( return Alert(
title: Text("Erase App Data"), title: Text(NSLocalizedString("Erase App Data", comment: "")),
message: Text("Are you sure you want to erase all app data? This action cannot be undone."), message: Text(NSLocalizedString("Are you sure you want to erase all app data? This action cannot be undone.", comment: "")),
primaryButton: .destructive(Text("Erase")) { primaryButton: .destructive(Text(NSLocalizedString("Erase", comment: ""))) {
eraseAppData() eraseAppData()
}, },
secondaryButton: .cancel() secondaryButton: .cancel()
) )
case .removeDocs: case .removeDocs:
return Alert( return Alert(
title: Text("Remove Documents"), title: Text(NSLocalizedString("Remove Documents", comment: "")),
message: Text("Are you sure you want to remove all files in the Documents folder? This will remove all modules."), message: Text(NSLocalizedString("Are you sure you want to remove all files in the Documents folder? This will remove all modules.", comment: "")),
primaryButton: .destructive(Text("Remove")) { primaryButton: .destructive(Text(NSLocalizedString("Remove", comment: ""))) {
removeAllFilesInDocuments() removeAllFilesInDocuments()
}, },
secondaryButton: .cancel() secondaryButton: .cancel()
) )
case .removeDownloads: case .removeDownloads:
return Alert( return Alert(
title: Text("Remove Downloaded Media"), title: Text(NSLocalizedString("Remove Downloaded Media", comment: "")),
message: Text("Are you sure you want to remove all downloaded media files (.mov, .mp4, .pkg)? This action cannot be undone."), message: Text(NSLocalizedString("Are you sure you want to remove all downloaded media files (.mov, .mp4, .pkg)? This action cannot be undone.", comment: "")),
primaryButton: .destructive(Text("Remove")) { primaryButton: .destructive(Text(NSLocalizedString("Remove", comment: ""))) {
removeDownloadedMedia() removeDownloadedMedia()
}, },
secondaryButton: .cancel() secondaryButton: .cancel()
) )
case .clearCache: case .clearCache:
return Alert( return Alert(
title: Text("Clear Cache"), title: Text(NSLocalizedString("Clear Cache", comment: "")),
message: Text("Are you sure you want to clear all cached data? This will help free up storage space."), message: Text(NSLocalizedString("Are you sure you want to clear all cached data? This will help free up storage space.", comment: "")),
primaryButton: .destructive(Text("Clear")) { primaryButton: .destructive(Text(NSLocalizedString("Clear", comment: ""))) {
clearAllCaches() clearAllCaches()
}, },
secondaryButton: .cancel() secondaryButton: .cancel()
@ -252,6 +252,7 @@ struct SettingsViewData: View {
} }
} }
} }
}
func calculateCacheSize() { func calculateCacheSize() {
isCalculatingSize = true isCalculatingSize = true
@ -429,5 +430,5 @@ struct SettingsViewData: View {
formatter.countStyle = .file formatter.countStyle = .file
return formatter.string(fromByteCount: bytes) return formatter.string(fromByteCount: bytes)
} }
}
} }

View file

@ -168,20 +168,21 @@ struct SettingsViewGeneral: View {
private let TMDBimageWidhtList = ["300", "500", "780", "1280", "original"] private let TMDBimageWidhtList = ["300", "500", "780", "1280", "original"]
private let sortOrderOptions = ["Ascending", "Descending"] private let sortOrderOptions = ["Ascending", "Descending"]
@EnvironmentObject var settings: Settings @EnvironmentObject var settings: Settings
@State private var showRestartAlert = false
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: 24) { VStack(spacing: 24) {
SettingsSection(title: "Interface") { SettingsSection(title: NSLocalizedString("Interface", comment: "")) {
SettingsPickerRow( SettingsPickerRow(
icon: "paintbrush", icon: "paintbrush",
title: "Appearance", title: NSLocalizedString("Appearance", comment: ""),
options: [Appearance.system, .light, .dark], options: [Appearance.system, .light, .dark],
optionToString: { appearance in optionToString: { appearance in
switch appearance { switch appearance {
case .system: return "System" case .system: return NSLocalizedString("System", comment: "")
case .light: return "Light" case .light: return NSLocalizedString("Light", comment: "")
case .dark: return "Dark" case .dark: return NSLocalizedString("Dark", comment: "")
} }
}, },
selection: $settings.selectedAppearance selection: $settings.selectedAppearance
@ -189,19 +190,32 @@ struct SettingsViewGeneral: View {
SettingsToggleRow( SettingsToggleRow(
icon: "wand.and.rays.inverse", icon: "wand.and.rays.inverse",
title: "Hide Splash Screen", title: NSLocalizedString("Hide Splash Screen", comment: ""),
isOn: $hideSplashScreenEnable, isOn: $hideSplashScreenEnable,
showDivider: false showDivider: false
) )
} }
SettingsSection(title: NSLocalizedString("Language", comment: "")) {
SettingsPickerRow(
icon: "globe",
title: NSLocalizedString("App Language", comment: ""),
options: ["English", "Dutch"],
optionToString: { $0 },
selection: $settings.selectedLanguage
)
.onChange(of: settings.selectedLanguage) { _ in
showRestartAlert = true
}
}
SettingsSection( SettingsSection(
title: "Media View", title: NSLocalizedString("Media View", comment: ""),
footer: "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.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers." footer: NSLocalizedString("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.\n\nFor episode metadata, it refers to the episode thumbnail and title, since sometimes it can contain spoilers.", comment: "")
) { ) {
SettingsPickerRow( SettingsPickerRow(
icon: "list.number", icon: "list.number",
title: "Episodes Range", title: NSLocalizedString("Episodes Range", comment: ""),
options: [25, 50, 75, 100], options: [25, 50, 75, 100],
optionToString: { "\($0)" }, optionToString: { "\($0)" },
selection: $episodeChunkSize selection: $episodeChunkSize
@ -209,7 +223,7 @@ struct SettingsViewGeneral: View {
SettingsToggleRow( SettingsToggleRow(
icon: "info.circle", icon: "info.circle",
title: "Fetch Episode metadata", title: NSLocalizedString("Fetch Episode metadata", comment: ""),
isOn: $fetchEpisodeMetadata isOn: $fetchEpisodeMetadata
) )
@ -230,10 +244,43 @@ struct SettingsViewGeneral: View {
SettingsSection( SettingsSection(
title: "Media Grid Layout", title: "Media Grid Layout",
footer: "Adjust the number of media items per row in portrait and landscape modes." footer: "Adjust the number of media items per row in portrait and landscape modes."
) {
SettingsPickerRow(
icon: "server.rack",
title: NSLocalizedString("Metadata Provider", comment: ""),
options: metadataProvidersList,
optionToString: { $0 },
selection: $metadataProviders,
showDivider: true
)
SettingsPickerRow(
icon: "square.stack.3d.down.right",
title: NSLocalizedString("Thumbnails Width", comment: ""),
options: TMDBimageWidhtList,
optionToString: { $0 },
selection: $TMDBimageWidht,
showDivider: false
)
} else {
SettingsPickerRow(
icon: "server.rack",
title: NSLocalizedString("Metadata Provider", comment: ""),
options: metadataProvidersList,
optionToString: { $0 },
selection: $metadataProviders,
showDivider: false
)
}
}
SettingsSection(
title: NSLocalizedString("Media Grid Layout", comment: ""),
footer: NSLocalizedString("Adjust the number of media items per row in portrait and landscape modes.", comment: "")
) { ) {
SettingsPickerRow( SettingsPickerRow(
icon: "rectangle.portrait", icon: "rectangle.portrait",
title: "Portrait Columns", title: NSLocalizedString("Portrait Columns", comment: ""),
options: UIDevice.current.userInterfaceIdiom == .pad ? Array(1...5) : Array(1...4), options: UIDevice.current.userInterfaceIdiom == .pad ? Array(1...5) : Array(1...4),
optionToString: { "\($0)" }, optionToString: { "\($0)" },
selection: $mediaColumnsPortrait selection: $mediaColumnsPortrait
@ -241,7 +288,7 @@ struct SettingsViewGeneral: View {
SettingsPickerRow( SettingsPickerRow(
icon: "rectangle", icon: "rectangle",
title: "Landscape Columns", title: NSLocalizedString("Landscape Columns", comment: ""),
options: UIDevice.current.userInterfaceIdiom == .pad ? Array(2...8) : Array(2...5), options: UIDevice.current.userInterfaceIdiom == .pad ? Array(2...8) : Array(2...5),
optionToString: { "\($0)" }, optionToString: { "\($0)" },
selection: $mediaColumnsLandscape, selection: $mediaColumnsLandscape,
@ -250,33 +297,40 @@ struct SettingsViewGeneral: View {
} }
SettingsSection( SettingsSection(
title: "Modules", title: NSLocalizedString("Modules", comment: ""),
footer: "Note that the modules will be replaced only if there is a different version string inside the JSON file." footer: NSLocalizedString("Note that the modules will be replaced only if there is a different version string inside the JSON file.", comment: "")
) { ) {
SettingsToggleRow( SettingsToggleRow(
icon: "arrow.clockwise", icon: "arrow.clockwise",
title: "Refresh Modules on Launch", title: NSLocalizedString("Refresh Modules on Launch", comment: ""),
isOn: $refreshModulesOnLaunch, isOn: $refreshModulesOnLaunch,
showDivider: false showDivider: false
) )
} }
SettingsSection( SettingsSection(
title: "Advanced", title: NSLocalizedString("Advanced", comment: ""),
footer: "Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time." footer: NSLocalizedString("Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time.", comment: "")
) { ) {
SettingsToggleRow( SettingsToggleRow(
icon: "chart.bar", icon: "chart.bar",
title: "Enable Analytics", title: NSLocalizedString("Enable Analytics", comment: ""),
isOn: $analyticsEnabled, isOn: $analyticsEnabled,
showDivider: false showDivider: false
) )
} }
} }
.padding(.vertical, 20)
}
.navigationTitle("General") .navigationTitle("General")
.scrollViewBottomPadding() .scrollViewBottomPadding()
} }
.navigationTitle(NSLocalizedString("General", comment: ""))
.scrollViewBottomPadding()
.alert(isPresented: $showRestartAlert) {
Alert(
title: Text(NSLocalizedString("Restart Required", comment: "")),
message: Text(NSLocalizedString("Please restart the app to apply the language change.", comment: "")),
dismissButton: .default(Text("OK"))
)
}
} }
} }

View file

@ -76,12 +76,12 @@ struct SettingsViewLogger: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: 24) { VStack(spacing: 24) {
SettingsSection(title: "Logs") { SettingsSection(title: NSLocalizedString("Logs", comment: "")) {
if isLoading { if isLoading {
HStack { HStack {
ProgressView() ProgressView()
.scaleEffect(0.8) .scaleEffect(0.8)
Text("Loading logs...") Text(NSLocalizedString("Loading logs...", comment: ""))
.font(.footnote) .font(.footnote)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
@ -99,7 +99,7 @@ struct SettingsViewLogger: View {
Button(action: { Button(action: {
showFullLogs = true showFullLogs = true
}) { }) {
Text("Show More (\(logs.count - displayCharacterLimit) more characters)") Text(NSLocalizedString("Show More (%lld more characters)", comment: "").replacingOccurrences(of: "%lld", with: "\(logs.count - displayCharacterLimit)"))
.font(.footnote) .font(.footnote)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
} }
@ -113,7 +113,7 @@ struct SettingsViewLogger: View {
} }
.padding(.vertical, 20) .padding(.vertical, 20)
} }
.navigationTitle("Logs") .navigationTitle(NSLocalizedString("Logs", comment: ""))
.onAppear { .onAppear {
loadLogsAsync() loadLogsAsync()
} }
@ -123,14 +123,14 @@ struct SettingsViewLogger: View {
Menu { Menu {
Button(action: { Button(action: {
UIPasteboard.general.string = logs UIPasteboard.general.string = logs
DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) DropManager.shared.showDrop(title: NSLocalizedString("Copied to Clipboard", comment: ""), subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill"))
}) { }) {
Label("Copy to Clipboard", systemImage: "doc.on.doc") Label(NSLocalizedString("Copy to Clipboard", comment: ""), systemImage: "doc.on.doc")
} }
Button(role: .destructive, action: { Button(role: .destructive, action: {
clearLogsAsync() clearLogsAsync()
}) { }) {
Label("Clear Logs", systemImage: "trash") Label(NSLocalizedString("Clear Logs", comment: ""), systemImage: "trash")
} }
} label: { } label: {
Image(systemName: "ellipsis.circle") Image(systemName: "ellipsis.circle")

View file

@ -115,11 +115,11 @@ class LogFilterViewModel: ObservableObject {
private let userDefaultsKey = "LogFilterStates" private let userDefaultsKey = "LogFilterStates"
private let hardcodedFilters: [(type: String, description: String, defaultState: Bool)] = [ private let hardcodedFilters: [(type: String, description: String, defaultState: Bool)] = [
("General", "General events and activities.", true), (NSLocalizedString("General", comment: ""), NSLocalizedString("General events and activities.", comment: ""), true),
("Stream", "Streaming and video playback.", true), (NSLocalizedString("Stream", comment: ""), NSLocalizedString("Streaming and video playback.", comment: ""), true),
("Error", "Errors and critical issues.", true), (NSLocalizedString("Error", comment: ""), NSLocalizedString("Errors and critical issues.", comment: ""), true),
("Debug", "Debugging and troubleshooting.", false), (NSLocalizedString("Debug", comment: ""), NSLocalizedString("Debugging and troubleshooting.", comment: ""), false),
("Download", "HLS video downloading.", true), (NSLocalizedString("Download", comment: ""), NSLocalizedString("HLS video downloading.", comment: ""), true),
("HTMLStrings", "", false) ("HTMLStrings", "", false)
] ]
@ -179,7 +179,7 @@ struct SettingsViewLoggerFilter: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: 24) { VStack(spacing: 24) {
SettingsSection(title: "Log Types") { SettingsSection(title: NSLocalizedString("Log Types", comment: "")) {
ForEach($viewModel.filters) { $filter in ForEach($viewModel.filters) { $filter in
SettingsToggleRow( SettingsToggleRow(
icon: iconForFilter(filter.type), icon: iconForFilter(filter.type),
@ -192,6 +192,6 @@ struct SettingsViewLoggerFilter: View {
} }
.padding(.vertical, 20) .padding(.vertical, 20)
} }
.navigationTitle("Log Filters") .navigationTitle(NSLocalizedString("Log Filters", comment: ""))
} }
} }

View file

@ -119,16 +119,16 @@ fileprivate struct ModuleListItemView: View {
.contextMenu { .contextMenu {
Button(action: { Button(action: {
UIPasteboard.general.string = module.metadataUrl UIPasteboard.general.string = module.metadataUrl
DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) DropManager.shared.showDrop(title: NSLocalizedString("Copied to Clipboard", comment: ""), subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill"))
}) { }) {
Label("Copy URL", systemImage: "doc.on.doc") Label(NSLocalizedString("Copy URL", comment: ""), systemImage: "doc.on.doc")
} }
Button(role: .destructive) { Button(role: .destructive) {
if selectedModuleId != module.id.uuidString { if selectedModuleId != module.id.uuidString {
onDelete() onDelete()
} }
} label: { } label: {
Label("Delete", systemImage: "trash") Label(NSLocalizedString("Delete", comment: ""), systemImage: "trash")
} }
.disabled(selectedModuleId == module.id.uuidString) .disabled(selectedModuleId == module.id.uuidString)
} }
@ -137,7 +137,7 @@ fileprivate struct ModuleListItemView: View {
Button(role: .destructive) { Button(role: .destructive) {
onDelete() onDelete()
} label: { } label: {
Label("Delete", systemImage: "trash") Label(NSLocalizedString("Delete", comment: ""), systemImage: "trash")
} }
} }
} }
@ -163,25 +163,25 @@ struct SettingsViewModule: View {
ScrollView { ScrollView {
VStack(spacing: 24) { VStack(spacing: 24) {
if moduleManager.modules.isEmpty { if moduleManager.modules.isEmpty {
SettingsSection(title: "Modules") { SettingsSection(title: NSLocalizedString("Modules", comment: "")) {
VStack(spacing: 16) { VStack(spacing: 16) {
Image(systemName: "plus.app") Image(systemName: "plus.app")
.font(.largeTitle) .font(.largeTitle)
.foregroundColor(.secondary) .foregroundColor(.secondary)
Text("No Modules") Text(NSLocalizedString("No Modules", comment: ""))
.font(.headline) .font(.headline)
if didReceiveDefaultPageLink { if didReceiveDefaultPageLink {
NavigationLink(destination: CommunityLibraryView() NavigationLink(destination: CommunityLibraryView()
.environmentObject(moduleManager)) { .environmentObject(moduleManager)) {
Text("Check out some community modules here!") Text(NSLocalizedString("Check out some community modules here!", comment: ""))
.font(.caption) .font(.caption)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} else { } else {
Text("Click the plus button to add a module!") Text(NSLocalizedString("Click the plus button to add a module!", comment: ""))
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@ -191,14 +191,14 @@ struct SettingsViewModule: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
} else { } else {
SettingsSection(title: "Installed Modules") { SettingsSection(title: NSLocalizedString("Installed Modules", comment: "")) {
ForEach(moduleManager.modules) { module in ForEach(moduleManager.modules) { module in
ModuleListItemView( ModuleListItemView(
module: module, module: module,
selectedModuleId: selectedModuleId, selectedModuleId: selectedModuleId,
onDelete: { onDelete: {
moduleManager.deleteModule(module) moduleManager.deleteModule(module)
DropManager.shared.showDrop(title: "Module Removed", subtitle: "", duration: 1.0, icon: UIImage(systemName: "trash")) DropManager.shared.showDrop(title: NSLocalizedString("Module Removed", comment: ""), subtitle: "", duration: 1.0, icon: UIImage(systemName: "trash"))
}, },
onSelect: { onSelect: {
selectedModuleId = module.id.uuidString selectedModuleId = module.id.uuidString
@ -216,7 +216,7 @@ struct SettingsViewModule: View {
.padding(.vertical, 20) .padding(.vertical, 20)
} }
.scrollViewBottomPadding() .scrollViewBottomPadding()
.navigationTitle("Modules") .navigationTitle(NSLocalizedString("Modules", comment: ""))
.navigationBarItems(trailing: .navigationBarItems(trailing:
HStack(spacing: 16) { HStack(spacing: 16) {
if didReceiveDefaultPageLink { if didReceiveDefaultPageLink {
@ -228,7 +228,7 @@ struct SettingsViewModule: View {
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(5) .padding(5)
} }
.accessibilityLabel("Open Community Library") .accessibilityLabel(NSLocalizedString("Open Community Library", comment: ""))
} }
Button(action: { Button(action: {
@ -239,7 +239,7 @@ struct SettingsViewModule: View {
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(5) .padding(5)
} }
.accessibilityLabel("Add Module") .accessibilityLabel(NSLocalizedString("Add Module", comment: ""))
} }
) )
.background( .background(

View file

@ -215,12 +215,12 @@ struct SettingsViewPlayer: View {
ScrollView { ScrollView {
VStack(spacing: 24) { VStack(spacing: 24) {
SettingsSection( SettingsSection(
title: "Media Player", title: NSLocalizedString("Media Player", comment: ""),
footer: "Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments." footer: NSLocalizedString("Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.", comment: "")
) { ) {
SettingsPickerRow( SettingsPickerRow(
icon: "play.circle", icon: "play.circle",
title: "Media Player", title: NSLocalizedString("Media Player", comment: ""),
options: mediaPlayers, options: mediaPlayers,
optionToString: { $0 }, optionToString: { $0 },
selection: $externalPlayer selection: $externalPlayer
@ -228,35 +228,35 @@ struct SettingsViewPlayer: View {
SettingsToggleRow( SettingsToggleRow(
icon: "rotate.right", icon: "rotate.right",
title: "Force Landscape", title: NSLocalizedString("Force Landscape", comment: ""),
isOn: $isAlwaysLandscape isOn: $isAlwaysLandscape
) )
SettingsToggleRow( SettingsToggleRow(
icon: "hand.tap", icon: "hand.tap",
title: "Two Finger Hold for Pause", title: NSLocalizedString("Two Finger Hold for Pause", comment: ""),
isOn: $holdForPauseEnabled, isOn: $holdForPauseEnabled,
showDivider: true showDivider: true
) )
SettingsToggleRow( SettingsToggleRow(
icon: "pip", icon: "pip",
title: "Show PiP Button", title: NSLocalizedString("Show PiP Button", comment: ""),
isOn: $pipButtonVisible, isOn: $pipButtonVisible,
showDivider: false showDivider: false
) )
} }
SettingsSection(title: "Speed Settings") { SettingsSection(title: NSLocalizedString("Speed Settings", comment: "")) {
SettingsToggleRow( SettingsToggleRow(
icon: "speedometer", icon: "speedometer",
title: "Remember Playback speed", title: NSLocalizedString("Remember Playback speed", comment: ""),
isOn: $isRememberPlaySpeed isOn: $isRememberPlaySpeed
) )
SettingsStepperRow( SettingsStepperRow(
icon: "forward.fill", icon: "forward.fill",
title: "Hold Speed", title: NSLocalizedString("Hold Speed", comment: ""),
value: $holdSpeedPlayer, value: $holdSpeedPlayer,
range: 0.25...2.5, range: 0.25...2.5,
step: 0.25, step: 0.25,
@ -264,14 +264,13 @@ struct SettingsViewPlayer: View {
showDivider: false showDivider: false
) )
} }
SettingsSection( SettingsSection(
title: "Video Quality Preferences", title: String(localized: "Video Quality Preferences"),
footer: "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." footer: String(localized: "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.")
) { ) {
SettingsPickerRow( SettingsPickerRow(
icon: "wifi", icon: "wifi",
title: "WiFi Quality", title: String(localized: "WiFi Quality"),
options: qualityOptions, options: qualityOptions,
optionToString: { $0 }, optionToString: { $0 },
selection: $wifiQuality selection: $wifiQuality
@ -279,7 +278,7 @@ struct SettingsViewPlayer: View {
SettingsPickerRow( SettingsPickerRow(
icon: "antenna.radiowaves.left.and.right", icon: "antenna.radiowaves.left.and.right",
title: "Cellular Quality", title: String(localized: "Cellular Quality"),
options: qualityOptions, options: qualityOptions,
optionToString: { $0 }, optionToString: { $0 },
selection: $cellularQuality, selection: $cellularQuality,
@ -287,8 +286,8 @@ struct SettingsViewPlayer: View {
) )
} }
SettingsSection(title: "Progress bar Marker Color") { SettingsSection(title: NSLocalizedString("Progress bar Marker Color", comment: "")) {
ColorPicker("Segments Color", selection: Binding( ColorPicker(NSLocalizedString("Segments Color", comment: ""), selection: Binding(
get: { get: {
if let data = UserDefaults.standard.data(forKey: "segmentsColorData"), if let data = UserDefaults.standard.data(forKey: "segmentsColorData"),
let uiColor = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? UIColor { let uiColor = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? UIColor {
@ -311,12 +310,12 @@ struct SettingsViewPlayer: View {
} }
SettingsSection( SettingsSection(
title: "Skip Settings", title: NSLocalizedString("Skip Settings", comment: ""),
footer: "Double tapping the screen on it's sides will skip with the short tap setting." footer: NSLocalizedString("Double tapping the screen on it's sides will skip with the short tap setting.", comment: "")
) { ) {
SettingsStepperRow( SettingsStepperRow(
icon: "goforward", icon: "goforward",
title: "Tap Skip", title: NSLocalizedString("Tap Skip", comment: ""),
value: $skipIncrement, value: $skipIncrement,
range: 5...300, range: 5...300,
step: 5, step: 5,
@ -325,7 +324,7 @@ struct SettingsViewPlayer: View {
SettingsStepperRow( SettingsStepperRow(
icon: "goforward.plus", icon: "goforward.plus",
title: "Long press Skip", title: NSLocalizedString("Long press Skip", comment: ""),
value: $skipIncrementHold, value: $skipIncrementHold,
range: 5...300, range: 5...300,
step: 5, step: 5,
@ -334,19 +333,19 @@ struct SettingsViewPlayer: View {
SettingsToggleRow( SettingsToggleRow(
icon: "hand.tap.fill", icon: "hand.tap.fill",
title: "Double Tap to Seek", title: NSLocalizedString("Double Tap to Seek", comment: ""),
isOn: $doubleTapSeekEnabled isOn: $doubleTapSeekEnabled
) )
SettingsToggleRow( SettingsToggleRow(
icon: "forward.end", icon: "forward.end",
title: "Show Skip 85s Button", title: NSLocalizedString("Show Skip 85s Button", comment: ""),
isOn: $skip85Visible isOn: $skip85Visible
) )
SettingsToggleRow( SettingsToggleRow(
icon: "forward.frame", icon: "forward.frame",
title: "Show Skip Intro / Outro Buttons", title: NSLocalizedString("Show Skip Intro / Outro Buttons", comment: ""),
isOn: $skipIntroOutroVisible, isOn: $skipIntroOutroVisible,
showDivider: false showDivider: false
) )
@ -357,7 +356,7 @@ struct SettingsViewPlayer: View {
.padding(.vertical, 20) .padding(.vertical, 20)
} }
.scrollViewBottomPadding() .scrollViewBottomPadding()
.navigationTitle("Player") .navigationTitle(NSLocalizedString("Player", comment: ""))
} }
} }
@ -374,10 +373,10 @@ struct SubtitleSettingsSection: View {
private let shadowOptions = [0, 1, 3, 6] private let shadowOptions = [0, 1, 3, 6]
var body: some View { var body: some View {
SettingsSection(title: "Subtitle Settings") { SettingsSection(title: NSLocalizedString("Subtitle Settings", comment: "")) {
SettingsToggleRow( SettingsToggleRow(
icon: "captions.bubble", icon: "captions.bubble",
title: "Enable Subtitles", title: NSLocalizedString("Enable Subtitles", comment: ""),
isOn: $subtitlesEnabled, isOn: $subtitlesEnabled,
showDivider: false showDivider: false
) )
@ -389,7 +388,7 @@ struct SubtitleSettingsSection: View {
SettingsPickerRow( SettingsPickerRow(
icon: "paintbrush", icon: "paintbrush",
title: "Subtitle Color", title: NSLocalizedString("Subtitle Color", comment: ""),
options: colors, options: colors,
optionToString: { $0.capitalized }, optionToString: { $0.capitalized },
selection: $foregroundColor selection: $foregroundColor
@ -402,7 +401,7 @@ struct SubtitleSettingsSection: View {
SettingsPickerRow( SettingsPickerRow(
icon: "shadow", icon: "shadow",
title: "Shadow", title: NSLocalizedString("Shadow", comment: ""),
options: shadowOptions, options: shadowOptions,
optionToString: { "\($0)" }, optionToString: { "\($0)" },
selection: Binding( selection: Binding(
@ -418,7 +417,7 @@ struct SubtitleSettingsSection: View {
SettingsToggleRow( SettingsToggleRow(
icon: "rectangle.fill", icon: "rectangle.fill",
title: "Background Enabled", title: NSLocalizedString("Background Enabled", comment: ""),
isOn: $backgroundEnabled isOn: $backgroundEnabled
) )
.onChange(of: backgroundEnabled) { newValue in .onChange(of: backgroundEnabled) { newValue in
@ -429,7 +428,7 @@ struct SubtitleSettingsSection: View {
SettingsStepperRow( SettingsStepperRow(
icon: "textformat.size", icon: "textformat.size",
title: "Font Size", title: NSLocalizedString("Font Size", comment: ""),
value: $fontSize, value: $fontSize,
range: 12...36, range: 12...36,
step: 1 step: 1
@ -442,7 +441,7 @@ struct SubtitleSettingsSection: View {
SettingsStepperRow( SettingsStepperRow(
icon: "arrow.up.and.down", icon: "arrow.up.and.down",
title: "Bottom Padding", title: NSLocalizedString("Bottom Padding", comment: ""),
value: $bottomPadding, value: $bottomPadding,
range: 0...50, range: 0...50,
step: 1, step: 1,

View file

@ -117,7 +117,7 @@ struct SettingsViewTrackers: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: 24) { VStack(spacing: 24) {
SettingsSection(title: "AniList") { SettingsSection(title: NSLocalizedString("AniList", comment: "")) {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(alignment: .center, spacing: 10) { HStack(alignment: .center, spacing: 10) {
LazyImage(url: URL(string: "https://raw.githubusercontent.com/cranci1/Ryu/2f10226aa087154974a70c1ec78aa83a47daced9/Ryu/Assets.xcassets/Listing/Anilist.imageset/anilist.png")) { state in LazyImage(url: URL(string: "https://raw.githubusercontent.com/cranci1/Ryu/2f10226aa087154974a70c1ec78aa83a47daced9/Ryu/Assets.xcassets/Listing/Anilist.imageset/anilist.png")) { state in
@ -137,18 +137,17 @@ struct SettingsViewTrackers: View {
} }
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("AniList.co") Text(NSLocalizedString("AniList.co", comment: ""))
.font(.title3) .font(.title3)
.fontWeight(.semibold) .fontWeight(.semibold)
Group {
if isAnilistLoading { if isAnilistLoading {
ProgressView() ProgressView()
.scaleEffect(0.8) .scaleEffect(0.8)
.frame(height: 18) .frame(height: 18)
} else if isAnilistLoggedIn { } else if isAnilistLoggedIn {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("Logged in as ") Text(NSLocalizedString("Logged in as", comment: ""))
.font(.footnote) .font(.footnote)
.foregroundStyle(.gray) .foregroundStyle(.gray)
Text(anilistUsername) Text(anilistUsername)
@ -158,13 +157,12 @@ struct SettingsViewTrackers: View {
} }
.frame(height: 18) .frame(height: 18)
} else { } else {
Text(anilistStatus) Text(NSLocalizedString("You are not logged in", comment: ""))
.font(.footnote) .font(.footnote)
.foregroundStyle(.gray) .foregroundStyle(.gray)
.frame(height: 18) .frame(height: 18)
} }
} }
}
.frame(height: 60, alignment: .center) .frame(height: 60, alignment: .center)
Spacer() Spacer()
@ -179,7 +177,7 @@ struct SettingsViewTrackers: View {
SettingsToggleRow( SettingsToggleRow(
icon: "arrow.triangle.2.circlepath", icon: "arrow.triangle.2.circlepath",
title: "Sync anime progress", title: NSLocalizedString("Sync anime progress", comment: ""),
isOn: $isSendPushUpdates, isOn: $isSendPushUpdates,
showDivider: false showDivider: false
) )
@ -200,7 +198,7 @@ struct SettingsViewTrackers: View {
.frame(width: 24, height: 24) .frame(width: 24, height: 24)
.foregroundStyle(isAnilistLoggedIn ? .red : .accentColor) .foregroundStyle(isAnilistLoggedIn ? .red : .accentColor)
Text(isAnilistLoggedIn ? "Log Out from AniList" : "Log In with AniList") Text(isAnilistLoggedIn ? NSLocalizedString("Log Out from AniList", comment: "") : NSLocalizedString("Log In with AniList", comment: ""))
.foregroundStyle(isAnilistLoggedIn ? .red : .accentColor) .foregroundStyle(isAnilistLoggedIn ? .red : .accentColor)
Spacer() Spacer()
@ -212,7 +210,7 @@ struct SettingsViewTrackers: View {
} }
} }
SettingsSection(title: "Trakt") { SettingsSection(title: NSLocalizedString("Trakt", comment: "")) {
VStack(spacing: 0) { VStack(spacing: 0) {
HStack(alignment: .center, spacing: 10) { HStack(alignment: .center, spacing: 10) {
LazyImage(url: URL(string: "https://static-00.iconduck.com/assets.00/trakt-icon-2048x2048-2633ksxg.png")) { state in LazyImage(url: URL(string: "https://static-00.iconduck.com/assets.00/trakt-icon-2048x2048-2633ksxg.png")) { state in
@ -232,34 +230,32 @@ struct SettingsViewTrackers: View {
} }
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("Trakt.tv") Text(NSLocalizedString("Trakt.tv", comment: ""))
.font(.title3) .font(.title3)
.fontWeight(.semibold) .fontWeight(.semibold)
Group {
if isTraktLoading { if isTraktLoading {
ProgressView() ProgressView()
.scaleEffect(0.8) .scaleEffect(0.8)
.frame(height: 18) .frame(height: 18)
} else if isTraktLoggedIn { } else if isTraktLoggedIn {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("Logged in as ") Text(NSLocalizedString("Logged in as", comment: ""))
.font(.footnote) .font(.footnote)
.foregroundStyle(.gray) .foregroundStyle(.gray)
Text(traktUsername) Text(traktUsername)
.font(.footnote) .font(.footnote)
.fontWeight(.medium) .fontWeight(.medium)
.foregroundStyle(.primary) .foregroundStyle(Color.accentColor)
} }
.frame(height: 18) .frame(height: 18)
} else { } else {
Text(traktStatus) Text(NSLocalizedString("You are not logged in", comment: ""))
.font(.footnote) .font(.footnote)
.foregroundStyle(.gray) .foregroundStyle(.gray)
.frame(height: 18) .frame(height: 18)
} }
} }
}
.frame(height: 60, alignment: .center) .frame(height: 60, alignment: .center)
Spacer() Spacer()
@ -268,6 +264,18 @@ struct SettingsViewTrackers: View {
.padding(.vertical, 12) .padding(.vertical, 12)
.frame(height: 84) .frame(height: 84)
if isTraktLoggedIn {
Divider()
.padding(.horizontal, 16)
SettingsToggleRow(
icon: "arrow.triangle.2.circlepath",
title: NSLocalizedString("Sync TV shows progress", comment: ""),
isOn: $isSendTraktUpdates,
showDivider: false
)
}
Divider() Divider()
.padding(.horizontal, 16) .padding(.horizontal, 16)
@ -283,7 +291,7 @@ struct SettingsViewTrackers: View {
.frame(width: 24, height: 24) .frame(width: 24, height: 24)
.foregroundStyle(isTraktLoggedIn ? .red : .accentColor) .foregroundStyle(isTraktLoggedIn ? .red : .accentColor)
Text(isTraktLoggedIn ? "Log Out from Trakt" : "Log In with Trakt") Text(isTraktLoggedIn ? NSLocalizedString("Log Out from Trakt", comment: "") : NSLocalizedString("Log In with Trakt", comment: ""))
.foregroundStyle(isTraktLoggedIn ? .red : .accentColor) .foregroundStyle(isTraktLoggedIn ? .red : .accentColor)
Spacer() Spacer()
@ -296,14 +304,14 @@ struct SettingsViewTrackers: View {
} }
SettingsSection( SettingsSection(
title: "Info", title: NSLocalizedString("Info", comment: ""),
footer: "Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate." footer: NSLocalizedString("Sora and cranci1 are not affiliated with AniList or Trakt in any way.\n\nAlso note that progress updates may not be 100% accurate.", comment: "")
) {} ) {}
} }
.padding(.vertical, 20) .padding(.vertical, 20)
} }
.scrollViewBottomPadding() .scrollViewBottomPadding()
.navigationTitle("Trackers") .navigationTitle(NSLocalizedString("Trackers", comment: ""))
.onAppear { .onAppear {
updateAniListStatus() updateAniListStatus()
updateTraktStatus() updateTraktStatus()

View file

@ -10,13 +10,13 @@ import NukeUI
fileprivate struct SettingsNavigationRow: View { fileprivate struct SettingsNavigationRow: View {
let icon: String let icon: String
let title: String let titleKey: String
let isExternal: Bool let isExternal: Bool
let textColor: Color let textColor: Color
init(icon: String, title: String, isExternal: Bool = false, textColor: Color = .primary) { init(icon: String, titleKey: String, isExternal: Bool = false, textColor: Color = .primary) {
self.icon = icon self.icon = icon
self.title = title self.titleKey = titleKey
self.isExternal = isExternal self.isExternal = isExternal
self.textColor = textColor self.textColor = textColor
} }
@ -27,7 +27,7 @@ fileprivate struct SettingsNavigationRow: View {
.frame(width: 24, height: 24) .frame(width: 24, height: 24)
.foregroundStyle(textColor) .foregroundStyle(textColor)
Text(title) Text(NSLocalizedString(titleKey, comment: ""))
.foregroundStyle(textColor) .foregroundStyle(textColor)
Spacer() Spacer()
@ -164,22 +164,22 @@ struct SettingsView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
NavigationLink(destination: SettingsViewGeneral()) { NavigationLink(destination: SettingsViewGeneral()) {
SettingsNavigationRow(icon: "gearshape", title: "General Settings") SettingsNavigationRow(icon: "gearshape", titleKey: "General Preferences")
} }
Divider().padding(.horizontal, 16) Divider().padding(.horizontal, 16)
NavigationLink(destination: SettingsViewPlayer()) { NavigationLink(destination: SettingsViewPlayer()) {
SettingsNavigationRow(icon: "play.circle", title: "Player Settings") SettingsNavigationRow(icon: "play.circle", titleKey: "Video Player")
} }
Divider().padding(.horizontal, 16) Divider().padding(.horizontal, 16)
NavigationLink(destination: SettingsViewDownloads()) { NavigationLink(destination: SettingsViewDownloads()) {
SettingsNavigationRow(icon: "arrow.down.circle", title: "Download Settings") SettingsNavigationRow(icon: "arrow.down.circle", titleKey: "Download")
} }
Divider().padding(.horizontal, 16) Divider().padding(.horizontal, 16)
NavigationLink(destination: SettingsViewTrackers()) { NavigationLink(destination: SettingsViewTrackers()) {
SettingsNavigationRow(icon: "square.stack.3d.up", title: "Tracking Services") SettingsNavigationRow(icon: "square.stack.3d.up", titleKey: "Trackers")
} }
} }
.background(.ultraThinMaterial) .background(.ultraThinMaterial)
@ -209,12 +209,12 @@ struct SettingsView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
NavigationLink(destination: SettingsViewData()) { NavigationLink(destination: SettingsViewData()) {
SettingsNavigationRow(icon: "folder", title: "Data") SettingsNavigationRow(icon: "folder", titleKey: "Data")
} }
Divider().padding(.horizontal, 16) Divider().padding(.horizontal, 16)
NavigationLink(destination: SettingsViewLogger()) { NavigationLink(destination: SettingsViewLogger()) {
SettingsNavigationRow(icon: "doc.text", title: "Logs") SettingsNavigationRow(icon: "doc.text", titleKey: "Logs")
} }
} }
.background(.ultraThinMaterial) .background(.ultraThinMaterial)
@ -237,21 +237,21 @@ struct SettingsView: View {
} }
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("INFORMATION") Text(NSLocalizedString("INFOS", comment: ""))
.font(.footnote) .font(.footnote)
.foregroundStyle(.gray) .foregroundStyle(.gray)
.padding(.horizontal, 20) .padding(.horizontal, 20)
VStack(spacing: 0) { VStack(spacing: 0) {
NavigationLink(destination: SettingsViewAbout()) { NavigationLink(destination: SettingsViewAbout()) {
SettingsNavigationRow(icon: "info.circle", title: "About Sora") SettingsNavigationRow(icon: "info.circle", titleKey: "About Sora")
} }
Divider().padding(.horizontal, 16) Divider().padding(.horizontal, 16)
Link(destination: URL(string: "https://github.com/cranci1/Sora")!) { Link(destination: URL(string: "https://github.com/cranci1/Sora")!) {
SettingsNavigationRow( SettingsNavigationRow(
icon: "chevron.left.forwardslash.chevron.right", icon: "chevron.left.forwardslash.chevron.right",
title: "Sora GitHub Repository", titleKey: "Sora GitHub Repository",
isExternal: true, isExternal: true,
textColor: .gray textColor: .gray
) )
@ -261,7 +261,7 @@ struct SettingsView: View {
Link(destination: URL(string: "https://discord.gg/x7hppDWFDZ")!) { Link(destination: URL(string: "https://discord.gg/x7hppDWFDZ")!) {
SettingsNavigationRow( SettingsNavigationRow(
icon: "bubble.left.and.bubble.right", icon: "bubble.left.and.bubble.right",
title: "Join Discord Community", titleKey: "Join the Discord",
isExternal: true, isExternal: true,
textColor: .gray textColor: .gray
) )
@ -271,7 +271,7 @@ struct SettingsView: View {
Link(destination: URL(string: "https://github.com/cranci1/Sora/issues")!) { Link(destination: URL(string: "https://github.com/cranci1/Sora/issues")!) {
SettingsNavigationRow( SettingsNavigationRow(
icon: "exclamationmark.circle", icon: "exclamationmark.circle",
title: "Report an Issue on GitHub", titleKey: "Report an Issue",
isExternal: true, isExternal: true,
textColor: .gray textColor: .gray
) )
@ -281,7 +281,7 @@ struct SettingsView: View {
Link(destination: URL(string: "https://github.com/cranci1/Sora/blob/dev/LICENSE")!) { Link(destination: URL(string: "https://github.com/cranci1/Sora/blob/dev/LICENSE")!) {
SettingsNavigationRow( SettingsNavigationRow(
icon: "doc.text", icon: "doc.text",
title: "License (GPLv3.0)", titleKey: "License (GPLv3.0)",
isExternal: true, isExternal: true,
textColor: .gray textColor: .gray
) )
@ -350,6 +350,12 @@ class Settings: ObservableObject {
updateAppearance() updateAppearance()
} }
} }
@Published var selectedLanguage: String {
didSet {
UserDefaults.standard.set(selectedLanguage, forKey: "selectedLanguage")
updateLanguage()
}
}
init() { init() {
self.accentColor = .primary self.accentColor = .primary
@ -359,7 +365,9 @@ class Settings: ObservableObject {
} else { } else {
self.selectedAppearance = .system self.selectedAppearance = .system
} }
self.selectedLanguage = UserDefaults.standard.string(forKey: "selectedLanguage") ?? "English"
updateAppearance() updateAppearance()
updateLanguage()
} }
func updateAccentColor(currentColorScheme: ColorScheme? = nil) { func updateAccentColor(currentColorScheme: ColorScheme? = nil) {
@ -390,4 +398,10 @@ class Settings: ObservableObject {
windowScene.windows.first?.overrideUserInterfaceStyle = .dark windowScene.windows.first?.overrideUserInterfaceStyle = .dark
} }
} }
func updateLanguage() {
let languageCode = selectedLanguage == "Dutch" ? "nl" : "en"
UserDefaults.standard.set([languageCode], forKey: "AppleLanguages")
UserDefaults.standard.synchronize()
}
} }