From b81d3edf4283f645b15d9eb3f1e089daad6c49fb Mon Sep 17 00:00:00 2001 From: tigerenwork Date: Mon, 29 Jun 2026 21:08:51 +0800 Subject: [PATCH] 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 --- Sources/Models/AppConfig.swift | 3 ++ Sources/Services/PollerService.swift | 38 +++++++++++++++++++++-- Sources/Views/Settings/SettingsView.swift | 32 +++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/Sources/Models/AppConfig.swift b/Sources/Models/AppConfig.swift index 31943bb..1ccd90f 100644 --- a/Sources/Models/AppConfig.swift +++ b/Sources/Models/AppConfig.swift @@ -6,6 +6,9 @@ struct AppConfig: Codable, Equatable { 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 = "xorbitlab.feishu.cn" diff --git a/Sources/Services/PollerService.swift b/Sources/Services/PollerService.swift index d6839eb..3fb0a23 100644 --- a/Sources/Services/PollerService.swift +++ b/Sources/Services/PollerService.swift @@ -12,6 +12,7 @@ final class PollerService { private var timer: Timer? private var isFetching = false + private var lastFetchTime: Date? private(set) var isRunning = false @@ -32,10 +33,18 @@ final class PollerService { Task { await performFetch() } - timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in - Task { await self?.performFetch() } + if interval < 0 { + // 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() { @@ -53,11 +62,34 @@ final class PollerService { 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 { guard !isFetching else { return } isFetching = true defer { isFetching = false } + lastFetchTime = Date() + await MainActor.run { bugStore.setLoading(true) bugStore.setError(nil) diff --git a/Sources/Views/Settings/SettingsView.swift b/Sources/Views/Settings/SettingsView.swift index b279a1e..e403bc0 100644 --- a/Sources/Views/Settings/SettingsView.swift +++ b/Sources/Views/Settings/SettingsView.swift @@ -51,7 +51,25 @@ struct SettingsView: View { Text("1 hour").tag(3_600) Text("2 hours").tag(7_200) Text("4 hours").tag(14_400) + Text("8 hours").tag(28_800) + Text("12 hours").tag(43_200) 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) } + // MARK: - Daily times binding + + private var dailyTimesBinding: Binding { + Binding( + get: { config.dailyRefreshTimes.joined(separator: ", ") }, + set: { newValue in + config.dailyRefreshTimes = newValue + .components(separatedBy: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + } + ) + } + // MARK: - Actions private func save() {