WIP but steps can be launched

splits
Laurent 2 years ago
parent 63f4327e8c
commit a60750ce97
  1. 8
      LeCountdown.xcodeproj/project.pbxproj
  2. 45
      LeCountdown/Conductor.swift
  3. 91
      LeCountdown/LeCountdownApp.swift
  4. 5
      LeCountdown/Model/Generation/Countdown+CoreDataProperties.swift
  5. 3
      LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.6.xcdatamodel/contents
  6. 102
      LeCountdown/Model/Model+CSV.swift
  7. 4
      LeCountdown/Model/Model+SharedExtensions.swift
  8. 4
      LeCountdown/Model/Persistence.swift
  9. 62
      LeCountdown/Patcher.swift
  10. 13
      LeCountdown/Views/Countdown/CountdownFormView.swift
  11. 15
      LeCountdown/Views/Countdown/NewCountdownView.swift
  12. 25
      LeCountdown/Views/Countdown/RangeFormView.swift
  13. 50
      LeCountdown/Views/LiveTimerListView.swift
  14. 5
      LeCountdown/Views/Reusable/MailView.swift
  15. 1
      LeCountdown/Views/StartView.swift
  16. 12
      LeCountdown/Views/TimerModel.swift

@ -98,6 +98,8 @@
C4556F7329E40EC200DEB40B /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F6E29E40BED00DEB40B /* FileUtils.swift */; };
C4556F7429E40EC500DEB40B /* Codable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4556F7029E40DCF00DEB40B /* Codable+Extensions.swift */; };
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 */; };
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 */; };
@ -418,6 +420,8 @@
C4556F6E29E40BED00DEB40B /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
C4556F7029E40DCF00DEB40B /* Codable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Codable+Extensions.swift"; sourceTree = "<group>"; };
C4556F7529E411A400DEB40B /* LogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsView.swift; sourceTree = "<group>"; };
C45D6AB42B173DFD00A5B649 /* Patcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patcher.swift; sourceTree = "<group>"; };
C45D6AB82B17499200A5B649 /* Model+CSV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Model+CSV.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>"; };
@ -613,6 +617,7 @@
children = (
C438C7C829803CA000BF3EF9 /* AppDelegate.swift */,
C4060DBF297AE73B003FAB80 /* LeCountdownApp.swift */,
C45D6AB42B173DFD00A5B649 /* Patcher.swift */,
C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */,
C4F8B15629891271005C86A5 /* Conductor.swift */,
C4F8B1D7298C0727005C86A5 /* TimerRouter.swift */,
@ -750,6 +755,7 @@
C438C80C2982847300BF3EF9 /* CoreDataRequests.swift */,
C49C346829DECA4400AAC6FC /* LiveStopWatch.swift */,
C498E5A0298D543900E90DE0 /* LiveTimer.swift */,
C45D6AB82B17499200A5B649 /* Model+CSV.swift */,
C438C806298195E600BF3EF9 /* Model+Extensions.swift */,
C4BA2B39299F838000CB4FBA /* Model+SharedExtensions.swift */,
C438C7C4298024E900BF3EF9 /* NSManagedContext+Extensions.swift */,
@ -1299,6 +1305,7 @@
C4BA2B5F299FFC8400CB4FBA /* StoreView.swift in Sources */,
C4BA2B7929A65C1400CB4FBA /* UIDevice+Extensions.swift in Sources */,
C4F8B17E298AC234005C86A5 /* Alarm+CoreDataClass.swift in Sources */,
C45D6AB92B17499200A5B649 /* Model+CSV.swift in Sources */,
C42E970729E6EDF5005B1B8C /* StatisticsSubView.swift in Sources */,
C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */,
C4F8B1BF298ACA0B005C86A5 /* StopwatchDialView.swift in Sources */,
@ -1325,6 +1332,7 @@
C4BA2AFD299A3A3700CB4FBA /* AppleMusicPlayer.swift in Sources */,
C4BA2B3A299F838000CB4FBA /* Model+SharedExtensions.swift in Sources */,
C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */,
C45D6AB52B173DFD00A5B649 /* Patcher.swift in Sources */,
C4556F6B29E40B7800DEB40B /* FileLogger.swift in Sources */,
C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */,
C4BA2B25299D35C100CB4FBA /* HomeView.swift in Sources */,

