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.
281 lines
8.0 KiB
281 lines
8.0 KiB
//
|
|
// LiveTimerView.swift
|
|
// LeCountdown
|
|
//
|
|
// Created by Laurent Morvillier on 03/02/2023.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
fileprivate let liveViewSize: CGFloat = 70.0
|
|
fileprivate let timerFontSize: CGFloat = 32.0
|
|
fileprivate let actionButtonFontSize: CGFloat = 36.0
|
|
|
|
struct TimeView: View {
|
|
|
|
var text: String
|
|
|
|
var body: some View {
|
|
Text(self.text)
|
|
.font(.system(size: timerFontSize, weight: .medium))
|
|
.minimumScaleFactor(0.1)
|
|
}
|
|
|
|
}
|
|
|
|
struct LiveTimerListView: View {
|
|
|
|
@Environment(\.managedObjectContext) private var viewContext
|
|
@EnvironmentObject var conductor: Conductor
|
|
|
|
var body: some View {
|
|
|
|
VStack {
|
|
ForEach(conductor.liveTimers) { liveTimer in
|
|
if let timer: AbstractTimer = liveTimer.timer(context: self.viewContext) {
|
|
LiveTimerView(timer: timer, date: liveTimer.date, endDate: liveTimer.endDate)
|
|
}
|
|
}
|
|
|
|
}.padding()
|
|
|
|
}
|
|
|
|
fileprivate func _columnCount() -> Int {
|
|
#if os(iOS)
|
|
if UIDevice.isPhoneIdiom {
|
|
return 18
|
|
} else {
|
|
return 3
|
|
}
|
|
#else
|
|
return 3
|
|
#endif
|
|
}
|
|
|
|
fileprivate func _columns() -> [GridItem] {
|
|
return (0..<self._columnCount()).map { _ in GridItem(spacing: 20.0) }
|
|
}
|
|
|
|
}
|
|
|
|
struct LiveTimerView: View {
|
|
|
|
var timer: AbstractTimer
|
|
var date: Date
|
|
var endDate: Date?
|
|
|
|
var body: some View {
|
|
|
|
switch self.timer {
|
|
case let cd as Countdown:
|
|
LiveCountdownView(countdown: cd, date: self.date)
|
|
case let sw as Stopwatch:
|
|
LiveStopwatchView(stopwatch: sw, date: self.date, endDate: self.endDate)
|
|
default:
|
|
Text("unmanaged timer: \(timer)")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
struct LiveStopwatchView: View {
|
|
|
|
@Environment(\.managedObjectContext) private var viewContext
|
|
|
|
@State var stopwatch: Stopwatch
|
|
|
|
var date: Date
|
|
var endDate: Date?
|
|
|
|
var body: some View {
|
|
|
|
HStack {
|
|
|
|
VStack(alignment: .leading) {
|
|
|
|
if self.endDate == nil {
|
|
TimelineView(.periodic(from: self.date, by: 0.01)) { context in
|
|
TimeView(text: self._formattedDuration(date: context.date))
|
|
}
|
|
} else {
|
|
let duration = self.endDate?.timeIntervalSince(self.date) ?? 0.0
|
|
TimeView(text: duration.hourMinuteSecondHS)
|
|
}
|
|
Text(self.stopwatch.displayName.uppercased())
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Group {
|
|
if self.endDate != nil {
|
|
GreenCheckmarkView()
|
|
} else {
|
|
Image(systemName: "stop.circle.fill")
|
|
.foregroundColor(.accentColor)
|
|
}
|
|
}.font(.system(size: actionButtonFontSize))
|
|
|
|
}
|
|
.monospaced()
|
|
.frame(height: liveViewSize)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
self._actionHandler()
|
|
}
|
|
}
|
|
|
|
fileprivate func _actionHandler() {
|
|
FileLogger.log("stopwatch _actionHandler")
|
|
withAnimation {
|
|
if self.endDate == nil {
|
|
self._stop()
|
|
} else {
|
|
self._dismiss()
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func _dismiss() {
|
|
Conductor.maestro.removeLiveTimer(id: self.stopwatch.stringId)
|
|
}
|
|
|
|
fileprivate func _formattedDuration(date: Date) -> String {
|
|
let duration = date.timeIntervalSince(self.date)
|
|
return duration.hourMinuteSecondHS
|
|
}
|
|
|
|
fileprivate func _stop() {
|
|
Conductor.maestro.stopStopwatch(self.stopwatch)
|
|
}
|
|
}
|
|
|
|
struct LiveCountdownView: View {
|
|
|
|
@Environment(\.managedObjectContext) private var viewContext
|
|
|
|
@State var countdown: Countdown
|
|
var date: Date
|
|
|
|
@State var showConfirmationPopup: Bool = false
|
|
|
|
var body: some View {
|
|
|
|
TimelineView(.periodic(from: self.date, by: 0.01)) { context in
|
|
|
|
let cancelled = Conductor.maestro.isCountdownCancelled(self.countdown)
|
|
|
|
HStack {
|
|
|
|
let running = self.date > context.date
|
|
|
|
VStack(alignment: .leading) {
|
|
|
|
if cancelled {
|
|
TimeView(text: NSLocalizedString("Cancelled", comment: ""))
|
|
} else if running {
|
|
TimeView(text: self._formattedDuration(date: context.date))
|
|
} else {
|
|
TimeView(text: self.date.formatted(date: .omitted, time: .shortened))
|
|
}
|
|
|
|
Text(self.countdown.displayName.uppercased())
|
|
|
|
}
|
|
Spacer()
|
|
|
|
Group {
|
|
if cancelled {
|
|
Image(systemName: "xmark.circle")
|
|
.foregroundColor(.accentColor)
|
|
} else if !running {
|
|
GreenCheckmarkView()
|
|
} else {
|
|
Button {
|
|
self.showConfirmationPopup = true
|
|
} label: {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.foregroundColor(.accentColor)
|
|
}
|
|
}
|
|
}.font(.system(size: actionButtonFontSize))
|
|
|
|
}
|
|
}
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
if Date() > self.date || Conductor.maestro.isCountdownCancelled(self.countdown) {
|
|
self._dismiss()
|
|
}
|
|
}
|
|
.frame(height: liveViewSize)
|
|
.monospaced()
|
|
.fullScreenCover(isPresented: self.$showConfirmationPopup) {
|
|
|
|
ZStack {
|
|
Color.black.opacity(0.1)
|
|
.edgesIgnoringSafeArea(.all)
|
|
|
|
let name = self.countdown.displayName
|
|
|
|
Button(String(format: NSLocalizedString("Cancel %@", comment: ""), name)) {
|
|
self._cancelCountdown()
|
|
self.showConfirmationPopup = false
|
|
}
|
|
.monospaced()
|
|
.padding()
|
|
.foregroundColor(.white)
|
|
.background(Color.accentColor)
|
|
.cornerRadius(8.0)
|
|
}.onTapGesture {
|
|
self.showConfirmationPopup = false
|
|
}
|
|
.background(BackgroundBlurView())
|
|
|
|
}
|
|
}
|
|
|
|
fileprivate func _actionHandler() {
|
|
|
|
FileLogger.log("countdown _actionHandler")
|
|
if Conductor.maestro.currentCountdowns[self.countdown.stringId] != nil {
|
|
self._cancelCountdown()
|
|
} else {
|
|
self._dismiss()
|
|
}
|
|
}
|
|
|
|
fileprivate func _dismiss() {
|
|
withAnimation {
|
|
Conductor.maestro.removeLiveTimer(id: self.countdown.stringId)
|
|
}
|
|
}
|
|
|
|
fileprivate func _formattedDuration(date: Date) -> String {
|
|
let duration = self.date.timeIntervalSince(date)
|
|
return duration.minuteSecond
|
|
}
|
|
|
|
fileprivate func _cancelCountdown() {
|
|
withAnimation {
|
|
Conductor.maestro.cancelCountdown(id: self.countdown.stringId)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
struct LiveTimerView_Previews: PreviewProvider {
|
|
|
|
static var previews: some View {
|
|
|
|
Group {
|
|
VStack(spacing: 20.0) {
|
|
LiveCountdownView(countdown: Countdown.fake(context: PersistenceController.preview.container.viewContext), date: Date().addingTimeInterval(3600.0))
|
|
.environmentObject(Conductor.maestro)
|
|
LiveStopwatchView(stopwatch: Stopwatch.fake(context: PersistenceController.preview.container.viewContext), date: Date())
|
|
.environmentObject(Conductor.maestro)
|
|
}
|
|
}.padding()
|
|
}
|
|
|
|
}
|
|
|