116 lines
4.4 KiB
Swift
116 lines
4.4 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#if os(iOS)
|
|
import SwiftUI
|
|
import TermindApplication
|
|
|
|
public enum OperationConfirmationPlacement {
|
|
case anchored
|
|
case centered
|
|
}
|
|
|
|
public enum OperationConfirmationText {
|
|
case localized(LocalizedStringResource)
|
|
case verbatim(String)
|
|
|
|
public init(_ resource: LocalizedStringResource) { self = .localized(resource) }
|
|
public init(_ value: String) { self = .verbatim(value) }
|
|
|
|
fileprivate func resolve(locale: Locale) -> String {
|
|
switch self {
|
|
case .localized(let resource): AppLocalization.resolve(resource, locale: locale)
|
|
case .verbatim(let value): value
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct OperationConfirmationAction {
|
|
let title: LocalizedStringResource
|
|
let role: ButtonRole?
|
|
let perform: () -> Void
|
|
|
|
public init(_ title: LocalizedStringResource, role: ButtonRole? = nil, action: @escaping () -> Void) {
|
|
self.title = title
|
|
self.role = role
|
|
perform = action
|
|
}
|
|
}
|
|
|
|
/// The app provides centered confirmations; anchored confirmations use SwiftUI's native source transition.
|
|
public struct OperationConfirmationRequest {
|
|
public struct Action {
|
|
public let title: String
|
|
public let role: ButtonRole?
|
|
public let perform: () -> Void
|
|
}
|
|
|
|
public let title: String
|
|
public let message: String?
|
|
public let isPresented: Binding<Bool>
|
|
public let placement: OperationConfirmationPlacement
|
|
public let actions: [Action]
|
|
}
|
|
|
|
public typealias OperationConfirmationRenderer = @MainActor @Sendable (OperationConfirmationRequest) -> AnyView
|
|
|
|
extension EnvironmentValues {
|
|
@Entry public var operationConfirmationRenderer: OperationConfirmationRenderer? = nil
|
|
}
|
|
|
|
extension View {
|
|
public func operationConfirmation(
|
|
_ title: OperationConfirmationText, isPresented: Binding<Bool>,
|
|
placement: OperationConfirmationPlacement = .anchored,
|
|
actions: [OperationConfirmationAction], message: OperationConfirmationText? = nil
|
|
) -> some View {
|
|
modifier(
|
|
OperationConfirmation(
|
|
title: title, isPresented: isPresented, placement: placement, actions: actions, message: message))
|
|
}
|
|
}
|
|
|
|
private struct OperationConfirmation: ViewModifier {
|
|
let title: OperationConfirmationText
|
|
@Binding var isPresented: Bool
|
|
let placement: OperationConfirmationPlacement
|
|
let actions: [OperationConfirmationAction]
|
|
let message: OperationConfirmationText?
|
|
@Environment(\.locale) private var locale
|
|
@Environment(\.operationConfirmationRenderer) private var renderer
|
|
|
|
func body(content: Content) -> some View {
|
|
if let renderer, placement == .centered {
|
|
content.background {
|
|
renderer(
|
|
OperationConfirmationRequest(
|
|
title: title.resolve(locale: locale), message: message?.resolve(locale: locale),
|
|
isPresented: $isPresented, placement: placement,
|
|
actions: actions.map {
|
|
.init(
|
|
title: AppLocalization.resolve($0.title, locale: locale), role: $0.role,
|
|
perform: $0.perform)
|
|
}))
|
|
}
|
|
} else {
|
|
content.confirmationDialog(
|
|
Text(verbatim: title.resolve(locale: locale)), isPresented: $isPresented,
|
|
titleVisibility: .visible
|
|
) {
|
|
ForEach(actions.indices, id: \.self) { index in
|
|
// Keep the explicit Cancel visible in the floating menu; SwiftUI owns outside dismissal.
|
|
Button(
|
|
role: actions[index].role == .cancel ? nil : actions[index].role,
|
|
action: actions[index].perform
|
|
) {
|
|
Text(actions[index].title)
|
|
}
|
|
}
|
|
} message: {
|
|
if let message { Text(verbatim: message.resolve(locale: locale)) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|