From c2149579c98cbeb3ebd7a6f17a40460b03933fda Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 2 Jul 2020 09:57:36 +0800 Subject: [PATCH] Converts AppDefaults to singleton --- Multiplatform/Shared/AppDefaults.swift | 450 ++++++++++-------- Multiplatform/Shared/AppSettings.swift | 321 ------------- Multiplatform/Shared/MainApp.swift | 4 +- Multiplatform/iOS/AppDelegate.swift | 8 +- Multiplatform/iOS/Settings/SettingsView.swift | 2 +- .../Submenus/TimelineLayoutView.swift | 4 +- Multiplatform/macOS/AppDelegate.swift | 6 +- .../{Views => }/MacPreferencesView.swift | 8 +- .../Preferences/Model/MacPreferences.swift | 96 ---- .../AccountsPreferencesView.swift | 0 .../AdvancedPreferencesView.swift | 2 +- .../View/GeneralPreferencesView.swift | 33 ++ .../Views/GeneralPreferencesView.swift | 33 -- NetNewsWire.xcodeproj/project.pbxproj | 46 +- .../ExtensionPointManager.swift | 6 +- .../AddWebFeedDefaultContainer.swift | 10 +- Shared/Extensions/CacheCleaner.swift | 6 +- Shared/Timer/AccountRefreshTimer.swift | 4 +- Shared/Timer/RefreshInterval.swift | 4 +- 19 files changed, 336 insertions(+), 707 deletions(-) delete mode 100644 Multiplatform/Shared/AppSettings.swift rename Multiplatform/macOS/Preferences/{Views => }/MacPreferencesView.swift (89%) delete mode 100644 Multiplatform/macOS/Preferences/Model/MacPreferences.swift rename Multiplatform/macOS/Preferences/{Views => View}/AccountsPreferencesView.swift (100%) rename Multiplatform/macOS/Preferences/{Views => View}/AdvancedPreferencesView.swift (96%) create mode 100644 Multiplatform/macOS/Preferences/View/GeneralPreferencesView.swift delete mode 100644 Multiplatform/macOS/Preferences/Views/GeneralPreferencesView.swift diff --git a/Multiplatform/Shared/AppDefaults.swift b/Multiplatform/Shared/AppDefaults.swift index 265a16028..1031c7ef0 100644 --- a/Multiplatform/Shared/AppDefaults.swift +++ b/Multiplatform/Shared/AppDefaults.swift @@ -2,290 +2,352 @@ // AppDefaults.swift // NetNewsWire // -// Created by Maurice Parker on 6/28/20. +// 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") + } + } +} -struct AppDefaults { +enum FontSize: Int { + case small = 0 + case medium = 1 + case large = 2 + case veryLarge = 3 +} +final class AppDefaults: ObservableObject { + #if os(macOS) - static var shared: UserDefaults = UserDefaults.standard + static let store: UserDefaults = UserDefaults.standard #endif #if os(iOS) - static var shared: UserDefaults = { + 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 userInterfaceColorPalette = "userInterfaceColorPalette" static let activeExtensionPointIDs = "activeExtensionPointIDs" static let lastImageCacheFlushDate = "lastImageCacheFlushDate" static let firstRunDate = "firstRunDate" - static let timelineGroupByFeed = "timelineGroupByFeed" - static let refreshClearsReadArticles = "refreshClearsReadArticles" - static let timelineNumberOfLines = "timelineNumberOfLines" - static let timelineIconSize = "timelineIconSize" - static let timelineSortDirection = "timelineSortDirection" - static let articleFullscreenAvailable = "articleFullscreenAvailable" - static let articleFullscreenEnabled = "articleFullscreenEnabled" - static let confirmMarkAllAsRead = "confirmMarkAllAsRead" 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 + } - - static let isDeveloperBuild: Bool = { + + 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 }() - - static let isFirstRun: Bool = { - if let _ = AppDefaults.shared.object(forKey: Key.firstRunDate) as? Date { - return false - } - firstRunDate = Date() - return true - }() - static var refreshInterval: RefreshInterval { - get { - let rawValue = UserDefaults.standard.integer(forKey: Key.refreshInterval) - return RefreshInterval(rawValue: rawValue) ?? RefreshInterval.everyHour - } + // MARK: First Run Details + var firstRunDate: Date? { set { - UserDefaults.standard.set(newValue.rawValue, forKey: Key.refreshInterval) + AppDefaults.store.setValue(newValue, forKey: Key.firstRunDate) + objectWillChange.send() + } + get { + AppDefaults.store.object(forKey: Key.firstRunDate) as? Date } } - - static var hideDockUnreadCount: Bool { - return bool(for: Key.hideDockUnreadCount) + + // MARK: Refresh Interval + @AppStorage(wrappedValue: 4, Key.refreshInterval, store: store) var interval: Int { + didSet { + objectWillChange.send() + } } - - static var userInterfaceColorPalette: UserInterfaceColorPalette { + + 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 result = UserInterfaceColorPalette(rawValue: int(for: Key.userInterfaceColorPalette)) { - return result + if let palette = UserInterfaceColorPalette(rawValue: AppDefaults.store.integer(forKey: Key.userInterfaceColorPalette)) { + return palette } return .automatic } set { - setInt(for: Key.userInterfaceColorPalette, newValue.rawValue) - } - } - - static var addWebFeedAccountID: String? { - get { - return string(for: Key.addWebFeedAccountID) - } - set { - setString(for: Key.addWebFeedAccountID, newValue) + AppDefaults.store.set(newValue.rawValue, forKey: Key.userInterfaceColorPalette) + objectWillChange.send() } } - static var addWebFeedFolderName: String? { - get { - return string(for: Key.addWebFeedFolderName) - } - set { - setString(for: Key.addWebFeedFolderName, newValue) - } - } + // MARK: Feeds & Folders + @AppStorage(Key.addWebFeedAccountID, store: store) var addWebFeedAccountID: String? - static var addFolderAccountID: String? { - get { - return string(for: Key.addFolderAccountID) - } - set { - setString(for: Key.addFolderAccountID, newValue) - } - } + @AppStorage(Key.addWebFeedFolderName, store: store) var addWebFeedFolderName: String? - static var activeExtensionPointIDs: [[AnyHashable : AnyHashable]]? { + @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 UserDefaults.standard.object(forKey: Key.activeExtensionPointIDs) as? [[AnyHashable : AnyHashable]] + return AppDefaults.store.object(forKey: Key.activeExtensionPointIDs) as? [[AnyHashable : AnyHashable]] } set { UserDefaults.standard.set(newValue, forKey: Key.activeExtensionPointIDs) + objectWillChange.send() } } - static var lastImageCacheFlushDate: Date? { - get { - return date(for: Key.lastImageCacheFlushDate) - } + // MARK: Image Cache + var lastImageCacheFlushDate: Date? { set { - setDate(for: Key.lastImageCacheFlushDate, newValue) + AppDefaults.store.setValue(newValue, forKey: Key.lastImageCacheFlushDate) + objectWillChange.send() } - } - - static var timelineGroupByFeed: Bool { get { - return bool(for: Key.timelineGroupByFeed) - } - set { - setBool(for: Key.timelineGroupByFeed, newValue) - } - } - - static var refreshClearsReadArticles: Bool { - get { - return bool(for: Key.refreshClearsReadArticles) - } - set { - setBool(for: Key.refreshClearsReadArticles, newValue) - } - } - - static var timelineSortDirection: ComparisonResult { - get { - return sortDirection(for: Key.timelineSortDirection) - } - set { - setSortDirection(for: Key.timelineSortDirection, newValue) - } - } - - static var articleFullscreenAvailable: Bool { - get { - return bool(for: Key.articleFullscreenAvailable) - } - set { - setBool(for: Key.articleFullscreenAvailable, newValue) - } - } - - static var articleFullscreenEnabled: Bool { - get { - return bool(for: Key.articleFullscreenEnabled) - } - set { - setBool(for: Key.articleFullscreenEnabled, newValue) - } - } - - static var confirmMarkAllAsRead: Bool { - get { - return bool(for: Key.confirmMarkAllAsRead) - } - set { - setBool(for: Key.confirmMarkAllAsRead, newValue) + AppDefaults.store.object(forKey: Key.lastImageCacheFlushDate) as? Date } } - static var lastRefresh: 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 { - return date(for: Key.lastRefresh) + 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 { - setDate(for: Key.lastRefresh, newValue) + UserDefaults.standard.set(newValue, forKey: Key.windowState) + objectWillChange.send() } } - static var timelineNumberOfLines: Int { + @AppStorage(wrappedValue: false, Key.openInBrowserInBackground, store: store) var openInBrowserInBackground: Bool { + didSet { + objectWillChange.send() + } + } + + var sidebarFontSize: FontSize { get { - return int(for: Key.timelineNumberOfLines) + return fontSize(for: Key.sidebarFontSize) } set { - setInt(for: Key.timelineNumberOfLines, newValue) + AppDefaults.store.set(newValue.rawValue, forKey: Key.sidebarFontSize) + objectWillChange.send() } } - static var timelineIconSize: IconSize { + var timelineFontSize: FontSize { get { - let rawValue = AppDefaults.shared.integer(forKey: Key.timelineIconSize) - return IconSize(rawValue: rawValue) ?? IconSize.medium + return fontSize(for: Key.timelineFontSize) } set { - AppDefaults.shared.set(newValue.rawValue, forKey: Key.timelineIconSize) + AppDefaults.store.set(newValue.rawValue, forKey: Key.timelineFontSize) + objectWillChange.send() } } - static func registerDefaults() { - let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue, - Key.timelineGroupByFeed: false, - Key.refreshClearsReadArticles: false, - Key.timelineNumberOfLines: 2, - Key.timelineIconSize: IconSize.medium.rawValue, - Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, - Key.articleFullscreenAvailable: false, - Key.articleFullscreenEnabled: false, - Key.confirmMarkAllAsRead: true] - AppDefaults.shared.register(defaults: defaults) - } - -} - -private extension AppDefaults { - - static var firstRunDate: Date? { + var detailFontSize: FontSize { get { - return date(for: Key.firstRunDate) + return fontSize(for: Key.detailFontSize) } set { - setDate(for: Key.firstRunDate, newValue) + AppDefaults.store.set(newValue.rawValue, forKey: Key.detailFontSize) + objectWillChange.send() } } - - static func string(for key: String) -> String? { - return AppDefaults.shared.string(forKey: key) - } - static func setString(for key: String, _ value: String?) { - AppDefaults.shared.set(value, forKey: key) - } - - static func bool(for key: String) -> Bool { - return AppDefaults.shared.bool(forKey: key) - } - - static func setBool(for key: String, _ flag: Bool) { - AppDefaults.shared.set(flag, forKey: key) - } - - static func int(for key: String) -> Int { - return AppDefaults.shared.integer(forKey: key) - } - - static func setInt(for key: String, _ x: Int) { - AppDefaults.shared.set(x, forKey: key) - } - - static func date(for key: String) -> Date? { - return AppDefaults.shared.object(forKey: key) as? Date - } - - static func setDate(for key: String, _ date: Date?) { - AppDefaults.shared.set(date, forKey: key) - } - - static func sortDirection(for key:String) -> ComparisonResult { - let rawInt = int(for: key) - if rawInt == ComparisonResult.orderedAscending.rawValue { - return .orderedAscending + @AppStorage(Key.importOPMLAccountID, store: store) var importOPMLAccountID: String? { + didSet { + objectWillChange.send() } - return .orderedDescending } - - static func setSortDirection(for key: String, _ value: ComparisonResult) { - if value == .orderedAscending { - setInt(for: key, ComparisonResult.orderedAscending.rawValue) + + @AppStorage(Key.exportOPMLAccountID, store: store) var exportOPMLAccountID: String? { + didSet { + objectWillChange.send() } - else { - setInt(for: key, ComparisonResult.orderedDescending.rawValue) + } + + @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 + } +} diff --git a/Multiplatform/Shared/AppSettings.swift b/Multiplatform/Shared/AppSettings.swift deleted file mode 100644 index a584034c2..000000000 --- a/Multiplatform/Shared/AppSettings.swift +++ /dev/null @@ -1,321 +0,0 @@ -// -// AppSettings.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 AppSettings: 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 = AppSettings() - 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" - - // 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 { - AppSettings.store.setValue(newValue, forKey: Key.firstRunDate) - objectWillChange.send() - } - get { - AppSettings.store.object(forKey: Key.firstRunDate) as? Date - } - } - - // MARK: Refresh Timings - @AppStorage(wrappedValue: RefreshInterval.everyHour, Key.refreshInterval, store: store) var refreshInterval: RefreshInterval - - // MARK: Dock Badge - @AppStorage(wrappedValue: false, Key.hideDockUnreadCount, store: store) var hideDockUnreadCount - - // MARK: Color Palette - var userInterfaceColorPalette: UserInterfaceColorPalette { - get { - if let palette = UserInterfaceColorPalette(rawValue: AppSettings.store.integer(forKey: Key.userInterfaceColorPalette)) { - return palette - } - return .automatic - } - set { - AppSettings.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 AppSettings.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 { - AppSettings.store.setValue(newValue, forKey: Key.lastImageCacheFlushDate) - objectWillChange.send() - } - get { - AppSettings.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 { - AppSettings.store.setValue(newValue, forKey: Key.lastRefresh) - objectWillChange.send() - } - get { - AppSettings.store.object(forKey: Key.lastRefresh) as? Date - } - } - - // MARK: Window State - var windowState: [AnyHashable : Any]? { - get { - return AppSettings.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 { - AppSettings.store.set(newValue.rawValue, forKey: Key.sidebarFontSize) - objectWillChange.send() - } - } - - var timelineFontSize: FontSize { - get { - return fontSize(for: Key.timelineFontSize) - } - set { - AppSettings.store.set(newValue.rawValue, forKey: Key.timelineFontSize) - objectWillChange.send() - } - } - - var detailFontSize: FontSize { - get { - return fontSize(for: Key.detailFontSize) - } - set { - AppSettings.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 - - -} - -extension AppSettings { - - func isFirstRun() -> Bool { - if let _ = AppSettings.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 - } -} diff --git a/Multiplatform/Shared/MainApp.swift b/Multiplatform/Shared/MainApp.swift index 7fe88ed18..a6dfa2f3b 100644 --- a/Multiplatform/Shared/MainApp.swift +++ b/Multiplatform/Shared/MainApp.swift @@ -13,13 +13,13 @@ struct MainApp: App { #if os(macOS) @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate - let preferences = MacPreferences() #endif #if os(iOS) @UIApplicationDelegateAdaptor(AppDelegate.self) private var delegate #endif @StateObject private var sceneModel = SceneModel() + @StateObject private var defaults = AppDefaults.shared @SceneBuilder var body: some Scene { #if os(macOS) @@ -134,7 +134,7 @@ struct MainApp: App { .padding() .frame(width: 500) .navigationTitle("Preferences") - .environmentObject(preferences) + .environmentObject(defaults) } .windowToolbarStyle(UnifiedWindowToolbarStyle()) diff --git a/Multiplatform/iOS/AppDelegate.swift b/Multiplatform/iOS/AppDelegate.swift index 6124a75bb..3df75f60d 100644 --- a/Multiplatform/iOS/AppDelegate.swift +++ b/Multiplatform/iOS/AppDelegate.swift @@ -71,9 +71,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - AppDefaults.registerDefaults() + //AppDefaults.registerDefaults() - let isFirstRun = AppDefaults.isFirstRun + let isFirstRun = AppDefaults.shared.isFirstRun() if isFirstRun { os_log("Is first run.", log: log, type: .info) } @@ -139,7 +139,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } @objc func accountRefreshDidFinish(_ note: Notification) { - AppDefaults.lastRefresh = Date() + AppDefaults.shared.lastRefresh = Date() } // MARK: - API @@ -163,7 +163,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // extensionFeedAddRequestFile.resume() syncTimer?.update() - if let lastRefresh = AppDefaults.lastRefresh { + if let lastRefresh = AppDefaults.shared.lastRefresh { if Date() > lastRefresh.addingTimeInterval(15 * 60) { AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) } else { diff --git a/Multiplatform/iOS/Settings/SettingsView.swift b/Multiplatform/iOS/Settings/SettingsView.swift index 61cfb879f..b08cd249b 100644 --- a/Multiplatform/iOS/Settings/SettingsView.swift +++ b/Multiplatform/iOS/Settings/SettingsView.swift @@ -57,7 +57,7 @@ struct SettingsView: View { @Environment(\.presentationMode) var presentationMode @StateObject private var viewModel = SettingsViewModel() - @StateObject private var settings = AppSettings.shared + @StateObject private var settings = AppDefaults.shared var body: some View { NavigationView { diff --git a/Multiplatform/iOS/Settings/Submenus/TimelineLayoutView.swift b/Multiplatform/iOS/Settings/Submenus/TimelineLayoutView.swift index ce12a95a0..2b064917c 100644 --- a/Multiplatform/iOS/Settings/Submenus/TimelineLayoutView.swift +++ b/Multiplatform/iOS/Settings/Submenus/TimelineLayoutView.swift @@ -10,7 +10,7 @@ import SwiftUI struct TimelineLayoutView: View { - @EnvironmentObject private var appSettings: AppSettings + @EnvironmentObject private var appSettings: AppDefaults private let sampleTitle = "Lorem dolor sed viverra ipsum. Gravida rutrum quisque non tellus. Rutrum tellus pellentesque eu tincidunt tortor. Sed blandit libero volutpat sed cras ornare. Et netus et malesuada fames ac. Ultrices eros in cursus turpis massa tincidunt dui ut ornare. Lacus sed viverra tellus in. Sollicitudin ac orci phasellus egestas. Purus in mollis nunc sed. Sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque. Interdum consectetur libero id faucibus nisl tincidunt eget." @@ -33,7 +33,7 @@ struct TimelineLayoutView: View { } var iconSize: some View { - Slider(value: $appSettings.timelineIconSize, in: 20...60, minimumValueLabel: Text("Small"), maximumValueLabel: Text("Large"), label: { + Slider(value: $appSettings.timelineIconSize, in: 20...60, step: 10, minimumValueLabel: Text("Small"), maximumValueLabel: Text("Large"), label: { Text(String(appSettings.timelineIconSize)) }) } diff --git a/Multiplatform/macOS/AppDelegate.swift b/Multiplatform/macOS/AppDelegate.swift index 3d064ff87..e01a4f244 100644 --- a/Multiplatform/macOS/AppDelegate.swift +++ b/Multiplatform/macOS/AppDelegate.swift @@ -133,8 +133,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele } #endif - AppDefaults.registerDefaults() - let isFirstRun = AppDefaults.isFirstRun + //AppDefaults.registerDefaults() + let isFirstRun = AppDefaults.shared.isFirstRun() if isFirstRun { os_log(.debug, log: log, "Is first run.") } @@ -245,7 +245,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele // MARK: - Dock Badge @objc func updateDockBadge() { - let label = unreadCount > 0 && !AppDefaults.hideDockUnreadCount ? "\(unreadCount)" : "" + let label = unreadCount > 0 && !AppDefaults.shared.hideDockUnreadCount ? "\(unreadCount)" : "" NSApplication.shared.dockTile.badgeLabel = label } diff --git a/Multiplatform/macOS/Preferences/Views/MacPreferencesView.swift b/Multiplatform/macOS/Preferences/MacPreferencesView.swift similarity index 89% rename from Multiplatform/macOS/Preferences/Views/MacPreferencesView.swift rename to Multiplatform/macOS/Preferences/MacPreferencesView.swift index f7f70d0c3..606d7eb27 100644 --- a/Multiplatform/macOS/Preferences/Views/MacPreferencesView.swift +++ b/Multiplatform/macOS/Preferences/MacPreferencesView.swift @@ -32,19 +32,19 @@ struct MacPreferenceViewModel { struct MacPreferencesView: View { - @EnvironmentObject var preferences: MacPreferences + @EnvironmentObject var defaults: AppDefaults @State private var viewModel = MacPreferenceViewModel() var body: some View { VStack { if viewModel.currentPreferencePane == .general { - AnyView(GeneralPreferencesView()) + AnyView(GeneralPreferencesView().environmentObject(defaults)) } else if viewModel.currentPreferencePane == .accounts { - AnyView(AccountsPreferencesView()) + AnyView(AccountsPreferencesView().environmentObject(defaults)) } else { - AnyView(AdvancedPreferencesView(preferences: preferences)) + AnyView(AdvancedPreferencesView().environmentObject(defaults)) } } .toolbar { diff --git a/Multiplatform/macOS/Preferences/Model/MacPreferences.swift b/Multiplatform/macOS/Preferences/Model/MacPreferences.swift deleted file mode 100644 index 4c0d4e2e8..000000000 --- a/Multiplatform/macOS/Preferences/Model/MacPreferences.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// MacPreferences.swift -// macOS -// -// Created by Stuart Breckenridge on 27/6/20. -// - -import SwiftUI - -enum FontSize: Int { - case small = 0 - case medium = 1 - case large = 2 - case veryLarge = 3 -} - -/// The `MacPreferences` object stores all macOS specific user preferences. -class MacPreferences: ObservableObject { - - private struct AppKeys { - static let refreshInterval = "refreshInterval" - static let openInBackground = "openInBrowserInBackground" - static let showUnreadCountInDock = "showUnreadCountInDock" - static let checkForUpdatesAutomatically = "checkForAppUpdates" - static let downloadTestBuilds = "downloadTestBuilds" - static let sendCrashLogs = "sendCrashLogs" - } - - // Refresh Interval - public let refreshIntervals:[String] = RefreshFrequencies.allCases.map({ $0.description }) - @AppStorage(wrappedValue: 0, AppKeys.refreshInterval) var refreshFrequency { - didSet { - objectWillChange.send() - } - } - - // Open in background - @AppStorage(wrappedValue: false, AppKeys.openInBackground) var openInBackground { - didSet { - objectWillChange.send() - } - } - - // Unread Count in Dock - @AppStorage(wrappedValue: true, AppKeys.showUnreadCountInDock) var showUnreadCountInDock { - didSet { - objectWillChange.send() - } - } - - // Check for App Updates - @AppStorage(wrappedValue: true, AppKeys.checkForUpdatesAutomatically) var checkForUpdatesAutomatically { - didSet { - objectWillChange.send() - } - } - - // Test builds - @AppStorage(wrappedValue: false, AppKeys.downloadTestBuilds) var downloadTestBuilds { - didSet { - objectWillChange.send() - } - } - - // Crash Logs - @AppStorage(wrappedValue: false, AppKeys.sendCrashLogs) var sendCrashLogs { - didSet { - objectWillChange.send() - } - } -} - - -enum RefreshFrequencies: CaseIterable, CustomStringConvertible { - - case refreshEvery10Mins, refreshEvery20Mins, refreshHourly, refreshEvery2Hours, refreshEvery4Hours, refreshEvery8Hours, none - - var description: String { - switch self { - case .refreshEvery10Mins: - return "Every 10 minutes" - case .refreshEvery20Mins: - return "Every 20 minutes" - case .refreshHourly: - return "Every hour" - case .refreshEvery2Hours: - return "Every 2 hours" - case .refreshEvery4Hours: - return "Every 4 hours" - case .refreshEvery8Hours: - return "Every 8 hours" - case .none: - return "Manually" - } - } -} diff --git a/Multiplatform/macOS/Preferences/Views/AccountsPreferencesView.swift b/Multiplatform/macOS/Preferences/View/AccountsPreferencesView.swift similarity index 100% rename from Multiplatform/macOS/Preferences/Views/AccountsPreferencesView.swift rename to Multiplatform/macOS/Preferences/View/AccountsPreferencesView.swift diff --git a/Multiplatform/macOS/Preferences/Views/AdvancedPreferencesView.swift b/Multiplatform/macOS/Preferences/View/AdvancedPreferencesView.swift similarity index 96% rename from Multiplatform/macOS/Preferences/Views/AdvancedPreferencesView.swift rename to Multiplatform/macOS/Preferences/View/AdvancedPreferencesView.swift index 2d2fba65f..e493f3582 100644 --- a/Multiplatform/macOS/Preferences/Views/AdvancedPreferencesView.swift +++ b/Multiplatform/macOS/Preferences/View/AdvancedPreferencesView.swift @@ -9,7 +9,7 @@ import SwiftUI struct AdvancedPreferencesView: View { - @StateObject var preferences: MacPreferences + @EnvironmentObject private var preferences: AppDefaults var body: some View { VStack { diff --git a/Multiplatform/macOS/Preferences/View/GeneralPreferencesView.swift b/Multiplatform/macOS/Preferences/View/GeneralPreferencesView.swift new file mode 100644 index 000000000..909e9a682 --- /dev/null +++ b/Multiplatform/macOS/Preferences/View/GeneralPreferencesView.swift @@ -0,0 +1,33 @@ +// +// GeneralPreferencesView.swift +// macOS +// +// Created by Stuart Breckenridge on 27/6/20. +// + +import SwiftUI + +struct GeneralPreferencesView: View { + + @EnvironmentObject private var defaults: AppDefaults + + var body: some View { + VStack { + Form { + Picker("Refresh Feeds", + selection: $defaults.interval, + content: { + ForEach(RefreshInterval.allCases, content: { interval in + Text(interval.description()).tag(interval.rawValue) + }) + }).frame(width: 300, alignment: .center) + + Toggle("Open webpages in background in browser", isOn: $defaults.openInBrowserInBackground) + + Toggle("Hide Unread Count in Dock", isOn: $defaults.hideDockUnreadCount) + } + Spacer() + }.frame(width: 300, alignment: .center) + } + +} diff --git a/Multiplatform/macOS/Preferences/Views/GeneralPreferencesView.swift b/Multiplatform/macOS/Preferences/Views/GeneralPreferencesView.swift deleted file mode 100644 index a5bbd6e64..000000000 --- a/Multiplatform/macOS/Preferences/Views/GeneralPreferencesView.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// GeneralPreferencesView.swift -// macOS -// -// Created by Stuart Breckenridge on 27/6/20. -// - -import SwiftUI - -struct GeneralPreferencesView: View { - - @ObservedObject private var preferences = MacPreferences() - - var body: some View { - VStack { - Form { - Picker("Refresh Feeds", - selection: $preferences.refreshFrequency, - content: { - ForEach(0.. String { switch self { case .manually: