Add toolbar for JSON editor

This commit is contained in:
世界
2026-01-05 16:28:59 +08:00
parent 3b447dd46c
commit 91edd77b00
10 changed files with 462 additions and 5 deletions
@@ -1,4 +1,5 @@
import Foundation
import Libbox
import Library
import SwiftUI
@@ -7,8 +8,10 @@ public final class EditProfileContentViewModel: BaseViewModel {
@Published public var profile: Profile?
@Published public var profileContent = ""
@Published public var isChanged = false
@Published public var configurationError: String?
private let profileID: Int64?
private var validationTask: Task<Void, Never>?
public init(profileID: Int64?) {
self.profileID = profileID
@@ -18,6 +21,7 @@ public final class EditProfileContentViewModel: BaseViewModel {
public func markAsChanged() {
isChanged = true
scheduleValidation()
}
public func reset() {
@@ -25,9 +29,53 @@ public final class EditProfileContentViewModel: BaseViewModel {
profile = nil
profileContent = ""
isChanged = false
configurationError = nil
validationTask?.cancel()
validationTask = nil
alert = nil
}
public func scheduleValidation() {
configurationError = nil
validationTask?.cancel()
validationTask = Task {
try? await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
guard !Task.isCancelled else { return }
await checkConfiguration()
}
}
private func checkConfiguration() async {
let content = profileContent
if content.isEmpty { return }
var error: NSError?
LibboxCheckConfig(content, &error)
if let error {
configurationError = error.localizedDescription
} else {
configurationError = nil
}
}
public func formatConfiguration() async {
let content = profileContent
if content.isEmpty { return }
var error: NSError?
let result = LibboxFormatConfig(content, &error)
if let error {
configurationError = error.localizedDescription
return
}
if let formatted = result?.value, formatted != content {
profileContent = formatted
isChanged = true
}
}
public func dismissConfigurationError() {
configurationError = nil
}
public func loadContent() async {
do {
try await loadContentBackground()
@@ -0,0 +1,149 @@
import SwiftUI
#if os(iOS) || os(macOS)
public struct EditorToolbarView: View {
let canUndo: Bool
let canRedo: Bool
let onUndo: () -> Void
let onRedo: () -> Void
let onFormat: () -> Void
let onInsertSymbol: (String) -> Void
let configurationError: String?
let onDismissError: () -> Void
public init(
canUndo: Bool,
canRedo: Bool,
onUndo: @escaping () -> Void,
onRedo: @escaping () -> Void,
onFormat: @escaping () -> Void,
onInsertSymbol: @escaping (String) -> Void,
configurationError: String?,
onDismissError: @escaping () -> Void
) {
self.canUndo = canUndo
self.canRedo = canRedo
self.onUndo = onUndo
self.onRedo = onRedo
self.onFormat = onFormat
self.onInsertSymbol = onInsertSymbol
self.configurationError = configurationError
self.onDismissError = onDismissError
}
public var body: some View {
VStack(spacing: 2) {
if let error = configurationError {
errorBanner(error)
}
symbolBar
}
}
private func errorBanner(_ error: String) -> some View {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
Text(error)
.font(.footnote)
.lineLimit(2)
Spacer()
Button {
onDismissError()
} label: {
Image(systemName: "xmark")
.font(.footnote.weight(.semibold))
}
.buttonStyle(.plain)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
#if os(iOS)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12))
#else
.background(Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: 8))
#endif
.padding(.horizontal, 8)
}
private var symbolBar: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
actionButtons
Divider().frame(height: 24).padding(.horizontal, 4)
symbolButtons
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
}
#if os(iOS)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12))
#else
.background(Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: 8))
#endif
.padding(.horizontal, 8)
.padding(.bottom, 8)
}
private var actionButtons: some View {
HStack(spacing: 4) {
Button {
onUndo()
} label: {
Label("Undo", systemImage: "arrow.uturn.backward")
}
.disabled(!canUndo)
Button {
onRedo()
} label: {
Label("Redo", systemImage: "arrow.uturn.forward")
}
.disabled(!canRedo)
Button {
onFormat()
} label: {
Label("Format", systemImage: "text.alignleft")
}
}
.buttonStyle(.bordered)
}
private var symbolButtons: some View {
HStack(spacing: 2) {
ForEach(primarySymbols, id: \.self) { symbol in
symbolButton(symbol)
}
ForEach(secondarySymbols, id: \.self) { symbol in
symbolButton(symbol)
}
}
.buttonStyle(.bordered)
}
private var primarySymbols: [String] {
["\"", ":", ",", "{", "}", "[", "]"]
}
private var secondarySymbols: [String] {
["true", "false"]
}
private func symbolButton(_ symbol: String) -> some View {
Button {
onInsertSymbol(symbol)
} label: {
Text(symbol)
.font(.system(.body, design: .monospaced).weight(.medium))
#if os(iOS)
.frame(minWidth: symbol.count > 1 ? nil : 32, minHeight: 32)
#else
.frame(minWidth: symbol.count > 1 ? nil : 24)
#endif
}
}
}
#endif