New navigation + presets + timers custom sort

release
Laurent 3 years ago
parent a23cf012c8
commit f1e082e3e9
  1. 14
      LeCountdown.xcodeproj/project.pbxproj
  2. 13
      LeCountdown/Conductor.swift
  3. 2
      LeCountdown/Info.plist
  4. 40
      LeCountdown/LeCountdownApp.swift
  5. 38
      LeCountdown/Utils/AppleMusicPlayer.swift
  6. 12
      LeCountdown/Views/Components/SoundImageFormView.swift
  7. 140
      LeCountdown/Views/ContentView.swift
  8. 1
      LeCountdown/Views/Countdown/CountdownDialView.swift
  9. 108
      LeCountdown/Views/Countdown/NewCountdownView.swift
  10. 26
      LeCountdown/Views/DialView.swift
  11. 67
      LeCountdown/Views/NewDataView.swift
  12. 116
      LeCountdown/Views/PresetsView.swift
  13. 3
      LeCountdown/Views/ReorderableForEach.swift
  14. 19
      LeCountdown/Views/Stopwatch/NewStopwatchView.swift
  15. 5
      LeCountdown/Views/Stopwatch/StopwatchDialView.swift

@ -85,6 +85,9 @@
C4BA2AF72996A4EF00CB4FBA /* CustomSound+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */; }; C4BA2AF72996A4EF00CB4FBA /* CustomSound+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */; };
C4BA2AF82996A4F000CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEE2996A11900CB4FBA /* CustomSound+CoreDataProperties.swift */; }; C4BA2AF82996A4F000CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEE2996A11900CB4FBA /* CustomSound+CoreDataProperties.swift */; };
C4BA2AF92996A4F000CB4FBA /* CustomSound+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */; }; C4BA2AF92996A4F000CB4FBA /* CustomSound+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */; };
C4BA2AFD299A3A3700CB4FBA /* AppleMusicPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AFC299A3A3700CB4FBA /* AppleMusicPlayer.swift */; };
C4BA2B04299A42EF00CB4FBA /* NewDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B03299A42EF00CB4FBA /* NewDataView.swift */; };
C4BA2B06299A8F8D00CB4FBA /* PresetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B05299A8F8D00CB4FBA /* PresetsView.swift */; };
C4F8B1532987FE6F005C86A5 /* LaunchWidgetLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.swift */; }; C4F8B1532987FE6F005C86A5 /* LaunchWidgetLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.swift */; };
C4F8B15729891271005C86A5 /* Conductor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15629891271005C86A5 /* Conductor.swift */; }; C4F8B15729891271005C86A5 /* Conductor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15629891271005C86A5 /* Conductor.swift */; };
C4F8B15929891528005C86A5 /* forest_stream.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C4F8B15829891528005C86A5 /* forest_stream.mp3 */; }; C4F8B15929891528005C86A5 /* forest_stream.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C4F8B15829891528005C86A5 /* forest_stream.mp3 */; };
@ -264,6 +267,10 @@
C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomSound+CoreDataClass.swift"; sourceTree = "<group>"; }; C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomSound+CoreDataClass.swift"; sourceTree = "<group>"; };
C4BA2AEE2996A11900CB4FBA /* CustomSound+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomSound+CoreDataProperties.swift"; sourceTree = "<group>"; }; C4BA2AEE2996A11900CB4FBA /* CustomSound+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomSound+CoreDataProperties.swift"; sourceTree = "<group>"; };
C4BA2AEF2996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AbstractSoundTimer+CoreDataProperties.swift"; sourceTree = "<group>"; }; C4BA2AEF2996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AbstractSoundTimer+CoreDataProperties.swift"; sourceTree = "<group>"; };
C4BA2AFC299A3A3700CB4FBA /* AppleMusicPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleMusicPlayer.swift; sourceTree = "<group>"; };
C4BA2AFE299A3A9E00CB4FBA /* MusicKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MusicKit.framework; path = System/Library/Frameworks/MusicKit.framework; sourceTree = SDKROOT; };
C4BA2B03299A42EF00CB4FBA /* NewDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDataView.swift; sourceTree = "<group>"; };
C4BA2B05299A8F8D00CB4FBA /* PresetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetsView.swift; sourceTree = "<group>"; };
C4F8B15629891271005C86A5 /* Conductor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conductor.swift; sourceTree = "<group>"; }; C4F8B15629891271005C86A5 /* Conductor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conductor.swift; sourceTree = "<group>"; };
C4F8B15829891528005C86A5 /* forest_stream.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = forest_stream.mp3; sourceTree = "<group>"; }; C4F8B15829891528005C86A5 /* forest_stream.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = forest_stream.mp3; sourceTree = "<group>"; };
C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = "<group>"; }; C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = "<group>"; };
@ -412,6 +419,7 @@
C438C7CF2981216200BF3EF9 /* Frameworks */ = { C438C7CF2981216200BF3EF9 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C4BA2AFE299A3A9E00CB4FBA /* MusicKit.framework */,
C438C7D02981216200BF3EF9 /* WidgetKit.framework */, C438C7D02981216200BF3EF9 /* WidgetKit.framework */,
C438C7D22981216200BF3EF9 /* SwiftUI.framework */, C438C7D22981216200BF3EF9 /* SwiftUI.framework */,
C438C7F129812BB200BF3EF9 /* Intents.framework */, C438C7F129812BB200BF3EF9 /* Intents.framework */,
@ -473,6 +481,7 @@
C4060DF4297AE9A7003FAB80 /* TimeInterval+Extensions.swift */, C4060DF4297AE9A7003FAB80 /* TimeInterval+Extensions.swift */,
C4742B5E2984205000D5D950 /* ViewModifiers.swift */, C4742B5E2984205000D5D950 /* ViewModifiers.swift */,
C40FDB612992985C0042A390 /* TextToSpeechRecorder.swift */, C40FDB612992985C0042A390 /* TextToSpeechRecorder.swift */,
C4BA2AFC299A3A3700CB4FBA /* AppleMusicPlayer.swift */,
); );
path = Utils; path = Utils;
sourceTree = "<group>"; sourceTree = "<group>";
@ -487,6 +496,8 @@
C4060DC1297AE73B003FAB80 /* ContentView.swift */, C4060DC1297AE73B003FAB80 /* ContentView.swift */,
C4F8B1CF298BF2E2005C86A5 /* DialView.swift */, C4F8B1CF298BF2E2005C86A5 /* DialView.swift */,
C498E59E298D4DEA00E90DE0 /* LiveTimerListView.swift */, C498E59E298D4DEA00E90DE0 /* LiveTimerListView.swift */,
C4BA2B03299A42EF00CB4FBA /* NewDataView.swift */,
C4BA2B05299A8F8D00CB4FBA /* PresetsView.swift */,
C438C80E29828B8600BF3EF9 /* RecordsView.swift */, C438C80E29828B8600BF3EF9 /* RecordsView.swift */,
C498E5A2298D720600E90DE0 /* TestView.swift */, C498E5A2298D720600E90DE0 /* TestView.swift */,
C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */, C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */,
@ -810,6 +821,7 @@
C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */, C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */,
C4F8B1BF298ACA0B005C86A5 /* StopwatchDialView.swift in Sources */, C4F8B1BF298ACA0B005C86A5 /* StopwatchDialView.swift in Sources */,
C438C807298195E600BF3EF9 /* Model+Extensions.swift in Sources */, C438C807298195E600BF3EF9 /* Model+Extensions.swift in Sources */,
C4BA2B04299A42EF00CB4FBA /* NewDataView.swift in Sources */,
C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */, C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */,
C4F8B183298AC234005C86A5 /* Stopwatch+CoreDataProperties.swift in Sources */, C4F8B183298AC234005C86A5 /* Stopwatch+CoreDataProperties.swift in Sources */,
C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */, C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */,
@ -818,12 +830,14 @@
C4F8B1B8298AC81D005C86A5 /* CountdownDialView.swift in Sources */, C4F8B1B8298AC81D005C86A5 /* CountdownDialView.swift in Sources */,
C445FA922987CC8A0054D761 /* Sound.swift in Sources */, C445FA922987CC8A0054D761 /* Sound.swift in Sources */,
C4F8B1D0298BF2E2005C86A5 /* DialView.swift in Sources */, C4F8B1D0298BF2E2005C86A5 /* DialView.swift in Sources */,
C4BA2AFD299A3A3700CB4FBA /* AppleMusicPlayer.swift in Sources */,
C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */, C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */,
C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */, C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */,
C4BA2AF12996A11900CB4FBA /* CustomSound+CoreDataClass.swift in Sources */, C4BA2AF12996A11900CB4FBA /* CustomSound+CoreDataClass.swift in Sources */,
C4060DC2297AE73B003FAB80 /* ContentView.swift in Sources */, C4060DC2297AE73B003FAB80 /* ContentView.swift in Sources */,
C438C7C12980228B00BF3EF9 /* CountdownScheduler.swift in Sources */, C438C7C12980228B00BF3EF9 /* CountdownScheduler.swift in Sources */,
C498E5A3298D720600E90DE0 /* TestView.swift in Sources */, C498E5A3298D720600E90DE0 /* TestView.swift in Sources */,
C4BA2B06299A8F8D00CB4FBA /* PresetsView.swift in Sources */,
C445FA8F2987B83B0054D761 /* SoundPlayer.swift in Sources */, C445FA8F2987B83B0054D761 /* SoundPlayer.swift in Sources */,
C4F8B1A7298AC2FC005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */, C4F8B1A7298AC2FC005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */,
C438C7C929803CA000BF3EF9 /* AppDelegate.swift in Sources */, C438C7C929803CA000BF3EF9 /* AppDelegate.swift in Sources */,

