From 24c850712a7f887e6c1750fb75f454f7673bc7a2 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sun, 3 May 2026 13:24:04 -0700 Subject: [PATCH] toolbar: use updated confirmation on iOS 15+ --- Platform/Shared/VMConfirmActionModifier.swift | 172 ++++++++++++++---- Platform/Shared/VMContextMenuModifier.swift | 14 +- Platform/Shared/VMToolbarModifier.swift | 14 +- 3 files changed, 151 insertions(+), 49 deletions(-) diff --git a/Platform/Shared/VMConfirmActionModifier.swift b/Platform/Shared/VMConfirmActionModifier.swift index 6ac0dcd5..5e17bfc9 100644 --- a/Platform/Shared/VMConfirmActionModifier.swift +++ b/Platform/Shared/VMConfirmActionModifier.swift @@ -16,65 +16,167 @@ import SwiftUI -enum ConfirmAction: Int, Identifiable { - case confirmCloneVM - case confirmDeleteVM - case confirmDeleteShortcut - case confirmStopVM - case confirmMoveVM - - var id: Int { rawValue } +enum ConfirmAction: Identifiable, Equatable { + case confirmCloneVM(vm: VMData) + case confirmDeleteVM(vm: VMData) + case confirmStopVM(vm: VMData) + case confirmMoveVM(vm: VMData) + + struct ID: Hashable { + let index: Int + let uuid: UUID + } + + @MainActor + var id: ID { + switch self { + case .confirmCloneVM(let vm): + return ID(index: 0, uuid: vm.id) + case .confirmDeleteVM(let vm): + return ID(index: 1, uuid: vm.id) + case .confirmStopVM(let vm): + return ID(index: 2, uuid: vm.id) + case .confirmMoveVM(let vm): + return ID(index: 3, uuid: vm.id) + } + } + + @MainActor + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } } struct VMConfirmActionModifier: ViewModifier { - let vm: VMData @Binding var confirmAction: ConfirmAction? - let onConfirm: () -> Void + let onConfirm: (ConfirmAction) -> Void @EnvironmentObject private var data: UTMData - - func body(content: Content) -> some View { + @State private var isPresented: Bool = false + + @ViewBuilder func body(content: Content) -> some View { + if #available(iOS 15, macOS 12, *) { + newBody(content: content) + } else { + oldBody(content: content) + } + } + + @ViewBuilder func oldBody(content: Content) -> some View { content.alert(item: $confirmAction) { action in switch action { - case .confirmCloneVM: + case .confirmCloneVM(let vm): if vm.isShortcut { - return Alert(title: Text("Do you want to copy this VM and all its data to internal storage?"), primaryButton: .cancel(), secondaryButton: .default(Text("Yes")) { + return Alert(title: Text("Do you want to copy this VM and all its data to internal storage?"), primaryButton: .cancel(), secondaryButton: .default(Text("Copy")) { data.busyWorkAsync { try await data.clone(vm: vm) } - onConfirm() + onConfirm(action) }) } else { - return Alert(title: Text("Do you want to duplicate this VM and all its data?"), primaryButton: .cancel(), secondaryButton: .default(Text("Yes")) { + return Alert(title: Text("Do you want to duplicate this VM and all its data?"), primaryButton: .cancel(), secondaryButton: .default(Text("Copy")) { data.busyWorkAsync { try await data.clone(vm: vm) } - onConfirm() + onConfirm(action) }) } - case .confirmDeleteVM: - return Alert(title: Text("Do you want to delete this VM and all its data?"), primaryButton: .cancel(), secondaryButton: .destructive(Text("Delete")) { - data.busyWorkAsync { - try await data.delete(vm: vm) - } - onConfirm() - }) - case .confirmDeleteShortcut: - return Alert(title: Text("Do you want to remove this shortcut? The data will not be deleted."), primaryButton: .cancel(), secondaryButton: .destructive(Text("Remove")) { - data.busyWorkAsync { - await data.listRemove(vm: vm) - } - onConfirm() - }) - case .confirmStopVM: + case .confirmDeleteVM(let vm): + if vm.wrapped?.isShortcut ?? false { + return Alert(title: Text("Do you want to remove this shortcut? The data will not be deleted."), primaryButton: .cancel(), secondaryButton: .destructive(Text("Remove")) { + data.busyWorkAsync { + await data.listRemove(vm: vm) + } + onConfirm(action) + }) + } else { + return Alert(title: Text("Do you want to delete this VM and all its data?"), primaryButton: .cancel(), secondaryButton: .destructive(Text("Delete")) { + data.busyWorkAsync { + try await data.delete(vm: vm) + } + onConfirm(action) + }) + } + case .confirmStopVM(let vm): return Alert(title: Text("Do you want to force stop this VM and lose all unsaved data?"), primaryButton: .cancel(), secondaryButton: .destructive(Text("Stop")) { data.stop(vm: vm) - onConfirm() + onConfirm(action) }) - case .confirmMoveVM: + case .confirmMoveVM(_): return Alert(title: Text("Do you want to move this VM to another location? This will copy the data to the new location, delete the data from the original location, and then create a shortcut."), primaryButton: .cancel(), secondaryButton: .destructive(Text("Confirm")) { - onConfirm() + onConfirm(action) }) } } } + + @available(iOS 15, macOS 12, *) + @ViewBuilder func newBody(content: Content) -> some View { + content.confirmationDialog("Confirm", isPresented: $isPresented, presenting: confirmAction) { action in + Button("Cancel", role: .cancel) {} + switch action { + case .confirmCloneVM(let vm): + Button("Copy") { + data.busyWorkAsync { + try await data.clone(vm: vm) + } + onConfirm(action) + } + case .confirmDeleteVM(let vm): + if vm.wrapped?.isShortcut ?? false { + Button("Remove", role: .destructive) { + data.busyWorkAsync { + await data.listRemove(vm: vm) + } + onConfirm(action) + } + } else { + Button("Delete", role: .destructive) { + data.busyWorkAsync { + try await data.delete(vm: vm) + } + onConfirm(action) + } + } + case .confirmStopVM(let vm): + Button("Stop", role: .destructive) { + data.stop(vm: vm) + onConfirm(action) + } + case .confirmMoveVM(_): + if #available(iOS 26, macOS 26, visionOS 26, *) { + Button("Confirm", role: .confirm) { + onConfirm(action) + } + } else { + Button("Confirm") { + onConfirm(action) + } + } + } + } message: { action in + switch action { + case .confirmCloneVM(let vm): + if vm.isShortcut { + Text("Do you want to copy this VM and all its data to internal storage?") + } else { + Text("Do you want to duplicate this VM and all its data?") + } + case .confirmDeleteVM(let vm): + if vm.wrapped?.isShortcut ?? false { + Text("Do you want to remove this shortcut? The data will not be deleted.") + } else { + Text("Do you want to delete this VM and all its data?") + } + case .confirmStopVM(_): + Text("Do you want to force stop this VM and lose all unsaved data?") + case .confirmMoveVM(_): + Text("Do you want to move this VM to another location? This will copy the data to the new location, delete the data from the original location, and then create a shortcut.") + } + } + .onChange(of: confirmAction) { newValue in + if newValue != nil { + isPresented = true + } + } + } } diff --git a/Platform/Shared/VMContextMenuModifier.swift b/Platform/Shared/VMContextMenuModifier.swift index 08bd12c4..5bf9efe8 100644 --- a/Platform/Shared/VMContextMenuModifier.swift +++ b/Platform/Shared/VMContextMenuModifier.swift @@ -72,7 +72,7 @@ struct VMContextMenuModifier: ViewModifier { #endif if vm.hasSuspendState || !vm.isStopped { Button { - confirmAction = .confirmStopVM + confirmAction = .confirmStopVM(vm: vm) } label: { Label("Stop", systemImage: "stop") }.help("Stop the running VM.") @@ -131,7 +131,7 @@ struct VMContextMenuModifier: ViewModifier { #if os(macOS) if !vm.isShortcut { Button { - confirmAction = .confirmMoveVM + confirmAction = .confirmMoveVM(vm: vm) } label: { Label("Move…", systemImage: "arrow.down.doc") }.disabled(!vm.isModifyAllowed) @@ -139,7 +139,7 @@ struct VMContextMenuModifier: ViewModifier { } #endif Button { - confirmAction = .confirmCloneVM + confirmAction = .confirmCloneVM(vm: vm) } label: { Label("Clone…", systemImage: "doc.on.doc") }.help("Duplicate this VM along with all its data.") @@ -153,14 +153,14 @@ struct VMContextMenuModifier: ViewModifier { Divider() if vm.isShortcut { DestructiveButton { - confirmAction = .confirmDeleteShortcut + confirmAction = .confirmDeleteVM(vm: vm) } label: { Label("Remove", systemImage: "trash") }.disabled(!vm.isModifyAllowed) .help("Delete this shortcut. The underlying data will not be deleted.") } else { DestructiveButton { - confirmAction = .confirmDeleteVM + confirmAction = .confirmDeleteVM(vm: vm) } label: { Label("Delete", systemImage: "trash") }.disabled(!vm.isModifyAllowed) @@ -169,8 +169,8 @@ struct VMContextMenuModifier: ViewModifier { #endif } .modifier(VMShareItemModifier(isPresented: $showSharePopup, shareItem: shareItem)) - .modifier(VMConfirmActionModifier(vm: vm, confirmAction: $confirmAction) { - if confirmAction == .confirmMoveVM { + .modifier(VMConfirmActionModifier(confirmAction: $confirmAction) { action in + if case .confirmMoveVM(let vm) = action { shareItem = .utmMove(vm) showSharePopup.toggle() } diff --git a/Platform/Shared/VMToolbarModifier.swift b/Platform/Shared/VMToolbarModifier.swift index d8c34502..362d8746 100644 --- a/Platform/Shared/VMToolbarModifier.swift +++ b/Platform/Shared/VMToolbarModifier.swift @@ -54,7 +54,7 @@ struct VMToolbarModifier: ViewModifier { #if !WITH_REMOTE // FIXME: implement remote feature if vm.isShortcut { DestructiveButton { - confirmAction = .confirmDeleteShortcut + confirmAction = .confirmDeleteVM(vm: vm) } label: { Label("Remove", systemImage: "trash") .labelStyle(.iconOnly) @@ -63,7 +63,7 @@ struct VMToolbarModifier: ViewModifier { .padding(.leading, padding) } else { DestructiveButton { - confirmAction = .confirmDeleteVM + confirmAction = .confirmDeleteVM(vm: vm) } label: { Label("Delete", systemImage: "trash") .labelStyle(.iconOnly) @@ -77,7 +77,7 @@ struct VMToolbarModifier: ViewModifier { } #endif Button { - confirmAction = .confirmCloneVM + confirmAction = .confirmCloneVM(vm: vm) } label: { Label("Clone", systemImage: "doc.on.doc") .labelStyle(.iconOnly) @@ -91,7 +91,7 @@ struct VMToolbarModifier: ViewModifier { #if os(macOS) if !vm.isShortcut { Button { - confirmAction = .confirmMoveVM + confirmAction = .confirmMoveVM(vm: vm) } label: { Label("Move", systemImage: "arrow.down.doc") .labelStyle(.iconOnly) @@ -116,7 +116,7 @@ struct VMToolbarModifier: ViewModifier { #endif if vm.hasSuspendState || !vm.isStopped { Button { - confirmAction = .confirmStopVM + confirmAction = .confirmStopVM(vm: vm) } label: { Label("Stop", systemImage: "stop") .labelStyle(.iconOnly) @@ -150,8 +150,8 @@ struct VMToolbarModifier: ViewModifier { } } .modifier(VMShareItemModifier(isPresented: $showSharePopup, shareItem: shareItem)) - .modifier(VMConfirmActionModifier(vm: vm, confirmAction: $confirmAction) { - if confirmAction == .confirmMoveVM { + .modifier(VMConfirmActionModifier(confirmAction: $confirmAction) { action in + if case .confirmMoveVM(let vm) = action { shareItem = .utmMove(vm) showSharePopup.toggle() }