Laurent 2 years ago
parent b3447d2f00
commit 786b6bb894
  1. 32
      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. 10
      LeCountdown/Model/NSManagedContext+Extensions.swift
  13. 2
      LeCountdown/Sound/Sound.swift
  14. 2
      LeCountdown/TimerRouter.swift
  15. 2
      LeCountdown/Views/Countdown/CountdownDialView.swift
  16. 18
      LeCountdown/Views/Countdown/NewCountdownView.swift
  17. 10
      LeCountdown/Views/LiveTimerListView.swift
  18. 6
      LeCountdown/Views/StartView.swift

@ -8,34 +8,7 @@
import Intents
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
func resolveTimer(for intent: SelectTimerIntent) async -> [TimerPropertiesResolutionResult] {
@ -58,7 +31,7 @@ class IntentHandler: INExtension, SelectTimerIntentHandling {
let displayName: String
switch timer {
case let countdown as Countdown:
let formattedDuration = countdown.duration.hourMinuteSecond
let formattedDuration = countdown.formattedDuration
if let name = timer.activity?.name, !name.isEmpty {
displayName = "\(name) (\(formattedDuration))"
} else {
@ -86,7 +59,6 @@ class IntentHandler: INExtension, SelectTimerIntentHandling {
}
}
override func handler(for intent: INIntent) -> Any {
// 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.

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

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

@ -29,6 +29,11 @@ enum CountdownState {
case cancelled
}
struct CountdownSpan {
var interval: DateInterval
var name: String?
}
class Conductor: ObservableObject {
static let maestro: Conductor = Conductor()
@ -87,25 +92,11 @@ 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()
}
static let notificationIdSeparator: String = "||"
fileprivate func _buildLiveTimers() {
let liveCountdowns = self.currentCountdowns.map {
let liveCountdowns: [LiveTimer] = self.currentCountdowns.map {
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 {
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) {
self.cancelCurrentNotifications(countdownId: countdown.stringId)
let countdownId = countdown.stringId
self._cleanupPreviousTimerIfNecessary(countdownId)
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 {
self._playConfirmationSound(timer: countdown)
}
handler(.success(end))
handler(.success(Date(timeIntervalSinceNow: totalDuration)))
} catch {
FileLogger.log("start error : \(error.localizedDescription)")
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 end = start.addingTimeInterval(interval)
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)
self._delayedSoundPlayers[countdownId] = soundPlayer
try soundPlayer.start(in: interval,
repeatCount: Int(countdown.repeatCount))
let dateInterval = DateInterval(start: start, end: end)
self.currentCountdowns[countdownId] = dateInterval
self._launchLiveActivity(timer: countdown, date: end)
return end
}
@ -205,7 +225,7 @@ class Conductor: ObservableObject {
func cancelCountdown(id: TimerID) {
FileLogger.log("Cancel \(self._timerName(id))")
CountdownScheduler.master.cancelCurrentNotifications(countdownId: id)
self.cancelCurrentNotifications(countdownId: id)
self.currentCountdowns.removeValue(forKey: id)
self.removeLiveTimer(id: id)
@ -251,7 +271,7 @@ class Conductor: ObservableObject {
self.pausedCountdowns[id] = remainingTime
// cancel stuff
CountdownScheduler.master.cancelCurrentNotifications(countdownId: id)
self.cancelCurrentNotifications(countdownId: id)
self.cancelSoundPlayer(id: id)
self._endLiveActivity(timerId: id)
}
@ -325,7 +345,7 @@ class Conductor: ObservableObject {
if let countdown: Countdown = context.object(stringId: countdownId) {
do {
let sound: Sound = countdown.someSound
let sound: Sound = countdown.someSound ?? Sound.default
let soundPlayer = try DelaySoundPlayer(timerID: countdownId, sound: sound)
self._delayedSoundPlayers[countdownId] = soundPlayer
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
fileprivate func _playSound(timerId: String) {
@ -464,16 +538,16 @@ class Conductor: ObservableObject {
// interaction.donate()
// }
fileprivate func _scheduleAppRefresh(countdown: Countdown) {
let request = BGAppRefreshTaskRequest(identifier: BGTaskIdentifier.refresh.rawValue)
request.earliestBeginDate = Date(timeIntervalSinceNow: countdown.duration)
do {
try BGTaskScheduler.shared.submit(request)
print("request submitted with date: \(String(describing: request.earliestBeginDate))")
} catch {
Logger.error(error)
}
}
// fileprivate func _scheduleAppRefresh(countdown: Countdown) {
// let request = BGAppRefreshTaskRequest(identifier: BGTaskIdentifier.refresh.rawValue)
// request.earliestBeginDate = Date(timeIntervalSinceNow: countdown.duration)
// do {
// try BGTaskScheduler.shared.submit(request)
// print("request submitted with date: \(String(describing: request.earliestBeginDate))")
// } catch {
// Logger.error(error)
// }
// }
// MARK: - Live Activity

@ -8,70 +8,19 @@
import Foundation
import UserNotifications
class CountdownScheduler {
static let master = CountdownScheduler()
static let notificationIdSeparator: String = "||"
func scheduleIfPossible(countdown: Countdown, handler: @escaping (Result<Date?, Error>) -> Void) {
DispatchQueue.main.async {
self.cancelCurrentNotifications(countdownId: countdown.stringId)
Conductor.maestro.startCountdown(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)
}
}
}
//class CountdownScheduler {
//
// static let master = CountdownScheduler()
//
// static let notificationIdSeparator: String = "||"
//
// func scheduleIfPossible(countdown: Countdown, handler: @escaping (Result<Date?, Error>) -> Void) {
// DispatchQueue.main.async {
// self.cancelCurrentNotifications(countdownId: countdown.stringId)
// Conductor.maestro.startCountdown(countdown: countdown, handler: handler)
// self._scheduleCountdownNotification(countdown: countdown, handler: handler)
// }
// }
//
//
//}

@ -12,7 +12,11 @@ extension Countdown {
static func fake(context: NSManagedObjectContext) -> Countdown {
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)
activity.name = "Tea"
cd.activity = activity

@ -2,7 +2,7 @@
// Countdown+CoreDataProperties.swift
// 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")
}
@NSManaged public var duration: Double
@NSManaged public var timeRanges: NSSet?
}

@ -2,7 +2,7 @@
// TimeRange+CoreDataProperties.swift
// 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 soundList: String?
@NSManaged public var name: String?
@NSManaged public var order: Int16
@NSManaged public var playableIds: String?
@NSManaged public var countdown: Countdown?
}

@ -19,7 +19,6 @@
<attribute name="fireDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
<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"/>
</entity>
<entity name="CustomSound" representedClassName="CustomSound" syncable="YES">
@ -44,7 +43,7 @@
<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="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"/>
</entity>
</model>

@ -9,16 +9,16 @@ import Foundation
import SwiftUI
import CoreData
extension AbstractSoundTimer {
protocol StoresSound: ManagedObject {
var playableIds: String? { get }
}
extension 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] = []
@ -40,18 +40,7 @@ extension AbstractSoundTimer {
return self.playables.reduce(Set<Sound>()) { $0.union($1.soundList) }
}
var confirmationSounds: Set<Sound> {
if let confirmationSoundList {
return Set(confirmationSoundList.enumItems())
}
return []
}
func setConfirmationSounds(_ sounds: Set<Sound>) {
self.confirmationSoundList = sounds.stringRepresentation
}
var someSound: Sound {
var someSound: Sound? {
var sounds: Set<Sound> = self.allSounds
if !AppGuard.main.isSubscriber {
sounds = sounds.filter { !$0.isRestricted }
@ -76,8 +65,49 @@ extension AbstractSoundTimer {
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
}
}
@ -158,7 +188,7 @@ extension CustomSound : Localized {
}
extension TimeRange {
extension TimeRange: StoresSound {
static func fake(context: NSManagedObjectContext) -> TimeRange {
let timeRange = TimeRange(context: context)

@ -46,6 +46,13 @@ extension AbstractTimer {
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 {
return NSLocalizedString("Countdown", comment: "")
}
@ -54,6 +61,22 @@ extension Countdown {
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 {

@ -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 {
return self.objectID.isTemporaryID
@ -76,5 +82,5 @@ extension NSManagedObject {
static var entityName: String {
return self.entity().managedObjectClassName
}
}

@ -12,7 +12,7 @@ protocol Playable: StringRepresentable, Equatable, Hashable {
var soundList: Set<Sound> { get }
}
extension Playlist : Playable {
extension Playlist: Playable {
var stringValue: String { self.rawValue }
var soundList: Set<Sound> {
return Set(SoundCatalog.main.sounds(for: self))

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

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

@ -205,7 +205,16 @@ struct CountdownEditView : View {
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 {
self.nameString = name
}
@ -235,7 +244,7 @@ struct CountdownEditView : View {
cd = Countdown(context: viewContext)
}
cd.duration = self.duration
// cd.duration = self.duration
if self._isNewCountdown {
let max: Int16
@ -269,6 +278,11 @@ struct CountdownEditView : View {
range.order = Int16(index)
cd.addToTimeRanges(range)
}
} else {
let timeRange = TimeRange(context: viewContext)
timeRange.duration = self.duration
timeRange.name = self.nameString
cd.addToTimeRanges(timeRange)
}
if !self.nameString.isEmpty {

@ -200,16 +200,6 @@ struct LiveCountdownView: View {
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())
.foregroundColor(self.colorScheme == .dark ? .white : .black)

@ -142,7 +142,11 @@ class Customization: ObservableObject {
let context = PersistenceController.shared.container.viewContext
let countdown = Countdown(context: context)
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
return countdown
}

Loading…
Cancel
Save