- AppSettings: snake_case Codable model — argyll_binary_dir, default_instrument (stored, never applied to argv), log_level (nil -> Debug debug / Info release), delta_e thresholds (2.0/5.0), custom_presets, enable_i1pro2_leds, calibration_stale_days 30, default_install_location user, ask_before_overwrite_profile, open_color_panel_after_install - Validation with the exact contract strings; save() refuses invalid settings; corrupt/missing JSON -> defaults; settingsDidChange notification posted on save (for #20) - LogSink: rolling file at ~/Library/Logs/com.gronod.iccery2/ iccery.log, 5 MiB x 5 segments, runtime setLevel applied at startup and on save (#158); AppLogger gates os_log+file through it - SettingsView sheet: Argyll dir picker, instrument (display-only caveat), i1Pro2 LEDs, ΔE fields + inline errors, stale days, install location, overwrite + ColorSync toggles, log level, open-log-folder / copy-path / copy-excerpt - v1 settings path never read; writes atomic via AtomicFileWriter - 13 new tests; 60/60 green Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
69 lines
2.0 KiB
Swift
69 lines
2.0 KiB
Swift
import AppKit
|
|
import Foundation
|
|
import ICCeryCore
|
|
|
|
/// Backs the Settings sheet (issue #5). Load → edit → save with
|
|
/// validation; the log level is applied live via `LogSink` (#158) and a
|
|
/// `settingsDidChange` notification fans out to #20.
|
|
@MainActor
|
|
@Observable
|
|
final class SettingsViewModel {
|
|
|
|
var settings: AppSettings
|
|
var validationErrors: [String] = []
|
|
var savedFlash = false
|
|
|
|
private let store: SettingsStore
|
|
private let sink: LogSink
|
|
|
|
init(store: SettingsStore = SettingsStore(), sink: LogSink = .shared) {
|
|
self.store = store
|
|
self.sink = sink
|
|
self.settings = store.load()
|
|
}
|
|
|
|
/// Persists after validation. Returns false (and shows inline
|
|
/// errors) when the form is invalid.
|
|
@discardableResult
|
|
func save() -> Bool {
|
|
validationErrors = settings.validate()
|
|
guard validationErrors.isEmpty else { return false }
|
|
do {
|
|
try store.save(settings)
|
|
sink.applySettings(settings)
|
|
savedFlash = true
|
|
Task {
|
|
try? await Task.sleep(for: .seconds(1.5))
|
|
savedFlash = false
|
|
}
|
|
return true
|
|
} catch {
|
|
validationErrors = ["Could not save settings: \(error.localizedDescription)"]
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: - Log helpers
|
|
|
|
var logFileURL: URL { AppPaths.logFile }
|
|
|
|
func openLogFolder() {
|
|
try? FileManager.default.createDirectory(
|
|
at: AppPaths.logDir, withIntermediateDirectories: true
|
|
)
|
|
NSWorkspace.shared.selectFile(
|
|
AppPaths.logFile.path, inFileViewerRootedAtPath: AppPaths.logDir.path
|
|
)
|
|
}
|
|
|
|
func copyLogPath() {
|
|
NSPasteboard.general.clearContents()
|
|
NSPasteboard.general.setString(AppPaths.logFile.path, forType: .string)
|
|
}
|
|
|
|
func copyLogExcerpt() {
|
|
NSPasteboard.general.clearContents()
|
|
NSPasteboard.general.setString(sink.tailExcerpt(), forType: .string)
|
|
}
|
|
}
|