From 7ef780b8a4161d41ec9a1d7bc1b7e5f55c4b06c7 Mon Sep 17 00:00:00 2001 From: Davide Date: Wed, 13 Nov 2024 22:35:50 +0100 Subject: [PATCH] Decouple Mac settings from AppMenu (#865) - Rename AppMenu.Model to MacSettingsModel for global reuse in macOS app - Fix compile errors on iOS --- Passepartout/App/AppDelegate.swift | 11 +++ Passepartout/App/PassepartoutApp.swift | 6 ++ Passepartout/App/Platforms/App+macOS.swift | 21 +---- .../AppUIMain/Business/MacSettingsModel.swift | 90 +++++++++++++++++++ .../Views/AppMenu/macOS/AppMenu+Model.swift | 73 --------------- .../Views/AppMenu/macOS/AppMenu.swift | 13 ++- .../Views/Settings/SettingsSectionGroup.swift | 40 ++++----- 7 files changed, 135 insertions(+), 119 deletions(-) create mode 100644 Passepartout/Library/Sources/AppUIMain/Business/MacSettingsModel.swift delete mode 100644 Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenu+Model.swift diff --git a/Passepartout/App/AppDelegate.swift b/Passepartout/App/AppDelegate.swift index 0c33e2df..5bcfe050 100644 --- a/Passepartout/App/AppDelegate.swift +++ b/Passepartout/App/AppDelegate.swift @@ -23,6 +23,9 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) +import AppUIMain +#endif import CommonLibrary import PassepartoutKit import SwiftUI @@ -33,6 +36,14 @@ final class AppDelegate: NSObject { let context: AppContext = .shared // let context: AppContext = .mock(withRegistry: .shared) +#if os(macOS) + let settings = MacSettingsModel( + defaults: .standard, + appWindow: .shared, + loginItemId: BundleConfiguration.mainString(for: .loginItemId) + ) +#endif + func configure(with uiConfiguring: UILibraryConfiguring) { UILibrary(uiConfiguring) .configure(with: context) diff --git a/Passepartout/App/PassepartoutApp.swift b/Passepartout/App/PassepartoutApp.swift index 40c2f6d5..51a43cf9 100644 --- a/Passepartout/App/PassepartoutApp.swift +++ b/Passepartout/App/PassepartoutApp.swift @@ -64,6 +64,12 @@ extension PassepartoutApp { appDelegate.context } +#if os(macOS) + var settings: MacSettingsModel { + appDelegate.settings + } +#endif + func contentView() -> some View { AppCoordinator( profileManager: context.profileManager, diff --git a/Passepartout/App/Platforms/App+macOS.swift b/Passepartout/App/Platforms/App+macOS.swift index 3df7b9ec..3ddbf090 100644 --- a/Passepartout/App/Platforms/App+macOS.swift +++ b/Passepartout/App/Platforms/App+macOS.swift @@ -35,14 +35,14 @@ extension AppDelegate: NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { configure(with: AppUIMain()) context.onApplicationActive() - if isStartedFromLoginItem { + if settings.isStartedFromLoginItem { AppWindow.shared.isVisible = false } } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { AppWindow.shared.isVisible = false - return !keepsInMenu + return !settings.keepsInMenu } func application(_ application: NSApplication, open urls: [URL]) { @@ -50,21 +50,6 @@ extension AppDelegate: NSApplicationDelegate { } } -private extension AppDelegate { - var keepsInMenu: Bool { - get { - UserDefaults.standard.bool(forKey: AppPreference.keepsInMenu.key) - } - set { - UserDefaults.standard.set(newValue, forKey: AppPreference.keepsInMenu.key) - } - } - - var isStartedFromLoginItem: Bool { - NSApp.isHidden - } -} - extension PassepartoutApp { @SceneBuilder @@ -82,6 +67,7 @@ extension PassepartoutApp { SettingsView(profileManager: context.profileManager) .frame(minWidth: 300, minHeight: 300) .withEnvironment(from: context, theme: theme) + .environmentObject(settings) } MenuBarExtra { AppMenu( @@ -89,6 +75,7 @@ extension PassepartoutApp { tunnel: context.tunnel ) .withEnvironment(from: context, theme: theme) + .environmentObject(settings) } label: { AppMenuImage(tunnel: context.tunnel) .environmentObject(theme) diff --git a/Passepartout/Library/Sources/AppUIMain/Business/MacSettingsModel.swift b/Passepartout/Library/Sources/AppUIMain/Business/MacSettingsModel.swift new file mode 100644 index 00000000..da914e03 --- /dev/null +++ b/Passepartout/Library/Sources/AppUIMain/Business/MacSettingsModel.swift @@ -0,0 +1,90 @@ +// +// MacSettingsModel.swift +// Passepartout +// +// Created by Davide De Rosa on 10/29/24. +// Copyright (c) 2024 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +#if os(macOS) + +import AppKit +import CommonLibrary +import PassepartoutKit +import ServiceManagement + +@MainActor +public final class MacSettingsModel: ObservableObject { + private let defaults: UserDefaults + + private let appWindow: AppWindow + + private let appService: SMAppService + + public var isStartedFromLoginItem: Bool { + NSApp.isHidden + } + + public var isVisible: Bool { + get { + appWindow.isVisible + } + set { + appWindow.isVisible = newValue + objectWillChange.send() + } + } + + public var launchesOnLogin: Bool { + get { + appService.status == .enabled + } + set { + do { + if newValue { + try appService.register() + } else { + try appService.unregister() + } + } catch { + pp_log(.app, .error, "Unable to (un)register login item: \(error)") + } + objectWillChange.send() + } + } + + public var keepsInMenu: Bool { + get { + defaults.bool(forKey: AppPreference.keepsInMenu.key) + } + set { + defaults.set(newValue, forKey: AppPreference.keepsInMenu.key) + objectWillChange.send() + } + } + + public init(defaults: UserDefaults, appWindow: AppWindow, loginItemId: String) { + self.defaults = defaults + self.appWindow = appWindow + appService = SMAppService.loginItem(identifier: loginItemId) + } +} + +#endif diff --git a/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenu+Model.swift b/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenu+Model.swift deleted file mode 100644 index b0145ab9..00000000 --- a/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenu+Model.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// AppMenu+Model.swift -// Passepartout -// -// Created by Davide De Rosa on 10/29/24. -// Copyright (c) 2024 Davide De Rosa. All rights reserved. -// -// https://github.com/passepartoutvpn -// -// This file is part of Passepartout. -// -// Passepartout is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Passepartout is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Passepartout. If not, see . -// - -#if os(macOS) - -import AppKit -import PassepartoutKit -import ServiceManagement - -extension AppMenu { - - @MainActor - final class Model: ObservableObject { - private let appService: SMAppService - - var isVisible: Bool { - get { - AppWindow.shared.isVisible - } - set { - AppWindow.shared.isVisible = newValue - objectWillChange.send() - } - } - - var launchesOnLogin: Bool { - get { - appService.status == .enabled - } - set { - do { - if newValue { - try appService.register() - } else { - try appService.unregister() - } - } catch { - pp_log(.app, .error, "Unable to (un)register login item: \(error)") - } - objectWillChange.send() - } - } - - init() { - let loginItemId = BundleConfiguration.mainString(for: .loginItemId) - appService = SMAppService.loginItem(identifier: loginItemId) - } - } -} - -#endif diff --git a/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenu.swift b/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenu.swift index 89f3597d..c0410c48 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenu.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenu.swift @@ -32,8 +32,8 @@ import SwiftUI public struct AppMenu: View { - @AppStorage(AppPreference.keepsInMenu.key) - private var keepsInMenu = true + @EnvironmentObject + private var settings: MacSettingsModel @ObservedObject private var profileManager: ProfileManager @@ -41,9 +41,6 @@ public struct AppMenu: View { @ObservedObject private var tunnel: ExtendedTunnel - @StateObject - private var model = Model() - public init(profileManager: ProfileManager, tunnel: ExtendedTunnel) { self.profileManager = profileManager self.tunnel = tunnel @@ -70,16 +67,16 @@ private extension AppMenu { var showToggle: some View { Button(Strings.Global.show) { - model.isVisible = true + settings.isVisible = true } } var loginToggle: some View { - Toggle(Strings.Views.Settings.launchesOnLogin, isOn: $model.launchesOnLogin) + Toggle(Strings.Views.Settings.launchesOnLogin, isOn: $settings.launchesOnLogin) } var keepToggle: some View { - Toggle(Strings.Views.Settings.keepsInMenu, isOn: $keepsInMenu) + Toggle(Strings.Views.Settings.keepsInMenu, isOn: $settings.keepsInMenu) } var profilesList: some View { diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Settings/SettingsSectionGroup.swift b/Passepartout/Library/Sources/AppUIMain/Views/Settings/SettingsSectionGroup.swift index 0f4949f0..7cd3c88c 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/Settings/SettingsSectionGroup.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/Settings/SettingsSectionGroup.swift @@ -29,17 +29,15 @@ import PassepartoutKit import SwiftUI struct SettingsSectionGroup: View { - - @AppStorage(AppPreference.keepsInMenu.key) - private var keepsInMenu = true - - @AppStorage(AppPreference.locksInBackground.key) - private var locksInBackground = false - let profileManager: ProfileManager - @StateObject - private var model = AppMenu.Model() +#if os(iOS) + @AppStorage(AppPreference.locksInBackground.key) + private var locksInBackground = false +#else + @EnvironmentObject + private var settings: MacSettingsModel +#endif @State private var isConfirmingEraseiCloud = false @@ -50,8 +48,7 @@ struct SettingsSectionGroup: View { var body: some View { #if os(iOS) lockInBackgroundToggle -#endif -#if os(macOS) +#else launchesOnLoginToggle keepsInMenuToggle #endif @@ -60,21 +57,22 @@ struct SettingsSectionGroup: View { } private extension SettingsSectionGroup { - var launchesOnLoginToggle: some View { - Toggle(Strings.Views.Settings.launchesOnLogin, isOn: $model.launchesOnLogin) - .themeSectionWithSingleRow(footer: Strings.Views.Settings.LaunchesOnLogin.footer) - } - - var keepsInMenuToggle: some View { - Toggle(Strings.Views.Settings.keepsInMenu, isOn: $keepsInMenu) - .themeSectionWithSingleRow(footer: Strings.Views.Settings.KeepsInMenu.footer) - } - +#if os(iOS) var lockInBackgroundToggle: some View { Toggle(Strings.Views.Settings.locksInBackground, isOn: $locksInBackground) .themeSectionWithSingleRow(footer: Strings.Views.Settings.LocksInBackground.footer) } +#else + var launchesOnLoginToggle: some View { + Toggle(Strings.Views.Settings.launchesOnLogin, isOn: $settings.launchesOnLogin) + .themeSectionWithSingleRow(footer: Strings.Views.Settings.LaunchesOnLogin.footer) + } + var keepsInMenuToggle: some View { + Toggle(Strings.Views.Settings.keepsInMenu, isOn: $settings.keepsInMenu) + .themeSectionWithSingleRow(footer: Strings.Views.Settings.KeepsInMenu.footer) + } +#endif var eraseCloudKitButton: some View { Button(Strings.Views.Settings.eraseIcloud, role: .destructive) { isConfirmingEraseiCloud = true