bugger/Sources/Models/AppConfig.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)
}
}