diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index fdde7a8..706234a 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -168,6 +168,8 @@ C4E5D68029B8FD93008E7465 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D67F29B8FD93008E7465 /* Store.swift */; }; C4E5D68229B93583008E7465 /* PVP_Stab_Oneshot_Bleep_Em.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4E5D68129B93583008E7465 /* PVP_Stab_Oneshot_Bleep_Em.wav */; }; C4E5D68429BB2425008E7465 /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D68329BB2425008E7465 /* SeparatorView.swift */; }; + C4E5D68629BB369E008E7465 /* TimersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D68529BB369E008E7465 /* TimersView.swift */; }; + C4E5D68829BB3FE1008E7465 /* SiriTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D68729BB3FE1008E7465 /* SiriTimerView.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 */; }; @@ -385,6 +387,8 @@ C4E5D67F29B8FD93008E7465 /* Store.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; C4E5D68129B93583008E7465 /* PVP_Stab_Oneshot_Bleep_Em.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = PVP_Stab_Oneshot_Bleep_Em.wav; sourceTree = ""; }; C4E5D68329BB2425008E7465 /* SeparatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorView.swift; sourceTree = ""; }; + C4E5D68529BB369E008E7465 /* TimersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimersView.swift; sourceTree = ""; }; + C4E5D68729BB3FE1008E7465 /* SiriTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiriTimerView.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 = ""; }; @@ -630,6 +634,7 @@ C498E59E298D4DEA00E90DE0 /* LiveTimerListView.swift */, C4BA2B03299A42EF00CB4FBA /* NewDataView.swift */, C4BA2B05299A8F8D00CB4FBA /* PresetsView.swift */, + C4E5D68529BB369E008E7465 /* TimersView.swift */, C498E5A2298D720600E90DE0 /* TestView.swift */, ); path = Views; @@ -777,6 +782,7 @@ C4F8B1D1298BF646005C86A5 /* PermissionAlertView.swift */, C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */, C4E5D68329BB2425008E7465 /* SeparatorView.swift */, + C4E5D68729BB3FE1008E7465 /* SiriTimerView.swift */, C4F8B165298A9ABB005C86A5 /* SoundImageFormView.swift */, C4BA2AD52993F62700CB4FBA /* SoundSelectionView.swift */, C4BA2ADA299549BC00CB4FBA /* TimerModel.swift */, @@ -1029,6 +1035,7 @@ C4E5D68429BB2425008E7465 /* SeparatorView.swift in Sources */, C4E5D67429B88734008E7465 /* DelaySoundPlayer.swift in Sources */, C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */, + C4E5D68829BB3FE1008E7465 /* SiriTimerView.swift in Sources */, C4E5D66629B73AED008E7465 /* StartTimerIntent.swift in Sources */, C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */, C498E5A5299152B400E90DE0 /* GreenCheckmarkView.swift in Sources */, @@ -1073,6 +1080,7 @@ C40FDB622992985C0042A390 /* TextToSpeechRecorder.swift in Sources */, C4BA2B43299FCB2B00CB4FBA /* RecordsView.swift in Sources */, C4BA2B2F299E69A000CB4FBA /* View+Extension.swift in Sources */, + C4E5D68629BB369E008E7465 /* TimersView.swift in Sources */, C4060DF7297AFEF2003FAB80 /* NewCountdownView.swift in Sources */, C4060DCC297AE73D003FAB80 /* LeCountdown.xcdatamodeld in Sources */, C4BA2B49299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */, diff --git a/LeCountdown/Views/ContentView.swift b/LeCountdown/Views/ContentView.swift index 5887135..5354de2 100644 --- a/LeCountdown/Views/ContentView.swift +++ b/LeCountdown/Views/ContentView.swift @@ -8,7 +8,6 @@ import SwiftUI import CoreData import Combine -import _AppIntents_SwiftUI class BoringContext : ObservableObject { @@ -16,7 +15,7 @@ class BoringContext : ObservableObject { @Published var error: Error? @Published var showDefaultAlert: Bool = false @Published var showPermissionAlert: Bool = false - + @Published var siriTimer: AbstractTimer? = nil } class TimerSpot : Identifiable, Equatable { @@ -49,87 +48,49 @@ struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \T.order, ascending: true)], - animation: .default) - private var timers: FetchedResults - @State private var isEditing: Bool = false @State private var tipsShown: Bool = false @State private var siriTipShown: Bool = false - - fileprivate let itemSpacing: CGFloat = 10.0 var body: some View { - let columns: [GridItem] = self._columns() - let timers: [AbstractTimer] = Array(self.timers) - - Group { - if timers.count > 0 { + VStack { + + TimersView(isEditing: self.$isEditing) + .environment(\.managedObjectContext, viewContext) + .environmentObject(self.boringContext) + + + // if !self.tipsShown, let tip = Preferences.tipToShow { + // TipView(tip: tip) { + // self._hideTip(tip) + // }.padding() + // } + + Spacer() + + SiriTimerView(timer: self.boringContext.siriTimer) + + if !conductor.liveTimers.isEmpty { - GeometryReader { reader in - let width: CGFloat = reader.size.width / CGFloat(columns.count) - 15.0 - - VStack { - - ScrollView { - - LazyVGrid( - columns: columns, - spacing: itemSpacing - ) { - - ReorderableForEach(items: timers) { timer in - - DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width, handler: { id in - self._handleSiriTips(timerId: id) - }) - .environment(\.managedObjectContext, viewContext) - .environmentObject(Conductor.maestro) - .environmentObject(boringContext) - } moveAction: { from, to in - self._reorder(from: from, to: to) - } - - } - }.padding(.horizontal, itemSpacing) - - // if !self.tipsShown, let tip = Preferences.tipToShow { - // TipView(tip: tip) { - // self._hideTip(tip) - // }.padding() - // } - - Spacer() - - SiriTipView(intent: StartTimerIntent(), isVisible: self.$siriTipShown) - .siriTipViewStyle(SiriTipViewStyle.dark).padding() - - if !conductor.liveTimers.isEmpty { - - HStack(alignment: .center) { - VolumeView().padding(12.0) - }.frame(width: 300.0, height: 40.0) - .background(Color(white: 0.9)) - .cornerRadius(16.0) - - LiveTimerListView() - .environment(\.managedObjectContext, viewContext) - .environmentObject(conductor) - .padding(.horizontal, 12.0) - .foregroundColor(.white) - .background(Color(white: 0.1)) - .cornerRadius(32.0, corners: [.topRight, .topLeft]) - } - } - } - } else { - Text("You'll find your timers here. Start by creating them on the left screen") + HStack(alignment: .center) { + VolumeView().padding(12.0) + }.frame(width: 300.0, height: 40.0) + .background(Color(white: 0.9)) + .cornerRadius(16.0) + + LiveTimerListView() + .environment(\.managedObjectContext, viewContext) + .environmentObject(conductor) + .padding(.horizontal, 12.0) + .foregroundColor(.white) + .background(Color(white: 0.1)) + .cornerRadius(32.0, corners: [.topRight, .topLeft]) } } + .navigationTitle("Home") .alert(boringContext.error?.localizedDescription ?? "missing error", isPresented: $boringContext.showDefaultAlert) { Button("OK", role: .cancel) { } @@ -159,22 +120,6 @@ struct ContentView: View { } - fileprivate func _columnCount() -> Int { - #if os(iOS) - if UIDevice.isPhoneIdiom { - return 2 - } else { - return 3 - } - #else - return 3 - #endif - } - - fileprivate func _columns() -> [GridItem] { - return (0..: View { self.tipsShown = true } - fileprivate func _reorder(from: IndexSet, to: Int) { - var timers: [AbstractTimer] = Array(self.timers) - timers.move(fromOffsets: from, toOffset: to) - for (i, countdown) in timers.enumerated() { - countdown.order = Int16(i) - } - do { - try viewContext.save() - } catch { - Logger.error(error) - self.boringContext.error = error - } - } - fileprivate func _askPermissions() { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { success, error in print("requestAuthorization > success = \(success), error = \(String(describing: error))") } } - fileprivate func _handleSiriTips(timerId: String) { - if !Preferences.timerSiriTips.contains(timerId) { - self.siriTipShown = true - Preferences.timerSiriTips.insert(timerId) - } - } - fileprivate func _performActionIfPossible(url: URL) { // hide new window if launching a timer diff --git a/LeCountdown/Views/DialView.swift b/LeCountdown/Views/DialView.swift index 9a3026c..99b9dfc 100644 --- a/LeCountdown/Views/DialView.swift +++ b/LeCountdown/Views/DialView.swift @@ -17,11 +17,9 @@ struct DialView: View { @State var timer: AbstractTimer var isEditingBinding: Binding -// @State var showSilentModeAlert: Bool = false - var frameSize: CGFloat - var handler: ((String) -> ())? = nil + var handler: ((AbstractTimer) -> ())? = nil var body: some View { ZStack { @@ -34,12 +32,6 @@ struct DialView: View { case false: Button { self._launchTimer() - -// if Preferences.hideSilentModeAlerts { -// self._launchTimer() -// } else { -// self.showSilentModeAlert = true -// } } label: { VStack { Spacer() @@ -133,7 +125,7 @@ struct DialView: View { fileprivate func _launchTimer() { - self.handler?(self.timer.stringId) + self.handler?(self.timer) TimerRouter.performAction(timer: self.timer) { result in switch result { diff --git a/LeCountdown/Views/Reusable/SiriTimerView.swift b/LeCountdown/Views/Reusable/SiriTimerView.swift new file mode 100644 index 0000000..64a081f --- /dev/null +++ b/LeCountdown/Views/Reusable/SiriTimerView.swift @@ -0,0 +1,40 @@ +// +// SiriTimerView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 10/03/2023. +// + +import SwiftUI +import _AppIntents_SwiftUI + +struct SiriTimerView: View { + + var timer: AbstractTimer? = nil + + @State var isVisible = true + + var body: some View { + if self.timer != nil { + SiriTipView(intent: self.intent(), isVisible: self.$isVisible) + .siriTipViewStyle(SiriTipViewStyle.dark).padding() + } else { + Text("no siri view") + } + } + + fileprivate func intent() -> StartTimerIntent { + let intent = StartTimerIntent() + if let timer { + intent.timer = TimerIdentifierAppEntity(id: timer.stringId, displayString: timer.displayName) + } + return intent + } + +} + +struct SiriTimerView_Previews: PreviewProvider { + static var previews: some View { + SiriTimerView() + } +} diff --git a/LeCountdown/Views/TimersView.swift b/LeCountdown/Views/TimersView.swift new file mode 100644 index 0000000..353a9bc --- /dev/null +++ b/LeCountdown/Views/TimersView.swift @@ -0,0 +1,107 @@ +// +// TimersView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 10/03/2023. +// + +import SwiftUI + +struct TimersView: View { + + @EnvironmentObject var boringContext: BoringContext + + @Environment(\.managedObjectContext) private var viewContext + + @Binding var isEditing: Bool + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \AbstractTimer.order, ascending: true)], + animation: .default) + private var timers: FetchedResults + + fileprivate let itemSpacing: CGFloat = 10.0 + + var body: some View { + + GeometryReader { reader in + + let columns: [GridItem] = self._columns() + let width: CGFloat = reader.size.width / CGFloat(columns.count) - 15.0 + let abstractTimers: [AbstractTimer] = Array(self.timers) + + if abstractTimers.count > 0 { + + ScrollView { + + LazyVGrid( + columns: columns, + spacing: itemSpacing + ) { + + ReorderableForEach(items: abstractTimers) { timer in + + DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width, handler: { timer in + self._handleSiriTips(timer: timer) + }) + .environment(\.managedObjectContext, viewContext) + .environmentObject(Conductor.maestro) + .environmentObject(boringContext) + } moveAction: { from, to in + self._reorder(from: from, to: to) + } + + } + } + } else { + Text("You'll find your timers here. Start by creating them on the left screen") + } + }.padding(.horizontal, itemSpacing) + + } + + fileprivate func _reorder(from: IndexSet, to: Int) { + var timers: [AbstractTimer] = Array(self.timers) + timers.move(fromOffsets: from, toOffset: to) + for (i, countdown) in timers.enumerated() { + countdown.order = Int16(i) + } + do { + try viewContext.save() + } catch { + Logger.error(error) + self.boringContext.error = error + } + } + + fileprivate func _handleSiriTips(timer: AbstractTimer) { + let timerId = timer.stringId + if !Preferences.timerSiriTips.contains(timerId) { + self.boringContext.siriTimer = timer + Preferences.timerSiriTips.insert(timerId) + } + } + + fileprivate func _columnCount() -> Int { + #if os(iOS) + if UIDevice.isPhoneIdiom { + return 2 + } else { + return 3 + } + #else + return 3 + #endif + } + + fileprivate func _columns() -> [GridItem] { + return (0..