From f1e082e3e9044ddf05e91909501b547189dbd3ad Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 13 Feb 2023 18:56:34 +0100 Subject: [PATCH] New navigation + presets + timers custom sort --- LeCountdown.xcodeproj/project.pbxproj | 14 ++ LeCountdown/Conductor.swift | 13 +- LeCountdown/Info.plist | 2 + LeCountdown/LeCountdownApp.swift | 38 +++-- LeCountdown/Utils/AppleMusicPlayer.swift | 38 +++++ .../Views/Components/SoundImageFormView.swift | 12 +- LeCountdown/Views/ContentView.swift | 140 ++++++++++++++++-- .../Views/Countdown/CountdownDialView.swift | 1 - .../Views/Countdown/NewCountdownView.swift | 120 +++++++++------ LeCountdown/Views/DialView.swift | 26 ++-- LeCountdown/Views/NewDataView.swift | 67 +++++++++ LeCountdown/Views/PresetsView.swift | 116 +++++++++++++++ LeCountdown/Views/ReorderableForEach.swift | 3 +- .../Views/Stopwatch/NewStopwatchView.swift | 19 ++- .../Views/Stopwatch/StopwatchDialView.swift | 5 +- 15 files changed, 513 insertions(+), 101 deletions(-) create mode 100644 LeCountdown/Utils/AppleMusicPlayer.swift create mode 100644 LeCountdown/Views/NewDataView.swift create mode 100644 LeCountdown/Views/PresetsView.swift diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index 940e84f..486e813 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -85,6 +85,9 @@ C4BA2AF72996A4EF00CB4FBA /* CustomSound+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */; }; C4BA2AF82996A4F000CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEE2996A11900CB4FBA /* CustomSound+CoreDataProperties.swift */; }; C4BA2AF92996A4F000CB4FBA /* CustomSound+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */; }; + C4BA2AFD299A3A3700CB4FBA /* AppleMusicPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AFC299A3A3700CB4FBA /* AppleMusicPlayer.swift */; }; + C4BA2B04299A42EF00CB4FBA /* NewDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B03299A42EF00CB4FBA /* NewDataView.swift */; }; + C4BA2B06299A8F8D00CB4FBA /* PresetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B05299A8F8D00CB4FBA /* PresetsView.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 */; }; @@ -264,6 +267,10 @@ C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomSound+CoreDataClass.swift"; sourceTree = ""; }; C4BA2AEE2996A11900CB4FBA /* CustomSound+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomSound+CoreDataProperties.swift"; sourceTree = ""; }; C4BA2AEF2996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AbstractSoundTimer+CoreDataProperties.swift"; sourceTree = ""; }; + C4BA2AFC299A3A3700CB4FBA /* AppleMusicPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleMusicPlayer.swift; sourceTree = ""; }; + C4BA2AFE299A3A9E00CB4FBA /* MusicKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MusicKit.framework; path = System/Library/Frameworks/MusicKit.framework; sourceTree = SDKROOT; }; + C4BA2B03299A42EF00CB4FBA /* NewDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDataView.swift; sourceTree = ""; }; + C4BA2B05299A8F8D00CB4FBA /* PresetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetsView.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 = ""; }; @@ -412,6 +419,7 @@ C438C7CF2981216200BF3EF9 /* Frameworks */ = { isa = PBXGroup; children = ( + C4BA2AFE299A3A9E00CB4FBA /* MusicKit.framework */, C438C7D02981216200BF3EF9 /* WidgetKit.framework */, C438C7D22981216200BF3EF9 /* SwiftUI.framework */, C438C7F129812BB200BF3EF9 /* Intents.framework */, @@ -473,6 +481,7 @@ C4060DF4297AE9A7003FAB80 /* TimeInterval+Extensions.swift */, C4742B5E2984205000D5D950 /* ViewModifiers.swift */, C40FDB612992985C0042A390 /* TextToSpeechRecorder.swift */, + C4BA2AFC299A3A3700CB4FBA /* AppleMusicPlayer.swift */, ); path = Utils; sourceTree = ""; @@ -487,6 +496,8 @@ C4060DC1297AE73B003FAB80 /* ContentView.swift */, C4F8B1CF298BF2E2005C86A5 /* DialView.swift */, C498E59E298D4DEA00E90DE0 /* LiveTimerListView.swift */, + C4BA2B03299A42EF00CB4FBA /* NewDataView.swift */, + C4BA2B05299A8F8D00CB4FBA /* PresetsView.swift */, C438C80E29828B8600BF3EF9 /* RecordsView.swift */, C498E5A2298D720600E90DE0 /* TestView.swift */, C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */, @@ -810,6 +821,7 @@ C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */, C4F8B1BF298ACA0B005C86A5 /* StopwatchDialView.swift in Sources */, C438C807298195E600BF3EF9 /* Model+Extensions.swift in Sources */, + C4BA2B04299A42EF00CB4FBA /* NewDataView.swift in Sources */, C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */, C4F8B183298AC234005C86A5 /* Stopwatch+CoreDataProperties.swift in Sources */, C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */, @@ -818,12 +830,14 @@ C4F8B1B8298AC81D005C86A5 /* CountdownDialView.swift in Sources */, C445FA922987CC8A0054D761 /* Sound.swift in Sources */, C4F8B1D0298BF2E2005C86A5 /* DialView.swift in Sources */, + C4BA2AFD299A3A3700CB4FBA /* AppleMusicPlayer.swift in Sources */, C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */, C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */, C4BA2AF12996A11900CB4FBA /* CustomSound+CoreDataClass.swift in Sources */, C4060DC2297AE73B003FAB80 /* ContentView.swift in Sources */, C438C7C12980228B00BF3EF9 /* CountdownScheduler.swift in Sources */, C498E5A3298D720600E90DE0 /* TestView.swift in Sources */, + C4BA2B06299A8F8D00CB4FBA /* PresetsView.swift in Sources */, C445FA8F2987B83B0054D761 /* SoundPlayer.swift in Sources */, C4F8B1A7298AC2FC005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */, C438C7C929803CA000BF3EF9 /* AppDelegate.swift in Sources */, diff --git a/LeCountdown/Conductor.swift b/LeCountdown/Conductor.swift index 23c021e..0d1ff7c 100644 --- a/LeCountdown/Conductor.swift +++ b/LeCountdown/Conductor.swift @@ -85,9 +85,16 @@ class Conductor: ObservableObject { return LiveTimer(id: $0, date: $1) } for liveStopwatch in liveStopwatches { - if self.liveTimers.first(where: { $0.id == liveStopwatch.id }) == nil { + if let livetimer = self.liveTimers.first(where: { $0.id == liveStopwatch.id }) { + self.liveTimers.replace([livetimer], with: [liveStopwatch]) + } else { self.liveTimers.append(liveStopwatch) } +// +// +// if self.liveTimers.first(where: { $0.id == liveStopwatch.id }) == nil { +// self.liveTimers.append(liveStopwatch) +// } } } @@ -227,9 +234,9 @@ class Conductor: ObservableObject { do { let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent) - print("Requested a Countdown Live Activity: \(String(describing: liveActivity.id)).") + print("Requested a Live Activity: \(String(describing: liveActivity.id)).") } catch (let error) { - print("Error requesting countdown Live Activity \(error.localizedDescription).") + print("Error requesting Live Activity \(error.localizedDescription).") } // self._scheduleAppRefresh(countdown: countdown) diff --git a/LeCountdown/Info.plist b/LeCountdown/Info.plist index 1e9948b..472acae 100644 --- a/LeCountdown/Info.plist +++ b/LeCountdown/Info.plist @@ -2,6 +2,8 @@ + NSAppleMusicUsageDescription + NSAppleMusicUsageDescription BGTaskSchedulerPermittedIdentifiers com.staxriver.lecountdown.refresh diff --git a/LeCountdown/LeCountdownApp.swift b/LeCountdown/LeCountdownApp.swift index 272b502..e8ae9b0 100644 --- a/LeCountdown/LeCountdownApp.swift +++ b/LeCountdown/LeCountdownApp.swift @@ -25,31 +25,29 @@ struct LeCountdownApp: App { init() { UITabBar.appearance().backgroundColor = UIColor(white: 0.96, alpha: 1.0) + self._registerBackgroundRefreshes() } var body: some Scene { WindowGroup { - TabView(selection: $tabSelection) { - ContentView() - .environment(\.managedObjectContext, persistenceController.container.viewContext) - .environmentObject(Conductor.maestro) - .tabItem { Label("Countdown", systemImage: "timer") } - .tag(1) - ContentView() - .environment(\.managedObjectContext, persistenceController.container.viewContext) - .environmentObject(Conductor.maestro) - .tabItem { Label("Stopwatch", systemImage: "stopwatch") } - .tag(2) -// ContentView() -// .environment(\.managedObjectContext, persistenceController.container.viewContext) -// .environmentObject(Conductor.maestro) -// .tabItem { Label("Alarm", systemImage: "alarm") } -// .tag(3) - RecordsView().environment(\.managedObjectContext, persistenceController.container.viewContext) - .tabItem { Label("Stats", systemImage: "chart.bar.fill") } - .tag(4) + NavigationStack { + TabView(selection: $tabSelection) { + PresetsView(tabSelection: $tabSelection) + .environment(\.managedObjectContext, persistenceController.container.viewContext) + .tabItem { Label("Presets", systemImage: "globe") } + .tag(0) + ContentView() + .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environmentObject(Conductor.maestro) + .tabItem { Label("Home", systemImage: "clock.fill") } + .tag(1) + RecordsView().environment(\.managedObjectContext, persistenceController.container.viewContext) + .tabItem { Label("Stats", systemImage: "chart.bar.fill") } + .tag(2) + + }.tabViewStyle(.page) } .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in self._willEnterForegroundNotification() @@ -61,6 +59,7 @@ struct LeCountdownApp: App { print("open URL = \(url)") self._performActionIfPossible(url: url) } + } } @@ -69,7 +68,6 @@ struct LeCountdownApp: App { } fileprivate func _onAppear() { - self._registerBackgroundRefreshes() // let voices = AVSpeechSynthesisVoice.speechVoices() // let grouped = Dictionary(grouping: voices, by: { $0.language }) diff --git a/LeCountdown/Utils/AppleMusicPlayer.swift b/LeCountdown/Utils/AppleMusicPlayer.swift new file mode 100644 index 0000000..5c00387 --- /dev/null +++ b/LeCountdown/Utils/AppleMusicPlayer.swift @@ -0,0 +1,38 @@ +// +// AppleMusicPlayer.swift +// LeCountdown +// +// Created by Laurent Morvillier on 13/02/2023. +// + +import Foundation +import MediaPlayer + +@objc class AppleMusicPlayer : NSObject, MPMediaPickerControllerDelegate { + +// func play() { +// +// let musicPlayer = MPMusicPlayerApplicationController.applicationQueuePlayer +// musicPlayer.setQueue(with: .songs()) +// +// } +// +// func showMediaPicker(source: UIView) { +// let controller = MPMediaPickerController(mediaTypes: .music) +// controller.allowsPickingMultipleItems = true +// controller.popoverPresentationController?.sourceView = source +// controller.delegate = self +//// present(controller, animated: true) +// } +// +// func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) { +// +// +//// let p = MPMediaItemCollection(items: [MPMediaItem]) +// +// let i = MPMediaItem() +// i. +// +// } + +} diff --git a/LeCountdown/Views/Components/SoundImageFormView.swift b/LeCountdown/Views/Components/SoundImageFormView.swift index 9263787..d8c98df 100644 --- a/LeCountdown/Views/Components/SoundImageFormView.swift +++ b/LeCountdown/Views/Components/SoundImageFormView.swift @@ -69,12 +69,14 @@ struct SoundImageFormView : View { } } - Picker("Repeat Count", selection: self.repeatCountBinding!) { - ForEach(0..<6) { - let count = Int16($0) - Text("\(count)").tag(count) + if self.repeatCountBinding != nil { + Picker("Repeat Count", selection: self.repeatCountBinding!) { + ForEach(0..<6) { + let count = Int16($0) + Text("\(count)").tag(count) + } + } - } } diff --git a/LeCountdown/Views/ContentView.swift b/LeCountdown/Views/ContentView.swift index 379541d..62d4712 100644 --- a/LeCountdown/Views/ContentView.swift +++ b/LeCountdown/Views/ContentView.swift @@ -17,6 +17,35 @@ class BoringContext : ObservableObject { } +class TimerSpot : Identifiable, Equatable { + + var id: Int16 { return self.order } + + var order: Int16 + var timer: AbstractTimer? + + init(order: Int16, timer: AbstractTimer? = nil) { + self.order = order + self.timer = timer + } + + func setOrder(order: Int16) { + self.order = order + self.timer?.order = order + } + + static func == (lhs: TimerSpot, rhs: TimerSpot) -> Bool { + return lhs.order == rhs.order && lhs.timer?.stringId == rhs.timer?.stringId + } + +} + +class TimersModel : ObservableObject { + + @Published var spots: [TimerSpot] = [] + +} + struct ContentView: View { @StateObject var boringContext: BoringContext = BoringContext() @@ -24,12 +53,25 @@ struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext + @StateObject fileprivate var model: TimersModel = TimersModel() + @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \T.order, ascending: true)], animation: .default) - private var timers: FetchedResults + private var timers: FetchedResults { + didSet { + self._buildItemsList() + } + } - @State private var isEditing: Bool = false + @State private var isEditing: Bool = false { + didSet { + if self.isEditing == false { + self._saveOrder() + } + self._buildItemsList() + } + } fileprivate let itemSpacing: CGFloat = 10.0 @@ -57,16 +99,55 @@ struct ContentView: View { spacing: itemSpacing ) { - ReorderableForEach(items: timersArray) { timer in + if !self.isEditing { + + ForEach(self.model.spots) { spot in + if let timer = spot.timer { + DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width) + .environment(\.managedObjectContext, viewContext) + .environmentObject(Conductor.maestro) + .environmentObject(boringContext) + + } else { + Color.clear + .frame(width: width, height: 80.0) + .cornerRadius(20.0) + } + } + + } else { - DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width) - .environment(\.managedObjectContext, viewContext) - .environmentObject(Conductor.maestro) - .environmentObject(boringContext) + ReorderableForEach(items: self.model.spots) { spot in + + if let timer = spot.timer { + DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width) + .environment(\.managedObjectContext, viewContext) + .environmentObject(Conductor.maestro) + .environmentObject(boringContext) - } moveAction: { from, to in - self._reorder(from: from, to: to) + } else { + + Color(white: 0.9) + .frame(width: width, height: 80.0) + .cornerRadius(20.0) + } + + } moveAction: { from, to in + self._reorderSpots(from: from, to: to) + } } + + +// ReorderableForEach(items: timersArray) { timer in +// +// DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width) +// .environment(\.managedObjectContext, viewContext) +// .environmentObject(Conductor.maestro) +// .environmentObject(boringContext) +// +// } moveAction: { from, to in +// self._reorder(from: from, to: to) +// } } }.padding(.horizontal, itemSpacing) @@ -75,11 +156,12 @@ struct ContentView: View { .environment(\.managedObjectContext, viewContext) .environmentObject(conductor) .background(Color(white: 0.9)) + .padding(.bottom, 40.0) .cornerRadius(16.0, corners: [.topRight, .topLeft]) } } } - .navigationTitle("\(String(describing: T.self))") +// .navigationTitle("Yeah!") .alert(boringContext.error?.localizedDescription ?? "missing error", isPresented: $boringContext.showDefaultAlert) { Button("OK", role: .cancel) { } } @@ -111,6 +193,7 @@ struct ContentView: View { } } .onAppear { + self._buildItemsList() self._askPermissions() } .onOpenURL { url in @@ -120,6 +203,33 @@ struct ContentView: View { } + fileprivate func _buildItemsList() { + + var spots: [TimerSpot] = [] + + let more: Int = self.isEditing ? 20 : 0 // add 20 empty spots when editing + + let count = max(self.timers.count, Int(self.timers.last?.order ?? 0) + 1) + more + + for i in 0..: View { case is Stopwatch.Type: NewStopwatchView(isPresented: isPresented) default: - Text("missing new view") + NewDataView(isPresented: isPresented) } } // MARK: - Business + fileprivate func _reorderSpots(from: IndexSet, to: Int) { + var spots: [TimerSpot] = self.model.spots + spots.move(fromOffsets: from, toOffset: to) + self.model.spots = spots + } + fileprivate func _reorder(from: IndexSet, to: Int) { var timers: [AbstractTimer] = self.timersArray timers.move(fromOffsets: from, toOffset: to) @@ -148,7 +264,7 @@ struct ContentView: View { do { try viewContext.save() } catch { - boringContext.error = error + self.boringContext.error = error } } diff --git a/LeCountdown/Views/Countdown/CountdownDialView.swift b/LeCountdown/Views/Countdown/CountdownDialView.swift index 765a41a..2defdea 100644 --- a/LeCountdown/Views/Countdown/CountdownDialView.swift +++ b/LeCountdown/Views/Countdown/CountdownDialView.swift @@ -19,7 +19,6 @@ struct CountdownDialView: View { VStack(alignment: .leading) { Text(countdown.activity?.name?.uppercased() ?? "") Text(countdown.duration.minuteSecond) - Spacer() } Spacer() } diff --git a/LeCountdown/Views/Countdown/NewCountdownView.swift b/LeCountdown/Views/Countdown/NewCountdownView.swift index e63ed6b..f84c2ad 100644 --- a/LeCountdown/Views/Countdown/NewCountdownView.swift +++ b/LeCountdown/Views/Countdown/NewCountdownView.swift @@ -31,6 +31,7 @@ struct CountdownEditView : View { @StateObject var model: TimerModel = TimerModel() var countdown: Countdown? = nil + var preset: Preset? = nil @Binding var isPresented: Bool @@ -48,26 +49,35 @@ struct CountdownEditView : View { @State var errorShown: Bool = false @State var error: Error? = nil + var tabSelection: Binding? = nil + @FocusState private var textFieldIsFocused: Bool @FetchRequest(sortDescriptors: []) - private var countdowns: FetchedResults + private var timers: FetchedResults - @State var _isAdding: Bool = false + @State var _isNewCountdown: Bool = false // false if editing an existing countdown @State var _hasLoaded = false @Environment(\.isPresented) var envIsPresented -// -// init() { -// self._load() -// } + + init(isPresented: Binding, countdown: Countdown? = nil) { + _isPresented = isPresented + self.countdown = countdown + } + + init(isPresented: Binding, preset: Preset, tabSelection: Binding) { + _isPresented = isPresented + self.preset = preset + self.tabSelection = tabSelection + } var body: some View { NavigationStack { Rectangle() .frame(width: 0.0, height: 0.0) .onChange(of: envIsPresented) { newValue in - if !newValue && !self._isAdding { + if !newValue && !self._isNewCountdown { self._save() // save when leaving an edit screen } } @@ -109,7 +119,7 @@ struct CountdownEditView : View { Text(error?.localizedDescription ?? "error") }) .toolbar { - if self._isAdding { + if self._isNewCountdown { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { self._cancel() @@ -133,7 +143,7 @@ struct CountdownEditView : View { Button { textFieldIsFocused = false } label: { - Image(systemName: "checkmark") + Image(systemName: "keyboard.chevron.compact.down") } } } @@ -146,41 +156,57 @@ struct CountdownEditView : View { fileprivate func _onAppear() { - self._isAdding = (self.countdown == nil) + self._isNewCountdown = (self.countdown == nil) if !self._hasLoaded { - self._load() + if let countdown { + self._loadCountdown(countdown) + } else if let preset { + self._loadPreset(preset) + } self._hasLoaded = true } + } - fileprivate func _load() { - if let countdown { - - let minutes = Int(countdown.duration / 60.0) - let seconds = countdown.duration - Double(minutes * 60) - - if minutes > 0 { - self.minutesString = self._numberFormatter.string(from: NSNumber(value: minutes)) ?? "" - } + fileprivate func _loadPreset(_ preset: Preset) { + self.nameString = preset.localizedName + + let nf = NumberFormatter() + let minutes = Int(preset.duration / 60.0) + self.minutesString = nf.string(from: NSNumber(value: minutes)) ?? "" + let seconds = Int(preset.duration) - minutes * 60 + self.secondsString = nf.string(from: NSNumber(value: seconds)) ?? "" + + self.model.sounds = preset.sound - if seconds > 0 { - self.secondsString = self._numberFormatter.string(from: NSNumber(value: seconds)) ?? "" - } - - if let name = countdown.activity?.name, !name.isEmpty { - self.nameString = name - } - - self.model.sounds = countdown.sounds - -// if let sound = Sound(rawValue: Int(countdown.sound)) { -// self.sound = sound -// } - self.soundRepeatCount = countdown.repeatCount - - if let image = countdown.image, let coolpic = CoolPic(rawValue: image) { - self.image = coolpic - } + } + + fileprivate func _loadCountdown(_ countdown: Countdown) { + + let minutes = Int(countdown.duration / 60.0) + let seconds = countdown.duration - Double(minutes * 60) + + if minutes > 0 { + self.minutesString = self._numberFormatter.string(from: NSNumber(value: minutes)) ?? "" + } + + if seconds > 0 { + self.secondsString = self._numberFormatter.string(from: NSNumber(value: seconds)) ?? "" + } + + if let name = countdown.activity?.name, !name.isEmpty { + self.nameString = name + } + + self.model.sounds = countdown.sounds + + // if let sound = Sound(rawValue: Int(countdown.sound)) { + // self.sound = sound + // } + self.soundRepeatCount = countdown.repeatCount + + if let image = countdown.image, let coolpic = CoolPic(rawValue: image) { + self.image = coolpic } } @@ -209,9 +235,14 @@ struct CountdownEditView : View { } cd.duration = self._minutes * 60.0 + self._seconds - if self._isAdding { - let max = self.countdowns.map { $0.order }.max() ?? 0 - cd.order = max + 1 + if self._isNewCountdown { + let max: Int16 + if let maxOrder = self.timers.map({ $0.order }).max() { + max = maxOrder + 1 + } else { + max = 0 + } + cd.order = max } cd.image = self.image.rawValue @@ -251,11 +282,16 @@ struct CountdownEditView : View { } fileprivate func _popOrDismiss() { - if self._isAdding { + if self._isNewCountdown { self.isPresented = false } else { dismiss() } + + if self.preset != nil { + self.tabSelection?.wrappedValue = 1 + } + } fileprivate func _delete() { diff --git a/LeCountdown/Views/DialView.swift b/LeCountdown/Views/DialView.swift index 38ad5d7..115c1a4 100644 --- a/LeCountdown/Views/DialView.swift +++ b/LeCountdown/Views/DialView.swift @@ -29,27 +29,35 @@ struct DialView: View { Button { self._launchTimer() } label: { - self._dialView().padding() + VStack { + Spacer() + self._dialView().padding(.horizontal) + Spacer() + } } case true: - self._dialView().padding() - VStack { + Spacer() + self._dialView().padding(.horizontal) + Spacer() + } + + HStack { Spacer() NavigationLink { self._editView(timer: timer, isPresented: $boringContext.isShowingNewData) } label: { Image(systemName: "gearshape.fill") - .font(.system(size: 50.0)) - .padding(30.0) + .font(.system(size: 30.0)) + .padding(.horizontal) .foregroundColor(Color.accentColor) } } } } - .frame(width: frameSize, height: frameSize) - .cornerRadius(40.0) + .frame(width: frameSize, height: 80.0) + .cornerRadius(20.0) } @ViewBuilder @@ -78,7 +86,7 @@ struct DialView: View { fileprivate func _editView(timer: AbstractTimer, isPresented: Binding) -> some View { switch timer { case let countdown as Countdown: - CountdownEditView(countdown: countdown, isPresented: isPresented) + CountdownEditView(isPresented: isPresented, countdown: countdown) .environment(\.managedObjectContext, viewContext) case let alarm as Alarm: AlarmEditView(alarm: alarm, isPresented: isPresented) @@ -117,7 +125,7 @@ struct DialView_Previews: PreviewProvider { DialView( timer: Countdown.fake(context: PersistenceController.preview.container.viewContext), - isEditingBinding: .constant(false), frameSize: 150.0) + isEditingBinding: .constant(true), frameSize: 150.0) .environmentObject(Conductor.maestro) .environmentObject(BoringContext()) diff --git a/LeCountdown/Views/NewDataView.swift b/LeCountdown/Views/NewDataView.swift new file mode 100644 index 0000000..a7b2852 --- /dev/null +++ b/LeCountdown/Views/NewDataView.swift @@ -0,0 +1,67 @@ +// +// NewDataView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 13/02/2023. +// + +import SwiftUI + +enum DataTab: Int, Identifiable, CaseIterable { + + case countdown + case stopwatch + + var id: Int { return self.rawValue } + + var localizedString: String { + switch self { + case .countdown: return NSLocalizedString("Coundown", comment: "") + case .stopwatch: return NSLocalizedString("Stopwatch", comment: "") + } + } +} + + +struct NewDataView: View { + + @Environment(\.managedObjectContext) private var viewContext + + @Binding var isPresented: Bool + + @State var selection: Int = 0 + + var body: some View { + + NavigationStack { + + VStack { + + Picker("", selection: $selection) { + ForEach(DataTab.allCases) { tab in + Text(tab.localizedString) + } + } + .pickerStyle(.segmented) + .padding() + + TabView(selection: $selection) { + NewCountdownView(isPresented: $isPresented) + .tag(0) + .environment(\.managedObjectContext, viewContext) + NewStopwatchView(isPresented: $isPresented) + .tag(1) + .environment(\.managedObjectContext, viewContext) + } + } + } + + } +} + +struct NewDataView_Previews: PreviewProvider { + static var previews: some View { + NewDataView(isPresented: .constant(true)) + .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/LeCountdown/Views/PresetsView.swift b/LeCountdown/Views/PresetsView.swift new file mode 100644 index 0000000..1f65da7 --- /dev/null +++ b/LeCountdown/Views/PresetsView.swift @@ -0,0 +1,116 @@ +// +// PresetsView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 13/02/2023. +// + +import SwiftUI + +enum PresetSection: Int, Identifiable, CaseIterable { + var id: Int { return self.rawValue } + + case cooking + case workout + case meditation + + var presets: [Preset] { + switch self { + case .cooking: return [.softBoiled, .mediumBoiledEggs, .hardBoiledEggs] + case .workout: return [] + case .meditation: return [] + } + } + + var localizedName: String { + switch self { + case .cooking: return NSLocalizedString("Cooking", comment: "") + case .workout: return NSLocalizedString("Workout", comment: "") + case .meditation: return NSLocalizedString("Meditation", comment: "") + } + } +} + +enum Preset: Int, Identifiable, CaseIterable { + var id: Int { return self.rawValue } + + case softBoiled + case mediumBoiledEggs + case hardBoiledEggs + + var localizedName: String { + switch self { + case .hardBoiledEggs: return NSLocalizedString("Hard boiled eggs", comment: "") + case .softBoiled: return NSLocalizedString("Soft boiled eggs", comment: "") + case .mediumBoiledEggs: return NSLocalizedString("Medium boiled eggs", comment: "") + } + } + + var duration: TimeInterval { + switch self { + case .softBoiled: return 3 * 60 + case .mediumBoiledEggs: return 6 * 60 + case .hardBoiledEggs: return 10 * 60 + } + } + + var sound: Set { + switch self { + case .softBoiled: return [] + case .mediumBoiledEggs: return [] + case .hardBoiledEggs: return [] + } + } + +} + +class PresetModel : ObservableObject { + + @Published var selectedPreset: Preset = Preset.hardBoiledEggs + +} + +struct PresetsView: View { + + @Environment(\.managedObjectContext) private var viewContext + + @StateObject var model: PresetModel = PresetModel() + + @State var isPresented: Bool = false + + var tabSelection: Binding + + var body: some View { + + NavigationStack { + List { + ForEach(PresetSection.allCases) { section in + Section(header: Text(section.localizedName)) { + ForEach(section.presets) { preset in + + Button { + self.model.selectedPreset = preset + self.isPresented = true + } label: { + Text(preset.localizedName) + } + + } + } + } + } + .sheet(isPresented: $isPresented, content: { + CountdownEditView(isPresented: $isPresented, preset: self.model.selectedPreset, tabSelection: self.tabSelection) + .environment(\.managedObjectContext, viewContext) + }) + .navigationTitle("Presets") + } + + } +} + +struct PresetsView_Previews: PreviewProvider { + static var previews: some View { + PresetsView(tabSelection: .constant(0)) + } +} diff --git a/LeCountdown/Views/ReorderableForEach.swift b/LeCountdown/Views/ReorderableForEach.swift index 158f7ab..299d5b2 100644 --- a/LeCountdown/Views/ReorderableForEach.swift +++ b/LeCountdown/Views/ReorderableForEach.swift @@ -99,11 +99,12 @@ struct ForEachGridView_Previews: PreviewProvider { static var previews: some View { - LazyVGrid(columns: [GridItem(.fixed(50.0)), GridItem(.fixed(side))], spacing: 10.0) { + LazyVGrid(columns: [GridItem(.fixed(50.0)), GridItem(.fixed(side))], spacing: 0.0) { ReorderableForEach(items: gridData) { data in Text(data.stringId) .frame(width: side, height: side) .background(.cyan) + .padding() } moveAction: { from, to in gridData.move(fromOffsets: from, toOffset: to) } diff --git a/LeCountdown/Views/Stopwatch/NewStopwatchView.swift b/LeCountdown/Views/Stopwatch/NewStopwatchView.swift index e17cbf4..1b1564a 100644 --- a/LeCountdown/Views/Stopwatch/NewStopwatchView.swift +++ b/LeCountdown/Views/Stopwatch/NewStopwatchView.swift @@ -50,14 +50,14 @@ struct StopwatchEditView: View { @State var error: Error? = nil @FocusState private var textFieldIsFocused: Bool - -// @FetchRequest(sortDescriptors: []) -// private var countdowns: FetchedResults @State var _isAdding: Bool = false @Environment(\.isPresented) var envIsPresented + @FetchRequest(sortDescriptors: []) + private var timers: FetchedResults + var body: some View { NavigationStack { Rectangle() @@ -125,7 +125,7 @@ struct StopwatchEditView: View { Button { textFieldIsFocused = false } label: { - Image(systemName: "checkmark") + Image(systemName: "keyboard.chevron.compact.down") } } } @@ -222,6 +222,17 @@ struct StopwatchEditView: View { sw = Stopwatch(context: viewContext) } + if self._isAdding { + let max: Int16 + if let maxOrder = self.timers.map({ $0.order }).max() { + max = maxOrder + 1 + } else { + max = 0 + } + sw.order = max + } + + // if self._isAdding { // let max = self.countdowns.map { $0.order }.max() ?? 0 // cd.order = max + 1 diff --git a/LeCountdown/Views/Stopwatch/StopwatchDialView.swift b/LeCountdown/Views/Stopwatch/StopwatchDialView.swift index eb6f52f..d26f664 100644 --- a/LeCountdown/Views/Stopwatch/StopwatchDialView.swift +++ b/LeCountdown/Views/Stopwatch/StopwatchDialView.swift @@ -15,10 +15,7 @@ struct StopwatchDialView: View { var body: some View { HStack { - VStack(alignment: .leading) { - Text(stopwatch.activity?.name?.uppercased() ?? "") - Spacer() - } + Text(stopwatch.activity?.name?.uppercased() ?? "") Spacer() } }