Make the restore / resume work

splits
Laurent 2 years ago
parent a60750ce97
commit 2871a7abaa
  1. 44
      LeCountdown.xcodeproj/project.pbxproj
  2. 239
      LeCountdown/Conductor.swift
  3. 118
      LeCountdown/CountdownSequence.swift
  4. 7
      LeCountdown/Model/Fakes.swift
  5. 22
      LeCountdown/Model/Generation/Countdown+CoreDataProperties.swift
  6. 15
      LeCountdown/Model/Generation/Step+CoreDataClass.swift
  7. 12
      LeCountdown/Model/Generation/Step+CoreDataProperties.swift
  8. 15
      LeCountdown/Model/Generation/TimeRange+CoreDataClass.swift
  9. 16
      LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.6.xcdatamodel/contents
  10. 12
      LeCountdown/Model/Model+Extensions.swift
  11. 16
      LeCountdown/Model/Model+SharedExtensions.swift
  12. 18
      LeCountdown/Patcher.swift
  13. 2
      LeCountdown/Sound/DelaySoundPlayer.swift
  14. 1
      LeCountdown/Views/Countdown/CountdownDialView.swift
  15. 16
      LeCountdown/Views/Countdown/CountdownFormView.swift
  16. 50
      LeCountdown/Views/Countdown/NewCountdownView.swift
  17. 12
      LeCountdown/Views/Countdown/RangeFormView.swift
  18. 2
      LeCountdown/Views/LiveTimerListView.swift
  19. 9
      LeCountdown/Views/PresetsView.swift
  20. 6
      LeCountdown/Views/StartView.swift
  21. 40
      LeCountdown/Views/TimerModel.swift
  22. 3
      LeCountdown/en.lproj/Localizable.strings
  23. 3
      LeCountdown/fr.lproj/Localizable.strings
  24. 15
      TimeRange+CoreDataClass.swift
  25. 29
      TimeRange+CoreDataProperties.swift

