Work on onboarding

main
Laurent 3 years ago
parent afbcd4b5d5
commit e76429b7cb
  1. 4
      LeCountdown.xcodeproj/project.pbxproj
  2. 3
      LeCountdown/LeCountdownApp.swift
  3. 4
      LeCountdown/Views/ContentView.swift
  4. 83
      LeCountdown/Views/Countdown/CountdownFormView.swift
  5. 20
      LeCountdown/Views/Countdown/NewCountdownView.swift
  6. 10
      LeCountdown/Views/HomeView.swift
  7. 7
      LeCountdown/Views/NewDataView.swift
  8. 95
      LeCountdown/Views/PresetsView.swift
  9. 4
      LeCountdown/Views/Reusable/TimerModel.swift
  10. 283
      LeCountdown/Views/StartView.swift
  11. 15
      LeCountdown/Views/Stopwatch/NewStopwatchView.swift
  12. 9
      LeCountdown/fr.lproj/Localizable.strings

@ -46,6 +46,7 @@
C4286EAE2A17753A0070D075 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4286E952A14EC4E0070D075 /* AppError.swift */; }; C4286EAE2A17753A0070D075 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4286E952A14EC4E0070D075 /* AppError.swift */; };
C4286EB02A1B75AB0070D075 /* BoringContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4286EAF2A1B75AB0070D075 /* BoringContext.swift */; }; C4286EB02A1B75AB0070D075 /* BoringContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4286EAF2A1B75AB0070D075 /* BoringContext.swift */; };
C4286EB22A1B75C60070D075 /* BoringContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4286EAF2A1B75AB0070D075 /* BoringContext.swift */; }; C4286EB22A1B75C60070D075 /* BoringContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4286EAF2A1B75AB0070D075 /* BoringContext.swift */; };
C4286EB72A1B98420070D075 /* StartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4286EB62A1B98420070D075 /* StartView.swift */; };
C42E96FB29E59E72005B1B8C /* BackgroundBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E96FA29E59E72005B1B8C /* BackgroundBlurView.swift */; }; C42E96FB29E59E72005B1B8C /* BackgroundBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E96FA29E59E72005B1B8C /* BackgroundBlurView.swift */; };
C42E96FD29E5B06D005B1B8C /* ActivityCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E96FC29E5B06D005B1B8C /* ActivityCalendarView.swift */; }; C42E96FD29E5B06D005B1B8C /* ActivityCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E96FC29E5B06D005B1B8C /* ActivityCalendarView.swift */; };
C42E96FE29E5B5CD005B1B8C /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B6429A3C37D00CB4FBA /* Filter.swift */; }; C42E96FE29E5B5CD005B1B8C /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B6429A3C37D00CB4FBA /* Filter.swift */; };
@ -380,6 +381,7 @@
C4286E9F2A1502FD0070D075 /* Stopwatch+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Stopwatch+CoreDataClass.swift"; sourceTree = "<group>"; }; C4286E9F2A1502FD0070D075 /* Stopwatch+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Stopwatch+CoreDataClass.swift"; sourceTree = "<group>"; };
C4286EA52A150A7E0070D075 /* TimePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimePickerView.swift; sourceTree = "<group>"; }; C4286EA52A150A7E0070D075 /* TimePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimePickerView.swift; sourceTree = "<group>"; };
C4286EAF2A1B75AB0070D075 /* BoringContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoringContext.swift; sourceTree = "<group>"; }; C4286EAF2A1B75AB0070D075 /* BoringContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoringContext.swift; sourceTree = "<group>"; };
C4286EB62A1B98420070D075 /* StartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartView.swift; sourceTree = "<group>"; };
C42E96FA29E59E72005B1B8C /* BackgroundBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundBlurView.swift; sourceTree = "<group>"; }; C42E96FA29E59E72005B1B8C /* BackgroundBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundBlurView.swift; sourceTree = "<group>"; };
C42E96FC29E5B06D005B1B8C /* ActivityCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityCalendarView.swift; sourceTree = "<group>"; }; C42E96FC29E5B06D005B1B8C /* ActivityCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityCalendarView.swift; sourceTree = "<group>"; };
C42E970129E6B32B005B1B8C /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; }; C42E970129E6B32B005B1B8C /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
@ -790,6 +792,7 @@
C4BA2B03299A42EF00CB4FBA /* NewDataView.swift */, C4BA2B03299A42EF00CB4FBA /* NewDataView.swift */,
C4BA2B05299A8F8D00CB4FBA /* PresetsView.swift */, C4BA2B05299A8F8D00CB4FBA /* PresetsView.swift */,
C4E5D68929BB7953008E7465 /* SettingsView.swift */, C4E5D68929BB7953008E7465 /* SettingsView.swift */,
C4286EB62A1B98420070D075 /* StartView.swift */,
C4E5D68529BB369E008E7465 /* TimersView.swift */, C4E5D68529BB369E008E7465 /* TimersView.swift */,
); );
path = Views; path = Views;
@ -1219,6 +1222,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C4286EB72A1B98420070D075 /* StartView.swift in Sources */,
C4F8B1D2298BF646005C86A5 /* PermissionAlertView.swift in Sources */, C4F8B1D2298BF646005C86A5 /* PermissionAlertView.swift in Sources */,
C4BA2B7329A60CF000CB4FBA /* Shortcut.swift in Sources */, C4BA2B7329A60CF000CB4FBA /* Shortcut.swift in Sources */,
C4060DC9297AE73D003FAB80 /* Persistence.swift in Sources */, C4060DC9297AE73D003FAB80 /* Persistence.swift in Sources */,

