diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a35d5b..ce67b96 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: -scheme "TimeMachineStatus" \ -configuration "Release" \ -derivedDataPath "$RUNNER_TEMP/DerivedData" \ - DEVELOPMENT_TEAM=$APPLE_TEAM_ID + DEVELOPMENT_TEAM=$APPLE_TEAM_ID | xcbeautify - name: Clean up keychain and provisioning profile if: ${{ always() }} diff --git a/.swiftlint.yml b/.swiftlint.yml index 4ee21b7..e02032b 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -52,3 +52,7 @@ identifier_name: nesting: type_level: 2 + +excluded: + - DerivedData + - build diff --git a/TimeMachineStatus.xcodeproj/project.pbxproj b/TimeMachineStatus.xcodeproj/project.pbxproj index f9d33ff..e51c852 100644 --- a/TimeMachineStatus.xcodeproj/project.pbxproj +++ b/TimeMachineStatus.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 28263FA02B023FD100F74655 /* TimeMachineStatusHelper.app in Copy Helper */ = {isa = PBXBuildFile; fileRef = 28263F8E2B023D4E00F74655 /* TimeMachineStatusHelper.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 28263FA32B023FFE00F74655 /* ServiceManagement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28263FA22B023FFE00F74655 /* ServiceManagement.framework */; }; 2885D6652B024D0B00C260DB /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2885D6642B024D0B00C260DB /* main.swift */; }; + 2888D17C2C99B3E80081FBBB /* KeyedDecodingContainer+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2888D17B2C99B3E80081FBBB /* KeyedDecodingContainer+.swift */; }; 288F12FB2B011A9300678FAD /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 288F12FA2B011A9300678FAD /* Localizable.xcstrings */; }; 28A0021B2AFBBFC300E2A01E /* TimeMachineStatusApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28A0021A2AFBBFC300E2A01E /* TimeMachineStatusApp.swift */; }; 28A0021D2AFBBFC300E2A01E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28A0021C2AFBBFC300E2A01E /* SettingsView.swift */; }; @@ -96,6 +97,7 @@ 28263FA22B023FFE00F74655 /* ServiceManagement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ServiceManagement.framework; path = System/Library/Frameworks/ServiceManagement.framework; sourceTree = SDKROOT; }; 28263FA42B0241E200F74655 /* TimeMachineStatusHelper.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = TimeMachineStatusHelper.entitlements; sourceTree = ""; }; 2885D6642B024D0B00C260DB /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + 2888D17B2C99B3E80081FBBB /* KeyedDecodingContainer+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyedDecodingContainer+.swift"; sourceTree = ""; }; 288F12FA2B011A9300678FAD /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 28A002172AFBBFC300E2A01E /* TimeMachineStatus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TimeMachineStatus.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28A0021A2AFBBFC300E2A01E /* TimeMachineStatusApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeMachineStatusApp.swift; sourceTree = ""; }; @@ -260,6 +262,7 @@ 2818227D2AFE36780067E564 /* Bundle+.swift */, 281822522AFCF97C0067E564 /* FormatStyle+.swift */, 281822662AFD86AC0067E564 /* Color+RawRepresentable.swift */, + 2888D17B2C99B3E80081FBBB /* KeyedDecodingContainer+.swift */, ); path = Extensions; sourceTree = ""; @@ -492,6 +495,7 @@ 2818227E2AFE36780067E564 /* Bundle+.swift in Sources */, 28FAD5AD2AFF0D7200F642E7 /* ExpandableSection.swift in Sources */, 2818225F2AFD3FF20067E564 /* Stopping.swift in Sources */, + 2888D17C2C99B3E80081FBBB /* KeyedDecodingContainer+.swift in Sources */, 28A0024F2AFC030500E2A01E /* Symbols.swift in Sources */, 28A0025E2AFC04D300E2A01E /* Mounting.swift in Sources */, 28FAD5AF2AFF0D9600F642E7 /* UserfacingErrorView.swift in Sources */, diff --git a/TimeMachineStatus/Extensions/KeyedDecodingContainer+.swift b/TimeMachineStatus/Extensions/KeyedDecodingContainer+.swift new file mode 100644 index 0000000..a449ce0 --- /dev/null +++ b/TimeMachineStatus/Extensions/KeyedDecodingContainer+.swift @@ -0,0 +1,24 @@ +// +// Decodable+.swift +// TimeMachineStatus +// +// Created by Lukas Pistrol on 17.09.24. +// +// Copyright © 2024 Lukas Pistrol. All rights reserved. +// +// See LICENSE.md for license information. +// + +import Foundation + +extension KeyedDecodingContainer { + func decodeBoolOrIntIfPresent(for key: K, defaultValue: Bool? = nil) throws -> Bool? { + if let boolValue = try? decodeIfPresent(Bool.self, forKey: key) { + return boolValue + } else if let intValue = try? decodeIfPresent(Int.self, forKey: key) { + return intValue == 1 + } else { + return defaultValue + } + } +} diff --git a/TimeMachineStatus/Localizable.xcstrings b/TimeMachineStatus/Localizable.xcstrings index 050f754..f2cf52b 100644 --- a/TimeMachineStatus/Localizable.xcstrings +++ b/TimeMachineStatus/Localizable.xcstrings @@ -1566,6 +1566,94 @@ } } }, + "settings_item_loglevel" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log Level" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log Level" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log Level" + } + } + } + }, + "settings_item_loglevel_debug" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Debug" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Debug" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Debug" + } + } + } + }, + "settings_item_loglevel_footer" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erfordert einen Neustart der App" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Requires restarting the app to take effect" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Per avere effetto è necessario riavviare l'app" + } + } + } + }, + "settings_item_loglevel_info" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Info (Standard)" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Info (Default)" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Info (Predefinito)" + } + } + } + }, "settings_item_showpercentage" : { "localizations" : { "de" : { diff --git a/TimeMachineStatus/Model/BackupState/BackupState.swift b/TimeMachineStatus/Model/BackupState/BackupState.swift index 2a7161a..8ea1ef7 100644 --- a/TimeMachineStatus/Model/BackupState/BackupState.swift +++ b/TimeMachineStatus/Model/BackupState/BackupState.swift @@ -19,7 +19,7 @@ enum BackupState { static func getState() throws -> _State { let result = try shellOut(to: Constants.Commands.status) - log.trace("Raw State: \(result)") + log.trace("Raw State: \"\(result)\"") guard let data = result.data(using: .utf8) else { throw BackupStateError.couldNotConvertStringToData(string: result) diff --git a/TimeMachineStatus/Model/Preferences/Preferences.swift b/TimeMachineStatus/Model/Preferences/Preferences.swift index 29bb65d..088a47f 100644 --- a/TimeMachineStatus/Model/Preferences/Preferences.swift +++ b/TimeMachineStatus/Model/Preferences/Preferences.swift @@ -25,6 +25,23 @@ struct Preferences: Decodable { case skipPaths = "SkipPaths" } + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.autoBackup = try container.decodeBoolOrIntIfPresent(for: .autoBackup) + self.autoBackupInterval = try container.decodeIfPresent(Int.self, forKey: .autoBackupInterval) + self.excludedVolumeUUIDs = try container.decodeIfPresent([UUID].self, forKey: .excludedVolumeUUIDs) + self.preferencesVersion = try container.decode(Int.self, forKey: .preferencesVersion) + self.requiresACPower = try container.decodeBoolOrIntIfPresent(for: .requiresACPower) + self.lastConfigurationTraceDate = try container.decodeIfPresent(Date.self, forKey: .lastConfigurationTraceDate) + self.lastDestinationID = try container.decodeIfPresent(UUID.self, forKey: .lastDestinationID) + self.localizedDiskImageVolumeName = try container.decodeIfPresent( + String.self, + forKey: .localizedDiskImageVolumeName + ) + self.destinations = try container.decodeIfPresent([Destination].self, forKey: .destinations) + self.skipPaths = try container.decodeIfPresent([String].self, forKey: .skipPaths) + } + let autoBackup: Bool? let autoBackupInterval: Int? let excludedVolumeUUIDs: [UUID]? diff --git a/TimeMachineStatus/TimeMachineStatusApp.swift b/TimeMachineStatus/TimeMachineStatusApp.swift index a714310..dde0c47 100644 --- a/TimeMachineStatus/TimeMachineStatusApp.swift +++ b/TimeMachineStatus/TimeMachineStatusApp.swift @@ -16,17 +16,20 @@ import SwiftUI @main struct TimeMachineStatusApp: App { + @AppStorage(StorageKeys.logLevel.id) + private var logLevel: Logging.Logger.Level = StorageKeys.logLevel.default + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate init() { - LoggingSystem.bootstrap { id in + LoggingSystem.bootstrap { [logLevel] id in var mpx = MultiplexLogHandler([ LoggingOSLog(label: id) ]) #if DEBUG mpx.logLevel = .trace #else - mpx.logLevel = .info + mpx.logLevel = logLevel #endif return mpx } diff --git a/TimeMachineStatus/ViewModel/TMUtility.swift b/TimeMachineStatus/ViewModel/TMUtility.swift index 7245710..01965fd 100644 --- a/TimeMachineStatus/ViewModel/TMUtility.swift +++ b/TimeMachineStatus/ViewModel/TMUtility.swift @@ -81,10 +81,14 @@ class TMUtility: ObservableObject { func startBackup(id: UUID? = nil) { do { - _ = if let id { - try shellOut(to: Constants.Commands.startBackup(id: id)) + if let id { + log.info("Starting backup with id: \(id)") + let result = try shellOut(to: Constants.Commands.startBackup(id: id)) + log.trace("Started backup: \(result)") } else { - try shellOut(to: Constants.Commands.startBackup()) + log.info("Starting backup") + let result = try shellOut(to: Constants.Commands.startBackup()) + log.trace("Started backup: \(result)") } start(force: true) } catch { @@ -94,8 +98,9 @@ class TMUtility: ObservableObject { func stopBackup() { do { - let response = try shellOut(to: Constants.Commands.stopBackup) - print(response) + log.info("Stopping backup") + let result = try shellOut(to: Constants.Commands.stopBackup) + log.trace("Stopped backup: \(result)") start(force: true) } catch { log.error("Error stopping backup: \(error)") @@ -104,7 +109,9 @@ class TMUtility: ObservableObject { func launchTimeMachine() { do { - _ = try shellOut(to: Constants.Commands.launchTimeMachine) + log.info("Launching time machine") + let result = try shellOut(to: Constants.Commands.launchTimeMachine) + log.trace("Launched time machine: \(result)") } catch { log.error("Error launching time machine: \(error)") } diff --git a/TimeMachineStatus/ViewModel/UpdaterViewModel.swift b/TimeMachineStatus/ViewModel/UpdaterViewModel.swift index 384a65a..5367b54 100644 --- a/TimeMachineStatus/ViewModel/UpdaterViewModel.swift +++ b/TimeMachineStatus/ViewModel/UpdaterViewModel.swift @@ -11,13 +11,17 @@ import Combine import Foundation +import Logging import Sparkle class UpdaterViewModel: ObservableObject { - @Published private (set) var canCheckForUpdates = false + + private let log = Logger(label: "\(Bundle.identifier).UpdaterViewModel") + + @Published private(set) var canCheckForUpdates = false @Published var automaticallyChecksForUpdates: Bool { didSet { - print(automaticallyChecksForUpdates) + log.debug("Automatically checks for update changed: \(automaticallyChecksForUpdates)") updater.automaticallyChecksForUpdates = automaticallyChecksForUpdates } } diff --git a/TimeMachineStatus/Views/SettingsView.swift b/TimeMachineStatus/Views/SettingsView.swift index 203d7fe..9a96ce5 100644 --- a/TimeMachineStatus/Views/SettingsView.swift +++ b/TimeMachineStatus/Views/SettingsView.swift @@ -9,6 +9,7 @@ // See LICENSE.md for license information. // +import Logging import Sparkle import SwiftUI @@ -29,6 +30,8 @@ enum StorageKeys { static let cornerRadius = Key(id: "cornerRadius", default: 5.0) static let showPercentage = Key(id: "showPercentage", default: true) static let animateIcon = Key(id: "animateIcon", default: true) + + static let logLevel = Key(id: "logLevel", default: Logger.Level.info) } struct SettingsView: View { @@ -63,6 +66,9 @@ struct SettingsView: View { @AppStorage(StorageKeys.animateIcon.id) private var animateIcon: Bool = StorageKeys.animateIcon.default + @AppStorage(StorageKeys.logLevel.id) + private var logLevel: Logger.Level = StorageKeys.logLevel.default + private enum Tabs: Hashable, CaseIterable { case general case appearance @@ -71,8 +77,8 @@ struct SettingsView: View { var height: Double { switch self { case .about: 350 - case .appearance: 410 - case .general: 250 + case .appearance: 450 + case .general: 320 } } @@ -134,6 +140,17 @@ struct SettingsView: View { isOn: $updaterViewModel.automaticallyChecksForUpdates ) } + Section { + Picker("settings_item_loglevel", selection: $logLevel) { + Text("settings_item_loglevel_debug").tag(Logger.Level.trace) + Text("settings_item_loglevel_info").tag(Logger.Level.info) + } + } footer: { + Text("settings_item_loglevel_footer") + .font(.footnote) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } } .formStyle(.grouped) .tabItem { diff --git a/TimeMachineStatus/Views/StatusBarItem.swift b/TimeMachineStatus/Views/StatusBarItem.swift index 3393530..3756b1a 100644 --- a/TimeMachineStatus/Views/StatusBarItem.swift +++ b/TimeMachineStatus/Views/StatusBarItem.swift @@ -10,6 +10,7 @@ // import Combine +import Logging import SwiftUI struct ItemSizePreferenceKey: PreferenceKey { @@ -57,6 +58,8 @@ struct StatusBarItem: View { var sizePassthrough: PassthroughSubject @ObservedObject var utility: TMUtility + private let log = Logger(label: "\(Bundle.identifier).StatusBarItem") + private var mainContent: some View { HStack(spacing: spacing) { if utility.isIdle { @@ -98,12 +101,12 @@ struct StatusBarItem: View { } ) .onPreferenceChange(ItemSizePreferenceKey.self) { size in - print("Size: \(size)") + log.trace("Size: \(size)") sizePassthrough.send(size) } .offset(y: -1) .onChange(of: utility.isIdle) { oldValue, newValue in - print("Changed: \(oldValue) -> \(newValue)") + log.trace("Changed: \(oldValue) -> \(newValue)") } }