143 lines
5.0 KiB
Swift
Executable File
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)
|
|
}
|
|
}
|