@ -85,9 +85,16 @@ class Conductor: ObservableObject {
return LiveTimer(id: $0, date: $1) return LiveTimer(id: $0, date: $1)
} }
for liveStopwatch in liveStopwatches { for liveStopwatch in liveStopwatches {
if self.liveTimers.first(where: { $0.id == liveStopwatch.id }) == nil { if let livetimer = self.liveTimers.first(where: { $0.id == liveStopwatch.id }) {
self.liveTimers.replace([livetimer], with: [liveStopwatch])
} else {
self.liveTimers.append(liveStopwatch) self.liveTimers.append(liveStopwatch)
} }
//
//
// if self.liveTimers.first(where: { $0.id == liveStopwatch.id }) == nil {
// self.liveTimers.append(liveStopwatch)
// }
} }
} }
@ -227,9 +234,9 @@ class Conductor: ObservableObject {
do { do {
let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent) let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent)
print("Requested a Countdown Live Activity: \(String(describing: liveActivity.id)).") print("Requested a Live Activity: \(String(describing: liveActivity.id)).")
} catch (let error) { } catch (let error) {
print("Error requesting countdown Live Activity \(error.localizedDescription).") print("Error requesting Live Activity \(error.localizedDescription).")
} }
// self._scheduleAppRefresh(countdown: countdown) // self._scheduleAppRefresh(countdown: countdown)

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSAppleMusicUsageDescription</key>
<string>NSAppleMusicUsageDescription</string>
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.staxriver.lecountdown.refresh</string> <string>com.staxriver.lecountdown.refresh</string>