@ -35,6 +35,9 @@ struct LeCountdownApp: App {
WindowGroup { WindowGroup {
ZStack { ZStack {
// StartView()
CompactHomeView() CompactHomeView()
.environment(\.managedObjectContext, persistenceController.container.viewContext) .environment(\.managedObjectContext, persistenceController.container.viewContext)
} }

@ -239,10 +239,10 @@ struct MainToolbarView: ToolbarContent {
} }
} }
.sheet(isPresented: self.$showStopwatchSheet, content: { .sheet(isPresented: self.$showStopwatchSheet, content: {
NewStopwatchView(isPresented: $showStopwatchSheet, tabSelection: .constant(0)) NewStopwatchView(isPresented: $showStopwatchSheet)
}) })
.sheet(isPresented: self.$showTimerSheet, content: { .sheet(isPresented: self.$showTimerSheet, content: {
NewCountdownView(isPresented: $showTimerSheet, tabSelection: .constant(0)) NewCountdownView(isPresented: $showTimerSheet)
}) })
} }

@ -21,13 +21,7 @@ struct CountdownFormView : View {
@EnvironmentObject var model: TimerModel @EnvironmentObject var model: TimerModel
var nameBinding: Binding<String> var nameBinding: Binding<String>
// var secondsBinding: Binding<String>
// var minutesBinding: Binding<String>
// var hoursBinding: Binding<String>
var durationBinding: Binding<TimeInterval> var durationBinding: Binding<TimeInterval>
var imageBinding: Binding<CoolPic> var imageBinding: Binding<CoolPic>
var repeatCountBinding: Binding<Int16> var repeatCountBinding: Binding<Int16>
@ -45,49 +39,8 @@ struct CountdownFormView : View {
} }
} }
// Section {
//// self.focusedField = nil
//
// TextField("Name", text: nameBinding)
// .focused($focusedField, equals: .name)
// .onSubmit {
// self.focusedField = nil
//
//// self.focusNextField($focusedField)
// }
// } header: {
// Text("Name")
// } footer: {
// if !self.nameBinding.wrappedValue.isEmpty {
// Text("Ask Siri: \(Bundle.main.applicationName) \(self.nameBinding.wrappedValue)!")
// }
// }
Section { Section {
TimePickerView(duration: self.durationBinding) TimePickerView(duration: self.durationBinding)
// TextField("Hours", text: hoursBinding)
// .keyboardType(.numberPad)
// .focused($focusedField, equals: .hours)
// .onSubmit {
// self.focusNextField($focusedField)
// }
// TextField("Minutes", text: minutesBinding)
// .keyboardType(.numberPad)
// .focused($focusedField, equals: .minutes)
// .onSubmit {
// self.focusNextField($focusedField)
// }
// TextField("Seconds", text: secondsBinding)
// .keyboardType(.numberPad)
// .focused($focusedField, equals: .seconds)
// .onSubmit {
// self.focusedField = nil
// }
} header: { } header: {
LabeledContent("Duration", value: self.duration().hourMinuteSecond).font(.footnote) LabeledContent("Duration", value: self.duration().hourMinuteSecond).font(.footnote)
} }
@ -102,22 +55,6 @@ struct CountdownFormView : View {
func duration() -> TimeInterval { func duration() -> TimeInterval {
return self.durationBinding.wrappedValue return self.durationBinding.wrappedValue
// let formatter = NumberFormatter()
// var hours: Int = 0
// var minutes: Int = 0
// var seconds: Int = 0
// if let h = formatter.number(from: hoursBinding.wrappedValue) {
// hours = h.intValue
// }
// if let m = formatter.number(from: minutesBinding.wrappedValue) {
// minutes = m.intValue
// }
// if let s = formatter.number(from: secondsBinding.wrappedValue) {
// seconds = s.intValue
// }
// return Double(seconds) + 60 * Double(minutes) + 60 * 60 * Double(hours)
} }
} }
@ -127,18 +64,12 @@ struct CountdownFormView_Previews: PreviewProvider {
@FocusState static var textFieldIsFocused: Bool @FocusState static var textFieldIsFocused: Bool
static var previews: some View { static var previews: some View {
CountdownFormView(
Form { nameBinding: .constant(""),
CountdownFormView( durationBinding: .constant(0.0),
nameBinding: .constant(""), imageBinding: .constant(.pic3),
// secondsBinding: .constant(""), repeatCountBinding: .constant(2),
// minutesBinding: .constant(""), intervalRepeatBinding: .constant(2))
// hoursBinding: .constant(""), .environmentObject(TimerModel())
durationBinding: .constant(0.0),
imageBinding: .constant(.pic3),
repeatCountBinding: .constant(2),
intervalRepeatBinding: .constant(2))
.environmentObject(TimerModel())
}
} }
} }

