From a60750ce97c7139c7b6186a7ed9e618e783b61bb Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 30 Nov 2023 11:49:36 +0100 Subject: [PATCH] WIP but steps can be launched --- LeCountdown.xcodeproj/project.pbxproj | 8 ++ LeCountdown/Conductor.swift | 45 +++++--- LeCountdown/LeCountdownApp.swift | 91 +++++++--------- .../Countdown+CoreDataProperties.swift | 5 +- .../LeCountdown.0.6.6.xcdatamodel/contents | 3 +- LeCountdown/Model/Model+CSV.swift | 102 ++++++++++++++++++ .../Model/Model+SharedExtensions.swift | 4 +- LeCountdown/Model/Persistence.swift | 4 +- LeCountdown/Patcher.swift | 62 +++++++++++ .../Views/Countdown/CountdownFormView.swift | 13 ++- .../Views/Countdown/NewCountdownView.swift | 15 ++- .../Views/Countdown/RangeFormView.swift | 25 +++-- LeCountdown/Views/LiveTimerListView.swift | 50 ++------- LeCountdown/Views/Reusable/MailView.swift | 5 + LeCountdown/Views/StartView.swift | 1 + LeCountdown/Views/TimerModel.swift | 12 ++- 16 files changed, 310 insertions(+), 135 deletions(-) create mode 100644 LeCountdown/Model/Model+CSV.swift create mode 100644 LeCountdown/Patcher.swift diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index 864c695..db4a721 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -98,6 +98,8 @@ C4556F7329E40EC200DEB40B /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F6E29E40BED00DEB40B /* FileUtils.swift */; }; C4556F7429E40EC500DEB40B /* Codable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F7029E40DCF00DEB40B /* Codable+Extensions.swift */; }; C4556F7629E411A400DEB40B /* LogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F7529E411A400DEB40B /* LogsView.swift */; }; + C45D6AB52B173DFD00A5B649 /* Patcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6AB42B173DFD00A5B649 /* Patcher.swift */; }; + C45D6AB92B17499200A5B649 /* Model+CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6AB82B17499200A5B649 /* Model+CSV.swift */; }; C4636D9C29AF46BD00994E31 /* ActivityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C4636D9B29AF46BD00994E31 /* ActivityKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; C4636D9D29AF46D900994E31 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C438C7D02981216200BF3EF9 /* WidgetKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; C46926BD29DDC49E0003E310 /* SubscriptionButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46926BC29DDC49E0003E310 /* SubscriptionButtonView.swift */; }; @@ -418,6 +420,8 @@ C4556F6E29E40BED00DEB40B /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; C4556F7029E40DCF00DEB40B /* Codable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Codable+Extensions.swift"; sourceTree = ""; }; C4556F7529E411A400DEB40B /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = ""; }; + C45D6AB42B173DFD00A5B649 /* Patcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patcher.swift; sourceTree = ""; }; + C45D6AB82B17499200A5B649 /* Model+CSV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Model+CSV.swift"; sourceTree = ""; }; C4636D9B29AF46BD00994E31 /* ActivityKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ActivityKit.framework; path = System/Library/Frameworks/ActivityKit.framework; sourceTree = SDKROOT; }; C46926BC29DDC49E0003E310 /* SubscriptionButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionButtonView.swift; sourceTree = ""; }; C473C2F829A8DC0A0056B38A /* LaunchWidgetAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchWidgetAttributes.swift; sourceTree = ""; }; @@ -613,6 +617,7 @@ children = ( C438C7C829803CA000BF3EF9 /* AppDelegate.swift */, C4060DBF297AE73B003FAB80 /* LeCountdownApp.swift */, + C45D6AB42B173DFD00A5B649 /* Patcher.swift */, C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */, C4F8B15629891271005C86A5 /* Conductor.swift */, C4F8B1D7298C0727005C86A5 /* TimerRouter.swift */, @@ -750,6 +755,7 @@ C438C80C2982847300BF3EF9 /* CoreDataRequests.swift */, C49C346829DECA4400AAC6FC /* LiveStopWatch.swift */, C498E5A0298D543900E90DE0 /* LiveTimer.swift */, + C45D6AB82B17499200A5B649 /* Model+CSV.swift */, C438C806298195E600BF3EF9 /* Model+Extensions.swift */, C4BA2B39299F838000CB4FBA /* Model+SharedExtensions.swift */, C438C7C4298024E900BF3EF9 /* NSManagedContext+Extensions.swift */, @@ -1299,6 +1305,7 @@ C4BA2B5F299FFC8400CB4FBA /* StoreView.swift in Sources */, C4BA2B7929A65C1400CB4FBA /* UIDevice+Extensions.swift in Sources */, C4F8B17E298AC234005C86A5 /* Alarm+CoreDataClass.swift in Sources */, + C45D6AB92B17499200A5B649 /* Model+CSV.swift in Sources */, C42E970729E6EDF5005B1B8C /* StatisticsSubView.swift in Sources */, C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */, C4F8B1BF298ACA0B005C86A5 /* StopwatchDialView.swift in Sources */, @@ -1325,6 +1332,7 @@ C4BA2AFD299A3A3700CB4FBA /* AppleMusicPlayer.swift in Sources */, C4BA2B3A299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */, C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */, + C45D6AB52B173DFD00A5B649 /* Patcher.swift in Sources */, C4556F6B29E40B7800DEB40B /* FileLogger.swift in Sources */, C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */, C4BA2B25299D35C100CB4FBA /* HomeView.swift in Sources */, diff --git a/LeCountdown/Conductor.swift b/LeCountdown/Conductor.swift index 6b647f4..6863f2c 100644 --- a/LeCountdown/Conductor.swift +++ b/LeCountdown/Conductor.swift @@ -37,7 +37,7 @@ struct CountdownSequence: Codable { 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") + return current ?? self.spans.last ?? CountdownSequence.defaultSpan } var currentEnd: Date { @@ -58,12 +58,14 @@ struct CountdownSequence: Codable { } } - private static let defaultSpan = CountdownSpan(interval: DateInterval(start: Date(), end: Date()), name: "none") + private static let defaultSpan = CountdownSpan(interval: DateInterval(start: Date(), end: Date()), name: "none", index: 0, loopCount: 1) } struct CountdownSpan: Codable { var interval: DateInterval var name: String? + var index: Int16 + var loopCount: Int16 var start: Date { return self.interval.start @@ -72,6 +74,17 @@ struct CountdownSpan: Codable { var end: Date { return self.interval.end } + + var label: String { + var components: [String] = [] + if let name { + components.append(name) + } + if loopCount > 1 { + components.append("#\(index + 1)") + } + return components.joined(separator: " ") + } } class Conductor: ObservableObject { @@ -139,7 +152,7 @@ class Conductor: ObservableObject { let liveCountdowns: [LiveTimer] = self.currentCountdowns.map { id, sequence in let currentSpan = sequence.currentSpan - return LiveTimer(id: id, name: currentSpan.name, date: currentSpan.end) + return LiveTimer(id: id, name: currentSpan.label, date: currentSpan.end) } // add countdown if not present for liveCountdown in liveCountdowns { @@ -213,7 +226,7 @@ class Conductor: ObservableObject { var spans: [CountdownSpan] = [] let now = Date() - for _ in 0.. Date { let start = Date() let end = start.addingTimeInterval(interval) - let countdownId = countdown.stringId +// let countdownId = countdown.stringId FileLogger.log("schedule countdown \(range.name ?? "''") at \(end)") let sound = range.someSound ?? countdown.someSound ?? Sound.default - let playerId = range.stringId + interval.debugDescription + let idComponents = [countdown.stringId, range.stringId, interval.debugDescription] + let playerId = idComponents.joined(separator: idSeparator) let soundPlayer = try DelaySoundPlayer(sound: sound) self._delayedSoundPlayers[playerId] = soundPlayer @@ -547,12 +563,17 @@ class Conductor: ObservableObject { } func cancelSoundPlayer(id: TimerID) { - if let soundPlayer = self._delayedSoundPlayers[id] { - soundPlayer.stop() - self._delayedSoundPlayers.removeValue(forKey: id) - FileLogger.log("cancelled sound player for \(self._timerName(id))") - } + let keys = self._delayedSoundPlayers.keys.filter { $0.starts(with: id) } + + for key in keys { + if let soundPlayer = self._delayedSoundPlayers[key] { + soundPlayer.stop() + self._delayedSoundPlayers.removeValue(forKey: key) + } + } + FileLogger.log("cancelled \(keys.count) sound players for \(self._timerName(id))") + self.deactivateAudioSessionIfPossible() } diff --git a/LeCountdown/LeCountdownApp.swift b/LeCountdown/LeCountdownApp.swift index 9a34632..60b851a 100644 --- a/LeCountdown/LeCountdownApp.swift +++ b/LeCountdown/LeCountdownApp.swift @@ -16,10 +16,10 @@ import CloudKit struct LeCountdownApp: App { let persistenceController = PersistenceController.shared - + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @Environment(\.scenePhase) var scenePhase - + @State var showStartView: Bool = false init() { @@ -42,25 +42,25 @@ struct LeCountdownApp: App { StartView(isPresented: $showStartView) .environment(\.managedObjectContext, persistenceController.container.viewContext) } - .onAppear { - self._onAppear() - } - .onChange(of: scenePhase) { newPhase in - switch newPhase { - case .inactive: - Conductor.maestro.stopMainPlayersIfPossible() - Conductor.maestro.memoryWarningReceived = false - case .active: -// Logger.log("onChange(of: scenePhase) active") -// Logger.log(Conductor.maestro.currentCountdowns.count) - Conductor.maestro.restoreSoundPlayers() - Conductor.maestro.cleanup() - default: - break + .onAppear { + self._onAppear() + } + .onChange(of: scenePhase) { newPhase in + switch newPhase { + case .inactive: + Conductor.maestro.stopMainPlayersIfPossible() + Conductor.maestro.memoryWarningReceived = false + case .active: + // Logger.log("onChange(of: scenePhase) active") + // Logger.log(Conductor.maestro.currentCountdowns.count) + Conductor.maestro.restoreSoundPlayers() + Conductor.maestro.cleanup() + default: + break + } } - } } - + } fileprivate func _shouldShowStartView() -> Bool { @@ -78,23 +78,23 @@ struct LeCountdownApp: App { let containerAvailable = self.isICloudContainerAvailable() Logger.log("isICloudContainerAvailable = \(containerAvailable)") -// let voices = AVSpeechSynthesisVoice.speechVoices() -// let grouped = Dictionary(grouping: voices, by: { $0.language }) -// for language in grouped.keys { -// if let lvoices = grouped[language] { -// print("language = \(language)") -// for voice in lvoices { -// print("name = \(voice.name), gender = \(voice.gender)") -// } -// print("========") -// } -// } + // let voices = AVSpeechSynthesisVoice.speechVoices() + // let grouped = Dictionary(grouping: voices, by: { $0.language }) + // for language in grouped.keys { + // if let lvoices = grouped[language] { + // print("language = \(language)") + // for voice in lvoices { + // print("name = \(voice.name), gender = \(voice.gender)") + // } + // print("========") + // } + // } } fileprivate func _registerBackgroundRefreshes() { BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTaskIdentifier.refresh.rawValue, using: nil) { task in - self._handleAppRefresh(task: task as! BGAppRefreshTask) + self._handleAppRefresh(task: task as! BGAppRefreshTask) } } @@ -102,30 +102,19 @@ struct LeCountdownApp: App { print("_handleAppRefresh = \(task.description)") -// task.expirationHandler = { -// print("expired") -// } -// -// DispatchQueue.main.async { -// Conductor.maestro.updateLiveActivities() -// task.setTaskCompleted(success: true) -// } + // task.expirationHandler = { + // print("expired") + // } + // + // DispatchQueue.main.async { + // Conductor.maestro.updateLiveActivities() + // task.setTaskCompleted(success: true) + // } } fileprivate func _patch() { - - let context = PersistenceController.shared.container.viewContext - do { - let records = try context.fetch(Record.fetchRequest()) - for record in records { - record.preCompute() - } - try context.save() - } catch { - Logger.error(error) - } - + Patcher.patch() } func isICloudContainerAvailable() -> Bool { diff --git a/LeCountdown/Model/Generation/Countdown+CoreDataProperties.swift b/LeCountdown/Model/Generation/Countdown+CoreDataProperties.swift index b69b70e..b706ded 100644 --- a/LeCountdown/Model/Generation/Countdown+CoreDataProperties.swift +++ b/LeCountdown/Model/Generation/Countdown+CoreDataProperties.swift @@ -2,7 +2,7 @@ // Countdown+CoreDataProperties.swift // LeCountdown // -// Created by Laurent Morvillier on 24/11/2023. +// Created by Laurent Morvillier on 29/11/2023. // // @@ -16,7 +16,8 @@ extension Countdown { return NSFetchRequest(entityName: "Countdown") } - @NSManaged public var repeatCount: Int16 + @NSManaged public var loops: Int16 + @NSManaged public var duration: Double @NSManaged public var timeRanges: NSSet? } diff --git a/LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.6.xcdatamodel/contents b/LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.6.xcdatamodel/contents index 37c2a4b..0e3e888 100644 --- a/LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.6.xcdatamodel/contents +++ b/LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.6.xcdatamodel/contents @@ -18,7 +18,8 @@ - + + diff --git a/LeCountdown/Model/Model+CSV.swift b/LeCountdown/Model/Model+CSV.swift new file mode 100644 index 0000000..3759754 --- /dev/null +++ b/LeCountdown/Model/Model+CSV.swift @@ -0,0 +1,102 @@ +// +// Model+CSV.swift +// LeCountdown +// +// Created by Laurent Morvillier on 29/11/2023. +// + +import Foundation +import CoreData + +protocol CSVField { + associatedtype T: CSVRepresentable + var header: String { get } +} + +protocol CSVRepresentable: NSFetchRequestResult { + + associatedtype F: CSVField + + func toCSV() -> String + + static var fields: [F] { get } + + func value(field: F) -> String? +} + +extension CSVRepresentable { + + static func toCSV() -> String { + var csv: String = "" + + let context: NSManagedObjectContext = PersistenceController.shared.container.viewContext + + let request = NSFetchRequest(entityName: String(describing: self)) + + csv = self.fields.map { "\"\($0.header)\"" }.joined(separator: ",") + csv.append("\n") + + do { + let entities = try context.fetch(request) + for entity in entities { + let entityCSV = entity.toCSV() + csv.append("\(entityCSV)\n") + } + + + } catch { + Logger.error(error) + } + + return csv + } + + func toCSV() -> String { + let values: [String?] = Self.fields.map { field in + self.value(field: field) + } + return values.map { "\"\($0 ?? "")\"" }.joined(separator: ",") + } + +} + +extension Record : CSVRepresentable { + + static var fields: [RecordCSVField] { + return RecordCSVField.allCases + } + + func value(field: RecordCSVField) -> String? { + switch field { + case .activity: + return self.activity?.name + case .start: + return self.start?.formattedDateTime + case .end: + return self.end?.formattedDateTime + case .cancelled: + return self.cancelled.description + } + } + +} + +enum RecordCSVField: CSVField, CaseIterable { + + typealias T = Record + + case activity + case start + case end + case cancelled + + var header: String { + switch self { + case .activity: return "Activity" + case .start: return "Start" + case .end: return "End" + case .cancelled: return "Cancelled" + } + } + +} diff --git a/LeCountdown/Model/Model+SharedExtensions.swift b/LeCountdown/Model/Model+SharedExtensions.swift index 3117dec..575a4fb 100644 --- a/LeCountdown/Model/Model+SharedExtensions.swift +++ b/LeCountdown/Model/Model+SharedExtensions.swift @@ -70,8 +70,8 @@ extension Countdown { formatted = durations.first ?? "none" } - if self.repeatCount > 1 { - return "\(formatted) * \(repeatCount)" + if self.loops > 1 { + return "\(formatted) * \(loops)" } else { return formatted } diff --git a/LeCountdown/Model/Persistence.swift b/LeCountdown/Model/Persistence.swift index fa6c830..a381a69 100644 --- a/LeCountdown/Model/Persistence.swift +++ b/LeCountdown/Model/Persistence.swift @@ -66,8 +66,8 @@ struct PersistenceController { let storeURL = URL.storeURL(for: "group.com.staxriver.countdown", databaseName: "group.com.staxriver.countdown") let storeDescription = NSPersistentStoreDescription(url: storeURL) -// storeDescription.shouldMigrateStoreAutomatically = true -// storeDescription.shouldInferMappingModelAutomatically = true + storeDescription.shouldMigrateStoreAutomatically = true + storeDescription.shouldInferMappingModelAutomatically = true let id = "iCloud.LeCountdown" let options = NSPersistentCloudKitContainerOptions(containerIdentifier: id) diff --git a/LeCountdown/Patcher.swift b/LeCountdown/Patcher.swift new file mode 100644 index 0000000..cc6fc63 --- /dev/null +++ b/LeCountdown/Patcher.swift @@ -0,0 +1,62 @@ +// +// Patcher.swift +// LeCountdown +// +// Created by Laurent Morvillier on 29/11/2023. +// + +import Foundation +import CoreData + +enum Patch: String, CaseIterable { + case interval + + var key: String { + return "patch." + self.rawValue + } +} + +class Patcher { + + static func patch() { + + let context = PersistenceController.shared.container.viewContext + for patch in Patch.allCases { + + if true { + Logger.log("PATCH!!!") +// if !UserDefaults.standard.bool(forKey: patch.key) { + + do { + switch patch { + case .interval: + try self._patchIntervals(context: context) + } + + try context.save() + UserDefaults.standard.set(true, forKey: patch.key) + } catch { + Logger.error(error) + } + } + + } + + } + + static fileprivate func _patchIntervals(context: NSManagedObjectContext) throws { + + let countdowns: [Countdown] = try context.fetch(Countdown.fetchRequest()) + for countdown in countdowns { + if let ranges = countdown.timeRanges, ranges.count == 0 { + let range = TimeRange(context: context) + range.duration = countdown.duration + range.name = countdown.name + range.order = 0 + countdown.addToTimeRanges(range) + } + } + + } + +} diff --git a/LeCountdown/Views/Countdown/CountdownFormView.swift b/LeCountdown/Views/Countdown/CountdownFormView.swift index a0da34e..39c0220 100644 --- a/LeCountdown/Views/Countdown/CountdownFormView.swift +++ b/LeCountdown/Views/Countdown/CountdownFormView.swift @@ -52,6 +52,8 @@ struct CountdownFormView : View { } label: { LabeledContent(range.name ?? "", value: range.duration.hourMinuteSecond) } + }.onDelete { indexSet in + self.model.deleteRange(indexSet) } } @@ -68,9 +70,9 @@ struct CountdownFormView : View { Section { HStack { - Stepper("Repeat Count", value: self.$model.repeatCount, in: 1...100) + Stepper("Repeat Count", value: self.$model.loops, in: 1...100) Spacer() - Text(self.model.repeatCount.formatted()) + Text(self.model.loops.formatted()) .frame(width: 24.0) } } @@ -96,9 +98,10 @@ struct CountdownFormView : View { SoundFormView(model: self.model) } - .sheet(item: self.$selectedRange) { item in - RangeFormView(timeRange: item, - selectedItem: self.$selectedRange) + .sheet(item: self.$selectedRange, onDismiss: { + self.model.objectWillChange.send() + }) { item in + RangeFormView(timeRange: item) } } diff --git a/LeCountdown/Views/Countdown/NewCountdownView.swift b/LeCountdown/Views/Countdown/NewCountdownView.swift index ec8ada1..b53b6b0 100644 --- a/LeCountdown/Views/Countdown/NewCountdownView.swift +++ b/LeCountdown/Views/Countdown/NewCountdownView.swift @@ -203,9 +203,8 @@ struct CountdownEditView : View { fileprivate func _loadCountdown(_ countdown: Countdown) { let ranges = countdown.sortedRanges() - if ranges.count > 1 { - self.model.ranges = ranges - } else if let range = ranges.first { + self.model.ranges = ranges + if let range = ranges.first { self.duration = range.duration self.nameString = range.name ?? "" } @@ -217,11 +216,8 @@ struct CountdownEditView : View { self.model.soundModel.setPlayables(countdown.playables) self.model.confirmationSoundModel.setPlayables(countdown.confirmationPlayables) - self.model.repeatCount = countdown.repeatCount + self.model.loops = countdown.loops - if let image = countdown.image, - let coolpic = CoolPic(rawValue: image) { - } } fileprivate func _cancel() { @@ -259,16 +255,17 @@ struct CountdownEditView : View { cd.playableIds = self.model.soundModel.playableIds cd.setConfirmationSounds(self.model.confirmationSoundModel.sounds) - cd.repeatCount = self.model.repeatCount + cd.loops = self.model.loops if self.model.ranges.count > 0 { - if let timeRanges = cd.timeRanges { cd.removeFromTimeRanges(timeRanges) } for (index, range) in self.model.ranges.enumerated() { range.order = Int16(index) + range.duration = range.duration + range.name = range.name cd.addToTimeRanges(range) } } else { diff --git a/LeCountdown/Views/Countdown/RangeFormView.swift b/LeCountdown/Views/Countdown/RangeFormView.swift index ed596ce..0f8d0b9 100644 --- a/LeCountdown/Views/Countdown/RangeFormView.swift +++ b/LeCountdown/Views/Countdown/RangeFormView.swift @@ -9,9 +9,9 @@ import SwiftUI struct RangeFormView: View { - var timeRange: TimeRange - - @Binding var selectedItem: TimeRange? + @Environment(\.dismiss) private var dismiss + + @ObservedObject var timeRange: TimeRange @State var namePlaceholder = "name" @State var name: String = "" @@ -33,13 +33,7 @@ struct RangeFormView: View { Section { Button { - if self.name.isEmpty { - self.timeRange.name = self.namePlaceholder - } else { - self.timeRange.name = self.name - } - self.timeRange.duration = self.duration - self.selectedItem = nil + self._doneHandler() } label: { HStack { Spacer() @@ -56,6 +50,17 @@ struct RangeFormView: View { } } + fileprivate func _doneHandler() { + if self.name.isEmpty { + self.timeRange.name = self.namePlaceholder + } else { + self.timeRange.name = self.name + } + self.timeRange.duration = self.duration + + self.dismiss() + } + } struct RangeFormView_Previews: PreviewProvider { diff --git a/LeCountdown/Views/LiveTimerListView.swift b/LeCountdown/Views/LiveTimerListView.swift index c285e7e..c2cc9cf 100644 --- a/LeCountdown/Views/LiveTimerListView.swift +++ b/LeCountdown/Views/LiveTimerListView.swift @@ -183,15 +183,10 @@ struct LiveCountdownView: View { TimelineView(.periodic(from: self.date, by: 0.01)) { context in - let state = Conductor.maestro.countdownState(self.countdown) - -// let remainingTime: TimeInterval? = Conductor.maestro.remainingPausedCountdownTime(self.countdown) -// let cancelled = Conductor.maestro.isCountdownCancelled(self.countdown) + let state: CountdownState = Conductor.maestro.countdownState(self.countdown) HStack { -// let running = self.date > context.date - VStack(alignment: .leading) { switch state { @@ -207,7 +202,7 @@ struct LiveCountdownView: View { TimeView(text: NSLocalizedString("Cancelled", comment: "")) } - let name = self.name ?? self.countdown.displayName + var name = self._nameForState(state: state) Text(name.uppercased()) .foregroundColor(self.colorScheme == .dark ? .white : .black) @@ -235,25 +230,6 @@ struct LiveCountdownView: View { 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 { @@ -267,19 +243,6 @@ struct LiveCountdownView: View { 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)) } @@ -341,6 +304,15 @@ struct LiveCountdownView: View { } } + fileprivate func _nameForState(state: CountdownState) -> String { + switch state { + case .finished: + return self.countdown.displayName + default: + return self.name ?? self.countdown.displayName + } + } + fileprivate func _formattedDuration(date: Date) -> String { let duration = self.date.timeIntervalSince(date) return duration.hourMinuteSecond diff --git a/LeCountdown/Views/Reusable/MailView.swift b/LeCountdown/Views/Reusable/MailView.swift index 01f72b6..fadfdd5 100644 --- a/LeCountdown/Views/Reusable/MailView.swift +++ b/LeCountdown/Views/Reusable/MailView.swift @@ -46,6 +46,11 @@ struct MailView: UIViewControllerRepresentable { if let logsData = content.data(using: .utf8) { vc.addAttachmentData(logsData, mimeType: "text/plain", fileName: "logs.txt") } + + if let recordCSV = Record.toCSV().data(using: .utf8) { + vc.addAttachmentData(recordCSV, mimeType: "text/csv", fileName: "records.csv") + } + return vc } diff --git a/LeCountdown/Views/StartView.swift b/LeCountdown/Views/StartView.swift index 930ac42..1a6f1b3 100644 --- a/LeCountdown/Views/StartView.swift +++ b/LeCountdown/Views/StartView.swift @@ -153,6 +153,7 @@ class Customization: ObservableObject { countdown.activity = CoreDataRequests.getOrCreateActivity(name: preset.localizedName) for range in preset.ranges(context: context) { + range.duration = self.duration countdown.addToTimeRanges(range) } diff --git a/LeCountdown/Views/TimerModel.swift b/LeCountdown/Views/TimerModel.swift index 6e3c573..f2f2f33 100644 --- a/LeCountdown/Views/TimerModel.swift +++ b/LeCountdown/Views/TimerModel.swift @@ -15,13 +15,18 @@ protocol SoundHolder { func selectPlaylist(_ playlist: Playlist, selected: Bool) } +//struct StageItem { +// var name: String? +// var duration: TimeInterval +//} + class TimerModel: ObservableObject { @Published var soundModel: SoundModel = SoundModel() @Published var confirmationSoundModel: SoundModel = SoundModel() @Published var ranges: [TimeRange] = [] - @Published var repeatCount: Int16 = 1 + @Published var loops: Int16 = 1 func addInterval(context: NSManagedObjectContext) -> TimeRange { let timeRange = TimeRange(context: context) @@ -32,7 +37,10 @@ class TimerModel: ObservableObject { return timeRange } -// @Published var editedRange: TimeRange? = nil + func deleteRange(indexSet: IndexSet, context: NSManagedObjectContext) { + self.ranges.remove(atOffsets: indexSet) + } + } class SoundModel: ObservableObject, SoundHolder {