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:
parent
64a35752c5
commit
b81d3edf42
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue