Make the restore / resume work

splits
Laurent 2 years ago
parent a60750ce97
commit 2871a7abaa
  1. 44
      LeCountdown.xcodeproj/project.pbxproj
  2. 233
      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. 16
      LeCountdown/Patcher.swift
  13. 2
      LeCountdown/Sound/DelaySoundPlayer.swift
  14. 1
      LeCountdown/Views/Countdown/CountdownDialView.swift
  15. 16
      LeCountdown/Views/Countdown/CountdownFormView.swift
  16. 46
      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 */; };
C45D6AB52B173DFD00A5B649 /* Patcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D6AB42B173DFD00A5B649 /* Patcher.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, ); }; };
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 */; };
@ -224,15 +233,9 @@
C4BA2B7329A60CF000CB4FBA /* Shortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B7229A60CF000CB4FBA /* Shortcut.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 */; };
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 */; };
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 */; };
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 */; };
C4E5D66729B73AED008E7465 /* TimerIdentifierAppEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5D66529B73AED008E7465 /* TimerIdentifierAppEntity.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>"; };
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>"; };
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; };
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>"; };
@ -495,10 +501,6 @@
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>"; };
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>"; };
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>"; };
@ -620,6 +622,7 @@
C45D6AB42B173DFD00A5B649 /* Patcher.swift */,
C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */,
C4F8B15629891271005C86A5 /* Conductor.swift */,
C45D6AC72B18D02900A5B649 /* CountdownSequence.swift */,
C4F8B1D7298C0727005C86A5 /* TimerRouter.swift */,
C438C80B2981DE2E00BF3EF9 /* Views */,
C438C8092981DDF800BF3EF9 /* Model */,
@ -838,8 +841,6 @@
C48920632B0E422E00F6F4D8 /* Recovered References */ = {
isa = PBXGroup;
children = (
C4C826632B0E41610036C666 /* Countdown+CoreDataProperties.swift */,
C4C826642B0E41610036C666 /* TimeRange+CoreDataProperties.swift */,
);
name = "Recovered References";
sourceTree = "<group>";
@ -931,10 +932,10 @@
C4BA2AEE2996A11900CB4FBA /* CustomSound+CoreDataProperties.swift */,
C4F8B16E298AC234005C86A5 /* Record+CoreDataClass.swift */,
C4F8B16F298AC234005C86A5 /* Record+CoreDataProperties.swift */,
C45D6ABD2B18A09900A5B649 /* Step+CoreDataClass.swift */,
C45D6ABE2B18A09900A5B649 /* Step+CoreDataProperties.swift */,
C4286E9F2A1502FD0070D075 /* Stopwatch+CoreDataClass.swift */,
C4BA2B48299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift */,
C4C8265D2B0E411A0036C666 /* TimeRange+CoreDataClass.swift */,
C4C826672B0E41D20036C666 /* TimeRange+CoreDataProperties.swift */,
);
path = Generation;
sourceTree = "<group>";
@ -1322,6 +1323,7 @@
C4E5D66629B73AED008E7465 /* StartTimerIntent.swift in Sources */,
C4286EA62A150A7E0070D075 /* TimePickerView.swift in Sources */,
C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */,
C45D6AC02B18A09900A5B649 /* Step+CoreDataClass.swift in Sources */,
C498E5A5299152B400E90DE0 /* GreenCheckmarkView.swift in Sources */,
C4BA2B3E299FC86800CB4FBA /* StatisticsView.swift in Sources */,
C4F8B1B8298AC81D005C86A5 /* CountdownDialView.swift in Sources */,
@ -1332,6 +1334,7 @@
C4BA2AFD299A3A3700CB4FBA /* AppleMusicPlayer.swift in Sources */,
C4BA2B3A299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */,
C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */,
C45D6AC12B18A09900A5B649 /* Step+CoreDataProperties.swift in Sources */,
C45D6AB52B173DFD00A5B649 /* Patcher.swift in Sources */,
C4556F6B29E40B7800DEB40B /* FileLogger.swift in Sources */,
C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */,
@ -1362,8 +1365,6 @@
C4060DF5297AE9A7003FAB80 /* TimeInterval+Extensions.swift in Sources */,
C4F8B166298A9ABB005C86A5 /* SoundFormView.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 */,
C4F8B17A298AC234005C86A5 /* Countdown+CoreDataClass.swift in Sources */,
C42E970229E6B32B005B1B8C /* CalendarView.swift in Sources */,
@ -1391,6 +1392,7 @@
C4F8B1AC298AC3A0005C86A5 /* Alarm+CoreDataProperties.swift in Sources */,
C498E59F298D4DEA00E90DE0 /* LiveTimerListView.swift in Sources */,
C4060DC0297AE73B003FAB80 /* LeCountdownApp.swift in Sources */,
C45D6AC82B18D02900A5B649 /* CountdownSequence.swift in Sources */,
C473C31829A926F50056B38A /* LaunchWidget.intentdefinition in Sources */,
C4E5D66A29B73FC6008E7465 /* TimerShortcuts.swift in Sources */,
C4742B5729840F6400D5D950 /* CoolPic.swift in Sources */,
@ -1427,8 +1429,8 @@
C498E5A6299152C600E90DE0 /* GreenCheckmarkView.swift in Sources */,
C4BA2B3B299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */,
C438C7EB2981266F00BF3EF9 /* SingleTimerView.swift in Sources */,
C4C826702B0E41DB0036C666 /* TimeRange+CoreDataClass.swift in Sources */,
C47C933729F01B7A00C780E2 /* Codable+Extensions.swift in Sources */,
C45D6AC32B18A13C00A5B649 /* Step+CoreDataProperties.swift in Sources */,
C438C7D62981216200BF3EF9 /* LaunchWidgetBundle.swift in Sources */,
C4286EAE2A17753A0070D075 /* AppError.swift in Sources */,
C4F8B18B298AC288005C86A5 /* Record+CoreDataClass.swift in Sources */,
@ -1449,19 +1451,20 @@
C473C2FA29A8DC1E0056B38A /* LaunchWidgetAttributes.swift in Sources */,
C4286EA42A1503330070D075 /* Stopwatch+CoreDataClass.swift in Sources */,
C438C7E82981255D00BF3EF9 /* TimeInterval+Extensions.swift in Sources */,
C45D6AC22B18A13C00A5B649 /* Step+CoreDataClass.swift in Sources */,
C4BA2B37299F82FF00CB4FBA /* Fakes.swift in Sources */,
C4F8B18F298AC288005C86A5 /* AbstractTimer+CoreDataClass.swift in Sources */,
C438C7D82981216200BF3EF9 /* LaunchWidgetLiveActivity.swift in Sources */,
C4F8B18C298AC288005C86A5 /* Alarm+CoreDataClass.swift in Sources */,
C438C8192982BFDB00BF3EF9 /* NSManagedContext+Extensions.swift in Sources */,
C438C7DA2981216200BF3EF9 /* LaunchWidget.swift in Sources */,
C45D6ACB2B18D08100A5B649 /* CountdownSequence.swift in Sources */,
C4BA2AF62996A4EF00CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */,
C4F8B192298AC288005C86A5 /* Activity+CoreDataProperties.swift in Sources */,
C47C933629F01B6600C780E2 /* FileUtils.swift in Sources */,
C4F8B18E298AC288005C86A5 /* AbstractTimer+CoreDataProperties.swift in Sources */,
C4A16DC829D311C800143D5E /* Extensions.swift in Sources */,
C4F8B1AE298AC451005C86A5 /* Alarm+CoreDataProperties.swift in Sources */,
C4C8266A2B0E41D20036C666 /* TimeRange+CoreDataProperties.swift in Sources */,
C4BA2B32299F75DE00CB4FBA /* DefaultView.swift in Sources */,
C48ECC0829DAC45900DE5A66 /* AppGuard.swift in Sources */,
C438C8182982BFC100BF3EF9 /* Persistence.swift in Sources */,
@ -1476,6 +1479,7 @@
C49C346A29DECC7100AAC6FC /* LiveStopWatch.swift in Sources */,
C473C2F329A8DA6F0056B38A /* LiveTimer.swift in Sources */,
C4F8B1C6298ACC1F005C86A5 /* SoundPlayer.swift in Sources */,
C45D6ACA2B18D08000A5B649 /* CountdownSequence.swift in Sources */,
C4F8B1A2298AC288005C86A5 /* Record+CoreDataProperties.swift in Sources */,
C4F8B1B2298AC451005C86A5 /* Alarm+CoreDataProperties.swift in Sources */,
C473C2F229A8DA1F0056B38A /* CountdownScheduler.swift in Sources */,
@ -1485,7 +1489,6 @@
C4BA2B38299F82FF00CB4FBA /* Fakes.swift in Sources */,
C438C7F529812BB200BF3EF9 /* IntentHandler.swift in Sources */,
C473C33D29ACEC4F0056B38A /* Tip.swift in Sources */,
C4C8266B2B0E41D20036C666 /* TimeRange+CoreDataProperties.swift in Sources */,
C4F8B19C298AC288005C86A5 /* AbstractTimer+CoreDataProperties.swift in Sources */,
C4556F7429E40EC500DEB40B /* Codable+Extensions.swift in Sources */,
C4F8B1A3298AC288005C86A5 /* Activity+CoreDataClass.swift in Sources */,
@ -1495,8 +1498,8 @@
C438C802298132B900BF3EF9 /* LeCountdown.xcdatamodeld in Sources */,
C4F8B1A0298AC288005C86A5 /* Activity+CoreDataProperties.swift in Sources */,
C42E96FE29E5B5CD005B1B8C /* Filter.swift in Sources */,
C45D6AC52B18A13C00A5B649 /* Step+CoreDataProperties.swift in Sources */,
C438C81A2982BFF100BF3EF9 /* NSManagedContext+Extensions.swift in Sources */,
C4C8266F2B0E41DB0036C666 /* TimeRange+CoreDataClass.swift in Sources */,
C4F8B19D298AC288005C86A5 /* AbstractTimer+CoreDataClass.swift in Sources */,
C4BA2B3C299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */,
C4286EB22A1B75C60070D075 /* BoringContext.swift in Sources */,
@ -1513,6 +1516,7 @@
C4556F7229E40EBF00DEB40B /* FileLogger.swift in Sources */,
C4F8B1B1298AC451005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */,
C473C2F129A8DA0B0056B38A /* Conductor.swift in Sources */,
C45D6AC42B18A13C00A5B649 /* Step+CoreDataClass.swift in Sources */,
C473C2FC29A8DC4B0056B38A /* Date+Extensions.swift in Sources */,
C4286EA32A1503320070D075 /* Stopwatch+CoreDataClass.swift in Sources */,
C473C2F429A8DAE70056B38A /* Model+Extensions.swift in Sources */,

