mirror of
https://github.com/carey314/mio-plugin-fund.git
synced 2026-08-10 05:44:31 +00:00
179 lines
6.3 KiB
Swift
179 lines
6.3 KiB
Swift
|
|
//
|
||
|
|
// ConfirmDialog.swift
|
||
|
|
// 盯基金 plugin v0.3
|
||
|
|
//
|
||
|
|
// Programmatic confirm modal — `await ConfirmController.shared.ask(...)`
|
||
|
|
// returns true / false. Modeled after the web's `window.confirm` so
|
||
|
|
// callers don't have to thread modal state through their views.
|
||
|
|
//
|
||
|
|
// Implementation: single in-flight confirm at a time (rare enough that
|
||
|
|
// serializing is fine). The async result is delivered via a
|
||
|
|
// CheckedContinuation captured in the request.
|
||
|
|
//
|
||
|
|
|
||
|
|
import SwiftUI
|
||
|
|
|
||
|
|
struct ConfirmRequest: Identifiable {
|
||
|
|
let id = UUID()
|
||
|
|
let title: String
|
||
|
|
let message: String?
|
||
|
|
let confirmLabel: String
|
||
|
|
let cancelLabel: String
|
||
|
|
let danger: Bool
|
||
|
|
let continuation: CheckedContinuation<Bool, Never>
|
||
|
|
}
|
||
|
|
|
||
|
|
@MainActor
|
||
|
|
final class ConfirmController: ObservableObject {
|
||
|
|
static let shared = ConfirmController()
|
||
|
|
private init() {}
|
||
|
|
|
||
|
|
@Published var pending: ConfirmRequest? = nil
|
||
|
|
|
||
|
|
/// Returns once the user picks. Resolves false if dismissed by
|
||
|
|
/// tapping the backdrop or pressing Esc-equivalent (cancel button).
|
||
|
|
func ask(
|
||
|
|
title: String,
|
||
|
|
message: String? = nil,
|
||
|
|
confirmLabel: String = "确认",
|
||
|
|
cancelLabel: String = "取消",
|
||
|
|
danger: Bool = false
|
||
|
|
) async -> Bool {
|
||
|
|
await withCheckedContinuation { (cont: CheckedContinuation<Bool, Never>) in
|
||
|
|
let req = ConfirmRequest(
|
||
|
|
title: title,
|
||
|
|
message: message,
|
||
|
|
confirmLabel: confirmLabel,
|
||
|
|
cancelLabel: cancelLabel,
|
||
|
|
danger: danger,
|
||
|
|
continuation: cont
|
||
|
|
)
|
||
|
|
// If a confirm is already showing (shouldn't normally happen),
|
||
|
|
// resolve the previous one as cancelled and replace it. This
|
||
|
|
// prevents the continuation from leaking.
|
||
|
|
if let old = pending {
|
||
|
|
old.continuation.resume(returning: false)
|
||
|
|
}
|
||
|
|
withAnimation(.spring(response: 0.32, dampingFraction: 0.78)) {
|
||
|
|
pending = req
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func respond(_ value: Bool) {
|
||
|
|
guard let req = pending else { return }
|
||
|
|
req.continuation.resume(returning: value)
|
||
|
|
withAnimation(.easeOut(duration: 0.18)) {
|
||
|
|
pending = nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Overlay view
|
||
|
|
|
||
|
|
struct ConfirmOverlay: View {
|
||
|
|
@ObservedObject var controller = ConfirmController.shared
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
Group {
|
||
|
|
if let req = controller.pending {
|
||
|
|
ZStack {
|
||
|
|
Color.black.opacity(0.55)
|
||
|
|
.ignoresSafeArea()
|
||
|
|
.onTapGesture { controller.respond(false) }
|
||
|
|
ConfirmCard(req: req)
|
||
|
|
.transition(.scale(scale: 0.94).combined(with: .opacity))
|
||
|
|
}
|
||
|
|
.transition(.opacity)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private struct ConfirmCard: View {
|
||
|
|
let req: ConfirmRequest
|
||
|
|
@State private var hoveredCancel = false
|
||
|
|
@State private var hoveredConfirm = false
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
VStack(alignment: .leading, spacing: 10) {
|
||
|
|
HStack(alignment: .top, spacing: 10) {
|
||
|
|
ZStack {
|
||
|
|
Circle()
|
||
|
|
.fill((req.danger ? Color.red : FundTheme.lime).opacity(0.18))
|
||
|
|
.frame(width: 32, height: 32)
|
||
|
|
Image(systemName: req.danger ? "exclamationmark.triangle.fill" : "questionmark.circle.fill")
|
||
|
|
.font(.system(size: 14, weight: .semibold))
|
||
|
|
.foregroundColor(req.danger ? Color.red : FundTheme.lime)
|
||
|
|
}
|
||
|
|
VStack(alignment: .leading, spacing: 4) {
|
||
|
|
Text(req.title)
|
||
|
|
.font(.system(size: 13, weight: .semibold))
|
||
|
|
.foregroundColor(FundTheme.fgPrimary)
|
||
|
|
if let msg = req.message {
|
||
|
|
Text(msg)
|
||
|
|
.font(.system(size: 11.5))
|
||
|
|
.foregroundColor(FundTheme.fg55)
|
||
|
|
.fixedSize(horizontal: false, vertical: true)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Spacer(minLength: 0)
|
||
|
|
}
|
||
|
|
HStack(spacing: 8) {
|
||
|
|
Spacer()
|
||
|
|
Button(action: { ConfirmController.shared.respond(false) }) {
|
||
|
|
Text(req.cancelLabel)
|
||
|
|
.font(.system(size: 11.5, weight: .semibold))
|
||
|
|
.foregroundColor(FundTheme.fg55)
|
||
|
|
.padding(.horizontal, 14)
|
||
|
|
.frame(height: 28)
|
||
|
|
.background(
|
||
|
|
Capsule().fill(hoveredCancel ? FundTheme.overlay06 : FundTheme.overlay04)
|
||
|
|
)
|
||
|
|
}
|
||
|
|
.buttonStyle(.plain)
|
||
|
|
.onHover { hoveredCancel = $0 }
|
||
|
|
|
||
|
|
Button(action: { ConfirmController.shared.respond(true) }) {
|
||
|
|
Text(req.confirmLabel)
|
||
|
|
.font(.system(size: 11.5, weight: .semibold))
|
||
|
|
.foregroundColor(req.danger ? .white : .black)
|
||
|
|
.padding(.horizontal, 14)
|
||
|
|
.frame(height: 28)
|
||
|
|
.background(
|
||
|
|
Capsule().fill(
|
||
|
|
req.danger
|
||
|
|
? Color(red: 0.92, green: 0.30, blue: 0.30).opacity(hoveredConfirm ? 1.0 : 0.88)
|
||
|
|
: FundTheme.lime.opacity(hoveredConfirm ? 1.0 : 0.92)
|
||
|
|
)
|
||
|
|
)
|
||
|
|
}
|
||
|
|
.buttonStyle(.plain)
|
||
|
|
.onHover { hoveredConfirm = $0 }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.padding(14)
|
||
|
|
.frame(maxWidth: 320)
|
||
|
|
.background(
|
||
|
|
RoundedRectangle(cornerRadius: 14)
|
||
|
|
.fill(Color(red: 0.07, green: 0.07, blue: 0.08))
|
||
|
|
.overlay(
|
||
|
|
RoundedRectangle(cornerRadius: 14)
|
||
|
|
.stroke(Color.white.opacity(0.06), lineWidth: 0.8)
|
||
|
|
)
|
||
|
|
.shadow(color: Color.black.opacity(0.6), radius: 16, x: 0, y: 8)
|
||
|
|
)
|
||
|
|
.padding(.horizontal, 20)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - View modifier
|
||
|
|
|
||
|
|
extension View {
|
||
|
|
/// Attach the confirm dialog layer. Call once at the root of the
|
||
|
|
/// plugin's view tree (ExpandedView).
|
||
|
|
func confirmOverlay() -> some View {
|
||
|
|
self.overlay(ConfirmOverlay())
|
||
|
|
}
|
||
|
|
}
|