Laurent 2 years ago
parent b3447d2f00
commit 786b6bb894
  1. 30
      LaunchIntents/IntentHandler.swift
  2. 6
      LaunchWidget/SingleTimerView.swift
  3. 2
      LeCountdown/AppDelegate.swift
  4. 152
      LeCountdown/Conductor.swift
  5. 83
      LeCountdown/CountdownScheduler.swift
  6. 6
      LeCountdown/Model/Fakes.swift
  7. 3
      LeCountdown/Model/Generation/Countdown+CoreDataProperties.swift
  8. 4
      LeCountdown/Model/Generation/TimeRange+CoreDataProperties.swift
  9. 3
      LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.6.xcdatamodel/contents
  10. 68
      LeCountdown/Model/Model+Extensions.swift
  11. 23
      LeCountdown/Model/Model+SharedExtensions.swift
  12. 8
      LeCountdown/Model/NSManagedContext+Extensions.swift
  13. 2
      LeCountdown/TimerRouter.swift
  14. 2
      LeCountdown/Views/Countdown/CountdownDialView.swift
  15. 18
      LeCountdown/Views/Countdown/NewCountdownView.swift
  16. 10
      LeCountdown/Views/LiveTimerListView.swift
  17. 6
      LeCountdown/Views/StartView.swift

