mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
535 lines
23 KiB
Swift
535 lines
23 KiB
Swift
//
|
|
// SettingsView.swift
|
|
// Sora
|
|
//
|
|
// Created by Francesco on 05/01/25.
|
|
//
|
|
|
|
import SwiftUI
|
|
import NukeUI
|
|
|
|
fileprivate struct SettingsNavigationRow: View {
|
|
let icon: String
|
|
let titleKey: String
|
|
let isExternal: Bool
|
|
let textColor: Color
|
|
|
|
init(icon: String, titleKey: String, isExternal: Bool = false, textColor: Color = .primary) {
|
|
self.icon = icon
|
|
self.titleKey = titleKey
|
|
self.isExternal = isExternal
|
|
self.textColor = textColor
|
|
}
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Image(systemName: icon)
|
|
.frame(width: 24, height: 24)
|
|
.foregroundStyle(textColor)
|
|
|
|
Text(NSLocalizedString(titleKey, comment: ""))
|
|
.foregroundStyle(textColor)
|
|
|
|
Spacer()
|
|
|
|
if isExternal {
|
|
Image(systemName: "safari")
|
|
.foregroundStyle(.gray)
|
|
} else {
|
|
Image(systemName: "chevron.right")
|
|
.foregroundStyle(.gray)
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 12)
|
|
}
|
|
}
|
|
|
|
fileprivate struct ModulePreviewRow: View {
|
|
@EnvironmentObject var moduleManager: ModuleManager
|
|
@AppStorage("selectedModuleId") private var selectedModuleId: String?
|
|
|
|
private var selectedModule: ScrapingModule? {
|
|
guard let id = selectedModuleId else { return nil }
|
|
return moduleManager.modules.first { $0.id.uuidString == id }
|
|
}
|
|
|
|
var body: some View {
|
|
HStack(spacing: 16) {
|
|
if let module = selectedModule {
|
|
LazyImage(url: URL(string: module.metadata.iconUrl)) { state in
|
|
if let uiImage = state.imageContainer?.image {
|
|
Image(uiImage: uiImage)
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(width: 60, height: 60)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
} else {
|
|
Image(systemName: "cube")
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(width: 60, height: 60)
|
|
.foregroundStyle(Color.accentColor)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(module.metadata.sourceName)
|
|
.font(.headline)
|
|
.foregroundStyle(.primary)
|
|
|
|
Text("Tap to manage your modules")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.gray)
|
|
.lineLimit(1)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
} else {
|
|
Image(systemName: "cube")
|
|
.font(.system(size: 36))
|
|
.foregroundStyle(Color.accentColor)
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("No Module Selected")
|
|
.font(.headline)
|
|
.foregroundStyle(.primary)
|
|
|
|
Text("Tap to select a module")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.gray)
|
|
.lineLimit(1)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
Image(systemName: "chevron.right")
|
|
.foregroundStyle(.gray)
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 16)
|
|
.background(.ultraThinMaterial)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.strokeBorder(
|
|
LinearGradient(
|
|
gradient: Gradient(stops: [
|
|
.init(color: Color.accentColor.opacity(0.3), location: 0),
|
|
.init(color: Color.accentColor.opacity(0), location: 1)
|
|
]),
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
),
|
|
lineWidth: 0.5
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
struct SettingsView: View {
|
|
@Environment(\.colorScheme) var colorScheme
|
|
@StateObject var settings = Settings()
|
|
@EnvironmentObject var moduleManager: ModuleManager
|
|
|
|
@State private var isNavigationActive = false
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
ScrollView(showsIndicators: false) {
|
|
VStack(spacing: 24) {
|
|
Text("Settings")
|
|
.font(.largeTitle)
|
|
.fontWeight(.bold)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal, 20)
|
|
.padding(.top, 16)
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("MODULES")
|
|
.font(.footnote)
|
|
.foregroundStyle(.gray)
|
|
.padding(.horizontal, 20)
|
|
|
|
NavigationLink(destination: SettingsViewModule().navigationBarBackButtonHidden(false)) {
|
|
ModulePreviewRow()
|
|
}
|
|
.padding(.horizontal, 20)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("MAIN SETTINGS")
|
|
.font(.footnote)
|
|
.foregroundStyle(.gray)
|
|
.padding(.horizontal, 20)
|
|
|
|
VStack(spacing: 0) {
|
|
NavigationLink(destination: SettingsViewGeneral().navigationBarBackButtonHidden(false)) {
|
|
SettingsNavigationRow(icon: "gearshape", titleKey: "General Preferences")
|
|
}
|
|
Divider().padding(.horizontal, 16)
|
|
|
|
NavigationLink(destination: SettingsViewLibrary().navigationBarBackButtonHidden(false)) {
|
|
SettingsNavigationRow(icon: "books.vertical", titleKey: "Library")
|
|
}
|
|
Divider().padding(.horizontal, 16)
|
|
|
|
NavigationLink(destination: SettingsViewPlayer().navigationBarBackButtonHidden(false)) {
|
|
SettingsNavigationRow(icon: "play.circle", titleKey: "Video Player")
|
|
}
|
|
Divider().padding(.horizontal, 16)
|
|
|
|
NavigationLink(destination: SettingsViewDownloads().navigationBarBackButtonHidden(false)) {
|
|
SettingsNavigationRow(icon: "arrow.down.circle", titleKey: "Downloads")
|
|
}
|
|
Divider().padding(.horizontal, 16)
|
|
|
|
NavigationLink(destination: SettingsViewTrackers().navigationBarBackButtonHidden(false)) {
|
|
SettingsNavigationRow(icon: "square.3.stack.3d", titleKey: "Trackers")
|
|
}
|
|
}
|
|
.background(.ultraThinMaterial)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.strokeBorder(
|
|
LinearGradient(
|
|
gradient: Gradient(stops: [
|
|
.init(color: Color.accentColor.opacity(0.3), location: 0),
|
|
.init(color: Color.accentColor.opacity(0), location: 1)
|
|
]),
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
),
|
|
lineWidth: 0.5
|
|
)
|
|
)
|
|
.padding(.horizontal, 20)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("DATA & LOGS")
|
|
.font(.footnote)
|
|
.foregroundStyle(.gray)
|
|
.padding(.horizontal, 20)
|
|
|
|
VStack(spacing: 0) {
|
|
NavigationLink(destination: SettingsViewData().navigationBarBackButtonHidden(false)) {
|
|
SettingsNavigationRow(icon: "folder", titleKey: "Data")
|
|
}
|
|
Divider().padding(.horizontal, 16)
|
|
|
|
NavigationLink(destination: SettingsViewLogger().navigationBarBackButtonHidden(false)) {
|
|
SettingsNavigationRow(icon: "doc.text", titleKey: "Logs")
|
|
}
|
|
Divider().padding(.horizontal, 16)
|
|
|
|
NavigationLink(destination: SettingsViewBackup().navigationBarBackButtonHidden(false)) {
|
|
SettingsNavigationRow(icon: "arrow.triangle.2.circlepath", titleKey: NSLocalizedString("Backup & Restore", comment: "Settings navigation row for backup and restore"))
|
|
}
|
|
}
|
|
.background(.ultraThinMaterial)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.strokeBorder(
|
|
LinearGradient(
|
|
gradient: Gradient(stops: [
|
|
.init(color: Color.accentColor.opacity(0.3), location: 0),
|
|
.init(color: Color.accentColor.opacity(0), location: 1)
|
|
]),
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
),
|
|
lineWidth: 0.5
|
|
)
|
|
)
|
|
.padding(.horizontal, 20)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(NSLocalizedString("INFOS", comment: ""))
|
|
.font(.footnote)
|
|
.foregroundStyle(.gray)
|
|
.padding(.horizontal, 20)
|
|
|
|
VStack(spacing: 0) {
|
|
NavigationLink(destination: SettingsViewAbout().navigationBarBackButtonHidden(false)) {
|
|
SettingsNavigationRow(icon: "info.circle", titleKey: "About Sora")
|
|
}
|
|
Divider().padding(.horizontal, 16)
|
|
|
|
Link(destination: URL(string: "https://github.com/cranci1/Sora")!) {
|
|
HStack {
|
|
Image("Github Icon")
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(width: 20, height: 20)
|
|
.padding(.leading, 2)
|
|
.padding(.trailing, 4)
|
|
|
|
Text(NSLocalizedString("Sora GitHub Repository", comment: ""))
|
|
.foregroundStyle(.gray)
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "safari")
|
|
.foregroundStyle(.gray)
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 12)
|
|
}
|
|
Divider().padding(.horizontal, 16)
|
|
|
|
Link(destination: URL(string: "https://discord.gg/x7hppDWFDZ")!) {
|
|
HStack {
|
|
Image("Discord Icon")
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(width: 20, height: 20)
|
|
.padding(.leading, 2)
|
|
.padding(.trailing, 4)
|
|
|
|
Text(NSLocalizedString("Join the Discord", comment: ""))
|
|
.foregroundStyle(.gray)
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "safari")
|
|
.foregroundStyle(.gray)
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 12)
|
|
}
|
|
Divider().padding(.horizontal, 16)
|
|
|
|
Link(destination: URL(string: "https://github.com/cranci1/Sora/issues")!) {
|
|
SettingsNavigationRow(
|
|
icon: "exclamationmark.circle.fill",
|
|
titleKey: "Report an Issue",
|
|
isExternal: true,
|
|
textColor: .gray
|
|
)
|
|
}
|
|
Divider().padding(.horizontal, 16)
|
|
|
|
Link(destination: URL(string: "https://github.com/cranci1/Sora/blob/dev/LICENSE")!) {
|
|
SettingsNavigationRow(
|
|
icon: "doc.text.fill",
|
|
titleKey: "License (GPLv3.0)",
|
|
isExternal: true,
|
|
textColor: .gray
|
|
)
|
|
}
|
|
}
|
|
.background(.ultraThinMaterial)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.strokeBorder(
|
|
LinearGradient(
|
|
gradient: Gradient(stops: [
|
|
.init(color: Color.accentColor.opacity(0.3), location: 0),
|
|
.init(color: Color.accentColor.opacity(0), location: 1)
|
|
]),
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
),
|
|
lineWidth: 0.5
|
|
)
|
|
)
|
|
.padding(.horizontal, 20)
|
|
}
|
|
|
|
Text("Sora 1.0.1 by cranci1")
|
|
.font(.footnote)
|
|
.foregroundStyle(.gray)
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
.padding(.top, 8)
|
|
}
|
|
.scrollViewBottomPadding()
|
|
.padding(.bottom, 20)
|
|
}
|
|
.deviceScaled()
|
|
}
|
|
.navigationViewStyle(StackNavigationViewStyle())
|
|
.navigationBarHidden(true)
|
|
.onChange(of: colorScheme) { newScheme in
|
|
if settings.selectedAppearance == .system {
|
|
settings.updateAccentColor(currentColorScheme: newScheme)
|
|
}
|
|
}
|
|
.onChange(of: settings.selectedAppearance) { _ in
|
|
settings.updateAccentColor(currentColorScheme: colorScheme)
|
|
}
|
|
.onAppear {
|
|
settings.updateAccentColor(currentColorScheme: colorScheme)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum Appearance: String, CaseIterable, Identifiable {
|
|
case system, light, dark
|
|
|
|
var id: String { self.rawValue }
|
|
}
|
|
|
|
class Settings: ObservableObject {
|
|
@Published var accentColor: Color {
|
|
didSet {
|
|
}
|
|
}
|
|
@Published var selectedAppearance: Appearance {
|
|
didSet {
|
|
UserDefaults.standard.set(selectedAppearance.rawValue, forKey: "selectedAppearance")
|
|
updateAppearance()
|
|
}
|
|
}
|
|
@Published var selectedLanguage: String {
|
|
didSet {
|
|
UserDefaults.standard.set(selectedLanguage, forKey: "selectedLanguage")
|
|
updateLanguage()
|
|
}
|
|
}
|
|
|
|
init() {
|
|
self.accentColor = .primary
|
|
if let appearanceRawValue = UserDefaults.standard.string(forKey: "selectedAppearance"),
|
|
let appearance = Appearance(rawValue: appearanceRawValue) {
|
|
self.selectedAppearance = appearance
|
|
} else {
|
|
self.selectedAppearance = .system
|
|
}
|
|
self.selectedLanguage = UserDefaults.standard.string(forKey: "selectedLanguage") ?? "English"
|
|
updateAppearance()
|
|
updateLanguage()
|
|
}
|
|
|
|
func updateAccentColor(currentColorScheme: ColorScheme? = nil) {
|
|
switch selectedAppearance {
|
|
case .system:
|
|
if let scheme = currentColorScheme {
|
|
accentColor = scheme == .dark ? .white : .black
|
|
} else {
|
|
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
let window = windowScene.windows.first else { return }
|
|
accentColor = window.traitCollection.userInterfaceStyle == .dark ? .white : .black
|
|
}
|
|
case .light:
|
|
accentColor = .black
|
|
case .dark:
|
|
accentColor = .white
|
|
}
|
|
}
|
|
|
|
func updateAppearance() {
|
|
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
|
|
switch selectedAppearance {
|
|
case .system:
|
|
windowScene.windows.first?.overrideUserInterfaceStyle = .unspecified
|
|
case .light:
|
|
windowScene.windows.first?.overrideUserInterfaceStyle = .light
|
|
case .dark:
|
|
windowScene.windows.first?.overrideUserInterfaceStyle = .dark
|
|
}
|
|
}
|
|
|
|
func updateLanguage() {
|
|
let languageCode: String
|
|
switch selectedLanguage {
|
|
case "Dutch":
|
|
languageCode = "nl"
|
|
case "French":
|
|
languageCode = "fr"
|
|
case "German":
|
|
languageCode = "de"
|
|
case "Arabic":
|
|
languageCode = "ar"
|
|
case "Bosnian":
|
|
languageCode = "bos"
|
|
case "Czech":
|
|
languageCode = "cs"
|
|
case "Slovak":
|
|
languageCode = "sk"
|
|
case "Spanish":
|
|
languageCode = "es"
|
|
case "Russian":
|
|
languageCode = "ru"
|
|
case "Norsk":
|
|
languageCode = "nn"
|
|
case "Kazakh":
|
|
languageCode = "kk"
|
|
case "Mongolian":
|
|
languageCode = "mn"
|
|
|
|
let mainBundle = Bundle.main
|
|
if let lprojPaths = mainBundle.paths(forResourcesOfType: "lproj", inDirectory: nil) as? [String] {
|
|
let availableLangs = lprojPaths.map { path -> String in
|
|
let components = path.components(separatedBy: "/")
|
|
let filename = components.last ?? ""
|
|
return filename.replacingOccurrences(of: ".lproj", with: "")
|
|
}
|
|
Logger.shared.log("Available language bundles: \(availableLangs.joined(separator: ", "))", type: "Debug")
|
|
}
|
|
|
|
if let _ = mainBundle.path(forResource: "mn", ofType: "lproj") {
|
|
Logger.shared.log("Found mn.lproj bundle", type: "Debug")
|
|
} else {
|
|
Logger.shared.log("mn.lproj bundle not found", type: "Error")
|
|
}
|
|
case "Romanian":
|
|
languageCode = "ro"
|
|
|
|
let mainBundle = Bundle.main
|
|
if let lprojPaths = mainBundle.paths(forResourcesOfType: "lproj", inDirectory: nil) as? [String] {
|
|
let availableLangs = lprojPaths.map { path -> String in
|
|
let components = path.components(separatedBy: "/")
|
|
let filename = components.last ?? ""
|
|
return filename.replacingOccurrences(of: ".lproj", with: "")
|
|
}
|
|
Logger.shared.log("Available language bundles: \(availableLangs.joined(separator: ", "))", type: "Debug")
|
|
}
|
|
|
|
if let _ = mainBundle.path(forResource: "ro", ofType: "lproj") {
|
|
Logger.shared.log("Found ro.lproj bundle", type: "Debug")
|
|
} else {
|
|
Logger.shared.log("ro.lproj bundle not found", type: "Error")
|
|
}
|
|
case "Swedish":
|
|
languageCode = "sv"
|
|
case "Italian":
|
|
languageCode = "it"
|
|
default:
|
|
languageCode = "en"
|
|
}
|
|
|
|
UserDefaults.standard.set([languageCode], forKey: "AppleLanguages")
|
|
Logger.shared.log("Setting language to: \(languageCode) for \(selectedLanguage)", type: "Debug")
|
|
|
|
UserDefaults.standard.synchronize()
|
|
|
|
LocalizationManager.shared.setLanguage(languageCode)
|
|
|
|
if selectedLanguage == "Mongolian" {
|
|
if let mongolianBundle = Bundle(path: Bundle.main.path(forResource: "mn", ofType: "lproj") ?? "") {
|
|
Logger.shared.log("Mongolian bundle: \(mongolianBundle)", type: "Debug")
|
|
|
|
let testKey = "About"
|
|
let testString = mongolianBundle.localizedString(forKey: testKey, value: nil, table: nil)
|
|
Logger.shared.log("Test Mongolian string for '\(testKey)': \(testString)", type: "Debug")
|
|
}
|
|
}
|
|
|
|
if selectedLanguage == "Romanian" {
|
|
if let romanianBundle = Bundle(path: Bundle.main.path(forResource: "ro", ofType: "lproj") ?? "") {
|
|
Logger.shared.log("Romanian bundle: \(romanianBundle)", type: "Debug")
|
|
|
|
let testKey = "About"
|
|
let testString = romanianBundle.localizedString(forKey: testKey, value: nil, table: nil)
|
|
Logger.shared.log("Test Romanian string for '\(testKey)': \(testString)", type: "Debug")
|
|
}
|
|
}
|
|
}
|
|
}
|