|
|
|
@ -29,9 +29,49 @@ enum CountdownState { |
|
|
|
case cancelled |
|
|
|
case cancelled |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
struct CountdownSpan { |
|
|
|
struct CountdownSequence: Codable { |
|
|
|
|
|
|
|
var spans: [CountdownSpan] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var currentSpan: CountdownSpan { |
|
|
|
|
|
|
|
let now = Date() |
|
|
|
|
|
|
|
let current: CountdownSpan? = self.spans.first { span in |
|
|
|
|
|
|
|
return span.interval.start < now && span.interval.end > now |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return current ?? self.spans.last ?? CountdownSpan(interval: DateInterval(start: Date(), end: Date()), name: "none") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var currentEnd: Date { |
|
|
|
|
|
|
|
return self.currentSpan.interval.end |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var dateInterval: DateInterval { |
|
|
|
|
|
|
|
let firstSpan = self.spans.first ?? CountdownSequence.defaultSpan |
|
|
|
|
|
|
|
let lastSpan = self.spans.last ?? CountdownSequence.defaultSpan |
|
|
|
|
|
|
|
return DateInterval(start: firstSpan.start, end: lastSpan.end) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var end: Date { |
|
|
|
|
|
|
|
if let lastSpan = self.spans.last { |
|
|
|
|
|
|
|
return lastSpan.end |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
fatalError("no spans") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static let defaultSpan = CountdownSpan(interval: DateInterval(start: Date(), end: Date()), name: "none") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct CountdownSpan: Codable { |
|
|
|
var interval: DateInterval |
|
|
|
var interval: DateInterval |
|
|
|
var name: String? |
|
|
|
var name: String? |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var start: Date { |
|
|
|
|
|
|
|
return self.interval.start |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var end: Date { |
|
|
|
|
|
|
|
return self.interval.end |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class Conductor: ObservableObject { |
|
|
|
class Conductor: ObservableObject { |
|
|
|
@ -44,7 +84,7 @@ class Conductor: ObservableObject { |
|
|
|
|
|
|
|
|
|
|
|
fileprivate var beats: Timer? = nil |
|
|
|
fileprivate var beats: Timer? = nil |
|
|
|
|
|
|
|
|
|
|
|
@UserDefault(PreferenceKey.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : DateInterval] |
|
|
|
@UserDefault(PreferenceKey.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : CountdownSequence] |
|
|
|
@UserDefault(PreferenceKey.pausedCountdowns.rawValue, defaultValue: [:]) static var savedPausedCountdowns: [String : TimeInterval] |
|
|
|
@UserDefault(PreferenceKey.pausedCountdowns.rawValue, defaultValue: [:]) static var savedPausedCountdowns: [String : TimeInterval] |
|
|
|
@UserDefault(PreferenceKey.stopwatches.rawValue, defaultValue: [:]) static var savedStopwatches: [String : LiveStopWatch] |
|
|
|
@UserDefault(PreferenceKey.stopwatches.rawValue, defaultValue: [:]) static var savedStopwatches: [String : LiveStopWatch] |
|
|
|
|
|
|
|
|
|
|
|
@ -57,14 +97,15 @@ class Conductor: ObservableObject { |
|
|
|
self.currentStopwatches = Conductor.savedStopwatches |
|
|
|
self.currentStopwatches = Conductor.savedStopwatches |
|
|
|
self.pausedCountdowns = Conductor.savedPausedCountdowns |
|
|
|
self.pausedCountdowns = Conductor.savedPausedCountdowns |
|
|
|
|
|
|
|
|
|
|
|
self.beats = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { _ in |
|
|
|
self.beats = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in |
|
|
|
self._cleanupCountdowns() |
|
|
|
self._cleanupCountdowns() |
|
|
|
|
|
|
|
self._buildLiveTimers() |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Published var cancelledCountdowns: [String] = [] |
|
|
|
@Published var cancelledCountdowns: [String] = [] |
|
|
|
|
|
|
|
|
|
|
|
@Published var currentCountdowns: [String : DateInterval] = [:] { |
|
|
|
@Published var currentCountdowns: [String : CountdownSequence] = [:] { |
|
|
|
didSet { |
|
|
|
didSet { |
|
|
|
Conductor.savedCountdowns = currentCountdowns |
|
|
|
Conductor.savedCountdowns = currentCountdowns |
|
|
|
// Logger.log("**** currentCountdowns didSet, count = \(currentCountdowns.count)") |
|
|
|
// Logger.log("**** currentCountdowns didSet, count = \(currentCountdowns.count)") |
|
|
|
@ -96,10 +137,10 @@ class Conductor: ObservableObject { |
|
|
|
|
|
|
|
|
|
|
|
fileprivate func _buildLiveTimers() { |
|
|
|
fileprivate func _buildLiveTimers() { |
|
|
|
|
|
|
|
|
|
|
|
let liveCountdowns: [LiveTimer] = self.currentCountdowns.map { |
|
|
|
let liveCountdowns: [LiveTimer] = self.currentCountdowns.map { id, sequence in |
|
|
|
return LiveTimer(id: $0, date: $1.end) |
|
|
|
let currentSpan = sequence.currentSpan |
|
|
|
|
|
|
|
return LiveTimer(id: id, name: currentSpan.name, date: currentSpan.end) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// add countdown if not present |
|
|
|
// add countdown if not present |
|
|
|
for liveCountdown in liveCountdowns { |
|
|
|
for liveCountdown in liveCountdowns { |
|
|
|
if let index = self.liveTimers.firstIndex(where: { $0.id == liveCountdown.id }) { |
|
|
|
if let index = self.liveTimers.firstIndex(where: { $0.id == liveCountdown.id }) { |
|
|
|
@ -147,9 +188,9 @@ class Conductor: ObservableObject { |
|
|
|
fileprivate func _recordActivity(countdownId: String, cancelled: Bool) { |
|
|
|
fileprivate func _recordActivity(countdownId: String, cancelled: Bool) { |
|
|
|
let context = PersistenceController.shared.container.viewContext |
|
|
|
let context = PersistenceController.shared.container.viewContext |
|
|
|
if let countdown: Countdown = context.object(stringId: countdownId), |
|
|
|
if let countdown: Countdown = context.object(stringId: countdownId), |
|
|
|
let dateInterval = self.currentCountdowns[countdownId] { |
|
|
|
let sequence: CountdownSequence = self.currentCountdowns[countdownId] { |
|
|
|
do { |
|
|
|
do { |
|
|
|
try CoreDataRequests.recordActivity(timer: countdown, dateInterval: dateInterval, cancelled: cancelled) |
|
|
|
try CoreDataRequests.recordActivity(timer: countdown, dateInterval: sequence.dateInterval, cancelled: cancelled) |
|
|
|
} catch { |
|
|
|
} catch { |
|
|
|
Logger.error(error) |
|
|
|
Logger.error(error) |
|
|
|
// TODO: show error to user |
|
|
|
// TODO: show error to user |
|
|
|
@ -169,20 +210,29 @@ class Conductor: ObservableObject { |
|
|
|
do { |
|
|
|
do { |
|
|
|
|
|
|
|
|
|
|
|
var totalDuration = 0.0 |
|
|
|
var totalDuration = 0.0 |
|
|
|
|
|
|
|
var spans: [CountdownSpan] = [] |
|
|
|
|
|
|
|
let now = Date() |
|
|
|
|
|
|
|
|
|
|
|
for _ in 0...countdown.repeatCount { |
|
|
|
for _ in 0..<countdown.repeatCount { |
|
|
|
for range in countdown.sortedRanges() { |
|
|
|
for range in countdown.sortedRanges() { |
|
|
|
// TODO: est-ce qu'on schedule tout ou en séquence ? |
|
|
|
|
|
|
|
|
|
|
|
let start = now.addingTimeInterval(totalDuration) |
|
|
|
|
|
|
|
|
|
|
|
totalDuration += range.duration |
|
|
|
totalDuration += range.duration |
|
|
|
let end = try self._scheduleSoundPlayer(countdown: countdown, range: range, in: totalDuration) |
|
|
|
let end = try self._scheduleSoundPlayer(countdown: countdown, range: range, in: totalDuration) |
|
|
|
self._scheduleCountdownNotification(countdown: countdown, in: totalDuration, handler: handler) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let dateInterval = DateInterval(start: Date(), end: end) |
|
|
|
let dateInterval = DateInterval(start: start, end: end) |
|
|
|
self.currentCountdowns[countdownId] = dateInterval |
|
|
|
let span = CountdownSpan(interval: dateInterval, name: range.name) |
|
|
|
|
|
|
|
spans.append(span) |
|
|
|
self._launchLiveActivity(timer: countdown, date: end) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
self._scheduleCountdownNotification(countdown: countdown, in: totalDuration, handler: handler) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let sequence = CountdownSequence(spans: spans) |
|
|
|
|
|
|
|
self.currentCountdowns[countdownId] = sequence |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: live activity |
|
|
|
|
|
|
|
self._launchLiveActivity(timer: countdown, date: sequence.end) |
|
|
|
|
|
|
|
|
|
|
|
if Preferences.playConfirmationSound { |
|
|
|
if Preferences.playConfirmationSound { |
|
|
|
self._playConfirmationSound(timer: countdown) |
|
|
|
self._playConfirmationSound(timer: countdown) |
|
|
|
@ -205,8 +255,10 @@ class Conductor: ObservableObject { |
|
|
|
FileLogger.log("schedule countdown \(range.name ?? "''") at \(end)") |
|
|
|
FileLogger.log("schedule countdown \(range.name ?? "''") at \(end)") |
|
|
|
|
|
|
|
|
|
|
|
let sound = range.someSound ?? countdown.someSound ?? Sound.default |
|
|
|
let sound = range.someSound ?? countdown.someSound ?? Sound.default |
|
|
|
let soundPlayer = try DelaySoundPlayer(timerID: countdownId, sound: sound) |
|
|
|
|
|
|
|
self._delayedSoundPlayers[countdownId] = soundPlayer |
|
|
|
let playerId = range.stringId + interval.debugDescription |
|
|
|
|
|
|
|
let soundPlayer = try DelaySoundPlayer(sound: sound) |
|
|
|
|
|
|
|
self._delayedSoundPlayers[playerId] = soundPlayer |
|
|
|
|
|
|
|
|
|
|
|
try soundPlayer.start(in: interval, |
|
|
|
try soundPlayer.start(in: interval, |
|
|
|
repeatCount: Int(countdown.repeatCount)) |
|
|
|
repeatCount: Int(countdown.repeatCount)) |
|
|
|
@ -247,7 +299,7 @@ class Conductor: ObservableObject { |
|
|
|
return .cancelled |
|
|
|
return .cancelled |
|
|
|
} else if self.pausedCountdowns[id] != nil { |
|
|
|
} else if self.pausedCountdowns[id] != nil { |
|
|
|
return .paused |
|
|
|
return .paused |
|
|
|
} else if let interval = self.currentCountdowns[id], interval.end > Date() { |
|
|
|
} else if let end = self.currentCountdowns[id]?.end, end > Date() { |
|
|
|
return .inprogress |
|
|
|
return .inprogress |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
return .finished |
|
|
|
return .finished |
|
|
|
@ -263,11 +315,11 @@ class Conductor: ObservableObject { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func pauseCountdown(id: TimerID) { |
|
|
|
func pauseCountdown(id: TimerID) { |
|
|
|
guard let interval = self.currentCountdowns[id] else { |
|
|
|
guard let sequence = self.currentCountdowns[id] else { |
|
|
|
return |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let remainingTime = interval.end.timeIntervalSince(Date()) |
|
|
|
let remainingTime = sequence.currentSpan.end.timeIntervalSince(Date()) |
|
|
|
self.pausedCountdowns[id] = remainingTime |
|
|
|
self.pausedCountdowns[id] = remainingTime |
|
|
|
|
|
|
|
|
|
|
|
// cancel stuff |
|
|
|
// cancel stuff |
|
|
|
@ -280,7 +332,9 @@ class Conductor: ObservableObject { |
|
|
|
let context = PersistenceController.shared.container.viewContext |
|
|
|
let context = PersistenceController.shared.container.viewContext |
|
|
|
if let countdown: Countdown = context.object(stringId: id), |
|
|
|
if let countdown: Countdown = context.object(stringId: id), |
|
|
|
let remainingTime = self.pausedCountdowns[id] { |
|
|
|
let remainingTime = self.pausedCountdowns[id] { |
|
|
|
_ = try self._scheduleSoundPlayer(countdown: countdown, in: remainingTime) |
|
|
|
|
|
|
|
|
|
|
|
// TODO: RESUME |
|
|
|
|
|
|
|
// _ = try self._scheduleSoundPlayer(countdown: countdown, in: remainingTime) |
|
|
|
self.pausedCountdowns.removeValue(forKey: id) |
|
|
|
self.pausedCountdowns.removeValue(forKey: id) |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
throw AppError.timerNotFound(id: id) |
|
|
|
throw AppError.timerNotFound(id: id) |
|
|
|
@ -338,7 +392,7 @@ class Conductor: ObservableObject { |
|
|
|
|
|
|
|
|
|
|
|
func restoreSoundPlayers() { |
|
|
|
func restoreSoundPlayers() { |
|
|
|
|
|
|
|
|
|
|
|
for (countdownId, interval) in self.currentCountdowns { |
|
|
|
for (countdownId, span) in self.currentCountdowns { |
|
|
|
if !self._delayedSoundPlayers.contains(where: { $0.key == countdownId }) { |
|
|
|
if !self._delayedSoundPlayers.contains(where: { $0.key == countdownId }) { |
|
|
|
|
|
|
|
|
|
|
|
let context = PersistenceController.shared.container.viewContext |
|
|
|
let context = PersistenceController.shared.container.viewContext |
|
|
|
@ -346,9 +400,11 @@ class Conductor: ObservableObject { |
|
|
|
|
|
|
|
|
|
|
|
do { |
|
|
|
do { |
|
|
|
let sound: Sound = countdown.someSound ?? Sound.default |
|
|
|
let sound: Sound = countdown.someSound ?? Sound.default |
|
|
|
let soundPlayer = try DelaySoundPlayer(timerID: countdownId, sound: sound) |
|
|
|
let soundPlayer = try DelaySoundPlayer(sound: sound) |
|
|
|
self._delayedSoundPlayers[countdownId] = soundPlayer |
|
|
|
self._delayedSoundPlayers[countdownId] = soundPlayer |
|
|
|
try soundPlayer.restore(for: interval.end, repeatCount: Int(countdown.repeatCount)) |
|
|
|
|
|
|
|
|
|
|
|
// TODO: RESTORE |
|
|
|
|
|
|
|
// try soundPlayer.restore(for: span.interval.end, repeatCount: Int(countdown.repeatCount)) |
|
|
|
FileLogger.log("Restored sound player for \(self._timerName(countdownId))") |
|
|
|
FileLogger.log("Restored sound player for \(self._timerName(countdownId))") |
|
|
|
} catch { |
|
|
|
} catch { |
|
|
|
Logger.error(error) |
|
|
|
Logger.error(error) |
|
|
|
|