toolbar: use updated confirmation on iOS 15+

This commit is contained in:
osy
2026-05-03 13:26:15 -07:00
parent 113329e7c6
commit 24c850712a
3 changed files with 151 additions and 49 deletions
+137 -35
View File
@@ -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
}
}
}
}
+7 -7
View File
@@ -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()
}
+7 -7
View File
@@ -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()
}