@ -100,6 +100,15 @@
C4556F7629E411A400DEB40B /* LogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F7529E411A400DEB40B /* LogsView.swift */; }; C4556F7629E411A400DEB40B /* LogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F7529E411A400DEB40B /* LogsView.swift */; };
C45D6AB52B173DFD00A5B649 /* Patcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6AB42B173DFD00A5B649 /* Patcher.swift */; }; C45D6AB52B173DFD00A5B649 /* Patcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6AB42B173DFD00A5B649 /* Patcher.swift */; };
C45D6AB92B17499200A5B649 /* Model+CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6AB82B17499200A5B649 /* Model+CSV.swift */; }; C45D6AB92B17499200A5B649 /* Model+CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6AB82B17499200A5B649 /* Model+CSV.swift */; };
C45D6AC02B18A09900A5B649 /* Step+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6ABD2B18A09900A5B649 /* Step+CoreDataClass.swift */; };
C45D6AC12B18A09900A5B649 /* Step+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6ABE2B18A09900A5B649 /* Step+CoreDataProperties.swift */; };
C45D6AC22B18A13C00A5B649 /* Step+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6ABD2B18A09900A5B649 /* Step+CoreDataClass.swift */; };
C45D6AC32B18A13C00A5B649 /* Step+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6ABE2B18A09900A5B649 /* Step+CoreDataProperties.swift */; };
C45D6AC42B18A13C00A5B649 /* Step+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6ABD2B18A09900A5B649 /* Step+CoreDataClass.swift */; };
C45D6AC52B18A13C00A5B649 /* Step+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6ABE2B18A09900A5B649 /* Step+CoreDataProperties.swift */; };
C45D6AC82B18D02900A5B649 /* CountdownSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6AC72B18D02900A5B649 /* CountdownSequence.swift */; };
C45D6ACA2B18D08000A5B649 /* CountdownSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6AC72B18D02900A5B649 /* CountdownSequence.swift */; };
C45D6ACB2B18D08100A5B649 /* CountdownSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6AC72B18D02900A5B649 /* CountdownSequence.swift */; };
C4636D9C29AF46BD00994E31 /* ActivityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C4636D9B29AF46BD00994E31 /* ActivityKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; C4636D9C29AF46BD00994E31 /* ActivityKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C4636D9B29AF46BD00994E31 /* ActivityKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
C4636D9D29AF46D900994E31 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C438C7D02981216200BF3EF9 /* WidgetKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; C4636D9D29AF46D900994E31 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C438C7D02981216200BF3EF9 /* WidgetKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
C46926BD29DDC49E0003E310 /* SubscriptionButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46926BC29DDC49E0003E310 /* SubscriptionButtonView.swift */; }; C46926BD29DDC49E0003E310 /* SubscriptionButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46926BC29DDC49E0003E310 /* SubscriptionButtonView.swift */; };
@ -224,15 +233,9 @@
C4BA2B7329A60CF000CB4FBA /* Shortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B7229A60CF000CB4FBA /* Shortcut.swift */; }; C4BA2B7329A60CF000CB4FBA /* Shortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B7229A60CF000CB4FBA /* Shortcut.swift */; };
C4BA2B7929A65C1400CB4FBA /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B7829A65C1400CB4FBA /* UIDevice+Extensions.swift */; }; C4BA2B7929A65C1400CB4FBA /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B7829A65C1400CB4FBA /* UIDevice+Extensions.swift */; };
C4BCABB92A040B97009FFB0A /* QP01 0023 Surf moderate sandy.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BCABB82A040B97009FFB0A /* QP01 0023 Surf moderate sandy.wav */; }; C4BCABB92A040B97009FFB0A /* QP01 0023 Surf moderate sandy.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BCABB82A040B97009FFB0A /* QP01 0023 Surf moderate sandy.wav */; };
C4C826612B0E411A0036C666 /* TimeRange+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8265D2B0E411A0036C666 /* TimeRange+CoreDataClass.swift */; };
C4C826692B0E41D20036C666 /* TimeRange+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C826672B0E41D20036C666 /* TimeRange+CoreDataProperties.swift */; };
C4C8266A2B0E41D20036C666 /* TimeRange+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C826672B0E41D20036C666 /* TimeRange+CoreDataProperties.swift */; };
C4C8266B2B0E41D20036C666 /* TimeRange+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C826672B0E41D20036C666 /* TimeRange+CoreDataProperties.swift */; };
C4C8266C2B0E41D20036C666 /* Countdown+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C826682B0E41D20036C666 /* Countdown+CoreDataProperties.swift */; }; C4C8266C2B0E41D20036C666 /* Countdown+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C826682B0E41D20036C666 /* Countdown+CoreDataProperties.swift */; };
C4C8266D2B0E41D20036C666 /* Countdown+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C826682B0E41D20036C666 /* Countdown+CoreDataProperties.swift */; }; C4C8266D2B0E41D20036C666 /* Countdown+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C826682B0E41D20036C666 /* Countdown+CoreDataProperties.swift */; };
C4C8266E2B0E41D20036C666 /* Countdown+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C826682B0E41D20036C666 /* Countdown+CoreDataProperties.swift */; }; C4C8266E2B0E41D20036C666 /* Countdown+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C826682B0E41D20036C666 /* Countdown+CoreDataProperties.swift */; };
C4C8266F2B0E41DB0036C666 /* TimeRange+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8265D2B0E411A0036C666 /* TimeRange+CoreDataClass.swift */; };
C4C826702B0E41DB0036C666 /* TimeRange+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8265D2B0E411A0036C666 /* TimeRange+CoreDataClass.swift */; };
C4E5D66629B73AED008E7465 /* StartTimerIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D66429B73AED008E7465 /* StartTimerIntent.swift */; }; C4E5D66629B73AED008E7465 /* StartTimerIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D66429B73AED008E7465 /* StartTimerIntent.swift */; };
C4E5D66729B73AED008E7465 /* TimerIdentifierAppEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D66529B73AED008E7465 /* TimerIdentifierAppEntity.swift */; }; C4E5D66729B73AED008E7465 /* TimerIdentifierAppEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D66529B73AED008E7465 /* TimerIdentifierAppEntity.swift */; };
C4E5D66A29B73FC6008E7465 /* TimerShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D66929B73FC6008E7465 /* TimerShortcuts.swift */; }; C4E5D66A29B73FC6008E7465 /* TimerShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D66929B73FC6008E7465 /* TimerShortcuts.swift */; };
@ -422,6 +425,9 @@
C4556F7529E411A400DEB40B /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = "<group>"; }; C4556F7529E411A400DEB40B /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = "<group>"; };
C45D6AB42B173DFD00A5B649 /* Patcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patcher.swift; sourceTree = "<group>"; }; C45D6AB42B173DFD00A5B649 /* Patcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patcher.swift; sourceTree = "<group>"; };
C45D6AB82B17499200A5B649 /* Model+CSV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Model+CSV.swift"; sourceTree = "<group>"; }; C45D6AB82B17499200A5B649 /* Model+CSV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Model+CSV.swift"; sourceTree = "<group>"; };
C45D6ABD2B18A09900A5B649 /* Step+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Step+CoreDataClass.swift"; sourceTree = "<group>"; };
C45D6ABE2B18A09900A5B649 /* Step+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Step+CoreDataProperties.swift"; sourceTree = "<group>"; };
C45D6AC72B18D02900A5B649 /* CountdownSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownSequence.swift; sourceTree = "<group>"; };
C4636D9B29AF46BD00994E31 /* ActivityKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ActivityKit.framework; path = System/Library/Frameworks/ActivityKit.framework; sourceTree = SDKROOT; }; C4636D9B29AF46BD00994E31 /* ActivityKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ActivityKit.framework; path = System/Library/Frameworks/ActivityKit.framework; sourceTree = SDKROOT; };
C46926BC29DDC49E0003E310 /* SubscriptionButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionButtonView.swift; sourceTree = "<group>"; }; C46926BC29DDC49E0003E310 /* SubscriptionButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionButtonView.swift; sourceTree = "<group>"; };
C473C2F829A8DC0A0056B38A /* LaunchWidgetAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchWidgetAttributes.swift; sourceTree = "<group>"; }; C473C2F829A8DC0A0056B38A /* LaunchWidgetAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchWidgetAttributes.swift; sourceTree = "<group>"; };
@ -495,10 +501,6 @@
C4BA2B7829A65C1400CB4FBA /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = "<group>"; }; C4BA2B7829A65C1400CB4FBA /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = "<group>"; };
C4BCABB82A040B97009FFB0A /* QP01 0023 Surf moderate sandy.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = "QP01 0023 Surf moderate sandy.wav"; sourceTree = "<group>"; }; C4BCABB82A040B97009FFB0A /* QP01 0023 Surf moderate sandy.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = "QP01 0023 Surf moderate sandy.wav"; sourceTree = "<group>"; };
C4C8265A2B0E40350036C666 /* LeCountdown.0.6.6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.6.6.xcdatamodel; sourceTree = "<group>"; }; C4C8265A2B0E40350036C666 /* LeCountdown.0.6.6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.6.6.xcdatamodel; sourceTree = "<group>"; };
C4C8265D2B0E411A0036C666 /* TimeRange+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeRange+CoreDataClass.swift"; sourceTree = "<group>"; };
C4C826632B0E41610036C666 /* Countdown+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Countdown+CoreDataProperties.swift"; sourceTree = "<group>"; };
C4C826642B0E41610036C666 /* TimeRange+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeRange+CoreDataProperties.swift"; sourceTree = "<group>"; };
C4C826672B0E41D20036C666 /* TimeRange+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TimeRange+CoreDataProperties.swift"; sourceTree = "<group>"; };
C4C826682B0E41D20036C666 /* Countdown+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Countdown+CoreDataProperties.swift"; sourceTree = "<group>"; }; C4C826682B0E41D20036C666 /* Countdown+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Countdown+CoreDataProperties.swift"; sourceTree = "<group>"; };
C4E5D66429B73AED008E7465 /* StartTimerIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTimerIntent.swift; sourceTree = "<group>"; }; C4E5D66429B73AED008E7465 /* StartTimerIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTimerIntent.swift; sourceTree = "<group>"; };
C4E5D66529B73AED008E7465 /* TimerIdentifierAppEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerIdentifierAppEntity.swift; sourceTree = "<group>"; }; C4E5D66529B73AED008E7465 /* TimerIdentifierAppEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerIdentifierAppEntity.swift; sourceTree = "<group>"; };
@ -620,6 +622,7 @@
C45D6AB42B173DFD00A5B649 /* Patcher.swift */, C45D6AB42B173DFD00A5B649 /* Patcher.swift */,
C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */, C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */,
C4F8B15629891271005C86A5 /* Conductor.swift */, C4F8B15629891271005C86A5 /* Conductor.swift */,
C45D6AC72B18D02900A5B649 /* CountdownSequence.swift */,
C4F8B1D7298C0727005C86A5 /* TimerRouter.swift */, C4F8B1D7298C0727005C86A5 /* TimerRouter.swift */,
C438C80B2981DE2E00BF3EF9 /* Views */, C438C80B2981DE2E00BF3EF9 /* Views */,
C438C8092981DDF800BF3EF9 /* Model */, C438C8092981DDF800BF3EF9 /* Model */,
@ -838,8 +841,6 @@
C48920632B0E422E00F6F4D8 /* Recovered References */ = { C48920632B0E422E00F6F4D8 /* Recovered References */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C4C826632B0E41610036C666 /* Countdown+CoreDataProperties.swift */,
C4C826642B0E41610036C666 /* TimeRange+CoreDataProperties.swift */,
); );
name = "Recovered References"; name = "Recovered References";
sourceTree = "<group>"; sourceTree = "<group>";
@ -931,10 +932,10 @@
C4BA2AEE2996A11900CB4FBA /* CustomSound+CoreDataProperties.swift */, C4BA2AEE2996A11900CB4FBA /* CustomSound+CoreDataProperties.swift */,
C4F8B16E298AC234005C86A5 /* Record+CoreDataClass.swift */, C4F8B16E298AC234005C86A5 /* Record+CoreDataClass.swift */,
C4F8B16F298AC234005C86A5 /* Record+CoreDataProperties.swift */, C4F8B16F298AC234005C86A5 /* Record+CoreDataProperties.swift */,
C45D6ABD2B18A09900A5B649 /* Step+CoreDataClass.swift */,
C45D6ABE2B18A09900A5B649 /* Step+CoreDataProperties.swift */,
C4286E9F2A1502FD0070D075 /* Stopwatch+CoreDataClass.swift */, C4286E9F2A1502FD0070D075 /* Stopwatch+CoreDataClass.swift */,
C4BA2B48299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift */, C4BA2B48299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift */,
C4C8265D2B0E411A0036C666 /* TimeRange+CoreDataClass.swift */,
C4C826672B0E41D20036C666 /* TimeRange+CoreDataProperties.swift */,
); );
path = Generation; path = Generation;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1322,6 +1323,7 @@
C4E5D66629B73AED008E7465 /* StartTimerIntent.swift in Sources */, C4E5D66629B73AED008E7465 /* StartTimerIntent.swift in Sources */,
C4286EA62A150A7E0070D075 /* TimePickerView.swift in Sources */, C4286EA62A150A7E0070D075 /* TimePickerView.swift in Sources */,
C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */, C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */,
C45D6AC02B18A09900A5B649 /* Step+CoreDataClass.swift in Sources */,
C498E5A5299152B400E90DE0 /* GreenCheckmarkView.swift in Sources */, C498E5A5299152B400E90DE0 /* GreenCheckmarkView.swift in Sources */,
C4BA2B3E299FC86800CB4FBA /* StatisticsView.swift in Sources */, C4BA2B3E299FC86800CB4FBA /* StatisticsView.swift in Sources */,
C4F8B1B8298AC81D005C86A5 /* CountdownDialView.swift in Sources */, C4F8B1B8298AC81D005C86A5 /* CountdownDialView.swift in Sources */,
@ -1332,6 +1334,7 @@
C4BA2AFD299A3A3700CB4FBA /* AppleMusicPlayer.swift in Sources */, C4BA2AFD299A3A3700CB4FBA /* AppleMusicPlayer.swift in Sources */,
C4BA2B3A299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */, C4BA2B3A299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */,
C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */, C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */,
C45D6AC12B18A09900A5B649 /* Step+CoreDataProperties.swift in Sources */,
C45D6AB52B173DFD00A5B649 /* Patcher.swift in Sources */, C45D6AB52B173DFD00A5B649 /* Patcher.swift in Sources */,
C4556F6B29E40B7800DEB40B /* FileLogger.swift in Sources */, C4556F6B29E40B7800DEB40B /* FileLogger.swift in Sources */,
C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */, C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */,
@ -1362,8 +1365,6 @@
C4060DF5297AE9A7003FAB80 /* TimeInterval+Extensions.swift in Sources */, C4060DF5297AE9A7003FAB80 /* TimeInterval+Extensions.swift in Sources */,
C4F8B166298A9ABB005C86A5 /* SoundFormView.swift in Sources */, C4F8B166298A9ABB005C86A5 /* SoundFormView.swift in Sources */,
C4F8B17D298AC234005C86A5 /* Record+CoreDataProperties.swift in Sources */, C4F8B17D298AC234005C86A5 /* Record+CoreDataProperties.swift in Sources */,
C4C826612B0E411A0036C666 /* TimeRange+CoreDataClass.swift in Sources */,
C4C826692B0E41D20036C666 /* TimeRange+CoreDataProperties.swift in Sources */,
C4F8B184298AC234005C86A5 /* AbstractTimer+CoreDataClass.swift in Sources */, C4F8B184298AC234005C86A5 /* AbstractTimer+CoreDataClass.swift in Sources */,
C4F8B17A298AC234005C86A5 /* Countdown+CoreDataClass.swift in Sources */, C4F8B17A298AC234005C86A5 /* Countdown+CoreDataClass.swift in Sources */,
C42E970229E6B32B005B1B8C /* CalendarView.swift in Sources */, C42E970229E6B32B005B1B8C /* CalendarView.swift in Sources */,
@ -1391,6 +1392,7 @@
C4F8B1AC298AC3A0005C86A5 /* Alarm+CoreDataProperties.swift in Sources */, C4F8B1AC298AC3A0005C86A5 /* Alarm+CoreDataProperties.swift in Sources */,
C498E59F298D4DEA00E90DE0 /* LiveTimerListView.swift in Sources */, C498E59F298D4DEA00E90DE0 /* LiveTimerListView.swift in Sources */,
C4060DC0297AE73B003FAB80 /* LeCountdownApp.swift in Sources */, C4060DC0297AE73B003FAB80 /* LeCountdownApp.swift in Sources */,
C45D6AC82B18D02900A5B649 /* CountdownSequence.swift in Sources */,
C473C31829A926F50056B38A /* LaunchWidget.intentdefinition in Sources */, C473C31829A926F50056B38A /* LaunchWidget.intentdefinition in Sources */,
C4E5D66A29B73FC6008E7465 /* TimerShortcuts.swift in Sources */, C4E5D66A29B73FC6008E7465 /* TimerShortcuts.swift in Sources */,
C4742B5729840F6400D5D950 /* CoolPic.swift in Sources */, C4742B5729840F6400D5D950 /* CoolPic.swift in Sources */,
@ -1427,8 +1429,8 @@
C498E5A6299152C600E90DE0 /* GreenCheckmarkView.swift in Sources */, C498E5A6299152C600E90DE0 /* GreenCheckmarkView.swift in Sources */,
C4BA2B3B299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */, C4BA2B3B299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */,
C438C7EB2981266F00BF3EF9 /* SingleTimerView.swift in Sources */, C438C7EB2981266F00BF3EF9 /* SingleTimerView.swift in Sources */,
C4C826702B0E41DB0036C666 /* TimeRange+CoreDataClass.swift in Sources */,
C47C933729F01B7A00C780E2 /* Codable+Extensions.swift in Sources */, C47C933729F01B7A00C780E2 /* Codable+Extensions.swift in Sources */,
C45D6AC32B18A13C00A5B649 /* Step+CoreDataProperties.swift in Sources */,
C438C7D62981216200BF3EF9 /* LaunchWidgetBundle.swift in Sources */, C438C7D62981216200BF3EF9 /* LaunchWidgetBundle.swift in Sources */,
C4286EAE2A17753A0070D075 /* AppError.swift in Sources */, C4286EAE2A17753A0070D075 /* AppError.swift in Sources */,
C4F8B18B298AC288005C86A5 /* Record+CoreDataClass.swift in Sources */, C4F8B18B298AC288005C86A5 /* Record+CoreDataClass.swift in Sources */,
@ -1449,19 +1451,20 @@
C473C2FA29A8DC1E0056B38A /* LaunchWidgetAttributes.swift in Sources */, C473C2FA29A8DC1E0056B38A /* LaunchWidgetAttributes.swift in Sources */,
C4286EA42A1503330070D075 /* Stopwatch+CoreDataClass.swift in Sources */, C4286EA42A1503330070D075 /* Stopwatch+CoreDataClass.swift in Sources */,
C438C7E82981255D00BF3EF9 /* TimeInterval+Extensions.swift in Sources */, C438C7E82981255D00BF3EF9 /* TimeInterval+Extensions.swift in Sources */,
C45D6AC22B18A13C00A5B649 /* Step+CoreDataClass.swift in Sources */,
C4BA2B37299F82FF00CB4FBA /* Fakes.swift in Sources */, C4BA2B37299F82FF00CB4FBA /* Fakes.swift in Sources */,
C4F8B18F298AC288005C86A5 /* AbstractTimer+CoreDataClass.swift in Sources */, C4F8B18F298AC288005C86A5 /* AbstractTimer+CoreDataClass.swift in Sources */,
C438C7D82981216200BF3EF9 /* LaunchWidgetLiveActivity.swift in Sources */, C438C7D82981216200BF3EF9 /* LaunchWidgetLiveActivity.swift in Sources */,
C4F8B18C298AC288005C86A5 /* Alarm+CoreDataClass.swift in Sources */, C4F8B18C298AC288005C86A5 /* Alarm+CoreDataClass.swift in Sources */,
C438C8192982BFDB00BF3EF9 /* NSManagedContext+Extensions.swift in Sources */, C438C8192982BFDB00BF3EF9 /* NSManagedContext+Extensions.swift in Sources */,
C438C7DA2981216200BF3EF9 /* LaunchWidget.swift in Sources */, C438C7DA2981216200BF3EF9 /* LaunchWidget.swift in Sources */,
C45D6ACB2B18D08100A5B649 /* CountdownSequence.swift in Sources */,
C4BA2AF62996A4EF00CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */, C4BA2AF62996A4EF00CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */,
C4F8B192298AC288005C86A5 /* Activity+CoreDataProperties.swift in Sources */, C4F8B192298AC288005C86A5 /* Activity+CoreDataProperties.swift in Sources */,
C47C933629F01B6600C780E2 /* FileUtils.swift in Sources */, C47C933629F01B6600C780E2 /* FileUtils.swift in Sources */,
C4F8B18E298AC288005C86A5 /* AbstractTimer+CoreDataProperties.swift in Sources */, C4F8B18E298AC288005C86A5 /* AbstractTimer+CoreDataProperties.swift in Sources */,
C4A16DC829D311C800143D5E /* Extensions.swift in Sources */, C4A16DC829D311C800143D5E /* Extensions.swift in Sources */,
C4F8B1AE298AC451005C86A5 /* Alarm+CoreDataProperties.swift in Sources */, C4F8B1AE298AC451005C86A5 /* Alarm+CoreDataProperties.swift in Sources */,
C4C8266A2B0E41D20036C666 /* TimeRange+CoreDataProperties.swift in Sources */,
C4BA2B32299F75DE00CB4FBA /* DefaultView.swift in Sources */, C4BA2B32299F75DE00CB4FBA /* DefaultView.swift in Sources */,
C48ECC0829DAC45900DE5A66 /* AppGuard.swift in Sources */, C48ECC0829DAC45900DE5A66 /* AppGuard.swift in Sources */,
C438C8182982BFC100BF3EF9 /* Persistence.swift in Sources */, C438C8182982BFC100BF3EF9 /* Persistence.swift in Sources */,
@ -1476,6 +1479,7 @@
C49C346A29DECC7100AAC6FC /* LiveStopWatch.swift in Sources */, C49C346A29DECC7100AAC6FC /* LiveStopWatch.swift in Sources */,
C473C2F329A8DA6F0056B38A /* LiveTimer.swift in Sources */, C473C2F329A8DA6F0056B38A /* LiveTimer.swift in Sources */,
C4F8B1C6298ACC1F005C86A5 /* SoundPlayer.swift in Sources */, C4F8B1C6298ACC1F005C86A5 /* SoundPlayer.swift in Sources */,
C45D6ACA2B18D08000A5B649 /* CountdownSequence.swift in Sources */,
C4F8B1A2298AC288005C86A5 /* Record+CoreDataProperties.swift in Sources */, C4F8B1A2298AC288005C86A5 /* Record+CoreDataProperties.swift in Sources */,
C4F8B1B2298AC451005C86A5 /* Alarm+CoreDataProperties.swift in Sources */, C4F8B1B2298AC451005C86A5 /* Alarm+CoreDataProperties.swift in Sources */,
C473C2F229A8DA1F0056B38A /* CountdownScheduler.swift in Sources */, C473C2F229A8DA1F0056B38A /* CountdownScheduler.swift in Sources */,
@ -1485,7 +1489,6 @@
C4BA2B38299F82FF00CB4FBA /* Fakes.swift in Sources */, C4BA2B38299F82FF00CB4FBA /* Fakes.swift in Sources */,
C438C7F529812BB200BF3EF9 /* IntentHandler.swift in Sources */, C438C7F529812BB200BF3EF9 /* IntentHandler.swift in Sources */,
C473C33D29ACEC4F0056B38A /* Tip.swift in Sources */, C473C33D29ACEC4F0056B38A /* Tip.swift in Sources */,
C4C8266B2B0E41D20036C666 /* TimeRange+CoreDataProperties.swift in Sources */,
C4F8B19C298AC288005C86A5 /* AbstractTimer+CoreDataProperties.swift in Sources */, C4F8B19C298AC288005C86A5 /* AbstractTimer+CoreDataProperties.swift in Sources */,
C4556F7429E40EC500DEB40B /* Codable+Extensions.swift in Sources */, C4556F7429E40EC500DEB40B /* Codable+Extensions.swift in Sources */,
C4F8B1A3298AC288005C86A5 /* Activity+CoreDataClass.swift in Sources */, C4F8B1A3298AC288005C86A5 /* Activity+CoreDataClass.swift in Sources */,
@ -1495,8 +1498,8 @@
C438C802298132B900BF3EF9 /* LeCountdown.xcdatamodeld in Sources */, C438C802298132B900BF3EF9 /* LeCountdown.xcdatamodeld in Sources */,
C4F8B1A0298AC288005C86A5 /* Activity+CoreDataProperties.swift in Sources */, C4F8B1A0298AC288005C86A5 /* Activity+CoreDataProperties.swift in Sources */,
C42E96FE29E5B5CD005B1B8C /* Filter.swift in Sources */, C42E96FE29E5B5CD005B1B8C /* Filter.swift in Sources */,
C45D6AC52B18A13C00A5B649 /* Step+CoreDataProperties.swift in Sources */,
C438C81A2982BFF100BF3EF9 /* NSManagedContext+Extensions.swift in Sources */, C438C81A2982BFF100BF3EF9 /* NSManagedContext+Extensions.swift in Sources */,
C4C8266F2B0E41DB0036C666 /* TimeRange+CoreDataClass.swift in Sources */,
C4F8B19D298AC288005C86A5 /* AbstractTimer+CoreDataClass.swift in Sources */, C4F8B19D298AC288005C86A5 /* AbstractTimer+CoreDataClass.swift in Sources */,
C4BA2B3C299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */, C4BA2B3C299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */,
C4286EB22A1B75C60070D075 /* BoringContext.swift in Sources */, C4286EB22A1B75C60070D075 /* BoringContext.swift in Sources */,
@ -1513,6 +1516,7 @@
C4556F7229E40EBF00DEB40B /* FileLogger.swift in Sources */, C4556F7229E40EBF00DEB40B /* FileLogger.swift in Sources */,
C4F8B1B1298AC451005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */, C4F8B1B1298AC451005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */,
C473C2F129A8DA0B0056B38A /* Conductor.swift in Sources */, C473C2F129A8DA0B0056B38A /* Conductor.swift in Sources */,
C45D6AC42B18A13C00A5B649 /* Step+CoreDataClass.swift in Sources */,
C473C2FC29A8DC4B0056B38A /* Date+Extensions.swift in Sources */, C473C2FC29A8DC4B0056B38A /* Date+Extensions.swift in Sources */,
C4286EA32A1503320070D075 /* Stopwatch+CoreDataClass.swift in Sources */, C4286EA32A1503320070D075 /* Stopwatch+CoreDataClass.swift in Sources */,
C473C2F429A8DAE70056B38A /* Model+Extensions.swift in Sources */, C473C2F429A8DAE70056B38A /* Model+Extensions.swift in Sources */,

