diff --git a/TimeMachineStatus.xcodeproj/project.pbxproj b/TimeMachineStatus.xcodeproj/project.pbxproj index af832d3..fb496f8 100644 --- a/TimeMachineStatus.xcodeproj/project.pbxproj +++ b/TimeMachineStatus.xcodeproj/project.pbxproj @@ -525,7 +525,7 @@ CODE_SIGN_ENTITLEMENTS = TimeMachineStatusHelper/TimeMachineStatusHelper.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -538,7 +538,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.0.5; + MARKETING_VERSION = 0.0.6; PRODUCT_BUNDLE_IDENTIFIER = com.lukaspistrol.TimeMachineStatusHelper; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -554,7 +554,7 @@ CODE_SIGN_ENTITLEMENTS = TimeMachineStatusHelper/TimeMachineStatusHelper.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; GENERATE_INFOPLIST_FILE = YES; @@ -567,7 +567,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.0.5; + MARKETING_VERSION = 0.0.6; PRODUCT_BUNDLE_IDENTIFIER = com.lukaspistrol.TimeMachineStatusHelper; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -702,7 +702,7 @@ CODE_SIGN_ENTITLEMENTS = TimeMachineStatus/TimeMachineStatus.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_ASSET_PATHS = "\"TimeMachineStatus/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; @@ -719,7 +719,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.0.5; + MARKETING_VERSION = 0.0.6; PRODUCT_BUNDLE_IDENTIFIER = com.lukaspistrol.TimeMachineStatus; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -736,7 +736,7 @@ CODE_SIGN_ENTITLEMENTS = TimeMachineStatus/TimeMachineStatus.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_ASSET_PATHS = "\"TimeMachineStatus/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = NO; @@ -753,7 +753,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.0.5; + MARKETING_VERSION = 0.0.6; PRODUCT_BUNDLE_IDENTIFIER = com.lukaspistrol.TimeMachineStatus; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/TimeMachineStatus/Assets.xcassets/Icon-plain.imageset/Contents.json b/TimeMachineStatus/Assets.xcassets/Icon-plain.imageset/Contents.json new file mode 100644 index 0000000..0c24c4b --- /dev/null +++ b/TimeMachineStatus/Assets.xcassets/Icon-plain.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "Icon-plain.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Icon-plain@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Icon-plain@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/TimeMachineStatus/Assets.xcassets/Icon-plain.imageset/Icon-plain.png b/TimeMachineStatus/Assets.xcassets/Icon-plain.imageset/Icon-plain.png new file mode 100644 index 0000000..b956982 Binary files /dev/null and b/TimeMachineStatus/Assets.xcassets/Icon-plain.imageset/Icon-plain.png differ diff --git a/TimeMachineStatus/Assets.xcassets/Icon-plain.imageset/Icon-plain@2x.png b/TimeMachineStatus/Assets.xcassets/Icon-plain.imageset/Icon-plain@2x.png new file mode 100644 index 0000000..76455f4 Binary files /dev/null and b/TimeMachineStatus/Assets.xcassets/Icon-plain.imageset/Icon-plain@2x.png differ diff --git a/TimeMachineStatus/Assets.xcassets/Icon-plain.imageset/Icon-plain@3x.png b/TimeMachineStatus/Assets.xcassets/Icon-plain.imageset/Icon-plain@3x.png new file mode 100644 index 0000000..031d65b Binary files /dev/null and b/TimeMachineStatus/Assets.xcassets/Icon-plain.imageset/Icon-plain@3x.png differ diff --git a/TimeMachineStatus/Localizable.xcstrings b/TimeMachineStatus/Localizable.xcstrings index 79caeeb..9209fad 100644 --- a/TimeMachineStatus/Localizable.xcstrings +++ b/TimeMachineStatus/Localizable.xcstrings @@ -307,6 +307,39 @@ } } }, + "dest_label_no_size_info" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine Speicherplatz Informationen verfügbar" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No storage info available" + } + } + } + }, + "dest_label_no_volume_name" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unbekannt" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unknown" + } + } + } + }, "dest_label_progress_%lld_files_%@" : { "extractionState" : "manual", "localizations" : { @@ -348,6 +381,46 @@ } } }, + "dest_label_progress_found%lld" : { + "localizations" : { + "de" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Änderung gefunden" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Änderungen gefunden" + } + } + } + } + }, + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld change found" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld changes found" + } + } + } + } + } + } + }, "error_debug_description%@" : { "localizations" : { "de" : { @@ -533,6 +606,22 @@ } } }, + "label_currentbackup_%@" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Backup auf \"%@\" läuft..." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Backing up to \"%@\"..." + } + } + } + }, "label_lastbackup_%@_on_%@" : { "extractionState" : "manual", "localizations" : { diff --git a/TimeMachineStatus/Model/Preferences/Preferences.swift b/TimeMachineStatus/Model/Preferences/Preferences.swift index 941172e..29bb65d 100644 --- a/TimeMachineStatus/Model/Preferences/Preferences.swift +++ b/TimeMachineStatus/Model/Preferences/Preferences.swift @@ -25,19 +25,19 @@ struct Preferences: Decodable { case skipPaths = "SkipPaths" } - let autoBackup: Bool + let autoBackup: Bool? let autoBackupInterval: Int? let excludedVolumeUUIDs: [UUID]? let preferencesVersion: Int - let requiresACPower: Bool - let lastConfigurationTraceDate: Date + let requiresACPower: Bool? + let lastConfigurationTraceDate: Date? let lastDestinationID: UUID? - let localizedDiskImageVolumeName: String + let localizedDiskImageVolumeName: String? let skipPaths: [String]? let destinations: [Destination]? var latestBackupDate: Date? { - destinations?.map(\.snapshotDates).flatMap { $0 }.max() + destinations?.compactMap(\.snapshotDates).flatMap { $0 }.max() } var latestBackupVolume: String? { @@ -64,16 +64,16 @@ struct Destination: Decodable { case attemptDates = "AttemptDates" } - let lastKnownVolumeName: String - let bytesUsed: Int - let bytesAvailable: Int - let filesystemTypeName: String - let lastKnownEncryptionState: String + let lastKnownVolumeName: String? + let bytesUsed: Int? + let bytesAvailable: Int? + let filesystemTypeName: String? + let lastKnownEncryptionState: String? let quotaGB: Double? let networkURL: String? let destinationID: UUID - let consistencyScanDate: Date - let referenceLocalSnapshotDate: Date - let snapshotDates: [Date] - let attemptDates: [Date] + let consistencyScanDate: Date? + let referenceLocalSnapshotDate: Date? + let snapshotDates: [Date]? + let attemptDates: [Date]? } diff --git a/TimeMachineStatus/Views/DestinationCell.swift b/TimeMachineStatus/Views/DestinationCell.swift index ad58487..5603873 100644 --- a/TimeMachineStatus/Views/DestinationCell.swift +++ b/TimeMachineStatus/Views/DestinationCell.swift @@ -24,6 +24,13 @@ struct DestinationCell: View { utility.status.activeDestinationID == dest.destinationID } + private var findingChanges: BackupState.FindingChanges? { + if let findingChanges = utility.status as? BackupState.FindingChanges { + return findingChanges + } + return nil + } + private var copying: BackupState.Copying? { if let copying = utility.status as? BackupState.Copying { return copying @@ -85,23 +92,28 @@ struct DestinationCell: View { private var volumeInfo: some View { VStack(alignment: .leading) { HStack { - Text(dest.lastKnownVolumeName) + Text(dest.lastKnownVolumeName ?? "dest_label_no_volume_name") .font(.headline) - if let latest = dest.snapshotDates.max() { + if let latest = dest.snapshotDates?.max() { Text(latest.formatted(.relativeDate)) .font(.caption2) .foregroundStyle(.secondary) } } - HStack { - let bytesUsed = dest.bytesUsed.formatted(byteFormat) - let bytesAvailable = dest.bytesAvailable.formatted(byteFormat) - Text("dest_label_\(bytesUsed)_used_\(bytesAvailable)_free") + if let bytesUsed = dest.bytesUsed, let bytesAvailable = dest.bytesAvailable { + let used = bytesUsed.formatted(byteFormat) + let available = bytesAvailable.formatted(byteFormat) + Text("dest_label_\(used)_used_\(available)_free") .monospacedDigit() + .font(.caption2) + .foregroundStyle(.secondary) + } else { + Text("dest_label_no_size_info") + .font(.caption2) + .foregroundStyle(.secondary) } - .font(.caption2) - .foregroundStyle(.secondary) } + .lineLimit(1) } @ViewBuilder @@ -130,9 +142,10 @@ struct DestinationCell: View { @ViewBuilder private var contextMenuActions: some View { + let unknown = NSLocalizedString("dest_label_no_volume_name", comment: "") Button("button_show_info") { showInfo.toggle() } Divider() - Button("button_backup_to_\(dest.lastKnownVolumeName)_now") { + Button("button_backup_to_\(dest.lastKnownVolumeName ?? unknown)_now") { utility.startBackup(id: dest.destinationID) } } @@ -145,6 +158,9 @@ struct DestinationCell: View { Text(state.statusString) } Spacer() + if let findingChanges { + Text("dest_label_progress_found\(findingChanges.itemsFound)") + } if let copying { if let bytes = copying.progress.bytes, let files = copying.progress.files { Text("dest_label_progress_\(files)_files_\(bytes.formatted(byteFormat))") diff --git a/TimeMachineStatus/Views/DestinationInfoView.swift b/TimeMachineStatus/Views/DestinationInfoView.swift index f019564..9e3597c 100644 --- a/TimeMachineStatus/Views/DestinationInfoView.swift +++ b/TimeMachineStatus/Views/DestinationInfoView.swift @@ -16,25 +16,35 @@ struct DestinationInfoView: View { var body: some View { Form { Section { - LabeledContent("dest_info_name", value: dest.lastKnownVolumeName) - LabeledContent("dest_info_encrypted", value: dest.lastKnownEncryptionState) + if let lastKnownVolumeName = dest.lastKnownVolumeName { + LabeledContent("dest_info_name", value: lastKnownVolumeName) + } + if let lastKnownEncryptionState = dest.lastKnownEncryptionState { + LabeledContent("dest_info_encrypted", value: lastKnownEncryptionState) + } if let networkURL = dest.networkURL { LabeledContent("dest_info_url", value: networkURL) } } Section { - LabeledContent("dest_info_filesystem", value: dest.filesystemTypeName) - LabeledContent("dest_info_usedspace", value: dest.bytesUsed.formatted(.byteCount(style: .file))) - LabeledContent("dest_info_freespace", value: dest.bytesAvailable.formatted(.byteCount(style: .file))) + if let filesystemTypeName = dest.filesystemTypeName { + LabeledContent("dest_info_filesystem", value: filesystemTypeName) + } + if let bytesUsed = dest.bytesUsed { + LabeledContent("dest_info_usedspace", value: bytesUsed.formatted(.byteCount(style: .file))) + } + if let bytesAvailable = dest.bytesAvailable { + LabeledContent("dest_info_freespace", value: bytesAvailable.formatted(.byteCount(style: .file))) + } if let quotaGB = dest.quotaGB { LabeledContent("dest_info_quota", value: Int(quotaGB * 1e9).formatted(.byteCount(style: .file))) } } Section { - if let last = dest.snapshotDates.max() { + if let last = dest.snapshotDates?.max() { LabeledContent("dest_info_lastbackup", value: last.formatted(.relativeDate)) } - if let last = dest.attemptDates.max() { + if let last = dest.attemptDates?.max() { LabeledContent("dest_info_lastattempt", value: last.formatted(.relativeDate)) } } diff --git a/TimeMachineStatus/Views/MenuView.swift b/TimeMachineStatus/Views/MenuView.swift index 0321e6e..76bbfbc 100644 --- a/TimeMachineStatus/Views/MenuView.swift +++ b/TimeMachineStatus/Views/MenuView.swift @@ -63,27 +63,33 @@ struct MenuView: View { if let preferences = utility.preferences { ExpandableSection(expanded: false) { VStack { - LabeledContent("general_info_volumename", value: preferences.localizedDiskImageVolumeName) - .padding(10) - .card(.background.secondary) - LabeledContent("general_info_autobackup", value: preferences.autoBackup ? "Enabled" : "Disabled") - .padding(10) - .card(.background.secondary) - if let interval = preferences.autoBackupInterval, preferences.autoBackup { - let measurement = Measurement( - value: Double(interval), - unit: UnitDuration.seconds - ).converted(to: .hours) - LabeledContent( - "general_info_interval", - value: measurement.formatted(.measurement(width: .wide)) - ) - .padding(10) - .card(.background.secondary) + if let volumeName = preferences.localizedDiskImageVolumeName { + LabeledContent("general_info_volumename", value: volumeName) + .padding(10) + .card(.background.secondary) + } + if let autoBackup = preferences.autoBackup { + LabeledContent("general_info_autobackup", value: autoBackup ? "Enabled" : "Disabled") + .padding(10) + .card(.background.secondary) + if let interval = preferences.autoBackupInterval, autoBackup { + let measurement = Measurement( + value: Double(interval), + unit: UnitDuration.seconds + ).converted(to: .hours) + LabeledContent( + "general_info_interval", + value: measurement.formatted(.measurement(width: .wide)) + ) + .padding(10) + .card(.background.secondary) + } + } + if let requiresACPower = preferences.requiresACPower { + LabeledContent("general_info_requirespower", value: requiresACPower ? "Yes" : "No") + .padding(10) + .card(.background.secondary) } - LabeledContent("general_info_requirespower", value: preferences.requiresACPower ? "Yes" : "No") - .padding(10) - .card(.background.secondary) LabeledContent("general_info_skippaths") { VStack(alignment: .trailing, spacing: 4) { if let skipPaths = preferences.skipPaths { @@ -118,7 +124,43 @@ struct MenuView: View { Label("button_startbackup", systemImage: utility.isIdle ? Symbols.playFill() : Symbols.stopFill()) } .focusable(false) + toolbarStatus + Spacer() + + toolbarMenu + } + .lineLimit(1) + .imageScale(.large) + .labelStyle(.iconOnly) + .buttonStyle(.custom) + .padding(.horizontal) + .frame(height: 42) + .frame(maxWidth: .infinity) + .background(Material.bar, in: .rect) + .overlay(alignment: .top) { + Divider() + } + } + @ViewBuilder + private var toolbarStatus: some View { + if let activeUUID = utility.status.activeDestinationID, + let destination = utility.preferences?.destinations?.first(where: { $0.destinationID == activeUUID }) { + let unknown = NSLocalizedString("dest_label_no_volume_name", comment: "") + VStack(alignment: .leading, spacing: 0) { + Text("label_currentbackup_\(destination.lastKnownVolumeName ?? unknown)") + HStack(spacing: 0) { + Text(utility.status.statusString) + if let percentage = utility.status.progessPercentage { + Text(verbatim: " – " + percentage.formatted(.percent.precision(.fractionLength(0)))) + } + } + .font(.caption) + .opacity(0.8) + } + .foregroundStyle(.secondary) + .font(.caption2) + } else { if let latestDate = utility.preferences?.latestBackupDate, let latestVolume = utility.preferences?.latestBackupVolume { VStack(alignment: .leading, spacing: 0) { @@ -136,42 +178,44 @@ struct MenuView: View { .foregroundStyle(.secondary) .font(.caption2) } - Spacer() + } + } - Menu { - SettingsLink { - Text("settings_button_settings") - } - .keyboardShortcut(",", modifiers: .command) - Button("settings_button_checkforupdates") { - updater.checkForUpdates() - } - .disabled(!updaterViewModel.canCheckForUpdates) - Button("button_browsebackups") { - utility.launchTimeMachine() - } - Divider() - Button { - NSApp.terminate(nil) - } label: { - Text("button_quit") - } - .keyboardShortcut("q", modifiers: .command) - } label: { - Label("settings_button_settings", systemImage: Symbols.gearshapeFill()) + private var toolbarMenu: some View { + Menu { + SettingsLink { + Text("settings_button_settings") + } + .keyboardShortcut(",", modifiers: .command) + Button("settings_button_checkforupdates") { + updater.checkForUpdates() + } + .disabled(!updaterViewModel.canCheckForUpdates) + Button("button_browsebackups") { + utility.launchTimeMachine() } - .focusable(false) - } - .imageScale(.large) - .labelStyle(.iconOnly) - .buttonStyle(.custom) - .padding(.horizontal) - .frame(height: 40) - .frame(maxWidth: .infinity) - .background(Material.bar, in: .rect) - .overlay(alignment: .top) { Divider() + Button { + NSApp.terminate(nil) + } label: { + Text("button_quit") + } + .keyboardShortcut("q", modifiers: .command) + } label: { + Label { + Text("settings_button_settings") + } icon: { + Image(.iconPlain) + .shadow( + color: .black.opacity(0.8), + radius: 0.5, + x: 0, + y: 0.5 + ) + } } + .focusable(false) + .buttonStyle(.plain) } }