You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
LeCountdown/LeCountdown/Views/Countdown/NewCountdownView.swift

413 lines
13 KiB

//
// NewCountdownView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 20/01/2023.
//
import SwiftUI
import CoreData
import WidgetKit
import Intents
struct NewCountdownView : View {
@Environment(\.managedObjectContext) private var viewContext
@Binding var isPresented: Bool
var tabSelection: Binding<Int>
var userActivity: NSUserActivity
init(isPresented: Binding<Bool>, tabSelection: Binding<Int>) {
_isPresented = isPresented
self.tabSelection = tabSelection
self.userActivity = Shortcut.newCountdown.userActivity
// let shortcut = INShortcut(userActivity: self.userActivity)
}
var body: some View {
NavigationStack {
CountdownEditView(isPresented: $isPresented, tabSelection: self.tabSelection)
.environment(\.managedObjectContext, viewContext)
.onAppear {
self.userActivity.becomeCurrent()
}
.onDisappear {
self.userActivity.resignCurrent()
}
}
}
}
struct CountdownEditView : View {
@Environment(\.managedObjectContext) private var viewContext
@Environment(\.dismiss) private var dismiss
@StateObject var model: TimerModel = TimerModel()
var countdown: Countdown? = nil
var preset: Preset? = nil
@Binding var isPresented: Bool
@State var nameString: String = ""
@State var secondsString: String = ""
@State var minutesString: String = ""
@State var hoursString: String = ""
@State var soundRepeatCount: Int16 = 0
@State var image: CoolPic = .pic1
@State var deleteConfirmationShown: Bool = false
@State var activityNameConfirmationShown: Bool = false
@State fileprivate var _rename: Bool? = nil
@State var errorShown: Bool = false
@State var error: Error? = nil
var tabSelection: Binding<Int>? = nil
// @FocusState private var textFieldIsFocused: Bool
// @FetchRequest(sortDescriptors: [])
// private var timers: FetchedResults<AbstractTimer>
@State var _isNewCountdown: Bool = false // false if editing an existing countdown
@State var _hasLoaded = false
@Environment(\.isPresented) var envIsPresented
@State var shouldScrollToTop: Bool = false
@FocusState private var focusedField: CountdownField?
init(isPresented: Binding<Bool>, countdown: Countdown? = nil, tabSelection: Binding<Int>? = nil) {
_isPresented = isPresented
self.countdown = countdown
self.tabSelection = tabSelection
}
init(isPresented: Binding<Bool>, preset: Preset, tabSelection: Binding<Int>) {
_isPresented = isPresented
self.preset = preset
self.tabSelection = tabSelection
}
fileprivate var _formId = "formId"
var body: some View {
NavigationStack {
ScrollViewReader { reader in
Rectangle()
.frame(width: 0.0, height: 0.0)
.onChange(of: envIsPresented) { newValue in
if !newValue && !self._isNewCountdown {
self._save() // save when leaving an edit screen
}
}
Form {
EmptyView().id("anchor")
CountdownFormView(
focusedField: _focusedField,
nameBinding: $nameString,
secondsBinding: $secondsString,
minutesBinding: $minutesString,
hoursBinding: $hoursString,
imageBinding: $image,
repeatCountBinding: $soundRepeatCount)
.environmentObject(self.model)
BasePresetsView { preset in
self._loadPreset(preset)
}
}.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button {
Logger.log("NIL!!!!")
self.focusedField = nil
} label: {
Image(systemName: "keyboard.chevron.compact.down")
}
Spacer()
Button {
self.focusPreviousField($focusedField)
} label: {
Image(systemName: "chevron.up")
}
Button {
self.focusNextField($focusedField)
} label: {
Image(systemName: "chevron.down")
}
}
}
.onChange(of: self.shouldScrollToTop) { newValue in
withAnimation {
reader.scrollTo("anchor")
}
}
}
.navigationTitle(self.countdown?.name ?? NSLocalizedString("New timer", comment: ""))
}
.onAppear {
self._onAppear()
}
.confirmationDialog("", isPresented: $deleteConfirmationShown, actions: {
Button("Yes", role: .destructive) {
withAnimation {
self._delete()
}
}.keyboardShortcut(.defaultAction)
Button("No", role: .cancel) {}
}, message: {
Text("Do you really want to delete?")
})
.confirmationDialog("", isPresented: $activityNameConfirmationShown, actions: {
Button("Rename") {
self._rename = true
self._save()
}
Button("New activity") {
self._rename = false
self._save()
}
}, message: {
Text("Do you wish to rename or create a new activity")
})
.alert("", isPresented: $errorShown, actions: { }, message: {
Text(error?.localizedDescription ?? "error")
})
.toolbar {
if self._isNewCountdown {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
self._cancel()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
self._save()
}
}
} else {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
self.deleteConfirmationShown = true
} label: {
Image(systemName: "trash")
}
}
}
}
}
// MARK: - Business
fileprivate func _onAppear() {
self._isNewCountdown = (self.countdown == nil)
if !self._hasLoaded {
if let countdown {
self._loadCountdown(countdown)
} else if let preset {
self._loadPreset(preset)
}
self._hasLoaded = true
}
}
fileprivate func _loadPreset(_ preset: Preset) {
self.nameString = preset.localizedName
let nf = NumberFormatter()
let minutes = Int(preset.duration / 60.0)
if minutes > 0 {
self.minutesString = nf.string(from: NSNumber(value: minutes)) ?? ""
} else {
self.minutesString = ""
}
let seconds = Int(preset.duration) - minutes * 60
if seconds > 0 {
self.secondsString = nf.string(from: NSNumber(value: seconds)) ?? ""
} else {
self.secondsString = ""
}
self.model.group = preset.intervalGroup
self.model.soundModel.loadPreset(preset)
self.shouldScrollToTop.toggle()
}
fileprivate func _loadCountdown(_ countdown: Countdown) {
let hours = Int(countdown.duration / 3600.0)
let minutes = Int(countdown.duration - Double(hours * 3600)) / 60
let seconds = countdown.duration - Double(hours * 3600) - Double(minutes * 60)
if hours > 0 {
self.hoursString = self._numberFormatter.string(from: NSNumber(value: hours)) ?? ""
}
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.soundModel.setPlayables(countdown.playables)
self.soundRepeatCount = countdown.repeatCount
if let image = countdown.image,
let coolpic = CoolPic(rawValue: image) {
self.image = coolpic
}
}
fileprivate let _numberFormatter = NumberFormatter()
fileprivate var _seconds: Double {
return self._numberFormatter.number(from: self.secondsString)?.doubleValue ?? 0.0
}
fileprivate var _minutes: Double {
return self._numberFormatter.number(from: self.minutesString)?.doubleValue ?? 0.0
}
fileprivate var _hours: Double {
return self._numberFormatter.number(from: self.hoursString)?.doubleValue ?? 0.0
}
fileprivate func _cancel() {
self.viewContext.rollback()
self.isPresented = false
}
fileprivate func _save() {
let cd: Countdown
if let countdown {
cd = countdown
} else {
cd = Countdown(context: viewContext)
}
cd.duration = self._hours * 3600.0 + self._minutes * 60.0 + self._seconds
if self._isNewCountdown {
let max: Int16
do {
let request = AbstractTimer.fetchRequest()
let timers = try viewContext.fetch(request)
if let maxOrder = timers.map({ $0.order }).max() {
max = maxOrder + 1
} else {
max = 0
}
} catch {
max = 0
}
cd.order = max
}
cd.image = self.image.rawValue
cd.playableIds = self.model.soundModel.playableIds
cd.setConfirmationSounds(self.model.confirmationSoundModel.sounds)
cd.repeatCount = self.soundRepeatCount
if !self.nameString.isEmpty {
let trimmed = self.nameString.trimmingCharacters(in: .whitespacesAndNewlines)
if let activity = cd.activity, let currentActivityName = activity.name, trimmed != currentActivityName {
switch self._rename {
case .none:
self.activityNameConfirmationShown = true
return
case .some(let rename):
if rename {
activity.name = trimmed
} else {
cd.activity = CoreDataRequests.getOrCreateActivity(name: trimmed)
}
}
} else {
cd.activity = CoreDataRequests.getOrCreateActivity(name: trimmed)
}
}
self._saveContext()
WidgetCenter.shared.reloadAllTimelines() // refreshes the visual of existing widgets
self._popOrDismiss()
}
fileprivate func _popOrDismiss() {
if self._isNewCountdown {
self.isPresented = false
} else {
dismiss()
}
self.tabSelection?.wrappedValue = 1
}
fileprivate func _delete() {
guard let countdown else {
return
}
// remove the siri donation
INInteraction.delete(with: [countdown.stringId]) { _ in }
viewContext.delete(countdown)
self._saveContext()
WidgetCenter.shared.reloadAllTimelines() // refreshes the visual of existing widgets
self._popOrDismiss()
}
fileprivate func _saveContext() {
do {
try viewContext.save()
} catch {
Logger.error(error)
self.errorShown = true
self.error = error
}
}
}
struct NewCountdownView_Previews: PreviewProvider {
static var previews: some View {
NewCountdownView(isPresented: .constant(true),
tabSelection: .constant(0))
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}