Decouple Mac settings from AppMenu (#865)

- Rename AppMenu.Model to MacSettingsModel for global reuse in macOS app
- Fix compile errors on iOS
This commit is contained in:
Davide 2024-11-13 22:35:50 +01:00 committed by GitHub
parent 1b10f86bbb
commit 7ef780b8a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 135 additions and 119 deletions

View File

@ -23,6 +23,9 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>. // along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
// //
#if !os(tvOS)
import AppUIMain
#endif
import CommonLibrary import CommonLibrary
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
@ -33,6 +36,14 @@ final class AppDelegate: NSObject {
let context: AppContext = .shared let context: AppContext = .shared
// let context: AppContext = .mock(withRegistry: .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) { func configure(with uiConfiguring: UILibraryConfiguring) {
UILibrary(uiConfiguring) UILibrary(uiConfiguring)
.configure(with: context) .configure(with: context)

View File

@ -64,6 +64,12 @@ extension PassepartoutApp {
appDelegate.context appDelegate.context
} }
#if os(macOS)
var settings: MacSettingsModel {
appDelegate.settings
}
#endif
func contentView() -> some View { func contentView() -> some View {
AppCoordinator( AppCoordinator(
profileManager: context.profileManager, profileManager: context.profileManager,

View File

@ -35,14 +35,14 @@ extension AppDelegate: NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
configure(with: AppUIMain()) configure(with: AppUIMain())
context.onApplicationActive() context.onApplicationActive()
if isStartedFromLoginItem { if settings.isStartedFromLoginItem {
AppWindow.shared.isVisible = false AppWindow.shared.isVisible = false
} }
} }
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
AppWindow.shared.isVisible = false AppWindow.shared.isVisible = false
return !keepsInMenu return !settings.keepsInMenu
} }
func application(_ application: NSApplication, open urls: [URL]) { 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 { extension PassepartoutApp {
@SceneBuilder @SceneBuilder
@ -82,6 +67,7 @@ extension PassepartoutApp {
SettingsView(profileManager: context.profileManager) SettingsView(profileManager: context.profileManager)
.frame(minWidth: 300, minHeight: 300) .frame(minWidth: 300, minHeight: 300)
.withEnvironment(from: context, theme: theme) .withEnvironment(from: context, theme: theme)
.environmentObject(settings)
} }
MenuBarExtra { MenuBarExtra {
AppMenu( AppMenu(
@ -89,6 +75,7 @@ extension PassepartoutApp {
tunnel: context.tunnel tunnel: context.tunnel
) )
.withEnvironment(from: context, theme: theme) .withEnvironment(from: context, theme: theme)
.environmentObject(settings)
} label: { } label: {
AppMenuImage(tunnel: context.tunnel) AppMenuImage(tunnel: context.tunnel)
.environmentObject(theme) .environmentObject(theme)

View File

@ -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 <http://www.gnu.org/licenses/>.
//
#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

View File

@ -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 <http://www.gnu.org/licenses/>.
//
#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

View File

@ -32,8 +32,8 @@ import SwiftUI
public struct AppMenu: View { public struct AppMenu: View {
@AppStorage(AppPreference.keepsInMenu.key) @EnvironmentObject
private var keepsInMenu = true private var settings: MacSettingsModel
@ObservedObject @ObservedObject
private var profileManager: ProfileManager private var profileManager: ProfileManager
@ -41,9 +41,6 @@ public struct AppMenu: View {
@ObservedObject @ObservedObject
private var tunnel: ExtendedTunnel private var tunnel: ExtendedTunnel
@StateObject
private var model = Model()
public init(profileManager: ProfileManager, tunnel: ExtendedTunnel) { public init(profileManager: ProfileManager, tunnel: ExtendedTunnel) {
self.profileManager = profileManager self.profileManager = profileManager
self.tunnel = tunnel self.tunnel = tunnel
@ -70,16 +67,16 @@ private extension AppMenu {
var showToggle: some View { var showToggle: some View {
Button(Strings.Global.show) { Button(Strings.Global.show) {
model.isVisible = true settings.isVisible = true
} }
} }
var loginToggle: some View { var loginToggle: some View {
Toggle(Strings.Views.Settings.launchesOnLogin, isOn: $model.launchesOnLogin) Toggle(Strings.Views.Settings.launchesOnLogin, isOn: $settings.launchesOnLogin)
} }
var keepToggle: some View { var keepToggle: some View {
Toggle(Strings.Views.Settings.keepsInMenu, isOn: $keepsInMenu) Toggle(Strings.Views.Settings.keepsInMenu, isOn: $settings.keepsInMenu)
} }
var profilesList: some View { var profilesList: some View {

View File

@ -29,17 +29,15 @@ import PassepartoutKit
import SwiftUI import SwiftUI
struct SettingsSectionGroup: View { struct SettingsSectionGroup: View {
@AppStorage(AppPreference.keepsInMenu.key)
private var keepsInMenu = true
@AppStorage(AppPreference.locksInBackground.key)
private var locksInBackground = false
let profileManager: ProfileManager let profileManager: ProfileManager
@StateObject #if os(iOS)
private var model = AppMenu.Model() @AppStorage(AppPreference.locksInBackground.key)
private var locksInBackground = false
#else
@EnvironmentObject
private var settings: MacSettingsModel
#endif
@State @State
private var isConfirmingEraseiCloud = false private var isConfirmingEraseiCloud = false
@ -50,8 +48,7 @@ struct SettingsSectionGroup: View {
var body: some View { var body: some View {
#if os(iOS) #if os(iOS)
lockInBackgroundToggle lockInBackgroundToggle
#endif #else
#if os(macOS)
launchesOnLoginToggle launchesOnLoginToggle
keepsInMenuToggle keepsInMenuToggle
#endif #endif
@ -60,21 +57,22 @@ struct SettingsSectionGroup: View {
} }
private extension SettingsSectionGroup { private extension SettingsSectionGroup {
var launchesOnLoginToggle: some View { #if os(iOS)
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)
}
var lockInBackgroundToggle: some View { var lockInBackgroundToggle: some View {
Toggle(Strings.Views.Settings.locksInBackground, isOn: $locksInBackground) Toggle(Strings.Views.Settings.locksInBackground, isOn: $locksInBackground)
.themeSectionWithSingleRow(footer: Strings.Views.Settings.LocksInBackground.footer) .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 { var eraseCloudKitButton: some View {
Button(Strings.Views.Settings.eraseIcloud, role: .destructive) { Button(Strings.Views.Settings.eraseIcloud, role: .destructive) {
isConfirmingEraseiCloud = true isConfirmingEraseiCloud = true