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)