From 934ef97d0f258a4615f6dc0fb2aaec78638ea4ee Mon Sep 17 00:00:00 2001 From: Lynnnya <125717287+Lynnnya@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:48:21 -0500 Subject: [PATCH 1/3] changed button colors from accent color to white, and changed the stop icon from stopfil to xmark --- .gitignore | 1 + .../Components/CustomButtonStyle.swift | 6 +- TimeMachineStatus/Views/DestinationCell.swift | 419 +++++++------ TimeMachineStatus/Views/MenuView.swift | 552 +++++++++--------- 4 files changed, 493 insertions(+), 485 deletions(-) diff --git a/.gitignore b/.gitignore index e8426f5..5a7d0c6 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,4 @@ fastlane/test_output iOSInjectionProject/ .env +.DS_Store \ No newline at end of file diff --git a/TimeMachineStatus/Components/CustomButtonStyle.swift b/TimeMachineStatus/Components/CustomButtonStyle.swift index 13f7c25..f49fbdc 100644 --- a/TimeMachineStatus/Components/CustomButtonStyle.swift +++ b/TimeMachineStatus/Components/CustomButtonStyle.swift @@ -24,12 +24,12 @@ struct CustomButtonStyle: ButtonStyle { private func foreground(_ configuration: Configuration) -> Color { if isEnabled { if configuration.isPressed { - Color.accentColor.opacity(0.5) + return .white.opacity(0.5) // Changed from accentColor } else { - Color.accentColor + return .white // Changed from accentColor } } else { - Color.secondary + return .secondary } } } diff --git a/TimeMachineStatus/Views/DestinationCell.swift b/TimeMachineStatus/Views/DestinationCell.swift index 2067880..8d39dd9 100644 --- a/TimeMachineStatus/Views/DestinationCell.swift +++ b/TimeMachineStatus/Views/DestinationCell.swift @@ -7,260 +7,259 @@ // Copyright © 2023 Lukas Pistrol. All rights reserved. // // See LICENSE.md for license information. -// +// +import Sparkle import SwiftUI struct DestinationCell: View { - @State private var utility: any TMUtility + @State private var utility: any TMUtility - let dest: Destination + let dest: Destination - init(_ dest: Destination, utility: any TMUtility) { - self.dest = dest - self.utility = utility - } + init(_ dest: Destination, utility: any TMUtility) { + self.dest = dest + self.utility = utility + } - private var isActive: Bool { - utility.status.activeDestinationID == dest.destinationID - } + private var isActive: Bool { + utility.status.activeDestinationID == dest.destinationID + } - private var findingChanges: BackupState.FindingChanges? { - if let findingChanges = utility.status as? BackupState.FindingChanges { - return findingChanges - } - return nil + 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 - } - return nil + private var copying: BackupState.Copying? { + if let copying = utility.status as? BackupState.Copying { + return copying } + return nil + } - @State private var showInfo: Bool = false - @State private var hovering: Bool = false + @State private var showInfo: Bool = false + @State private var hovering: Bool = false - var body: some View { - VStack(spacing: 0) { - HStack { - symbol - volumeInfo - Spacer() - failureWarning - progressIndicator - startStopButton - } - .padding(.vertical, 4) - .padding(.horizontal, 8) - .padding(.trailing, 4) - status - } - .overlay { hoverOverlay } - .contentShape(.rect) - .contextMenu { contextMenuActions } - .card(.background.secondary) - .onTapGesture { - showInfo.toggle() - } - .onHover { hovering in - withAnimation(.snappy) { - self.hovering = hovering - } - } - .popover(isPresented: $showInfo) { - DestinationInfoView(dest: dest) - } + var body: some View { + VStack(spacing: 0) { + HStack { + symbol + volumeInfo + Spacer() + failureWarning + progressIndicator + startStopButton + } + .padding(.vertical, 4) + .padding(.horizontal, 8) + .padding(.trailing, 4) + status } - - @ViewBuilder - private var hoverOverlay: some View { - if hovering { - Rectangle() - .fill(.fill.secondary.opacity(0.5)) - .allowsHitTesting(false) - } + .overlay { hoverOverlay } + .contentShape(.rect) + .contextMenu { contextMenuActions } + .card(.background.secondary) + .onTapGesture { + showInfo.toggle() } + .onHover { hovering in + withAnimation(.snappy) { + self.hovering = hovering + } + } + .popover(isPresented: $showInfo) { + DestinationInfoView(dest: dest) + } + } - private var symbol: some View { - Group { - if dest.networkURL != nil { - Image(systemSymbol: .externaldriveFillBadgeWifi) - } else { - Image(systemSymbol: .externaldriveFillBadgeTimemachine) - } - } - .imageScale(.large) - .font(.title2) - .foregroundStyle(.secondary) + @ViewBuilder + private var hoverOverlay: some View { + if hovering { + Rectangle() + .fill(.fill.secondary.opacity(0.5)) + .allowsHitTesting(false) } + } - private var volumeInfo: some View { - VStack(alignment: .leading) { - HStack { - Text(dest.lastKnownVolumeName ?? "dest_label_no_volume_name") - .font(.headline) - if let latest = dest.snapshotDates?.max() { - Text(latest.formatted(.relativeDate)) - .font(.caption2) - .foregroundStyle(.secondary) - } - } - 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) - } - } - .lineLimit(1) + private var symbol: some View { + Group { + if dest.networkURL != nil { + Image(systemSymbol: .externaldriveFillBadgeWifi) + } else { + Image(systemSymbol: .externaldriveFillBadgeTimemachine) + } } + .imageScale(.large) + .font(.title2) + .foregroundStyle(.secondary) + } - @ViewBuilder - private var progressIndicator: some View { - if isActive { - ProgressView() - .scaleEffect(0.4) - .frame(height: 20) + private var volumeInfo: some View { + VStack(alignment: .leading) { + HStack { + Text(dest.lastKnownVolumeName ?? "dest_label_no_volume_name") + .font(.headline) + if let latest = dest.snapshotDates?.max() { + Text(latest.formatted(.relativeDate)) + .font(.caption2) + .foregroundStyle(.secondary) } + } + 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) + } } + .lineLimit(1) + } - @ViewBuilder - private var failureWarning: some View { - if dest.lastBackupFailed && !isActive { - Button { - NSWorkspace.shared.open(Constants.URLs.timeMachineSystemSettings) - } label: { - Image(systemSymbol: .exclamationmarkTriangleFill) - .foregroundStyle(.red) - .padding([.vertical, .leading], 4) - } - .buttonStyle(.plain) - .focusable(false) - .help( - String( - localized: dest.destinationError?.localizedString ?? - "dest_label_last_backup_failed_\(dest.result ?? -1)" - ) - ) - } + @ViewBuilder + private var progressIndicator: some View { + if isActive { + ProgressView() + .scaleEffect(0.4) + .frame(height: 20) } + } - @ViewBuilder - private var startStopButton: some View { - if !utility.isIdle && !isActive { - EmptyView() - } else { - Button { - startStopBackup() - } label: { - if utility.status.activeDestinationID == dest.destinationID { - Image(systemSymbol: .stopFill) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 13) - } else { - Image(systemSymbol: .playFill) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 13) - } - } - .disabled(!utility.isIdle && !isActive) - .buttonStyle(.custom) - .focusable(false) - } + @ViewBuilder + private var failureWarning: some View { + if dest.lastBackupFailed && !isActive { + Button { + NSWorkspace.shared.open(Constants.URLs.timeMachineSystemSettings) + } label: { + Image(systemSymbol: .exclamationmarkTriangleFill) + .foregroundStyle(.red) + .padding([.vertical, .leading], 4) + } + .buttonStyle(.plain) + .focusable(false) + .help( + String( + localized: dest.destinationError?.localizedString + ?? "dest_label_last_backup_failed_\(dest.result ?? -1)" + ) + ) } + } - private func startStopBackup() { + @ViewBuilder + private var startStopButton: some View { + if !utility.isIdle && !isActive { + EmptyView() + } else { + Button { + startStopBackup() + } label: { if utility.status.activeDestinationID == dest.destinationID { - utility.stopBackup() + Image(systemSymbol: .xmark) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 13) } else { - utility.startBackup(id: dest.destinationID) + Image(systemSymbol: .playFill) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 13) } + } + .disabled(!utility.isIdle && !isActive) + .buttonStyle(.custom) + .focusable(false) } + } - @ViewBuilder - private var contextMenuActions: some View { - let unknown = NSLocalizedString("dest_label_no_volume_name", comment: "") - Button("button_show_info") { showInfo.toggle() } - Divider() - Button { - startStopBackup() - } label: { - if utility.status.activeDestinationID == dest.destinationID { - Text("button_cancel_backup") - } else { - Text("button_backup_to_\(dest.lastKnownVolumeName ?? unknown)_now") - } - } - .disabled(!utility.isIdle && !isActive) + private func startStopBackup() { + if utility.status.activeDestinationID == dest.destinationID { + utility.stopBackup() + } else { + utility.startBackup(id: dest.destinationID) } + } - @ViewBuilder - private var status: some View { - if isActive { - HStack { - if let state = utility.status as? BackupState._BaseState { - 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))") - } - } - } - .font(.caption2) - .foregroundStyle(.secondary) - .frame(maxWidth: .infinity) - .padding(.horizontal, 8) - .padding(.vertical, 2) - .background { progressBackground } - .background(.fill.tertiary, in: .rect) - } + @ViewBuilder + private var contextMenuActions: some View { + let unknown = NSLocalizedString("dest_label_no_volume_name", comment: "") + Button("button_show_info") { showInfo.toggle() } + Divider() + Button { + startStopBackup() + } label: { + if utility.status.activeDestinationID == dest.destinationID { + Text("button_cancel_backup") + } else { + Text("button_backup_to_\(dest.lastKnownVolumeName ?? unknown)_now") + } } + .disabled(!utility.isIdle && !isActive) + } - private var progressBackground: some View { - GeometryReader { geometry in - if let percent = utility.status.progessPercentage { - Color.accentColor - .frame(width: geometry.size.width * percent) - .opacity(0.3) - .animation(.easeInOut, value: percent) - } + @ViewBuilder + private var status: some View { + if isActive { + HStack { + if let state = utility.status as? BackupState._BaseState { + 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))") + } } + } + .font(.caption2) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity) + .padding(.horizontal, 8) + .padding(.vertical, 2) + .background { progressBackground } + .background(.fill.tertiary, in: .rect) } + } - private var byteFormat: ByteCountFormatStyle { - .byteCount(style: .file) + private var progressBackground: some View { + GeometryReader { geometry in + if let percent = utility.status.progessPercentage { + Color.white + .frame(width: geometry.size.width * percent) + .opacity(0.3) + .animation(.easeInOut, value: percent) + } } -} + } -import Sparkle + private var byteFormat: ByteCountFormatStyle { + .byteCount(style: .file) + } +} #Preview("Light") { - MenuView( - utility: TMUtilityMock(preferences: .mock), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.light) + MenuView( + utility: TMUtilityMock(preferences: .mock), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.light) } #Preview("Dark") { - MenuView( - utility: TMUtilityMock(preferences: .mock), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.dark) + MenuView( + utility: TMUtilityMock(preferences: .mock), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.dark) } diff --git a/TimeMachineStatus/Views/MenuView.swift b/TimeMachineStatus/Views/MenuView.swift index 743ba82..3d33a87 100644 --- a/TimeMachineStatus/Views/MenuView.swift +++ b/TimeMachineStatus/Views/MenuView.swift @@ -7,319 +7,327 @@ // Copyright © 2023 Lukas Pistrol. All rights reserved. // // See LICENSE.md for license information. -// +// import Sparkle import SwiftUI struct MenuView: View { - @State private var utility: any TMUtility - @ObservedObject private var updaterViewModel: UpdaterViewModel - private let updater: SPUUpdater + @State private var utility: any TMUtility + @ObservedObject private var updaterViewModel: UpdaterViewModel + private let updater: SPUUpdater + + init(utility: any TMUtility, updater: SPUUpdater) { + self.utility = utility + self.updater = updater + self.updaterViewModel = UpdaterViewModel(updater: updater) + } - init(utility: any TMUtility, updater: SPUUpdater) { - self.utility = utility - self.updater = updater - self.updaterViewModel = UpdaterViewModel(updater: updater) + var body: some View { + VStack(spacing: 8) { + UserfacingErrorView(error: utility.error) + VStack(spacing: 16) { + destinationsSection + generalInfoSection + } + .padding() + bottomToolbar } + .frame(width: Constants.Sizes.popoverWidth) + .fixedSize() + .navigationTitle("window_title") + .background(.regularMaterial, in: .rect) + } - var body: some View { - VStack(spacing: 8) { - UserfacingErrorView(error: utility.error) - VStack(spacing: 16) { - destinationsSection - generalInfoSection - } - .padding() - bottomToolbar + @ViewBuilder + private var destinationsSection: some View { + if let destinations = utility.preferences?.destinations { + ExpandableSection { + VStack(alignment: .leading) { + ForEach(destinations, id: \.destinationID) { dest in + DestinationCell(dest, utility: utility) + .frame(maxWidth: .infinity) + } } - .frame(width: Constants.Sizes.popoverWidth) - .fixedSize() - .navigationTitle("window_title") - .background(.regularMaterial, in: .rect) + } header: { + Text("section_destinations") + } + .listRowSeparator(.hidden, edges: .all) } + } - @ViewBuilder - private var destinationsSection: some View { - if let destinations = utility.preferences?.destinations { - ExpandableSection { - VStack(alignment: .leading) { - ForEach(destinations, id: \.destinationID) { dest in - DestinationCell(dest, utility: utility) - .frame(maxWidth: .infinity) - } + @ViewBuilder + private var generalInfoSection: some View { + if let preferences = utility.preferences { + ExpandableSection(expanded: false) { + VStack { + 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") { + if autoBackup { + Text("general_info_autobackup_enabled") + } else { + Text("general_info_autobackup_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") { + if requiresACPower { + Text("general_info_requirespower_true") + } else { + Text("general_info_requirespower_false") + } + } + .padding(10) + .card(.background.secondary) + } + LabeledContent("general_info_skippaths") { + VStack(alignment: .trailing, spacing: 4) { + if let skipPaths = preferences.skipPaths { + ForEach(skipPaths, id: \.self) { path in + Text(path) } - } header: { - Text("section_destinations") + } else { + Text("general_info_skippaths_empty") + .foregroundColor(.secondary) + } } - .listRowSeparator(.hidden, edges: .all) + } + .padding(10) + .card(.background.secondary) } + } header: { + Text("section_general_info") + } + .labeledContentStyle(.custom) } + } - @ViewBuilder - private var generalInfoSection: some View { - if let preferences = utility.preferences { - ExpandableSection(expanded: false) { - VStack { - 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") { - if autoBackup { - Text("general_info_autobackup_enabled") - } else { - Text("general_info_autobackup_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") { - if requiresACPower { - Text("general_info_requirespower_true") - } else { - Text("general_info_requirespower_false") - } - } - .padding(10) - .card(.background.secondary) - } - LabeledContent("general_info_skippaths") { - VStack(alignment: .trailing, spacing: 4) { - if let skipPaths = preferences.skipPaths { - ForEach(skipPaths, id: \.self) { path in - Text(path) - } - } else { - Text("general_info_skippaths_empty") - .foregroundColor(.secondary) - } - } - } - .padding(10) - .card(.background.secondary) - } - } header: { - Text("section_general_info") - } - .labeledContentStyle(.custom) + private var bottomToolbar: some View { + HStack(spacing: 12) { + Button { + if utility.isIdle { + utility.startBackup() + } else { + utility.stopBackup() } - } + } label: { + // 1. Changed .stopFill to .xmark + Label("button_startbackup", systemSymbol: utility.isIdle ? .playFill : .xmark) + } + .focusable(false) + toolbarStatus + Spacer() - private var bottomToolbar: some View { - HStack(spacing: 12) { - Button { - if utility.isIdle { - utility.startBackup() - } else { - utility.stopBackup() - } - } label: { - Label("button_startbackup", systemSymbol: utility.isIdle ? .playFill : .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() + } + } - toolbarMenu + @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)))) + } } - .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() + .font(.caption) + .opacity(0.8) + } + .foregroundStyle(.secondary) + .font(.caption2) + } else if let status = utility.status as? BackupState.Unknown { + Text(status.statusString) + .foregroundStyle(.secondary) + .font(.caption2) + .help(status.rawState) + .onTapGesture { + NSPasteboard.general.setString(status.rawState, forType: .string) } - } - - @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)))) - } - } + } else { + if let latestDate = utility.preferences?.latestBackupDate, + let latestVolume = utility.preferences?.latestBackupVolume + { + VStack(alignment: .leading, spacing: 0) { + Text("label_lastbackup_\(latestDate.formatted(.relativeDate))_on_\(latestVolume)") + if let interval = utility.preferences?.autoBackupInterval, + utility.preferences?.autoBackup == true + { + let nextDate = latestDate.addingTimeInterval(.init(interval)) + if nextDate < .now { + Text("label_nextbackup_issue") + .font(.caption) + } else { + Text("label_nextbackup_\(nextDate.formatted(.relativeDate))") .font(.caption) - .opacity(0.8) - } - .foregroundStyle(.secondary) - .font(.caption2) - } else if let status = utility.status as? BackupState.Unknown { - Text(status.statusString) - .foregroundStyle(.secondary) - .font(.caption2) - .help(status.rawState) - .onTapGesture { - NSPasteboard.general.setString(status.rawState, forType: .string) - } - } else { - if let latestDate = utility.preferences?.latestBackupDate, - let latestVolume = utility.preferences?.latestBackupVolume { - VStack(alignment: .leading, spacing: 0) { - Text("label_lastbackup_\(latestDate.formatted(.relativeDate))_on_\(latestVolume)") - if let interval = utility.preferences?.autoBackupInterval, utility.preferences?.autoBackup == true { - let nextDate = latestDate.addingTimeInterval(.init(interval)) - if nextDate < .now { - Text("label_nextbackup_issue") - .font(.caption) - } else { - Text("label_nextbackup_\(nextDate.formatted(.relativeDate))") - .font(.caption) - } - } else { - Text("label_autobackupdisabled") - .font(.caption) - .opacity(0.8) - } - } - .foregroundStyle(.secondary) - .font(.caption2) } + } else { + Text("label_autobackupdisabled") + .font(.caption) + .opacity(0.8) + } } + .foregroundStyle(.secondary) + .font(.caption2) + } } + } - private var toolbarMenu: some View { - Menu { - SettingsLink { - Text("settings_button_settings") - } - .keyboardShortcut(",", modifiers: .command) - Button("settings_button_checkforupdates") { - updater.checkForUpdates() - } - .disabled(!updaterViewModel.canCheckForUpdates) - Divider() - Button("button_browsebackups") { - utility.launchTimeMachine() - } - Button("button_time_machine_system_settings") { - NSWorkspace.shared.open(Constants.URLs.timeMachineSystemSettings) - } - Divider() - Menu("button_feedback") { - Button("button_bug_report_feature_request") { - NSWorkspace.shared.open(Constants.URLs.bugReport) - } - Divider() - Button("button_view_issues") { - NSWorkspace.shared.open(Constants.URLs.issues) - } - } - Menu("button_support_project") { - Button("button_github_sponsor") { - NSWorkspace.shared.open(Constants.URLs.githubSponsor) - } - Button("button_buymeacoffee") { - NSWorkspace.shared.open(Constants.URLs.buymeacoffee) - } - } - 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 - ) - } + private var toolbarMenu: some View { + Menu { + SettingsLink { + Text("settings_button_settings") + } + .keyboardShortcut(",", modifiers: .command) + Button("settings_button_checkforupdates") { + updater.checkForUpdates() + } + .disabled(!updaterViewModel.canCheckForUpdates) + Divider() + Button("button_browsebackups") { + utility.launchTimeMachine() + } + Button("button_time_machine_system_settings") { + NSWorkspace.shared.open(Constants.URLs.timeMachineSystemSettings) + } + Divider() + Menu("button_feedback") { + Button("button_bug_report_feature_request") { + NSWorkspace.shared.open(Constants.URLs.bugReport) + } + Divider() + Button("button_view_issues") { + NSWorkspace.shared.open(Constants.URLs.issues) + } + } + Menu("button_support_project") { + Button("button_github_sponsor") { + NSWorkspace.shared.open(Constants.URLs.githubSponsor) + } + Button("button_buymeacoffee") { + NSWorkspace.shared.open(Constants.URLs.buymeacoffee) } - .focusable(false) - .buttonStyle(.plain) + } + 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) + } } #Preview("Light") { - MenuView( - utility: TMUtilityMock(preferences: .mock), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.light) + MenuView( + utility: TMUtilityMock(preferences: .mock), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.light) } #Preview("Light Error") { - MenuView( - utility: TMUtilityMock( - error: .preferencesFilePermissionNotGranted, - canReadPreferences: false - ), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.light) + MenuView( + utility: TMUtilityMock( + error: .preferencesFilePermissionNotGranted, + canReadPreferences: false + ), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.light) } #Preview("Dark Copying") { - let preferences = Preferences.mock - // swiftlint:disable:next force_unwrapping - let status = BackupState._State.Mock.copying(preferences.destinations!.first!.destinationID) - MenuView( - utility: TMUtilityMock( - status: status, - preferences: preferences - ), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.dark) + let preferences = Preferences.mock + // swiftlint:disable:next force_unwrapping + let status = BackupState._State.Mock.copying(preferences.destinations!.first!.destinationID) + MenuView( + utility: TMUtilityMock( + status: status, + preferences: preferences + ), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.dark) } #Preview("Dark Finding Changes") { - let preferences = Preferences.mock - // swiftlint:disable:next force_unwrapping - let status = BackupState._State.Mock.findingChanges(preferences.destinations!.first!.destinationID) - MenuView( - utility: TMUtilityMock( - status: status, - preferences: preferences - ), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.dark) + let preferences = Preferences.mock + // swiftlint:disable:next force_unwrapping + let status = BackupState._State.Mock.findingChanges( + preferences.destinations!.first!.destinationID) + MenuView( + utility: TMUtilityMock( + status: status, + preferences: preferences + ), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.dark) } #Preview("Dark Unknown") { - let preferences = Preferences.mock - let status = BackupState._State.Mock.unknown - MenuView( - utility: TMUtilityMock( - status: status, - preferences: preferences - ), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.dark) + let preferences = Preferences.mock + let status = BackupState._State.Mock.unknown + MenuView( + utility: TMUtilityMock( + status: status, + preferences: preferences + ), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.dark) } From 516a051a03c65da479ecac91d6dc4b75ab69d30e Mon Sep 17 00:00:00 2001 From: Lynnnya <125717287+Lynnnya@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:48:21 -0500 Subject: [PATCH 2/3] added host name of backup drive on DestinationCell in response to issue #53 --- TimeMachineStatus/Views/DestinationCell.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/TimeMachineStatus/Views/DestinationCell.swift b/TimeMachineStatus/Views/DestinationCell.swift index 8d39dd9..53cc343 100644 --- a/TimeMachineStatus/Views/DestinationCell.swift +++ b/TimeMachineStatus/Views/DestinationCell.swift @@ -108,6 +108,12 @@ struct DestinationCell: View { .foregroundStyle(.secondary) } } + //added destinition url info and extracted hostname with regex + if dest.networkURL != nil { + if let match = dest.networkURL!.firstMatch(of: /@([^\/]+)\//) { + Text(match.output.1) + } + } if let bytesUsed = dest.bytesUsed, let bytesAvailable = dest.bytesAvailable { let used = bytesUsed.formatted(byteFormat) let available = bytesAvailable.formatted(byteFormat) From a83273e12976e82c9d2a54abfca294c5fb61f938 Mon Sep 17 00:00:00 2001 From: Lynnnya <125717287+Lynnnya@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:58:00 -0500 Subject: [PATCH 3/3] reverted formatting changes --- TimeMachineStatus/Views/DestinationCell.swift | 426 ++++++------- TimeMachineStatus/Views/MenuView.swift | 562 +++++++++--------- 2 files changed, 496 insertions(+), 492 deletions(-) diff --git a/TimeMachineStatus/Views/DestinationCell.swift b/TimeMachineStatus/Views/DestinationCell.swift index 53cc343..9ff74d7 100644 --- a/TimeMachineStatus/Views/DestinationCell.swift +++ b/TimeMachineStatus/Views/DestinationCell.swift @@ -13,259 +13,259 @@ import Sparkle import SwiftUI struct DestinationCell: View { - @State private var utility: any TMUtility + @State private var utility: any TMUtility - let dest: Destination + let dest: Destination - init(_ dest: Destination, utility: any TMUtility) { - self.dest = dest - self.utility = utility - } + init(_ dest: Destination, utility: any TMUtility) { + self.dest = dest + self.utility = utility + } - private var isActive: Bool { - utility.status.activeDestinationID == dest.destinationID - } + private var isActive: Bool { + utility.status.activeDestinationID == dest.destinationID + } - private var findingChanges: BackupState.FindingChanges? { - if let findingChanges = utility.status as? BackupState.FindingChanges { - return findingChanges + private var findingChanges: BackupState.FindingChanges? { + if let findingChanges = utility.status as? BackupState.FindingChanges { + return findingChanges + } + return nil } - return nil - } - private var copying: BackupState.Copying? { - if let copying = utility.status as? BackupState.Copying { - return copying + private var copying: BackupState.Copying? { + if let copying = utility.status as? BackupState.Copying { + return copying + } + return nil } - return nil - } - @State private var showInfo: Bool = false - @State private var hovering: Bool = false + @State private var showInfo: Bool = false + @State private var hovering: Bool = false - var body: some View { - VStack(spacing: 0) { - HStack { - symbol - volumeInfo - Spacer() - failureWarning - progressIndicator - startStopButton - } - .padding(.vertical, 4) - .padding(.horizontal, 8) - .padding(.trailing, 4) - status - } - .overlay { hoverOverlay } - .contentShape(.rect) - .contextMenu { contextMenuActions } - .card(.background.secondary) - .onTapGesture { - showInfo.toggle() - } - .onHover { hovering in - withAnimation(.snappy) { - self.hovering = hovering - } - } - .popover(isPresented: $showInfo) { - DestinationInfoView(dest: dest) + var body: some View { + VStack(spacing: 0) { + HStack { + symbol + volumeInfo + Spacer() + failureWarning + progressIndicator + startStopButton + } + .padding(.vertical, 4) + .padding(.horizontal, 8) + .padding(.trailing, 4) + status + } + .overlay { hoverOverlay } + .contentShape(.rect) + .contextMenu { contextMenuActions } + .card(.background.secondary) + .onTapGesture { + showInfo.toggle() + } + .onHover { hovering in + withAnimation(.snappy) { + self.hovering = hovering + } + } + .popover(isPresented: $showInfo) { + DestinationInfoView(dest: dest) + } } - } - @ViewBuilder - private var hoverOverlay: some View { - if hovering { - Rectangle() - .fill(.fill.secondary.opacity(0.5)) - .allowsHitTesting(false) + @ViewBuilder + private var hoverOverlay: some View { + if hovering { + Rectangle() + .fill(.fill.secondary.opacity(0.5)) + .allowsHitTesting(false) + } } - } - private var symbol: some View { - Group { - if dest.networkURL != nil { - Image(systemSymbol: .externaldriveFillBadgeWifi) - } else { - Image(systemSymbol: .externaldriveFillBadgeTimemachine) - } + private var symbol: some View { + Group { + if dest.networkURL != nil { + Image(systemSymbol: .externaldriveFillBadgeWifi) + } else { + Image(systemSymbol: .externaldriveFillBadgeTimemachine) + } + } + .imageScale(.large) + .font(.title2) + .foregroundStyle(.secondary) } - .imageScale(.large) - .font(.title2) - .foregroundStyle(.secondary) - } - private var volumeInfo: some View { - VStack(alignment: .leading) { - HStack { - Text(dest.lastKnownVolumeName ?? "dest_label_no_volume_name") - .font(.headline) - if let latest = dest.snapshotDates?.max() { - Text(latest.formatted(.relativeDate)) - .font(.caption2) - .foregroundStyle(.secondary) + private var volumeInfo: some View { + VStack(alignment: .leading) { + HStack { + Text(dest.lastKnownVolumeName ?? "dest_label_no_volume_name") + .font(.headline) + if let latest = dest.snapshotDates?.max() { + Text(latest.formatted(.relativeDate)) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + //added destinition url info and extracted hostname with regex + if dest.networkURL != nil { + if let match = dest.networkURL!.firstMatch(of: /@([^\/]+)\//) { + Text(match.output.1) + } + } + 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) + } } - } - //added destinition url info and extracted hostname with regex - if dest.networkURL != nil { - if let match = dest.networkURL!.firstMatch(of: /@([^\/]+)\//) { - Text(match.output.1) + .lineLimit(1) + } + + @ViewBuilder + private var progressIndicator: some View { + if isActive { + ProgressView() + .scaleEffect(0.4) + .frame(height: 20) } - } - 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) - } } - .lineLimit(1) - } - @ViewBuilder - private var progressIndicator: some View { - if isActive { - ProgressView() - .scaleEffect(0.4) - .frame(height: 20) + @ViewBuilder + private var failureWarning: some View { + if dest.lastBackupFailed && !isActive { + Button { + NSWorkspace.shared.open(Constants.URLs.timeMachineSystemSettings) + } label: { + Image(systemSymbol: .exclamationmarkTriangleFill) + .foregroundStyle(.red) + .padding([.vertical, .leading], 4) + } + .buttonStyle(.plain) + .focusable(false) + .help( + String( + localized: dest.destinationError?.localizedString + ?? "dest_label_last_backup_failed_\(dest.result ?? -1)" + ) + ) + } } - } - @ViewBuilder - private var failureWarning: some View { - if dest.lastBackupFailed && !isActive { - Button { - NSWorkspace.shared.open(Constants.URLs.timeMachineSystemSettings) - } label: { - Image(systemSymbol: .exclamationmarkTriangleFill) - .foregroundStyle(.red) - .padding([.vertical, .leading], 4) - } - .buttonStyle(.plain) - .focusable(false) - .help( - String( - localized: dest.destinationError?.localizedString - ?? "dest_label_last_backup_failed_\(dest.result ?? -1)" - ) - ) + @ViewBuilder + private var startStopButton: some View { + if !utility.isIdle && !isActive { + EmptyView() + } else { + Button { + startStopBackup() + } label: { + if utility.status.activeDestinationID == dest.destinationID { + Image(systemSymbol: .xmark) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 13) + } else { + Image(systemSymbol: .playFill) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 13) + } + } + .disabled(!utility.isIdle && !isActive) + .buttonStyle(.custom) + .focusable(false) + } } - } - @ViewBuilder - private var startStopButton: some View { - if !utility.isIdle && !isActive { - EmptyView() - } else { - Button { - startStopBackup() - } label: { + private func startStopBackup() { if utility.status.activeDestinationID == dest.destinationID { - Image(systemSymbol: .xmark) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 13) + utility.stopBackup() } else { - Image(systemSymbol: .playFill) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 13) + utility.startBackup(id: dest.destinationID) } - } - .disabled(!utility.isIdle && !isActive) - .buttonStyle(.custom) - .focusable(false) } - } - private func startStopBackup() { - if utility.status.activeDestinationID == dest.destinationID { - utility.stopBackup() - } else { - utility.startBackup(id: dest.destinationID) + @ViewBuilder + private var contextMenuActions: some View { + let unknown = NSLocalizedString("dest_label_no_volume_name", comment: "") + Button("button_show_info") { showInfo.toggle() } + Divider() + Button { + startStopBackup() + } label: { + if utility.status.activeDestinationID == dest.destinationID { + Text("button_cancel_backup") + } else { + Text("button_backup_to_\(dest.lastKnownVolumeName ?? unknown)_now") + } + } + .disabled(!utility.isIdle && !isActive) } - } - @ViewBuilder - private var contextMenuActions: some View { - let unknown = NSLocalizedString("dest_label_no_volume_name", comment: "") - Button("button_show_info") { showInfo.toggle() } - Divider() - Button { - startStopBackup() - } label: { - if utility.status.activeDestinationID == dest.destinationID { - Text("button_cancel_backup") - } else { - Text("button_backup_to_\(dest.lastKnownVolumeName ?? unknown)_now") - } + @ViewBuilder + private var status: some View { + if isActive { + HStack { + if let state = utility.status as? BackupState._BaseState { + 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))") + } + } + } + .font(.caption2) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity) + .padding(.horizontal, 8) + .padding(.vertical, 2) + .background { progressBackground } + .background(.fill.tertiary, in: .rect) + } } - .disabled(!utility.isIdle && !isActive) - } - @ViewBuilder - private var status: some View { - if isActive { - HStack { - if let state = utility.status as? BackupState._BaseState { - Text(state.statusString) + private var progressBackground: some View { + GeometryReader { geometry in + if let percent = utility.status.progessPercentage { + Color.white + .frame(width: geometry.size.width * percent) + .opacity(0.3) + .animation(.easeInOut, value: percent) + } } - 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))") - } - } - } - .font(.caption2) - .foregroundStyle(.secondary) - .frame(maxWidth: .infinity) - .padding(.horizontal, 8) - .padding(.vertical, 2) - .background { progressBackground } - .background(.fill.tertiary, in: .rect) } - } - private var progressBackground: some View { - GeometryReader { geometry in - if let percent = utility.status.progessPercentage { - Color.white - .frame(width: geometry.size.width * percent) - .opacity(0.3) - .animation(.easeInOut, value: percent) - } + private var byteFormat: ByteCountFormatStyle { + .byteCount(style: .file) } - } - - private var byteFormat: ByteCountFormatStyle { - .byteCount(style: .file) - } } #Preview("Light") { - MenuView( - utility: TMUtilityMock(preferences: .mock), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.light) + MenuView( + utility: TMUtilityMock(preferences: .mock), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.light) } #Preview("Dark") { - MenuView( - utility: TMUtilityMock(preferences: .mock), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.dark) + MenuView( + utility: TMUtilityMock(preferences: .mock), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.dark) } diff --git a/TimeMachineStatus/Views/MenuView.swift b/TimeMachineStatus/Views/MenuView.swift index 3d33a87..5017d7f 100644 --- a/TimeMachineStatus/Views/MenuView.swift +++ b/TimeMachineStatus/Views/MenuView.swift @@ -13,321 +13,325 @@ import Sparkle import SwiftUI struct MenuView: View { - @State private var utility: any TMUtility - @ObservedObject private var updaterViewModel: UpdaterViewModel - private let updater: SPUUpdater + @State private var utility: any TMUtility + @ObservedObject private var updaterViewModel: UpdaterViewModel + private let updater: SPUUpdater - init(utility: any TMUtility, updater: SPUUpdater) { - self.utility = utility - self.updater = updater - self.updaterViewModel = UpdaterViewModel(updater: updater) - } - - var body: some View { - VStack(spacing: 8) { - UserfacingErrorView(error: utility.error) - VStack(spacing: 16) { - destinationsSection - generalInfoSection - } - .padding() - bottomToolbar + init(utility: any TMUtility, updater: SPUUpdater) { + self.utility = utility + self.updater = updater + self.updaterViewModel = UpdaterViewModel(updater: updater) } - .frame(width: Constants.Sizes.popoverWidth) - .fixedSize() - .navigationTitle("window_title") - .background(.regularMaterial, in: .rect) - } - @ViewBuilder - private var destinationsSection: some View { - if let destinations = utility.preferences?.destinations { - ExpandableSection { - VStack(alignment: .leading) { - ForEach(destinations, id: \.destinationID) { dest in - DestinationCell(dest, utility: utility) - .frame(maxWidth: .infinity) - } + var body: some View { + VStack(spacing: 8) { + UserfacingErrorView(error: utility.error) + VStack(spacing: 16) { + destinationsSection + generalInfoSection + } + .padding() + bottomToolbar } - } header: { - Text("section_destinations") - } - .listRowSeparator(.hidden, edges: .all) + .frame(width: Constants.Sizes.popoverWidth) + .fixedSize() + .navigationTitle("window_title") + .background(.regularMaterial, in: .rect) } - } - @ViewBuilder - private var generalInfoSection: some View { - if let preferences = utility.preferences { - ExpandableSection(expanded: false) { - VStack { - 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") { - if autoBackup { - Text("general_info_autobackup_enabled") - } else { - Text("general_info_autobackup_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") { - if requiresACPower { - Text("general_info_requirespower_true") - } else { - Text("general_info_requirespower_false") - } - } - .padding(10) - .card(.background.secondary) - } - LabeledContent("general_info_skippaths") { - VStack(alignment: .trailing, spacing: 4) { - if let skipPaths = preferences.skipPaths { - ForEach(skipPaths, id: \.self) { path in - Text(path) + @ViewBuilder + private var destinationsSection: some View { + if let destinations = utility.preferences?.destinations { + ExpandableSection { + VStack(alignment: .leading) { + ForEach(destinations, id: \.destinationID) { dest in + DestinationCell(dest, utility: utility) + .frame(maxWidth: .infinity) + } } - } else { - Text("general_info_skippaths_empty") - .foregroundColor(.secondary) - } + } header: { + Text("section_destinations") } - } - .padding(10) - .card(.background.secondary) + .listRowSeparator(.hidden, edges: .all) } - } header: { - Text("section_general_info") - } - .labeledContentStyle(.custom) } - } - private var bottomToolbar: some View { - HStack(spacing: 12) { - Button { - if utility.isIdle { - utility.startBackup() - } else { - utility.stopBackup() + @ViewBuilder + private var generalInfoSection: some View { + if let preferences = utility.preferences { + ExpandableSection(expanded: false) { + VStack { + 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") { + if autoBackup { + Text("general_info_autobackup_enabled") + } else { + Text("general_info_autobackup_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") { + if requiresACPower { + Text("general_info_requirespower_true") + } else { + Text("general_info_requirespower_false") + } + } + .padding(10) + .card(.background.secondary) + } + LabeledContent("general_info_skippaths") { + VStack(alignment: .trailing, spacing: 4) { + if let skipPaths = preferences.skipPaths { + ForEach(skipPaths, id: \.self) { path in + Text(path) + } + } else { + Text("general_info_skippaths_empty") + .foregroundColor(.secondary) + } + } + } + .padding(10) + .card(.background.secondary) + } + } header: { + Text("section_general_info") + } + .labeledContentStyle(.custom) } - } label: { - // 1. Changed .stopFill to .xmark - Label("button_startbackup", systemSymbol: utility.isIdle ? .playFill : .xmark) - } - .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)))) - } + private var bottomToolbar: some View { + HStack(spacing: 12) { + Button { + if utility.isIdle { + utility.startBackup() + } else { + utility.stopBackup() + } + } label: { + // 1. Changed .stopFill to .xmark + Label("button_startbackup", systemSymbol: utility.isIdle ? .playFill : .xmark) + } + .focusable(false) + toolbarStatus + Spacer() + + toolbarMenu } - .font(.caption) - .opacity(0.8) - } - .foregroundStyle(.secondary) - .font(.caption2) - } else if let status = utility.status as? BackupState.Unknown { - Text(status.statusString) - .foregroundStyle(.secondary) - .font(.caption2) - .help(status.rawState) - .onTapGesture { - NSPasteboard.general.setString(status.rawState, forType: .string) + .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() } - } else { - if let latestDate = utility.preferences?.latestBackupDate, - let latestVolume = utility.preferences?.latestBackupVolume - { - VStack(alignment: .leading, spacing: 0) { - Text("label_lastbackup_\(latestDate.formatted(.relativeDate))_on_\(latestVolume)") - if let interval = utility.preferences?.autoBackupInterval, - utility.preferences?.autoBackup == true - { - let nextDate = latestDate.addingTimeInterval(.init(interval)) - if nextDate < .now { - Text("label_nextbackup_issue") - .font(.caption) - } else { - Text("label_nextbackup_\(nextDate.formatted(.relativeDate))") + } + + @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 status = utility.status as? BackupState.Unknown { + Text(status.statusString) + .foregroundStyle(.secondary) + .font(.caption2) + .help(status.rawState) + .onTapGesture { + NSPasteboard.general.setString(status.rawState, forType: .string) + } + } else { + if let latestDate = utility.preferences?.latestBackupDate, + let latestVolume = utility.preferences?.latestBackupVolume + { + VStack(alignment: .leading, spacing: 0) { + Text( + "label_lastbackup_\(latestDate.formatted(.relativeDate))_on_\(latestVolume)" + ) + if let interval = utility.preferences?.autoBackupInterval, + utility.preferences?.autoBackup == true + { + let nextDate = latestDate.addingTimeInterval(.init(interval)) + if nextDate < .now { + Text("label_nextbackup_issue") + .font(.caption) + } else { + Text("label_nextbackup_\(nextDate.formatted(.relativeDate))") + .font(.caption) + } + } else { + Text("label_autobackupdisabled") + .font(.caption) + .opacity(0.8) + } + } + .foregroundStyle(.secondary) + .font(.caption2) } - } else { - Text("label_autobackupdisabled") - .font(.caption) - .opacity(0.8) - } } - .foregroundStyle(.secondary) - .font(.caption2) - } } - } - private var toolbarMenu: some View { - Menu { - SettingsLink { - Text("settings_button_settings") - } - .keyboardShortcut(",", modifiers: .command) - Button("settings_button_checkforupdates") { - updater.checkForUpdates() - } - .disabled(!updaterViewModel.canCheckForUpdates) - Divider() - Button("button_browsebackups") { - utility.launchTimeMachine() - } - Button("button_time_machine_system_settings") { - NSWorkspace.shared.open(Constants.URLs.timeMachineSystemSettings) - } - Divider() - Menu("button_feedback") { - Button("button_bug_report_feature_request") { - NSWorkspace.shared.open(Constants.URLs.bugReport) - } - Divider() - Button("button_view_issues") { - NSWorkspace.shared.open(Constants.URLs.issues) - } - } - Menu("button_support_project") { - Button("button_github_sponsor") { - NSWorkspace.shared.open(Constants.URLs.githubSponsor) - } - Button("button_buymeacoffee") { - NSWorkspace.shared.open(Constants.URLs.buymeacoffee) + private var toolbarMenu: some View { + Menu { + SettingsLink { + Text("settings_button_settings") + } + .keyboardShortcut(",", modifiers: .command) + Button("settings_button_checkforupdates") { + updater.checkForUpdates() + } + .disabled(!updaterViewModel.canCheckForUpdates) + Divider() + Button("button_browsebackups") { + utility.launchTimeMachine() + } + Button("button_time_machine_system_settings") { + NSWorkspace.shared.open(Constants.URLs.timeMachineSystemSettings) + } + Divider() + Menu("button_feedback") { + Button("button_bug_report_feature_request") { + NSWorkspace.shared.open(Constants.URLs.bugReport) + } + Divider() + Button("button_view_issues") { + NSWorkspace.shared.open(Constants.URLs.issues) + } + } + Menu("button_support_project") { + Button("button_github_sponsor") { + NSWorkspace.shared.open(Constants.URLs.githubSponsor) + } + Button("button_buymeacoffee") { + NSWorkspace.shared.open(Constants.URLs.buymeacoffee) + } + } + 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 + ) + } } - } - 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) } - .focusable(false) - .buttonStyle(.plain) - } } #Preview("Light") { - MenuView( - utility: TMUtilityMock(preferences: .mock), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.light) + MenuView( + utility: TMUtilityMock(preferences: .mock), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.light) } #Preview("Light Error") { - MenuView( - utility: TMUtilityMock( - error: .preferencesFilePermissionNotGranted, - canReadPreferences: false - ), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.light) + MenuView( + utility: TMUtilityMock( + error: .preferencesFilePermissionNotGranted, + canReadPreferences: false + ), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.light) } #Preview("Dark Copying") { - let preferences = Preferences.mock - // swiftlint:disable:next force_unwrapping - let status = BackupState._State.Mock.copying(preferences.destinations!.first!.destinationID) - MenuView( - utility: TMUtilityMock( - status: status, - preferences: preferences - ), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.dark) + let preferences = Preferences.mock + // swiftlint:disable:next force_unwrapping + let status = BackupState._State.Mock.copying(preferences.destinations!.first!.destinationID) + MenuView( + utility: TMUtilityMock( + status: status, + preferences: preferences + ), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.dark) } #Preview("Dark Finding Changes") { - let preferences = Preferences.mock - // swiftlint:disable:next force_unwrapping - let status = BackupState._State.Mock.findingChanges( - preferences.destinations!.first!.destinationID) - MenuView( - utility: TMUtilityMock( - status: status, - preferences: preferences - ), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.dark) + let preferences = Preferences.mock + // swiftlint:disable:next force_unwrapping + let status = BackupState._State.Mock.findingChanges( + preferences.destinations!.first!.destinationID) + MenuView( + utility: TMUtilityMock( + status: status, + preferences: preferences + ), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.dark) } #Preview("Dark Unknown") { - let preferences = Preferences.mock - let status = BackupState._State.Mock.unknown - MenuView( - utility: TMUtilityMock( - status: status, - preferences: preferences - ), - updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater - ) - .preferredColorScheme(.dark) + let preferences = Preferences.mock + let status = BackupState._State.Mock.unknown + MenuView( + utility: TMUtilityMock( + status: status, + preferences: preferences + ), + updater: SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).updater + ) + .preferredColorScheme(.dark) }