diff --git a/SitStandTimer.xcodeproj/project.pbxproj b/SitStandTimer.xcodeproj/project.pbxproj index d24e9f1..b0a678d 100644 --- a/SitStandTimer.xcodeproj/project.pbxproj +++ b/SitStandTimer.xcodeproj/project.pbxproj @@ -6,6 +6,10 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + 5E500F652D2953CE00635A51 /* DynamicNotchKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5E500F642D2953CE00635A51 /* DynamicNotchKit */; }; +/* End PBXBuildFile section */ + /* Begin PBXContainerItemProxy section */ 5E3EDEC22D089F7100E83C77 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -24,7 +28,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 5E3EDEB02D089F7000E83C77 /* SitStandTimer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SitStandTimer.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 5E3EDEB02D089F7000E83C77 /* Stand.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stand.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5E3EDEC12D089F7100E83C77 /* SitStandTimerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SitStandTimerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 5E3EDECB2D089F7100E83C77 /* SitStandTimerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SitStandTimerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -52,6 +56,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5E500F652D2953CE00635A51 /* DynamicNotchKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -85,7 +90,7 @@ 5E3EDEB12D089F7000E83C77 /* Products */ = { isa = PBXGroup; children = ( - 5E3EDEB02D089F7000E83C77 /* SitStandTimer.app */, + 5E3EDEB02D089F7000E83C77 /* Stand.app */, 5E3EDEC12D089F7100E83C77 /* SitStandTimerTests.xctest */, 5E3EDECB2D089F7100E83C77 /* SitStandTimerUITests.xctest */, ); @@ -112,9 +117,10 @@ ); name = SitStandTimer; packageProductDependencies = ( + 5E500F642D2953CE00635A51 /* DynamicNotchKit */, ); productName = SitStandTimer; - productReference = 5E3EDEB02D089F7000E83C77 /* SitStandTimer.app */; + productReference = 5E3EDEB02D089F7000E83C77 /* Stand.app */; productType = "com.apple.product-type.application"; }; 5E3EDEC02D089F7100E83C77 /* SitStandTimerTests */ = { @@ -195,6 +201,9 @@ ); mainGroup = 5E3EDEA72D089F7000E83C77; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 5E500F632D2953CE00635A51 /* XCRemoteSwiftPackageReference "DynamicNotchKit" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 5E3EDEB12D089F7000E83C77 /* Products */; projectDirPath = ""; @@ -395,20 +404,22 @@ CODE_SIGN_ENTITLEMENTS = SitStandTimer/SitStandTimer.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"SitStandTimer/Preview Content\""; DEVELOPMENT_TEAM = UCN29PZL3V; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Stand; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = ash.SitStandTimer; - PRODUCT_NAME = "$(TARGET_NAME)"; + MARKETING_VERSION = 1.1; + PRODUCT_BUNDLE_IDENTIFIER = ash.Stand; + PRODUCT_NAME = Stand; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; @@ -422,20 +433,22 @@ CODE_SIGN_ENTITLEMENTS = SitStandTimer/SitStandTimer.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"SitStandTimer/Preview Content\""; DEVELOPMENT_TEAM = UCN29PZL3V; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Stand; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = ash.SitStandTimer; - PRODUCT_NAME = "$(TARGET_NAME)"; + MARKETING_VERSION = 1.1; + PRODUCT_BUNDLE_IDENTIFIER = ash.Stand; + PRODUCT_NAME = Stand; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; @@ -549,6 +562,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 5E500F632D2953CE00635A51 /* XCRemoteSwiftPackageReference "DynamicNotchKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/MrKai77/DynamicNotchKit"; + requirement = { + branch = main; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 5E500F642D2953CE00635A51 /* DynamicNotchKit */ = { + isa = XCSwiftPackageProductDependency; + package = 5E500F632D2953CE00635A51 /* XCRemoteSwiftPackageReference "DynamicNotchKit" */; + productName = DynamicNotchKit; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 5E3EDEA82D089F7000E83C77 /* Project object */; } diff --git a/SitStandTimer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SitStandTimer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..84caf25 --- /dev/null +++ b/SitStandTimer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "d5769d12c0b5b65dda654df0cbb42bbd4eda359a7b80d39b0bb60240f971ce19", + "pins" : [ + { + "identity" : "dynamicnotchkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MrKai77/DynamicNotchKit", + "state" : { + "branch" : "main", + "revision" : "0f2213c671585db887f53e86812134a7a6cc74e5" + } + } + ], + "version" : 3 +} diff --git a/SitStandTimer.xcodeproj/project.xcworkspace/xcuserdata/ash.xcuserdatad/UserInterfaceState.xcuserstate b/SitStandTimer.xcodeproj/project.xcworkspace/xcuserdata/ash.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..524daa6 Binary files /dev/null and b/SitStandTimer.xcodeproj/project.xcworkspace/xcuserdata/ash.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/SitStandTimer.xcodeproj/xcuserdata/ash.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/SitStandTimer.xcodeproj/xcuserdata/ash.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..27f526c --- /dev/null +++ b/SitStandTimer.xcodeproj/xcuserdata/ash.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Contents.json b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Contents.json index 3f00db4..b1295bd 100644 --- a/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,51 +1,61 @@ { "images" : [ { + "filename" : "Stand-16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { + "filename" : "Stand-32 1.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { + "filename" : "Stand-32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { + "filename" : "Stand-64.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { + "filename" : "Stand-128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { + "filename" : "Stand-256 1.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { + "filename" : "Stand-256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { + "filename" : "Stand-512 1.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { + "filename" : "Stand-512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { + "filename" : "Stand-1024.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" diff --git a/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-1024.png b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-1024.png new file mode 100644 index 0000000..7e8936a Binary files /dev/null and b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-1024.png differ diff --git a/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-128.png b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-128.png new file mode 100644 index 0000000..e21bccc Binary files /dev/null and b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-128.png differ diff --git a/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-16.png b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-16.png new file mode 100644 index 0000000..1535380 Binary files /dev/null and b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-16.png differ diff --git a/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-256 1.png b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-256 1.png new file mode 100644 index 0000000..5503b1d Binary files /dev/null and b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-256 1.png differ diff --git a/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-256.png b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-256.png new file mode 100644 index 0000000..5503b1d Binary files /dev/null and b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-256.png differ diff --git a/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-32 1.png b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-32 1.png new file mode 100644 index 0000000..2aff9db Binary files /dev/null and b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-32 1.png differ diff --git a/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-32.png b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-32.png new file mode 100644 index 0000000..2aff9db Binary files /dev/null and b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-32.png differ diff --git a/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-512 1.png b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-512 1.png new file mode 100644 index 0000000..57b9b6e Binary files /dev/null and b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-512 1.png differ diff --git a/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-512.png b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-512.png new file mode 100644 index 0000000..57b9b6e Binary files /dev/null and b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-512.png differ diff --git a/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-64.png b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-64.png new file mode 100644 index 0000000..74d3e0d Binary files /dev/null and b/SitStandTimer/Assets.xcassets/AppIcon.appiconset/Stand-64.png differ diff --git a/SitStandTimer/ContentView.swift b/SitStandTimer/ContentView.swift index c11daa6..1be60f4 100644 --- a/SitStandTimer/ContentView.swift +++ b/SitStandTimer/ContentView.swift @@ -6,19 +6,288 @@ // import SwiftUI +import DynamicNotchKit +struct LargeClockView: View { + let currentTime: Date + + var body: some View { + Text(timeString(from: currentTime)) + .font(.system(size: 96, design: .monospaced)) + .fontWeight(.light) + .foregroundColor(.gray) + } + + private func timeString(from date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" + return formatter.string(from: date) + } +} + +// Status text for idle mode +struct IdleStatusText: View { + @ObservedObject var timerManager: TimerManager + @State private var currentTime = Date() + let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + + private var statusText: String { + if timerManager.isRunning { + return "\(timerManager.currentInterval == .sitting ? "Sitting" : "Standing") - \(timeString(from: timerManager.remainingTime)) remaining" + } else { + return "Timer Paused" + } + } + + var body: some View { + Text(statusText) + .font(.title2) + .foregroundColor(.gray) + .padding(.top, 20) + .onReceive(timer) { input in + currentTime = input + } + } + + private func timeString(from timeInterval: TimeInterval) -> String { + let minutes = Int(timeInterval) / 60 + let seconds = Int(timeInterval) % 60 + return String(format: "%02d:%02d", minutes, seconds) + } +} + + +// Updated ContentView with idle mode struct ContentView: View { + @StateObject private var timerManager = TimerManager() + @AppStorage("sittingTime") private var sittingTime: Double = 30 + @AppStorage("standingTime") private var standingTime: Double = 10 + @State private var isFullScreen = false + @State private var currentTime = Date() + + let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + + var body: some View { + ZStack { + VisualEffectView(material: .sidebar, blendingMode: .behindWindow) + .edgesIgnoringSafeArea(.all) + + if isFullScreen { + IdleModeView(timerManager: timerManager, currentTime: currentTime) + } else { + NormalModeView(timerManager: timerManager, sittingTime: $sittingTime, standingTime: $standingTime) + } + } + .frame(width: isFullScreen ? nil : 450, height: isFullScreen ? nil : 400) + .onReceive(timer) { input in + currentTime = input + } + .onReceive(NotificationCenter.default.publisher(for: NSWindow.willEnterFullScreenNotification)) { _ in + isFullScreen = true + } + .onReceive(NotificationCenter.default.publisher(for: NSWindow.willExitFullScreenNotification)) { _ in + isFullScreen = false + } + } +} + +// Idle mode layout +struct IdleModeView: View { + @ObservedObject var timerManager: TimerManager + let currentTime: Date + var body: some View { VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") + // Status text at top + IdleStatusText(timerManager: timerManager) + + Spacer() + + // Timer controls in middle + VStack(spacing: 20) { + // Interval Display + HStack(spacing: 15) { + Image(systemName: timerManager.currentInterval == .sitting ? "figure.seated.side.left" : "figure.stand") + .font(.largeTitle) + .foregroundColor(timerManager.currentInterval == .sitting ? .indigo : .yellow) + Text(timerManager.currentInterval == .sitting ? "Sitting" : "Standing") + .font(.title) + .foregroundColor(timerManager.currentInterval == .sitting ? .indigo : .yellow) + } + + // Time Display + Text(timeString(from: timerManager.remainingTime)) + .font(.system(size: 48, design: .monospaced)) + .fontWeight(.bold) + + // Control Buttons + HStack(spacing: 15) { + Button(action: { + timerManager.resetTimer() + }) { + Image(systemName: "arrow.clockwise") + .foregroundColor(.white) + .frame(width: 10, height: 25) + } + + Button(action: { + if timerManager.isRunning { + timerManager.pauseTimer() + } else { + timerManager.resumeTimer() + } + }) { + Image(systemName: timerManager.isRunning ? "pause.fill" : "play.fill") + .foregroundColor(.white) + .frame(width: 20, height: 35) + } + + Button(action: { + timerManager.switchInterval() + }) { + Image(systemName: "repeat") + .foregroundColor(.white) + .frame(width: 10, height: 25) + } + } + } + + Spacer() + + // Clock at bottom left + HStack { + LargeClockView(currentTime: currentTime) + Spacer() + } + .padding(.bottom, 20) + .padding(.leading, 40) + } + .padding() + } + + private func timeString(from timeInterval: TimeInterval) -> String { + let minutes = Int(timeInterval) / 60 + let seconds = Int(timeInterval) % 60 + return String(format: "%02d:%02d", minutes, seconds) + } +} + + +// Normal mode layout (existing view) +struct NormalModeView: View { + @ObservedObject var timerManager: TimerManager + @Binding var sittingTime: Double + @Binding var standingTime: Double + + var body: some View { + VStack(spacing: 20) { + // Title + Text("Stand") + .font(.largeTitle) + .fontWeight(.medium) + + // Interval Display + HStack(spacing: 15) { + Image(systemName: timerManager.currentInterval == .sitting ? "figure.seated.side.left" : "figure.stand") + .font(.largeTitle) + .foregroundColor(timerManager.currentInterval == .sitting ? .indigo : .yellow) + Text(timerManager.currentInterval == .sitting ? "Sitting" : "Standing") + .font(.title) + .foregroundColor(timerManager.currentInterval == .sitting ? .indigo : .yellow) + } + + // Time Display + Text(timeString(from: timerManager.remainingTime)) + .font(.system(size: 48, design: .monospaced)) + .fontWeight(.bold) + + // Interval Configuration + HStack { + VStack { + Text("Sitting Time") + Slider(value: $sittingTime, in: 5...60, step: 5) + .onChange(of: sittingTime) { newValue in + timerManager.updateIntervalTime(type: .sitting, time: newValue) + } + .frame(width: 150) + Text("\(Int(sittingTime)) min") + } + + VStack { + Text("Standing Time") + Slider(value: $standingTime, in: 5...60, step: 5) + .onChange(of: standingTime) { newValue in + timerManager.updateIntervalTime(type: .standing, time: newValue) + } + .frame(width: 150) + Text("\(Int(standingTime)) min") + } + } + .padding() + + // Control Buttons + HStack(spacing: 15) { + Button(action: { + timerManager.resetTimer() + }) { + Image(systemName: "arrow.clockwise") + .foregroundColor(.white) + .frame(width: 10, height: 25) + } + + Button(action: { + if timerManager.isRunning { + timerManager.pauseTimer() + } else { + timerManager.resumeTimer() + } + }) { + Image(systemName: timerManager.isRunning ? "pause.fill" : "play.fill") + .foregroundColor(.white) + .frame(width: 20, height: 35) + } + + Button(action: { + timerManager.switchInterval() + }) { + Image(systemName: "repeat") + .foregroundColor(.white) + .frame(width: 10, height: 25) + } + } } .padding() + .onAppear { + timerManager.initializeWithStoredTimes(sitting: sittingTime, standing: standingTime) + } + } + + private func timeString(from timeInterval: TimeInterval) -> String { + let minutes = Int(timeInterval) / 60 + let seconds = Int(timeInterval) % 60 + return String(format: "%02d:%02d", minutes, seconds) + } +} + +struct VisualEffectView: NSViewRepresentable { + let material: NSVisualEffectView.Material + let blendingMode: NSVisualEffectView.BlendingMode + + func makeNSView(context: Context) -> NSVisualEffectView { + let visualEffectView = NSVisualEffectView() + visualEffectView.material = material + visualEffectView.blendingMode = blendingMode + visualEffectView.state = .active + return visualEffectView + } + + func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) { + visualEffectView.material = material + visualEffectView.blendingMode = blendingMode } } #Preview { ContentView() } + diff --git a/SitStandTimer/SitStandTimerApp.swift b/SitStandTimer/SitStandTimerApp.swift index a0e958f..3f16dca 100644 --- a/SitStandTimer/SitStandTimerApp.swift +++ b/SitStandTimer/SitStandTimerApp.swift @@ -6,6 +6,7 @@ // import SwiftUI +import DynamicNotchKit @main struct SitStandTimerApp: App { @@ -13,5 +14,7 @@ struct SitStandTimerApp: App { WindowGroup { ContentView() } + .windowStyle(HiddenTitleBarWindowStyle()) +// .windowResizability(.contentSize) } } diff --git a/SitStandTimer/TimerManager.swift b/SitStandTimer/TimerManager.swift new file mode 100644 index 0000000..31b4dcb --- /dev/null +++ b/SitStandTimer/TimerManager.swift @@ -0,0 +1,112 @@ +// +// TimerManager.swift +// SitStandTimer +// +// Recreated by ash on 1/4/25. +// + +import Foundation +import DynamicNotchKit +import SwiftUI +import AppKit + +enum IntervalType { + case sitting + case standing +} + +class TimerManager: ObservableObject { + @Published var currentInterval: IntervalType = .sitting + @Published var remainingTime: TimeInterval = 0 + @Published var isRunning: Bool = false + + private var timer: Timer? + private var sittingTime: TimeInterval = 30 * 60 // 30 minutes in seconds + private var standingTime: TimeInterval = 10 * 60 // 10 minutes in seconds + + // Initialize the pauseNotch as a property + private lazy var pauseNotch: DynamicNotchInfo = { + DynamicNotchInfo( + icon: Image(systemName: "pause.circle.fill"), + title: "Timer paused", + description: "Remember to press play!" + ) + }() + + // System sound for interval changes + private let switchSound = NSSound(named: "Funk") + + func initializeWithStoredTimes(sitting: Double, standing: Double) { + sittingTime = sitting * 60 + standingTime = standing * 60 + remainingTime = sittingTime + } + + func updateIntervalTime(type: IntervalType, time: Double) { + let timeInSeconds = time * 60 + switch type { + case .sitting: + sittingTime = timeInSeconds + if currentInterval == .sitting && !isRunning { + remainingTime = timeInSeconds + } + case .standing: + standingTime = timeInSeconds + if currentInterval == .standing && !isRunning { + remainingTime = timeInSeconds + } + } + } + + func resumeTimer() { + guard !isRunning else { return } + + pauseNotch.hide() + isRunning = true + + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + guard let self = self else { return } + + if self.remainingTime > 0 { + self.remainingTime -= 1 + } else { + self.switchInterval() + } + } + } + + func pauseTimer() { + isRunning = false + timer?.invalidate() + timer = nil + + // Show the pause notification without a duration + pauseNotch.show() + } + + func resetTimer() { + pauseTimer() + currentInterval = .sitting + remainingTime = sittingTime + } + + func switchInterval() { + pauseTimer() + + currentInterval = currentInterval == .sitting ? .standing : .sitting + remainingTime = currentInterval == .sitting ? sittingTime : standingTime + + // Play the switch sound + switchSound?.play() + + // Show dynamic notch notification + let notch = DynamicNotchInfo( + icon: Image(systemName: currentInterval == .sitting ? "figure.seated.side.left" : "figure.stand"), + title: "Time to \(currentInterval == .sitting ? "Sit" : "Stand")!", + description: "Switch your position to \(currentInterval == .sitting ? "sitting" : "standing")" + ) + notch.show(for: 3) + + resumeTimer() + } +}