@ -8,33 +8,6 @@
import Intents import Intents
class IntentHandler: INExtension, SelectTimerIntentHandling { class IntentHandler: INExtension, SelectTimerIntentHandling {
//
// // MARK: - SelectTimerIntentHandling
//
// func resolveTimer(for intent: LaunchTimerIntent) async -> TimerIdentifierResolutionResult {
// if let timer = intent.timer {
// print("resolveTimer(for intent: LaunchTimerIntent) success !")
// return .success(with: timer)
// }
// print("resolveTimer(for intent: LaunchTimerIntent) needsValue")
// return .needsValue()
// }
//
// func handle(intent: LaunchTimerIntent) async -> LaunchTimerIntentResponse {
// if let timerIdentifier = intent.timer,
// let timerId = timerIdentifier.identifier,
// let timer = IntentDataProvider.main.timer(id: timerId) {
// do {
// let _ = try await TimerRouter.performAction(timer: timer)
// print("handle(intent: LaunchTimerIntent) success !")
// return .success(timer: timerIdentifier)
// } catch {
// return LaunchTimerIntentResponse(code: LaunchTimerIntentResponseCode(rawValue: 1)!, userActivity: nil)
// }
// }
// print("handle(intent: LaunchTimerIntent) no timer")
// return LaunchTimerIntentResponse(code: LaunchTimerIntentResponseCode(rawValue: 1)!, userActivity: nil)
// }
// MARK: - SelectTimerIntentHandling // MARK: - SelectTimerIntentHandling
@ -58,7 +31,7 @@ class IntentHandler: INExtension, SelectTimerIntentHandling {
let displayName: String let displayName: String
switch timer { switch timer {
case let countdown as Countdown: case let countdown as Countdown:
let formattedDuration = countdown.duration.hourMinuteSecond let formattedDuration = countdown.formattedDuration
if let name = timer.activity?.name, !name.isEmpty { if let name = timer.activity?.name, !name.isEmpty {
displayName = "\(name) (\(formattedDuration))" displayName = "\(name) (\(formattedDuration))"
} else { } else {
@ -86,7 +59,6 @@ class IntentHandler: INExtension, SelectTimerIntentHandling {
} }
} }
override func handler(for intent: INIntent) -> Any { override func handler(for intent: INIntent) -> Any {
// This is the default implementation. If you want different objects to handle different intents, // This is the default implementation. If you want different objects to handle different intents,
// you can override this and return the handler you want for that particular intent. // you can override this and return the handler you want for that particular intent.

@ -58,7 +58,7 @@ struct SingleTimerView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(timer.displayName.uppercased()) Text(timer.displayName.uppercased())
if let countdown = timer as? Countdown { if let countdown = timer as? Countdown {
Text(countdown.duration.hourMinuteSecond) Text(countdown.formattedDuration)
} }
} }
Spacer() Spacer()
@ -102,7 +102,7 @@ struct LockScreenCountdownView: View {
default: default:
Text(title) Text(title)
if let countdown = self.timer as? Countdown { if let countdown = self.timer as? Countdown {
Text(countdown.duration.hourMinuteSecond) Text(countdown.formattedDuration)
.monospaced() .monospaced()
} }
} }
@ -175,7 +175,7 @@ struct MultiCountdownView: View {
Spacer() Spacer()
Text(timer.displayName.uppercased()).lineLimit(1) Text(timer.displayName.uppercased()).lineLimit(1)
if let countdown = timer as? Countdown { if let countdown = timer as? Countdown {
Text(countdown.duration.hourMinuteSecond) Text(countdown.formattedDuration)
} }
Spacer() Spacer()
} }

@ -107,7 +107,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
} }
fileprivate func _timerId(notificationId: String) -> TimerID? { fileprivate func _timerId(notificationId: String) -> TimerID? {
let components = notificationId.components(separatedBy: CountdownScheduler.notificationIdSeparator) let components = notificationId.components(separatedBy: Conductor.notificationIdSeparator)
if components.count == 2 { if components.count == 2 {
return components[0] return components[0]
} else { } else {

@ -29,6 +29,11 @@ enum CountdownState {
case cancelled case cancelled
} }
struct CountdownSpan {
var interval: DateInterval
var name: String?
}
class Conductor: ObservableObject { class Conductor: ObservableObject {
static let maestro: Conductor = Conductor() static let maestro: Conductor = Conductor()
@ -87,25 +92,11 @@ class Conductor: ObservableObject {
} }
} }
func removeLiveTimer(id: TimerID) { static let notificationIdSeparator: String = "||"
// Logger.log("removeLiveTimer")
self.liveTimers.removeAll(where: { $0.id == id })
self.cancelledCountdowns.removeAll(where: { $0 == id })
self.currentStopwatches.removeValue(forKey: id)
self.pausedCountdowns.removeValue(forKey: id)
if let soundPlayer = self._delayedSoundPlayers[id] {
FileLogger.log("Stop sound player: \(self._timerName(id))")
soundPlayer.stop()
}
}
fileprivate func _cleanupLiveTimers() {
self.liveTimers.removeAll()
}
fileprivate func _buildLiveTimers() { fileprivate func _buildLiveTimers() {
let liveCountdowns = self.currentCountdowns.map { let liveCountdowns: [LiveTimer] = self.currentCountdowns.map {
return LiveTimer(id: $0, date: $1.end) return LiveTimer(id: $0, date: $1.end)
} }
@ -133,6 +124,22 @@ class Conductor: ObservableObject {
} }
func removeLiveTimer(id: TimerID) {
// Logger.log("removeLiveTimer")
self.liveTimers.removeAll(where: { $0.id == id })
self.cancelledCountdowns.removeAll(where: { $0 == id })
self.currentStopwatches.removeValue(forKey: id)
self.pausedCountdowns.removeValue(forKey: id)
if let soundPlayer = self._delayedSoundPlayers[id] {
FileLogger.log("Stop sound player: \(self._timerName(id))")
soundPlayer.stop()
}
}
fileprivate func _cleanupLiveTimers() {
self.liveTimers.removeAll()
}
func isCountdownCancelled(_ countdown: Countdown) -> Bool { func isCountdownCancelled(_ countdown: Countdown) -> Bool {
return self.cancelledCountdowns.contains(where: { $0 == countdown.stringId }) return self.cancelledCountdowns.contains(where: { $0 == countdown.stringId })
} }
@ -154,15 +161,33 @@ class Conductor: ObservableObject {
func startCountdown(countdown: Countdown, handler: @escaping (Result<Date?, Error>) -> Void) { func startCountdown(countdown: Countdown, handler: @escaping (Result<Date?, Error>) -> Void) {
self.cancelCurrentNotifications(countdownId: countdown.stringId)
let countdownId = countdown.stringId let countdownId = countdown.stringId
self._cleanupPreviousTimerIfNecessary(countdownId) self._cleanupPreviousTimerIfNecessary(countdownId)
do { do {
let end = try self._scheduleSoundPlayer(countdown: countdown, in: countdown.duration)
var totalDuration = 0.0
for _ in 0...countdown.repeatCount {
for range in countdown.sortedRanges() {
// TODO: est-ce qu'on schedule tout ou en séquence ?
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)
}
}
if Preferences.playConfirmationSound { if Preferences.playConfirmationSound {
self._playConfirmationSound(timer: countdown) self._playConfirmationSound(timer: countdown)
} }
handler(.success(end)) handler(.success(Date(timeIntervalSinceNow: totalDuration)))
} catch { } catch {
FileLogger.log("start error : \(error.localizedDescription)") FileLogger.log("start error : \(error.localizedDescription)")
Logger.error(error) Logger.error(error)
@ -171,26 +196,21 @@ class Conductor: ObservableObject {
} }
fileprivate func _scheduleSoundPlayer(countdown: Countdown, in interval: TimeInterval) throws -> Date { fileprivate func _scheduleSoundPlayer(countdown: Countdown, range: TimeRange, 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 \(self._timerName(countdownId)) at \(end)") FileLogger.log("schedule countdown \(range.name ?? "''") at \(end)")
let sound = countdown.someSound let sound = range.someSound ?? countdown.someSound ?? Sound.default
let soundPlayer = try DelaySoundPlayer(timerID: countdownId, sound: sound) let soundPlayer = try DelaySoundPlayer(timerID: countdownId, sound: sound)
self._delayedSoundPlayers[countdownId] = soundPlayer self._delayedSoundPlayers[countdownId] = soundPlayer
try soundPlayer.start(in: interval, try soundPlayer.start(in: interval,
repeatCount: Int(countdown.repeatCount)) repeatCount: Int(countdown.repeatCount))
let dateInterval = DateInterval(start: start, end: end)
self.currentCountdowns[countdownId] = dateInterval
self._launchLiveActivity(timer: countdown, date: end)
return end return end
} }
@ -205,7 +225,7 @@ class Conductor: ObservableObject {
func cancelCountdown(id: TimerID) { func cancelCountdown(id: TimerID) {
FileLogger.log("Cancel \(self._timerName(id))") FileLogger.log("Cancel \(self._timerName(id))")
CountdownScheduler.master.cancelCurrentNotifications(countdownId: id) self.cancelCurrentNotifications(countdownId: id)
self.currentCountdowns.removeValue(forKey: id) self.currentCountdowns.removeValue(forKey: id)
self.removeLiveTimer(id: id) self.removeLiveTimer(id: id)
@ -251,7 +271,7 @@ class Conductor: ObservableObject {
self.pausedCountdowns[id] = remainingTime self.pausedCountdowns[id] = remainingTime
// cancel stuff // cancel stuff
CountdownScheduler.master.cancelCurrentNotifications(countdownId: id) self.cancelCurrentNotifications(countdownId: id)
self.cancelSoundPlayer(id: id) self.cancelSoundPlayer(id: id)
self._endLiveActivity(timerId: id) self._endLiveActivity(timerId: id)
} }
@ -325,7 +345,7 @@ class Conductor: ObservableObject {
if let countdown: Countdown = context.object(stringId: countdownId) { if let countdown: Countdown = context.object(stringId: countdownId) {
do { do {
let sound: Sound = countdown.someSound let sound: Sound = countdown.someSound ?? Sound.default
let soundPlayer = try DelaySoundPlayer(timerID: countdownId, sound: sound) let soundPlayer = try DelaySoundPlayer(timerID: countdownId, sound: sound)
self._delayedSoundPlayers[countdownId] = soundPlayer self._delayedSoundPlayers[countdownId] = soundPlayer
try soundPlayer.restore(for: interval.end, repeatCount: Int(countdown.repeatCount)) try soundPlayer.restore(for: interval.end, repeatCount: Int(countdown.repeatCount))
@ -361,6 +381,60 @@ class Conductor: ObservableObject {
} }
} }
// MARK: - Notifications
fileprivate func _scheduleCountdownNotification(countdown: Countdown, in duration: TimeInterval, handler: @escaping (Result<Date?, Error>) -> Void) {
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("It's time!", comment: "")
// let duration = countdown.duration
let body: String
if let name = countdown.activity?.name {
let timesup = NSLocalizedString("Time's up for %@!", comment: "")
body = String(format: timesup, name)
} else {
let timesup = NSLocalizedString("Your %@ countdown is over!", comment: "")
body = String(format: timesup, duration.hourMinuteSecond)
}
content.body = body
self._createNotification(countdown: countdown, in: duration, content: content, handler: handler)
// content.sound = UNNotificationSound.criticalSoundNamed(UNNotificationSoundName(rawValue: sound), withAudioVolume: 1.0)
// content.interruptionLevel = .critical
content.relevanceScore = 1.0
}
fileprivate func _createNotification(countdown: Countdown, in duration: TimeInterval, offset: TimeInterval = 0.0, content: UNMutableNotificationContent, handler: @escaping (Result<Date?, Error>) -> Void) {
// let duration = countdown.duration
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: duration + offset, repeats: false)
let identifier: String = [countdown.objectID.uriRepresentation().absoluteString, "\(offset)"].joined(separator: Conductor.notificationIdSeparator)
let request = UNNotificationRequest(identifier: identifier,
content: content,
trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
DispatchQueue.main.async {
if let error {
handler(.failure(error))
print("Scheduling error = \(error)")
}
}
}
}
func cancelCurrentNotifications(countdownId: String) {
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
let ids = requests.map { $0.identifier }.filter { $0.hasPrefix(countdownId) }
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids)
}
}
// MARK: - Sound // MARK: - Sound
fileprivate func _playSound(timerId: String) { fileprivate func _playSound(timerId: String) {
@ -464,16 +538,16 @@ class Conductor: ObservableObject {
// interaction.donate() // interaction.donate()
// } // }
fileprivate func _scheduleAppRefresh(countdown: Countdown) { // fileprivate func _scheduleAppRefresh(countdown: Countdown) {
let request = BGAppRefreshTaskRequest(identifier: BGTaskIdentifier.refresh.rawValue) // let request = BGAppRefreshTaskRequest(identifier: BGTaskIdentifier.refresh.rawValue)
request.earliestBeginDate = Date(timeIntervalSinceNow: countdown.duration) // request.earliestBeginDate = Date(timeIntervalSinceNow: countdown.duration)
do { // do {
try BGTaskScheduler.shared.submit(request) // try BGTaskScheduler.shared.submit(request)
print("request submitted with date: \(String(describing: request.earliestBeginDate))") // print("request submitted with date: \(String(describing: request.earliestBeginDate))")
} catch { // } catch {
Logger.error(error) // Logger.error(error)
} // }
} // }
// MARK: - Live Activity // MARK: - Live Activity

@ -8,70 +8,19 @@
import Foundation import Foundation
import UserNotifications import UserNotifications
class CountdownScheduler { //class CountdownScheduler {
//
static let master = CountdownScheduler() // static let master = CountdownScheduler()
//
static let notificationIdSeparator: String = "||" // static let notificationIdSeparator: String = "||"
//
func scheduleIfPossible(countdown: Countdown, handler: @escaping (Result<Date?, Error>) -> Void) { // func scheduleIfPossible(countdown: Countdown, handler: @escaping (Result<Date?, Error>) -> Void) {
DispatchQueue.main.async { // DispatchQueue.main.async {
self.cancelCurrentNotifications(countdownId: countdown.stringId) // self.cancelCurrentNotifications(countdownId: countdown.stringId)
Conductor.maestro.startCountdown(countdown: countdown, handler: handler) // Conductor.maestro.startCountdown(countdown: countdown, handler: handler)
self._scheduleCountdownNotification(countdown: countdown, handler: handler) // self._scheduleCountdownNotification(countdown: countdown, handler: handler)
} // }
} // }
//
fileprivate func _scheduleCountdownNotification(countdown: Countdown, handler: @escaping (Result<Date?, Error>) -> Void) { //
let content = UNMutableNotificationContent() //}
content.title = NSLocalizedString("It's time!", comment: "")
let duration = countdown.duration
let body: String
if let name = countdown.activity?.name {
let timesup = NSLocalizedString("Time's up for %@!", comment: "")
body = String(format: timesup, name)
} else {
let timesup = NSLocalizedString("Your %@ countdown is over!", comment: "")
body = String(format: timesup, duration.hourMinuteSecond)
}
content.body = body
self._createNotification(countdown: countdown, content: content, handler: handler)
// content.sound = UNNotificationSound.criticalSoundNamed(UNNotificationSoundName(rawValue: sound), withAudioVolume: 1.0)
// content.interruptionLevel = .critical
content.relevanceScore = 1.0
}
fileprivate func _createNotification(countdown: Countdown, offset: TimeInterval = 0.0, content: UNMutableNotificationContent, handler: @escaping (Result<Date?, Error>) -> Void) {
let duration = countdown.duration
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: duration + offset, repeats: false)
let identifier: String = [countdown.objectID.uriRepresentation().absoluteString, "\(offset)"].joined(separator: CountdownScheduler.notificationIdSeparator)
let request = UNNotificationRequest(identifier: identifier,
content: content,
trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
DispatchQueue.main.async {
if let error {
handler(.failure(error))
print("Scheduling error = \(error)")
}
}
}
}
func cancelCurrentNotifications(countdownId: String) {
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
let ids = requests.map { $0.identifier }.filter { $0.hasPrefix(countdownId) }
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids)
}
}
}

@ -12,7 +12,11 @@ extension Countdown {
static func fake(context: NSManagedObjectContext) -> Countdown { static func fake(context: NSManagedObjectContext) -> Countdown {
let cd = Countdown(context: context) let cd = Countdown(context: context)
cd.duration = 4 * 60.0
let timeRange = TimeRange(context: context)
timeRange.duration = 5.0
timeRange.name = "Infusion"
let activity = Activity(context: context) let activity = Activity(context: context)
activity.name = "Tea" activity.name = "Tea"
cd.activity = activity cd.activity = activity

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

@ -2,7 +2,7 @@
// TimeRange+CoreDataProperties.swift // TimeRange+CoreDataProperties.swift
// LeCountdown // LeCountdown
// //
// Created by Laurent Morvillier on 22/11/2023. // Created by Laurent Morvillier on 23/11/2023.
// //
// //
@ -17,9 +17,9 @@ extension TimeRange {
} }
@NSManaged public var duration: Double @NSManaged public var duration: Double
@NSManaged public var soundList: String?
@NSManaged public var name: String? @NSManaged public var name: String?
@NSManaged public var order: Int16 @NSManaged public var order: Int16
@NSManaged public var playableIds: String?
@NSManaged public var countdown: Countdown? @NSManaged public var countdown: Countdown?
} }

@ -19,7 +19,6 @@
<attribute name="fireDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/> <attribute name="fireDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity> </entity>
<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" usesScalarValueType="YES"/>
<relationship name="timeRanges" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TimeRange" inverseName="countdown" inverseEntity="TimeRange"/> <relationship name="timeRanges" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TimeRange" inverseName="countdown" inverseEntity="TimeRange"/>
</entity> </entity>
<entity name="CustomSound" representedClassName="CustomSound" syncable="YES"> <entity name="CustomSound" representedClassName="CustomSound" syncable="YES">
@ -44,7 +43,7 @@
<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="soundList" 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="timeRanges" inverseEntity="Countdown"/>
</entity> </entity>
</model> </model>

@ -9,16 +9,16 @@ import Foundation
import SwiftUI import SwiftUI
import CoreData import CoreData
extension AbstractSoundTimer { protocol StoresSound: ManagedObject {
var playableIds: String? { get }
}
extension StoresSound {
var playables: [any Playable] { var playables: [any Playable] {
return playables(idList: self.playableIds) return playables(idList: self.playableIds)
} }
var confirmationPlayables: [any Playable] {
return playables(idList: self.confirmationSoundList)
}
func playables(idList: String?) -> [any Playable] { func playables(idList: String?) -> [any Playable] {
if let idList { if let idList {
var playables: [any Playable] = [] var playables: [any Playable] = []
@ -40,18 +40,7 @@ extension AbstractSoundTimer {
return self.playables.reduce(Set<Sound>()) { $0.union($1.soundList) } return self.playables.reduce(Set<Sound>()) { $0.union($1.soundList) }
} }
var confirmationSounds: Set<Sound> { var someSound: Sound? {
if let confirmationSoundList {
return Set(confirmationSoundList.enumItems())
}
return []
}
func setConfirmationSounds(_ sounds: Set<Sound>) {
self.confirmationSoundList = sounds.stringRepresentation
}
var someSound: Sound {
var sounds: Set<Sound> = self.allSounds var sounds: Set<Sound> = self.allSounds
if !AppGuard.main.isSubscriber { if !AppGuard.main.isSubscriber {
sounds = sounds.filter { !$0.isRestricted } sounds = sounds.filter { !$0.isRestricted }
@ -76,9 +65,50 @@ extension AbstractSoundTimer {
return random return random
} }
return Sound.default return nil
}
} }
extension AbstractSoundTimer: StoresSound {
// var playables: [any Playable] {
// return playables(idList: self.playableIds)
// }
var confirmationPlayables: [any Playable] {
return playables(idList: self.confirmationSoundList)
}
// func playables(idList: String?) -> [any Playable] {
// if let idList {
// var playables: [any Playable] = []
// let ids: [String] = idList.components(separatedBy: idSeparator)
// for id in ids {
// if let intId = numberFormatter.number(from: id)?.intValue,
// let sound = Sound(rawValue: intId) {
// playables.append(sound)
// } else if let playlist = Playlist(rawValue: id) {
// playables.append(playlist)
// }
// }
// return playables
// }
// return []
// }
var confirmationSounds: Set<Sound> {
if let confirmationSoundList {
return Set(confirmationSoundList.enumItems())
}
return []
}
func setConfirmationSounds(_ sounds: Set<Sound>) {
self.confirmationSoundList = sounds.stringRepresentation
}
} }
extension Stopwatch { extension Stopwatch {
@ -158,7 +188,7 @@ extension CustomSound : Localized {
} }
extension TimeRange { extension TimeRange: StoresSound {
static func fake(context: NSManagedObjectContext) -> TimeRange { static func fake(context: NSManagedObjectContext) -> TimeRange {
let timeRange = TimeRange(context: context) let timeRange = TimeRange(context: context)

@ -46,6 +46,13 @@ extension AbstractTimer {
extension Countdown { extension Countdown {
func sortedRanges() -> [TimeRange] {
guard let ranges = self.timeRanges as? Set<TimeRange> else {
return []
}
return ranges.sorted(using: SortDescriptor(\TimeRange.order, order: .forward))
}
override var defaultName: String { override var defaultName: String {
return NSLocalizedString("Countdown", comment: "") return NSLocalizedString("Countdown", comment: "")
} }
@ -54,6 +61,22 @@ extension Countdown {
return self.timeRanges?.count ?? 0 return self.timeRanges?.count ?? 0
} }
var formattedDuration: String {
let durations: [String] = self.sortedRanges().map { $0.duration.hourMinuteSecond }
var formatted: String
if durations.count > 2 {
formatted = durations.joined(separator: " / ")
} else {
formatted = durations.first ?? "none"
}
if self.repeatCount > 1 {
return "\(formatted) * \(repeatCount)"
} else {
return formatted
}
}
} }
extension Stopwatch { extension Stopwatch {

@ -63,7 +63,13 @@ extension NSManagedObjectContext {
} }
extension NSManagedObject { protocol ManagedObject {
var isTemporary: Bool { get }
var stringId: String { get }
static var entityName: String { get }
}
extension NSManagedObject : ManagedObject {
var isTemporary: Bool { var isTemporary: Bool {
return self.objectID.isTemporaryID return self.objectID.isTemporaryID

@ -59,7 +59,7 @@ class TimerRouter {
return return
} }
CountdownScheduler.master.scheduleIfPossible(countdown: countdown) { result in Conductor.maestro.startCountdown(countdown: countdown) { result in
switch result { switch result {
case .success: case .success:
handler(.success(Void())) handler(.success(Void()))

@ -26,7 +26,7 @@ struct CountdownDialView: View, DialStyle {
Text(countdown.activity?.name?.uppercased() ?? "") Text(countdown.activity?.name?.uppercased() ?? "")
.foregroundColor(self._titleColor) .foregroundColor(self._titleColor)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
Text(countdown.duration.hourMinuteSecond) Text(countdown.formattedDuration)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(self._durationColor) .foregroundColor(self._durationColor)
} }

@ -205,7 +205,16 @@ struct CountdownEditView : View {
fileprivate func _loadCountdown(_ countdown: Countdown) { fileprivate func _loadCountdown(_ countdown: Countdown) {
self.duration = countdown.duration // self.duration = countdown.duration
let ranges = countdown.sortedRanges()
if ranges.count > 1 {
self.model.ranges = ranges
} else if let range = ranges.first {
self.duration = range.duration
self.nameString = range.name ?? ""
}
if let name = countdown.activity?.name, !name.isEmpty { if let name = countdown.activity?.name, !name.isEmpty {
self.nameString = name self.nameString = name
} }
@ -235,7 +244,7 @@ struct CountdownEditView : View {
cd = Countdown(context: viewContext) cd = Countdown(context: viewContext)
} }
cd.duration = self.duration // cd.duration = self.duration
if self._isNewCountdown { if self._isNewCountdown {
let max: Int16 let max: Int16
@ -269,6 +278,11 @@ struct CountdownEditView : View {
range.order = Int16(index) range.order = Int16(index)
cd.addToTimeRanges(range) cd.addToTimeRanges(range)
} }
} else {
let timeRange = TimeRange(context: viewContext)
timeRange.duration = self.duration
timeRange.name = self.nameString
cd.addToTimeRanges(timeRange)
} }
if !self.nameString.isEmpty { if !self.nameString.isEmpty {

@ -200,16 +200,6 @@ struct LiveCountdownView: View {
TimeView(text: NSLocalizedString("Cancelled", comment: "")) TimeView(text: NSLocalizedString("Cancelled", comment: ""))
} }
// if cancelled {
// TimeView(text: NSLocalizedString("Cancelled", comment: ""))
// } else if let remainingTime {
// TimeView(text: remainingTime.hourMinuteSecond)
// } else if running {
// TimeView(text: self._formattedDuration(date: context.date))
// } else {
// TimeView(text: self.date.formatted(date: .omitted, time: .shortened))
// }
Text(self.countdown.displayName.uppercased()) Text(self.countdown.displayName.uppercased())
.foregroundColor(self.colorScheme == .dark ? .white : .black) .foregroundColor(self.colorScheme == .dark ? .white : .black)

@ -142,7 +142,11 @@ class Customization: ObservableObject {
let context = PersistenceController.shared.container.viewContext let context = PersistenceController.shared.container.viewContext
let countdown = Countdown(context: context) let countdown = Countdown(context: context)
countdown.activity = CoreDataRequests.getOrCreateActivity(name: preset.localizedName) countdown.activity = CoreDataRequests.getOrCreateActivity(name: preset.localizedName)
countdown.duration = self.duration
for range in preset.ranges(context: context) {
countdown.addToTimeRanges(range)
}
countdown.playableIds = self.timerModel.soundModel.playableIds countdown.playableIds = self.timerModel.soundModel.playableIds
return countdown return countdown
} }

Loading…
Cancel
Save