feat: add 8h/12h intervals + daily schedule refresh mode

- Added 8-hour and 12-hour polling interval options
- Added 'Daily schedule' mode (pollIntervalSeconds = -1): fires at
  specific wall-clock times configured in dailyRefreshTimes (e.g. 08:00, 14:00)
- PollerService: in daily mode, checks every 60s and triggers fetch when
  current time matches a scheduled time (with 2-min dedup window)
- Settings UI: shows HH:mm text field when daily schedule is selected
- AppConfig: added dailyRefreshTimes: [String] field

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tigerenwork 2026-06-29 21:08:51 +08:00
parent 64a35752c5
commit b81d3edf42
3 changed files with 70 additions and 3 deletions

View File

@ -6,6 +6,9 @@ struct AppConfig: Codable, Equatable {
var assigneeName: String = "" var assigneeName: String = ""
var fieldMappings: FieldMappings = FieldMappings() var fieldMappings: FieldMappings = FieldMappings()
var pollIntervalSeconds: Int = 300 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 showFloatingWidget: Bool = false
var launchAtLogin: Bool = true var launchAtLogin: Bool = true
var feishuBaseDomain: String = "xorbitlab.feishu.cn" var feishuBaseDomain: String = "xorbitlab.feishu.cn"

View File

@ -12,6 +12,7 @@ final class PollerService {
private var timer: Timer? private var timer: Timer?
private var isFetching = false private var isFetching = false
private var lastFetchTime: Date?
private(set) var isRunning = false private(set) var isRunning = false
@ -32,10 +33,18 @@ final class PollerService {
Task { await performFetch() } Task { await performFetch() }
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in if interval < 0 {
Task { await self?.performFetch() } // Daily schedule mode: check every 60s, fire at configured times
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
self?.checkDailySchedule()
}
timer?.tolerance = 5
} else {
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
Task { await self?.performFetch() }
}
timer?.tolerance = interval * 0.1
} }
timer?.tolerance = interval * 0.1
} }
func stop() { func stop() {
@ -53,11 +62,34 @@ final class PollerService {
await performFetch() await performFetch()
} }
// MARK: - Daily schedule
private func checkDailySchedule() {
guard let config = configService.config else { return }
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm"
let now = formatter.string(from: Date())
guard config.dailyRefreshTimes.contains(now) else { return }
// Avoid firing twice in the same minute
if let last = lastFetchTime, Date().timeIntervalSince(last) < 120 {
return
}
Task { await performFetch() }
}
// MARK: - Fetch
private func performFetch() async { private func performFetch() async {
guard !isFetching else { return } guard !isFetching else { return }
isFetching = true isFetching = true
defer { isFetching = false } defer { isFetching = false }
lastFetchTime = Date()
await MainActor.run { await MainActor.run {
bugStore.setLoading(true) bugStore.setLoading(true)
bugStore.setError(nil) bugStore.setError(nil)

View File

@ -51,7 +51,25 @@ struct SettingsView: View {
Text("1 hour").tag(3_600) Text("1 hour").tag(3_600)
Text("2 hours").tag(7_200) Text("2 hours").tag(7_200)
Text("4 hours").tag(14_400) Text("4 hours").tag(14_400)
Text("8 hours").tag(28_800)
Text("12 hours").tag(43_200)
Text("1 day").tag(86_400) Text("1 day").tag(86_400)
Divider()
Text("Daily schedule").tag(-1)
}
if config.pollIntervalSeconds == -1 {
VStack(alignment: .leading, spacing: 4) {
Text("Refresh at these times (comma-separated, 24h):")
.font(.caption)
.foregroundStyle(.secondary)
TextField("08:00, 14:00", text: dailyTimesBinding)
.font(.caption)
.textFieldStyle(.roundedBorder)
Text("Example: 08:00, 14:00, 20:00")
.font(.caption2)
.foregroundStyle(.secondary)
}
} }
} }
@ -147,6 +165,20 @@ struct SettingsView: View {
.padding(.vertical, 2) .padding(.vertical, 2)
} }
// MARK: - Daily times binding
private var dailyTimesBinding: Binding<String> {
Binding<String>(
get: { config.dailyRefreshTimes.joined(separator: ", ") },
set: { newValue in
config.dailyRefreshTimes = newValue
.components(separatedBy: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }
}
)
}
// MARK: - Actions // MARK: - Actions
private func save() { private func save() {