111 lines
5.3 KiB
Swift
111 lines
5.3 KiB
Swift
import Foundation
|
|
|
|
struct AppConfig: Codable, Equatable {
|
|
var appToken: String = BundledDefault.appToken
|
|
var tableId: String = BundledDefault.tableId
|
|
var assigneeName: String = ""
|
|
var fieldMappings: FieldMappings = FieldMappings()
|
|
var pollIntervalSeconds: Int = 300
|
|
/// Daily refresh times in "HH:mm" format, e.g. ["08:00", "14:00"].
|
|
/// Only used when pollIntervalSeconds == -1.
|
|
var dailyRefreshTimes: [String] = []
|
|
var showFloatingWidget: Bool = false
|
|
var launchAtLogin: Bool = true
|
|
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.
|
|
var refreshOnStart: Bool = false
|
|
/// Base URL of the feishu-app change-notification service, e.g.
|
|
/// "http://localhost:8000". When set, Bugger opens an SSE stream to
|
|
/// `{feishuAppBaseURL}/api/v1/bitable/events` and refreshes immediately
|
|
/// on a change push. Empty = disabled (polling only).
|
|
var feishuAppBaseURL: String = ""
|
|
/// When true, Bugger periodically queries bugger-feishu for the list of
|
|
/// record IDs assigned to the current user and triggers a fetchNow() if
|
|
/// the list doesn't match the bugs in BugStore. Disabled by default —
|
|
/// only useful when feishuAppBaseURL is set and push reliability is a
|
|
/// concern.
|
|
var calibrationEnabled: Bool = false
|
|
|
|
struct FieldMappings: Codable, Equatable {
|
|
var titleField: String = "Title"
|
|
var priorityField: String = "Priority"
|
|
var statusField: String = "Status"
|
|
var assigneeField: String = "Assignee"
|
|
var reporterField: String = "Reporter"
|
|
var customerField: String = "CustomerName"
|
|
var createdAtField: String = "Created At"
|
|
var updatedAtField: String = "Updated At"
|
|
|
|
/// Map Feishu display values to standard BugStatus cases.
|
|
/// Keys are the Feishu select-option text; values are the standard status.
|
|
/// Unknown values default to .unknown (shown as active).
|
|
var statusMappings: [String: BugStatus] = [
|
|
"Bug Triage": .open,
|
|
"Open": .open,
|
|
"New": .open,
|
|
"待处理": .open,
|
|
"开发进行中": .inProgress,
|
|
"In Progress": .inProgress,
|
|
"验收中": .inReview,
|
|
"In Review": .inReview,
|
|
"验收完毕": .resolved,
|
|
"Resolved": .resolved,
|
|
"Fixed": .resolved,
|
|
"取消": .closed,
|
|
"Closed": .closed,
|
|
"Won't fix": .closed,
|
|
"转需求": .closed,
|
|
"验收不通过": .closed,
|
|
]
|
|
}
|
|
|
|
var isConfigured: Bool {
|
|
!appToken.isEmpty && !tableId.isEmpty
|
|
}
|
|
|
|
// Explicit keys + tolerant decoding so a config persisted before a new
|
|
// field was added (missing key) doesn't fail to decode and wipe settings.
|
|
enum CodingKeys: String, CodingKey {
|
|
case appToken, tableId, assigneeName, fieldMappings
|
|
case pollIntervalSeconds, dailyRefreshTimes
|
|
case showFloatingWidget, launchAtLogin, feishuBaseDomain, refreshOnStart
|
|
case feishuAppBaseURL, calibrationEnabled
|
|
}
|
|
|
|
init() {}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
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) ?? BundledDefault.feishuBaseDomain
|
|
refreshOnStart = try c.decodeIfPresent(Bool.self, forKey: .refreshOnStart) ?? false
|
|
feishuAppBaseURL = try c.decodeIfPresent(String.self, forKey: .feishuAppBaseURL) ?? ""
|
|
calibrationEnabled = try c.decodeIfPresent(Bool.self, forKey: .calibrationEnabled) ?? 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)
|
|
}
|
|
}
|