// // AppDefaults.swift // NetNewsWire // // Created by Stuart Breckenridge on 1/7/20. // Copyright © 2020 Ranchero Software. All rights reserved. // import Foundation import SwiftUI enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable { case automatic = 0 case light = 1 case dark = 2 var description: String { switch self { case .automatic: return NSLocalizedString("Automatic", comment: "Automatic") case .light: return NSLocalizedString("Light", comment: "Light") case .dark: return NSLocalizedString("Dark", comment: "Dark") } } } enum FontSize: Int { case small = 0 case medium = 1 case large = 2 case veryLarge = 3 } final class AppDefaults: ObservableObject { #if os(macOS) static let store: UserDefaults = UserDefaults.standard #endif #if os(iOS) static let store: UserDefaults = { let appIdentifierPrefix = Bundle.main.object(forInfoDictionaryKey: "AppIdentifierPrefix") as! String let suiteName = "\(appIdentifierPrefix)group.\(Bundle.main.bundleIdentifier!)" return UserDefaults.init(suiteName: suiteName)! }() #endif public static let shared = AppDefaults() private init() {} struct Key { // Shared Defaults static let refreshInterval = "refreshInterval" static let hideDockUnreadCount = "JustinMillerHideDockUnreadCount" static let activeExtensionPointIDs = "activeExtensionPointIDs" static let lastImageCacheFlushDate = "lastImageCacheFlushDate" static let firstRunDate = "firstRunDate" static let lastRefresh = "lastRefresh" static let addWebFeedAccountID = "addWebFeedAccountID" static let addWebFeedFolderName = "addWebFeedFolderName" static let addFolderAccountID = "addFolderAccountID" static let timelineSortDirection = "timelineSortDirection" // iOS Defaults static let userInterfaceColorPalette = "userInterfaceColorPalette" static let timelineGroupByFeed = "timelineGroupByFeed" static let refreshClearsReadArticles = "refreshClearsReadArticles" static let timelineNumberOfLines = "timelineNumberOfLines" static let timelineIconSize = "timelineIconSize" static let articleFullscreenAvailable = "articleFullscreenAvailable" static let articleFullscreenEnabled = "articleFullscreenEnabled" static let confirmMarkAllAsRead = "confirmMarkAllAsRead" // macOS Defaults static let windowState = "windowState" static let sidebarFontSize = "sidebarFontSize" static let timelineFontSize = "timelineFontSize" static let detailFontSize = "detailFontSize" static let openInBrowserInBackground = "openInBrowserInBackground" static let importOPMLAccountID = "importOPMLAccountID" static let exportOPMLAccountID = "exportOPMLAccountID" static let defaultBrowserID = "defaultBrowserID" static let checkForUpdatesAutomatically = "checkForUpdatesAutomatically" static let downloadTestBuilds = "downloadTestBuild" static let sendCrashLogs = "sendCrashLogs" // Hidden macOS Defaults static let showDebugMenu = "ShowDebugMenu" static let timelineShowsSeparators = "CorreiaSeparators" static let showTitleOnMainWindow = "KafasisTitleMode" #if !MAC_APP_STORE static let webInspectorEnabled = "WebInspectorEnabled" static let webInspectorStartsAttached = "__WebInspectorPageGroupLevel1__.WebKit2InspectorStartsAttached" #endif } private static let smallestFontSizeRawValue = FontSize.small.rawValue private static let largestFontSizeRawValue = FontSize.veryLarge.rawValue // MARK: Development Builds let isDeveloperBuild: Bool = { if let dev = Bundle.main.object(forInfoDictionaryKey: "DeveloperEntitlements") as? String, dev == "-dev" { return true } return false }() // MARK: First Run Details var firstRunDate: Date? { set { AppDefaults.store.setValue(newValue, forKey: Key.firstRunDate) objectWillChange.send() } get { AppDefaults.store.object(forKey: Key.firstRunDate) as? Date } } // MARK: Refresh Interval @AppStorage(wrappedValue: 4, Key.refreshInterval, store: store) var interval: Int { didSet { objectWillChange.send() } } var refreshInterval: RefreshInterval { RefreshInterval(rawValue: interval) ?? RefreshInterval.everyHour } // MARK: Dock Badge @AppStorage(wrappedValue: false, Key.hideDockUnreadCount, store: store) var hideDockUnreadCount { didSet { objectWillChange.send() } } // MARK: Color Palette var userInterfaceColorPalette: UserInterfaceColorPalette { get { if let palette = UserInterfaceColorPalette(rawValue: AppDefaults.store.integer(forKey: Key.userInterfaceColorPalette)) { return palette } return .automatic } set { AppDefaults.store.set(newValue.rawValue, forKey: Key.userInterfaceColorPalette) objectWillChange.send() } } // MARK: Feeds & Folders @AppStorage(Key.addWebFeedAccountID, store: store) var addWebFeedAccountID: String? @AppStorage(Key.addWebFeedFolderName, store: store) var addWebFeedFolderName: String? @AppStorage(Key.addFolderAccountID, store: store) var addFolderAccountID: String? @AppStorage(wrappedValue: false, Key.confirmMarkAllAsRead, store: store) var confirmMarkAllAsRead: Bool // MARK: Extension Points var activeExtensionPointIDs: [[AnyHashable : AnyHashable]]? { get { return AppDefaults.store.object(forKey: Key.activeExtensionPointIDs) as? [[AnyHashable : AnyHashable]] } set { UserDefaults.standard.set(newValue, forKey: Key.activeExtensionPointIDs) objectWillChange.send() } } // MARK: Image Cache var lastImageCacheFlushDate: Date? { set { AppDefaults.store.setValue(newValue, forKey: Key.lastImageCacheFlushDate) objectWillChange.send() } get { AppDefaults.store.object(forKey: Key.lastImageCacheFlushDate) as? Date } } // MARK: Timeline @AppStorage(wrappedValue: false, Key.timelineGroupByFeed, store: store) var timelineGroupByFeed: Bool @AppStorage(wrappedValue: 3, Key.timelineNumberOfLines, store: store) var timelineNumberOfLines: Int { didSet { objectWillChange.send() } } @AppStorage(wrappedValue: 40.0, Key.timelineIconSize, store: store) var timelineIconSize: Double { didSet { objectWillChange.send() } } /// Set to `true` to sort oldest to newest, `false` for newest to oldest. Default is `false`. @AppStorage(wrappedValue: false, Key.timelineSortDirection, store: store) var timelineSortDirection: Bool // MARK: Refresh @AppStorage(wrappedValue: false, Key.refreshClearsReadArticles, store: store) var refreshClearsReadArticles: Bool // MARK: Articles @AppStorage(wrappedValue: false, Key.articleFullscreenAvailable, store: store) var articleFullscreenAvailable: Bool // MARK: Refresh var lastRefresh: Date? { set { AppDefaults.store.setValue(newValue, forKey: Key.lastRefresh) objectWillChange.send() } get { AppDefaults.store.object(forKey: Key.lastRefresh) as? Date } } // MARK: Window State var windowState: [AnyHashable : Any]? { get { return AppDefaults.store.object(forKey: Key.windowState) as? [AnyHashable : Any] } set { UserDefaults.standard.set(newValue, forKey: Key.windowState) objectWillChange.send() } } @AppStorage(wrappedValue: false, Key.openInBrowserInBackground, store: store) var openInBrowserInBackground: Bool { didSet { objectWillChange.send() } } var sidebarFontSize: FontSize { get { return fontSize(for: Key.sidebarFontSize) } set { AppDefaults.store.set(newValue.rawValue, forKey: Key.sidebarFontSize) objectWillChange.send() } } var timelineFontSize: FontSize { get { return fontSize(for: Key.timelineFontSize) } set { AppDefaults.store.set(newValue.rawValue, forKey: Key.timelineFontSize) objectWillChange.send() } } var detailFontSize: FontSize { get { return fontSize(for: Key.detailFontSize) } set { AppDefaults.store.set(newValue.rawValue, forKey: Key.detailFontSize) objectWillChange.send() } } @AppStorage(Key.importOPMLAccountID, store: store) var importOPMLAccountID: String? { didSet { objectWillChange.send() } } @AppStorage(Key.exportOPMLAccountID, store: store) var exportOPMLAccountID: String? { didSet { objectWillChange.send() } } @AppStorage(Key.defaultBrowserID, store: store) var defaultBrowserID: String? { didSet { objectWillChange.send() } } @AppStorage(Key.showTitleOnMainWindow, store: store) var showTitleOnMainWindow: Bool? { didSet { objectWillChange.send() } } @AppStorage(wrappedValue: false, Key.showDebugMenu, store: store) var showDebugMenu: Bool { didSet { objectWillChange.send() } } @AppStorage(wrappedValue: false, Key.timelineShowsSeparators, store: store) var timelineShowsSeparators: Bool { didSet { objectWillChange.send() } } #if !MAC_APP_STORE @AppStorage(wrappedValue: false, Key.webInspectorEnabled, store: store) var webInspectorEnabled: Bool { didSet { objectWillChange.send() } } @AppStorage(wrappedValue: false, Key.webInspectorStartsAttached, store: store) var webInspectorStartsAttached: Bool { didSet { objectWillChange.send() } } #endif @AppStorage(wrappedValue: true, Key.checkForUpdatesAutomatically, store: store) var checkForUpdatesAutomatically: Bool { didSet { objectWillChange.send() } } @AppStorage(wrappedValue: false, Key.downloadTestBuilds, store: store) var downloadTestBuilds: Bool { didSet { objectWillChange.send() } } @AppStorage(wrappedValue: true, Key.sendCrashLogs, store: store) var sendCrashLogs: Bool { didSet { objectWillChange.send() } } } extension AppDefaults { func isFirstRun() -> Bool { if let _ = AppDefaults.store.object(forKey: Key.firstRunDate) as? Date { return false } firstRunDate = Date() return true } func fontSize(for key: String) -> FontSize { // Punted till after 1.0. return .medium } }