@ -25,31 +25,29 @@ struct LeCountdownApp: App {
init() { init() {
UITabBar.appearance().backgroundColor = UIColor(white: 0.96, alpha: 1.0) UITabBar.appearance().backgroundColor = UIColor(white: 0.96, alpha: 1.0)
self._registerBackgroundRefreshes()
} }
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
TabView(selection: $tabSelection) { NavigationStack {
ContentView<Countdown>()
.environment(\.managedObjectContext, persistenceController.container.viewContext) TabView(selection: $tabSelection) {
.environmentObject(Conductor.maestro) PresetsView(tabSelection: $tabSelection)
.tabItem { Label("Countdown", systemImage: "timer") } .environment(\.managedObjectContext, persistenceController.container.viewContext)
.tag(1) .tabItem { Label("Presets", systemImage: "globe") }
ContentView<Stopwatch>() .tag(0)
.environment(\.managedObjectContext, persistenceController.container.viewContext) ContentView<AbstractTimer>()
.environmentObject(Conductor.maestro) .environment(\.managedObjectContext, persistenceController.container.viewContext)
.tabItem { Label("Stopwatch", systemImage: "stopwatch") } .environmentObject(Conductor.maestro)
.tag(2) .tabItem { Label("Home", systemImage: "clock.fill") }
// ContentView<Alarm>() .tag(1)
// .environment(\.managedObjectContext, persistenceController.container.viewContext) RecordsView().environment(\.managedObjectContext, persistenceController.container.viewContext)
// .environmentObject(Conductor.maestro) .tabItem { Label("Stats", systemImage: "chart.bar.fill") }
// .tabItem { Label("Alarm", systemImage: "alarm") } .tag(2)
// .tag(3)
RecordsView().environment(\.managedObjectContext, persistenceController.container.viewContext) }.tabViewStyle(.page)
.tabItem { Label("Stats", systemImage: "chart.bar.fill") }
.tag(4)
} }
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
self._willEnterForegroundNotification() self._willEnterForegroundNotification()
@ -61,6 +59,7 @@ struct LeCountdownApp: App {
print("open URL = \(url)") print("open URL = \(url)")
self._performActionIfPossible(url: url) self._performActionIfPossible(url: url)
} }
} }
} }
@ -69,7 +68,6 @@ struct LeCountdownApp: App {
} }
fileprivate func _onAppear() { fileprivate func _onAppear() {
self._registerBackgroundRefreshes()
// let voices = AVSpeechSynthesisVoice.speechVoices() // let voices = AVSpeechSynthesisVoice.speechVoices()
// let grouped = Dictionary(grouping: voices, by: { $0.language }) // let grouped = Dictionary(grouping: voices, by: { $0.language })

@ -0,0 +1,38 @@
//
// AppleMusicPlayer.swift
// LeCountdown
//
// Created by Laurent Morvillier on 13/02/2023.
//
import Foundation
import MediaPlayer
@objc class AppleMusicPlayer : NSObject, MPMediaPickerControllerDelegate {
// func play() {
//
// let musicPlayer = MPMusicPlayerApplicationController.applicationQueuePlayer
// musicPlayer.setQueue(with: .songs())
//
// }
//
// func showMediaPicker(source: UIView) {
// let controller = MPMediaPickerController(mediaTypes: .music)
// controller.allowsPickingMultipleItems = true
// controller.popoverPresentationController?.sourceView = source
// controller.delegate = self
//// present(controller, animated: true)
// }
//
// func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) {
//
//
//// let p = MPMediaItemCollection(items: [MPMediaItem])
//
// let i = MPMediaItem()
// i.
//
// }
}

@ -69,12 +69,14 @@ struct SoundImageFormView : View {
} }
} }
Picker("Repeat Count", selection: self.repeatCountBinding!) { if self.repeatCountBinding != nil {
ForEach(0..<6) { Picker("Repeat Count", selection: self.repeatCountBinding!) {
let count = Int16($0) ForEach(0..<6) {
Text("\(count)").tag(count) let count = Int16($0)
} Text("\(count)").tag(count)
}
}
} }
} }

