diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index c30b6db..5cd6c1b 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -62,6 +62,8 @@ C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B58298411E800D5D950 /* CountdownFormView.swift */; }; C4742B5B298414B000D5D950 /* ImageSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5A298414B000D5D950 /* ImageSelectionView.swift */; }; C4742B5F2984205000D5D950 /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5E2984205000D5D950 /* ViewModifiers.swift */; }; + C498E59F298D4DEA00E90DE0 /* LiveTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C498E59E298D4DEA00E90DE0 /* LiveTimerView.swift */; }; + C498E5A1298D543900E90DE0 /* LiveTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C498E5A0298D543900E90DE0 /* LiveTimer.swift */; }; C4F8B1532987FE6F005C86A5 /* LaunchWidgetLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.swift */; }; C4F8B15729891271005C86A5 /* Conductor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15629891271005C86A5 /* Conductor.swift */; }; C4F8B15929891528005C86A5 /* forest_stream.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C4F8B15829891528005C86A5 /* forest_stream.mp3 */; }; @@ -220,6 +222,8 @@ C4742B58298411E800D5D950 /* CountdownFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownFormView.swift; sourceTree = ""; }; C4742B5A298414B000D5D950 /* ImageSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSelectionView.swift; sourceTree = ""; }; C4742B5E2984205000D5D950 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = ""; }; + C498E59E298D4DEA00E90DE0 /* LiveTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTimerView.swift; sourceTree = ""; }; + C498E5A0298D543900E90DE0 /* LiveTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTimer.swift; sourceTree = ""; }; C4F8B15629891271005C86A5 /* Conductor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conductor.swift; sourceTree = ""; }; C4F8B15829891528005C86A5 /* forest_stream.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = forest_stream.mp3; sourceTree = ""; }; C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = ""; }; @@ -413,6 +417,7 @@ C4F8B188298AC248005C86A5 /* Generation */, C4060DCA297AE73D003FAB80 /* LeCountdown.xcdatamodeld */, C438C80C2982847300BF3EF9 /* CoreDataRequests.swift */, + C498E5A0298D543900E90DE0 /* LiveTimer.swift */, C438C806298195E600BF3EF9 /* Model+Extensions.swift */, C438C7C4298024E900BF3EF9 /* NSManagedContext+Extensions.swift */, C4060DC8297AE73D003FAB80 /* Persistence.swift */, @@ -440,6 +445,7 @@ C4F8B1D3298BF686005C86A5 /* Components */, C4060DC1297AE73B003FAB80 /* ContentView.swift */, C4F8B1CF298BF2E2005C86A5 /* DialView.swift */, + C498E59E298D4DEA00E90DE0 /* LiveTimerView.swift */, C438C80E29828B8600BF3EF9 /* RecordsView.swift */, C4742B5A298414B000D5D950 /* ImageSelectionView.swift */, C4F8B165298A9ABB005C86A5 /* SoundImageFormView.swift */, @@ -720,6 +726,7 @@ files = ( C4F8B1D2298BF646005C86A5 /* PermissionAlertView.swift in Sources */, C4060DC9297AE73D003FAB80 /* Persistence.swift in Sources */, + C498E5A1298D543900E90DE0 /* LiveTimer.swift in Sources */, C4F8B1AB298AC3A0005C86A5 /* Countdown+CoreDataProperties.swift in Sources */, C438C80F29828B8600BF3EF9 /* RecordsView.swift in Sources */, C4F8B187298AC234005C86A5 /* Activity+CoreDataProperties.swift in Sources */, @@ -764,6 +771,7 @@ C438C7C5298024E900BF3EF9 /* NSManagedContext+Extensions.swift in Sources */, C4F8B15F298961A7005C86A5 /* ReorderableForEach.swift in Sources */, C4F8B1AC298AC3A0005C86A5 /* Alarm+CoreDataProperties.swift in Sources */, + C498E59F298D4DEA00E90DE0 /* LiveTimerView.swift in Sources */, C4060DC0297AE73B003FAB80 /* LeCountdownApp.swift in Sources */, C438C7E02981216300BF3EF9 /* LaunchWidget.intentdefinition in Sources */, C4742B5729840F6400D5D950 /* CoolPic.swift in Sources */, diff --git a/LeCountdown/Conductor.swift b/LeCountdown/Conductor.swift index 3c51af1..19b04b2 100644 --- a/LeCountdown/Conductor.swift +++ b/LeCountdown/Conductor.swift @@ -15,35 +15,50 @@ class Conductor : ObservableObject { @Published var soundPlayer: SoundPlayer? = nil - @UserDefault(Key.dates.rawValue, defaultValue: [:]) static var savedDates: [String : DateInterval] - @UserDefault(Key.stopwatch.rawValue, defaultValue: [:]) static var savedStopwatches: [String : Date] + @UserDefault(Key.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : DateInterval] + @UserDefault(Key.stopwatches.rawValue, defaultValue: [:]) static var savedStopwatches: [String : Date] + @Published var liveTimers: [LiveTimer] = [] + init() { - self.timerDates = Conductor.savedDates - self.stopwatchIntervals = Conductor.savedStopwatches + self.currentCountdowns = Conductor.savedCountdowns + self.currentStopwatches = Conductor.savedStopwatches } - @Published var timerDates: [String : DateInterval] = [:] { + @Published var currentCountdowns: [String : DateInterval] = [:] { didSet { - Conductor.savedDates = timerDates + Conductor.savedCountdowns = currentCountdowns + self._buildLiveTimers() } } - @Published var stopwatchIntervals: [String : Date] = [:] { + @Published var currentStopwatches: [String : Date] = [:] { didSet { - Conductor.savedStopwatches = stopwatchIntervals + Conductor.savedStopwatches = currentStopwatches + self._buildLiveTimers() + } + } + + fileprivate func _buildLiveTimers() { + var countdowns = self.currentCountdowns.map { + return LiveTimer(id: $0, date: $1.end) + } + var stopwatches = self.currentStopwatches.map { + return LiveTimer(id: $0, date: $1) } + countdowns.append(contentsOf: stopwatches) + self.liveTimers = countdowns.sorted() } enum Key : String { - case dates - case stopwatch + case countdowns + case stopwatches } func startCountdown(_ date: Date, countdown: Countdown) { DispatchQueue.main.async { let dateInterval = DateInterval(start: Date(), end: date) - self.timerDates[countdown.stringId] = dateInterval + self.currentCountdowns[countdown.stringId] = dateInterval self._launchLiveActivity(countdown: countdown, endDate: date) } @@ -60,15 +75,17 @@ class Conductor : ObservableObject { self._recordActivity(countdownId: countdownId) } - if self.timerDates.removeValue(forKey: countdownId) != nil { + if self.currentCountdowns.removeValue(forKey: countdownId) != nil { self._endLiveActivity(countdownId: countdownId) } + + self.stopSoundIfPossible() // multi use } } func cleanup() { let now = Date() - for (key, value) in self.timerDates { + for (key, value) in self.currentCountdowns { if value.end < now { self.endCountdown(countdownId: key, cancel: false) } @@ -78,7 +95,7 @@ class Conductor : ObservableObject { fileprivate func _recordActivity(countdownId: String) { let context = PersistenceController.shared.container.viewContext if let countdown = context.object(stringId: countdownId) as? Countdown, - let dateInterval = self.timerDates[countdownId] { + let dateInterval = self.currentCountdowns[countdownId] { do { try CoreDataRequests.recordActivity(timer: countdown, dateInterval: dateInterval) } catch { @@ -154,7 +171,7 @@ class Conductor : ObservableObject { func updateLiveActivities() { print("update live activity...") - for (countdownId, interval) in self.timerDates { + for (countdownId, interval) in self.currentCountdowns { if interval.end < Date() { self._endLiveActivity(countdownId: countdownId) diff --git a/LeCountdown/Model/LiveTimer.swift b/LeCountdown/Model/LiveTimer.swift new file mode 100644 index 0000000..552c201 --- /dev/null +++ b/LeCountdown/Model/LiveTimer.swift @@ -0,0 +1,22 @@ +// +// LiveData.swift +// LeCountdown +// +// Created by Laurent Morvillier on 03/02/2023. +// + +import Foundation +import CoreData + +struct LiveTimer: Identifiable, Comparable { + var id: String + var date: Date + + static func < (lhs: LiveTimer, rhs: LiveTimer) -> Bool { + return lhs.date < rhs.date + } + + func timer(context: NSManagedObjectContext) -> AbstractTimer? { + return context.object(stringId: self.id) as? AbstractTimer + } +} diff --git a/LeCountdown/TimerRouter.swift b/LeCountdown/TimerRouter.swift index db14bf3..59e6323 100644 --- a/LeCountdown/TimerRouter.swift +++ b/LeCountdown/TimerRouter.swift @@ -24,6 +24,17 @@ class TimerRouter { } + static func stopTimer(timer: AbstractTimer) { + switch timer { + case let countdown as Countdown: + Conductor.maestro.endCountdown(countdownId: countdown.stringId, cancel: true) + case let stopwatch as Stopwatch: + self._stopStopwatch(stopwatch) + default: + print("missing launcher for \(self)") + } + } + fileprivate static func _launchCountdown(_ countdown: Countdown, handler: @escaping (Result) -> Void) { UNUserNotificationCenter.current().getNotificationSettings { settings in @@ -50,19 +61,19 @@ class TimerRouter { fileprivate static func _startStopwatch(_ stopwatch: Stopwatch, handler: @escaping (Result) -> Void) { - if let start = Conductor.maestro.stopwatchIntervals[stopwatch.stringId] { - - Conductor.maestro.stopwatchIntervals.removeValue(forKey: stopwatch.stringId) + Conductor.maestro.currentStopwatches[stopwatch.stringId] = Date() + + } + + fileprivate static func _stopStopwatch(_ stopwatch: Stopwatch) { + if let start = Conductor.maestro.currentStopwatches[stopwatch.stringId] { + Conductor.maestro.currentStopwatches.removeValue(forKey: stopwatch.stringId) do { try CoreDataRequests.recordActivity(timer: stopwatch, dateInterval: DateInterval(start: start, end: Date())) } catch { - handler(.failure(error)) + print("could not record") } - - } else { - Conductor.maestro.stopwatchIntervals[stopwatch.stringId] = Date() } - } } diff --git a/LeCountdown/Utils/ViewModifiers.swift b/LeCountdown/Utils/ViewModifiers.swift index 92d03a7..dcdc1f6 100644 --- a/LeCountdown/Utils/ViewModifiers.swift +++ b/LeCountdown/Utils/ViewModifiers.swift @@ -13,7 +13,11 @@ extension View { func roundedCorner(selected: Bool) -> some View { modifier(RoundedCornerSelection(selected: selected)) } - + + func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { + clipShape( RoundedCorner(radius: radius, corners: corners) ) + } + } struct RoundedCornerSelection: ViewModifier { @@ -33,3 +37,14 @@ struct RoundedCornerSelection: ViewModifier { } } + +struct RoundedCorner: Shape { + + var radius: CGFloat = .infinity + var corners: UIRectCorner = .allCorners + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) + return Path(path.cgPath) + } +} diff --git a/LeCountdown/Views/Alarm/AlarmDialView.swift b/LeCountdown/Views/Alarm/AlarmDialView.swift index aa62a3a..22a3b44 100644 --- a/LeCountdown/Views/Alarm/AlarmDialView.swift +++ b/LeCountdown/Views/Alarm/AlarmDialView.swift @@ -12,20 +12,16 @@ struct AlarmDialView: View { @ObservedObject var alarm: Alarm var body: some View { - VStack { + + HStack { - HStack { + VStack { Text(alarm.activity?.name?.uppercased() ?? "") - Spacer() - } - - if let fireDate = alarm.fireDate { - HStack { + if let fireDate = alarm.fireDate { Text(fireDate, style: .time) - Spacer() } + Spacer() } - Spacer() } } @@ -35,5 +31,6 @@ struct AlarmDialView: View { struct AlarmDialView_Previews: PreviewProvider { static var previews: some View { AlarmDialView(alarm: Alarm.fake(context: PersistenceController.preview.container.viewContext)) + .background(.cyan) } } diff --git a/LeCountdown/Views/ContentView.swift b/LeCountdown/Views/ContentView.swift index 89e9ad9..f2f6462 100644 --- a/LeCountdown/Views/ContentView.swift +++ b/LeCountdown/Views/ContentView.swift @@ -46,7 +46,7 @@ struct ContentView: View { GeometryReader { reader in let width: CGFloat = reader.size.width / 2 - 10.0 - ZStack(alignment: .bottom) { + VStack { ScrollView { @@ -65,24 +65,13 @@ struct ContentView: View { self._reorder(from: from, to: to) } } - } + }.padding(.horizontal, itemSpacing) - if Conductor.maestro.soundPlayer != nil { - Button { - Conductor.maestro.stopSoundIfPossible() - } label: { - Text("STOP") - .frame(minWidth: 0.0, maxWidth: .infinity, minHeight: 75.0, maxHeight: 75.0) - .foregroundColor(.white) - .background(.red) - .cornerRadius(16.0) - }.padding() - } - + LiveTimerView() .environment(\.managedObjectContext, viewContext) + .background(Color(white: 0.9)) + .cornerRadius(16.0, corners: [.topRight, .topLeft]) } - - - }.padding(.horizontal, itemSpacing) + } .navigationTitle("\(String(describing: T.self))") .alert(boringContext.error?.localizedDescription ?? "missing error", isPresented: $boringContext.showDefaultAlert) { Button("OK", role: .cancel) { } @@ -231,7 +220,7 @@ struct MainToolbarView: View { fileprivate extension Countdown { var endDate: Date? { - return Conductor.maestro.timerDates[self.stringId]?.end + return Conductor.maestro.currentCountdowns[self.stringId]?.end } var isLive: Bool { diff --git a/LeCountdown/Views/Countdown/CountdownDialView.swift b/LeCountdown/Views/Countdown/CountdownDialView.swift index 1ec8629..af85c69 100644 --- a/LeCountdown/Views/Countdown/CountdownDialView.swift +++ b/LeCountdown/Views/Countdown/CountdownDialView.swift @@ -19,7 +19,7 @@ struct CountdownDialView: View { VStack(alignment: .leading) { Text(countdown.activity?.name?.uppercased() ?? "") // let dateInterval = DateInterval(start: Date(), end: Date()) - if let dateInterval = conductor.timerDates[countdown.stringId] { + if let dateInterval = conductor.currentCountdowns[countdown.stringId] { Text(dateInterval.end, style: .timer) Spacer() HStack { diff --git a/LeCountdown/Views/LiveTimerView.swift b/LeCountdown/Views/LiveTimerView.swift new file mode 100644 index 0000000..a011d68 --- /dev/null +++ b/LeCountdown/Views/LiveTimerView.swift @@ -0,0 +1,70 @@ +// +// LiveTimerView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 03/02/2023. +// + +import SwiftUI + +struct LiveTimerView: View { + + @Environment(\.managedObjectContext) private var viewContext + + @EnvironmentObject var conductor: Conductor + + var body: some View { + LazyVStack { + ForEach(conductor.liveTimers) { liveTimer in + + let timer = liveTimer.timer(context: self.viewContext) + + HStack { + Text(timer?.displayName.uppercased() ?? "missing") + Spacer() + Text(liveTimer.date, style: .timer) + Spacer() + Button { + self._stopTimer(timer) + } label: { + Text("STOP") + .padding(8.0) + .foregroundColor(.red) + .background(.white) + .fontWeight(.semibold) + .cornerRadius(8.0) + }//.buttonStyle(.bordered).tint(.red) + + } + .padding() + .frame(height: 55.0) + .foregroundColor(.white) + .monospaced() + .background(.cyan) + .cornerRadius(16.0) + } + }.padding(8.0) + } + + fileprivate func _stopTimer(_ timer: AbstractTimer?) { + + guard let timer else { + return + } + + TimerRouter.stopTimer(timer: timer) + + } + +} + +struct LiveTimerView_Previews: PreviewProvider { + + init() { + Conductor.maestro.currentCountdowns["fef"] = DateInterval(start: Date(), end: Date()) + } + + static var previews: some View { + LiveTimerView().environmentObject(Conductor.maestro) + } +} diff --git a/LeCountdown/Views/Stopwatch/StopwatchDialView.swift b/LeCountdown/Views/Stopwatch/StopwatchDialView.swift index 74c4a09..80d42df 100644 --- a/LeCountdown/Views/Stopwatch/StopwatchDialView.swift +++ b/LeCountdown/Views/Stopwatch/StopwatchDialView.swift @@ -18,7 +18,7 @@ struct StopwatchDialView: View { VStack(alignment: .leading) { Text(stopwatch.activity?.name?.uppercased() ?? "") - if let start = conductor.stopwatchIntervals[stopwatch.stringId] { + if let start = conductor.currentStopwatches[stopwatch.stringId] { Text(start, style: .timer) }