wip for intervals

splits
Laurent 2 years ago
parent 786b6bb894
commit 83a74df852
  1. 2
      LeCountdown.xcodeproj/project.pbxproj
  2. 106
      LeCountdown/Conductor.swift
  3. 3
      LeCountdown/Model/Generation/AbstractSoundTimer+CoreDataProperties.swift
  4. 3
      LeCountdown/Model/Generation/Countdown+CoreDataProperties.swift
  5. 2
      LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.6.xcdatamodel/contents
  6. 1
      LeCountdown/Model/LiveTimer.swift
  7. 2
      LeCountdown/Model/Model+SharedExtensions.swift
  8. 6
      LeCountdown/Sound/DelaySoundPlayer.swift
  9. 2
      LeCountdown/Views/Alarm/NewAlarmView.swift
  10. 28
      LeCountdown/Views/Countdown/CountdownFormView.swift
  11. 9
      LeCountdown/Views/Countdown/NewCountdownView.swift
  12. 11
      LeCountdown/Views/Countdown/RangeFormView.swift
  13. 14
      LeCountdown/Views/LiveTimerListView.swift
  14. 15
      LeCountdown/Views/Reusable/SoundFormView.swift
  15. 15
      LeCountdown/Views/StartView.swift
  16. 8
      LeCountdown/Views/TimerModel.swift
  17. 4
      LeCountdown/en.lproj/Localizable.strings
  18. 6
      LeCountdown/fr.lproj/Localizable.strings

@ -802,6 +802,7 @@
C4E5D68929BB7953008E7465 /* SettingsView.swift */,
C4286EB62A1B98420070D075 /* StartView.swift */,
C4E5D68529BB369E008E7465 /* TimersView.swift */,
C4BA2ADA299549BC00CB4FBA /* TimerModel.swift */,
);
path = Views;
sourceTree = "<group>";
@ -979,7 +980,6 @@
C4F8B165298A9ABB005C86A5 /* SoundFormView.swift */,
C4BA2AD52993F62700CB4FBA /* SoundSelectionView.swift */,
C4286EA52A150A7E0070D075 /* TimePickerView.swift */,
C4BA2ADA299549BC00CB4FBA /* TimerModel.swift */,
C473C33829ACDBD70056B38A /* TipView.swift */,
C4BA2B2E299E69A000CB4FBA /* View+Extension.swift */,
C4742B5E2984205000D5D950 /* ViewModifiers.swift */,

