swiftbricks/Sources/SwiftBricks/Controls/TabViewControl.swift

311 lines
10 KiB
Swift

import SwiftUI
// MARK: - TabView Control
struct TabViewControl: View {
let schema: ControlSchema
@ObservedObject var engine: BricksEngine
@State private var selectedIndex: Int = 0
var body: some View {
VStack(spacing: 0) {
// Tab
HStack(spacing: 0) {
ForEach(Array(tabs.enumerated()), id: \.offset) { idx, tab in
Button(action: { selectedIndex = idx }) {
HStack(spacing: 4) {
if let icon = tab.icon { Text(icon) }
Text(engine.i18n.t(tab.label))
.font(.subheadline)
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
.foregroundColor(selectedIndex == idx ? .accentColor : .secondary)
.background(selectedIndex == idx ? Color.accentColor.opacity(0.1) : Color.clear)
}
.buttonStyle(.plain)
.overlay(alignment: .bottom) {
Rectangle()
.fill(selectedIndex == idx ? Color.accentColor : Color.clear)
.frame(height: 2)
}
}
Spacer()
}
.background(Color.secondary.opacity(0.05))
//
if selectedIndex < tabs.count {
let tab = tabs[selectedIndex]
if let content = tab.content {
ControlRenderer(schema: content, engine: engine)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
.onAppear {
selectedIndex = schema.options.activeTab ?? 0
}
}
private var tabs: [TabItem] {
schema.options.tabs ?? []
}
}
// MARK: - Menu Control ()
struct MenuControl: View {
let schema: ControlSchema
@ObservedObject var engine: BricksEngine
@State private var expandedItems: Set<String> = []
@State private var selectedItem: String = ""
var body: some View {
let isCollapsed = engine.store.collapsedMenus.contains(schema.effectiveId)
ScrollView {
VStack(alignment: .leading, spacing: 2) {
if let items = schema.options.items {
ForEach(items, id: \.name) { item in
menuItemView(item, depth: 0)
}
}
}
.padding(4)
}
.frame(width: isCollapsed ? 50 : nil)
.background(Color.secondary.opacity(0.05))
.animation(.easeInOut(duration: 0.2), value: isCollapsed)
}
@ViewBuilder
private func menuItemView(_ item: MenuItemSchema, depth: Int) -> AnyView {
let hasChildren = !(item.submenu?.isEmpty ?? true)
let isExpanded = expandedItems.contains(item.name)
let isSelected = selectedItem == item.name
return AnyView(
VStack(alignment: .leading, spacing: 0) {
Button(action: { handleMenuClick(item, hasChildren: hasChildren) }) {
HStack(spacing: 8) {
if let icon = item.icon {
Text(icon)
.frame(width: 20)
}
Text(engine.i18n.t(item.label))
.font(.subheadline)
.lineLimit(1)
Spacer()
if hasChildren {
Image(systemName: isExpanded ? "chevron.down" : "chevron.right")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.horizontal, 12 + CGFloat(depth * 16))
.padding(.vertical, 8)
.foregroundColor(isSelected ? .accentColor : .primary)
.background(isSelected ? Color.accentColor.opacity(0.1) : Color.clear)
.cornerRadius(6)
}
.buttonStyle(.plain)
//
if hasChildren && isExpanded {
if let subitems = item.submenu {
ForEach(subitems, id: \.name) { sub in
menuItemView(sub, depth: depth + 1)
}
}
}
}
)
}
private func handleMenuClick(_ item: MenuItemSchema, hasChildren: Bool) {
if hasChildren {
if expandedItems.contains(item.name) {
expandedItems.remove(item.name)
} else {
expandedItems.insert(item.name)
}
} else {
selectedItem = item.name
// binds
Task {
if let binds = item.binds {
for bind in binds {
if bind.event == "click" {
await engine.handleBind(bind, sourceId: schema.effectiveId)
}
}
}
}
}
}
}
// MARK: - PopupWindow Control
struct PopupWindowControl: View {
let schema: ControlSchema
@ObservedObject var engine: BricksEngine
var body: some View {
VStack(alignment: .leading, spacing: 0) {
//
HStack {
Text(engine.i18n.t(schema.options.title ?? ""))
.font(.headline)
Spacer()
Button(action: close) {
Image(systemName: "xmark")
.font(.caption.bold())
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
}
.padding(12)
.background(Color.secondary.opacity(0.1))
//
if let subwidgets = schema.subwidgets {
VStack(alignment: .leading, spacing: 8) {
SubWidgetsView(subwidgets: subwidgets, engine: engine)
}
.padding(12)
}
}
#if os(iOS)
.background(Color(.systemBackground))
#else
.background(Color(nsColor: .windowBackgroundColor))
#endif
.cornerRadius(12)
.shadow(radius: 8)
}
private func close() {
engine.activePopup = nil
}
}
// MARK: - HTML Control
struct HtmlControl: View {
let schema: ControlSchema
@ObservedObject var engine: BricksEngine
var body: some View {
let html = schema.options.html ?? ""
#if os(iOS)
Text(html) // iOSAttributedString from HTML
.font(.body)
#else
if let data = html.data(using: .utf8),
let attrStr = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) {
Text(AttributedString(attrStr))
} else {
Text(html)
}
#endif
}
}
// MARK: - Image Control
struct ImageControl: View {
let schema: ControlSchema
@ObservedObject var engine: BricksEngine
@State private var image: Image? = nil
@State private var isLoading: Bool = false
var body: some View {
Group {
if let image {
image.resizable().aspectRatio(contentMode: .fit)
} else if isLoading {
ProgressView()
} else {
Rectangle()
.fill(Color.secondary.opacity(0.1))
.overlay(Text(schema.options.alt ?? "🖼️"))
}
}
.task {
await loadImage()
}
}
private func loadImage() async {
guard let src = schema.options.src, let url = URL(string: src) else { return }
isLoading = true
do {
let (data, _) = try await URLSession.shared.data(from: url)
#if os(iOS)
if let uiImage = UIImage(data: data) {
image = Image(uiImage: uiImage)
}
#else
if let nsImage = NSImage(data: data) {
image = Image(nsImage: nsImage)
}
#endif
} catch {
print("[SwiftBricks] Image load failed: \(error)")
}
isLoading = false
}
}
// MARK: - UrlWidget Control (widget)
struct UrlWidgetControl: View {
let schema: ControlSchema
@ObservedObject var engine: BricksEngine
@State private var loadedSchema: ControlSchema? = nil
@State private var isLoading: Bool = true
@State private var errorMessage: String? = nil
var body: some View {
Group {
if isLoading {
ProgressView("加载中...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let error = errorMessage {
VStack {
Image(systemName: "exclamationmark.triangle")
.foregroundColor(.orange)
Text(error).font(.caption).foregroundColor(.secondary)
}
} else if let schema = loadedSchema {
ControlRenderer(schema: schema, engine: engine)
}
}
.task {
await loadContent()
}
}
private func loadContent() async {
guard let url = schema.options.url else {
errorMessage = "缺少url"
isLoading = false
return
}
do {
let response = try await engine.rpc.get(url)
if response.isSuccess {
loadedSchema = try JSONDecoder().decode(ControlSchema.self, from: response.data)
} else {
errorMessage = "HTTP \(response.statusCode)"
}
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}