Files

143 lines
5.0 KiB
Swift
Executable File

import Foundation
enum SyncScheduleKind: String, CaseIterable, Identifiable, Codable {
case manual
case once
case hourly
case daily
case weekly
case monthly
case custom
var id: String { rawValue }
var title: String {
switch self {
case .manual: return "Manual only"
case .once: return "Once"
case .hourly: return "Hourly"
case .daily: return "Daily"
case .weekly: return "Weekly"
case .monthly: return "Monthly"
case .custom: return "Custom interval"
}
}
var systemImage: String {
switch self {
case .manual: return "hand.tap"
case .once: return "calendar.badge.clock"
case .hourly: return "clock.arrow.circlepath"
case .daily: return "sun.max"
case .weekly: return "calendar"
case .monthly: return "calendar.circle"
case .custom: return "timer"
}
}
}
enum SyncRunTrigger: String, Codable {
case manual
case scheduled
case watched
case syncAll
var title: String {
switch self {
case .manual: return "Manual"
case .scheduled: return "Scheduled"
case .watched: return "File change"
case .syncAll: return "Sync All"
}
}
var systemImage: String {
switch self {
case .manual: return "play.fill"
case .scheduled: return "calendar.badge.clock"
case .watched: return "eye.fill"
case .syncAll: return "arrow.triangle.2.circlepath"
}
}
}
extension SyncJob {
var scheduleKind: SyncScheduleKind {
get { SyncScheduleKind(rawValue: scheduleKindRaw) ?? .manual }
set { scheduleKindRaw = newValue.rawValue }
}
var effectiveWatchPath: String {
let trimmed = watchPath.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? sourcePath : trimmed
}
var scheduleDescription: String {
guard scheduleEnabled, scheduleKind != .manual else { return "Manual" }
let time = scheduleStartAt.formatted(date: .omitted, time: .shortened)
switch scheduleKind {
case .manual: return "Manual"
case .once: return "Once · \(scheduleStartAt.formatted(date: .abbreviated, time: .shortened))"
case .hourly: return "Every hour"
case .daily: return "Daily · \(time)"
case .weekly:
return "Weekly · \(scheduleStartAt.formatted(.dateTime.weekday(.abbreviated))) · \(time)"
case .monthly:
return "Monthly · day \(Calendar.current.component(.day, from: scheduleStartAt)) · \(time)"
case .custom:
return "Every \(max(1, scheduleIntervalMinutes)) min"
}
}
func calculateNextRun(after date: Date = Date()) -> Date? {
guard scheduleEnabled, scheduleKind != .manual else { return nil }
let calendar = Calendar.current
switch scheduleKind {
case .manual:
return nil
case .once:
return scheduleStartAt > date ? scheduleStartAt : nil
case .hourly:
return nextInterval(after: date, seconds: 3600)
case .custom:
return nextInterval(after: date, seconds: TimeInterval(max(1, scheduleIntervalMinutes) * 60))
case .daily:
let time = calendar.dateComponents([.hour, .minute, .second], from: scheduleStartAt)
return calendar.nextDate(after: date, matching: time, matchingPolicy: .nextTime)
case .weekly:
var components = calendar.dateComponents([.hour, .minute, .second], from: scheduleStartAt)
components.weekday = calendar.component(.weekday, from: scheduleStartAt)
return calendar.nextDate(after: date, matching: components, matchingPolicy: .nextTime)
case .monthly:
var components = calendar.dateComponents([.hour, .minute, .second], from: scheduleStartAt)
components.day = calendar.component(.day, from: scheduleStartAt)
return calendar.nextDate(after: date, matching: components, matchingPolicy: .nextTime)
}
}
func occurrences(from start: Date, through end: Date, limit: Int = 400) -> [Date] {
guard scheduleEnabled, scheduleKind != .manual, start < end else { return [] }
var output: [Date] = []
var cursor = start.addingTimeInterval(-1)
if scheduleKind == .once {
return (scheduleStartAt >= start && scheduleStartAt < end) ? [scheduleStartAt] : []
}
while output.count < limit, let next = calculateNextRun(after: cursor), next < end {
if next >= start { output.append(next) }
cursor = next.addingTimeInterval(0.5)
}
return output
}
private func nextInterval(after date: Date, seconds: TimeInterval) -> Date? {
guard seconds > 0 else { return nil }
if scheduleStartAt > date { return scheduleStartAt }
let elapsed = date.timeIntervalSince(scheduleStartAt)
let steps = floor(elapsed / seconds) + 1
return scheduleStartAt.addingTimeInterval(steps * seconds)
}
}