@ -29,9 +29,49 @@ enum CountdownState {
case cancelled
}
struct CountdownSpan {
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 ?? CountdownSpan(interval: DateInterval(start: Date(), end: Date()), name: "none")
}
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")
}
struct CountdownSpan: Codable {
var interval: DateInterval
var name: String?
var start: Date {
return self.interval.start
}
var end: Date {
return self.interval.end
}
}
class Conductor: ObservableObject {
@ -44,7 +84,7 @@ class Conductor: ObservableObject {
fileprivate var beats: Timer? = nil
@UserDefault(PreferenceKey.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : DateInterval]
@UserDefault(PreferenceKey.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : CountdownSequence]
@UserDefault(PreferenceKey.pausedCountdowns.rawValue, defaultValue: [:]) static var savedPausedCountdowns: [String : TimeInterval]
@UserDefault(PreferenceKey.stopwatches.rawValue, defaultValue: [:]) static var savedStopwatches: [String : LiveStopWatch]
@ -57,14 +97,15 @@ class Conductor: ObservableObject {
self.currentStopwatches = Conductor.savedStopwatches
self.pausedCountdowns = Conductor.savedPausedCountdowns
self.beats = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { _ in
self.beats = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in
self._cleanupCountdowns()
self._buildLiveTimers()
})
}
@Published var cancelledCountdowns: [String] = []
@Published var currentCountdowns: [String : DateInterval] = [:] {
@Published var currentCountdowns: [String : CountdownSequence] = [:] {
didSet {
Conductor.savedCountdowns = currentCountdowns
// Logger.log("**** currentCountdowns didSet, count = \(currentCountdowns.count)")
@ -96,10 +137,10 @@ class Conductor: ObservableObject {
fileprivate func _buildLiveTimers() {
let liveCountdowns: [LiveTimer] = self.currentCountdowns.map {
return LiveTimer(id: $0, date: $1.end)
let liveCountdowns: [LiveTimer] = self.currentCountdowns.map { id, sequence in
let currentSpan = sequence.currentSpan
return LiveTimer(id: id, name: currentSpan.name, date: currentSpan.end)
}
// add countdown if not present
for liveCountdown in liveCountdowns {
if let index = self.liveTimers.firstIndex(where: { $0.id == liveCountdown.id }) {
@ -147,9 +188,9 @@ class Conductor: ObservableObject {
fileprivate func _recordActivity(countdownId: String, cancelled: Bool) {
let context = PersistenceController.shared.container.viewContext
if let countdown: Countdown = context.object(stringId: countdownId),
let dateInterval = self.currentCountdowns[countdownId] {
let sequence: CountdownSequence = self.currentCountdowns[countdownId] {
do {
try CoreDataRequests.recordActivity(timer: countdown, dateInterval: dateInterval, cancelled: cancelled)
try CoreDataRequests.recordActivity(timer: countdown, dateInterval: sequence.dateInterval, cancelled: cancelled)
} catch {
Logger.error(error)
// TODO: show error to user
@ -169,20 +210,29 @@ class Conductor: ObservableObject {
do {
var totalDuration = 0.0
var spans: [CountdownSpan] = []
let now = Date()
for _ in 0...countdown.repeatCount {
for _ in 0..<countdown.repeatCount {
for range in countdown.sortedRanges() {
// TODO: est-ce qu'on schedule tout ou en séquence ?
let start = now.addingTimeInterval(totalDuration)
totalDuration += range.duration
let end = try self._scheduleSoundPlayer(countdown: countdown, range: range, in: totalDuration)
self._scheduleCountdownNotification(countdown: countdown, in: totalDuration, handler: handler)
let dateInterval = DateInterval(start: Date(), end: end)
self.currentCountdowns[countdownId] = dateInterval
self._launchLiveActivity(timer: countdown, date: end)
let dateInterval = DateInterval(start: start, end: end)
let span = CountdownSpan(interval: dateInterval, name: range.name)
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)
if Preferences.playConfirmationSound {
self._playConfirmationSound(timer: countdown)
@ -205,8 +255,10 @@ class Conductor: ObservableObject {
FileLogger.log("schedule countdown \(range.name ?? "''") at \(end)")
let sound = range.someSound ?? countdown.someSound ?? Sound.default
let soundPlayer = try DelaySoundPlayer(timerID: countdownId, sound: sound)
self._delayedSoundPlayers[countdownId] = soundPlayer
let playerId = range.stringId + interval.debugDescription
let soundPlayer = try DelaySoundPlayer(sound: sound)
self._delayedSoundPlayers[playerId] = soundPlayer
try soundPlayer.start(in: interval,
repeatCount: Int(countdown.repeatCount))
@ -247,7 +299,7 @@ class Conductor: ObservableObject {
return .cancelled
} else if self.pausedCountdowns[id] != nil {
return .paused
} else if let interval = self.currentCountdowns[id], interval.end > Date() {
} else if let end = self.currentCountdowns[id]?.end, end > Date() {
return .inprogress
} else {
return .finished
@ -263,11 +315,11 @@ class Conductor: ObservableObject {
}
func pauseCountdown(id: TimerID) {
guard let interval = self.currentCountdowns[id] else {
guard let sequence = self.currentCountdowns[id] else {
return
}
let remainingTime = interval.end.timeIntervalSince(Date())
let remainingTime = sequence.currentSpan.end.timeIntervalSince(Date())
self.pausedCountdowns[id] = remainingTime
// cancel stuff
@ -280,7 +332,9 @@ class Conductor: ObservableObject {
let context = PersistenceController.shared.container.viewContext
if let countdown: Countdown = context.object(stringId: id),
let remainingTime = self.pausedCountdowns[id] {
_ = try self._scheduleSoundPlayer(countdown: countdown, in: remainingTime)
// TODO: RESUME
// _ = try self._scheduleSoundPlayer(countdown: countdown, in: remainingTime)
self.pausedCountdowns.removeValue(forKey: id)
} else {
throw AppError.timerNotFound(id: id)
@ -338,7 +392,7 @@ class Conductor: ObservableObject {
func restoreSoundPlayers() {
for (countdownId, interval) in self.currentCountdowns {
for (countdownId, span) in self.currentCountdowns {
if !self._delayedSoundPlayers.contains(where: { $0.key == countdownId }) {
let context = PersistenceController.shared.container.viewContext
@ -346,9 +400,11 @@ class Conductor: ObservableObject {
do {
let sound: Sound = countdown.someSound ?? Sound.default
let soundPlayer = try DelaySoundPlayer(timerID: countdownId, sound: sound)
let soundPlayer = try DelaySoundPlayer(sound: sound)
self._delayedSoundPlayers[countdownId] = soundPlayer
try soundPlayer.restore(for: interval.end, repeatCount: Int(countdown.repeatCount))
// TODO: RESTORE
// try soundPlayer.restore(for: span.interval.end, repeatCount: Int(countdown.repeatCount))
FileLogger.log("Restored sound player for \(self._timerName(countdownId))")
} catch {
Logger.error(error)

@ -2,7 +2,7 @@
// AbstractSoundTimer+CoreDataProperties.swift
// LeCountdown
//
// Created by Laurent Morvillier on 17/05/2023.
// Created by Laurent Morvillier on 24/11/2023.
//
//
@ -18,6 +18,5 @@ extension AbstractSoundTimer {
@NSManaged public var confirmationSoundList: String?
@NSManaged public var playableIds: String?
@NSManaged public var repeatCount: Int16
}

@ -2,7 +2,7 @@
// Countdown+CoreDataProperties.swift
// LeCountdown
//
// Created by Laurent Morvillier on 23/11/2023.
// Created by Laurent Morvillier on 24/11/2023.
//
//
@ -16,6 +16,7 @@ extension Countdown {
return NSFetchRequest<Countdown>(entityName: "Countdown")
}
@NSManaged public var repeatCount: Int16
@NSManaged public var timeRanges: NSSet?
}

@ -3,7 +3,6 @@
<entity name="AbstractSoundTimer" representedClassName="AbstractSoundTimer" isAbstract="YES" parentEntity="AbstractTimer" elementID="soundList" syncable="YES">
<attribute name="confirmationSoundList" optional="YES" attributeType="String"/>
<attribute name="playableIds" optional="YES" attributeType="String"/>
<attribute name="repeatCount" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
<entity name="AbstractTimer" representedClassName="AbstractTimer" isAbstract="YES" syncable="YES">
<attribute name="image" optional="YES" attributeType="String"/>
@ -19,6 +18,7 @@
<attribute name="fireDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
<entity name="Countdown" representedClassName="Countdown" parentEntity="AbstractSoundTimer" syncable="YES">
<attribute name="repeatCount" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="YES"/>
<relationship name="timeRanges" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TimeRange" inverseName="countdown" inverseEntity="TimeRange"/>
</entity>
<entity name="CustomSound" representedClassName="CustomSound" syncable="YES">

@ -10,6 +10,7 @@ import CoreData
struct LiveTimer: Identifiable, Comparable {
var id: String
var name: String?
var date: Date
var endDate: Date?

@ -64,7 +64,7 @@ extension Countdown {
var formattedDuration: String {
let durations: [String] = self.sortedRanges().map { $0.duration.hourMinuteSecond }
var formatted: String
if durations.count > 2 {
if durations.count > 1 {
formatted = durations.joined(separator: " / ")
} else {
formatted = durations.first ?? "none"

@ -12,12 +12,12 @@ import AVFoundation
fileprivate var _player: AVAudioPlayer
fileprivate var _timerID: TimerID
// fileprivate var _playerId: String
fileprivate var _timer: Timer? = nil
init(timerID: TimerID, sound: Sound) throws {
self._timerID = timerID
init(sound: Sound) throws {
// self._playerId = playerId
let soundFile = try sound.soundFile()

@ -183,7 +183,7 @@ struct AlarmEditView: View {
// a.setSounds(self.sounds)
// a.setPlaylists(self.playlists)
a.repeatCount = self.soundRepeatCount
// a.repeatCount = self.soundRepeatCount
if !self.nameString.isEmpty {

@ -26,11 +26,8 @@ struct CountdownFormView : View {
var durationBinding: Binding<TimeInterval>
var imageBinding: Binding<CoolPic>
var repeatCountBinding: Binding<Int16>
var hasRanges: Bool
var intervalRepeatBinding: Binding<Int>? = nil
@State var showRangeSheet = false
@State var selectedRange: TimeRange? = nil
@ -40,7 +37,7 @@ struct CountdownFormView : View {
Form {
if self.hasRanges {
Section(header: Text("Name for tracking the activity")) {
Section(header: Text("Name")) {
TextField("name", text: nameBinding)
.focused($focusedField, equals: .name)
.submitLabel(.continue)
@ -59,20 +56,29 @@ struct CountdownFormView : View {
}
}
Section {
HStack {
Stepper("Repeat Count", value: self.$model.repeatCount, in: 1...100)
Spacer()
Text(self.model.repeatCount.formatted())
.frame(width: 24.0)
}
}
Section {
Button {
self._addInterval()
} label: {
HStack {
Image(systemName: "plus.circle")
Text("Add range")
Text("Add interval")
}
}
}
} else {
Section(header: Text("Name for tracking the activity")) {
Section(header: Text("Name")) {
TextField("name", text: nameBinding)
.focused($focusedField, equals: .name)
.submitLabel(.continue)
@ -91,11 +97,11 @@ struct CountdownFormView : View {
SoundFormView(
model: self.model,
imageBinding: self.imageBinding,
repeatCountBinding: self.repeatCountBinding)
imageBinding: self.imageBinding)
}
.sheet(item: self.$selectedRange) { item in
RangeFormView(timeRange: item, selectedItem: self.$selectedRange)
RangeFormView(timeRange: item,
selectedItem: self.$selectedRange)
}
}
@ -117,9 +123,7 @@ struct CountdownFormView_Previews: PreviewProvider {
nameBinding: .constant(""),
durationBinding: .constant(0.0),
imageBinding: .constant(.pic3),
repeatCountBinding: .constant(2),
hasRanges: true,
intervalRepeatBinding: .constant(2))
hasRanges: true)
.environmentObject(TimerModel())
}
}

@ -55,7 +55,7 @@ struct CountdownEditView : View {
@State var nameString: String = ""
@State var duration: TimeInterval = 0.0
@State var soundRepeatCount: Int16 = 0
// @State var soundRepeatCount: Int16 = 0
@State var image: CoolPic = .pic1
@State var deleteConfirmationShown: Bool = false
@ -107,7 +107,6 @@ struct CountdownEditView : View {
nameBinding: $nameString,
durationBinding: $duration,
imageBinding: $image,
repeatCountBinding: $soundRepeatCount,
hasRanges: self.hasRanges)
.environmentObject(self.model)
@ -205,8 +204,6 @@ struct CountdownEditView : View {
fileprivate func _loadCountdown(_ countdown: Countdown) {
// self.duration = countdown.duration
let ranges = countdown.sortedRanges()
if ranges.count > 1 {
self.model.ranges = ranges
@ -222,7 +219,7 @@ struct CountdownEditView : View {
self.model.soundModel.setPlayables(countdown.playables)
self.model.confirmationSoundModel.setPlayables(countdown.confirmationPlayables)
self.soundRepeatCount = countdown.repeatCount
self.model.repeatCount = countdown.repeatCount
if let image = countdown.image,
let coolpic = CoolPic(rawValue: image) {
@ -266,7 +263,7 @@ struct CountdownEditView : View {
cd.image = self.image.rawValue
cd.playableIds = self.model.soundModel.playableIds
cd.setConfirmationSounds(self.model.confirmationSoundModel.sounds)
cd.repeatCount = self.soundRepeatCount
cd.repeatCount = self.model.repeatCount
if self.model.ranges.count > 0 {

@ -13,6 +13,7 @@ struct RangeFormView: View {
@Binding var selectedItem: TimeRange?
@State var namePlaceholder = "name"
@State var name: String = ""
@State var duration: TimeInterval = 0.0
@ -20,7 +21,7 @@ struct RangeFormView: View {
Form {
Section(header: Text("Name")) {
TextField("name", text: self.$name)
TextField(self.namePlaceholder, text: self.$name)
}
Section {
@ -32,7 +33,11 @@ struct RangeFormView: View {
Section {
Button {
if self.name.isEmpty {
self.timeRange.name = self.namePlaceholder
} else {
self.timeRange.name = self.name
}
self.timeRange.duration = self.duration
self.selectedItem = nil
} label: {
@ -44,8 +49,8 @@ struct RangeFormView: View {
}
}
}.onAppear {
if let name = self.timeRange.name {
self.name = name
if let name = self.timeRange.name, !name.isEmpty {
self.namePlaceholder = name
}
self.duration = self.timeRange.duration
}

@ -38,7 +38,10 @@ struct LiveTimerListView: View {
VStack {
ForEach(conductor.liveTimers) { liveTimer in
if let timer: AbstractTimer = liveTimer.timer(context: self.viewContext) {
LiveTimerView(timer: timer, date: liveTimer.date, endDate: liveTimer.endDate)
LiveTimerView(timer: timer,
date: liveTimer.date,
endDate: liveTimer.endDate,
name: liveTimer.name)
}
}
@ -71,12 +74,15 @@ struct LiveTimerView: View {
var timer: AbstractTimer
var date: Date
var endDate: Date?
var name: String?
var body: some View {
switch self.timer {
case let cd as Countdown:
LiveCountdownView(countdown: cd, date: self.date)
LiveCountdownView(countdown: cd,
date: self.date,
name: self.name)
case let sw as Stopwatch:
LiveStopwatchView(stopwatch: sw, date: self.date, endDate: self.endDate)
default:
@ -166,6 +172,7 @@ struct LiveCountdownView: View {
@State var countdown: Countdown
var date: Date
var name: String?
@State var showCancelConfirmationPopup: Bool = false
@ -200,7 +207,8 @@ struct LiveCountdownView: View {
TimeView(text: NSLocalizedString("Cancelled", comment: ""))
}
Text(self.countdown.displayName.uppercased())
let name = self.name ?? self.countdown.displayName
Text(name.uppercased())
.foregroundColor(self.colorScheme == .dark ? .white : .black)
}

@ -13,7 +13,7 @@ struct SoundFormView : View {
var imageBinding: Binding<CoolPic>
var repeatCountBinding: Binding<Int16>? = nil
// var repeatCountBinding: Binding<Int16>? = nil
var optionalSound: Binding<Bool>? = nil
@State var imageSelectionSheetShown: Bool = false
@ -37,16 +37,6 @@ struct SoundFormView : View {
catalog: .ring,
title: NSLocalizedString("Sound", comment: "") )
if self.repeatCountBinding != nil {
Picker("Repeat Count", selection: self.repeatCountBinding!) {
ForEach(0..<6) {
let count = Int16($0)
Text("\(count)").tag(count)
}
}
}
SoundLinkView(soundModel: self.model.confirmationSoundModel,
catalog: .confirmation,
title: NSLocalizedString("Start Sound", comment: ""))
@ -83,8 +73,7 @@ struct SoundImageFormView_Previews: PreviewProvider {
static var previews: some View {
Form {
SoundFormView(model: TimerModel(),
imageBinding: .constant(.pic1),
repeatCountBinding: .constant(2))
imageBinding: .constant(.pic1))
}
}
}

@ -22,7 +22,7 @@ struct StartView: View {
VStack(spacing: 0.5) {
Text("Select some of the predefined timers and customize them, or create your own")
.font(.callout)
.fontWeight(.medium)
.frame(maxWidth: .infinity)
.padding()
.background(Color.accentColor)
@ -44,7 +44,7 @@ struct StartView: View {
Button {
self.showMultiTimerScreen = true
} label: {
ImageButton(stringKey: "Create a timer with phases", systemImage: "plus.square.on.square")
ImageButton(stringKey: "Create an advanced timer", detailsStringKey: "Steps & repeat", systemImage: "crown")
}
.sheet(isPresented: self.$showMultiTimerScreen) {
NewCountdownView(isPresented: $showMultiTimerScreen, hasRanges: true)
@ -103,12 +103,21 @@ struct StartView: View {
struct ImageButton: View {
var stringKey: LocalizedStringKey
var detailsStringKey: LocalizedStringKey?
var systemImage: String
var body: some View {
HStack {
HStack(spacing: 10.0) {
Image(systemName: self.systemImage)
.fontWeight(.medium)
.frame(width: 30.0)
VStack(alignment: .leading) {
Text(self.stringKey)
.fontWeight(.bold)
if let detailsStringKey {
Text(detailsStringKey).font(.footnote)
}
}
Spacer()
}
.padding()

@ -20,16 +20,16 @@ class TimerModel: ObservableObject {
@Published var soundModel: SoundModel = SoundModel()
@Published var confirmationSoundModel: SoundModel = SoundModel()
// @Published var group: CountdownIntervalGroup =
// CountdownIntervalGroup(repeatCount: 0, intervals: [])
@Published var ranges: [TimeRange] = []
@Published var repeatCount: 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
self.ranges.append(timeRange)
return timeRange
// self.editedRange = timeRange
}
// @Published var editedRange: TimeRange? = nil

@ -3,3 +3,7 @@
"Widget Tip" = "Quickly launch your timers with widgets. You can add them by modifying your home or lock screen.";
"Play confirmation sound" = "Play sound on start";
"Create your first timer or stopwatch!" = "Create your first timer or stopwatch!";
"Step" = "Step";
"Create an advanced timer" = "Create an advanced timer";
"Steps & repeat" = "Steps & repeat";
"Add interval" = "Add interval";

@ -161,7 +161,7 @@
"Rename" = "Renommer";
/* No comment provided by engineer. */
"Repeat Count" = "Nombre de répétitions";
"Repeat Count" = "Nombre de boucles";
/* No comment provided by engineer. */
"Save" = "Sauvegarder";
@ -292,3 +292,7 @@
"tap to copy email" = "Tapez pour copier l'email";
"Write a review!" = "Écrivez un avis !";
"Adjust volume on launch" = "Ajuster le volume au lancement";
"Step" = "Étape";
"Create an advanced timer" = "Créer un minuteur avancé";
"Steps & repeat" = "Intervalles et répétitions";
"Add interval" = "Ajouter un intervalle";

Loading…
Cancel
Save