Files
Termind/TFI/UI/Components/MobileEditorSheet.swift
T

109 lines
4.1 KiB
Swift

import SwiftUI
struct MobileEditorToolbarAction: Identifiable {
let id: String
let title: LocalizedStringKey
let systemImage: String
var role: ButtonRole? = nil
var startsGroup = false
var isEnabled = true
let perform: () -> Void
}
/// Presentation is mobile-owned; form content can also live in desktop work columns.
struct MobileEditorSheet<Content: View>: View {
let title: String
let isDirty: Bool
var canSave = true
var compact = false
var actions: [MobileEditorToolbarAction] = []
var onCancel: () -> Void = {}
var topBar: AnyView? = nil
let save: () async throws -> Void
@ViewBuilder let content: () -> Content
@Environment(\.dismiss) private var dismiss
@State private var discard = false
@State private var error: String?
@State private var isSaving = false
@State private var saveTask: Task<Void, Never>?
var body: some View {
NavigationStack {
VStack(spacing: 0) {
if let topBar {
topBar
.disabled(isSaving)
.padding(.horizontal, 20)
.padding(.vertical, 12)
.frame(maxWidth: .infinity)
.background(.bar)
Divider()
}
Form {
content().disabled(isSaving)
if isSaving { ProgressView().frame(maxWidth: .infinity) }
}
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", systemImage: "xmark") {
if isDirty {
discard = true
} else {
cancel()
}
}
.labelStyle(.iconOnly).disabled(isSaving)
}
if !actions.isEmpty {
MobileToolbarOverflow {
ForEach(actions) { action in
if action.startsGroup { Divider() }
Button(role: action.role, action: action.perform) {
Label(action.title, systemImage: action.systemImage)
}
.disabled(!action.isEnabled || isSaving)
}
}
}
ToolbarItem(placement: .confirmationAction) {
Button {
guard !isSaving else { return }
isSaving = true
saveTask = Task {
defer { isSaving = false }
do {
try await save()
try Task.checkCancellation()
dismiss()
} catch is CancellationError {
} catch { self.error = error.localizedDescription }
}
} label: {
Label("Save", systemImage: "checkmark")
}.labelStyle(.iconOnly).accessibilityLabel(Text("Save")).disabled(!canSave || isSaving)
}
.mobileProminentAction()
}
.confirmationDialog("Discard Changes?", isPresented: $discard, titleVisibility: .visible) {
Button("Discard Changes", role: .destructive, action: cancel)
}
.alert("Unable to Save", isPresented: Binding(get: { error != nil }, set: { if !$0 { error = nil } })) {
Button("OK", role: .cancel, action: { error = nil })
} message: {
Text(error ?? "")
}
}
.mobileSheetPresentation(compact: compact)
.interactiveDismissDisabled(isDirty || isSaving)
.onDisappear { saveTask?.cancel() }
}
private func cancel() {
onCancel()
dismiss()
}
}