From b5ad9c1ec317540396a7657c867af52866d75df0 Mon Sep 17 00:00:00 2001 From: tigerenwork Date: Sat, 11 Jul 2026 01:32:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=A1=B5=E9=9D=A2=E5=B8=83=E5=B1=80=EF=BC=8C=E7=94=A8?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E5=88=86=E5=8C=BA=E5=92=8C=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E8=A1=8C=E6=9B=BF=E4=BB=A3=20Form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/BuggerApp.swift | 2 +- Sources/Views/Settings/SettingsView.swift | 397 ++++++++++++++-------- 2 files changed, 255 insertions(+), 144 deletions(-) diff --git a/Sources/BuggerApp.swift b/Sources/BuggerApp.swift index 2a1baa3..9224075 100644 --- a/Sources/BuggerApp.swift +++ b/Sources/BuggerApp.swift @@ -8,6 +8,6 @@ struct BuggerApp: App { Settings { SettingsView() } - .windowResizability(.contentSize) + .windowResizability(.contentMinSize) } } diff --git a/Sources/Views/Settings/SettingsView.swift b/Sources/Views/Settings/SettingsView.swift index 33a0d06..316d1d7 100644 --- a/Sources/Views/Settings/SettingsView.swift +++ b/Sources/Views/Settings/SettingsView.swift @@ -1,6 +1,10 @@ import ServiceManagement import SwiftUI +// MARK: - Constants + +private let kLabelWidth: CGFloat = 130 + struct SettingsView: View { @State private var config: AppConfig @State private var isTestingConnection = false @@ -13,96 +17,50 @@ struct SettingsView: View { var body: some View { ScrollView { - VStack(spacing: 12) { - // Highlighted "Start here" card lives outside the Form so its - // Spacer + multi-element HStack don't disturb the Form's - // two-column label/control layout (which was pushing the - // Feishu Bitable fields off the right edge of the window). - HStack(spacing: 12) { - Image(systemName: "person.fill") - .font(.title2) - .foregroundStyle(.tint) - Text("Your Name") - .font(.headline) - TextField("e.g. Alice", text: $config.assigneeName) - .textFieldStyle(.roundedBorder) - Spacer() - } - .padding(10) - .background(RoundedRectangle(cornerRadius: 8).fill(.tint.opacity(0.08))) - .overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(.tint.opacity(0.35), lineWidth: 1)) + VStack(alignment: .leading, spacing: 20) { + // MARK: Your Name card + yourNameCard - Form { - Section("Feishu Bitable") { - TextField("App Token (from Bitable URL)", text: $config.appToken) - TextField("Table ID", text: $config.tableId) - TextField("Feishu domain", text: $config.feishuBaseDomain) - - VStack(alignment: .leading, spacing: 4) { - HStack(spacing: 8) { - Button("Test Connection") { - testConnection() - } - .disabled(isTestingConnection || !config.isConfigured) - - if isTestingConnection { - ProgressView() - .scaleEffect(0.7) - } - } - if let connectionResult { - Text(connectionResult) - .font(.caption) - .foregroundStyle(connectionResult.contains("✓") ? .green : .red) - .lineLimit(3) - .fixedSize(horizontal: false, vertical: true) - } + // MARK: Feishu Bitable + settingsSection("Feishu Bitable") { + labeledRow("App Token") { + TextField("from Bitable URL", text: $config.appToken) + .textFieldStyle(.roundedBorder) } + labeledRow("Table ID") { + TextField("", text: $config.tableId) + .textFieldStyle(.roundedBorder) + } + labeledRow("Feishu domain") { + TextField("e.g. feishu.cn", text: $config.feishuBaseDomain) + .textFieldStyle(.roundedBorder) + } + connectionTestRow } - Section("Polling") { - Picker("Check every", selection: $config.pollIntervalSeconds) { - Text("1 minute").tag(60) - Text("5 minutes").tag(300) - Text("10 minutes").tag(600) - Text("30 minutes").tag(1_800) - Text("1 hour").tag(3_600) - Text("2 hours").tag(7_200) - Text("4 hours").tag(14_400) - Text("8 hours").tag(28_800) - Text("12 hours").tag(43_200) - Text("1 day").tag(86_400) - Divider() - Text("Daily schedule").tag(-1) + // MARK: Polling + settingsSection("Polling") { + labeledRow("Check every") { + Picker("", selection: $config.pollIntervalSeconds) { + Text("1 minute").tag(60) + Text("5 minutes").tag(300) + Text("10 minutes").tag(600) + Text("30 minutes").tag(1_800) + Text("1 hour").tag(3_600) + Text("2 hours").tag(7_200) + Text("4 hours").tag(14_400) + Text("8 hours").tag(28_800) + Text("12 hours").tag(43_200) + Text("1 day").tag(86_400) + Divider() + Text("Daily schedule").tag(-1) + } + .labelsHidden() + .fixedSize() } if config.pollIntervalSeconds == -1 { - VStack(alignment: .leading, spacing: 6) { - Text("Refresh at these times:") - .font(.caption) - .foregroundStyle(.secondary) - - ForEach(dailyTimeBindings.indices, id: \.self) { index in - HStack { - DatePicker("", selection: dailyTimeBindings[index], - displayedComponents: .hourAndMinute) - .labelsHidden() - - Button(action: { removeDailyTime(at: index) }) { - Image(systemName: "minus.circle.fill") - .foregroundStyle(.red) - } - .buttonStyle(.plain) - .disabled(config.dailyRefreshTimes.count <= 1) - } - } - - Button(action: addDailyTime) { - Label("Add time", systemImage: "plus.circle") - .font(.caption) - } - .buttonStyle(.plain) - } + dailyScheduleEditor } VStack(alignment: .leading, spacing: 4) { @@ -113,19 +71,21 @@ struct SettingsView: View { } } - Section("Real-time updates") { - VStack(alignment: .leading, spacing: 4) { - TextField("Subscribe base URL", text: $config.feishuAppBaseURL) + // MARK: Real-time updates + settingsSection("Real-time updates") { + labeledRow("Subscribe URL") { + TextField("e.g. https://example.com", text: $config.feishuAppBaseURL) .textFieldStyle(.roundedBorder) .autocorrectionDisabled() - Text("Base URL of the change-notification service, e.g. http://localhost:8000. When set, Bugger listens for change pushes and refreshes immediately — auto-refresh on change is enabled.") - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) } + Text("Base URL of the change-notification service. When set, Bugger listens for change pushes and refreshes immediately — auto-refresh on change is enabled.") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) } - Section("Calibration") { + // MARK: Calibration + settingsSection("Calibration") { Toggle("Enable calibration check", isOn: $config.calibrationEnabled) Text("Periodically verifies with the notification server that all your assigned bugs are in sync. Helps recover missed push notifications. Only works when a Subscribe base URL is set.") .font(.caption) @@ -133,7 +93,8 @@ struct SettingsView: View { .fixedSize(horizontal: false, vertical: true) } - Section("Display") { + // MARK: Display + settingsSection("Display") { Toggle("Show floating widget", isOn: $config.showFloatingWidget) .onChange(of: config.showFloatingWidget) { _, enabled in if enabled { @@ -145,57 +106,207 @@ struct SettingsView: View { Toggle("Launch at login", isOn: $config.launchAtLogin) } - Section { - DisclosureGroup(isExpanded: $showAdvanced) { - Text("Field Mappings") - .font(.caption) - .fontWeight(.semibold) - .foregroundStyle(.secondary) + // MARK: Advanced + advancedSection - TextField("Title field", text: $config.fieldMappings.titleField) - TextField("Priority field", text: $config.fieldMappings.priorityField) - TextField("Status field", text: $config.fieldMappings.statusField) - TextField("Assignee field", text: $config.fieldMappings.assigneeField) - TextField("Reporter field", text: $config.fieldMappings.reporterField) - TextField("Customer field", text: $config.fieldMappings.customerField) - TextField("Created At field", text: $config.fieldMappings.createdAtField) - TextField("Updated At field", text: $config.fieldMappings.updatedAtField) - - Text("Status Mappings") - .font(.caption) - .fontWeight(.semibold) - .foregroundStyle(.secondary) - Text("Map your Feishu status values to standard Bugger statuses.") - .font(.caption) - .foregroundStyle(.secondary) - - ForEach(BugStatus.allCases.filter { $0 != .unknown }, id: \.self) { status in - statusMappingRow(for: status) - } - } label: { - Label("Advanced: field & status mappings", systemImage: "slider.horizontal.3") - } - } - - HStack { - Button("Save") { - save() - } - .keyboardShortcut(.return) - - Button("Disconnect Feishu") { - TokenManager.shared.clearTokens() - PollerService.shared.stop() - BitableEventService.shared.disconnect() - CalibrationService.shared.stop() - } - .foregroundStyle(.red) - } - } // Form - } // VStack - .padding() + // MARK: Actions + actionButtons + } + .frame(maxWidth: 700, alignment: .leading) + .frame(maxWidth: .infinity, alignment: .center) + .padding(20) + } + .frame(minWidth: 520, idealWidth: 560, maxWidth: .infinity, + minHeight: 400, idealHeight: 600, maxHeight: .infinity) + } + + // MARK: - Your Name Card + + private var yourNameCard: some View { + HStack(spacing: 12) { + Image(systemName: "person.fill") + .font(.title2) + .foregroundStyle(.tint) + Text("Your Name") + .font(.headline) + TextField("e.g. Alice", text: $config.assigneeName) + .textFieldStyle(.roundedBorder) + } + .padding(10) + .background(RoundedRectangle(cornerRadius: 8).fill(.tint.opacity(0.08))) + .overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(.tint.opacity(0.35), lineWidth: 1)) + } + + // MARK: - Connection Test + + private var connectionTestRow: some View { + HStack(spacing: 8) { + Button("Test Connection") { + testConnection() + } + .disabled(isTestingConnection || !config.isConfigured) + + if isTestingConnection { + ProgressView() + .scaleEffect(0.7) + } + + if let connectionResult { + Text(connectionResult) + .font(.caption) + .foregroundStyle(connectionResult.contains("✓") ? .green : .red) + .lineLimit(3) + } + } + } + + // MARK: - Daily Schedule Editor + + private var dailyScheduleEditor: some View { + VStack(alignment: .leading, spacing: 6) { + Text("Refresh at these times:") + .font(.caption) + .foregroundStyle(.secondary) + + ForEach(dailyTimeBindings.indices, id: \.self) { index in + HStack { + DatePicker("", selection: dailyTimeBindings[index], + displayedComponents: .hourAndMinute) + .labelsHidden() + + Button(action: { removeDailyTime(at: index) }) { + Image(systemName: "minus.circle.fill") + .foregroundStyle(.red) + } + .buttonStyle(.plain) + .disabled(config.dailyRefreshTimes.count <= 1) + } + } + + Button(action: addDailyTime) { + Label("Add time", systemImage: "plus.circle") + .font(.caption) + } + .buttonStyle(.plain) + } + .padding(.leading, kLabelWidth + 8) + } + + // MARK: - Advanced Section + + private var advancedSection: some View { + DisclosureGroup(isExpanded: $showAdvanced) { + VStack(alignment: .leading, spacing: 16) { + // Field Mappings + VStack(alignment: .leading, spacing: 8) { + Text("Field Mappings") + .font(.subheadline) + .fontWeight(.semibold) + .foregroundStyle(.secondary) + + labeledRow("Title field") { + TextField("", text: $config.fieldMappings.titleField) + .textFieldStyle(.roundedBorder) + } + labeledRow("Priority field") { + TextField("", text: $config.fieldMappings.priorityField) + .textFieldStyle(.roundedBorder) + } + labeledRow("Status field") { + TextField("", text: $config.fieldMappings.statusField) + .textFieldStyle(.roundedBorder) + } + labeledRow("Assignee field") { + TextField("", text: $config.fieldMappings.assigneeField) + .textFieldStyle(.roundedBorder) + } + labeledRow("Reporter field") { + TextField("", text: $config.fieldMappings.reporterField) + .textFieldStyle(.roundedBorder) + } + labeledRow("Customer field") { + TextField("", text: $config.fieldMappings.customerField) + .textFieldStyle(.roundedBorder) + } + labeledRow("Created At field") { + TextField("", text: $config.fieldMappings.createdAtField) + .textFieldStyle(.roundedBorder) + } + labeledRow("Updated At field") { + TextField("", text: $config.fieldMappings.updatedAtField) + .textFieldStyle(.roundedBorder) + } + } + + Divider() + + // Status Mappings + VStack(alignment: .leading, spacing: 8) { + Text("Status Mappings") + .font(.subheadline) + .fontWeight(.semibold) + .foregroundStyle(.secondary) + Text("Map your Feishu status values to standard Bugger statuses.") + .font(.caption) + .foregroundStyle(.secondary) + + ForEach(BugStatus.allCases.filter { $0 != .unknown }, id: \.self) { status in + statusMappingRow(for: status) + } + } + } + .padding(.top, 8) + } label: { + Label("Advanced: field & status mappings", systemImage: "slider.horizontal.3") + } + } + + // MARK: - Action Buttons + + private var actionButtons: some View { + HStack(spacing: 12) { + Button("Save") { + save() + } + .keyboardShortcut(.return) + + Button("Disconnect Feishu") { + TokenManager.shared.clearTokens() + PollerService.shared.stop() + BitableEventService.shared.disconnect() + CalibrationService.shared.stop() + } + .foregroundStyle(.red) + } + } + + // MARK: - Layout Helpers + + /// A consistently-aligned label–control row. + private func labeledRow( + _ label: String, + @ViewBuilder content: () -> Content + ) -> some View { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(label) + .frame(width: kLabelWidth, alignment: .trailing) + .foregroundStyle(.secondary) + content() + } + } + + /// A section with a left-aligned header and divider. + private func settingsSection( + _ title: String, + @ViewBuilder content: () -> Content + ) -> some View { + VStack(alignment: .leading, spacing: 8) { + Text(title) + .font(.headline) + .foregroundStyle(.primary) + Divider() + content() } - .frame(minWidth: 500, idealWidth: 520, minHeight: 400, maxHeight: 1200) } // MARK: - Status Mapping Row @@ -225,8 +336,8 @@ struct SettingsView: View { } ) - return VStack(alignment: .leading, spacing: 2) { - HStack { + return VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 6) { StatusPill(status: status) Text(status.label) .font(.caption)