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. 38
      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. 120
      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 */; };
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 */; };
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 */; };
C4F8B15729891271005C86A5 /* Conductor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15629891271005C86A5 /* Conductor.swift */; };
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>"; };
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>"; };
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>"; };
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>"; };
@ -412,6 +419,7 @@
C438C7CF2981216200BF3EF9 /* Frameworks */ = {
isa = PBXGroup;
children = (
C4BA2AFE299A3A9E00CB4FBA /* MusicKit.framework */,
C438C7D02981216200BF3EF9 /* WidgetKit.framework */,
C438C7D22981216200BF3EF9 /* SwiftUI.framework */,
C438C7F129812BB200BF3EF9 /* Intents.framework */,
@ -473,6 +481,7 @@
C4060DF4297AE9A7003FAB80 /* TimeInterval+Extensions.swift */,
C4742B5E2984205000D5D950 /* ViewModifiers.swift */,
C40FDB612992985C0042A390 /* TextToSpeechRecorder.swift */,
C4BA2AFC299A3A3700CB4FBA /* AppleMusicPlayer.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -487,6 +496,8 @@
C4060DC1297AE73B003FAB80 /* ContentView.swift */,
C4F8B1CF298BF2E2005C86A5 /* DialView.swift */,
C498E59E298D4DEA00E90DE0 /* LiveTimerListView.swift */,
C4BA2B03299A42EF00CB4FBA /* NewDataView.swift */,
C4BA2B05299A8F8D00CB4FBA /* PresetsView.swift */,
C438C80E29828B8600BF3EF9 /* RecordsView.swift */,
C498E5A2298D720600E90DE0 /* TestView.swift */,
C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */,
@ -810,6 +821,7 @@
C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */,
C4F8B1BF298ACA0B005C86A5 /* StopwatchDialView.swift in Sources */,
C438C807298195E600BF3EF9 /* Model+Extensions.swift in Sources */,
C4BA2B04299A42EF00CB4FBA /* NewDataView.swift in Sources */,
C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */,
C4F8B183298AC234005C86A5 /* Stopwatch+CoreDataProperties.swift in Sources */,
C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */,
@ -818,12 +830,14 @@
C4F8B1B8298AC81D005C86A5 /* CountdownDialView.swift in Sources */,
C445FA922987CC8A0054D761 /* Sound.swift in Sources */,
C4F8B1D0298BF2E2005C86A5 /* DialView.swift in Sources */,
C4BA2AFD299A3A3700CB4FBA /* AppleMusicPlayer.swift in Sources */,
C4BA2AF32996A11900CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */,
C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */,
C4BA2AF12996A11900CB4FBA /* CustomSound+CoreDataClass.swift in Sources */,
C4060DC2297AE73B003FAB80 /* ContentView.swift in Sources */,
C438C7C12980228B00BF3EF9 /* CountdownScheduler.swift in Sources */,
C498E5A3298D720600E90DE0 /* TestView.swift in Sources */,
C4BA2B06299A8F8D00CB4FBA /* PresetsView.swift in Sources */,
C445FA8F2987B83B0054D761 /* SoundPlayer.swift in Sources */,
C4F8B1A7298AC2FC005C86A5 /* AbstractSoundTimer+CoreDataClass.swift in Sources */,
C438C7C929803CA000BF3EF9 /* AppDelegate.swift in Sources */,

@ -85,9 +85,16 @@ class Conductor: ObservableObject {
return LiveTimer(id: $0, date: $1)
}
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)
}
//
//
// if self.liveTimers.first(where: { $0.id == liveStopwatch.id }) == nil {
// self.liveTimers.append(liveStopwatch)
// }
}
}
@ -227,9 +234,9 @@ class Conductor: ObservableObject {
do {
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) {
print("Error requesting countdown Live Activity \(error.localizedDescription).")
print("Error requesting Live Activity \(error.localizedDescription).")
}
// 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">
<plist version="1.0">
<dict>
<key>NSAppleMusicUsageDescription</key>
<string>NSAppleMusicUsageDescription</string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.staxriver.lecountdown.refresh</string>

@ -25,31 +25,29 @@ struct LeCountdownApp: App {
init() {
UITabBar.appearance().backgroundColor = UIColor(white: 0.96, alpha: 1.0)
self._registerBackgroundRefreshes()
}
var body: some Scene {
WindowGroup {
TabView(selection: $tabSelection) {
ContentView<Countdown>()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(Conductor.maestro)
.tabItem { Label("Countdown", systemImage: "timer") }
.tag(1)
ContentView<Stopwatch>()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(Conductor.maestro)
.tabItem { Label("Stopwatch", systemImage: "stopwatch") }
.tag(2)
// ContentView<Alarm>()
// .environment(\.managedObjectContext, persistenceController.container.viewContext)
// .environmentObject(Conductor.maestro)
// .tabItem { Label("Alarm", systemImage: "alarm") }
// .tag(3)
RecordsView().environment(\.managedObjectContext, persistenceController.container.viewContext)
.tabItem { Label("Stats", systemImage: "chart.bar.fill") }
.tag(4)
NavigationStack {
TabView(selection: $tabSelection) {
PresetsView(tabSelection: $tabSelection)
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.tabItem { Label("Presets", systemImage: "globe") }
.tag(0)
ContentView<AbstractTimer>()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(Conductor.maestro)
.tabItem { Label("Home", systemImage: "clock.fill") }
.tag(1)
RecordsView().environment(\.managedObjectContext, persistenceController.container.viewContext)
.tabItem { Label("Stats", systemImage: "chart.bar.fill") }
.tag(2)
}.tabViewStyle(.page)
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
self._willEnterForegroundNotification()
@ -61,6 +59,7 @@ struct LeCountdownApp: App {
print("open URL = \(url)")
self._performActionIfPossible(url: url)
}
}
}
@ -69,7 +68,6 @@ struct LeCountdownApp: App {
}
fileprivate func _onAppear() {
self._registerBackgroundRefreshes()
// let voices = AVSpeechSynthesisVoice.speechVoices()
// 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!) {
ForEach(0..<6) {
let count = Int16($0)
Text("\(count)").tag(count)
if self.repeatCountBinding != nil {
Picker("Repeat Count", selection: self.repeatCountBinding!) {
ForEach(0..<6) {
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 {
@StateObject var boringContext: BoringContext = BoringContext()
@ -24,12 +53,25 @@ struct ContentView<T : AbstractTimer>: View {
@Environment(\.managedObjectContext) private var viewContext
@StateObject fileprivate var model: TimersModel = TimersModel()
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \T.order, ascending: true)],
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
@ -57,16 +99,55 @@ struct ContentView<T : AbstractTimer>: View {
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)
.environment(\.managedObjectContext, viewContext)
.environmentObject(Conductor.maestro)
.environmentObject(boringContext)
ReorderableForEach(items: 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)
} moveAction: { from, to in
self._reorder(from: from, to: to)
} 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)
@ -75,11 +156,12 @@ struct ContentView<T : AbstractTimer>: View {
.environment(\.managedObjectContext, viewContext)
.environmentObject(conductor)
.background(Color(white: 0.9))
.padding(.bottom, 40.0)
.cornerRadius(16.0, corners: [.topRight, .topLeft])
}
}
}
.navigationTitle("\(String(describing: T.self))")
// .navigationTitle("Yeah!")
.alert(boringContext.error?.localizedDescription ?? "missing error", isPresented: $boringContext.showDefaultAlert) {
Button("OK", role: .cancel) { }
}
@ -111,6 +193,7 @@ struct ContentView<T : AbstractTimer>: View {
}
}
.onAppear {
self._buildItemsList()
self._askPermissions()
}
.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
@ViewBuilder
@ -132,13 +242,19 @@ struct ContentView<T : AbstractTimer>: View {
case is Stopwatch.Type:
NewStopwatchView(isPresented: isPresented)
default:
Text("missing new view")
NewDataView(isPresented: isPresented)
}
}
// 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) {
var timers: [AbstractTimer] = self.timersArray
timers.move(fromOffsets: from, toOffset: to)
@ -148,7 +264,7 @@ struct ContentView<T : AbstractTimer>: View {
do {
try viewContext.save()
} catch {
boringContext.error = error
self.boringContext.error = error
}
}

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

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

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

@ -50,14 +50,14 @@ struct StopwatchEditView: View {
@State var error: Error? = nil
@FocusState private var textFieldIsFocused: Bool
// @FetchRequest(sortDescriptors: [])
// private var countdowns: FetchedResults<Countdown>
@State var _isAdding: Bool = false
@Environment(\.isPresented) var envIsPresented
@FetchRequest(sortDescriptors: [])
private var timers: FetchedResults<AbstractTimer>
var body: some View {
NavigationStack {
Rectangle()
@ -125,7 +125,7 @@ struct StopwatchEditView: View {
Button {
textFieldIsFocused = false
} label: {
Image(systemName: "checkmark")
Image(systemName: "keyboard.chevron.compact.down")
}
}
}
@ -222,6 +222,17 @@ struct StopwatchEditView: View {
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 {
// let max = self.countdowns.map { $0.order }.max() ?? 0
// cd.order = max + 1

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

Loading…
Cancel
Save