From 4d67d8bffd934a4e9fd0b6443aef5cfcd6029eb4 Mon Sep 17 00:00:00 2001 From: tigerenwork Date: Sun, 5 Jul 2026 22:57:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E6=97=B6=E6=B3=A8=E5=85=A5=E9=A3=9E=E4=B9=A6=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E8=AE=BE=E7=BD=AE=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Config.xcconfig.example | 5 ++ Resources/Info.plist | 4 ++ Sources/Models/AppConfig.swift | 28 ++++++++-- Sources/Views/Settings/SettingsView.swift | 68 ++++++++++++++++------- 4 files changed, 80 insertions(+), 25 deletions(-) diff --git a/Config.xcconfig.example b/Config.xcconfig.example index a181b9a..6e5362d 100644 --- a/Config.xcconfig.example +++ b/Config.xcconfig.example @@ -4,3 +4,8 @@ FEISHU_APP_ID = cli_your_app_id FEISHU_APP_SECRET = your_app_secret FEISHU_BASE_DOMAIN = xorbitlab.feishu.cn + +// Bitable app token & table ID (from the Bitable URL). Baked into the app at +// build time as defaults so internal users don't have to enter them in Settings. +FEISHU_APP_TOKEN = your_bitable_app_token +FEISHU_TABLE_ID = your_bitable_table_id diff --git a/Resources/Info.plist b/Resources/Info.plist index a574c36..b483d56 100644 --- a/Resources/Info.plist +++ b/Resources/Info.plist @@ -28,5 +28,9 @@ $(FEISHU_APP_SECRET) FEISHU_BASE_DOMAIN $(FEISHU_BASE_DOMAIN) + FEISHU_APP_TOKEN + $(FEISHU_APP_TOKEN) + FEISHU_TABLE_ID + $(FEISHU_TABLE_ID) diff --git a/Sources/Models/AppConfig.swift b/Sources/Models/AppConfig.swift index 0df6f29..6a6578b 100644 --- a/Sources/Models/AppConfig.swift +++ b/Sources/Models/AppConfig.swift @@ -1,8 +1,8 @@ import Foundation struct AppConfig: Codable, Equatable { - var appToken: String = "" - var tableId: String = "" + var appToken: String = BundledDefault.appToken + var tableId: String = BundledDefault.tableId var assigneeName: String = "" var fieldMappings: FieldMappings = FieldMappings() var pollIntervalSeconds: Int = 300 @@ -11,7 +11,7 @@ struct AppConfig: Codable, Equatable { var dailyRefreshTimes: [String] = [] var showFloatingWidget: Bool = false var launchAtLogin: Bool = true - var feishuBaseDomain: String = "xorbitlab.feishu.cn" + var feishuBaseDomain: String = BundledDefault.feishuBaseDomain /// When false (the default), Bugger waits for the next scheduled poll /// instead of fetching immediately on launch. Avoids a crash-on-start /// loop if the table fetch is failing. @@ -66,15 +66,31 @@ struct AppConfig: Codable, Equatable { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) - appToken = try c.decodeIfPresent(String.self, forKey: .appToken) ?? "" - tableId = try c.decodeIfPresent(String.self, forKey: .tableId) ?? "" + appToken = try c.decodeIfPresent(String.self, forKey: .appToken) ?? BundledDefault.appToken + tableId = try c.decodeIfPresent(String.self, forKey: .tableId) ?? BundledDefault.tableId assigneeName = try c.decodeIfPresent(String.self, forKey: .assigneeName) ?? "" fieldMappings = try c.decodeIfPresent(FieldMappings.self, forKey: .fieldMappings) ?? FieldMappings() pollIntervalSeconds = try c.decodeIfPresent(Int.self, forKey: .pollIntervalSeconds) ?? 300 dailyRefreshTimes = try c.decodeIfPresent([String].self, forKey: .dailyRefreshTimes) ?? [] showFloatingWidget = try c.decodeIfPresent(Bool.self, forKey: .showFloatingWidget) ?? false launchAtLogin = try c.decodeIfPresent(Bool.self, forKey: .launchAtLogin) ?? true - feishuBaseDomain = try c.decodeIfPresent(String.self, forKey: .feishuBaseDomain) ?? "xorbitlab.feishu.cn" + feishuBaseDomain = try c.decodeIfPresent(String.self, forKey: .feishuBaseDomain) ?? BundledDefault.feishuBaseDomain refreshOnStart = try c.decodeIfPresent(Bool.self, forKey: .refreshOnStart) ?? false } } + +/// Values baked into the app at build time via Config.xcconfig → Info.plist. +/// Used as defaults so internal users don't have to enter them in Settings. +enum BundledDefault { + static var appToken: String { string(for: "FEISHU_APP_TOKEN") } + static var tableId: String { string(for: "FEISHU_TABLE_ID") } + static var feishuBaseDomain: String { string(for: "FEISHU_BASE_DOMAIN") } + + private static func string(for key: String) -> String { + let raw = (Bundle.main.object(forInfoDictionaryKey: key) as? String) ?? "" + // Xcode leaves an undefined $(...) substitution as the literal string; + // treat that as "not set" rather than showing "$(FEISHU_APP_TOKEN)". + if raw.hasPrefix("$(") { return "" } + return raw.trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/Sources/Views/Settings/SettingsView.swift b/Sources/Views/Settings/SettingsView.swift index d64f75d..8bb4a24 100644 --- a/Sources/Views/Settings/SettingsView.swift +++ b/Sources/Views/Settings/SettingsView.swift @@ -13,11 +13,29 @@ struct SettingsView: View { var body: some View { ScrollView { - Form { + 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)) + + Form { Section("Feishu Bitable") { TextField("App Token (from Bitable URL)", text: $config.appToken) TextField("Table ID", text: $config.tableId) - TextField("Your Name (as in Assignee column, optional)", text: $config.assigneeName) TextField("Feishu domain", text: $config.feishuBaseDomain) VStack(alignment: .leading, spacing: 4) { @@ -107,24 +125,35 @@ struct SettingsView: View { Toggle("Launch at login", isOn: $config.launchAtLogin) } - Section("Field Mappings") { - 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) - } + Section { + DisclosureGroup(isExpanded: $showAdvanced) { + Text("Field Mappings") + .font(.caption) + .fontWeight(.semibold) + .foregroundStyle(.secondary) - Section("Status Mappings") { - Text("Map your Feishu status values to standard Bugger statuses.") - .font(.caption) - .foregroundStyle(.secondary) + 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) - ForEach(BugStatus.allCases.filter { $0 != .unknown }, id: \.self) { status in - statusMappingRow(for: status) + 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") } } @@ -140,7 +169,8 @@ struct SettingsView: View { } .foregroundStyle(.red) } - } + } // Form + } // VStack .padding() } .frame(minWidth: 500, idealWidth: 520, minHeight: 400, maxHeight: 800)