Improved timer widget
This commit is contained in:
Binary file not shown.
@@ -10,6 +10,22 @@ import SwiftUI
|
|||||||
|
|
||||||
|
|
||||||
class DurationComponents: ObservableObject {
|
class DurationComponents: ObservableObject {
|
||||||
|
@Published var secondComponent: String = "00" {
|
||||||
|
didSet {
|
||||||
|
if secondComponent.count > 2 {
|
||||||
|
secondComponent = oldValue
|
||||||
|
} else if secondComponent.count == 1 {
|
||||||
|
secondComponent = "0\(secondComponent)"
|
||||||
|
} else if secondComponent.count == 0 {
|
||||||
|
secondComponent = "00"
|
||||||
|
}
|
||||||
|
let filtered = secondComponent.filter { $0.isNumber }
|
||||||
|
if secondComponent != filtered {
|
||||||
|
secondComponent = filtered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Published var minuteComponent: String = "00" {
|
@Published var minuteComponent: String = "00" {
|
||||||
didSet {
|
didSet {
|
||||||
if minuteComponent.count > 2 {
|
if minuteComponent.count > 2 {
|
||||||
@@ -42,6 +58,19 @@ class DurationComponents: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func fromPTString(_ PTRepresentation: String) -> DurationComponents {
|
||||||
|
let duration = DurationComponents()
|
||||||
|
let hourRegex = /([0-9]{1,2})H/
|
||||||
|
let minuteRegex = /([0-9]{1,2})M/
|
||||||
|
if let match = PTRepresentation.firstMatch(of: hourRegex) {
|
||||||
|
duration.hourComponent = String(match.1)
|
||||||
|
}
|
||||||
|
if let match = PTRepresentation.firstMatch(of: minuteRegex) {
|
||||||
|
duration.minuteComponent = String(match.1)
|
||||||
|
}
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
|
||||||
func fromPTString(_ PTRepresentation: String) {
|
func fromPTString(_ PTRepresentation: String) {
|
||||||
let hourRegex = /([0-9]{1,2})H/
|
let hourRegex = /([0-9]{1,2})H/
|
||||||
let minuteRegex = /([0-9]{1,2})M/
|
let minuteRegex = /([0-9]{1,2})M/
|
||||||
@@ -60,6 +89,7 @@ class DurationComponents: ObservableObject {
|
|||||||
func toText() -> LocalizedStringKey {
|
func toText() -> LocalizedStringKey {
|
||||||
let intHour = Int(hourComponent) ?? 0
|
let intHour = Int(hourComponent) ?? 0
|
||||||
let intMinute = Int(minuteComponent) ?? 0
|
let intMinute = Int(minuteComponent) ?? 0
|
||||||
|
|
||||||
if intHour != 0 && intMinute != 0 {
|
if intHour != 0 && intMinute != 0 {
|
||||||
return "\(intHour) h, \(intMinute) min"
|
return "\(intHour) h, \(intMinute) min"
|
||||||
} else if intHour == 0 && intMinute != 0 {
|
} else if intHour == 0 && intMinute != 0 {
|
||||||
@@ -71,6 +101,32 @@ class DurationComponents: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toTimerText() -> String {
|
||||||
|
var timeString = ""
|
||||||
|
if hourComponent != "00" {
|
||||||
|
timeString.append("\(hourComponent):")
|
||||||
|
}
|
||||||
|
timeString.append("\(minuteComponent):")
|
||||||
|
timeString.append("\(secondComponent)")
|
||||||
|
return timeString
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSeconds() -> Double {
|
||||||
|
guard let hours = Double(hourComponent) else { return 0 }
|
||||||
|
guard let minutes = Double(minuteComponent) else { return 0 }
|
||||||
|
guard let seconds = Double(secondComponent) else { return 0 }
|
||||||
|
return hours * 3600 + minutes * 60 + seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromSeconds(_ totalSeconds: Int) {
|
||||||
|
let hours = totalSeconds / 3600
|
||||||
|
let minutes = (totalSeconds % 3600) / 60
|
||||||
|
let seconds = totalSeconds % 60
|
||||||
|
self.hourComponent = String(hours)
|
||||||
|
self.minuteComponent = String(minutes)
|
||||||
|
self.secondComponent = String(seconds)
|
||||||
|
}
|
||||||
|
|
||||||
static func ptToText(_ ptString: String) -> String? {
|
static func ptToText(_ ptString: String) -> String? {
|
||||||
let hourRegex = /([0-9]{1,2})H/
|
let hourRegex = /([0-9]{1,2})H/
|
||||||
let minuteRegex = /([0-9]{1,2})M/
|
let minuteRegex = /([0-9]{1,2})M/
|
||||||
|
|||||||
@@ -614,33 +614,13 @@ extension DateFormatter {
|
|||||||
|
|
||||||
// Timer logic
|
// Timer logic
|
||||||
extension MainViewModel {
|
extension MainViewModel {
|
||||||
func createTimer(forRecipe recipeId: String, timeTotal: Double) -> RecipeTimer {
|
func createTimer(forRecipe recipeId: String, duration: DurationComponents) -> RecipeTimer {
|
||||||
let timer = RecipeTimer(timeTotal: timeTotal)
|
let timer = RecipeTimer(duration: duration)
|
||||||
timers[recipeId] = timer
|
timers[recipeId] = timer
|
||||||
return timer
|
return timer
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTimer(forRecipe recipeId: String, timeTotal: Double) -> RecipeTimer {
|
func getTimer(forRecipe recipeId: String, duration: DurationComponents) -> RecipeTimer {
|
||||||
return timers[recipeId] ?? createTimer(forRecipe: recipeId, timeTotal: timeTotal)
|
return timers[recipeId] ?? createTimer(forRecipe: recipeId, duration: duration)
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func startTimer(forRecipe recipeId: String, timeTotal: Double) {
|
|
||||||
let timer = RecipeTimer(timeTotal: timeTotal)
|
|
||||||
timer.start()
|
|
||||||
timers[recipeId] = timer
|
|
||||||
}
|
|
||||||
|
|
||||||
func pauseTimer(forRecipe recipeId: String) {
|
|
||||||
timers[recipeId]?.pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
func resumeTimer(forRecipe recipeId: String) {
|
|
||||||
timers[recipeId]?.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
func cancelTimer(forRecipe recipeId: String) {
|
|
||||||
timers[recipeId]?.cancel()
|
|
||||||
timers[recipeId] = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ struct RecipeDetailView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
TimerView(timer: viewModel.getTimer(forRecipe: recipeDetail.id, timeTotal: 20))
|
|
||||||
RecipeDurationSection(recipeDetail: recipeDetail)
|
RecipeDurationSection(viewModel: viewModel, recipeDetail: recipeDetail)
|
||||||
|
|
||||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
|
||||||
if(!recipeDetail.recipeIngredient.isEmpty) {
|
if(!recipeDetail.recipeIngredient.isEmpty) {
|
||||||
@@ -214,10 +214,11 @@ fileprivate struct ShareView: View {
|
|||||||
|
|
||||||
|
|
||||||
fileprivate struct RecipeDurationSection: View {
|
fileprivate struct RecipeDurationSection: View {
|
||||||
|
@ObservedObject var viewModel: MainViewModel
|
||||||
@State var recipeDetail: RecipeDetail
|
@State var recipeDetail: RecipeDetail
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150), alignment: .leading)]) {
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 250), alignment: .leading)]) {
|
||||||
if let prepTime = recipeDetail.prepTime, let time = DurationComponents.ptToText(prepTime) {
|
if let prepTime = recipeDetail.prepTime, let time = DurationComponents.ptToText(prepTime) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -229,6 +230,12 @@ fileprivate struct RecipeDurationSection: View {
|
|||||||
}.padding()
|
}.padding()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let cookTime = recipeDetail.cookTime, let time = DurationComponents.ptToText(cookTime) {
|
||||||
|
TimerView(timer: viewModel.getTimer(forRecipe: recipeDetail.id, duration: DurationComponents.fromPTString(cookTime)))
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
if let cookTime = recipeDetail.cookTime, let time = DurationComponents.ptToText(cookTime) {
|
if let cookTime = recipeDetail.cookTime, let time = DurationComponents.ptToText(cookTime) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -238,7 +245,7 @@ fileprivate struct RecipeDurationSection: View {
|
|||||||
Text(time)
|
Text(time)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}.padding()
|
}.padding()
|
||||||
}
|
}*/
|
||||||
|
|
||||||
if let totalTime = recipeDetail.totalTime, let time = DurationComponents.ptToText(totalTime) {
|
if let totalTime = recipeDetail.totalTime, let time = DurationComponents.ptToText(totalTime) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|||||||
@@ -27,29 +27,27 @@ struct TimerView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
if timer.isRunning {
|
if timer.isRunning {
|
||||||
Image(systemName: "pause.fill")
|
Image(systemName: "pause.fill")
|
||||||
.foregroundStyle(.blue)
|
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: "play.fill")
|
Image(systemName: "play.fill")
|
||||||
.foregroundStyle(.blue)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.gaugeStyle(.accessoryCircularCapacity)
|
.gaugeStyle(.accessoryCircularCapacity)
|
||||||
.animation(.easeInOut, value: timer.timeElapsed)
|
.animation(.easeInOut, value: timer.timeElapsed)
|
||||||
.tint(.white)
|
.tint(timer.isRunning ? .green : .nextcloudBlue)
|
||||||
|
.foregroundStyle(timer.isRunning ? Color.green : Color.nextcloudBlue)
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
Text("Cooking")
|
||||||
Text("Cooking time")
|
Text(timer.duration.toTimerText())
|
||||||
.padding(.horizontal)
|
}
|
||||||
Button {
|
.padding(.horizontal)
|
||||||
timer.cancel()
|
|
||||||
} label: {
|
Button {
|
||||||
Image(systemName: "xmark.circle.fill")
|
timer.cancel()
|
||||||
}
|
} label: {
|
||||||
}
|
Image(systemName: "xmark.circle.fill")
|
||||||
Text("\(Int(timer.timeTotal - timer.timeElapsed))")
|
.foregroundStyle(timer.isRunning ? Color.nextcloudBlue : Color.secondary)
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.bold()
|
.bold()
|
||||||
@@ -65,6 +63,7 @@ struct TimerView: View {
|
|||||||
|
|
||||||
class RecipeTimer: ObservableObject {
|
class RecipeTimer: ObservableObject {
|
||||||
var timeTotal: Double
|
var timeTotal: Double
|
||||||
|
@Published var duration: DurationComponents
|
||||||
private var startDate: Date?
|
private var startDate: Date?
|
||||||
private var pauseDate: Date?
|
private var pauseDate: Date?
|
||||||
@Published var timeElapsed: Double = 0
|
@Published var timeElapsed: Double = 0
|
||||||
@@ -72,8 +71,9 @@ class RecipeTimer: ObservableObject {
|
|||||||
private var timer: Timer.TimerPublisher?
|
private var timer: Timer.TimerPublisher?
|
||||||
private var timerCancellable: Cancellable?
|
private var timerCancellable: Cancellable?
|
||||||
|
|
||||||
init(timeTotal: Double) {
|
init(duration: DurationComponents) {
|
||||||
self.timeTotal = timeTotal
|
self.duration = duration
|
||||||
|
self.timeTotal = duration.toSeconds()
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
@@ -93,8 +93,10 @@ class RecipeTimer: ObservableObject {
|
|||||||
let elapsed = Date().timeIntervalSince(startTime)
|
let elapsed = Date().timeIntervalSince(startTime)
|
||||||
if elapsed < self.timeTotal {
|
if elapsed < self.timeTotal {
|
||||||
self.timeElapsed = elapsed
|
self.timeElapsed = elapsed
|
||||||
|
self.duration.fromSeconds(Int(self.timeTotal - self.timeElapsed))
|
||||||
} else {
|
} else {
|
||||||
self.timeElapsed = self.timeTotal
|
self.timeElapsed = self.timeTotal
|
||||||
|
self.duration.fromSeconds(Int(self.timeTotal - self.timeElapsed))
|
||||||
self.pause()
|
self.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,5 +125,6 @@ class RecipeTimer: ObservableObject {
|
|||||||
self.timeElapsed = 0
|
self.timeElapsed = 0
|
||||||
self.startDate = nil
|
self.startDate = nil
|
||||||
self.pauseDate = nil
|
self.pauseDate = nil
|
||||||
|
self.duration.fromSeconds(Int(timeTotal))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user