@ -15,12 +15,10 @@ struct NewCountdownView : View {
@Environment(\.managedObjectContext) private var viewContext @Environment(\.managedObjectContext) private var viewContext
@Binding var isPresented: Bool @Binding var isPresented: Bool
var tabSelection: Binding<Int>
var userActivity: NSUserActivity var userActivity: NSUserActivity
init(isPresented: Binding<Bool>, tabSelection: Binding<Int>) { init(isPresented: Binding<Bool>) {
_isPresented = isPresented _isPresented = isPresented
self.tabSelection = tabSelection
self.userActivity = Shortcut.newCountdown.userActivity self.userActivity = Shortcut.newCountdown.userActivity
// let shortcut = INShortcut(userActivity: self.userActivity) // let shortcut = INShortcut(userActivity: self.userActivity)
@ -28,7 +26,7 @@ struct NewCountdownView : View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
CountdownEditView(isPresented: $isPresented, tabSelection: self.tabSelection) CountdownEditView(isPresented: $isPresented)
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
.onAppear { .onAppear {
self.userActivity.becomeCurrent() self.userActivity.becomeCurrent()
@ -71,8 +69,6 @@ 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
@State var _isNewCountdown: Bool = false // false if editing an existing countdown @State var _isNewCountdown: Bool = false // false if editing an existing countdown
@State var _hasLoaded = false @State var _hasLoaded = false
@ -82,16 +78,14 @@ struct CountdownEditView : View {
@FocusState private var focusedField: CountdownField? @FocusState private var focusedField: CountdownField?
init(isPresented: Binding<Bool>, countdown: Countdown? = nil, tabSelection: Binding<Int>? = nil) { init(isPresented: Binding<Bool>, countdown: Countdown? = nil) {
_isPresented = isPresented _isPresented = isPresented
self.countdown = countdown self.countdown = countdown
self.tabSelection = tabSelection
} }
init(isPresented: Binding<Bool>, preset: Preset, tabSelection: Binding<Int>) { init(isPresented: Binding<Bool>, preset: Preset) {
_isPresented = isPresented _isPresented = isPresented
self.preset = preset self.preset = preset
self.tabSelection = tabSelection
} }
fileprivate var _formId = "formId" fileprivate var _formId = "formId"
@ -375,9 +369,6 @@ struct CountdownEditView : View {
} else { } else {
dismiss() dismiss()
} }
self.tabSelection?.wrappedValue = 1
} }
fileprivate func _delete() { fileprivate func _delete() {
@ -411,8 +402,7 @@ struct CountdownEditView : View {
struct NewCountdownView_Previews: PreviewProvider { struct NewCountdownView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
NewCountdownView(isPresented: .constant(true), NewCountdownView(isPresented: .constant(true))
tabSelection: .constant(0))
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
} }
} }

@ -18,8 +18,6 @@ struct CompactHomeView: View {
animation: .default) animation: .default)
private var timers: FetchedResults<AbstractTimer> private var timers: FetchedResults<AbstractTimer>
@State private var tabSelection: Int = 0
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@ -27,14 +25,6 @@ struct CompactHomeView: View {
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
.environmentObject(Conductor.maestro) .environmentObject(Conductor.maestro)
} }
.onAppear {
if self.timers.count > 0 {
self.tabSelection = 1
}
}
.onOpenURL { _ in
self.tabSelection = 1
}
} }
} }

