Adds button to pause timers

main
Laurent 3 years ago
parent cd34b42882
commit f4975cb876
  1. 4
      LeCountdown/AppDelegate.swift
  2. 149
      LeCountdown/Conductor.swift
  3. 1
      LeCountdown/Utils/Preferences.swift
  4. 132
      LeCountdown/Views/LiveTimerListView.swift

@ -110,8 +110,8 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
print("willPresent notification")
// completionHandler([.sound])
let timerId = self._timerId(notificationId: notification.request.identifier)
Conductor.maestro.notifyUser(countdownId: timerId)
// let timerId = self._timerId(notificationId: notification.request.identifier)
// Conductor.maestro.notifyUser(countdownId: timerId)
}
fileprivate func _timerId(notificationId: String) -> TimerID {

@ -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
}
}

@ -10,6 +10,7 @@ import Foundation
enum PreferenceKey: String {
case installDate
case countdowns
case pausedCountdowns
case stopwatches
case playConfirmationSound
case playCancellationSound

@ -167,28 +167,49 @@ struct LiveCountdownView: View {
@State var countdown: Countdown
var date: Date
@State var showConfirmationPopup: Bool = false
@State var showCancelConfirmationPopup: Bool = false
@State private var error: AppError?
@State private var isShowingError: Bool = false
var body: some View {
TimelineView(.periodic(from: self.date, by: 0.01)) { context in
let cancelled = Conductor.maestro.isCountdownCancelled(self.countdown)
let state = Conductor.maestro.countdownState(self.countdown)
// let remainingTime: TimeInterval? = Conductor.maestro.remainingPausedCountdownTime(self.countdown)
// let cancelled = Conductor.maestro.isCountdownCancelled(self.countdown)
HStack {
let running = self.date > context.date
// let running = self.date > context.date
VStack(alignment: .leading) {
if cancelled {
TimeView(text: NSLocalizedString("Cancelled", comment: ""))
} else if running {
switch state {
case .inprogress:
TimeView(text: self._formattedDuration(date: context.date))
} else {
case .paused:
if let remainingTime = Conductor.maestro.remainingPausedCountdownTime(self.countdown) {
TimeView(text: remainingTime.hourMinuteSecond)
}
case .finished:
TimeView(text: self.date.formatted(date: .omitted, time: .shortened))
case .cancelled:
TimeView(text: NSLocalizedString("Cancelled", comment: ""))
}
// if cancelled {
// TimeView(text: NSLocalizedString("Cancelled", comment: ""))
// } else if let remainingTime {
// TimeView(text: remainingTime.hourMinuteSecond)
// } else if running {
// TimeView(text: self._formattedDuration(date: context.date))
// } else {
// TimeView(text: self.date.formatted(date: .omitted, time: .shortened))
// }
Text(self.countdown.displayName.uppercased())
.foregroundColor(self.colorScheme == .dark ? .white : .black)
@ -196,32 +217,84 @@ struct LiveCountdownView: View {
Spacer()
Group {
if cancelled {
Image(systemName: "xmark.circle")
.foregroundColor(.accentColor)
} else if !running {
GreenCheckmarkView()
} else {
switch state {
case .inprogress:
Button {
self._pause()
} label: {
Image(systemName: "pause.circle")
.foregroundColor(.accentColor)
}
case .paused:
Button {
self._resume()
} label: {
Image(systemName: "play.circle")
.foregroundColor(.accentColor)
}
default:
EmptyView()
}
// if !cancelled && (self.date > context.date && remainingTime != nil) { // pause / resume
// if remainingTime != nil {
// Button {
// self._resume()
// } label: {
// Image(systemName: "play.circle")
// .foregroundColor(.accentColor)
// }
// } else {
// Button {
// self._pause()
// } label: {
// Image(systemName: "pause.circle")
// .foregroundColor(.accentColor)
// }
// }
// }
switch state {
case .inprogress, .paused:
Button {
self.showConfirmationPopup = true
self.showCancelConfirmationPopup = true
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.accentColor)
}
case .finished:
GreenCheckmarkView()
case .cancelled:
Image(systemName: "xmark.circle").foregroundColor(.accentColor)
}
// if cancelled { // Cancelled image
// Image(systemName: "xmark.circle").foregroundColor(.accentColor)
// } else if !running && remainingTime == nil { // Ended
// GreenCheckmarkView()
// } else { // Cancel button
// Button {
// self.showCancelConfirmationPopup = true
// } label: {
// Image(systemName: "xmark.circle.fill")
// .foregroundColor(.accentColor)
// }
// }
}.font(.system(size: actionButtonFontSize))
}
}
.contentShape(Rectangle())
.onTapGesture {
if Date() > self.date || Conductor.maestro.isCountdownCancelled(self.countdown) {
if Date() > self.date || Conductor.maestro.isCountdownCancelled(self.countdown) {
self._dismiss()
}
}
.frame(height: liveViewSize)
.monospaced()
.fullScreenCover(isPresented: self.$showConfirmationPopup) {
.fullScreenCover(isPresented: self.$showCancelConfirmationPopup) {
ZStack {
Color.black.opacity(0.1)
@ -231,7 +304,7 @@ struct LiveCountdownView: View {
Button(String(format: NSLocalizedString("Cancel %@", comment: ""), name)) {
self._cancelCountdown()
self.showConfirmationPopup = false
self.showCancelConfirmationPopup = false
}
.monospaced()
.padding()
@ -239,11 +312,19 @@ struct LiveCountdownView: View {
.background(Color.accentColor)
.cornerRadius(8.0)
}.onTapGesture {
self.showConfirmationPopup = false
self.showCancelConfirmationPopup = false
}
.background(BackgroundBlurView())
}
.alert(isPresented: $isShowingError, error: error) { _ in
// buttons
} message: { error in
if let message = error.errorMessage {
Text(message)
}
}
}
fileprivate func _actionHandler() {
@ -264,7 +345,7 @@ struct LiveCountdownView: View {
fileprivate func _formattedDuration(date: Date) -> String {
let duration = self.date.timeIntervalSince(date)
return duration.minuteSecond
return duration.hourMinuteSecond
}
fileprivate func _cancelCountdown() {
@ -273,6 +354,19 @@ struct LiveCountdownView: View {
}
}
fileprivate func _pause() {
Conductor.maestro.pauseCountdown(id: self.countdown.stringId)
}
fileprivate func _resume() {
do {
try Conductor.maestro.resumeCountdown(id: self.countdown.stringId)
} catch {
Logger.error(error)
self.error = AppError.defaultError(error: error)
}
}
}
struct LiveTimerView_Previews: PreviewProvider {

Loading…
Cancel
Save