@ -29,64 +29,6 @@ enum CountdownState {
case cancelled case cancelled
} }
struct CountdownSequence: Codable {
var spans: [CountdownSpan]
var currentSpan: CountdownSpan {
let now = Date()
let current: CountdownSpan? = self.spans.first { span in
return span.interval.start < now && span.interval.end > now
}
return current ?? self.spans.last ?? CountdownSequence.defaultSpan
}
var currentEnd: Date {
return self.currentSpan.interval.end
}
var dateInterval: DateInterval {
let firstSpan = self.spans.first ?? CountdownSequence.defaultSpan
let lastSpan = self.spans.last ?? CountdownSequence.defaultSpan
return DateInterval(start: firstSpan.start, end: lastSpan.end)
}
var end: Date {
if let lastSpan = self.spans.last {
return lastSpan.end
} else {
fatalError("no spans")
}
}
private static let defaultSpan = CountdownSpan(interval: DateInterval(start: Date(), end: Date()), name: "none", index: 0, loopCount: 1)
}
struct CountdownSpan: Codable {
var interval: DateInterval
var name: String?
var index: Int16
var loopCount: Int16
var start: Date {
return self.interval.start
}
var end: Date {
return self.interval.end
}
var label: String {
var components: [String] = []
if let name {
components.append(name)
}
if loopCount > 1 {
components.append("#\(index + 1)")
}
return components.joined(separator: " ")
}
}
class Conductor: ObservableObject { class Conductor: ObservableObject {
static let maestro: Conductor = Conductor() static let maestro: Conductor = Conductor()
@ -98,7 +40,7 @@ class Conductor: ObservableObject {
fileprivate var beats: Timer? = nil fileprivate var beats: Timer? = nil
@UserDefault(PreferenceKey.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : CountdownSequence] @UserDefault(PreferenceKey.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : CountdownSequence]
@UserDefault(PreferenceKey.pausedCountdowns.rawValue, defaultValue: [:]) static var savedPausedCountdowns: [String : TimeInterval] // @UserDefault(PreferenceKey.pausedCountdowns.rawValue, defaultValue: [:]) static var savedPausedCountdowns: [String : Date]
@UserDefault(PreferenceKey.stopwatches.rawValue, defaultValue: [:]) static var savedStopwatches: [String : LiveStopWatch] @UserDefault(PreferenceKey.stopwatches.rawValue, defaultValue: [:]) static var savedStopwatches: [String : LiveStopWatch]
@Published private (set) var liveTimers: [LiveTimer] = [] @Published private (set) var liveTimers: [LiveTimer] = []
@ -108,7 +50,7 @@ class Conductor: ObservableObject {
init() { init() {
self.currentCountdowns = Conductor.savedCountdowns self.currentCountdowns = Conductor.savedCountdowns
self.currentStopwatches = Conductor.savedStopwatches self.currentStopwatches = Conductor.savedStopwatches
self.pausedCountdowns = Conductor.savedPausedCountdowns // self.pausedCountdowns = Conductor.savedPausedCountdowns
self.beats = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in self.beats = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in
self._cleanupCountdowns() self._cleanupCountdowns()
@ -128,14 +70,14 @@ class Conductor: ObservableObject {
} }
} }
@Published var pausedCountdowns: [String : TimeInterval] = [:] { // @Published var pausedCountdowns: [String : Date] = [:] {
didSet { // didSet {
Conductor.savedPausedCountdowns = pausedCountdowns // Conductor.savedPausedCountdowns = pausedCountdowns
withAnimation { // withAnimation {
self._buildLiveTimers() // self._buildLiveTimers()
} // }
} // }
} // }
@Published var currentStopwatches: [String : LiveStopWatch] = [:] { @Published var currentStopwatches: [String : LiveStopWatch] = [:] {
didSet { didSet {
@ -183,7 +125,7 @@ class Conductor: ObservableObject {
self.liveTimers.removeAll(where: { $0.id == id }) self.liveTimers.removeAll(where: { $0.id == id })
self.cancelledCountdowns.removeAll(where: { $0 == id }) self.cancelledCountdowns.removeAll(where: { $0 == id })
self.currentStopwatches.removeValue(forKey: id) self.currentStopwatches.removeValue(forKey: id)
self.pausedCountdowns.removeValue(forKey: id) // self.pausedCountdowns.removeValue(forKey: id)
if let soundPlayer = self._delayedSoundPlayers[id] { if let soundPlayer = self._delayedSoundPlayers[id] {
FileLogger.log("Stop sound player: \(self._timerName(id))") FileLogger.log("Stop sound player: \(self._timerName(id))")
soundPlayer.stop() soundPlayer.stop()
@ -222,35 +164,18 @@ class Conductor: ObservableObject {
do { do {
var totalDuration = 0.0 let totalDuration = try self._schedulePlayers(countdown: countdown)
var spans: [CountdownSpan] = []
let now = Date()
for i in 0..<countdown.loops {
for range in countdown.sortedRanges() {
let start = now.addingTimeInterval(totalDuration)
totalDuration += range.duration
let end = try self._scheduleSoundPlayer(countdown: countdown, range: range, in: totalDuration)
let dateInterval = DateInterval(start: start, end: end)
let span = CountdownSpan(interval: dateInterval, name: range.name, index: i, loopCount: countdown.loops)
spans.append(span)
}
}
self._scheduleCountdownNotification(countdown: countdown, in: totalDuration, handler: handler) self._scheduleCountdownNotification(countdown: countdown, in: totalDuration, handler: handler)
let sequence = CountdownSequence(spans: spans)
self.currentCountdowns[countdownId] = sequence
// TODO: live activity // TODO: live activity
self._launchLiveActivity(timer: countdown, date: sequence.end) let end = Date(timeIntervalSinceNow: totalDuration)
self._launchLiveActivity(timer: countdown, date: end)
if Preferences.playConfirmationSound { if Preferences.playConfirmationSound {
self._playConfirmationSound(timer: countdown) self._playConfirmationSound(timer: countdown)
} }
handler(.success(Date(timeIntervalSinceNow: totalDuration))) handler(.success(end))
} catch { } catch {
FileLogger.log("start error : \(error.localizedDescription)") FileLogger.log("start error : \(error.localizedDescription)")
Logger.error(error) Logger.error(error)
@ -259,19 +184,43 @@ class Conductor: ObservableObject {
} }
fileprivate func _schedulePlayers(countdown: Countdown) throws -> TimeInterval {
var totalDuration: TimeInterval = 0.0
var spans: [CountdownStep] = []
let now = Date()
for i in 0..<countdown.loops {
for step in countdown.sortedSteps() {
let start = now.addingTimeInterval(totalDuration)
totalDuration += step.duration
let end = try self._scheduleSoundPlayer(countdown: countdown, step: step, in: totalDuration)
let dateInterval = DateInterval(start: start, end: end)
let span = CountdownStep(interval: dateInterval, name: step.name, index: i, loopCount: countdown.loops, stepId: step.stringId)
spans.append(span)
}
}
let sequence = CountdownSequence(steps: spans)
self.currentCountdowns[countdown.stringId] = sequence
return totalDuration
}
fileprivate let idSeparator = "=&=" fileprivate let idSeparator = "=&="
fileprivate func _scheduleSoundPlayer(countdown: Countdown, range: TimeRange, in interval: TimeInterval) throws -> Date { fileprivate func _scheduleSoundPlayer(countdown: Countdown, step: Step, in interval: TimeInterval) throws -> Date {
let start = Date() let start = Date()
let end = start.addingTimeInterval(interval) let end = start.addingTimeInterval(interval)
// let countdownId = countdown.stringId // let countdownId = countdown.stringId
FileLogger.log("schedule countdown \(range.name ?? "''") at \(end)") FileLogger.log("schedule countdown \(step.name ?? "''") at \(end)")
let sound = range.someSound ?? countdown.someSound ?? Sound.default let sound = step.someSound ?? countdown.someSound ?? Sound.default
let idComponents = [countdown.stringId, range.stringId, interval.debugDescription] let idComponents = [countdown.stringId, step.stringId, interval.debugDescription]
let playerId = idComponents.joined(separator: idSeparator) let playerId = idComponents.joined(separator: idSeparator)
let soundPlayer = try DelaySoundPlayer(sound: sound) let soundPlayer = try DelaySoundPlayer(sound: sound)
self._delayedSoundPlayers[playerId] = soundPlayer self._delayedSoundPlayers[playerId] = soundPlayer
@ -299,7 +248,7 @@ class Conductor: ObservableObject {
self.cancelSoundPlayer(id: id) self.cancelSoundPlayer(id: id)
self._recordAndRemoveCountdown(countdownId: id, cancel: true) self._recordAndRemoveCountdown(countdownId: id, cancel: true)
self.pausedCountdowns.removeValue(forKey: id) // self.pausedCountdowns.removeValue(forKey: id)
if Preferences.playCancellationSound { if Preferences.playCancellationSound {
self._playCancellationSound() self._playCancellationSound()
@ -312,7 +261,7 @@ class Conductor: ObservableObject {
let id = countdown.stringId let id = countdown.stringId
if self.cancelledCountdowns.contains(id) { if self.cancelledCountdowns.contains(id) {
return .cancelled return .cancelled
} else if self.pausedCountdowns[id] != nil { } else if self.currentCountdowns[id]?.pauseDate != nil {
return .paused return .paused
} else if let end = self.currentCountdowns[id]?.end, end > Date() { } else if let end = self.currentCountdowns[id]?.end, end > Date() {
return .inprogress return .inprogress
@ -326,7 +275,10 @@ class Conductor: ObservableObject {
// } // }
func remainingPausedCountdownTime(_ countdown: Countdown) -> TimeInterval? { func remainingPausedCountdownTime(_ countdown: Countdown) -> TimeInterval? {
return self.pausedCountdowns[countdown.stringId] guard let sequence = self.currentCountdowns[countdown.stringId] else {
return nil
}
return sequence.remainingPausedCountdownTime()
} }
func pauseCountdown(id: TimerID) { func pauseCountdown(id: TimerID) {
@ -334,8 +286,11 @@ class Conductor: ObservableObject {
return return
} }
let remainingTime = sequence.currentSpan.end.timeIntervalSince(Date()) Logger.log("Pause countdown")
self.pausedCountdowns[id] = remainingTime sequence.pauseDate = Date()
// let remainingTime = sequence.currentSpan.end.timeIntervalSince(Date())
// self.pausedCountdowns[id] = Date() //remainingTime
// cancel stuff // cancel stuff
self.cancelCurrentNotifications(countdownId: id) self.cancelCurrentNotifications(countdownId: id)
@ -344,16 +299,38 @@ class Conductor: ObservableObject {
} }
func resumeCountdown(id: TimerID) throws { func resumeCountdown(id: TimerID) throws {
let now = Date()
let context = PersistenceController.shared.container.viewContext let context = PersistenceController.shared.container.viewContext
if let countdown: Countdown = context.object(stringId: id), if let countdown: Countdown = context.object(stringId: id),
let remainingTime = self.pausedCountdowns[id] { let sequence = self.currentCountdowns[countdown.stringId],
let pauseDate = sequence.pauseDate {
// let pauseDuration = now.timeIntervalSince(pauseDate)
// var steps: [CountdownStep] = []
for countdownStep in sequence.steps {
if countdownStep.end > pauseDate, let step = countdownStep.step(context: context) {
do {
let remainingTime = countdownStep.end.timeIntervalSince(pauseDate)
let _ = try self._scheduleSoundPlayer(countdown: countdown, step: step, in: remainingTime)
} catch {
Logger.error(error)
}
}
// steps.append(countdownStep.pauseAdjustedStep(duration: pauseDuration))
}
sequence.resume()
// self.currentCountdowns[countdown.stringId] = CountdownSequence(steps: steps)
// TODO: RESUME // TODO: RESUME
// _ = try self._scheduleSoundPlayer(countdown: countdown, in: remainingTime) // _ = try self._scheduleSoundPlayer(countdown: countdown, in: remainingTime)
self.pausedCountdowns.removeValue(forKey: id)
} else { } else {
throw AppError.timerNotFound(id: id) throw AppError.timerNotFound(id: id)
} }
// self.pausedCountdowns.removeValue(forKey: id)
} }
fileprivate func _recordAndRemoveCountdown(countdownId: String, cancel: Bool) { fileprivate func _recordAndRemoveCountdown(countdownId: String, cancel: Bool) {
@ -407,23 +384,41 @@ class Conductor: ObservableObject {
func restoreSoundPlayers() { func restoreSoundPlayers() {
for (countdownId, span) in self.currentCountdowns { for (countdownId, sequence) in self.currentCountdowns {
if !self._delayedSoundPlayers.contains(where: { $0.key == countdownId }) { if !self._delayedSoundPlayers.contains(where: { $0.key == countdownId }) {
let context = PersistenceController.shared.container.viewContext let context = PersistenceController.shared.container.viewContext
if let countdown: Countdown = context.object(stringId: countdownId) { if let countdown: Countdown = context.object(stringId: countdownId) {
do { let now = Date()
let sound: Sound = countdown.someSound ?? Sound.default for countdownStep in sequence.steps {
let soundPlayer = try DelaySoundPlayer(sound: sound)
self._delayedSoundPlayers[countdownId] = soundPlayer
// TODO: RESTORE if let step = countdownStep.step(context: context) {
// try soundPlayer.restore(for: span.interval.end, repeatCount: Int(countdown.repeatCount))
FileLogger.log("Restored sound player for \(self._timerName(countdownId))") let remainingTime = countdownStep.interval.end.timeIntervalSince(now)
} catch { if remainingTime > 0 {
Logger.error(error) do {
let _ = try self._scheduleSoundPlayer(countdown: countdown, step: step, in: remainingTime)
} catch {
Logger.error(error)
}
}
}
} }
// do {
//
// let sound: Sound = countdown.someSound ?? Sound.default
// let soundPlayer = try DelaySoundPlayer(sound: sound)
// self._delayedSoundPlayers[countdownId] = soundPlayer
//
// // TODO: RESTORE
// try soundPlayer.restore(for: sequence.interval.end, repeatCount: Int(countdown.repeatCount))
// FileLogger.log("Restored sound player for \(self._timerName(countdownId))")
// } catch {
// Logger.error(error)
// }
} }
} }
} }
@ -446,7 +441,7 @@ class Conductor: ObservableObject {
fileprivate func _cleanupCountdowns() { fileprivate func _cleanupCountdowns() {
let now = Date() let now = Date()
for (key, value) in self.currentCountdowns { for (key, value) in self.currentCountdowns {
if value.end < now || self.cancelledCountdowns.contains(key) { if (value.pauseDate == nil && value.end < now) || self.cancelledCountdowns.contains(key) {
self._recordAndRemoveCountdown(countdownId: key, cancel: false) self._recordAndRemoveCountdown(countdownId: key, cancel: false)
} }
} }
@ -562,17 +557,21 @@ class Conductor: ObservableObject {
} }
} }
fileprivate func _soundPlayers(id: TimerID) -> [TimerID : DelaySoundPlayer] {
return self._delayedSoundPlayers.filter { (key, value) in
key.starts(with: id)
}
}
func cancelSoundPlayer(id: TimerID) { func cancelSoundPlayer(id: TimerID) {
let keys = self._delayedSoundPlayers.keys.filter { $0.starts(with: id) } let players = self._soundPlayers(id: id)
for (key, player) in players {
for key in keys { player.stop()
if let soundPlayer = self._delayedSoundPlayers[key] { self._delayedSoundPlayers.removeValue(forKey: key)
soundPlayer.stop()
self._delayedSoundPlayers.removeValue(forKey: key)
}
} }
FileLogger.log("cancelled \(keys.count) sound players for \(self._timerName(id))")
FileLogger.log("cancelled \(players.count) sound players for \(self._timerName(id))")
self.deactivateAudioSessionIfPossible() self.deactivateAudioSessionIfPossible()
} }

@ -0,0 +1,118 @@
//
// CountdownSequence.swift
// LeCountdown
//
// Created by Laurent Morvillier on 30/11/2023.
//
import Foundation
import CoreData
class CountdownSequence: Codable {
var steps: [CountdownStep]
var pauseDate: Date? = nil
init(steps: [CountdownStep], pauseDate: Date? = nil) {
self.steps = steps
self.pauseDate = pauseDate
}
var currentSpan: CountdownStep {
let referenceDate = self.pauseDate ?? Date()
let current: CountdownStep? = self.steps.first { span in
return span.interval.start < referenceDate && span.interval.end > referenceDate
}
return current ?? self.steps.last ?? CountdownSequence.defaultSpan
}
var currentEnd: Date {
return self.currentSpan.interval.end
}
var dateInterval: DateInterval {
let firstSpan = self.steps.first ?? CountdownSequence.defaultSpan
let lastSpan = self.steps.last ?? CountdownSequence.defaultSpan
return DateInterval(start: firstSpan.start, end: lastSpan.end)
}
var end: Date {
if let lastSpan = self.steps.last {
return lastSpan.end
} else {
fatalError("no spans")
}
}
func remainingPausedCountdownTime() -> TimeInterval? {
if let pauseDate = self.pauseDate,
let currentEnd = self._endOfStep(for: pauseDate) {
return currentEnd.timeIntervalSince(pauseDate)
}
return nil
}
fileprivate func _endOfStep(for date: Date) -> Date? {
let step = self.steps.first { step in
return date > step.start && date < step.end
}
return step?.end
}
func resume() {
guard let pauseDate = self.pauseDate else { return }
let pauseDuration = Date().timeIntervalSince(pauseDate)
for step in self.steps {
step.pauseAdjustedStep(duration: pauseDuration)
}
self.pauseDate = nil
}
private static let defaultSpan = CountdownStep(interval: DateInterval(start: Date(), end: Date()), name: "none", index: 0, loopCount: 1, stepId: "")
}
class CountdownStep: Codable {
var interval: DateInterval
var name: String?
var index: Int16
var loopCount: Int16
var stepId: String
init(interval: DateInterval, name: String? = nil, index: Int16, loopCount: Int16, stepId: String) {
self.interval = interval
self.name = name
self.index = index
self.loopCount = loopCount
self.stepId = stepId
}
var start: Date {
return self.interval.start
}
var end: Date {
return self.interval.end
}
var label: String {
var components: [String] = []
if let name {
components.append(name)
}
if loopCount > 1 {
components.append("#\(index + 1)")
}
return components.joined(separator: " ")
}
func step(context: NSManagedObjectContext) -> Step? {
return context.object(stringId: self.stepId)
}
func pauseAdjustedStep(duration: TimeInterval) {
let start = self.interval.start.addingTimeInterval(duration)
let end = self.interval.end.addingTimeInterval(duration)
self.interval = DateInterval(start: start, end: end)
}
}

@ -13,9 +13,10 @@ extension Countdown {
static func fake(context: NSManagedObjectContext) -> Countdown { static func fake(context: NSManagedObjectContext) -> Countdown {
let cd = Countdown(context: context) let cd = Countdown(context: context)
let timeRange = TimeRange(context: context) let step = Step(context: context)
timeRange.duration = 5.0 step.duration = 5.0
timeRange.name = "Infusion" step.name = "Infusion"
cd.addToSteps(step)
let activity = Activity(context: context) let activity = Activity(context: context)
activity.name = "Tea" activity.name = "Tea"

@ -2,7 +2,7 @@
// Countdown+CoreDataProperties.swift // Countdown+CoreDataProperties.swift
// LeCountdown // LeCountdown
// //
// Created by Laurent Morvillier on 29/11/2023. // Created by Laurent Morvillier on 30/11/2023.
// //
// //
@ -18,23 +18,23 @@ extension Countdown {
@NSManaged public var loops: Int16 @NSManaged public var loops: Int16
@NSManaged public var duration: Double @NSManaged public var duration: Double
@NSManaged public var timeRanges: NSSet? @NSManaged public var steps: NSSet?
} }
// MARK: Generated accessors for timeRanges // MARK: Generated accessors for steps
extension Countdown { extension Countdown {
@objc(addTimeRangesObject:) @objc(addStepsObject:)
@NSManaged public func addToTimeRanges(_ value: TimeRange) @NSManaged public func addToSteps(_ value: Step)
@objc(removeTimeRangesObject:) @objc(removeStepsObject:)
@NSManaged public func removeFromTimeRanges(_ value: TimeRange) @NSManaged public func removeFromSteps(_ value: Step)
@objc(addTimeRanges:) @objc(addSteps:)
@NSManaged public func addToTimeRanges(_ values: NSSet) @NSManaged public func addToSteps(_ values: NSSet)
@objc(removeTimeRanges:) @objc(removeSteps:)
@NSManaged public func removeFromTimeRanges(_ values: NSSet) @NSManaged public func removeFromSteps(_ values: NSSet)
} }

@ -0,0 +1,15 @@
//
// Step+CoreDataClass.swift
// LeCountdown
//
// Created by Laurent Morvillier on 30/11/2023.
//
//
import Foundation
import CoreData
@objc(Step)
public class Step: NSManagedObject {
}

@ -1,8 +1,8 @@
// //
// TimeRange+CoreDataProperties.swift // Step+CoreDataProperties.swift
// LeCountdown // LeCountdown
// //
// Created by Laurent Morvillier on 23/11/2023. // Created by Laurent Morvillier on 30/11/2023.
// //
// //
@ -10,10 +10,10 @@ import Foundation
import CoreData import CoreData
extension TimeRange { extension Step {
@nonobjc public class func fetchRequest() -> NSFetchRequest<TimeRange> { @nonobjc public class func fetchRequest() -> NSFetchRequest<Step> {
return NSFetchRequest<TimeRange>(entityName: "TimeRange") return NSFetchRequest<Step>(entityName: "Step")
} }
@NSManaged public var duration: Double @NSManaged public var duration: Double
@ -24,6 +24,6 @@ extension TimeRange {
} }
extension TimeRange : Identifiable { extension Step : Identifiable {
} }

@ -1,15 +0,0 @@
//
// TimeRange+CoreDataClass.swift
// LeCountdown
//
// Created by Laurent Morvillier on 22/11/2023.
//
//
import Foundation
import CoreData
@objc(TimeRange)
public class TimeRange: NSManagedObject {
}

@ -20,7 +20,7 @@
<entity name="Countdown" representedClassName="Countdown" parentEntity="AbstractSoundTimer" syncable="YES"> <entity name="Countdown" representedClassName="Countdown" parentEntity="AbstractSoundTimer" syncable="YES">
<attribute name="duration" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/> <attribute name="duration" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="loops" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="YES"/> <attribute name="loops" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="YES"/>
<relationship name="timeRanges" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TimeRange" inverseName="countdown" inverseEntity="TimeRange"/> <relationship name="steps" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Step" inverseName="countdown" inverseEntity="Step"/>
</entity> </entity>
<entity name="CustomSound" representedClassName="CustomSound" syncable="YES"> <entity name="CustomSound" representedClassName="CustomSound" syncable="YES">
<attribute name="file" optional="YES" attributeType="String"/> <attribute name="file" optional="YES" attributeType="String"/>
@ -35,16 +35,16 @@
<attribute name="year" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/> <attribute name="year" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/>
<relationship name="activity" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Activity" inverseName="records" inverseEntity="Activity"/> <relationship name="activity" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Activity" inverseName="records" inverseEntity="Activity"/>
</entity> </entity>
<entity name="Stopwatch" representedClassName="Stopwatch" parentEntity="AbstractSoundTimer" syncable="YES"> <entity name="Step" representedClassName="Step" syncable="YES">
<attribute name="end" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sound" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/>
<attribute name="start" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
<entity name="TimeRange" representedClassName="TimeRange" syncable="YES">
<attribute name="duration" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/> <attribute name="duration" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/> <attribute name="name" optional="YES" attributeType="String"/>
<attribute name="order" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="order" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="playableIds" optional="YES" attributeType="String"/> <attribute name="playableIds" optional="YES" attributeType="String"/>
<relationship name="countdown" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Countdown" inverseName="timeRanges" inverseEntity="Countdown"/> <relationship name="countdown" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Countdown" inverseName="steps" inverseEntity="Countdown"/>
</entity>
<entity name="Stopwatch" representedClassName="Stopwatch" parentEntity="AbstractSoundTimer" syncable="YES">
<attribute name="end" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sound" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/>
<attribute name="start" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity> </entity>
</model> </model>

@ -188,13 +188,13 @@ extension CustomSound : Localized {
} }
extension TimeRange: StoresSound { extension Step: StoresSound {
static func fake(context: NSManagedObjectContext) -> TimeRange { static func fake(context: NSManagedObjectContext) -> Step {
let timeRange = TimeRange(context: context) let step = Step(context: context)
timeRange.duration = 30.0 step.duration = 30.0
timeRange.name = "Pause" step.name = "Pause"
return timeRange return step
} }
} }

@ -46,32 +46,32 @@ extension AbstractTimer {
extension Countdown { extension Countdown {
func sortedRanges() -> [TimeRange] { func sortedSteps() -> [Step] {
guard let ranges = self.timeRanges as? Set<TimeRange> else { guard let steps = self.steps as? Set<Step> else {
return [] return []
} }
return ranges.sorted(using: SortDescriptor(\TimeRange.order, order: .forward)) return steps.sorted(using: SortDescriptor(\Step.order, order: .forward))
} }
override var defaultName: String { override var defaultName: String {
return NSLocalizedString("Countdown", comment: "") return NSLocalizedString("Countdown", comment: "")
} }
var rangeCount: Int { var stepCount: Int {
return self.timeRanges?.count ?? 0 return self.steps?.count ?? 0
} }
var formattedDuration: String { var formattedDuration: String {
let durations: [String] = self.sortedRanges().map { $0.duration.hourMinuteSecond } let durations: [String] = self.sortedSteps().map { $0.duration.hourMinuteSecond }
var formatted: String var formatted: String
if durations.count > 1 { if durations.count > 1 {
formatted = durations.joined(separator: " / ") formatted = "\(durations.count) \(NSLocalizedString("Steps", comment: ""))"
} else { } else {
formatted = durations.first ?? "none" formatted = durations.first ?? "none"
} }
if self.loops > 1 { if self.loops > 1 {
return "\(formatted) * \(loops)" return "\(loops) * \(formatted)"
} else { } else {
return formatted return formatted
} }

@ -23,10 +23,10 @@ class Patcher {
let context = PersistenceController.shared.container.viewContext let context = PersistenceController.shared.container.viewContext
for patch in Patch.allCases { for patch in Patch.allCases {
if true { // if true {
if !UserDefaults.standard.bool(forKey: patch.key) {
Logger.log("PATCH!!!") Logger.log("PATCH!!!")
// if !UserDefaults.standard.bool(forKey: patch.key) {
do { do {
switch patch { switch patch {
case .interval: case .interval:
@ -48,12 +48,12 @@ class Patcher {
let countdowns: [Countdown] = try context.fetch(Countdown.fetchRequest()) let countdowns: [Countdown] = try context.fetch(Countdown.fetchRequest())
for countdown in countdowns { for countdown in countdowns {
if let ranges = countdown.timeRanges, ranges.count == 0 { if let steps = countdown.steps, steps.count == 0 {
let range = TimeRange(context: context) let step = Step(context: context)
range.duration = countdown.duration step.duration = countdown.duration
range.name = countdown.name step.name = countdown.name
range.order = 0 step.order = 0
countdown.addToTimeRanges(range) countdown.addToSteps(step)
} }
} }

@ -46,7 +46,7 @@ import AVFoundation
self._player.delegate = self self._player.delegate = self
// self._player.numberOfLoops = repeatCount // self._player.numberOfLoops = repeatCount
Logger.log("self._player.deviceCurrentTime = \(self._player.deviceCurrentTime)") // Logger.log("self._player.deviceCurrentTime = \(self._player.deviceCurrentTime)")
let time: TimeInterval = self._player.deviceCurrentTime + duration let time: TimeInterval = self._player.deviceCurrentTime + duration
let result = self._player.play(atTime: time) let result = self._player.play(atTime: time)
FileLogger.log("play \(String(describing: self._player.url)) >atTime: \(time.timeFormatted), result = \(result), isMainThread = \(Thread.isMainThread)") FileLogger.log("play \(String(describing: self._player.url)) >atTime: \(time.timeFormatted), result = \(result), isMainThread = \(Thread.isMainThread)")

@ -29,6 +29,7 @@ struct CountdownDialView: View, DialStyle {
Text(countdown.formattedDuration) Text(countdown.formattedDuration)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(self._durationColor) .foregroundColor(self._durationColor)
.multilineTextAlignment(.leading)
} }
Spacer() Spacer()
}.font(.system(size: self.dialFontSize)) }.font(.system(size: self.dialFontSize))

@ -29,7 +29,7 @@ struct CountdownFormView : View {
@State var showRangeSheet = false @State var showRangeSheet = false
@State var selectedRange: TimeRange? = nil @State var selectedStepItem: StepItem? = nil
var body: some View { var body: some View {
@ -46,14 +46,14 @@ struct CountdownFormView : View {
} }
Section { Section {
ForEach(self.model.ranges) { range in ForEach(self.model.stepItems) { stepItem in
Button { Button {
self.selectedRange = range self.selectedStepItem = stepItem
} label: { } label: {
LabeledContent(range.name ?? "", value: range.duration.hourMinuteSecond) LabeledContent(stepItem.name ?? "", value: stepItem.duration.hourMinuteSecond)
} }
}.onDelete { indexSet in }.onDelete { indexSet in
self.model.deleteRange(indexSet) self.model.deleteStep(indexSet: indexSet)
} }
} }
@ -98,16 +98,16 @@ struct CountdownFormView : View {
SoundFormView(model: self.model) SoundFormView(model: self.model)
} }
.sheet(item: self.$selectedRange, onDismiss: { .sheet(item: self.$selectedStepItem, onDismiss: {
self.model.objectWillChange.send() self.model.objectWillChange.send()
}) { item in }) { item in
RangeFormView(timeRange: item) RangeFormView(stepItem: item)
} }
} }
fileprivate func _addInterval() { fileprivate func _addInterval() {
self.selectedRange = self.model.addInterval(context: self.viewContext) self.selectedStepItem = self.model.addStepItem()
} }
func duration() -> TimeInterval { func duration() -> TimeInterval {

@ -79,7 +79,7 @@ struct CountdownEditView : View {
init(isPresented: Binding<Bool>, countdown: Countdown) { init(isPresented: Binding<Bool>, countdown: Countdown) {
_isPresented = isPresented _isPresented = isPresented
self.countdown = countdown self.countdown = countdown
self.hasRanges = countdown.rangeCount > 1 self.hasRanges = countdown.stepCount > 1
} }
init(isPresented: Binding<Bool>, preset: Preset) { init(isPresented: Binding<Bool>, preset: Preset) {
@ -195,16 +195,16 @@ struct CountdownEditView : View {
self.nameString = preset.localizedName self.nameString = preset.localizedName
self.duration = preset.duration self.duration = preset.duration
self.model.ranges = preset.ranges(context: self.viewContext) self.model.stepItems = preset.stepItems()
self.model.soundModel.loadPreset(preset) self.model.soundModel.loadPreset(preset)
} }
fileprivate func _loadCountdown(_ countdown: Countdown) { fileprivate func _loadCountdown(_ countdown: Countdown) {
let ranges = countdown.sortedRanges() let steps: [Step] = countdown.sortedSteps()
self.model.ranges = ranges self.model.stepItems = steps.map { $0.item }
if let range = ranges.first { if let range = steps.first {
self.duration = range.duration self.duration = range.duration
self.nameString = range.name ?? "" self.nameString = range.name ?? ""
} }
@ -234,8 +234,6 @@ struct CountdownEditView : View {
cd = Countdown(context: viewContext) cd = Countdown(context: viewContext)
} }
// cd.duration = self.duration
if self._isNewCountdown { if self._isNewCountdown {
let max: Int16 let max: Int16
do { do {
@ -257,22 +255,24 @@ struct CountdownEditView : View {
cd.setConfirmationSounds(self.model.confirmationSoundModel.sounds) cd.setConfirmationSounds(self.model.confirmationSoundModel.sounds)
cd.loops = self.model.loops cd.loops = self.model.loops
if self.model.ranges.count > 0 { for step in cd.sortedSteps() {
if let timeRanges = cd.timeRanges { viewContext.delete(step)
cd.removeFromTimeRanges(timeRanges) }
}
if self.model.stepItems.count > 0 {
for (index, range) in self.model.ranges.enumerated() { for (index, stepItem) in self.model.stepItems.enumerated() {
range.order = Int16(index)
range.duration = range.duration let step = stepItem.step(context: viewContext)
range.name = range.name step.order = Int16(index)
cd.addToTimeRanges(range) step.duration = stepItem.duration
step.name = stepItem.name
cd.addToSteps(step)
} }
} else { } else {
let timeRange = TimeRange(context: viewContext) let step = Step(context: viewContext)
timeRange.duration = self.duration step.duration = self.duration
timeRange.name = self.nameString step.name = self.nameString
cd.addToTimeRanges(timeRange) cd.addToSteps(step)
} }
if !self.nameString.isEmpty { if !self.nameString.isEmpty {
@ -321,6 +321,14 @@ struct CountdownEditView : View {
} }
fileprivate extension Step {
var item: StepItem {
return StepItem(name: self.name, duration: self.duration)
}
}
struct NewCountdownView_Previews: PreviewProvider { struct NewCountdownView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
NewCountdownView(isPresented: .constant(true), hasRanges: false) NewCountdownView(isPresented: .constant(true), hasRanges: false)

@ -11,7 +11,7 @@ struct RangeFormView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@ObservedObject var timeRange: TimeRange @ObservedObject var stepItem: StepItem
@State var namePlaceholder = "name" @State var namePlaceholder = "name"
@State var name: String = "" @State var name: String = ""
@ -43,20 +43,20 @@ struct RangeFormView: View {
} }
} }
}.onAppear { }.onAppear {
if let name = self.timeRange.name, !name.isEmpty { if let name = self.stepItem.name, !name.isEmpty {
self.namePlaceholder = name self.namePlaceholder = name
} }
self.duration = self.timeRange.duration self.duration = self.stepItem.duration
} }
} }
fileprivate func _doneHandler() { fileprivate func _doneHandler() {
if self.name.isEmpty { if self.name.isEmpty {
self.timeRange.name = self.namePlaceholder self.stepItem.name = self.namePlaceholder
} else { } else {
self.timeRange.name = self.name self.stepItem.name = self.name
} }
self.timeRange.duration = self.duration self.stepItem.duration = self.duration
self.dismiss() self.dismiss()
} }

@ -202,7 +202,7 @@ struct LiveCountdownView: View {
TimeView(text: NSLocalizedString("Cancelled", comment: "")) TimeView(text: NSLocalizedString("Cancelled", comment: ""))
} }
var name = self._nameForState(state: state) let name = self._nameForState(state: state)
Text(name.uppercased()) Text(name.uppercased())
.foregroundColor(self.colorScheme == .dark ? .white : .black) .foregroundColor(self.colorScheme == .dark ? .white : .black)

@ -289,15 +289,14 @@ enum Preset: Int, Identifiable, CaseIterable {
return CountdownIntervalGroup(repeatCount: 0, intervals: [CountdownInterval(duration: self.duration)]) return CountdownIntervalGroup(repeatCount: 0, intervals: [CountdownInterval(duration: self.duration)])
} }
func ranges(context: NSManagedObjectContext) -> [TimeRange] { func stepItems() -> [StepItem] {
switch self { switch self {
case .runningSplits: case .runningSplits:
return [] return []
default: default:
let timeRange = TimeRange(context: context) let step = StepItem(name: self.localizedName)
timeRange.name = self.localizedName step.duration = self.duration
timeRange.duration = self.duration return [step]
return [timeRange]
} }
} }

@ -152,9 +152,9 @@ class Customization: ObservableObject {
let countdown = Countdown(context: context) let countdown = Countdown(context: context)
countdown.activity = CoreDataRequests.getOrCreateActivity(name: preset.localizedName) countdown.activity = CoreDataRequests.getOrCreateActivity(name: preset.localizedName)
for range in preset.ranges(context: context) { for stepItem in preset.stepItems() {
range.duration = self.duration stepItem.duration = self.duration
countdown.addToTimeRanges(range) countdown.addToSteps(stepItem.step(context: context))
} }
countdown.playableIds = self.timerModel.soundModel.playableIds countdown.playableIds = self.timerModel.soundModel.playableIds

@ -15,30 +15,44 @@ protocol SoundHolder {
func selectPlaylist(_ playlist: Playlist, selected: Bool) func selectPlaylist(_ playlist: Playlist, selected: Bool)
} }
//struct StageItem { class StepItem: Identifiable, ObservableObject {
// var name: String? let id: String = UUID().uuidString
// var duration: TimeInterval var name: String? = nil
//} var duration: TimeInterval = 0.0
init(name: String? = nil, duration: TimeInterval = 0.0) {
self.name = name
self.duration = duration
}
func step(context: NSManagedObjectContext) -> Step {
let step = Step(context: context)
step.name = self.name
step.duration = self.duration
return step
}
}
class TimerModel: ObservableObject { class TimerModel: ObservableObject {
@Published var soundModel: SoundModel = SoundModel() @Published var soundModel: SoundModel = SoundModel()
@Published var confirmationSoundModel: SoundModel = SoundModel() @Published var confirmationSoundModel: SoundModel = SoundModel()
@Published var ranges: [TimeRange] = [] @Published var stepItems: [StepItem] = []
@Published var loops: Int16 = 1 @Published var loops: Int16 = 1
func addInterval(context: NSManagedObjectContext) -> TimeRange { func addStepItem() -> StepItem {
let timeRange = TimeRange(context: context) let index: String = (self.stepItems.count + 1).formatted()
let index: String = (self.ranges.count + 1).formatted() let name = NSLocalizedString("Step", comment: "") + " " + index
timeRange.name = NSLocalizedString("Step", comment: "") + " " + index let step = StepItem(name: name)
self.ranges.append(timeRange) self.stepItems.append(step)
return timeRange return step
} }
func deleteRange(indexSet: IndexSet, context: NSManagedObjectContext) { func deleteStep(indexSet: IndexSet) {
self.ranges.remove(atOffsets: indexSet) self.stepItems.remove(atOffsets: indexSet)
} }
} }

@ -4,6 +4,7 @@
"Play confirmation sound" = "Play sound on start"; "Play confirmation sound" = "Play sound on start";
"Create your first timer or stopwatch!" = "Create your first timer or stopwatch!"; "Create your first timer or stopwatch!" = "Create your first timer or stopwatch!";
"Step" = "Step"; "Step" = "Step";
"Steps" = "steps";
"Create an advanced timer" = "Create an advanced timer"; "Create an advanced timer" = "Create an advanced timer";
"Steps & repeat" = "Steps & repeat"; "Steps & repeat" = "Steps & repeat";
"Add interval" = "Add interval"; "Add interval" = "Add step";

@ -293,6 +293,7 @@
"Write a review!" = "Écrivez un avis !"; "Write a review!" = "Écrivez un avis !";
"Adjust volume on launch" = "Ajuster le volume au lancement"; "Adjust volume on launch" = "Ajuster le volume au lancement";
"Step" = "Étape"; "Step" = "Étape";
"Steps" = "étapes";
"Create an advanced timer" = "Créer un minuteur avancé"; "Create an advanced timer" = "Créer un minuteur avancé";
"Steps & repeat" = "Intervalles et répétitions"; "Steps & repeat" = "Intervalles et répétitions";
"Add interval" = "Ajouter un intervalle"; "Add interval" = "Ajouter une étape";

@ -1,15 +0,0 @@
//
// TimeRange+CoreDataClass.swift
// LeCountdown
//
// Created by Laurent Morvillier on 23/11/2023.
//
//
import Foundation
import CoreData
@objc(TimeRange)
public class TimeRange: NSManagedObject {
}

@ -1,29 +0,0 @@
//
// TimeRange+CoreDataProperties.swift
// LeCountdown
//
// Created by Laurent Morvillier on 23/11/2023.
//
//
import Foundation
import CoreData
extension TimeRange {
@nonobjc public class func fetchRequest() -> NSFetchRequest<TimeRange> {
return NSFetchRequest<TimeRange>(entityName: "TimeRange")
}
@NSManaged public var duration: Double
@NSManaged public var name: String?
@NSManaged public var order: Int16
@NSManaged public var soundList: String?
@NSManaged public var countdown: Countdown?
}
extension TimeRange : Identifiable {
}
Loading…
Cancel
Save