mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
feat: add app theming engine with Look and Feel settings
This commit is contained in:
@@ -65,6 +65,10 @@ struct DownloadProxyConfiguration: Equatable, Sendable {
|
||||
|
||||
@MainActor
|
||||
final class AppSettings: ObservableObject {
|
||||
@Published var appTheme: AppTheme = .system {
|
||||
didSet { save() }
|
||||
}
|
||||
|
||||
@Published var perServerConnections: Int {
|
||||
didSet {
|
||||
let clamped = min(max(perServerConnections, 1), 16)
|
||||
@@ -129,6 +133,7 @@ final class AppSettings: ObservableObject {
|
||||
|
||||
if let data = defaults.data(forKey: storageKey),
|
||||
let stored = try? JSONDecoder().decode(StoredSettings.self, from: data) {
|
||||
appTheme = stored.appTheme ?? .system
|
||||
perServerConnections = min(max(stored.perServerConnections, 1), 16)
|
||||
maxConcurrentDownloads = min(max(stored.maxConcurrentDownloads ?? 3, 1), 12)
|
||||
globalSpeedLimitKiBPerSecond = min(max(stored.globalSpeedLimitKiBPerSecond ?? 0, 0), 10_485_760)
|
||||
@@ -137,6 +142,7 @@ final class AppSettings: ObservableObject {
|
||||
siteLogins = stored.siteLogins
|
||||
downloadDirectories = Self.decodeDirectories(stored.downloadDirectories)
|
||||
} else {
|
||||
appTheme = .system
|
||||
perServerConnections = 16
|
||||
maxConcurrentDownloads = 3
|
||||
globalSpeedLimitKiBPerSecond = 0
|
||||
@@ -218,6 +224,7 @@ final class AppSettings: ObservableObject {
|
||||
|
||||
private func save() {
|
||||
let stored = StoredSettings(
|
||||
appTheme: appTheme,
|
||||
perServerConnections: perServerConnections,
|
||||
maxConcurrentDownloads: maxConcurrentDownloads,
|
||||
globalSpeedLimitKiBPerSecond: globalSpeedLimitKiBPerSecond,
|
||||
@@ -277,6 +284,7 @@ final class AppSettings: ObservableObject {
|
||||
}
|
||||
|
||||
private struct StoredSettings: Codable {
|
||||
var appTheme: AppTheme?
|
||||
var perServerConnections: Int
|
||||
var maxConcurrentDownloads: Int?
|
||||
var globalSpeedLimitKiBPerSecond: Int?
|
||||
|
||||
@@ -3,6 +3,7 @@ import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject private var controller: DownloadController
|
||||
@EnvironmentObject private var settings: AppSettings
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
@State private var selection: Set<DownloadItem.ID> = []
|
||||
@State private var sidebarSelection: SidebarSelection = .downloads(.all)
|
||||
@@ -12,8 +13,12 @@ struct ContentView: View {
|
||||
NavigationSplitView {
|
||||
SidebarView(selection: $sidebarSelection)
|
||||
.navigationSplitViewColumnWidth(min: 190, ideal: 220, max: 260)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(settings.appTheme.theme.secondaryBackground ?? Color(NSColor.windowBackgroundColor))
|
||||
} detail: {
|
||||
detailView
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(settings.appTheme.theme.background ?? Color(NSColor.windowBackgroundColor))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ struct FirelinkApp: App {
|
||||
.environmentObject(controller)
|
||||
.environmentObject(settings)
|
||||
.environmentObject(schedulerController)
|
||||
.modifier(AppThemeModifier(theme: settings.appTheme))
|
||||
.frame(minWidth: 1180, idealWidth: 1280, minHeight: 720, idealHeight: 760)
|
||||
}
|
||||
.windowStyle(.titleBar)
|
||||
@@ -28,6 +29,7 @@ struct FirelinkApp: App {
|
||||
AddDownloadsView()
|
||||
.environmentObject(controller)
|
||||
.environmentObject(settings)
|
||||
.modifier(AppThemeModifier(theme: settings.appTheme))
|
||||
}
|
||||
.windowResizability(.contentSize)
|
||||
|
||||
@@ -36,8 +38,10 @@ struct FirelinkApp: App {
|
||||
DownloadPropertiesWindow(downloadID: downloadID)
|
||||
.environmentObject(controller)
|
||||
.environmentObject(settings)
|
||||
.modifier(AppThemeModifier(theme: settings.appTheme))
|
||||
} else {
|
||||
ContentUnavailableView("Download Not Found", systemImage: "questionmark.circle")
|
||||
.modifier(AppThemeModifier(theme: settings.appTheme))
|
||||
}
|
||||
}
|
||||
.windowResizability(.contentSize)
|
||||
|
||||
@@ -4,6 +4,7 @@ import SwiftUI
|
||||
private enum SettingsSection: String, CaseIterable, Hashable {
|
||||
case downloads = "Downloads"
|
||||
case locations = "Locations"
|
||||
case lookAndFeel = "Look and feel"
|
||||
case network = "Network"
|
||||
case siteLogins = "Site Logins"
|
||||
case power = "Power"
|
||||
@@ -13,6 +14,7 @@ private enum SettingsSection: String, CaseIterable, Hashable {
|
||||
static let orderedCases: [SettingsSection] = [
|
||||
.downloads,
|
||||
.locations,
|
||||
.lookAndFeel,
|
||||
.network,
|
||||
.siteLogins,
|
||||
.power,
|
||||
@@ -23,6 +25,7 @@ private enum SettingsSection: String, CaseIterable, Hashable {
|
||||
var symbolName: String {
|
||||
switch self {
|
||||
case .downloads: "arrow.down.circle"
|
||||
case .lookAndFeel: "paintpalette"
|
||||
case .network: "network"
|
||||
case .locations: "folder"
|
||||
case .siteLogins: "key.fill"
|
||||
@@ -43,6 +46,7 @@ private enum SettingsSection: String, CaseIterable, Hashable {
|
||||
}
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject private var settings: AppSettings
|
||||
@State private var selection: SettingsSection = .downloads
|
||||
|
||||
var body: some View {
|
||||
@@ -69,6 +73,7 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.background(settings.appTheme.theme.background ?? Color(NSColor.windowBackgroundColor))
|
||||
}
|
||||
|
||||
private var settingsSidebar: some View {
|
||||
@@ -96,6 +101,8 @@ struct SettingsView: View {
|
||||
switch selection {
|
||||
case .downloads:
|
||||
DownloadSettingsPane()
|
||||
case .lookAndFeel:
|
||||
LookAndFeelSettingsPane()
|
||||
case .network:
|
||||
NetworkSettingsPane()
|
||||
case .locations:
|
||||
@@ -112,6 +119,29 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct LookAndFeelSettingsPane: View {
|
||||
@EnvironmentObject private var settings: AppSettings
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section("App Theme") {
|
||||
Picker("Theme", selection: $settings.appTheme) {
|
||||
ForEach(AppTheme.allCases) { theme in
|
||||
Text(theme.rawValue)
|
||||
.tag(theme)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.radioGroup)
|
||||
|
||||
Text("Select a color palette for the app's user interface.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
}
|
||||
}
|
||||
|
||||
private struct AboutSettingsPane: View {
|
||||
@StateObject private var updateChecker = AppUpdateChecker()
|
||||
@State private var availableUpdate: AvailableUpdate?
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import SwiftUI
|
||||
|
||||
enum AppTheme: String, Codable, CaseIterable, Identifiable, Sendable {
|
||||
case system = "System Default"
|
||||
case light = "Light"
|
||||
case dark = "Modern Dark"
|
||||
case dracula = "Dracula"
|
||||
case nord = "Nord"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var theme: Theme {
|
||||
switch self {
|
||||
case .system:
|
||||
return Theme.system
|
||||
case .light:
|
||||
return Theme.modernLight
|
||||
case .dark:
|
||||
return Theme.modernDark
|
||||
case .dracula:
|
||||
return Theme.dracula
|
||||
case .nord:
|
||||
return Theme.nord
|
||||
}
|
||||
}
|
||||
|
||||
var colorScheme: ColorScheme? {
|
||||
switch self {
|
||||
case .system: return nil
|
||||
case .light: return .light
|
||||
case .dark, .dracula, .nord: return .dark
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Theme: Equatable, Sendable {
|
||||
var background: Color?
|
||||
var secondaryBackground: Color?
|
||||
var text: Color?
|
||||
var secondaryText: Color?
|
||||
var accent: Color?
|
||||
|
||||
static let system = Theme(
|
||||
background: nil,
|
||||
secondaryBackground: nil,
|
||||
text: nil,
|
||||
secondaryText: nil,
|
||||
accent: nil
|
||||
)
|
||||
|
||||
static let modernLight = Theme(
|
||||
background: Color(nsColor: NSColor(calibratedRed: 0.98, green: 0.98, blue: 0.99, alpha: 1.0)),
|
||||
secondaryBackground: Color(nsColor: NSColor(calibratedRed: 0.94, green: 0.94, blue: 0.96, alpha: 1.0)),
|
||||
text: Color.primary,
|
||||
secondaryText: Color.secondary,
|
||||
accent: Color.accentColor
|
||||
)
|
||||
|
||||
static let modernDark = Theme(
|
||||
background: Color(nsColor: NSColor(calibratedRed: 0.08, green: 0.08, blue: 0.10, alpha: 1.0)),
|
||||
secondaryBackground: Color(nsColor: NSColor(calibratedRed: 0.12, green: 0.12, blue: 0.14, alpha: 1.0)),
|
||||
text: Color(nsColor: NSColor(calibratedRed: 0.9, green: 0.9, blue: 0.95, alpha: 1.0)),
|
||||
secondaryText: Color(nsColor: NSColor(calibratedRed: 0.6, green: 0.6, blue: 0.65, alpha: 1.0)),
|
||||
accent: Color(nsColor: NSColor(calibratedRed: 0.35, green: 0.55, blue: 0.95, alpha: 1.0))
|
||||
)
|
||||
|
||||
static let dracula = Theme(
|
||||
background: Color(nsColor: NSColor(calibratedRed: 0.16, green: 0.16, blue: 0.21, alpha: 1.0)),
|
||||
secondaryBackground: Color(nsColor: NSColor(calibratedRed: 0.27, green: 0.28, blue: 0.35, alpha: 1.0)),
|
||||
text: Color(nsColor: NSColor(calibratedRed: 0.97, green: 0.97, blue: 0.95, alpha: 1.0)),
|
||||
secondaryText: Color(nsColor: NSColor(calibratedRed: 0.38, green: 0.44, blue: 0.58, alpha: 1.0)),
|
||||
accent: Color(nsColor: NSColor(calibratedRed: 1.00, green: 0.47, blue: 0.65, alpha: 1.0)) // Pink
|
||||
)
|
||||
|
||||
static let nord = Theme(
|
||||
background: Color(nsColor: NSColor(calibratedRed: 0.18, green: 0.20, blue: 0.25, alpha: 1.0)), // nord0
|
||||
secondaryBackground: Color(nsColor: NSColor(calibratedRed: 0.23, green: 0.26, blue: 0.32, alpha: 1.0)), // nord1
|
||||
text: Color(nsColor: NSColor(calibratedRed: 0.85, green: 0.87, blue: 0.91, alpha: 1.0)), // nord4
|
||||
secondaryText: Color(nsColor: NSColor(calibratedRed: 0.57, green: 0.63, blue: 0.70, alpha: 1.0)), // nord3
|
||||
accent: Color(nsColor: NSColor(calibratedRed: 0.53, green: 0.75, blue: 0.82, alpha: 1.0)) // nord8
|
||||
)
|
||||
}
|
||||
|
||||
struct AppThemeModifier: ViewModifier {
|
||||
let theme: AppTheme
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.preferredColorScheme(theme.colorScheme)
|
||||
.tint(theme.theme.accent)
|
||||
// Apply custom background only if it's not system.
|
||||
// Using .background() on the root view might not cover the whole window.
|
||||
// In macOS, it's often better to just set the color scheme and tint,
|
||||
// and let specific views (like Sidebar, Content) use the theme colors explicitly.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user