@ -17,6 +17,35 @@ class BoringContext : ObservableObject {
} }
class TimerSpot : Identifiable, Equatable {
var id: Int16 { return self.order }
var order: Int16
var timer: AbstractTimer?
init(order: Int16, timer: AbstractTimer? = nil) {
self.order = order
self.timer = timer
}
func setOrder(order: Int16) {
self.order = order
self.timer?.order = order
}
static func == (lhs: TimerSpot, rhs: TimerSpot) -> Bool {
return lhs.order == rhs.order && lhs.timer?.stringId == rhs.timer?.stringId
}
}
class TimersModel : ObservableObject {
@Published var spots: [TimerSpot] = []
}
struct ContentView<T : AbstractTimer>: View { struct ContentView<T : AbstractTimer>: View {
@StateObject var boringContext: BoringContext = BoringContext() @StateObject var boringContext: BoringContext = BoringContext()
@ -24,12 +53,25 @@ struct ContentView<T : AbstractTimer>: View {
@Environment(\.managedObjectContext) private var viewContext @Environment(\.managedObjectContext) private var viewContext
@StateObject fileprivate var model: TimersModel = TimersModel()
@FetchRequest( @FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \T.order, ascending: true)], sortDescriptors: [NSSortDescriptor(keyPath: \T.order, ascending: true)],
animation: .default) animation: .default)
private var timers: FetchedResults<T> private var timers: FetchedResults<T> {
didSet {
self._buildItemsList()
}
}
@State private var isEditing: Bool = false @State private var isEditing: Bool = false {
didSet {
if self.isEditing == false {
self._saveOrder()
}
self._buildItemsList()
}
}
fileprivate let itemSpacing: CGFloat = 10.0 fileprivate let itemSpacing: CGFloat = 10.0
@ -57,16 +99,55 @@ struct ContentView<T : AbstractTimer>: View {
spacing: itemSpacing spacing: itemSpacing
) { ) {
ReorderableForEach(items: timersArray) { timer in if !self.isEditing {
ForEach(self.model.spots) { spot in
if let timer = spot.timer {
DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width)
.environment(\.managedObjectContext, viewContext)
.environmentObject(Conductor.maestro)
.environmentObject(boringContext)
} else {
Color.clear
.frame(width: width, height: 80.0)
.cornerRadius(20.0)
}
}
} else {
DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width) ReorderableForEach(items: self.model.spots) { spot in
.environment(\.managedObjectContext, viewContext)
.environmentObject(Conductor.maestro)
.environmentObject(boringContext)
} moveAction: { from, to in if let timer = spot.timer {
self._reorder(from: from, to: to) DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width)
.environment(\.managedObjectContext, viewContext)
.environmentObject(Conductor.maestro)
.environmentObject(boringContext)
} else {
Color(white: 0.9)
.frame(width: width, height: 80.0)
.cornerRadius(20.0)
}
} moveAction: { from, to in
self._reorderSpots(from: from, to: to)
}
} }
// ReorderableForEach(items: timersArray) { timer in
//
// DialView(timer: timer, isEditingBinding: self.$isEditing, frameSize: width)
// .environment(\.managedObjectContext, viewContext)
// .environmentObject(Conductor.maestro)
// .environmentObject(boringContext)
//
// } moveAction: { from, to in
// self._reorder(from: from, to: to)
// }
} }
}.padding(.horizontal, itemSpacing) }.padding(.horizontal, itemSpacing)
@ -75,11 +156,12 @@ struct ContentView<T : AbstractTimer>: View {
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
.environmentObject(conductor) .environmentObject(conductor)
.background(Color(white: 0.9)) .background(Color(white: 0.9))
.padding(.bottom, 40.0)
.cornerRadius(16.0, corners: [.topRight, .topLeft]) .cornerRadius(16.0, corners: [.topRight, .topLeft])
} }
} }
} }
.navigationTitle("\(String(describing: T.self))") // .navigationTitle("Yeah!")
.alert(boringContext.error?.localizedDescription ?? "missing error", isPresented: $boringContext.showDefaultAlert) { .alert(boringContext.error?.localizedDescription ?? "missing error", isPresented: $boringContext.showDefaultAlert) {
Button("OK", role: .cancel) { } Button("OK", role: .cancel) { }
} }
@ -111,6 +193,7 @@ struct ContentView<T : AbstractTimer>: View {
} }
} }
.onAppear { .onAppear {
self._buildItemsList()
self._askPermissions() self._askPermissions()
} }
.onOpenURL { url in .onOpenURL { url in
@ -120,6 +203,33 @@ struct ContentView<T : AbstractTimer>: View {
} }
fileprivate func _buildItemsList() {
var spots: [TimerSpot] = []
let more: Int = self.isEditing ? 20 : 0 // add 20 empty spots when editing
let count = max(self.timers.count, Int(self.timers.last?.order ?? 0) + 1) + more
for i in 0..<count {
let timer = self.timers.first(where: { $0.order == i })
let spot = TimerSpot(order: Int16(i), timer: timer)
spots.append(spot)
}
self.model.spots = spots
}
fileprivate func _saveOrder() {
for (i, spot) in self.model.spots.enumerated() {
spot.setOrder(order: Int16(i))
}
do {
try viewContext.save()
} catch {
self.boringContext.error = error
}
}
// MARK: - Subviews // MARK: - Subviews
@ViewBuilder @ViewBuilder
@ -132,13 +242,19 @@ struct ContentView<T : AbstractTimer>: View {
case is Stopwatch.Type: case is Stopwatch.Type:
NewStopwatchView(isPresented: isPresented) NewStopwatchView(isPresented: isPresented)
default: default:
Text("missing new view") NewDataView(isPresented: isPresented)
} }
} }
// MARK: - Business // MARK: - Business
fileprivate func _reorderSpots(from: IndexSet, to: Int) {
var spots: [TimerSpot] = self.model.spots
spots.move(fromOffsets: from, toOffset: to)
self.model.spots = spots
}
fileprivate func _reorder(from: IndexSet, to: Int) { fileprivate func _reorder(from: IndexSet, to: Int) {
var timers: [AbstractTimer] = self.timersArray var timers: [AbstractTimer] = self.timersArray
timers.move(fromOffsets: from, toOffset: to) timers.move(fromOffsets: from, toOffset: to)
@ -148,7 +264,7 @@ struct ContentView<T : AbstractTimer>: View {
do { do {
try viewContext.save() try viewContext.save()
} catch { } catch {
boringContext.error = error self.boringContext.error = error
} }
} }

@ -19,7 +19,6 @@ struct CountdownDialView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(countdown.activity?.name?.uppercased() ?? "") Text(countdown.activity?.name?.uppercased() ?? "")
Text(countdown.duration.minuteSecond) Text(countdown.duration.minuteSecond)
Spacer()
} }
Spacer() Spacer()
} }

