|
|
|
|
@ -22,6 +22,13 @@ fileprivate enum Const: String { |
|
|
|
|
case cancellationSound = "MRKRSTPHR_synth_one_shot_bleep_G.wav" |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
enum CountdownState { |
|
|
|
|
case inprogress |
|
|
|
|
case paused |
|
|
|
|
case finished |
|
|
|
|
case cancelled |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class Conductor: ObservableObject { |
|
|
|
|
|
|
|
|
|
static let maestro: Conductor = Conductor() |
|
|
|
|
@ -30,7 +37,10 @@ class Conductor: ObservableObject { |
|
|
|
|
|
|
|
|
|
fileprivate var _delayedSoundPlayers: [TimerID : DelaySoundPlayer] = [:] |
|
|
|
|
|
|
|
|
|
fileprivate var beats: Timer? = nil |
|
|
|
|
|
|
|
|
|
@UserDefault(PreferenceKey.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : DateInterval] |
|
|
|
|
@UserDefault(PreferenceKey.pausedCountdowns.rawValue, defaultValue: [:]) static var savedPausedCountdowns: [String : TimeInterval] |
|
|
|
|
@UserDefault(PreferenceKey.stopwatches.rawValue, defaultValue: [:]) static var savedStopwatches: [String : LiveStopWatch] |
|
|
|
|
|
|
|
|
|
@Published private (set) var liveTimers: [LiveTimer] = [] |
|
|
|
|
@ -40,6 +50,11 @@ class Conductor: ObservableObject { |
|
|
|
|
init() { |
|
|
|
|
self.currentCountdowns = Conductor.savedCountdowns |
|
|
|
|
self.currentStopwatches = Conductor.savedStopwatches |
|
|
|
|
self.pausedCountdowns = Conductor.savedPausedCountdowns |
|
|
|
|
|
|
|
|
|
self.beats = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { _ in |
|
|
|
|
self._cleanupCountdowns() |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Published var cancelledCountdowns: [String] = [] |
|
|
|
|
@ -54,6 +69,15 @@ class Conductor: ObservableObject { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Published var pausedCountdowns: [String : TimeInterval] = [:] { |
|
|
|
|
didSet { |
|
|
|
|
Conductor.savedPausedCountdowns = pausedCountdowns |
|
|
|
|
withAnimation { |
|
|
|
|
self._buildLiveTimers() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Published var currentStopwatches: [String : LiveStopWatch] = [:] { |
|
|
|
|
didSet { |
|
|
|
|
Conductor.savedStopwatches = currentStopwatches |
|
|
|
|
@ -66,8 +90,9 @@ class Conductor: ObservableObject { |
|
|
|
|
func removeLiveTimer(id: TimerID) { |
|
|
|
|
// Logger.log("removeLiveTimer") |
|
|
|
|
self.liveTimers.removeAll(where: { $0.id == id }) |
|
|
|
|
self.currentStopwatches.removeValue(forKey: id) |
|
|
|
|
self.cancelledCountdowns.removeAll(where: { $0 == id }) |
|
|
|
|
self.pausedCountdowns.removeValue(forKey: id) |
|
|
|
|
self.currentStopwatches.removeValue(forKey: id) |
|
|
|
|
if let soundPlayer = self._delayedSoundPlayers[id] { |
|
|
|
|
FileLogger.log("Stop sound player: \(self._timerName(id))") |
|
|
|
|
soundPlayer.stop() |
|
|
|
|
@ -112,10 +137,10 @@ class Conductor: ObservableObject { |
|
|
|
|
return self.cancelledCountdowns.contains(where: { $0 == countdown.stringId }) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func notifyUser(countdownId: String) { |
|
|
|
|
// self._playSound(timerId: countdownId) |
|
|
|
|
self._endCountdown(countdownId: countdownId, cancel: false) |
|
|
|
|
} |
|
|
|
|
// func notifyUser(countdownId: String) { |
|
|
|
|
// // self._playSound(timerId: countdownId) |
|
|
|
|
// self._endCountdown(countdownId: countdownId, cancel: false) |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
fileprivate func _recordActivity(countdownId: String) { |
|
|
|
|
let context = PersistenceController.shared.container.viewContext |
|
|
|
|
@ -140,28 +165,31 @@ class Conductor: ObservableObject { |
|
|
|
|
self._cleanupPreviousTimerIfNecessary(countdownId) |
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
let start = Date() |
|
|
|
|
let end = start.addingTimeInterval(countdown.duration) |
|
|
|
|
FileLogger.log("schedule countdown \(self._timerName(countdownId)) at \(end)") |
|
|
|
|
let end = try self._scheduleSoundPlayer(countdown: countdown, in: countdown.duration) |
|
|
|
|
|
|
|
|
|
let sound = countdown.someSound |
|
|
|
|
let soundPlayer = try DelaySoundPlayer(timerID: countdownId, sound: sound) |
|
|
|
|
|
|
|
|
|
FileLogger.log("a) self._delayedSoundPlayers count = \(self._delayedSoundPlayers.count)") |
|
|
|
|
self._delayedSoundPlayers[countdownId] = soundPlayer |
|
|
|
|
FileLogger.log("b) self._delayedSoundPlayers count = \(self._delayedSoundPlayers.count)") |
|
|
|
|
|
|
|
|
|
try soundPlayer.start(in: countdown.duration, |
|
|
|
|
repeatCount: Int(countdown.repeatCount)) |
|
|
|
|
|
|
|
|
|
let dateInterval = DateInterval(start: start, end: end) |
|
|
|
|
self.currentCountdowns[countdownId] = dateInterval |
|
|
|
|
// let start = Date() |
|
|
|
|
// let end = start.addingTimeInterval(countdown.duration) |
|
|
|
|
// FileLogger.log("schedule countdown \(self._timerName(countdownId)) at \(end)") |
|
|
|
|
// |
|
|
|
|
// let sound = countdown.someSound |
|
|
|
|
// let soundPlayer = try DelaySoundPlayer(timerID: countdownId, sound: sound) |
|
|
|
|
// |
|
|
|
|
// FileLogger.log("a) self._delayedSoundPlayers count = \(self._delayedSoundPlayers.count)") |
|
|
|
|
// self._delayedSoundPlayers[countdownId] = soundPlayer |
|
|
|
|
// FileLogger.log("b) self._delayedSoundPlayers count = \(self._delayedSoundPlayers.count)") |
|
|
|
|
// |
|
|
|
|
// try soundPlayer.start(in: countdown.duration, |
|
|
|
|
// repeatCount: Int(countdown.repeatCount)) |
|
|
|
|
// |
|
|
|
|
// let dateInterval = DateInterval(start: start, end: end) |
|
|
|
|
// self.currentCountdowns[countdownId] = dateInterval |
|
|
|
|
// |
|
|
|
|
// self._launchLiveActivity(timer: countdown, date: end) |
|
|
|
|
|
|
|
|
|
if Preferences.playConfirmationSound { |
|
|
|
|
self._playConfirmationSound(timer: countdown) |
|
|
|
|
} |
|
|
|
|
self._launchLiveActivity(timer: countdown, date: end) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handler(.success(end)) |
|
|
|
|
} catch { |
|
|
|
|
FileLogger.log("start error : \(error.localizedDescription)") |
|
|
|
|
@ -171,6 +199,29 @@ class Conductor: ObservableObject { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _scheduleSoundPlayer(countdown: Countdown, in interval: TimeInterval) throws -> Date { |
|
|
|
|
let start = Date() |
|
|
|
|
let end = start.addingTimeInterval(interval) |
|
|
|
|
|
|
|
|
|
let countdownId = countdown.stringId |
|
|
|
|
|
|
|
|
|
FileLogger.log("schedule countdown \(self._timerName(countdownId)) at \(end)") |
|
|
|
|
|
|
|
|
|
let sound = countdown.someSound |
|
|
|
|
let soundPlayer = try DelaySoundPlayer(timerID: countdownId, sound: sound) |
|
|
|
|
self._delayedSoundPlayers[countdownId] = soundPlayer |
|
|
|
|
|
|
|
|
|
try soundPlayer.start(in: interval, |
|
|
|
|
repeatCount: Int(countdown.repeatCount)) |
|
|
|
|
|
|
|
|
|
let dateInterval = DateInterval(start: start, end: end) |
|
|
|
|
self.currentCountdowns[countdownId] = dateInterval |
|
|
|
|
|
|
|
|
|
self._launchLiveActivity(timer: countdown, date: end) |
|
|
|
|
|
|
|
|
|
return end |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _cleanupPreviousTimerIfNecessary(_ timerId: TimerID) { |
|
|
|
|
self.removeLiveTimer(id: timerId) |
|
|
|
|
if let player = self._delayedSoundPlayers[timerId] { |
|
|
|
|
@ -186,7 +237,8 @@ class Conductor: ObservableObject { |
|
|
|
|
self.cancelSoundPlayer(id: id) |
|
|
|
|
self.cancelledCountdowns.append(id) |
|
|
|
|
self._endCountdown(countdownId: id, cancel: true) |
|
|
|
|
|
|
|
|
|
self.pausedCountdowns.removeValue(forKey: id) |
|
|
|
|
|
|
|
|
|
if Preferences.playCancellationSound { |
|
|
|
|
self._playCancellationSound() |
|
|
|
|
} |
|
|
|
|
@ -194,6 +246,52 @@ class Conductor: ObservableObject { |
|
|
|
|
self._endLiveActivity(timerId: id) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func countdownState(_ countdown: Countdown) -> CountdownState { |
|
|
|
|
let id = countdown.stringId |
|
|
|
|
if self.cancelledCountdowns.contains(id) { |
|
|
|
|
return .cancelled |
|
|
|
|
} else if self.pausedCountdowns[id] != nil { |
|
|
|
|
return .paused |
|
|
|
|
} else if let interval = self.currentCountdowns[id], interval.end > Date() { |
|
|
|
|
return .inprogress |
|
|
|
|
} else { |
|
|
|
|
return .finished |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// func isCountdownPaused(_ countdown: Countdown) -> Bool { |
|
|
|
|
// return self.pausedCountdowns[countdown.stringId] != nil |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
func remainingPausedCountdownTime(_ countdown: Countdown) -> TimeInterval? { |
|
|
|
|
return self.pausedCountdowns[countdown.stringId] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func pauseCountdown(id: TimerID) { |
|
|
|
|
guard let interval = self.currentCountdowns[id] else { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let remainingTime = interval.end.timeIntervalSince(Date()) |
|
|
|
|
self.pausedCountdowns[id] = remainingTime |
|
|
|
|
|
|
|
|
|
// cancel stuff |
|
|
|
|
CountdownScheduler.master.cancelCurrentNotifications(countdownId: id) |
|
|
|
|
self.cancelSoundPlayer(id: id) |
|
|
|
|
self._endLiveActivity(timerId: id) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func resumeCountdown(id: TimerID) throws { |
|
|
|
|
let context = PersistenceController.shared.container.viewContext |
|
|
|
|
if let countdown: Countdown = context.object(stringId: id), |
|
|
|
|
let remainingTime = self.pausedCountdowns[id] { |
|
|
|
|
_ = try self._scheduleSoundPlayer(countdown: countdown, in: remainingTime) |
|
|
|
|
self.pausedCountdowns.removeValue(forKey: id) |
|
|
|
|
} else { |
|
|
|
|
throw AppError.timerNotFound(id: id) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _endCountdown(countdownId: String, cancel: Bool) { |
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
#if DEBUG |
|
|
|
|
@ -453,5 +551,10 @@ class Conductor: ObservableObject { |
|
|
|
|
fileprivate func _timerName(_ id: TimerID) -> String { |
|
|
|
|
return IntentDataProvider.main.timer(id: id)?.name ?? id |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
deinit { |
|
|
|
|
self.beats?.invalidate() |
|
|
|
|
self.beats = nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|