diff --git a/.swiftlint.yml b/.swiftlint.yml index 5f82533..a0ba7e8 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -63,3 +63,8 @@ custom_rules: message: "Use `SFSafeSymbols` via `systemSymbol` parameters for type safety." regex: "(Image\\(systemName:)|(NSImage\\(symbolName:)|(Label[^,]+?,\\s*systemImage:)|(UIApplicationShortcutIcon\\(systemImageName:)" severity: warning + no_print_statements: + name: "No print statements" + message: "Use the `Logging` package instead." + regex: "(print\\(.*\\))|(debugPrint\\(.*\\))" + severity: warning diff --git a/TimeMachineStatus.xcodeproj/project.pbxproj b/TimeMachineStatus.xcodeproj/project.pbxproj index 523adf5..3457bbb 100644 --- a/TimeMachineStatus.xcodeproj/project.pbxproj +++ b/TimeMachineStatus.xcodeproj/project.pbxproj @@ -190,6 +190,7 @@ 28FAD5A62AFE3DBF00F642E7 /* CardViewModifier.swift */, 28FAD5AC2AFF0D7200F642E7 /* ExpandableSection.swift */, 28FAD5AE2AFF0D9600F642E7 /* UserfacingErrorView.swift */, + 281C819D2CBC887D00CDFED6 /* HiddenViewModifier+.swift */, ); path = Components; sourceTree = ""; @@ -281,7 +282,6 @@ 281822662AFD86AC0067E564 /* Color+RawRepresentable.swift */, 2888D17B2C99B3E80081FBBB /* KeyedDecodingContainer+.swift */, 28710D2E2CAF16AF00033855 /* Timer+Sendable.swift */, - 281C819D2CBC887D00CDFED6 /* HiddenViewModifier+.swift */, ); path = Extensions; sourceTree = ""; diff --git a/TimeMachineStatus/Components/CustomLabeledContentStyle.swift b/TimeMachineStatus/Components/CustomLabeledContentStyle.swift index a9cdba1..a00c702 100644 --- a/TimeMachineStatus/Components/CustomLabeledContentStyle.swift +++ b/TimeMachineStatus/Components/CustomLabeledContentStyle.swift @@ -28,3 +28,9 @@ struct CustomLabeledContentStyle: LabeledContentStyle { } } } + +extension LabeledContentStyle where Self == CustomLabeledContentStyle { + static var custom: CustomLabeledContentStyle { + CustomLabeledContentStyle() + } +} diff --git a/TimeMachineStatus/Components/ExpandableSection.swift b/TimeMachineStatus/Components/ExpandableSection.swift index 233156f..5ca7240 100644 --- a/TimeMachineStatus/Components/ExpandableSection.swift +++ b/TimeMachineStatus/Components/ExpandableSection.swift @@ -51,3 +51,15 @@ struct ExpandableSection: View { } } } + +#Preview { + List { + ExpandableSection { + Text("Item 1") + Text("Item 2") + Text("Item 3") + } header: { + Text("Header") + } + } +} diff --git a/TimeMachineStatus/Extensions/HiddenViewModifier+.swift b/TimeMachineStatus/Components/HiddenViewModifier+.swift similarity index 100% rename from TimeMachineStatus/Extensions/HiddenViewModifier+.swift rename to TimeMachineStatus/Components/HiddenViewModifier+.swift diff --git a/TimeMachineStatus/Localizable.xcstrings b/TimeMachineStatus/Localizable.xcstrings index 234ad11..75fbb57 100644 --- a/TimeMachineStatus/Localizable.xcstrings +++ b/TimeMachineStatus/Localizable.xcstrings @@ -426,6 +426,7 @@ } }, "D" : { + "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { @@ -433,6 +434,12 @@ "value" : "D" } }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "D" + } + }, "it" : { "stringUnit" : { "state" : "translated", @@ -464,6 +471,28 @@ } } }, + "dest_info_error" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fehler Code" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error Code" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Codice errore" + } + } + } + }, "dest_info_filesystem" : { "extractionState" : "manual", "localizations" : { @@ -579,6 +608,28 @@ } } }, + "dest_info_number_of_backups" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anzahl an Sicherungen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Number of backups" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Numero di backup" + } + } + } + }, "dest_info_quota" : { "extractionState" : "manual", "localizations" : { @@ -671,25 +722,25 @@ } } }, - "dest_label_last_backup_failed" : { + "dest_label_last_backup_failed_%lld" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Letztes Backup fehlgeschlagen. TimeMachine Einstellungen öffnen um mehr zu erfahren." + "value" : "Letztes Backup fehlgeschlagen (%lld). TimeMachine Einstellungen öffnen um mehr zu erfahren." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Last Backup Failed. Open TimeMachine Settings for more information." + "value" : "Last Backup Failed (%lld). Open TimeMachine Settings for more information." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Ultimo backup non riuscito. Apri le Impostazioni di TimeMachine per ulteriori informazioni." + "value" : "Ultimo backup non riuscito (%lld). Apri le Impostazioni di TimeMachine per ulteriori informazioni." } } } diff --git a/TimeMachineStatus/Model/BackupState/BackupState.swift b/TimeMachineStatus/Model/BackupState/BackupState.swift index 5a1d905..542587e 100644 --- a/TimeMachineStatus/Model/BackupState/BackupState.swift +++ b/TimeMachineStatus/Model/BackupState/BackupState.swift @@ -28,12 +28,12 @@ enum BackupState { if let state = try? _decodePlist(data, raw: result) { return state } + #if DEBUG log.error("Unknown state: \(result)") - fatalError("Unknown state: \(result)") - #else - throw BackupStateError.invalidState(raw: result) #endif + + throw BackupStateError.invalidState(raw: result) } } @@ -72,11 +72,8 @@ extension BackupState { case ._thinning: return try decoder.decode(BackupState.Thinning.self, from: data) case ._unknown(let state): - #if DEBUG - throw BackupStateError.invalidState(raw: raw) - #else + log.warning("Unknown state: \(state), raw: \(raw)") return BackupState.Unknown(title: state, raw: raw) - #endif } } } diff --git a/TimeMachineStatus/Model/Preferences/Preferences.swift b/TimeMachineStatus/Model/Preferences/Preferences.swift index b967490..9c6bfb2 100644 --- a/TimeMachineStatus/Model/Preferences/Preferences.swift +++ b/TimeMachineStatus/Model/Preferences/Preferences.swift @@ -105,6 +105,7 @@ struct Destination: Decodable { case referenceLocalSnapshotDate = "ReferenceLocalSnapshotDate" case snapshotDates = "SnapshotDates" case attemptDates = "AttemptDates" + case result = "RESULT" } let lastKnownVolumeName: String? @@ -119,6 +120,7 @@ struct Destination: Decodable { let referenceLocalSnapshotDate: Date? let snapshotDates: [Date]? let attemptDates: [Date]? + let result: Int? var lastBackupFailed: Bool { guard let snapshotDates, @@ -129,6 +131,10 @@ struct Destination: Decodable { return lastAttempt > last } + + var numberOfBackups: Int { + snapshotDates?.count ?? 0 + } } extension Preferences { @@ -160,7 +166,8 @@ extension Destination { consistencyScanDate: .distantPast, referenceLocalSnapshotDate: .now, snapshotDates: [.distantPast, .now.addingTimeInterval(.random(in: -100_000...0))], - attemptDates: [.distantPast, .now.addingTimeInterval(.random(in: -100_000...0))] + attemptDates: [.distantPast, .now.addingTimeInterval(.random(in: -100_000...0))], + result: 0 ) } } diff --git a/TimeMachineStatus/Views/DestinationCell.swift b/TimeMachineStatus/Views/DestinationCell.swift index 52842b6..d398513 100644 --- a/TimeMachineStatus/Views/DestinationCell.swift +++ b/TimeMachineStatus/Views/DestinationCell.swift @@ -61,6 +61,9 @@ struct DestinationCell: View { .contentShape(.rect) .contextMenu { contextMenuActions } .card(.background.secondary) + .onTapGesture { + showInfo.toggle() + } .onHover { hovering in withAnimation(.snappy) { self.hovering = hovering @@ -141,7 +144,7 @@ struct DestinationCell: View { } .buttonStyle(.plain) .focusable(false) - .help("dest_label_last_backup_failed") + .help("dest_label_last_backup_failed_\(dest.result ?? -1)") } } diff --git a/TimeMachineStatus/Views/DestinationInfoView.swift b/TimeMachineStatus/Views/DestinationInfoView.swift index 9e3597c..688ca3d 100644 --- a/TimeMachineStatus/Views/DestinationInfoView.swift +++ b/TimeMachineStatus/Views/DestinationInfoView.swift @@ -22,7 +22,7 @@ struct DestinationInfoView: View { if let lastKnownEncryptionState = dest.lastKnownEncryptionState { LabeledContent("dest_info_encrypted", value: lastKnownEncryptionState) } - if let networkURL = dest.networkURL { + if let networkURL = dest.networkURL?.removingPercentEncoding { LabeledContent("dest_info_url", value: networkURL) } } @@ -41,6 +41,7 @@ struct DestinationInfoView: View { } } Section { + LabeledContent("dest_info_number_of_backups", value: "\(dest.numberOfBackups)") if let last = dest.snapshotDates?.max() { LabeledContent("dest_info_lastbackup", value: last.formatted(.relativeDate)) } @@ -48,9 +49,14 @@ struct DestinationInfoView: View { LabeledContent("dest_info_lastattempt", value: last.formatted(.relativeDate)) } } + Section { + if let errorCode = dest.result, errorCode != 0 { + LabeledContent("dest_info_error", value: "\(errorCode)") + } + } } .formStyle(.grouped) - .labeledContentStyle(CustomLabeledContentStyle()) + .labeledContentStyle(.custom) .frame(width: 500) } } diff --git a/TimeMachineStatus/Views/MenuView.swift b/TimeMachineStatus/Views/MenuView.swift index 54489af..743ba82 100644 --- a/TimeMachineStatus/Views/MenuView.swift +++ b/TimeMachineStatus/Views/MenuView.swift @@ -118,7 +118,7 @@ struct MenuView: View { } header: { Text("section_general_info") } - .labeledContentStyle(CustomLabeledContentStyle()) + .labeledContentStyle(.custom) } }