@ -31,6 +31,7 @@ struct CountdownEditView : View {
@StateObject var model: TimerModel = TimerModel() @StateObject var model: TimerModel = TimerModel()
var countdown: Countdown? = nil var countdown: Countdown? = nil
var preset: Preset? = nil
@Binding var isPresented: Bool @Binding var isPresented: Bool
@ -48,26 +49,35 @@ struct CountdownEditView : View {
@State var errorShown: Bool = false @State var errorShown: Bool = false
@State var error: Error? = nil @State var error: Error? = nil
var tabSelection: Binding<Int>? = nil
@FocusState private var textFieldIsFocused: Bool @FocusState private var textFieldIsFocused: Bool
@FetchRequest(sortDescriptors: []) @FetchRequest(sortDescriptors: [])
private var countdowns: FetchedResults<Countdown> private var timers: FetchedResults<AbstractTimer>
@State var _isAdding: Bool = false @State var _isNewCountdown: Bool = false // false if editing an existing countdown
@State var _hasLoaded = false @State var _hasLoaded = false
@Environment(\.isPresented) var envIsPresented @Environment(\.isPresented) var envIsPresented
//
// init() { init(isPresented: Binding<Bool>, countdown: Countdown? = nil) {
// self._load() _isPresented = isPresented
// } self.countdown = countdown
}
init(isPresented: Binding<Bool>, preset: Preset, tabSelection: Binding<Int>) {
_isPresented = isPresented
self.preset = preset
self.tabSelection = tabSelection
}
var body: some View { var body: some View {
NavigationStack { NavigationStack {
Rectangle() Rectangle()
.frame(width: 0.0, height: 0.0) .frame(width: 0.0, height: 0.0)
.onChange(of: envIsPresented) { newValue in .onChange(of: envIsPresented) { newValue in
if !newValue && !self._isAdding { if !newValue && !self._isNewCountdown {
self._save() // save when leaving an edit screen self._save() // save when leaving an edit screen
} }
} }
@ -109,7 +119,7 @@ struct CountdownEditView : View {
Text(error?.localizedDescription ?? "error") Text(error?.localizedDescription ?? "error")
}) })
.toolbar { .toolbar {
if self._isAdding { if self._isNewCountdown {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") { Button("Cancel") {
self._cancel() self._cancel()
@ -133,7 +143,7 @@ struct CountdownEditView : View {
Button { Button {
textFieldIsFocused = false textFieldIsFocused = false
} label: { } label: {
Image(systemName: "checkmark") Image(systemName: "keyboard.chevron.compact.down")
} }
} }
} }
@ -146,41 +156,57 @@ struct CountdownEditView : View {
fileprivate func _onAppear() { fileprivate func _onAppear() {
self._isAdding = (self.countdown == nil) self._isNewCountdown = (self.countdown == nil)
if !self._hasLoaded { if !self._hasLoaded {
self._load() if let countdown {
self._loadCountdown(countdown)
} else if let preset {
self._loadPreset(preset)
}
self._hasLoaded = true self._hasLoaded = true
} }
} }
fileprivate func _load() { fileprivate func _loadPreset(_ preset: Preset) {
if let countdown { self.nameString = preset.localizedName
let minutes = Int(countdown.duration / 60.0) let nf = NumberFormatter()
let seconds = countdown.duration - Double(minutes * 60) let minutes = Int(preset.duration / 60.0)
self.minutesString = nf.string(from: NSNumber(value: minutes)) ?? ""
let seconds = Int(preset.duration) - minutes * 60
self.secondsString = nf.string(from: NSNumber(value: seconds)) ?? ""
if minutes > 0 { self.model.sounds = preset.sound
self.minutesString = self._numberFormatter.string(from: NSNumber(value: minutes)) ?? ""
}
if seconds > 0 { }
self.secondsString = self._numberFormatter.string(from: NSNumber(value: seconds)) ?? ""
}
if let name = countdown.activity?.name, !name.isEmpty { fileprivate func _loadCountdown(_ countdown: Countdown) {
self.nameString = name
}
self.model.sounds = countdown.sounds let minutes = Int(countdown.duration / 60.0)
let seconds = countdown.duration - Double(minutes * 60)
// if let sound = Sound(rawValue: Int(countdown.sound)) { if minutes > 0 {
// self.sound = sound self.minutesString = self._numberFormatter.string(from: NSNumber(value: minutes)) ?? ""
// } }
self.soundRepeatCount = countdown.repeatCount
if let image = countdown.image, let coolpic = CoolPic(rawValue: image) { if seconds > 0 {
self.image = coolpic self.secondsString = self._numberFormatter.string(from: NSNumber(value: seconds)) ?? ""
} }
if let name = countdown.activity?.name, !name.isEmpty {
self.nameString = name
}
self.model.sounds = countdown.sounds
// if let sound = Sound(rawValue: Int(countdown.sound)) {
// self.sound = sound
// }
self.soundRepeatCount = countdown.repeatCount
if let image = countdown.image, let coolpic = CoolPic(rawValue: image) {
self.image = coolpic
} }
} }
@ -209,9 +235,14 @@ struct CountdownEditView : View {
} }
cd.duration = self._minutes * 60.0 + self._seconds cd.duration = self._minutes * 60.0 + self._seconds
if self._isAdding { if self._isNewCountdown {
let max = self.countdowns.map { $0.order }.max() ?? 0 let max: Int16
cd.order = max + 1 if let maxOrder = self.timers.map({ $0.order }).max() {
max = maxOrder + 1
} else {
max = 0
}
cd.order = max
} }
cd.image = self.image.rawValue cd.image = self.image.rawValue
@ -251,11 +282,16 @@ struct CountdownEditView : View {
} }
fileprivate func _popOrDismiss() { fileprivate func _popOrDismiss() {
if self._isAdding { if self._isNewCountdown {
self.isPresented = false self.isPresented = false
} else { } else {
dismiss() dismiss()
} }
if self.preset != nil {
self.tabSelection?.wrappedValue = 1
}
} }
fileprivate func _delete() { fileprivate func _delete() {

@ -29,27 +29,35 @@ struct DialView: View {
Button { Button {
self._launchTimer() self._launchTimer()
} label: { } label: {
self._dialView().padding() VStack {
Spacer()
self._dialView().padding(.horizontal)
Spacer()
}
} }
case true: case true:
self._dialView().padding()
VStack { VStack {
Spacer()
self._dialView().padding(.horizontal)
Spacer()
}
HStack {
Spacer() Spacer()
NavigationLink { NavigationLink {
self._editView(timer: timer, isPresented: $boringContext.isShowingNewData) self._editView(timer: timer, isPresented: $boringContext.isShowingNewData)
} label: { } label: {
Image(systemName: "gearshape.fill") Image(systemName: "gearshape.fill")
.font(.system(size: 50.0)) .font(.system(size: 30.0))
.padding(30.0) .padding(.horizontal)
.foregroundColor(Color.accentColor) .foregroundColor(Color.accentColor)
} }
} }
} }
} }
.frame(width: frameSize, height: frameSize) .frame(width: frameSize, height: 80.0)
.cornerRadius(40.0) .cornerRadius(20.0)
} }
@ViewBuilder @ViewBuilder
@ -78,7 +86,7 @@ struct DialView: View {
fileprivate func _editView(timer: AbstractTimer, isPresented: Binding<Bool>) -> some View { fileprivate func _editView(timer: AbstractTimer, isPresented: Binding<Bool>) -> some View {
switch timer { switch timer {
case let countdown as Countdown: case let countdown as Countdown:
CountdownEditView(countdown: countdown, isPresented: isPresented) CountdownEditView(isPresented: isPresented, countdown: countdown)
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
case let alarm as Alarm: case let alarm as Alarm:
AlarmEditView(alarm: alarm, isPresented: isPresented) AlarmEditView(alarm: alarm, isPresented: isPresented)
@ -117,7 +125,7 @@ struct DialView_Previews: PreviewProvider {
DialView( DialView(
timer: Countdown.fake(context: PersistenceController.preview.container.viewContext), timer: Countdown.fake(context: PersistenceController.preview.container.viewContext),
isEditingBinding: .constant(false), frameSize: 150.0) isEditingBinding: .constant(true), frameSize: 150.0)
.environmentObject(Conductor.maestro) .environmentObject(Conductor.maestro)
.environmentObject(BoringContext()) .environmentObject(BoringContext())

@ -0,0 +1,67 @@
//
// NewDataView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 13/02/2023.
//
import SwiftUI
enum DataTab: Int, Identifiable, CaseIterable {
case countdown
case stopwatch
var id: Int { return self.rawValue }
var localizedString: String {
switch self {
case .countdown: return NSLocalizedString("Coundown", comment: "")
case .stopwatch: return NSLocalizedString("Stopwatch", comment: "")
}
}
}
struct NewDataView: View {
@Environment(\.managedObjectContext) private var viewContext
@Binding var isPresented: Bool
@State var selection: Int = 0
var body: some View {
NavigationStack {
VStack {
Picker("", selection: $selection) {
ForEach(DataTab.allCases) { tab in
Text(tab.localizedString)
}
}
.pickerStyle(.segmented)
.padding()
TabView(selection: $selection) {
NewCountdownView(isPresented: $isPresented)
.tag(0)
.environment(\.managedObjectContext, viewContext)
NewStopwatchView(isPresented: $isPresented)
.tag(1)
.environment(\.managedObjectContext, viewContext)
}
}
}
}
}
struct NewDataView_Previews: PreviewProvider {
static var previews: some View {
NewDataView(isPresented: .constant(true))
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

@ -0,0 +1,116 @@
//
// PresetsView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 13/02/2023.
//
import SwiftUI
enum PresetSection: Int, Identifiable, CaseIterable {
var id: Int { return self.rawValue }
case cooking
case workout
case meditation
var presets: [Preset] {
switch self {
case .cooking: return [.softBoiled, .mediumBoiledEggs, .hardBoiledEggs]
case .workout: return []
case .meditation: return []
}
}
var localizedName: String {
switch self {
case .cooking: return NSLocalizedString("Cooking", comment: "")
case .workout: return NSLocalizedString("Workout", comment: "")
case .meditation: return NSLocalizedString("Meditation", comment: "")
}
}
}
enum Preset: Int, Identifiable, CaseIterable {
var id: Int { return self.rawValue }
case softBoiled
case mediumBoiledEggs
case hardBoiledEggs
var localizedName: String {
switch self {
case .hardBoiledEggs: return NSLocalizedString("Hard boiled eggs", comment: "")
case .softBoiled: return NSLocalizedString("Soft boiled eggs", comment: "")
case .mediumBoiledEggs: return NSLocalizedString("Medium boiled eggs", comment: "")
}
}
var duration: TimeInterval {
switch self {
case .softBoiled: return 3 * 60
case .mediumBoiledEggs: return 6 * 60
case .hardBoiledEggs: return 10 * 60
}
}
var sound: Set<Sound> {
switch self {
case .softBoiled: return []
case .mediumBoiledEggs: return []
case .hardBoiledEggs: return []
}
}
}
class PresetModel : ObservableObject {
@Published var selectedPreset: Preset = Preset.hardBoiledEggs
}
struct PresetsView: View {
@Environment(\.managedObjectContext) private var viewContext
@StateObject var model: PresetModel = PresetModel()
@State var isPresented: Bool = false
var tabSelection: Binding<Int>
var body: some View {
NavigationStack {
List {
ForEach(PresetSection.allCases) { section in
Section(header: Text(section.localizedName)) {
ForEach(section.presets) { preset in
Button {
self.model.selectedPreset = preset
self.isPresented = true
} label: {
Text(preset.localizedName)
}
}
}
}
}
.sheet(isPresented: $isPresented, content: {
CountdownEditView(isPresented: $isPresented, preset: self.model.selectedPreset, tabSelection: self.tabSelection)
.environment(\.managedObjectContext, viewContext)
})
.navigationTitle("Presets")
}
}
}
struct PresetsView_Previews: PreviewProvider {
static var previews: some View {
PresetsView(tabSelection: .constant(0))
}
}

@ -99,11 +99,12 @@ struct ForEachGridView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
LazyVGrid(columns: [GridItem(.fixed(50.0)), GridItem(.fixed(side))], spacing: 10.0) { LazyVGrid(columns: [GridItem(.fixed(50.0)), GridItem(.fixed(side))], spacing: 0.0) {
ReorderableForEach(items: gridData) { data in ReorderableForEach(items: gridData) { data in
Text(data.stringId) Text(data.stringId)
.frame(width: side, height: side) .frame(width: side, height: side)
.background(.cyan) .background(.cyan)
.padding()
} moveAction: { from, to in } moveAction: { from, to in
gridData.move(fromOffsets: from, toOffset: to) gridData.move(fromOffsets: from, toOffset: to)
} }

@ -51,13 +51,13 @@ struct StopwatchEditView: View {
@FocusState private var textFieldIsFocused: Bool @FocusState private var textFieldIsFocused: Bool
// @FetchRequest(sortDescriptors: [])
// private var countdowns: FetchedResults<Countdown>
@State var _isAdding: Bool = false @State var _isAdding: Bool = false
@Environment(\.isPresented) var envIsPresented @Environment(\.isPresented) var envIsPresented
@FetchRequest(sortDescriptors: [])
private var timers: FetchedResults<AbstractTimer>
var body: some View { var body: some View {
NavigationStack { NavigationStack {
Rectangle() Rectangle()
@ -125,7 +125,7 @@ struct StopwatchEditView: View {
Button { Button {
textFieldIsFocused = false textFieldIsFocused = false
} label: { } label: {
Image(systemName: "checkmark") Image(systemName: "keyboard.chevron.compact.down")
} }
} }
} }
@ -222,6 +222,17 @@ struct StopwatchEditView: View {
sw = Stopwatch(context: viewContext) sw = Stopwatch(context: viewContext)
} }
if self._isAdding {
let max: Int16
if let maxOrder = self.timers.map({ $0.order }).max() {
max = maxOrder + 1
} else {
max = 0
}
sw.order = max
}
// if self._isAdding { // if self._isAdding {
// let max = self.countdowns.map { $0.order }.max() ?? 0 // let max = self.countdowns.map { $0.order }.max() ?? 0
// cd.order = max + 1 // cd.order = max + 1

@ -15,10 +15,7 @@ struct StopwatchDialView: View {
var body: some View { var body: some View {
HStack { HStack {
VStack(alignment: .leading) { Text(stopwatch.activity?.name?.uppercased() ?? "")
Text(stopwatch.activity?.name?.uppercased() ?? "")
Spacer()
}
Spacer() Spacer()
} }
} }

Loading…
Cancel
Save