diff --git a/LaunchIntents/IntentHandler.swift b/LaunchIntents/IntentHandler.swift index e70ac28..69b21dc 100644 --- a/LaunchIntents/IntentHandler.swift +++ b/LaunchIntents/IntentHandler.swift @@ -51,7 +51,7 @@ class IntentHandler: INExtension, SelectTimerIntentHandling { return collection } catch { - print("error = \(error)") + Logger.error(error) throw error // completion(nil, error) } diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index f47ca07..7019710 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -44,7 +44,7 @@ C438C80529813FB400BF3EF9 /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4060DF4297AE9A7003FAB80 /* TimeInterval+Extensions.swift */; }; C438C807298195E600BF3EF9 /* Model+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C806298195E600BF3EF9 /* Model+Extensions.swift */; }; C438C80D2982847300BF3EF9 /* CoreDataRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C80C2982847300BF3EF9 /* CoreDataRequests.swift */; }; - C438C80F29828B8600BF3EF9 /* RecordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C80E29828B8600BF3EF9 /* RecordsView.swift */; }; + C438C80F29828B8600BF3EF9 /* ActivitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C80E29828B8600BF3EF9 /* ActivitiesView.swift */; }; C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C81029829EAF00BF3EF9 /* PropertyWrappers.swift */; }; C438C8152982BD9000BF3EF9 /* IntentDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7FE2981300500BF3EF9 /* IntentDataProvider.swift */; }; C438C8162982BE1E00BF3EF9 /* LeCountdown.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C4060DCA297AE73D003FAB80 /* LeCountdown.xcdatamodeld */; }; @@ -75,7 +75,6 @@ C4BA2AE62995AC3F00CB4FBA /* Loop_ToneSD_Boavista.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BA2AE52995AC3E00CB4FBA /* Loop_ToneSD_Boavista.wav */; }; C4BA2AE82995ACC200CB4FBA /* Clave_Loop_LLL.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BA2AE72995ACC200CB4FBA /* Clave_Loop_LLL.wav */; }; C4BA2AEA2995AD1C00CB4FBA /* SEM_Synths_Loop4_Nothing_Like_You.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BA2AE92995AD1C00CB4FBA /* SEM_Synths_Loop4_Nothing_Like_You.wav */; }; - C4BA2AF02996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEC2996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift */; }; C4BA2AF12996A11900CB4FBA /* CustomSound+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */; }; C4BA2AF22996A11900CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEE2996A11900CB4FBA /* CustomSound+CoreDataProperties.swift */; }; C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEF2996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift */; }; @@ -89,18 +88,13 @@ C4BA2B0F299BE61E00CB4FBA /* Interval+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0A299BE61E00CB4FBA /* Interval+CoreDataClass.swift */; }; C4BA2B10299BE61E00CB4FBA /* Interval+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0B299BE61E00CB4FBA /* Interval+CoreDataProperties.swift */; }; C4BA2B11299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0C299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift */; }; - C4BA2B12299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0D299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift */; }; C4BA2B13299BE61E00CB4FBA /* Countdown+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0E299BE61E00CB4FBA /* Countdown+CoreDataProperties.swift */; }; - C4BA2B14299BE6A000CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0D299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift */; }; C4BA2B15299BE6A000CB4FBA /* Interval+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0B299BE61E00CB4FBA /* Interval+CoreDataProperties.swift */; }; C4BA2B16299BE6A000CB4FBA /* IntervalGroup+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0C299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift */; }; - C4BA2B17299BE6A000CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEC2996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift */; }; C4BA2B18299BE6A000CB4FBA /* Interval+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0A299BE61E00CB4FBA /* Interval+CoreDataClass.swift */; }; C4BA2B19299BE6A000CB4FBA /* Countdown+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0E299BE61E00CB4FBA /* Countdown+CoreDataProperties.swift */; }; - C4BA2B1A299BE6A100CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0D299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift */; }; C4BA2B1B299BE6A100CB4FBA /* Interval+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0B299BE61E00CB4FBA /* Interval+CoreDataProperties.swift */; }; C4BA2B1C299BE6A100CB4FBA /* IntervalGroup+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0C299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift */; }; - C4BA2B1D299BE6A100CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEC2996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift */; }; C4BA2B1E299BE6A100CB4FBA /* Interval+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0A299BE61E00CB4FBA /* Interval+CoreDataClass.swift */; }; C4BA2B1F299BE6A100CB4FBA /* Countdown+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B0E299BE61E00CB4FBA /* Countdown+CoreDataProperties.swift */; }; C4BA2B22299BE82E00CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEF2996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift */; }; @@ -115,6 +109,21 @@ C4BA2B3A299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B39299F838000CB4FBA /* Model+SharedExtensions.swift */; }; C4BA2B3B299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B39299F838000CB4FBA /* Model+SharedExtensions.swift */; }; C4BA2B3C299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B39299F838000CB4FBA /* Model+SharedExtensions.swift */; }; + C4BA2B3E299FC86800CB4FBA /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B3D299FC86800CB4FBA /* StatsView.swift */; }; + C4BA2B3F299FC86800CB4FBA /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B3D299FC86800CB4FBA /* StatsView.swift */; }; + C4BA2B40299FC86800CB4FBA /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B3D299FC86800CB4FBA /* StatsView.swift */; }; + C4BA2B43299FCB2B00CB4FBA /* RecordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B42299FCB2B00CB4FBA /* RecordsView.swift */; }; + C4BA2B49299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B47299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift */; }; + C4BA2B4A299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B47299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift */; }; + C4BA2B4B299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B47299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift */; }; + C4BA2B4C299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B48299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift */; }; + C4BA2B4D299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B48299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift */; }; + C4BA2B4E299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B48299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift */; }; + C4BA2B57299FFA4F00CB4FBA /* AppGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B56299FFA4F00CB4FBA /* AppGuard.swift */; }; + C4BA2B5B299FFAB000CB4FBA /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B5A299FFAB000CB4FBA /* Logger.swift */; }; + C4BA2B5C299FFAB000CB4FBA /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B5A299FFAB000CB4FBA /* Logger.swift */; }; + C4BA2B5D299FFAB000CB4FBA /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B5A299FFAB000CB4FBA /* Logger.swift */; }; + C4BA2B5F299FFC8400CB4FBA /* StoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B5E299FFC8300CB4FBA /* StoreView.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 */; }; @@ -254,7 +263,7 @@ C438C80429813B3100BF3EF9 /* LeCountdown.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LeCountdown.entitlements; sourceTree = ""; }; C438C806298195E600BF3EF9 /* Model+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Model+Extensions.swift"; sourceTree = ""; }; C438C80C2982847300BF3EF9 /* CoreDataRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataRequests.swift; sourceTree = ""; }; - C438C80E29828B8600BF3EF9 /* RecordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsView.swift; sourceTree = ""; }; + C438C80E29828B8600BF3EF9 /* ActivitiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivitiesView.swift; sourceTree = ""; }; C438C81029829EAF00BF3EF9 /* PropertyWrappers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyWrappers.swift; sourceTree = ""; }; C445FA8E2987B83B0054D761 /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = ""; }; C445FA902987C0CF0054D761 /* LeCountdown.0.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.2.xcdatamodel; sourceTree = ""; }; @@ -279,7 +288,6 @@ C4BA2AE72995ACC200CB4FBA /* Clave_Loop_LLL.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = Clave_Loop_LLL.wav; sourceTree = ""; }; C4BA2AE92995AD1C00CB4FBA /* SEM_Synths_Loop4_Nothing_Like_You.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = SEM_Synths_Loop4_Nothing_Like_You.wav; sourceTree = ""; }; C4BA2AEB2996A09600CB4FBA /* LeCountdown.0.5.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.5.1.xcdatamodel; sourceTree = ""; }; - C4BA2AEC2996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Stopwatch+CoreDataProperties.swift"; sourceTree = ""; }; 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 = ""; }; @@ -291,7 +299,6 @@ C4BA2B0A299BE61E00CB4FBA /* Interval+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Interval+CoreDataClass.swift"; sourceTree = ""; }; C4BA2B0B299BE61E00CB4FBA /* Interval+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Interval+CoreDataProperties.swift"; sourceTree = ""; }; C4BA2B0C299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntervalGroup+CoreDataClass.swift"; sourceTree = ""; }; - C4BA2B0D299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntervalGroup+CoreDataProperties.swift"; sourceTree = ""; }; C4BA2B0E299BE61E00CB4FBA /* Countdown+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Countdown+CoreDataProperties.swift"; sourceTree = ""; }; C4BA2B24299D35C100CB4FBA /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; C4BA2B2C299E2DEE00CB4FBA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; @@ -299,6 +306,14 @@ C4BA2B30299F759700CB4FBA /* DefaultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultView.swift; sourceTree = ""; }; C4BA2B35299F82FB00CB4FBA /* Fakes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fakes.swift; sourceTree = ""; }; C4BA2B39299F838000CB4FBA /* Model+SharedExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Model+SharedExtensions.swift"; sourceTree = ""; }; + C4BA2B3D299FC86800CB4FBA /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = ""; }; + C4BA2B42299FCB2B00CB4FBA /* RecordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsView.swift; sourceTree = ""; }; + C4BA2B46299FCD8B00CB4FBA /* LeCountdown.0.6.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.6.1.xcdatamodel; sourceTree = ""; }; + C4BA2B47299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntervalGroup+CoreDataProperties.swift"; sourceTree = ""; }; + C4BA2B48299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Stopwatch+CoreDataProperties.swift"; sourceTree = ""; }; + C4BA2B56299FFA4F00CB4FBA /* AppGuard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppGuard.swift; sourceTree = ""; }; + C4BA2B5A299FFAB000CB4FBA /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + C4BA2B5E299FFC8300CB4FBA /* StoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreView.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 = ""; }; @@ -408,6 +423,7 @@ C438C80A2981DE1A00BF3EF9 /* Utils */, C438C8082981DDD200BF3EF9 /* Widget */, C445FA962987D0CF0054D761 /* Sound_Assets */, + C4BA2B55299FFA3700CB4FBA /* Subscription */, C4060DC3297AE73D003FAB80 /* Assets.xcassets */, C438C80429813B3100BF3EF9 /* LeCountdown.entitlements */, C4060DCD297AE73D003FAB80 /* Info.plist */, @@ -506,6 +522,7 @@ children = ( C4BA2AFC299A3A3700CB4FBA /* AppleMusicPlayer.swift */, C4742B5629840F6400D5D950 /* CoolPic.swift */, + C4BA2B5A299FFAB000CB4FBA /* Logger.swift */, C4BA2B2C299E2DEE00CB4FBA /* Preferences.swift */, C438C81029829EAF00BF3EF9 /* PropertyWrappers.swift */, C40FDB612992985C0042A390 /* TextToSpeechRecorder.swift */, @@ -522,13 +539,13 @@ C4F8B1B9298AC830005C86A5 /* Countdown */, C4F8B1BB298AC848005C86A5 /* Stopwatch */, C4F8B1D3298BF686005C86A5 /* Components */, + C4BA2B41299FCB0100CB4FBA /* Stats */, C4060DC1297AE73B003FAB80 /* ContentView.swift */, C4F8B1CF298BF2E2005C86A5 /* DialView.swift */, C4BA2B24299D35C100CB4FBA /* HomeView.swift */, C498E59E298D4DEA00E90DE0 /* LiveTimerListView.swift */, C4BA2B03299A42EF00CB4FBA /* NewDataView.swift */, C4BA2B05299A8F8D00CB4FBA /* PresetsView.swift */, - C438C80E29828B8600BF3EF9 /* RecordsView.swift */, C498E5A2298D720600E90DE0 /* TestView.swift */, C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */, ); @@ -568,6 +585,25 @@ path = Stephan_Bodzin; sourceTree = ""; }; + C4BA2B41299FCB0100CB4FBA /* Stats */ = { + isa = PBXGroup; + children = ( + C438C80E29828B8600BF3EF9 /* ActivitiesView.swift */, + C4BA2B42299FCB2B00CB4FBA /* RecordsView.swift */, + C4BA2B3D299FC86800CB4FBA /* StatsView.swift */, + ); + path = Stats; + sourceTree = ""; + }; + C4BA2B55299FFA3700CB4FBA /* Subscription */ = { + isa = PBXGroup; + children = ( + C4BA2B56299FFA4F00CB4FBA /* AppGuard.swift */, + C4BA2B5E299FFC8300CB4FBA /* StoreView.swift */, + ); + path = Subscription; + sourceTree = ""; + }; C4F8B188298AC248005C86A5 /* Generation */ = { isa = PBXGroup; children = ( @@ -586,11 +622,11 @@ C4BA2B0A299BE61E00CB4FBA /* Interval+CoreDataClass.swift */, C4BA2B0B299BE61E00CB4FBA /* Interval+CoreDataProperties.swift */, C4BA2B0C299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift */, - C4BA2B0D299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift */, + C4BA2B47299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift */, C4F8B16E298AC234005C86A5 /* Record+CoreDataClass.swift */, C4F8B16F298AC234005C86A5 /* Record+CoreDataProperties.swift */, C4F8B174298AC234005C86A5 /* Stopwatch+CoreDataClass.swift */, - C4BA2AEC2996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift */, + C4BA2B48299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift */, ); path = Generation; sourceTree = ""; @@ -843,28 +879,31 @@ C4060DC9297AE73D003FAB80 /* Persistence.swift in Sources */, C4BA2B36299F82FB00CB4FBA /* Fakes.swift in Sources */, C498E5A1298D543900E90DE0 /* LiveTimer.swift in Sources */, - C438C80F29828B8600BF3EF9 /* RecordsView.swift in Sources */, + C438C80F29828B8600BF3EF9 /* ActivitiesView.swift in Sources */, C4F8B187298AC234005C86A5 /* Activity+CoreDataProperties.swift in Sources */, C438C80D2982847300BF3EF9 /* CoreDataRequests.swift in Sources */, C4742B5F2984205000D5D950 /* ViewModifiers.swift in Sources */, C4F8B1D8298C0727005C86A5 /* TimerRouter.swift in Sources */, C4BA2B13299BE61E00CB4FBA /* Countdown+CoreDataProperties.swift in Sources */, C4F8B186298AC234005C86A5 /* Activity+CoreDataClass.swift in Sources */, + C4BA2B57299FFA4F00CB4FBA /* AppGuard.swift in Sources */, C4F8B15729891271005C86A5 /* Conductor.swift in Sources */, + C4BA2B5F299FFC8400CB4FBA /* StoreView.swift in Sources */, C4F8B17E298AC234005C86A5 /* Alarm+CoreDataClass.swift in Sources */, C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */, C4F8B1BF298ACA0B005C86A5 /* StopwatchDialView.swift in Sources */, C438C807298195E600BF3EF9 /* Model+Extensions.swift in Sources */, C4BA2B04299A42EF00CB4FBA /* NewDataView.swift in Sources */, + C4BA2B4C299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */, C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */, C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */, C498E5A5299152B400E90DE0 /* GreenCheckmarkView.swift in Sources */, + C4BA2B3E299FC86800CB4FBA /* StatsView.swift in Sources */, C4F8B1B8298AC81D005C86A5 /* CountdownDialView.swift in Sources */, C445FA922987CC8A0054D761 /* Sound.swift in Sources */, C4F8B1D0298BF2E2005C86A5 /* DialView.swift in Sources */, C4BA2B11299BE61E00CB4FBA /* IntervalGroup+CoreDataClass.swift in Sources */, C4BA2AFD299A3A3700CB4FBA /* AppleMusicPlayer.swift in Sources */, - C4BA2B12299BE61E00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */, C4BA2B3A299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */, C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */, C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */, @@ -884,7 +923,6 @@ C4F8B1532987FE6F005C86A5 /* LaunchWidgetLiveActivity.swift in Sources */, C4F8B182298AC234005C86A5 /* Stopwatch+CoreDataClass.swift in Sources */, C4BA2ADB299549BC00CB4FBA /* TimerModel.swift in Sources */, - C4BA2AF02996A11900CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */, C4F8B16B298AA240005C86A5 /* NewStopwatchView.swift in Sources */, C4BA2AF22996A11900CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */, C4060DF5297AE9A7003FAB80 /* TimeInterval+Extensions.swift in Sources */, @@ -895,10 +933,13 @@ C4BA2B0F299BE61E00CB4FBA /* Interval+CoreDataClass.swift in Sources */, C4F8B164298A9A92005C86A5 /* AlarmFormView.swift in Sources */, C40FDB622992985C0042A390 /* TextToSpeechRecorder.swift in Sources */, + C4BA2B43299FCB2B00CB4FBA /* RecordsView.swift in Sources */, C4BA2B2F299E69A000CB4FBA /* View+Extension.swift in Sources */, C4060DF7297AFEF2003FAB80 /* NewCountdownView.swift in Sources */, C4060DCC297AE73D003FAB80 /* LeCountdown.xcdatamodeld in Sources */, + C4BA2B49299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */, C4F8B17C298AC234005C86A5 /* Record+CoreDataClass.swift in Sources */, + C4BA2B5B299FFAB000CB4FBA /* Logger.swift in Sources */, C4F8B162298A9A1F005C86A5 /* NewAlarmView.swift in Sources */, C4742B5B298414B000D5D950 /* ImageSelectionView.swift in Sources */, C438C7C5298024E900BF3EF9 /* NSManagedContext+Extensions.swift in Sources */, @@ -943,15 +984,18 @@ C4F8B195298AC288005C86A5 /* Activity+CoreDataClass.swift in Sources */, C4BA2B22299BE82E00CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */, C4F8B193298AC288005C86A5 /* Countdown+CoreDataClass.swift in Sources */, - C4BA2B17299BE6A000CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */, C4BA2AF72996A4EF00CB4FBA /* CustomSound+CoreDataClass.swift in Sources */, C4F8B1AD298AC451005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */, C445FA87298448730054D761 /* CoolPic.swift in Sources */, + C4BA2B3F299FC86800CB4FBA /* StatsView.swift in Sources */, C438C8162982BE1E00BF3EF9 /* LeCountdown.xcdatamodeld in Sources */, + C4BA2B4A299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */, + C4BA2B4D299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */, C4F8B194298AC288005C86A5 /* Record+CoreDataProperties.swift in Sources */, C438C8152982BD9000BF3EF9 /* IntentDataProvider.swift in Sources */, C438C7DF2981216300BF3EF9 /* LaunchWidget.intentdefinition in Sources */, C4F8B15B29892D40005C86A5 /* SoundPlayer.swift in Sources */, + C4BA2B5C299FFAB000CB4FBA /* Logger.swift in Sources */, C438C7E82981255D00BF3EF9 /* TimeInterval+Extensions.swift in Sources */, C4BA2B37299F82FF00CB4FBA /* Fakes.swift in Sources */, C4F8B18F298AC288005C86A5 /* AbstractTimer+CoreDataClass.swift in Sources */, @@ -967,7 +1011,6 @@ C4F8B1AE298AC451005C86A5 /* Alarm+CoreDataProperties.swift in Sources */, C4BA2B32299F75DE00CB4FBA /* DefaultView.swift in Sources */, C4F8B18D298AC288005C86A5 /* Stopwatch+CoreDataClass.swift in Sources */, - C4BA2B14299BE6A000CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */, C438C8182982BFC100BF3EF9 /* Persistence.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -996,15 +1039,17 @@ C438C81A2982BFF100BF3EF9 /* NSManagedContext+Extensions.swift in Sources */, C4F8B19D298AC288005C86A5 /* AbstractTimer+CoreDataClass.swift in Sources */, C4BA2B3C299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */, + C4BA2B40299FC86800CB4FBA /* StatsView.swift in Sources */, C4BA2AF82996A4F000CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */, C4F8B199298AC288005C86A5 /* Record+CoreDataClass.swift in Sources */, C438C8012981327600BF3EF9 /* Persistence.swift in Sources */, C438C7FD29812BF700BF3EF9 /* LaunchWidget.intentdefinition in Sources */, + C4BA2B5D299FFAB000CB4FBA /* Logger.swift in Sources */, + C4BA2B4B299FCE0C00CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */, C4F8B1B1298AC451005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */, C4BA2B1E299BE6A100CB4FBA /* Interval+CoreDataClass.swift in Sources */, - C4BA2B1A299BE6A100CB4FBA /* IntervalGroup+CoreDataProperties.swift in Sources */, - C4BA2B1D299BE6A100CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */, C445FA86298448720054D761 /* CoolPic.swift in Sources */, + C4BA2B4E299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */, C4BA2B23299BE82E00CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */, C438C800298130E900BF3EF9 /* IntentDataProvider.swift in Sources */, ); @@ -1461,6 +1506,7 @@ C4060DCA297AE73D003FAB80 /* LeCountdown.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + C4BA2B46299FCD8B00CB4FBA /* LeCountdown.0.6.1.xcdatamodel */, C4BA2B07299BDAE000CB4FBA /* LeCountdown.0.6.xcdatamodel */, C4BA2AEB2996A09600CB4FBA /* LeCountdown.0.5.1.xcdatamodel */, C4BA2AD72993F7D200CB4FBA /* LeCountdown.0.5.xcdatamodel */, @@ -1470,7 +1516,7 @@ C418A14F298428CB00C22230 /* LeCountdown.0.1.xcdatamodel */, C4060DCB297AE73D003FAB80 /* LeCountdown.xcdatamodel */, ); - currentVersion = C4BA2B07299BDAE000CB4FBA /* LeCountdown.0.6.xcdatamodel */; + currentVersion = C4BA2B46299FCD8B00CB4FBA /* LeCountdown.0.6.1.xcdatamodel */; path = LeCountdown.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/LeCountdown/Conductor.swift b/LeCountdown/Conductor.swift index 3d72b66..2c8f1c7 100644 --- a/LeCountdown/Conductor.swift +++ b/LeCountdown/Conductor.swift @@ -106,7 +106,7 @@ class Conductor: ObservableObject { do { try CoreDataRequests.recordActivity(timer: countdown, dateInterval: dateInterval) } catch { - print("Could not record activity = \(error)") + Logger.error(error) // TODO: show error to user } } @@ -170,7 +170,7 @@ class Conductor: ObservableObject { do { try CoreDataRequests.recordActivity(timer: stopwatch, dateInterval: DateInterval(start: start, end: end ?? Date())) } catch { - print("could not record") + Logger.error(error) } self._endLiveActivity(timerId: stopwatch.stringId) } @@ -207,7 +207,7 @@ class Conductor: ObservableObject { self.soundPlayer = soundPlayer try soundPlayer.playSound(soundFile: soundFile, repeats: false) } catch { - print("error = \(error)") + Logger.error(error) // TODO: manage error } } @@ -231,7 +231,7 @@ class Conductor: ObservableObject { let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent) print("Requested a Live Activity: \(String(describing: liveActivity.id)).") } catch (let error) { - print("Error requesting Live Activity \(error.localizedDescription).") + Logger.error(error) } // self._scheduleAppRefresh(countdown: countdown) @@ -246,7 +246,7 @@ class Conductor: ObservableObject { try BGTaskScheduler.shared.submit(request) print("request submitted with date: \(String(describing: request.earliestBeginDate))") } catch { - print("Could not schedule app refresh: \(error)") + Logger.error(error) } } diff --git a/LeCountdown/Model/CoreDataRequests.swift b/LeCountdown/Model/CoreDataRequests.swift index d111d3e..8c7d6fa 100644 --- a/LeCountdown/Model/CoreDataRequests.swift +++ b/LeCountdown/Model/CoreDataRequests.swift @@ -34,6 +34,7 @@ class CoreDataRequests { return activity } } catch { + Logger.error(error) print("error = \(error)") } let context = PersistenceController.shared.container.viewContext diff --git a/LeCountdown/Model/Fakes.swift b/LeCountdown/Model/Fakes.swift index 73c92bf..bd93646 100644 --- a/LeCountdown/Model/Fakes.swift +++ b/LeCountdown/Model/Fakes.swift @@ -33,3 +33,13 @@ extension Alarm { } } + +extension Activity { + + static func fake(context: NSManagedObjectContext) -> Activity { + let activity = Activity(context: context) + activity.name = "Tea" + return activity + } + +} diff --git a/LeCountdown/Model/Generation/IntervalGroup+CoreDataProperties.swift b/LeCountdown/Model/Generation/IntervalGroup+CoreDataProperties.swift index abb7c2f..8300975 100644 --- a/LeCountdown/Model/Generation/IntervalGroup+CoreDataProperties.swift +++ b/LeCountdown/Model/Generation/IntervalGroup+CoreDataProperties.swift @@ -2,7 +2,7 @@ // IntervalGroup+CoreDataProperties.swift // LeCountdown // -// Created by Laurent Morvillier on 14/02/2023. +// Created by Laurent Morvillier on 17/02/2023. // // @@ -17,8 +17,25 @@ extension IntervalGroup { } @NSManaged public var repeatCount: Int16 - @NSManaged public var intervals: Interval? @NSManaged public var countdown: Countdown? + @NSManaged public var intervals: NSSet? + +} + +// MARK: Generated accessors for intervals +extension IntervalGroup { + + @objc(addIntervalsObject:) + @NSManaged public func addToIntervals(_ value: Interval) + + @objc(removeIntervalsObject:) + @NSManaged public func removeFromIntervals(_ value: Interval) + + @objc(addIntervals:) + @NSManaged public func addToIntervals(_ values: NSSet) + + @objc(removeIntervals:) + @NSManaged public func removeFromIntervals(_ values: NSSet) } diff --git a/LeCountdown/Model/Generation/Record+CoreDataProperties.swift b/LeCountdown/Model/Generation/Record+CoreDataProperties.swift index 2f60924..d3238e0 100644 --- a/LeCountdown/Model/Generation/Record+CoreDataProperties.swift +++ b/LeCountdown/Model/Generation/Record+CoreDataProperties.swift @@ -2,7 +2,7 @@ // Record+CoreDataProperties.swift // LeCountdown // -// Created by Laurent Morvillier on 01/02/2023. +// Created by Laurent Morvillier on 17/02/2023. // // @@ -18,6 +18,7 @@ extension Record { @NSManaged public var end: Date? @NSManaged public var start: Date? + @NSManaged public var duration: Double @NSManaged public var activity: Activity? } diff --git a/LeCountdown/Model/Generation/Stopwatch+CoreDataProperties.swift b/LeCountdown/Model/Generation/Stopwatch+CoreDataProperties.swift index 06dbc3d..832ec8a 100644 --- a/LeCountdown/Model/Generation/Stopwatch+CoreDataProperties.swift +++ b/LeCountdown/Model/Generation/Stopwatch+CoreDataProperties.swift @@ -2,7 +2,7 @@ // Stopwatch+CoreDataProperties.swift // LeCountdown // -// Created by Laurent Morvillier on 10/02/2023. +// Created by Laurent Morvillier on 17/02/2023. // // diff --git a/LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion b/LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion index 44ed1f5..8caf740 100644 --- a/LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion +++ b/LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - LeCountdown.0.6.xcdatamodel + LeCountdown.0.6.1.xcdatamodel diff --git a/LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.1.xcdatamodel/contents b/LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.1.xcdatamodel/contents new file mode 100644 index 0000000..ed964a2 --- /dev/null +++ b/LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.1.xcdatamodel/contents @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LeCountdown/Model/Model+Extensions.swift b/LeCountdown/Model/Model+Extensions.swift index 64555e5..b144f17 100644 --- a/LeCountdown/Model/Model+Extensions.swift +++ b/LeCountdown/Model/Model+Extensions.swift @@ -67,14 +67,31 @@ extension Stopwatch { extension Record { + public override func didChangeValue(forKey key: String) { + super.didChangeValue(forKey: key) + + switch key { + case "start", "end": + self.computeDuration() + default: + break + } + } + + func computeDuration() { + if let start, let end { + self.duration = end.timeIntervalSince(start) + } + } + var details: String { if let start, let end { - return "\(start) - \(end)" + return "\(start.formatted()) - \(end.formatted())" } else { return "no details" } } - + } extension Activity { diff --git a/LeCountdown/Model/NSManagedContext+Extensions.swift b/LeCountdown/Model/NSManagedContext+Extensions.swift index 8f19b6f..1460363 100644 --- a/LeCountdown/Model/NSManagedContext+Extensions.swift +++ b/LeCountdown/Model/NSManagedContext+Extensions.swift @@ -15,7 +15,72 @@ extension NSManagedObjectContext { guard let objectId = PersistenceController.shared.container.persistentStoreCoordinator.managedObjectID(forURIRepresentation: url) else { return nil } return self.object(with: objectId) } - + + func compute(activity: Activity, stats: [Stat]) throws -> [StatValue] { + + // Step 1: + // - Create the summing expression on the amount attribute. + // - Name the expression result as 'amountTotal'. + // - Assign the expression result data type as a Double. + + var expressions: [NSExpressionDescription] = [] + + for stat in stats { + let expression = NSExpressionDescription() + expression.expression = NSExpression(forFunction: stat.function, arguments:[NSExpression(forKeyPath: stat.field)]) + expression.name = stat.stringIdentifier; + expression.expressionResultType = NSAttributeType.doubleAttributeType + + expressions.append(expression) + } + + // Step 2: + // - Create the fetch request for the Movement entity. + // - Indicate that the fetched properties are those that were + // described in `expression`. + // - Indicate that the result type is a dictionary. + + let fetchRequest = NSFetchRequest(entityName: "Record") + fetchRequest.predicate = NSPredicate(format: "activity = %@", activity) + fetchRequest.propertiesToFetch = expressions + fetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType + + // Step 3: + // - Execute the fetch request which returns an array. + // - There will only be one result. Get the first array + // element and assign to 'resultMap'. + // - The summed amount value is in the dictionary as + // 'amountTotal'. This will be summed value. + + var statValues: [StatValue] = [] + + let results = try self.fetch(fetchRequest) + if let resultMap = results.first as? [String : Any] { + + for stat in stats { + + if let result = resultMap[stat.stringIdentifier] { + + var value: NSDecimalNumber? = nil + switch result { + case let double as Double: + value = NSDecimalNumber(value: double) + case let integer as Int: + value = NSDecimalNumber(integerLiteral: integer) + default: + print("unmanaged value type = \(String(describing: result))") + break + } + + if let value { + let sv = StatValue(stat: stat, value: value) + statValues.append(sv) + } + } + } + } + return statValues + } } extension NSManagedObject { diff --git a/LeCountdown/Model/Persistence.swift b/LeCountdown/Model/Persistence.swift index ac4cd85..fb7be47 100644 --- a/LeCountdown/Model/Persistence.swift +++ b/LeCountdown/Model/Persistence.swift @@ -34,6 +34,7 @@ struct PersistenceController { do { try viewContext.save() } catch { + Logger.error(error) // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. let nsError = error as NSError diff --git a/LeCountdown/Subscription/AppGuard.swift b/LeCountdown/Subscription/AppGuard.swift new file mode 100644 index 0000000..c512451 --- /dev/null +++ b/LeCountdown/Subscription/AppGuard.swift @@ -0,0 +1,148 @@ +// +// Guard.swift +// Poker Analytics 6 +// +// Created by Laurent Morvillier on 20/04/2022. +// + +import Foundation +import StoreKit + +public enum StoreError: Error { + case failedVerification +} + +enum StorePlan : String, CaseIterable { + case none + case unlimited = "com.staxriver.lecountdown.unlimited" +} + +extension Notification.Name { + static let StoreEventHappened = Notification.Name("storeEventHappened") +} + +@objc class AppGuard : NSObject { + + static var main: AppGuard = AppGuard() + + @Published private(set) var purchasedTransactions = Set() + + var currentBestPlan: StoreKit.Transaction? = nil + + var updateListenerTask: Task? = nil + + override init() { + + super.init() + + self.updateListenerTask = self.listenForTransactions() + + Task { + await self.refreshPurchasedProducts() + } + } + + func refreshPurchasedProducts() async { + + // Iterate through the user's purchased products. + for await verificationResult in Transaction.currentEntitlements { + do { + let transaction = try await self.processTransactionResult(verificationResult) + DispatchQueue.main.async { + NotificationCenter.default.post(name: Notification.Name.StoreEventHappened, object: nil) + } + await transaction.finish() + } catch { + Logger.error(error) + } + } + } + + func listenForTransactions() -> Task { + return Task.detached { + //Iterate through any transactions which didn't come from a direct call to `purchase()`. + for await result in Transaction.updates { + do { + let transaction = try self.checkVerified(result) + + //Deliver content to the user. + await self.updatePurchasedIdentifiers(transaction) + + //Always finish a transaction. + await transaction.finish() + } catch { + Logger.error(error) + //StoreKit has a receipt it can read but it failed verification. Don't deliver content to the user. + print("Transaction failed verification") + } + } + } + } + + func checkVerified(_ result: VerificationResult) throws -> T { + //Check if the transaction passes StoreKit verification. + switch result { + case .unverified: + //StoreKit has parsed the JWS but failed verification. Don't deliver content to the user. + throw StoreError.failedVerification + case .verified(let safe): + //If the transaction is verified, unwrap and return it. + return safe + } + } + + @MainActor + func updatePurchasedIdentifiers(_ transaction: StoreKit.Transaction) async { + if transaction.revocationDate == nil { + // If the App Store has not revoked the transaction, add it to the list of `purchasedIdentifiers`. + purchasedTransactions.insert(transaction) + } else { + // If the App Store has revoked this transaction, remove it from the list of `purchasedIdentifiers`. + purchasedTransactions.remove(transaction) + } + + self._updateBestPlan() + } + + func processTransactionResult(_ result: VerificationResult) async throws -> StoreKit.Transaction { + + let transaction = try checkVerified(result) + + // Deliver content to the user. + await updatePurchasedIdentifiers(transaction) + + return transaction + } + + var isAuthorized: Bool { + return self.currentPlan == .unlimited + } + + var currentPlan: StorePlan { + + #if DEBUG + return .unlimited + #else + if let currentBestPlan = self.currentBestPlan, let plan = StorePlan(rawValue: currentBestPlan.productID) { + return plan + } + if let vf = Preferences.verifiedTransaction(), + vf.expiryDate > Date(), vf.graceDate > Date(), + let plan = StorePlan(rawValue: vf.productId) { + return plan + } + return .none + #endif + } + + fileprivate func _updateBestPlan() { + + if let unlimited = self.purchasedTransactions.first(where: { $0.productID == StorePlan.unlimited.rawValue }) { + self.currentBestPlan = unlimited + } else { + self.currentBestPlan = nil + } + + } + +} diff --git a/LeCountdown/Subscription/StoreView.swift b/LeCountdown/Subscription/StoreView.swift new file mode 100644 index 0000000..32b3ec4 --- /dev/null +++ b/LeCountdown/Subscription/StoreView.swift @@ -0,0 +1,20 @@ +// +// StoreView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 17/02/2023. +// + +import SwiftUI + +struct StoreView: View { + var body: some View { + Text("Hello Store!") + } +} + +struct StoreView_Previews: PreviewProvider { + static var previews: some View { + StoreView() + } +} diff --git a/LeCountdown/Utils/Logger.swift b/LeCountdown/Utils/Logger.swift new file mode 100644 index 0000000..b072edd --- /dev/null +++ b/LeCountdown/Utils/Logger.swift @@ -0,0 +1,62 @@ +// +// Logger.swift +// Poker Analytics 4 +// +// Created by Laurent Morvillier on 29/03/2018. +// + +import Foundation + +@objc public class Logger : NSObject { + + @objc static public func log(_ message: Any, file: String = #file, function: String = #function, line: Int = #line) { + let filestr: NSString = NSString(string: file) + print("\(filestr.lastPathComponent).\(line).\(function): \(message)") + } + + @objc static public func error(_ error: Error) { + Logger.error(error, file: #file, function: #function, line: #line) + } + + static public func error(_ error: Error, file: String = #file, function: String = #function, line: Int = #line) { + let filestr: NSString = NSString(string: file) + var fireBaseError: Error { + if let customError = error as? CustomNSError & LocalizedError { + return customError.fireBaseError + } else { + return error + } + } + print("ERROR: \(filestr.lastPathComponent).\(line).\(function): \(fireBaseError)") + } + + @objc static public func w(_ message: Any, file: String = #file, function: String = #function, line: Int = #line) { + let filestr: NSString = NSString(string: file) + print("Warning: \(filestr.lastPathComponent).\(line).\(function): \(message)") + } + + @objc static public func crashLogging(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + let fileName: String = file.components(separatedBy: "/").last ?? file + #if DEBUG + NSLogv("%@.%i.%@: %@", getVaList([fileName, line, function, message])) + #else + NSLogv("%@.%i.%@: %@", getVaList([fileName, line, function, message])) + #endif + } + +} + +extension LocalizedError where Self: CustomNSError { + var simpleErrorDescription: String { + let mirror = Mirror(reflecting: self) + if let label = mirror.children.first?.label { + return label + } else { + return String(describing:self) + } + } + + var fireBaseError: NSError { + NSError(domain: Self.errorDomain + "." + self.simpleErrorDescription, code: self.errorCode, userInfo: self.errorUserInfo) + } +} diff --git a/LeCountdown/Utils/TextToSpeechRecorder.swift b/LeCountdown/Utils/TextToSpeechRecorder.swift index 15f4b23..2a04b5f 100644 --- a/LeCountdown/Utils/TextToSpeechRecorder.swift +++ b/LeCountdown/Utils/TextToSpeechRecorder.swift @@ -46,6 +46,7 @@ class TextToSpeechRecorder { } try output?.write(from: pcmBuffer) } catch { + Logger.error(error) handler(.failure(error)) } } diff --git a/LeCountdown/Views/Alarm/NewAlarmView.swift b/LeCountdown/Views/Alarm/NewAlarmView.swift index 105a750..ad4183e 100644 --- a/LeCountdown/Views/Alarm/NewAlarmView.swift +++ b/LeCountdown/Views/Alarm/NewAlarmView.swift @@ -239,6 +239,7 @@ struct AlarmEditView: View { do { try viewContext.save() } catch { + Logger.error(error) self.errorShown = true self.error = error } diff --git a/LeCountdown/Views/ContentView.swift b/LeCountdown/Views/ContentView.swift index eadd35b..ceeac13 100644 --- a/LeCountdown/Views/ContentView.swift +++ b/LeCountdown/Views/ContentView.swift @@ -264,6 +264,7 @@ struct ContentView: View { do { try viewContext.save() } catch { + Logger.error(error) self.boringContext.error = error } } diff --git a/LeCountdown/Views/Countdown/NewCountdownView.swift b/LeCountdown/Views/Countdown/NewCountdownView.swift index fe7ffae..4d1f187 100644 --- a/LeCountdown/Views/Countdown/NewCountdownView.swift +++ b/LeCountdown/Views/Countdown/NewCountdownView.swift @@ -310,6 +310,7 @@ struct CountdownEditView : View { do { try viewContext.save() } catch { + Logger.error(error) self.errorShown = true self.error = error } diff --git a/LeCountdown/Views/HomeView.swift b/LeCountdown/Views/HomeView.swift index fe0594b..6986cad 100644 --- a/LeCountdown/Views/HomeView.swift +++ b/LeCountdown/Views/HomeView.swift @@ -26,7 +26,7 @@ struct CompactHomeView: View { .environmentObject(Conductor.maestro) .tabItem { Label("Home", systemImage: "clock.fill") } .tag(1) - RecordsView() + ActivitiesView() .environment(\.managedObjectContext, viewContext) .tabItem { Label("Stats", systemImage: "chart.bar.fill") } .tag(2) @@ -59,7 +59,7 @@ struct RegularHomeView: View { .environmentObject(Conductor.maestro) .tabItem { Label("Home", systemImage: "clock.fill") } .tag(1) - RecordsView() + ActivitiesView() .environment(\.managedObjectContext, viewContext) .tabItem { Label("Stats", systemImage: "chart.bar.fill") } .tag(2) @@ -76,11 +76,17 @@ struct RegularHomeView: View { struct CompactHomeView_Previews: PreviewProvider { static var previews: some View { CompactHomeView() + .environmentObject(Conductor.maestro) + .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } } struct RegularHomeView_Previews: PreviewProvider { static var previews: some View { RegularHomeView() + .environmentObject(Conductor.maestro) + .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } } diff --git a/LeCountdown/Views/RecordsView.swift b/LeCountdown/Views/RecordsView.swift deleted file mode 100644 index 4ddaa30..0000000 --- a/LeCountdown/Views/RecordsView.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// RecordsView.swift -// LeCountdown -// -// Created by Laurent Morvillier on 26/01/2023. -// - -import SwiftUI - -struct RecordsView: View { - - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \Record.start, ascending: false)], - animation: .default) - private var records: FetchedResults - - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \Activity.name, ascending: true)], - predicate: NSPredicate(format: "records.@count > 0"), animation: .default) - private var activities: FetchedResults - - var body: some View { - - VStack { - if records.isEmpty { - Text("You don't have any recorded activity yet") - } else { - List { - ForEach(activities) { activity in - HStack { - Text(activity.name ?? "no activity") - Spacer() - Text(activity.recordCount) - } - } - } - } - }.navigationTitle("Activities") - - } -} - -struct RecordsView_Previews: PreviewProvider { - static var previews: some View { - NavigationStack { - RecordsView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } - } -} diff --git a/LeCountdown/Views/Stats/ActivitiesView.swift b/LeCountdown/Views/Stats/ActivitiesView.swift new file mode 100644 index 0000000..a526c8e --- /dev/null +++ b/LeCountdown/Views/Stats/ActivitiesView.swift @@ -0,0 +1,62 @@ +// +// RecordsView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 26/01/2023. +// + +import SwiftUI + +struct ActivitiesView: View { + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Record.start, ascending: false)], + animation: .default) + private var records: FetchedResults + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Activity.name, ascending: true)], + predicate: NSPredicate(format: "records.@count > 0"), animation: .default) + private var activities: FetchedResults + + var body: some View { + + VStack { + if self.records.isEmpty { + Text("You don't have any recorded activity yet") + } else { + List { + ForEach(self.activities) { activity in + NavigationLink { + HStack { + StatsView(activity: activity).frame(width: 300) + .background(.red) + RecordsView(activity: activity) + .frame(maxWidth: .infinity) + .background(.green) + } + + } label: { + HStack { + Text(activity.name ?? "no activity") + Spacer() + Text(activity.recordCount) + } + } + } + } + } + } + .navigationTitle("Activities") + + } +} + +struct ActivitiesView_Previews: PreviewProvider { + static var previews: some View { + NavigationStack { + ActivitiesView() + .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } + } +} diff --git a/LeCountdown/Views/Stats/RecordsView.swift b/LeCountdown/Views/Stats/RecordsView.swift new file mode 100644 index 0000000..7440912 --- /dev/null +++ b/LeCountdown/Views/Stats/RecordsView.swift @@ -0,0 +1,43 @@ +// +// RecordsView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 17/02/2023. +// + +import SwiftUI + +struct RecordsView: View { + + var activity: Activity + + var body: some View { + + List { + + if let records = self.activity.records as? Set { + + let array: [Record] = Array(records) + + ForEach(array) { record in + HStack { + Text(record.details) + Spacer() + if let duration = record.duration { + Text(duration.minuteSecond) + } + } + } + } + } + + } + +} + +struct RecordsView_Previews: PreviewProvider { + static var previews: some View { + RecordsView(activity: + Activity.fake(context: PersistenceController.preview.container.viewContext)) + } +} diff --git a/LeCountdown/Views/Stats/StatsView.swift b/LeCountdown/Views/Stats/StatsView.swift new file mode 100644 index 0000000..df852e7 --- /dev/null +++ b/LeCountdown/Views/Stats/StatsView.swift @@ -0,0 +1,162 @@ +// +// StatsView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 17/02/2023. +// + +import SwiftUI +import CoreData + +enum Stat: Int, CaseIterable { + case count + case totalDuration + case averageDuration + + var localizedName: String { + switch self { + case .count: return NSLocalizedString("Count", comment: "") + case .totalDuration: return NSLocalizedString("Duration", comment: "") + case .averageDuration: return NSLocalizedString("Average duration", comment: "") + } + } + + var field: String { + switch self { + case .count: + return "duration" + case .totalDuration: + return "duration" + case .averageDuration: + return "duration" + } + } + + var function: String { + switch self { + case .count: + return "count:" + case .totalDuration: + return "sum:" + case .averageDuration: + return "average:" + } + } + + var stringIdentifier: String { + return self.function + self.field + } + + var type: NSAttributeType { + switch self { + case .count: + return .integer32AttributeType + default: + return .doubleAttributeType + } + } +} + +struct StatValue: Identifiable { + + var id: Int { return self.stat.rawValue } + + var stat: Stat + var value: NSDecimalNumber + + var formattedValue: String { + + switch self.stat { + case .averageDuration, .totalDuration: + return self.value.doubleValue.minuteSecond + default: + let formatter: NumberFormatter = NumberFormatter() + return formatter.string(from: self.value) ?? "--" + } + + } +} + +class StatModel: ObservableObject { + + @Published var statValues: [StatValue] = [] + + @Published var isComputing = false + + @Published var error: Error? = nil + + func compute(activity: Activity) { + + self.isComputing = true + + PersistenceController.shared.container.performBackgroundTask { context in + + var values: [StatValue] = [] + + do { + values = try context.compute(activity: activity, stats: Stat.allCases) + } catch { + Logger.error(error) + self.error = error + } + + DispatchQueue.main.async { + self.statValues = values + self.isComputing = false + } + + } + + } + +} + +struct StatsView: View { + + var activity: Activity + + @StateObject var model: StatModel = StatModel() + + var body: some View { + + VStack() { + + if self.model.isComputing { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } else { + HStack { + ForEach(self.model.statValues) { statValue in + StatView(name: statValue.stat.localizedName, value: statValue.formattedValue) + } + } + } + Spacer() + } + .onAppear() { + self.model.compute(activity: self.activity) + } + .navigationTitle(self.activity.name ?? "") + } + +} + +struct StatView: View { + + var name: String + var value: String + + var body: some View { + VStack(alignment: .leading) { + Text(self.name.uppercased()).font(.caption) + Text(self.value).font(.title) + } + } + +} + +struct StatsView_Previews: PreviewProvider { + static var previews: some View { + StatsView(activity: Activity.fake(context: PersistenceController.preview.container.viewContext)) + } +} diff --git a/LeCountdown/Views/Stopwatch/NewStopwatchView.swift b/LeCountdown/Views/Stopwatch/NewStopwatchView.swift index 581a94c..22fbe64 100644 --- a/LeCountdown/Views/Stopwatch/NewStopwatchView.swift +++ b/LeCountdown/Views/Stopwatch/NewStopwatchView.swift @@ -290,6 +290,7 @@ struct StopwatchEditView: View { do { try viewContext.save() } catch { + Logger.error(error) self.errorShown = true self.error = error } diff --git a/LeCountdown/Views/TestView.swift b/LeCountdown/Views/TestView.swift index 65a7a41..0797b28 100644 --- a/LeCountdown/Views/TestView.swift +++ b/LeCountdown/Views/TestView.swift @@ -15,8 +15,8 @@ struct TestSubView: View { Spacer() Image(systemName: "xmark").frame(minWidth: 0.0, maxWidth: 60.0, minHeight: 0.0, maxHeight: .infinity).background(.red) } - .background(.gray) - .cornerRadius(16.0) + .background(.gray) + .cornerRadius(16.0) } }