diff --git a/Reader.xcodeproj/project.pbxproj b/Reader.xcodeproj/project.pbxproj index 1ed7040..0311931 100644 --- a/Reader.xcodeproj/project.pbxproj +++ b/Reader.xcodeproj/project.pbxproj @@ -15,6 +15,9 @@ D65B18BC27504FE7004A9448 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18BB27504FE7004A9448 /* Token.swift */; }; D65B18BE275051A1004A9448 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18BD275051A1004A9448 /* LocalData.swift */; }; D65B18C127505348004A9448 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18C027505348004A9448 /* HomeViewController.swift */; }; + D68408E827947D0800E327D2 /* PrefsSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68408E727947D0800E327D2 /* PrefsSceneDelegate.swift */; }; + D68408ED2794803D00E327D2 /* PrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68408EC2794803D00E327D2 /* PrefsView.swift */; }; + D68408EF2794808E00E327D2 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68408EE2794808E00E327D2 /* Preferences.swift */; }; D68B303627907D9200E8B3FA /* ExcerptGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68B303527907D9200E8B3FA /* ExcerptGenerator.swift */; }; D68B303D2792204B00E8B3FA /* read.js in Resources */ = {isa = PBXBuildFile; fileRef = D68B303C2792204B00E8B3FA /* read.js */; }; D68B30402792729A00E8B3FA /* AppSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68B303F2792729A00E8B3FA /* AppSplitViewController.swift */; }; @@ -101,6 +104,9 @@ D65B18BB27504FE7004A9448 /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; D65B18BD275051A1004A9448 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = ""; }; D65B18C027505348004A9448 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; + D68408E727947D0800E327D2 /* PrefsSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsSceneDelegate.swift; sourceTree = ""; }; + D68408EC2794803D00E327D2 /* PrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsView.swift; sourceTree = ""; }; + D68408EE2794808E00E327D2 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; D68B3032278FDD1A00E8B3FA /* Reader-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Reader-Bridging-Header.h"; sourceTree = ""; }; D68B303527907D9200E8B3FA /* ExcerptGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExcerptGenerator.swift; sourceTree = ""; }; D68B3037279099FD00E8B3FA /* liblolhtml.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = liblolhtml.a; path = "lol-html/c-api/target/aarch64-apple-ios-sim/release/liblolhtml.a"; sourceTree = ""; }; @@ -188,6 +194,7 @@ D65B18B027504691004A9448 /* Login */, D6E2434A278B455C0005E546 /* Items */, D6E2436C278BD80B0005E546 /* Read */, + D68408E927947E3800E327D2 /* Preferences */, ); path = Screens; sourceTree = ""; @@ -209,6 +216,14 @@ path = Home; sourceTree = ""; }; + D68408E927947E3800E327D2 /* Preferences */ = { + isa = PBXGroup; + children = ( + D68408EC2794803D00E327D2 /* PrefsView.swift */, + ); + path = Preferences; + sourceTree = ""; + }; D68B302E278FDCE200E8B3FA /* Frameworks */ = { isa = PBXGroup; children = ( @@ -262,12 +277,14 @@ D68B3032278FDD1A00E8B3FA /* Reader-Bridging-Header.h */, D6C687EB272CD27600874C10 /* AppDelegate.swift */, D6C687ED272CD27600874C10 /* SceneDelegate.swift */, + D68408E727947D0800E327D2 /* PrefsSceneDelegate.swift */, D65B18B527504920004A9448 /* FervorController.swift */, D65B18BD275051A1004A9448 /* LocalData.swift */, D6E24368278BABB40005E546 /* UIColor+App.swift */, D6EB531E278E4A7500AD2E61 /* StretchyMenuInteraction.swift */, D68B303527907D9200E8B3FA /* ExcerptGenerator.swift */, D68B304127932ED500E8B3FA /* UserActivities.swift */, + D68408EE2794808E00E327D2 /* Preferences.swift */, D6A8A33527766E9300CCEC72 /* CoreData */, D65B18AF2750468B004A9448 /* Screens */, D6C687F7272CD27700874C10 /* Assets.xcassets */, @@ -545,6 +562,7 @@ D6E2435F278B97240005E546 /* Group+CoreDataClass.swift in Sources */, D6E24369278BABB40005E546 /* UIColor+App.swift in Sources */, D6E2435D278B97240005E546 /* Item+CoreDataClass.swift in Sources */, + D68408E827947D0800E327D2 /* PrefsSceneDelegate.swift in Sources */, D6EB531D278C89C300AD2E61 /* AppNavigationController.swift in Sources */, D6E24360278B97240005E546 /* Group+CoreDataProperties.swift in Sources */, D6E2434C278B456A0005E546 /* ItemsViewController.swift in Sources */, @@ -554,6 +572,8 @@ D68B30402792729A00E8B3FA /* AppSplitViewController.swift in Sources */, D65B18BE275051A1004A9448 /* LocalData.swift in Sources */, D65B18B22750469D004A9448 /* LoginViewController.swift in Sources */, + D68408ED2794803D00E327D2 /* PrefsView.swift in Sources */, + D68408EF2794808E00E327D2 /* Preferences.swift in Sources */, D68B303627907D9200E8B3FA /* ExcerptGenerator.swift in Sources */, D6E24363278BA1410005E546 /* ItemCollectionViewCell.swift in Sources */, D6E2436E278BD8160005E546 /* ReadViewController.swift in Sources */, diff --git a/Reader/AppDelegate.swift b/Reader/AppDelegate.swift index ed0cd87..bf80c01 100644 --- a/Reader/AppDelegate.swift +++ b/Reader/AppDelegate.swift @@ -8,22 +8,40 @@ import UIKit import WebKit import OSLog +import Combine @main class AppDelegate: UIResponder, UIApplicationDelegate { + + private var cancellables = Set() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { swizzleWKWebView() + Preferences.shared.objectWillChange + .debounce(for: .milliseconds(250), scheduler: RunLoop.main) + .sink { _ in + Preferences.save() + } + .store(in: &cancellables) + return true } // MARK: UISceneSession Lifecycle func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "main", sessionRole: connectingSceneSession.role) + let name: String + #if targetEnvironment(macCatalyst) + if options.userActivities.first?.activityType == NSUserActivity.preferencesType { + name = "prefs" + } else { + name = "main" + } + #else + name = "main" + #endif + return UISceneConfiguration(name: name, sessionRole: connectingSceneSession.role) } func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { @@ -34,7 +52,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { override func buildMenu(with builder: UIMenuBuilder) { if builder.system == .main { - + builder.insertSibling(UIMenu(options: .displayInline, children: [ + UIKeyCommand(title: "Preferences…", action: #selector(showPreferences), input: ",", modifierFlags: .command) + ]), afterMenu: .about) var children = [UIMenuElement]() let accounts: [UIMenuElement] = LocalData.accounts.map { account in @@ -45,7 +65,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let state: UIAction.State if let activeScene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }), - (activeScene.delegate as! SceneDelegate).fervorController?.account?.id == account.id { + let sceneDelegate = activeScene.delegate as? SceneDelegate, + sceneDelegate.fervorController?.account?.id == account.id { state = .on } else { state = .off @@ -89,6 +110,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } as (@convention(block) (WKWebView) -> Void)) originalIMP = class_replaceMethod(WKWebView.self, selector, imp, "v@:") } + + @objc private func showPreferences() { + UIApplication.shared.requestSceneSessionActivation(nil, userActivity: .preferences(), options: nil, errorHandler: nil) + } } diff --git a/Reader/Info.plist b/Reader/Info.plist index 81d0b72..e8dc7c1 100644 --- a/Reader/Info.plist +++ b/Reader/Info.plist @@ -4,6 +4,7 @@ NSUserActivityTypes + $(PRODUCT_BUNDLE_IDENTIFIER).activity.preferences $(PRODUCT_BUNDLE_IDENTIFIER).activity.add-account $(PRODUCT_BUNDLE_IDENTIFIER).activity.activate-account @@ -21,6 +22,12 @@ UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate + + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).PrefsSceneDelegate + UISceneConfigurationName + prefs + diff --git a/Reader/Preferences.swift b/Reader/Preferences.swift new file mode 100644 index 0000000..56a9d4c --- /dev/null +++ b/Reader/Preferences.swift @@ -0,0 +1,67 @@ +// +// Preferences.swift +// Reader +// +// Created by Shadowfacts on 1/16/22. +// + +import UIKit + +class Preferences: Codable, ObservableObject { + + private static let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + private static let archiveURL = Preferences.documentsDirectory.appendingPathComponent("preferences").appendingPathExtension("plist") + private static let decoder = PropertyListDecoder() + private static let encoder = PropertyListEncoder() + + static var shared = load() + + private static func load() -> Preferences { + if let data = try? Data(contentsOf: archiveURL), + let prefs = try? decoder.decode(Preferences.self, from: data) { + return prefs + } else { + return Preferences() + } + } + + static func save() { + if let data = try? encoder.encode(shared) { + try? data.write(to: archiveURL, options: .noFileProtection) + } + } + + init() { + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.appearance = try container.decode(UIUserInterfaceStyle.self, forKey: .appearance) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(appearance, forKey: .appearance) + } + + var appearance = UIUserInterfaceStyle.unspecified { + willSet { + objectWillChange.send() + } + didSet { + NotificationCenter.default.post(name: .userInterfaceStyleChanged, object: nil) + } + } + + private enum CodingKeys: String, CodingKey { + case appearance + } + +} + +extension UIUserInterfaceStyle: Codable { +} + +extension Notification.Name { + static let userInterfaceStyleChanged = Notification.Name("userInterfaceStyleChanged") +} diff --git a/Reader/PrefsSceneDelegate.swift b/Reader/PrefsSceneDelegate.swift new file mode 100644 index 0000000..a54dda1 --- /dev/null +++ b/Reader/PrefsSceneDelegate.swift @@ -0,0 +1,61 @@ +// +// PrefsSceneDelegate.swift +// Reader +// +// Created by Shadowfacts on 1/16/22. +// + +#if targetEnvironment(macCatalyst) +import UIKit +import SwiftUI + +class PrefsSceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = scene as? UIWindowScene else { + return + } + + window = UIWindow(windowScene: windowScene) + window!.tintColor = .appTintColor + windowScene.sizeRestrictions?.minimumSize = CGSize(width: 640, height: 480) + windowScene.sizeRestrictions?.maximumSize = CGSize(width: 640, height: 480) + + window!.rootViewController = UIHostingController(rootView: PrefsView()) + + if let titlebar = windowScene.titlebar { + titlebar.toolbarStyle = .preference + titlebar.toolbar = NSToolbar(identifier: .init("ReaderPrefsToolbar")) + titlebar.toolbar!.delegate = self + titlebar.toolbar!.allowsUserCustomization = false + } + + window!.makeKeyAndVisible() + + NotificationCenter.default.addObserver(self, selector: #selector(updateUserInterfaceStyle), name: .userInterfaceStyleChanged, object: nil) + updateUserInterfaceStyle() + } + + @objc private func updateUserInterfaceStyle() { + window?.overrideUserInterfaceStyle = Preferences.shared.appearance + } + +} + +extension PrefsSceneDelegate: NSToolbarDelegate { + func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + return nil + } + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [] + } + + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [] + } +} + +#endif diff --git a/Reader/SceneDelegate.swift b/Reader/SceneDelegate.swift index 5b8c2a5..e65c309 100644 --- a/Reader/SceneDelegate.swift +++ b/Reader/SceneDelegate.swift @@ -54,6 +54,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { #endif window!.makeKeyAndVisible() + + NotificationCenter.default.addObserver(self, selector: #selector(updateUserInterfaceStyle), name: .userInterfaceStyleChanged, object: nil) + updateUserInterfaceStyle() } func sceneDidDisconnect(_ scene: UIScene) { @@ -116,6 +119,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { ExcerptGenerator.generateAll(fervorController) } } + + @objc private func updateUserInterfaceStyle() { + window?.overrideUserInterfaceStyle = Preferences.shared.appearance + } } diff --git a/Reader/Screens/Preferences/PrefsView.swift b/Reader/Screens/Preferences/PrefsView.swift new file mode 100644 index 0000000..5e2382e --- /dev/null +++ b/Reader/Screens/Preferences/PrefsView.swift @@ -0,0 +1,38 @@ +// +// PrefsView.swift +// Reader +// +// Created by Shadowfacts on 1/16/22. +// + +import SwiftUI + +struct PrefsView: View { + @ObservedObject private var preferences = Preferences.shared + + var body: some View { + VStack { + GroupBox { + VStack { + appearance + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + }.padding() + } + + private var appearance: some View { + Picker("Appearance", selection: $preferences.appearance) { + Text("System").tag(UIUserInterfaceStyle.unspecified) + Text("Dark").tag(UIUserInterfaceStyle.dark) + Text("Light").tag(UIUserInterfaceStyle.light) + } + } +} + +struct PrefsView_Previews: PreviewProvider { + static var previews: some View { + PrefsView() + } +} diff --git a/Reader/UserActivities.swift b/Reader/UserActivities.swift index 10a865a..305fd32 100644 --- a/Reader/UserActivities.swift +++ b/Reader/UserActivities.swift @@ -9,9 +9,14 @@ import Foundation extension NSUserActivity { + static let preferencesType = "net.shadowfacts.Reader.activity.preferences" static let addAccountType = "net.shadowfacts.Reader.activity.add-account" static let activateAccountType = "net.shadowfacts.Reader.activity.activate-account" + static func preferences() -> NSUserActivity { + return NSUserActivity(activityType: preferencesType) + } + static func addAccount() -> NSUserActivity { return NSUserActivity(activityType: addAccountType) }