@ -29,64 +29,6 @@ enum CountdownState {
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 {
static let maestro: Conductor = Conductor()
@ -98,7 +40,7 @@ class Conductor: ObservableObject {
fileprivate var beats: Timer? = nil
@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]
@Published private (set) var liveTimers: [LiveTimer] = []
@ -108,7 +50,7 @@ class Conductor: ObservableObject {
init() {
self.currentCountdowns = Conductor.savedCountdowns
self.currentStopwatches = Conductor.savedStopwatches
self.pausedCountdowns = Conductor.savedPausedCountdowns
// self.pausedCountdowns = Conductor.savedPausedCountdowns
self.beats = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in
self._cleanupCountdowns()
@ -128,14 +70,14 @@ class Conductor: ObservableObject {
}
}
@Published var pausedCountdowns: [String : TimeInterval] = [:] {
didSet {
Conductor.savedPausedCountdowns = pausedCountdowns
withAnimation {
self._buildLiveTimers()
}
}
}
// @Published var pausedCountdowns: [String : Date] = [:] {
// didSet {
// Conductor.savedPausedCountdowns = pausedCountdowns
// withAnimation {
// self._buildLiveTimers()
// }
// }
// }
@Published var currentStopwatches: [String : LiveStopWatch] = [:] {
didSet {
@ -183,7 +125,7 @@ class Conductor: ObservableObject {
self.liveTimers.removeAll(where: { $0.id == id })
self.cancelledCountdowns.removeAll(where: { $0 == id })
self.currentStopwatches.removeValue(forKey: id)
self.pausedCountdowns.removeValue(forKey: id)
// self.pausedCountdowns.removeValue(forKey: id)
if let soundPlayer = self._delayedSoundPlayers[id] {
FileLogger.log("Stop sound player: \(self._timerName(id))")
soundPlayer.stop()
@ -222,35 +164,18 @@ class Conductor: ObservableObject {
do {
var totalDuration = 0.0
var spans: [CountdownSpan] = []
let now = Date()
for i in 0..<countdown.loops {
for range in countdown.sortedRanges() {
let start = now.addingTimeInterval(totalDuration)
let totalDuration = try self._schedulePlayers(countdown: countdown)
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)
let sequence = CountdownSequence(spans: spans)
self.currentCountdowns[countdownId] = sequence
// TODO: live activity
self._launchLiveActivity(timer: countdown, date: sequence.end)
let end = Date(timeIntervalSinceNow: totalDuration)
self._launchLiveActivity(timer: countdown, date: end)
if Preferences.playConfirmationSound {
self._playConfirmationSound(timer: countdown)
}
handler(.success(Date(timeIntervalSinceNow: totalDuration)))
handler(.success(end))
} catch {
FileLogger.log("start error : \(error.localizedDescription)")
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 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 end = start.addingTimeInterval(interval)
// 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 soundPlayer = try DelaySoundPlayer(sound: sound)
self._delayedSoundPlayers[playerId] = soundPlayer
@ -299,7 +248,7 @@ class Conductor: ObservableObject {
self.cancelSoundPlayer(id: id)
self._recordAndRemoveCountdown(countdownId: id, cancel: true)
self.pausedCountdowns.removeValue(forKey: id)
// self.pausedCountdowns.removeValue(forKey: id)
if Preferences.playCancellationSound {
self._playCancellationSound()
@ -312,7 +261,7 @@ class Conductor: ObservableObject {
let id = countdown.stringId
if self.cancelledCountdowns.contains(id) {
return .cancelled
} else if self.pausedCountdowns[id] != nil {
} else if self.currentCountdowns[id]?.pauseDate != nil {
return .paused
} else if let end = self.currentCountdowns[id]?.end, end > Date() {
return .inprogress
@ -326,7 +275,10 @@ class Conductor: ObservableObject {
// }
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) {
@ -334,8 +286,11 @@ class Conductor: ObservableObject {
return
}
let remainingTime = sequence.currentSpan.end.timeIntervalSince(Date())
self.pausedCountdowns[id] = remainingTime
Logger.log("Pause countdown")
sequence.pauseDate = Date()
// let remainingTime = sequence.currentSpan.end.timeIntervalSince(Date())
// self.pausedCountdowns[id] = Date() //remainingTime
// cancel stuff
self.cancelCurrentNotifications(countdownId: id)
@ -344,16 +299,38 @@ class Conductor: ObservableObject {
}
func resumeCountdown(id: TimerID) throws {
let now = Date()
let context = PersistenceController.shared.container.viewContext
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
// _ = try self._scheduleSoundPlayer(countdown: countdown, in: remainingTime)
self.pausedCountdowns.removeValue(forKey: id)
} else {
throw AppError.timerNotFound(id: id)
}
// self.pausedCountdowns.removeValue(forKey: id)
}
fileprivate func _recordAndRemoveCountdown(countdownId: String, cancel: Bool) {
@ -407,20 +384,21 @@ class Conductor: ObservableObject {
func restoreSoundPlayers() {
for (countdownId, span) in self.currentCountdowns {
for (countdownId, sequence) in self.currentCountdowns {
if !self._delayedSoundPlayers.contains(where: { $0.key == countdownId }) {
let context = PersistenceController.shared.container.viewContext
if let countdown: Countdown = context.object(stringId: countdownId) {
do {
let sound: Sound = countdown.someSound ?? Sound.default
let soundPlayer = try DelaySoundPlayer(sound: sound)
self._delayedSoundPlayers[countdownId] = soundPlayer
let now = Date()
for countdownStep in sequence.steps {
if let step = countdownStep.step(context: context) {
// TODO: RESTORE
// 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)
if remainingTime > 0 {
do {
let _ = try self._scheduleSoundPlayer(countdown: countdown, step: step, in: remainingTime)
} catch {
Logger.error(error)
}
@ -428,6 +406,23 @@ class Conductor: ObservableObject {
}
}
// 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)
// }
}
}
}
}
// MARK: - Cleanup
@ -446,7 +441,7 @@ class Conductor: ObservableObject {
fileprivate func _cleanupCountdowns() {
let now = Date()
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)
}
}
@ -562,17 +557,21 @@ class Conductor: ObservableObject {
}
}
func cancelSoundPlayer(id: TimerID) {
fileprivate func _soundPlayers(id: TimerID) -> [TimerID : DelaySoundPlayer] {
return self._delayedSoundPlayers.filter { (key, value) in
key.starts(with: id)
}
}
let keys = self._delayedSoundPlayers.keys.filter { $0.starts(with: id) }
func cancelSoundPlayer(id: TimerID) {
for key in keys {
if let soundPlayer = self._delayedSoundPlayers[key] {
soundPlayer.stop()
let players = self._soundPlayers(id: id)
for (key, player) in players {
player.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()
}

@ -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 {
let cd = Countdown(context: context)
let timeRange = TimeRange(context: context)
timeRange.duration = 5.0
timeRange.name = "Infusion"
let step = Step(context: context)
step.duration = 5.0
step.name = "Infusion"
cd.addToSteps(step)
let activity = Activity(context: context)
activity.name = "Tea"

@ -2,7 +2,7 @@
// Countdown+CoreDataProperties.swift
// 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 duration: Double
@NSManaged public var timeRanges: NSSet?
@NSManaged public var steps: NSSet?
}
// MARK: Generated accessors for timeRanges
// MARK: Generated accessors for steps
extension Countdown {
@objc(addTimeRangesObject:)
@NSManaged public func addToTimeRanges(_ value: TimeRange)
@objc(addStepsObject:)
@NSManaged public func addToSteps(_ value: Step)
@objc(removeTimeRangesObject:)
@NSManaged public func removeFromTimeRanges(_ value: TimeRange)
@objc(removeStepsObject:)
@NSManaged public func removeFromSteps(_ value: Step)
@objc(addTimeRanges:)
@NSManaged public func addToTimeRanges(_ values: NSSet)
@objc(addSteps:)
@NSManaged public func addToSteps(_ values: NSSet)
@objc(removeTimeRanges:)
@NSManaged public func removeFromTimeRanges(_ values: NSSet)
@objc(removeSteps:)
@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
//
// Created by Laurent Morvillier on 23/11/2023.
// Created by Laurent Morvillier on 30/11/2023.
//
//
@ -10,10 +10,10 @@ import Foundation
import CoreData
extension TimeRange {
extension Step {
@nonobjc public class func fetchRequest() -> NSFetchRequest<TimeRange> {
return NSFetchRequest<TimeRange>(entityName: "TimeRange")
@nonobjc public class func fetchRequest() -> NSFetchRequest<Step> {
return NSFetchRequest<Step>(entityName: "Step")
}
@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">
<attribute name="duration" attributeType="Double" defaultValueString="0.0" 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 name="CustomSound" representedClassName="CustomSound" syncable="YES">
<attribute name="file" optional="YES" attributeType="String"/>
@ -35,16 +35,16 @@
<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"/>
</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 name="TimeRange" representedClassName="TimeRange" syncable="YES">
<entity name="Step" representedClassName="Step" syncable="YES">
<attribute name="duration" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="order" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<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>
</model>

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

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

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

@ -46,7 +46,7 @@ import AVFoundation
self._player.delegate = self
// 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 result = self._player.play(atTime: time)
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)
.fontWeight(.semibold)
.foregroundColor(self._durationColor)
.multilineTextAlignment(.leading)
}
Spacer()
}.font(.system(size: self.dialFontSize))

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

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

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

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

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

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

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

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

@ -293,6 +293,7 @@
"Write a review!" = "Écrivez un avis !";
"Adjust volume on launch" = "Ajuster le volume au lancement";
"Step" = "Étape";
"Steps" = "étapes";
"Create an advanced timer" = "Créer un minuteur avancé";
"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