@ -37,7 +37,7 @@ struct CountdownSequence: Codable {
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")
return current ?? self.spans.last ?? CountdownSequence.defaultSpan
}
var currentEnd: Date {
@ -58,12 +58,14 @@ struct CountdownSequence: Codable {
}
}
private static let defaultSpan = CountdownSpan(interval: DateInterval(start: Date(), end: Date()), name: "none")
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
@ -72,6 +74,17 @@ struct CountdownSpan: Codable {
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 {
@ -139,7 +152,7 @@ class Conductor: ObservableObject {
let liveCountdowns: [LiveTimer] = self.currentCountdowns.map { id, sequence in
let currentSpan = sequence.currentSpan
return LiveTimer(id: id, name: currentSpan.name, date: currentSpan.end)
return LiveTimer(id: id, name: currentSpan.label, date: currentSpan.end)
}
// add countdown if not present
for liveCountdown in liveCountdowns {
@ -213,7 +226,7 @@ class Conductor: ObservableObject {
var spans: [CountdownSpan] = []
let now = Date()
for _ in 0..<countdown.repeatCount {
for i in 0..<countdown.loops {
for range in countdown.sortedRanges() {
let start = now.addingTimeInterval(totalDuration)
@ -222,7 +235,7 @@ class Conductor: ObservableObject {
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)
let span = CountdownSpan(interval: dateInterval, name: range.name, index: i, loopCount: countdown.loops)
spans.append(span)
}
}
@ -246,17 +259,20 @@ class Conductor: ObservableObject {
}
fileprivate let idSeparator = "=&="
fileprivate func _scheduleSoundPlayer(countdown: Countdown, range: TimeRange, in interval: TimeInterval) throws -> Date {
let start = Date()
let end = start.addingTimeInterval(interval)
let countdownId = countdown.stringId
// let countdownId = countdown.stringId
FileLogger.log("schedule countdown \(range.name ?? "''") at \(end)")
let sound = range.someSound ?? countdown.someSound ?? Sound.default
let playerId = range.stringId + interval.debugDescription
let idComponents = [countdown.stringId, range.stringId, interval.debugDescription]
let playerId = idComponents.joined(separator: idSeparator)
let soundPlayer = try DelaySoundPlayer(sound: sound)
self._delayedSoundPlayers[playerId] = soundPlayer
@ -547,12 +563,17 @@ class Conductor: ObservableObject {
}
func cancelSoundPlayer(id: TimerID) {
if let soundPlayer = self._delayedSoundPlayers[id] {
soundPlayer.stop()
self._delayedSoundPlayers.removeValue(forKey: id)
FileLogger.log("cancelled sound player for \(self._timerName(id))")
}
let keys = self._delayedSoundPlayers.keys.filter { $0.starts(with: id) }
for key in keys {
if let soundPlayer = self._delayedSoundPlayers[key] {
soundPlayer.stop()
self._delayedSoundPlayers.removeValue(forKey: key)
}
}
FileLogger.log("cancelled \(keys.count) sound players for \(self._timerName(id))")
self.deactivateAudioSessionIfPossible()
}

@ -16,10 +16,10 @@ import CloudKit
struct LeCountdownApp: App {
let persistenceController = PersistenceController.shared
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@Environment(\.scenePhase) var scenePhase
@State var showStartView: Bool = false
init() {
@ -42,25 +42,25 @@ struct LeCountdownApp: App {
StartView(isPresented: $showStartView)
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
.onAppear {
self._onAppear()
}
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .inactive:
Conductor.maestro.stopMainPlayersIfPossible()
Conductor.maestro.memoryWarningReceived = false
case .active:
// Logger.log("onChange(of: scenePhase) active")
// Logger.log(Conductor.maestro.currentCountdowns.count)
Conductor.maestro.restoreSoundPlayers()
Conductor.maestro.cleanup()
default:
break
.onAppear {
self._onAppear()
}
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .inactive:
Conductor.maestro.stopMainPlayersIfPossible()
Conductor.maestro.memoryWarningReceived = false
case .active:
// Logger.log("onChange(of: scenePhase) active")
// Logger.log(Conductor.maestro.currentCountdowns.count)
Conductor.maestro.restoreSoundPlayers()
Conductor.maestro.cleanup()
default:
break
}
}
}
}
}
fileprivate func _shouldShowStartView() -> Bool {
@ -78,23 +78,23 @@ struct LeCountdownApp: App {
let containerAvailable = self.isICloudContainerAvailable()
Logger.log("isICloudContainerAvailable = \(containerAvailable)")
// let voices = AVSpeechSynthesisVoice.speechVoices()
// let grouped = Dictionary(grouping: voices, by: { $0.language })
// for language in grouped.keys {
// if let lvoices = grouped[language] {
// print("language = \(language)")
// for voice in lvoices {
// print("name = \(voice.name), gender = \(voice.gender)")
// }
// print("========")
// }
// }
// let voices = AVSpeechSynthesisVoice.speechVoices()
// let grouped = Dictionary(grouping: voices, by: { $0.language })
// for language in grouped.keys {
// if let lvoices = grouped[language] {
// print("language = \(language)")
// for voice in lvoices {
// print("name = \(voice.name), gender = \(voice.gender)")
// }
// print("========")
// }
// }
}
fileprivate func _registerBackgroundRefreshes() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTaskIdentifier.refresh.rawValue, using: nil) { task in
self._handleAppRefresh(task: task as! BGAppRefreshTask)
self._handleAppRefresh(task: task as! BGAppRefreshTask)
}
}
@ -102,30 +102,19 @@ struct LeCountdownApp: App {
print("_handleAppRefresh = \(task.description)")
// task.expirationHandler = {
// print("expired")
// }
//
// DispatchQueue.main.async {
// Conductor.maestro.updateLiveActivities()
// task.setTaskCompleted(success: true)
// }
// task.expirationHandler = {
// print("expired")
// }
//
// DispatchQueue.main.async {
// Conductor.maestro.updateLiveActivities()
// task.setTaskCompleted(success: true)
// }
}
fileprivate func _patch() {
let context = PersistenceController.shared.container.viewContext
do {
let records = try context.fetch(Record.fetchRequest())
for record in records {
record.preCompute()
}
try context.save()
} catch {
Logger.error(error)
}
Patcher.patch()
}
func isICloudContainerAvailable() -> Bool {

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

@ -18,7 +18,8 @@
<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"/>
<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"/>
</entity>
<entity name="CustomSound" representedClassName="CustomSound" syncable="YES">

@ -0,0 +1,102 @@
//
// Model+CSV.swift
// LeCountdown
//
// Created by Laurent Morvillier on 29/11/2023.
//
import Foundation
import CoreData
protocol CSVField {
associatedtype T: CSVRepresentable
var header: String { get }
}
protocol CSVRepresentable: NSFetchRequestResult {
associatedtype F: CSVField
func toCSV() -> String
static var fields: [F] { get }
func value(field: F) -> String?
}
extension CSVRepresentable {
static func toCSV() -> String {
var csv: String = ""
let context: NSManagedObjectContext = PersistenceController.shared.container.viewContext
let request = NSFetchRequest<Self>(entityName: String(describing: self))
csv = self.fields.map { "\"\($0.header)\"" }.joined(separator: ",")
csv.append("\n")
do {
let entities = try context.fetch(request)
for entity in entities {
let entityCSV = entity.toCSV()
csv.append("\(entityCSV)\n")
}
} catch {
Logger.error(error)
}
return csv
}
func toCSV() -> String {
let values: [String?] = Self.fields.map { field in
self.value(field: field)
}
return values.map { "\"\($0 ?? "")\"" }.joined(separator: ",")
}
}
extension Record : CSVRepresentable {
static var fields: [RecordCSVField] {
return RecordCSVField.allCases
}
func value(field: RecordCSVField) -> String? {
switch field {
case .activity:
return self.activity?.name
case .start:
return self.start?.formattedDateTime
case .end:
return self.end?.formattedDateTime
case .cancelled:
return self.cancelled.description
}
}
}
enum RecordCSVField: CSVField, CaseIterable {
typealias T = Record
case activity
case start
case end
case cancelled
var header: String {
switch self {
case .activity: return "Activity"
case .start: return "Start"
case .end: return "End"
case .cancelled: return "Cancelled"
}
}
}

@ -70,8 +70,8 @@ extension Countdown {
formatted = durations.first ?? "none"
}
if self.repeatCount > 1 {
return "\(formatted) * \(repeatCount)"
if self.loops > 1 {
return "\(formatted) * \(loops)"
} else {
return formatted
}

@ -66,8 +66,8 @@ struct PersistenceController {
let storeURL = URL.storeURL(for: "group.com.staxriver.countdown", databaseName: "group.com.staxriver.countdown")
let storeDescription = NSPersistentStoreDescription(url: storeURL)
// storeDescription.shouldMigrateStoreAutomatically = true
// storeDescription.shouldInferMappingModelAutomatically = true
storeDescription.shouldMigrateStoreAutomatically = true
storeDescription.shouldInferMappingModelAutomatically = true
let id = "iCloud.LeCountdown"
let options = NSPersistentCloudKitContainerOptions(containerIdentifier: id)

@ -0,0 +1,62 @@
//
// Patcher.swift
// LeCountdown
//
// Created by Laurent Morvillier on 29/11/2023.
//
import Foundation
import CoreData
enum Patch: String, CaseIterable {
case interval
var key: String {
return "patch." + self.rawValue
}
}
class Patcher {
static func patch() {
let context = PersistenceController.shared.container.viewContext
for patch in Patch.allCases {
if true {
Logger.log("PATCH!!!")
// if !UserDefaults.standard.bool(forKey: patch.key) {
do {
switch patch {
case .interval:
try self._patchIntervals(context: context)
}
try context.save()
UserDefaults.standard.set(true, forKey: patch.key)
} catch {
Logger.error(error)
}
}
}
}
static fileprivate func _patchIntervals(context: NSManagedObjectContext) throws {
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)
}
}
}
}

@ -52,6 +52,8 @@ struct CountdownFormView : View {
} label: {
LabeledContent(range.name ?? "", value: range.duration.hourMinuteSecond)
}
}.onDelete { indexSet in
self.model.deleteRange(indexSet)
}
}
@ -68,9 +70,9 @@ struct CountdownFormView : View {
Section {
HStack {
Stepper("Repeat Count", value: self.$model.repeatCount, in: 1...100)
Stepper("Repeat Count", value: self.$model.loops, in: 1...100)
Spacer()
Text(self.model.repeatCount.formatted())
Text(self.model.loops.formatted())
.frame(width: 24.0)
}
}
@ -96,9 +98,10 @@ struct CountdownFormView : View {
SoundFormView(model: self.model)
}
.sheet(item: self.$selectedRange) { item in
RangeFormView(timeRange: item,
selectedItem: self.$selectedRange)
.sheet(item: self.$selectedRange, onDismiss: {
self.model.objectWillChange.send()
}) { item in
RangeFormView(timeRange: item)
}
}

@ -203,9 +203,8 @@ struct CountdownEditView : View {
fileprivate func _loadCountdown(_ countdown: Countdown) {
let ranges = countdown.sortedRanges()
if ranges.count > 1 {
self.model.ranges = ranges
} else if let range = ranges.first {
self.model.ranges = ranges
if let range = ranges.first {
self.duration = range.duration
self.nameString = range.name ?? ""
}
@ -217,11 +216,8 @@ struct CountdownEditView : View {
self.model.soundModel.setPlayables(countdown.playables)
self.model.confirmationSoundModel.setPlayables(countdown.confirmationPlayables)
self.model.repeatCount = countdown.repeatCount
self.model.loops = countdown.loops
if let image = countdown.image,
let coolpic = CoolPic(rawValue: image) {
}
}
fileprivate func _cancel() {
@ -259,16 +255,17 @@ struct CountdownEditView : View {
cd.playableIds = self.model.soundModel.playableIds
cd.setConfirmationSounds(self.model.confirmationSoundModel.sounds)
cd.repeatCount = self.model.repeatCount
cd.loops = self.model.loops
if self.model.ranges.count > 0 {
if let timeRanges = cd.timeRanges {
cd.removeFromTimeRanges(timeRanges)
}
for (index, range) in self.model.ranges.enumerated() {
range.order = Int16(index)
range.duration = range.duration
range.name = range.name
cd.addToTimeRanges(range)
}
} else {

@ -9,9 +9,9 @@ import SwiftUI
struct RangeFormView: View {
var timeRange: TimeRange
@Binding var selectedItem: TimeRange?
@Environment(\.dismiss) private var dismiss
@ObservedObject var timeRange: TimeRange
@State var namePlaceholder = "name"
@State var name: String = ""
@ -33,13 +33,7 @@ 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
self._doneHandler()
} label: {
HStack {
Spacer()
@ -56,6 +50,17 @@ struct RangeFormView: View {
}
}
fileprivate func _doneHandler() {
if self.name.isEmpty {
self.timeRange.name = self.namePlaceholder
} else {
self.timeRange.name = self.name
}
self.timeRange.duration = self.duration
self.dismiss()
}
}
struct RangeFormView_Previews: PreviewProvider {

@ -183,15 +183,10 @@ struct LiveCountdownView: View {
TimelineView(.periodic(from: self.date, by: 0.01)) { context in
let state = Conductor.maestro.countdownState(self.countdown)
// let remainingTime: TimeInterval? = Conductor.maestro.remainingPausedCountdownTime(self.countdown)
// let cancelled = Conductor.maestro.isCountdownCancelled(self.countdown)
let state: CountdownState = Conductor.maestro.countdownState(self.countdown)
HStack {
// let running = self.date > context.date
VStack(alignment: .leading) {
switch state {
@ -207,7 +202,7 @@ struct LiveCountdownView: View {
TimeView(text: NSLocalizedString("Cancelled", comment: ""))
}
let name = self.name ?? self.countdown.displayName
var name = self._nameForState(state: state)
Text(name.uppercased())
.foregroundColor(self.colorScheme == .dark ? .white : .black)
@ -235,25 +230,6 @@ struct LiveCountdownView: View {
EmptyView()
}
// if !cancelled && (self.date > context.date && remainingTime != nil) { // pause / resume
// if remainingTime != nil {
// Button {
// self._resume()
// } label: {
// Image(systemName: "play.circle")
// .foregroundColor(.accentColor)
// }
// } else {
// Button {
// self._pause()
// } label: {
// Image(systemName: "pause.circle")
// .foregroundColor(.accentColor)
// }
// }
// }
switch state {
case .inprogress, .paused:
Button {
@ -267,19 +243,6 @@ struct LiveCountdownView: View {
case .cancelled:
Image(systemName: "xmark.circle").foregroundColor(.accentColor)
}
// if cancelled { // Cancelled image
// Image(systemName: "xmark.circle").foregroundColor(.accentColor)
// } else if !running && remainingTime == nil { // Ended
// GreenCheckmarkView()
// } else { // Cancel button
// Button {
// self.showCancelConfirmationPopup = true
// } label: {
// Image(systemName: "xmark.circle.fill")
// .foregroundColor(.accentColor)
// }
// }
}.font(.system(size: actionButtonFontSize))
}
@ -341,6 +304,15 @@ struct LiveCountdownView: View {
}
}
fileprivate func _nameForState(state: CountdownState) -> String {
switch state {
case .finished:
return self.countdown.displayName
default:
return self.name ?? self.countdown.displayName
}
}
fileprivate func _formattedDuration(date: Date) -> String {
let duration = self.date.timeIntervalSince(date)
return duration.hourMinuteSecond

@ -46,6 +46,11 @@ struct MailView: UIViewControllerRepresentable {
if let logsData = content.data(using: .utf8) {
vc.addAttachmentData(logsData, mimeType: "text/plain", fileName: "logs.txt")
}
if let recordCSV = Record.toCSV().data(using: .utf8) {
vc.addAttachmentData(recordCSV, mimeType: "text/csv", fileName: "records.csv")
}
return vc
}

@ -153,6 +153,7 @@ class Customization: ObservableObject {
countdown.activity = CoreDataRequests.getOrCreateActivity(name: preset.localizedName)
for range in preset.ranges(context: context) {
range.duration = self.duration
countdown.addToTimeRanges(range)
}

@ -15,13 +15,18 @@ protocol SoundHolder {
func selectPlaylist(_ playlist: Playlist, selected: Bool)
}
//struct StageItem {
// var name: String?
// var duration: TimeInterval
//}
class TimerModel: ObservableObject {
@Published var soundModel: SoundModel = SoundModel()
@Published var confirmationSoundModel: SoundModel = SoundModel()
@Published var ranges: [TimeRange] = []
@Published var repeatCount: Int16 = 1
@Published var loops: Int16 = 1
func addInterval(context: NSManagedObjectContext) -> TimeRange {
let timeRange = TimeRange(context: context)
@ -32,7 +37,10 @@ class TimerModel: ObservableObject {
return timeRange
}
// @Published var editedRange: TimeRange? = nil
func deleteRange(indexSet: IndexSet, context: NSManagedObjectContext) {
self.ranges.remove(atOffsets: indexSet)
}
}
class SoundModel: ObservableObject, SoundHolder {

Loading…
Cancel
Save