diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj index 9236c86..35f3cfb 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ A7FB0D7E2B25C6A200A3469E /* V2LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FB0D7D2B25C6A200A3469E /* V2LoginView.swift */; }; A9CA6CEF2B4C086100F78AB5 /* RecipeExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA6CEE2B4C086100F78AB5 /* RecipeExporter.swift */; }; A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */ = {isa = PBXBuildFile; productRef = A9CA6CF52B4C63F200F78AB5 /* TPPDF */; }; + A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D89AAF2B4FE97800F49D92 /* TimerView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -113,6 +114,7 @@ A7FB0D7B2B25C68500A3469E /* TokenLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenLoginView.swift; sourceTree = ""; }; A7FB0D7D2B25C6A200A3469E /* V2LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V2LoginView.swift; sourceTree = ""; }; A9CA6CEE2B4C086100F78AB5 /* RecipeExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeExporter.swift; sourceTree = ""; }; + A9D89AAF2B4FE97800F49D92 /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -243,6 +245,7 @@ A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */, A76B8A702AE002AE00096CEC /* Alerts.swift */, A7CD3FD12B2C546A00D764AD /* CollapsibleView.swift */, + A9D89AAF2B4FE97800F49D92 /* TimerView.swift */, ); path = Views; sourceTree = ""; @@ -462,6 +465,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */, A79AA8E22AFF8C14007D25F2 /* RecipeEditViewModel.swift in Sources */, A7FB0D7C2B25C68500A3469E /* TokenLoginView.swift in Sources */, A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */, diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate index 072e5e8..c1608cf 100644 Binary files a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate and b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Nextcloud Cookbook iOS Client/Localizable.xcstrings b/Nextcloud Cookbook iOS Client/Localizable.xcstrings index 4dc9b12..4454279 100644 --- a/Nextcloud Cookbook iOS Client/Localizable.xcstrings +++ b/Nextcloud Cookbook iOS Client/Localizable.xcstrings @@ -776,6 +776,9 @@ } } } + }, + "Cooking time" : { + }, "Copy Link" : { "localizations" : { diff --git a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift index 389cf69..bf45e8f 100644 --- a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift +++ b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift @@ -16,10 +16,12 @@ import UIKit @Published var categories: [Category] = [] @Published var recipes: [String: [Recipe]] = [:] @Published var recipeDetails: [Int: RecipeDetail] = [:] + @Published var timers: [String: RecipeTimer] = [:] var recipeImages: [Int: [String: UIImage]] = [:] var imagesNeedUpdate: [Int: [String: Bool]] = [:] var lastUpdates: [String: Date] = [:] + private let api: CookbookApi.Type private let dataStore: DataStore @@ -608,3 +610,37 @@ extension DateFormatter { return dateFormatter.string(from: date) } } + + +// Timer logic +extension MainViewModel { + func createTimer(forRecipe recipeId: String, timeTotal: Double) -> RecipeTimer { + let timer = RecipeTimer(timeTotal: timeTotal) + timers[recipeId] = timer + return timer + } + + func getTimer(forRecipe recipeId: String, timeTotal: Double) -> RecipeTimer { + return timers[recipeId] ?? createTimer(forRecipe: recipeId, timeTotal: timeTotal) + } + + + 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 + } +} diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift index b8d8c2e..30c2dda 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift @@ -61,7 +61,7 @@ struct RecipeDetailView: View { } Divider() - + TimerView(timer: viewModel.getTimer(forRecipe: recipeDetail.id, timeTotal: 20)) RecipeDurationSection(recipeDetail: recipeDetail) LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) { diff --git a/Nextcloud Cookbook iOS Client/Views/TimerView.swift b/Nextcloud Cookbook iOS Client/Views/TimerView.swift new file mode 100644 index 0000000..b95b2c3 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Views/TimerView.swift @@ -0,0 +1,127 @@ +// +// TimerView.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 11.01.24. +// + +import Foundation +import SwiftUI +import Combine + + +struct TimerView: View { + @ObservedObject var timer: RecipeTimer + + var body: some View { + HStack { + Gauge(value: timer.timeTotal - timer.timeElapsed, in: 0...timer.timeTotal) { + Text("Cooking time") + } currentValueLabel: { + Button { + if timer.isRunning { + timer.pause() + } else { + timer.start() + } + } label: { + if timer.isRunning { + Image(systemName: "pause.fill") + .foregroundStyle(.blue) + } else { + Image(systemName: "play.fill") + .foregroundStyle(.blue) + } + } + } + .gaugeStyle(.accessoryCircularCapacity) + .animation(.easeInOut, value: timer.timeElapsed) + .tint(.white) + + VStack(alignment: .leading) { + HStack { + Text("Cooking time") + .padding(.horizontal) + Button { + timer.cancel() + } label: { + Image(systemName: "xmark.circle.fill") + } + } + Text("\(Int(timer.timeTotal - timer.timeElapsed))") + .padding(.horizontal) + } + } + .bold() + .padding() + .background { + RoundedRectangle(cornerRadius: 20) + .foregroundStyle(.ultraThickMaterial) + } + } +} + + + +class RecipeTimer: ObservableObject { + var timeTotal: Double + private var startDate: Date? + private var pauseDate: Date? + @Published var timeElapsed: Double = 0 + @Published var isRunning: Bool = false + private var timer: Timer.TimerPublisher? + private var timerCancellable: Cancellable? + + init(timeTotal: Double) { + self.timeTotal = timeTotal + } + + func start() { + self.isRunning = true + if startDate == nil { + startDate = Date() + } else if let pauseDate = pauseDate { + // Adjust start date based on the pause duration + let pauseDuration = Date().timeIntervalSince(pauseDate) + startDate = startDate?.addingTimeInterval(pauseDuration) + } + + self.timer = Timer.publish(every: 1, on: .main, in: .common) + self.timerCancellable = self.timer?.autoconnect().sink { [weak self] _ in + DispatchQueue.main.async { + if let self = self, let startTime = self.startDate { + let elapsed = Date().timeIntervalSince(startTime) + if elapsed < self.timeTotal { + self.timeElapsed = elapsed + } else { + self.timeElapsed = self.timeTotal + self.pause() + } + } + } + } + } + + func pause() { + self.isRunning = false + pauseDate = Date() + self.timerCancellable?.cancel() + self.timerCancellable = nil + self.timer = nil + } + + func resume() { + self.isRunning = true + start() + } + + func cancel() { + self.isRunning = false + self.timerCancellable?.cancel() + self.timerCancellable = nil + self.timer = nil + self.timeElapsed = 0 + self.startDate = nil + self.pauseDate = nil + } +}