aaffa3771c
Two-target restructure: shared sources (models, services, settings, extensions, team logos) move into Shared/, consumed by both the existing macOS menu bar app and a new iOS app. MainService no longer imports AppKit — platform code attaches via a MainServiceObserver protocol (MacObserverAdapter wires back to MenuManager / StatusItemManager / NotificationManager). iPhone app is a single SwiftUI page mirroring the macOS menu (playoff round + yesterday/today/tomorrow), with a gear-icon settings sheet (display option + IndieAbout for license/changelog). Persistent JSON snapshot in Application Support paints last-known data on cold launch; "Updated …" header escalates secondary → orange (>5min) → red (>30min) so staleness is visually unmistakable. Foreground polling, scenePhase refresh, and pull-to-refresh; no notifications on iOS in v1.
114 lines
3.4 KiB
Swift
114 lines
3.4 KiB
Swift
//
|
|
// AppSettings.swift
|
|
// IceGlass
|
|
//
|
|
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
|
|
//
|
|
|
|
import Foundation
|
|
#if os(macOS)
|
|
import ServiceManagement
|
|
#endif
|
|
|
|
class AppSettings: @unchecked Sendable {
|
|
private let logger = IceGlassLogger(
|
|
subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.IceGlass",
|
|
category: "AppSettings"
|
|
)
|
|
|
|
static let shared = AppSettings()
|
|
|
|
private enum UserDefaultsKey {
|
|
static let launchAtLogin = "launchAtLogin"
|
|
static let displayOption = "displayOption"
|
|
static let statusBarOption = "statusBarOption"
|
|
}
|
|
|
|
/// Controls which days are shown in the menu
|
|
enum DisplayOption: String, CaseIterable {
|
|
case yesterdayTodayTomorrow = "yesterdayTodayTomorrow"
|
|
case todayTomorrow = "todayTomorrow"
|
|
case todayOnly = "todayOnly"
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .yesterdayTodayTomorrow: return "Yesterday / Today / Tomorrow"
|
|
case .todayTomorrow: return "Today / Tomorrow"
|
|
case .todayOnly: return "Today"
|
|
}
|
|
}
|
|
|
|
func includedDates() -> Set<String> {
|
|
switch self {
|
|
case .yesterdayTodayTomorrow:
|
|
return [Date.yesterdayET, Date.todayET, Date.tomorrowET]
|
|
case .todayTomorrow:
|
|
return [Date.todayET, Date.tomorrowET]
|
|
case .todayOnly:
|
|
return [Date.todayET]
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Controls what number shows next to the menu bar icon
|
|
enum StatusBarOption: String, CaseIterable {
|
|
case gameCount = "gameCount"
|
|
case gamesPlayed = "gamesPlayed"
|
|
case gamesPlayedTotal = "gamesPlayedTotal"
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .gameCount: return "Game Count"
|
|
case .gamesPlayed: return "Games Played"
|
|
case .gamesPlayedTotal: return "Games Played / Total"
|
|
}
|
|
}
|
|
}
|
|
|
|
// Launch at login
|
|
var launchAtLogin: Bool {
|
|
get { UserDefaults.standard.bool(forKey: UserDefaultsKey.launchAtLogin) }
|
|
set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKey.launchAtLogin) }
|
|
}
|
|
|
|
// Display option
|
|
var displayOption: DisplayOption {
|
|
get {
|
|
if let rawValue = UserDefaults.standard.string(forKey: UserDefaultsKey.displayOption),
|
|
let option = DisplayOption(rawValue: rawValue) {
|
|
return option
|
|
}
|
|
return .yesterdayTodayTomorrow
|
|
}
|
|
set { UserDefaults.standard.set(newValue.rawValue, forKey: UserDefaultsKey.displayOption) }
|
|
}
|
|
|
|
// Status bar option
|
|
var statusBarOption: StatusBarOption {
|
|
get {
|
|
if let rawValue = UserDefaults.standard.string(forKey: UserDefaultsKey.statusBarOption),
|
|
let option = StatusBarOption(rawValue: rawValue) {
|
|
return option
|
|
}
|
|
return .gameCount
|
|
}
|
|
set { UserDefaults.standard.set(newValue.rawValue, forKey: UserDefaultsKey.statusBarOption) }
|
|
}
|
|
|
|
func updateLoginItem(enabled: Bool) {
|
|
#if os(macOS)
|
|
do {
|
|
if enabled {
|
|
try SMAppService.mainApp.register()
|
|
} else {
|
|
try SMAppService.mainApp.unregister()
|
|
}
|
|
} catch {
|
|
logger.error("Failed to \(enabled ? "enable" : "disable") launch at login: \(error.localizedDescription)")
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private init() {}
|
|
}
|