@ -28,7 +28,6 @@ struct NewDataView: View {
@Environment(\.managedObjectContext) private var viewContext @Environment(\.managedObjectContext) private var viewContext
@Binding var isPresented: Bool @Binding var isPresented: Bool
var tabSelection: Binding<Int>
@State var selection: Int = 0 @State var selection: Int = 0
@ -47,10 +46,10 @@ struct NewDataView: View {
.padding(.horizontal) .padding(.horizontal)
TabView(selection: $selection) { TabView(selection: $selection) {
NewCountdownView(isPresented: $isPresented, tabSelection: self.tabSelection) NewCountdownView(isPresented: $isPresented)
.tag(0) .tag(0)
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
NewStopwatchView(isPresented: $isPresented, tabSelection: self.tabSelection) NewStopwatchView(isPresented: $isPresented)
.tag(1) .tag(1)
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
}.tabViewStyle(.page(indexDisplayMode: .never)) }.tabViewStyle(.page(indexDisplayMode: .never))
@ -62,7 +61,7 @@ struct NewDataView: View {
struct NewDataView_Previews: PreviewProvider { struct NewDataView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
NewDataView(isPresented: .constant(true), tabSelection: .constant(0)) NewDataView(isPresented: .constant(true))
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
} }
} }

@ -60,8 +60,6 @@ struct PresetsView: View {
@State var isShowingNewCountdown = false @State var isShowingNewCountdown = false
@State var isShowingNewStopwatch = false @State var isShowingNewStopwatch = false
var tabSelection: Binding<Int>
fileprivate func _columnCount() -> Int { fileprivate func _columnCount() -> Int {
return 2 return 2
@ -135,15 +133,15 @@ struct PresetsView: View {
Spacer() Spacer()
} }
.sheet(isPresented: $isShowingNewStopwatch, content: { .sheet(isPresented: $isShowingNewStopwatch, content: {
NewStopwatchView(isPresented: $isShowingNewStopwatch, tabSelection: self.tabSelection) NewStopwatchView(isPresented: $isShowingNewStopwatch)
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
}) })
.sheet(isPresented: $isShowingNewCountdown, content: { .sheet(isPresented: $isShowingNewCountdown, content: {
NewCountdownView(isPresented: $isShowingNewCountdown, tabSelection: self.tabSelection) NewCountdownView(isPresented: $isShowingNewCountdown)
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
}) })
.sheet(isPresented: $isPresented, content: { .sheet(isPresented: $isPresented, content: {
CountdownEditView(isPresented: $isPresented, preset: self.model.selectedPreset, tabSelection: self.tabSelection) CountdownEditView(isPresented: $isPresented, preset: self.model.selectedPreset)
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
}) })
.sheet(isPresented: $isShowingSubscription, content: { .sheet(isPresented: $isShowingSubscription, content: {
@ -174,12 +172,21 @@ struct TimerItemView: View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(self.name.uppercased()).foregroundColor(colorScheme == .dark ? .white : .black) Text(self.name.uppercased()).foregroundColor(colorScheme == .dark ? .white : .black)
Text(self.duration) HStack(spacing: 4.0) {
Group {
Image(systemName: "timer")
Text(self.duration)
}
.foregroundColor(Color.accentColor) .foregroundColor(Color.accentColor)
Text(self.sound.uppercased()).foregroundColor(Color(white: 0.7)) Group {
Image(systemName: "music.note")
Text(self.sound.uppercased())
}
.foregroundColor(Color(white: 0.25))
}.font(.footnote)
}//.padding() }//.padding()
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
Spacer() // Spacer()
}//.background(Color(white: 0.1)) }//.background(Color(white: 0.1))
//.cornerRadius(16.0) //.cornerRadius(16.0)
.monospaced() .monospaced()
@ -193,18 +200,18 @@ enum PresetSection: Int, Identifiable, CaseIterable {
var id: Int { return self.rawValue } var id: Int { return self.rawValue }
// case workout // case workout
case chill case mindfullness
case cooking case cooking
case tea case tea
case other case move
var presets: [Preset] { var presets: [Preset] {
switch self { switch self {
case .cooking: return [.pasta, .rice, .softBoiled, .mediumBoiledEggs, .hardBoiledEggs] case .cooking: return [.pasta, .rice, .softBoiled, .mediumBoiledEggs, .hardBoiledEggs]
case .tea: return [.greenTea, .blackTea] case .tea: return [.greenTea, .blackTea]
// case .workout: return [.runningSplits] // case .workout: return [.runningSplits]
case .chill: return [.nap, .meditation] case .mindfullness: return [.nap, .meditation, .writing, .reading]
case .other: return [.toothbrushing] case .move: return [.workout, .stretching, .toothbrushing, .cleaning]
} }
} }
@ -212,8 +219,8 @@ enum PresetSection: Int, Identifiable, CaseIterable {
switch self { switch self {
case .cooking: return NSLocalizedString("Cooking", comment: "") case .cooking: return NSLocalizedString("Cooking", comment: "")
// case .workout: return NSLocalizedString("Workout", comment: "") // case .workout: return NSLocalizedString("Workout", comment: "")
case .chill: return NSLocalizedString("Chill", comment: "") case .mindfullness: return NSLocalizedString("Self", comment: "")
case .other: return NSLocalizedString("Other", comment: "") case .move: return NSLocalizedString("Move", comment: "")
case .tea: return NSLocalizedString("Tea", comment: "") case .tea: return NSLocalizedString("Tea", comment: "")
} }
} }
@ -232,18 +239,23 @@ struct CountdownInterval {
enum Preset: Int, Identifiable, CaseIterable { enum Preset: Int, Identifiable, CaseIterable {
var id: Int { return self.rawValue } var id: Int { return self.rawValue }
case softBoiled
case mediumBoiledEggs
case hardBoiledEggs
case meditation case meditation
case nap case nap
case runningSplits case workout
case toothbrushing // case runningSplits
case blackTea
case greenTea
case pasta case pasta
case rice case rice
case blackTea
case greenTea
case toothbrushing
case softBoiled
case mediumBoiledEggs
case hardBoiledEggs
case writing
case reading
case stretching
case cleaning
var localizedName: String { var localizedName: String {
switch self { switch self {
case .hardBoiledEggs: return NSLocalizedString("Hard boiled eggs", comment: "") case .hardBoiledEggs: return NSLocalizedString("Hard boiled eggs", comment: "")
@ -251,24 +263,29 @@ enum Preset: Int, Identifiable, CaseIterable {
case .mediumBoiledEggs: return NSLocalizedString("Medium boiled eggs", comment: "") case .mediumBoiledEggs: return NSLocalizedString("Medium boiled eggs", comment: "")
case .meditation: return NSLocalizedString("Meditation", comment: "") case .meditation: return NSLocalizedString("Meditation", comment: "")
case .nap: return NSLocalizedString("Nap", comment: "") case .nap: return NSLocalizedString("Nap", comment: "")
case .runningSplits: return NSLocalizedString("Running splits", comment: "") // case .runningSplits: return NSLocalizedString("Running splits", comment: "")
case .toothbrushing: return NSLocalizedString("Tooth brushing", comment: "") case .toothbrushing: return NSLocalizedString("Tooth brushing", comment: "")
case .blackTea: return NSLocalizedString("Black tea", comment: "") case .blackTea: return NSLocalizedString("Black tea", comment: "")
case .greenTea: return NSLocalizedString("Green tea", comment: "") case .greenTea: return NSLocalizedString("Green tea", comment: "")
case .pasta: return NSLocalizedString("Pasta", comment: "") case .pasta: return NSLocalizedString("Pasta", comment: "")
case .rice: return NSLocalizedString("Rice", comment: "") case .rice: return NSLocalizedString("Rice", comment: "")
case .workout: return NSLocalizedString("Workout", comment: "")
case .writing: return NSLocalizedString("Writing", comment: "")
case .reading: return NSLocalizedString("Reading", comment: "")
case .stretching: return NSLocalizedString("Stretching", comment: "")
case .cleaning: return NSLocalizedString("Cleaning", comment: "")
} }
} }
var intervalGroup: CountdownIntervalGroup { var intervalGroup: CountdownIntervalGroup {
switch self { // switch self {
case .runningSplits: // case .runningSplits:
let runInterval = CountdownInterval(duration: 30.0, sound: Sound.sbArpeggio_Loop_River) // let runInterval = CountdownInterval(duration: 30.0, sound: Sound.sbArpeggio_Loop_River)
let breakInterval = CountdownInterval(duration: 30.0, sound: Sound.sbLoop_ToneSD_Boavista) // let breakInterval = CountdownInterval(duration: 30.0, sound: Sound.sbLoop_ToneSD_Boavista)
return CountdownIntervalGroup(repeatCount: 8, intervals: [runInterval, breakInterval]) // return CountdownIntervalGroup(repeatCount: 8, intervals: [runInterval, breakInterval])
default: // default:
return CountdownIntervalGroup(repeatCount: 0, intervals: [CountdownInterval(duration: self.duration)]) return CountdownIntervalGroup(repeatCount: 0, intervals: [CountdownInterval(duration: self.duration)])
} // }
} }
var duration: TimeInterval { var duration: TimeInterval {
@ -278,22 +295,27 @@ enum Preset: Int, Identifiable, CaseIterable {
case .hardBoiledEggs: return 10 * 60 case .hardBoiledEggs: return 10 * 60
case .meditation: return 15 * 60 case .meditation: return 15 * 60
case .nap: return 20 * 60 case .nap: return 20 * 60
case .runningSplits: return 0.0 // case .runningSplits: return 0.0
case .toothbrushing: return 2 * 60.0 case .toothbrushing: return 2 * 60.0
case .greenTea: return 3 * 60.0 case .greenTea: return 3 * 60.0
case .blackTea: return 4 * 60.0 case .blackTea: return 4 * 60.0
case .pasta: return 10 * 60.0 case .pasta: return 10 * 60.0
case .rice: return 10 * 60.0 case .rice: return 10 * 60.0
case .workout: return 30 * 60.0
case .stretching: return 3 * 60.0
case .writing: return 30 * 60.0
case .reading: return 20 * 60.0
case .cleaning: return 60 * 60.0
} }
} }
var playlist: Playlist { var playlist: Playlist {
switch self { switch self {
case .softBoiled, .mediumBoiledEggs, .hardBoiledEggs, .pasta, .rice, .runningSplits, .toothbrushing: case .softBoiled, .mediumBoiledEggs, .hardBoiledEggs, .pasta, .rice, .toothbrushing, .workout, .stretching:
return .stephanBodzin return .stephanBodzin
case .meditation, .blackTea, .greenTea: case .meditation, .blackTea, .greenTea, .writing, .reading:
return .relax return .relax
case .nap: case .nap, .cleaning:
return .nature return .nature
} }
} }
@ -305,7 +327,7 @@ enum Preset: Int, Identifiable, CaseIterable {
var formattedDuration: String { var formattedDuration: String {
let group = self.intervalGroup let group = self.intervalGroup
let count = group.repeatCount.formatted() let count = group.repeatCount.formatted()
let durations = group.intervals.map { $0.duration.minuteSecond } let durations = group.intervals.map { $0.duration.hourMinuteSecond }
let formattedIntervals = durations.joined(separator: "/") let formattedIntervals = durations.joined(separator: "/")
if group.repeatCount > 1 { if group.repeatCount > 1 {
@ -326,7 +348,7 @@ enum Preset: Int, Identifiable, CaseIterable {
struct PresetsView_Previews: PreviewProvider { struct PresetsView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
PresetsView(tabSelection: .constant(0)) PresetsView()
} }
} }
@ -335,6 +357,5 @@ struct TimerItemView_Previews: PreviewProvider {
TimerItemView(name: "Hard boiled eggs", TimerItemView(name: "Hard boiled eggs",
duration: "10:00", duration: "10:00",
sound: "Stephan Bodzin") sound: "Stephan Bodzin")
.frame(width: UIScreen.main.bounds.width / 2.0)
} }
} }

@ -7,6 +7,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import Combine
protocol SoundHolder { protocol SoundHolder {
func selectSound(_ sound: Sound, selected: Bool) func selectSound(_ sound: Sound, selected: Bool)
@ -52,6 +53,9 @@ class SoundModel: ObservableObject, SoundHolder {
} }
var soundSelection: String { var soundSelection: String {
if self.playlists.count == 1 {
return self.playlists.first!.localizedString
}
if !sounds.isEmpty { if !sounds.isEmpty {
if sounds.count == 1 { if sounds.count == 1 {
return sounds.first!.localizedString return sounds.first!.localizedString

@ -0,0 +1,283 @@
//
// StartView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 22/05/2023.
//
import SwiftUI
class Customization: ObservableObject {
var preset: Preset
@Published var added: Bool = false
@Published var duration: Double = 0.0 {
didSet {
self.added = true
}
}
@Published var timerModel: TimerModel = TimerModel()
init(preset: Preset) {
self.preset = preset
self.duration = preset.duration
self.timerModel.soundModel.selectPlaylist(preset.playlist, selected: true)
self.added = false
}
func toggleAdd() {
self.added = !self.added
Logger.log("ADDDED = \(self.added)")
}
func createTimer() -> AbstractTimer {
let context = PersistenceController.shared.container.viewContext
let countdown = Countdown(context: context)
countdown.duration = self.duration
countdown.playableIds = self.timerModel.soundModel.playableIds
return countdown
}
}
class PresetSelectionModel: ObservableObject {
// @Published var addedPresets: Set<Preset> = []
@Published var customizations: [Preset : Customization] = [:]
// @Published var expanded: Set<Preset> = []
// @Published var duration: [TimeInterval] = []
init() {
self.customizations = Preset.allCases.reduce(into: [Preset: Customization]()) { $0[$1] = Customization(preset: $1) }
}
}
struct DurationButtonView: View {
@StateObject var customization: Customization
@State var showDurationSheet: Bool = false
var body: some View {
Button {
self.showDurationSheet = true
} label: {
Image(systemName: "timer")
Text(customization.duration.hourMinuteSecond)
}
.sheet(isPresented: $showDurationSheet) {
TimePickerView(duration: self.$customization.duration)
.presentationDetents([.height(320.0)])
}
}
}
struct SoundButtonView: View {
@StateObject var soundModel: SoundModel
@State var showSoundSheet: Bool = false
var body: some View {
Button {
self.showSoundSheet = true
} label: {
Image(systemName: "music.note")
Text(self.soundModel.soundSelection)
}.sheet(isPresented: $showSoundSheet) {
PlaylistsView(model: self.soundModel,
catalog: .ring)
.presentationDetents([.height(320.0)])
}
}
}
struct PresetSelectionView: View {
@StateObject var model: PresetSelectionModel
var body: some View {
List {
ForEach(PresetSection.allCases) { section in
Section(section.localizedName.uppercased()) {
ForEach(section.presets.indices, id: \.self) { i in
let preset = section.presets[i]
let customization = self.model.customizations[preset]!
HStack {
Button {
self._addOrRemove(preset: preset)
} label: {
HStack {
let added = customization.added
let image = added ? "checkmark.circle.fill" : "circle"
Image(systemName: image)
.padding(.trailing, 8.0)
.font(.title2)
.foregroundColor(Color.accentColor)
Text(preset.localizedName)
Spacer()
VStack(alignment: .trailing) {
DurationButtonView(customization: customization)
SoundButtonView(soundModel: customization.timerModel.soundModel)
}.buttonStyle(.bordered)
}.font(.callout)
}
}
}
}
}
}.listStyle(.inset)
}
fileprivate func _addOrRemove(preset: Preset) {
self.model.customizations[preset]?.toggleAdd()
}
fileprivate func _added(preset: Preset) -> Bool {
return self.model.customizations[preset]?.added ?? false
}
fileprivate func _image(preset: Preset) -> String {
return self._added(preset: preset) ? "checkmark.circle.fill" : "circle"
}
}
struct StartView: View {
@StateObject var model: PresetSelectionModel = PresetSelectionModel()
@State var showAddScreen: Bool = false
var body: some View {
VStack(spacing: 0.5) {
PresetSelectionView(model: self.model).monospaced()
Button {
self.showAddScreen = true
} label: {
HStack {
Image(systemName: "plus.circle").font(.title)
Text("Create your own").font(.title3)
}
.padding()
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.background(Color.accentColor)
}.sheet(isPresented: self.$showAddScreen) {
NewCountdownView(isPresented: $showAddScreen)
}
Button {
self._done()
} label: {
Text("Done")
.font(.title2).fontWeight(.semibold)
.padding()
.frame(maxWidth: .infinity)
// .foregroundColor(.white)
.background(.white)
}
}.background(.gray)
}
fileprivate func _done() {
let customizations = self.model.customizations.values.filter { $0.added }
for custo in customizations {
let _ = custo.createTimer()
}
let context = PersistenceController.shared.container.viewContext
do {
try context.save()
} catch {
Logger.error(error)
}
}
}
struct ConfigurationButtonView: View {
var preset: Preset
@Binding var duration: TimeInterval
@State var selectedPreset: Preset? = nil
var body: some View {
Button {
self.selectedPreset = preset
} label: {
Image(systemName: "gearshape.fill")
}
.buttonStyle(.plain)
// .foregroundColor(.gray)
.sheet(item: $selectedPreset) { preset in
ConfigurationView(preset: preset,
model: TimerModel(),
duration: self.$duration)
.presentationDetents([.height(320.0)])
}
}
}
struct ConfigurationView: View {
var preset: Preset
var model: TimerModel
@Binding var duration: TimeInterval
var body: some View {
List {
SoundLinkView(soundModel: self.model.soundModel,
catalog: .ring,
title: NSLocalizedString("Sound", comment: ""))
TimePickerView(duration: self.$duration)
}
.listStyle(.plain)
.onAppear {
self.model.soundModel.setPlayables([preset.playlist])
self.duration = preset.duration
}
}
}
struct StartView_Previews: PreviewProvider {
static var previews: some View {
StartView()
ConfigurationView(preset: Preset.blackTea,
model: TimerModel(),
duration: .constant(60.0))
}
}

@ -15,15 +15,13 @@ struct NewStopwatchView: View {
@Environment(\.managedObjectContext) private var viewContext @Environment(\.managedObjectContext) private var viewContext
@Binding var isPresented: Bool @Binding var isPresented: Bool
var tabSelection: Binding<Int>
init(isPresented: Binding<Bool>, tabSelection: Binding<Int>) { init(isPresented: Binding<Bool>) {
_isPresented = isPresented _isPresented = isPresented
self.tabSelection = tabSelection
} }
var body: some View { var body: some View {
StopwatchEditView(isPresented: $isPresented, tabSelection: self.tabSelection) StopwatchEditView(isPresented: $isPresented)
.environment(\.managedObjectContext, viewContext) .environment(\.managedObjectContext, viewContext)
.navigationTitle("New stopwatch") .navigationTitle("New stopwatch")
} }
@ -61,15 +59,12 @@ struct StopwatchEditView: View {
@Environment(\.isPresented) var envIsPresented @Environment(\.isPresented) var envIsPresented
var tabSelection: Binding<Int>? = nil
@FetchRequest(sortDescriptors: []) @FetchRequest(sortDescriptors: [])
private var timers: FetchedResults<AbstractTimer> private var timers: FetchedResults<AbstractTimer>
init(isPresented: Binding<Bool>, stopwatch: Stopwatch? = nil, tabSelection: Binding<Int>? = nil) { init(isPresented: Binding<Bool>, stopwatch: Stopwatch? = nil) {
_isPresented = isPresented _isPresented = isPresented
self.stopwatch = stopwatch self.stopwatch = stopwatch
self.tabSelection = tabSelection
} }
var body: some View { var body: some View {
@ -241,8 +236,6 @@ struct StopwatchEditView: View {
} else { } else {
dismiss() dismiss()
} }
self.tabSelection?.wrappedValue = 1
} }
fileprivate func _delete() { fileprivate func _delete() {
@ -276,7 +269,7 @@ struct StopwatchEditView: View {
struct NewStopwatchView_Previews: PreviewProvider { struct NewStopwatchView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
NewStopwatchView(isPresented: .constant(true), tabSelection: .constant(0)) NewStopwatchView(isPresented: .constant(true))
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
} }
} }

@ -267,3 +267,12 @@
"Calendar" = "Calendrier"; "Calendar" = "Calendrier";
"Disclaimer" = "Ceci est une version beta d'Enchante.\n\nPour l'instant, veillez à ne pas dépendre de l'app pour des évènements trop critiques, on ne sait jamais :)\n\nSi vous avez des remarques ou un problème, merci de me contacter en allant dans les réglages."; "Disclaimer" = "Ceci est une version beta d'Enchante.\n\nPour l'instant, veillez à ne pas dépendre de l'app pour des évènements trop critiques, on ne sait jamais :)\n\nSi vous avez des remarques ou un problème, merci de me contacter en allant dans les réglages.";
"Volume" = "Volume"; "Volume" = "Volume";
"Workout" = "Exercice";
"Move" = "Bouger";
"Self" = "Soi";
"Writing" = "Écriture";
"Reading" = "Lecture";
"Stretching" = "Étirements";
"Cleaning" = "Nettoyage";
"Done" = "OK";
"Create your own" = "Créez les vôtres";

Loading…
Cancel
Save