import SwiftUI public struct SettingsView: View { @State public var config: SyncConfig public var onSave: (SyncConfig) -> Void @State private var serverURL: String = "" @State private var apiToken: String = "" @State private var syncFolder: String = "" @State private var defaultUploadFolder: String = "" @State private var newNoteAction: NewNoteAction = .clipboard @State private var autoSync: Bool = true @State private var syncInterval: Double = 30 @State private var selectedIcon: String = "tray.circle.fill" private let labelWidth: CGFloat = 120 public init(config: SyncConfig, onSave: @escaping (SyncConfig) -> Void) { self.config = config self.onSave = onSave } public var body: some View { VStack(spacing: 0) { ScrollView { VStack(spacing: 0) { serverSection Divider().padding(.vertical, 12) syncSection Divider().padding(.vertical, 12) uploadsSection Divider().padding(.vertical, 12) appearanceSection } .padding(20) } .scrollIndicators(.hidden) Divider() saveButton .padding(.horizontal, 20) .padding(.vertical, 12) } .frame(width: 520, height: 640) .onAppear { serverURL = config.serverURL apiToken = config.apiToken ?? "" syncFolder = config.syncFolder ?? "" defaultUploadFolder = config.defaultUploadFolder ?? "" newNoteAction = config.effectiveNewNoteAction autoSync = config.autoSync syncInterval = Double(config.syncInterval) selectedIcon = config.iconName } } // MARK: - Sections private var serverSection: some View { VStack(alignment: .leading, spacing: 12) { sectionHeader("Server", icon: "server.rack") formRow(label: "Server URL") { TextField("", text: $serverURL) .textFieldStyle(.roundedBorder) .frame(width: 280) } formRow(label: "API Token") { SecureField("", text: $apiToken) .textFieldStyle(.roundedBorder) .frame(width: 280) } HStack { Spacer() .frame(width: labelWidth) Text("Create an API token from the web app at /account/tokens/new") .font(.caption) .foregroundColor(.secondary) .lineLimit(nil) .fixedSize(horizontal: false, vertical: true) .frame(width: 280, alignment: .leading) } } } private var syncSection: some View { VStack(alignment: .leading, spacing: 12) { sectionHeader("Sync", icon: "arrow.triangle.2.circlepath") formRow(label: "Sync Folder") { HStack(spacing: 8) { TextField("", text: $syncFolder) .textFieldStyle(.roundedBorder) .frame(width: 200) Button("Choose...") { chooseFolder() } .controlSize(.small) } } formRow(label: "") { Toggle("Auto Sync", isOn: $autoSync) } if autoSync { formRow(label: "Interval") { HStack(spacing: 12) { Slider(value: $syncInterval, in: 5...300, step: 5) .frame(width: 160) Text("\(Int(syncInterval))s") .font(.system(size: 12, design: .monospaced)) .foregroundColor(.secondary) .frame(width: 40, alignment: .trailing) } } } } } private var uploadsSection: some View { VStack(alignment: .leading, spacing: 12) { sectionHeader("Uploads", icon: "arrow.up.doc") formRow(label: "Upload Folder") { TextField("e.g. inbox", text: $defaultUploadFolder) .textFieldStyle(.roundedBorder) .frame(width: 280) } formRow(label: "New Note") { Picker("", selection: $newNoteAction) { Text("From Clipboard").tag(NewNoteAction.clipboard) Text("Open Terminal $EDITOR").tag(NewNoteAction.editor) } .pickerStyle(.menu) .frame(width: 200) } HStack { Spacer() .frame(width: labelWidth) Text("Notes and uploads will be saved to this folder. Editor notes upload after the editor exits.") .font(.caption) .foregroundColor(.secondary) .frame(width: 280, alignment: .leading) } } } private var appearanceSection: some View { VStack(alignment: .leading, spacing: 12) { sectionHeader("Appearance", icon: "paintbrush") formRow(label: "Menu Icon") { Picker("", selection: $selectedIcon) { ForEach(SyncConfig.iconOptions, id: \.self) { icon in HStack(spacing: 8) { Image(systemName: icon) .frame(width: 20, alignment: .center) Text(iconNameToLabel(icon)) } .tag(icon) } } .pickerStyle(.menu) .frame(width: 180) } } } private var saveButton: some View { HStack { Spacer() Button { let newConfig = SyncConfig( serverURL: serverURL, apiToken: apiToken.isEmpty ? nil : apiToken, syncFolder: syncFolder.isEmpty ? nil : syncFolder, defaultUploadFolder: defaultUploadFolder.isEmpty ? nil : defaultUploadFolder, newNoteAction: newNoteAction, autoSync: autoSync, syncInterval: Int(syncInterval), deviceId: config.deviceId, iconName: selectedIcon ) onSave(newConfig) } label: { Text("Save") .frame(width: 80) } .keyboardShortcut(.defaultAction) .controlSize(.large) .buttonStyle(.borderedProminent) } .frame(maxWidth: .infinity) } // MARK: - Helpers private func sectionHeader(_ title: String, icon: String) -> some View { HStack(spacing: 8) { Image(systemName: icon) .foregroundColor(.accentColor) .font(.system(size: 16, weight: .medium)) Text(title) .font(.system(size: 16, weight: .semibold)) Spacer() } .padding(.bottom, 4) } private func formRow(label: String, @ViewBuilder content: () -> Content) -> some View { HStack(alignment: .firstTextBaseline, spacing: 16) { Text(label) .font(.system(size: 13)) .foregroundColor(.primary) .frame(width: labelWidth, alignment: .trailing) content() Spacer() } } private func chooseFolder() { let panel = NSOpenPanel() panel.canChooseFiles = false panel.canChooseDirectories = true panel.allowsMultipleSelection = false panel.prompt = "Select Folder" if panel.runModal() == .OK { syncFolder = panel.url?.path ?? "" } } private func iconNameToLabel(_ name: String) -> String { let labels: [String: String] = [ "tray.circle.fill": "Tray (Default)", "arrow.triangle.2.circlepath": "Sync", "arrow.up.doc": "Upload", "doc.badge.arrow.up": "Doc Upload", "arrow.up.and.down.and.arrow.left.and.right": "Transfer", "cloud": "Cloud", "externaldrive.connected.to.line.below": "Server", "note.text": "Notes", "doc.text": "Documents", "arrow.clockwise.circle": "Refresh", "rectangle.stack.badge.plus": "Stack" ] return labels[name] ?? name } } #Preview { SettingsView(config: SyncConfig()) { _ in } }