feat: 支持构建时注入飞书配置并优化设置页面体验
This commit is contained in:
parent
5e4587909d
commit
4d67d8bffd
|
|
@ -4,3 +4,8 @@
|
||||||
FEISHU_APP_ID = cli_your_app_id
|
FEISHU_APP_ID = cli_your_app_id
|
||||||
FEISHU_APP_SECRET = your_app_secret
|
FEISHU_APP_SECRET = your_app_secret
|
||||||
FEISHU_BASE_DOMAIN = xorbitlab.feishu.cn
|
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
|
||||||
|
|
|
||||||
|
|
@ -28,5 +28,9 @@
|
||||||
<string>$(FEISHU_APP_SECRET)</string>
|
<string>$(FEISHU_APP_SECRET)</string>
|
||||||
<key>FEISHU_BASE_DOMAIN</key>
|
<key>FEISHU_BASE_DOMAIN</key>
|
||||||
<string>$(FEISHU_BASE_DOMAIN)</string>
|
<string>$(FEISHU_BASE_DOMAIN)</string>
|
||||||
|
<key>FEISHU_APP_TOKEN</key>
|
||||||
|
<string>$(FEISHU_APP_TOKEN)</string>
|
||||||
|
<key>FEISHU_TABLE_ID</key>
|
||||||
|
<string>$(FEISHU_TABLE_ID)</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct AppConfig: Codable, Equatable {
|
struct AppConfig: Codable, Equatable {
|
||||||
var appToken: String = ""
|
var appToken: String = BundledDefault.appToken
|
||||||
var tableId: String = ""
|
var tableId: String = BundledDefault.tableId
|
||||||
var assigneeName: String = ""
|
var assigneeName: String = ""
|
||||||
var fieldMappings: FieldMappings = FieldMappings()
|
var fieldMappings: FieldMappings = FieldMappings()
|
||||||
var pollIntervalSeconds: Int = 300
|
var pollIntervalSeconds: Int = 300
|
||||||
|
|
@ -11,7 +11,7 @@ struct AppConfig: Codable, Equatable {
|
||||||
var dailyRefreshTimes: [String] = []
|
var dailyRefreshTimes: [String] = []
|
||||||
var showFloatingWidget: Bool = false
|
var showFloatingWidget: Bool = false
|
||||||
var launchAtLogin: Bool = true
|
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
|
/// When false (the default), Bugger waits for the next scheduled poll
|
||||||
/// instead of fetching immediately on launch. Avoids a crash-on-start
|
/// instead of fetching immediately on launch. Avoids a crash-on-start
|
||||||
/// loop if the table fetch is failing.
|
/// loop if the table fetch is failing.
|
||||||
|
|
@ -66,15 +66,31 @@ struct AppConfig: Codable, Equatable {
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
appToken = try c.decodeIfPresent(String.self, forKey: .appToken) ?? ""
|
appToken = try c.decodeIfPresent(String.self, forKey: .appToken) ?? BundledDefault.appToken
|
||||||
tableId = try c.decodeIfPresent(String.self, forKey: .tableId) ?? ""
|
tableId = try c.decodeIfPresent(String.self, forKey: .tableId) ?? BundledDefault.tableId
|
||||||
assigneeName = try c.decodeIfPresent(String.self, forKey: .assigneeName) ?? ""
|
assigneeName = try c.decodeIfPresent(String.self, forKey: .assigneeName) ?? ""
|
||||||
fieldMappings = try c.decodeIfPresent(FieldMappings.self, forKey: .fieldMappings) ?? FieldMappings()
|
fieldMappings = try c.decodeIfPresent(FieldMappings.self, forKey: .fieldMappings) ?? FieldMappings()
|
||||||
pollIntervalSeconds = try c.decodeIfPresent(Int.self, forKey: .pollIntervalSeconds) ?? 300
|
pollIntervalSeconds = try c.decodeIfPresent(Int.self, forKey: .pollIntervalSeconds) ?? 300
|
||||||
dailyRefreshTimes = try c.decodeIfPresent([String].self, forKey: .dailyRefreshTimes) ?? []
|
dailyRefreshTimes = try c.decodeIfPresent([String].self, forKey: .dailyRefreshTimes) ?? []
|
||||||
showFloatingWidget = try c.decodeIfPresent(Bool.self, forKey: .showFloatingWidget) ?? false
|
showFloatingWidget = try c.decodeIfPresent(Bool.self, forKey: .showFloatingWidget) ?? false
|
||||||
launchAtLogin = try c.decodeIfPresent(Bool.self, forKey: .launchAtLogin) ?? true
|
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
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,29 @@ struct SettingsView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
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))
|
||||||
|
|
||||||
Form {
|
Form {
|
||||||
Section("Feishu Bitable") {
|
Section("Feishu Bitable") {
|
||||||
TextField("App Token (from Bitable URL)", text: $config.appToken)
|
TextField("App Token (from Bitable URL)", text: $config.appToken)
|
||||||
TextField("Table ID", text: $config.tableId)
|
TextField("Table ID", text: $config.tableId)
|
||||||
TextField("Your Name (as in Assignee column, optional)", text: $config.assigneeName)
|
|
||||||
TextField("Feishu domain", text: $config.feishuBaseDomain)
|
TextField("Feishu domain", text: $config.feishuBaseDomain)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
|
@ -107,7 +125,13 @@ struct SettingsView: View {
|
||||||
Toggle("Launch at login", isOn: $config.launchAtLogin)
|
Toggle("Launch at login", isOn: $config.launchAtLogin)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("Field Mappings") {
|
Section {
|
||||||
|
DisclosureGroup(isExpanded: $showAdvanced) {
|
||||||
|
Text("Field Mappings")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
TextField("Title field", text: $config.fieldMappings.titleField)
|
TextField("Title field", text: $config.fieldMappings.titleField)
|
||||||
TextField("Priority field", text: $config.fieldMappings.priorityField)
|
TextField("Priority field", text: $config.fieldMappings.priorityField)
|
||||||
TextField("Status field", text: $config.fieldMappings.statusField)
|
TextField("Status field", text: $config.fieldMappings.statusField)
|
||||||
|
|
@ -116,9 +140,11 @@ struct SettingsView: View {
|
||||||
TextField("Customer field", text: $config.fieldMappings.customerField)
|
TextField("Customer field", text: $config.fieldMappings.customerField)
|
||||||
TextField("Created At field", text: $config.fieldMappings.createdAtField)
|
TextField("Created At field", text: $config.fieldMappings.createdAtField)
|
||||||
TextField("Updated At field", text: $config.fieldMappings.updatedAtField)
|
TextField("Updated At field", text: $config.fieldMappings.updatedAtField)
|
||||||
}
|
|
||||||
|
|
||||||
Section("Status Mappings") {
|
Text("Status Mappings")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
Text("Map your Feishu status values to standard Bugger statuses.")
|
Text("Map your Feishu status values to standard Bugger statuses.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
@ -126,6 +152,9 @@ struct SettingsView: View {
|
||||||
ForEach(BugStatus.allCases.filter { $0 != .unknown }, id: \.self) { status in
|
ForEach(BugStatus.allCases.filter { $0 != .unknown }, id: \.self) { status in
|
||||||
statusMappingRow(for: status)
|
statusMappingRow(for: status)
|
||||||
}
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Advanced: field & status mappings", systemImage: "slider.horizontal.3")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
|
|
@ -140,7 +169,8 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
.foregroundStyle(.red)
|
.foregroundStyle(.red)
|
||||||
}
|
}
|
||||||
}
|
} // Form
|
||||||
|
} // VStack
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
.frame(minWidth: 500, idealWidth: 520, minHeight: 400, maxHeight: 800)
|
.frame(minWidth: 500, idealWidth: 520, minHeight: 400, maxHeight: 800)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue