### Koin Dependency Injection Setup Source: https://context7.com/adrcotfas/goodtime/llms.txt Sets up Koin for multiplatform dependency injection, including core modules, platform-specific modules, and various feature modules like data, timer management, and billing. It also shows how to retrieve singletons using KoinComponent. ```kotlin import org.koin.core.context.startKoin import org.koin.core.component.KoinComponent import org.koin.core.inject import org.koin.core.qualifier.named import kotlinx.coroutines.CoroutineScope // In Application.onCreate() (Android) or app init (iOS): // startKoin { // modules( // coroutineScopeModule, // MAIN_SCOPE, IO_SCOPE, WORKER_SCOPE CoroutineScopes // coreModule, // SettingsRepository, LocalDataRepository, TimeProvider, // // FinishedSessionsHandler, ReminderManager // platformModule, // DataStore path, DatabaseBuilder, platform services // localDataModule, // ProductivityDatabase construction // timerManagerModule, // TimerManager + TimerForegroundMonitor singleton // viewModelModule, // all ViewModels // backupModule, // BackupFileManager // billingModule, // PurchaseManager / RevenueCat // ) // } // Retrieve a singleton anywhere via KoinComponent: // class MyComponent : KoinComponent { // private val timerManager: TimerManager by inject() // private val settingsRepo: SettingsRepository by inject() // private val logger: Logger by inject { parametersOf("MyComponent") } // } // Coroutine scopes available as named qualifiers: // named(MAIN_SCOPE) → Dispatchers.Main // named(IO_SCOPE) → Dispatchers.IO // named(WORKER_SCOPE) → Dispatchers.Default // val ioScope: CoroutineScope by inject(named(IO_SCOPE)) ``` -------------------------------- ### SettingsRepository: Toggle Auto-Start Source: https://context7.com/adrcotfas/goodtime/llms.txt Enable or disable the automatic start of work or break timers. ```kotlin settingsRepo.setAutoStartWork(enabled = true) settingsRepo.setAutoStartBreak(enabled = false) ``` -------------------------------- ### SettingsRepository - Auto-Start Behavior Source: https://context7.com/adrcotfas/goodtime/llms.txt Toggles the automatic start of work or break timers. ```APIDOC ## SettingsRepository - Auto-Start Behavior ### Description Toggles the automatic start of work or break timers. ### Enable Auto-Start Work ```kotlin settingsRepo.setAutoStartWork(enabled = true) ``` ### Disable Auto-Start Break ```kotlin settingsRepo.setAutoStartBreak(enabled = false) ``` ``` -------------------------------- ### Set Live Activity Label in Host App Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Set the label for a Live Activity using TimerStateManager before starting it. This ensures the extension can correctly associate completed sessions with the appropriate label. ```kotlin is Event.Start -> { log.v { "IosLiveActivityListener: Starting Live Activity (isFocus=${event.isFocus}, countdown=${event.endTime != 0L})" } // ... existing code for duration, localizedStrings, etc. ... // NEW: Set label in extension before starting Live Activity // This is needed so the extension can store completed sessions with the correct label TimerStateManager.shared.setLabel( name: event.labelName, color: labelColorHex ) // Then start the Live Activity (existing code) liveActivityBridge.start( isFocus = event.isFocus, isCountdown = isCountdown, durationSeconds = durationSeconds, labelName = event.labelName, isDefaultLabel = event.isDefaultLabel, labelColorHex = labelColorHex, localizedStrings = localizedStrings, ) } ``` -------------------------------- ### Before: Dual Source Architecture Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Illustrates the previous architecture where both the host app and extension had separate data stores, leading to potential state drift. ```text Host App Extension │ │ ▼ ▼ DataStore UserDefaults │ │ └── sync ─────────┘ ← Potential for drift ``` -------------------------------- ### Persist Timer State on Event Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Listens for timer events and persists the timer state using UserDefaultsStorage. Handles start, pause, and finished events. ```kotlin class IosTimerStatePersistenceListener( private val userDefaultsStorage: IosUserDefaultsTimerStorage, private val localDataRepo: LocalDataRepository, // For direct Room writes when app is alive private val timeProvider: TimeProvider, private val coroutineScope: CoroutineScope, private val log: Logger, private val getCurrentLabel: () -> Label, // NEW: way to get current label ) : EventListener { override fun onEvent(event: Event) { when (event) { is Event.Start -> { persistTimerState(event.domainTimerData, event.label) } is Event.Pause -> { persistTimerState(event.domainTimerData, event.label) } is Event.Finished -> { // Timer completed naturally - store session if >= 1 min event.domainTimerData?.let { data -> event.label?.let { label -> ``` -------------------------------- ### TimerProfile Defaults and Customization Source: https://context7.com/adrcotfas/goodtime/llms.txt Illustrates creating and using TimerProfile objects for different timer modes. Shows how to define default, custom, and count-up flow profiles, and calculate session end times and durations. ```kotlin // Default Pomodoro profile (25/5, no long break) val defaultProfile = TimerProfile.default() // TimerProfile(isCountdown=true, workDuration=25, breakDuration=5, // isLongBreakEnabled=false, longBreakDuration=15, // sessionsBeforeLongBreak=4, workBreakRatio=3) // Custom profile: 50-minute work, 10-minute break, long break after 3 sessions val customProfile = TimerProfile( name = "Deep Work", isCountdown = true, workDuration = 50, isBreakEnabled = true, breakDuration = 10, isLongBreakEnabled = true, longBreakDuration = 20, sessionsBeforeLongBreak = 3, ) // Count-up Flow profile: break accrues at 1 min per 3 min of work val flowProfile = TimerProfile( name = "Flow", isCountdown = false, workBreakRatio = 3, isBreakEnabled = true, breakDuration = 0, // ignored; computed from budget ) // Compute absolute end time for a FOCUS session starting now val endTimeMillis: Long = customProfile.endTime( timerType = TimerType.FOCUS, elapsedRealTime = SystemClock.elapsedRealtime(), ) // = elapsedRealTime + 50 * 60 * 1000 // Get the duration in minutes for a given session type val workMins: Int = customProfile.duration(TimerType.FOCUS) // 50 val breakMins: Int = customProfile.duration(TimerType.BREAK) // 10 val longBreakMins: Int = customProfile.duration(TimerType.LONG_BREAK) // 20 ``` -------------------------------- ### Event.kt: Add Full Data for UserDefaults Storage Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Extends the `Event.Start` data class to include all necessary data fields for UserDefaults storage. This ensures that when an event is triggered, it carries all the required information. ```kotlin sealed class Event { data class Start( val isFocus: Boolean = true, val autoStarted: Boolean = false, val endTime: Long = 0, val labelName: String = Label.DEFAULT_LABEL_NAME, val isDefaultLabel: Boolean = true, val labelColorIndex: Int = Label.DEFAULT_LABEL_COLOR_INDEX, val isBreakEnabled: Boolean = true, val isCountdown: Boolean = true, val runtimeState: TimerRuntimeState = TimerRuntimeState(), // NEW: Add full data for UserDefaults storage ``` -------------------------------- ### After: Single Source Architecture Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Depicts the simplified architecture using UserDefaults as the single source of truth, ensuring consistency between the host app and extension. ```text Host App ──────────── Extension │ │ └── UserDefaults ────┘ ← One truth ``` -------------------------------- ### Initialize ReminderManager Observation Source: https://context7.com/adrcotfas/goodtime/llms.txt Starts observing changes in reminder settings. This function suspends and runs a collection loop for the lifetime of the provided coroutine scope. Changes automatically trigger a reschedule. ```kotlin val reminderManager: ReminderManager by inject() // In application startup / service: begin observing settings changes // (suspends; collect loop runs for the lifetime of the coroutine scope) coroutineScope.launch { reminderManager.init() } ``` -------------------------------- ### Architecture Overview Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Diagram illustrating the proposed architecture where shared UserDefaults acts as the single source of truth for Live Activity state between the host app and its extension. ```text ┌─────────────────────────────────────────────────────────────────────────────┐ │ Shared State (UserDefaults + App Group) │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ goodtime_timer_state (DomainTimerData equivalent) │ │ │ │ goodtime_completed_sessions (array of finished sessions) │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ ▲ ▲ │ │ ┌─────────┴────────────┴─────────┐ │ │ ┌───────────────────▼───────────┐ ┌───────────────▼────────────────────┐ │ Host App (Kotlin) │ │ Extension (Swift) │ │ ───────────────────────── │ │ ────────────────────────────── │ │ • Reads on launch │ │ • Reads on button tap │ │ • Writes on state change │ │ • Writes on button tap │ │ • Migrates sessions → Room │ │ • Can schedule UN notifications │ │ • Single source of truth │ │ • Single source of truth │ └───────────────────────────────┘ └──────────────────────────────────────┘ ``` -------------------------------- ### TimerManager API Source: https://context7.com/adrcotfas/goodtime/llms.txt The TimerManager is the core state machine for the timer. It exposes timer state via a StateFlow and provides methods to control timer operations like starting, pausing, skipping, and finishing sessions. ```APIDOC ## TimerManager ### Description The `TimerManager` is the single source of truth for all timer runtime state. It exposes a `StateFlow` and provides methods to start, toggle, skip, finish, and reset the timer. It reacts to label changes, manages long-break streaks, and accumulates the break budget for count-up profiles. ### Usage ```kotlin // Koin wires TimerManager; interact with it via the TimerManagerModule single val timerManager: TimerManager by inject() // Observe the live timer state timerManager.timerData .onEach { data -> val stateLabel = when (data.state) { TimerState.RESET -> "Ready" TimerState.RUNNING -> "Running" TimerState.PAUSED -> "Paused" TimerState.FINISHED -> "Finished" } val sessionType = data.type // FOCUS | BREAK | LONG_BREAK val remaining = data.getBaseTime(timeProvider) // millis remaining/elapsed println("[$stateLabel] $sessionType – ${formatMillisToTime(remaining)}") } .launchIn(coroutineScope) // Start a FOCUS session timerManager.start(timerType = TimerType.FOCUS, autoStarted = false) // Pause / resume (toggle) timerManager.toggle() // Add one minute to a running countdown timerManager.addOneMinute() // Skip the current session (saves it as interrupted, increments streak) timerManager.skip() // Mark current session done and proceed to the next timerManager.next(actionType = FinishActionType.MANUAL_NEXT) // Called automatically when the countdown reaches zero timerManager.finish(actionType = FinishActionType.AUTO) // Reset (stop) without saving if session was < 1 minute timerManager.reset(actionType = FinishActionType.MANUAL_RESET) // Update the just-finished session to include idle time and/or notes timerManager.updateFinishedSession(updateDuration = true, notes = "Deep work on feature X") // Lifecycle hooks for foreground/background transitions timerManager.onSendToBackground() timerManager.onBringToForeground() ``` ### Methods - **`start(timerType: TimerType, autoStarted: Boolean)`**: Starts a new timer session. - **`toggle()`**: Pauses or resumes the current timer session. - **`addOneMinute()`**: Adds one minute to a running countdown timer. - **`skip()`**: Skips the current session, saving it as interrupted and incrementing the streak. - **`next(actionType: FinishActionType)`**: Marks the current session as done and proceeds to the next session. - **`finish(actionType: FinishActionType)`**: Marks the current session as finished, typically called automatically when the countdown reaches zero. - **`reset(actionType: FinishActionType)`**: Resets the timer, stopping it without saving if the session was less than one minute. - **`updateFinishedSession(updateDuration: Boolean, notes: String?)`**: Updates the details of a just-finished session, optionally including duration and notes. - **`onSendToBackground()`**: Lifecycle hook for when the app goes into the background. - **`onBringToForeground()`**: Lifecycle hook for when the app is brought to the foreground. ### State Observation - **`timerData: StateFlow`**: Exposes the current timer state as a `StateFlow`. ``` -------------------------------- ### Handle Notification Actions in iOS Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Implements UNUserNotificationCenterDelegate to process user actions on notifications, such as starting the next session. It reads timer state, stores completed sessions, and writes pending actions to UserDefaults. ```kotlin class IosNotificationDelegate( private val userDefaultsStorage: IosUserDefaultsTimerStorage, private val localDataRepo: LocalDataRepository, private val timeProvider: TimeProvider, private val log: Logger, ) : UNUserNotificationCenterDelegate { private var onStartNextSession: (() -> Unit)? = null fun init(onStartNextSession: () -> Unit) { this.onStartNextSession = onStartNextSession } override fun userNotificationCenter( center: UNUserNotificationCenter, didReceiveNotificationResponse: UNNotificationResponse, withCompletionHandler: () -> Unit, ) { val actionIdentifier = didReceiveNotificationResponse.actionIdentifier log.i { "Received notification action: $actionIdentifier" } when (actionIdentifier) { ACTION_START_NEXT -> { handleStartNextAction() } "com.apple.UNNotificationDefaultActionIdentifier" -> { log.i { "User tapped notification body (default action)" } } else -> { log.w { "Unknown action identifier: $actionIdentifier" } } } withCompletionHandler() } private fun handleStartNextAction() { // 1. Read current timer state from UserDefaults val currentState = userDefaultsStorage.readTimerState() // 2. If timer exists and is completed, store the session currentState?.let { state -> if (isTimerCompleted(state)) { maybeStoreCompletedSession(state) } } // 3. Write pending action to UserDefaults val pendingAction = if (currentState?.timerType == "focus") { "start_break" } else { "start_focus" } userDefaultsStorage.writePendingAction(pendingAction) log.i { "Wrote pending action to UserDefaults: $pendingAction" } // 4. Set callback for when app becomes active // (This will be called by TimerStateRestoration instead) } private fun isTimerCompleted(state: TimerState): Boolean { if (!state.isCountdown || state.state != "running") { return false } val now = CACurrentMediaTime() * 1000 return now >= state.endTime } private fun maybeStoreCompletedSession(state: TimerState) { val duration = state.endTime - state.startTime if (duration >= 60_000) { // 1 minute // Convert to Session.kt format val durationMinutes = (duration / 60000.0).toInt() val interruptionsMinutes = (state.timeSpentPaused / 60000.0).toInt() val isWork = state.timerType == "focus" // NOTE: We don't have the label name here since we removed it from TimerState. // The notification action is triggered AFTER the timer completes, so we should // use a default label or retrieve it from somewhere else. // For now, use default label name. val session = CompletedSession( timestamp = state.startTime, duration = durationMinutes, interruptions = interruptionsMinutes, label = Label.DEFAULT_LABEL_NAME, isWork = isWork ) userDefaultsStorage.writeCompletedSession(session) } } companion object { private const val ACTION_START_NEXT = "START_NEXT" } } ``` -------------------------------- ### SettingsRepository: Configure Reminder Settings Source: https://context7.com/adrcotfas/goodtime/llms.txt Set up productivity reminders, specifying the days of the week and the time of day. ```kotlin settingsRepo.updateReminderSettings { current -> current.copy( days = listOf(1, 3), // ISO day numbers secondOfDay = LocalTime(9, 0).toSecondOfDay(), ) } ``` -------------------------------- ### SettingsRepository: Observe All Settings Source: https://context7.com/adrcotfas/goodtime/llms.txt Observe the complete application settings object as a Flow. Useful for reacting to any change in user preferences. ```kotlin val settingsRepo: SettingsRepository by inject() // Observe the full settings object settingsRepo.settings.collect { println("isPro=${settings.isPro}, theme=${settings.uiSettings.themePreference}") println("autoStartFocus=${settings.autoStartFocus}") println("activeLabel=${settings.labelName}") } ``` -------------------------------- ### SettingsRepository: Set Backup Settings Source: https://context7.com/adrcotfas/goodtime/llms.txt Configure backup preferences, including enabling auto-backup, cloud auto-backup, and specifying the backup path. ```kotlin settingsRepo.setBackupSettings( BackupSettings( autoBackupEnabled = true, cloudAutoBackupEnabled = false, path = "/storage/emulated/0/Backups/Goodtime", ) ) ``` -------------------------------- ### Backup Database with BackupFileManager Source: https://context7.com/adrcotfas/goodtime/llms.txt Initiates a full SQLite database backup. The user selects the destination via a platform file picker. Handles success, cancellation, and failure. ```kotlin val backupManager: BackupFileManager by inject() // --- Backup (copies DB to a user-chosen destination via platform file picker) --- backupManager.backup { result -> when (result) { BackupPromptResult.SUCCESS -> showSnackbar("Backup saved!") BackupPromptResult.CANCELLED -> { /* user cancelled */ } BackupPromptResult.FAILED -> showSnackbar("Backup failed") } } ``` -------------------------------- ### SettingsRepository - Observe All Settings Source: https://context7.com/adrcotfas/goodtime/llms.txt Observes the full settings object as a Flow. ```APIDOC ## SettingsRepository - Observe All Settings ### Description Observes the full settings object as a Flow. ### Observe Settings ```kotlin settingsRepo.settings.collect { settings -> println("isPro=${settings.isPro}, theme=${settings.uiSettings.themePreference}") println("autoStartFocus=${settings.autoStartFocus}") println("activeLabel=${settings.labelName}") } ``` ``` -------------------------------- ### SettingsRepository: Activate Label Source: https://context7.com/adrcotfas/goodtime/llms.txt Switch the currently active label, either by its name or by activating the default label. ```kotlin settingsRepo.activateLabelWithName("Deep Work") settingsRepo.activateDefaultLabel() ``` -------------------------------- ### iOS Live Activity Architecture Overview Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Visual representation of the host app's architecture, including TimerManager, EventListeners, ActivityKit, UNUserNotification, and their interactions with the Live Activity extension. ```text ┌─────────────────────────────────────────────────────────────────────────────┐ │ Host App (Kotlin) │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ TimerManager │ │ │ │ ────────────────────── │ │ │ │ • Core timer logic │ │ │ │ • Session tracking & statistics (Room) │ │ │ │ • State restoration on app launch │ │ │ │ • Device reboot handling │ │ │ │ • Expired timer detection │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ EventListeners │ │ │ │ ────────────────────── │ │ │ │ • IosTimerStatePersistenceListener (saves to DataStore) │ │ │ │ • IosLiveActivityListener (updates Live Activity via Bridge) │ │ │ │ • IosNotificationHandler (schedules UNUserNotification) │ │ │ └──────────────────────────────────────────────────────────────────────┘ │ │ │ │ └────────────────────────────────────┼──────────────────────────────────────────┘ │ ┌────────────────┴────────────────┐ │ │ ┌──────────▼──────────┐ ┌───────────▼──────────┐ │ ActivityKit │ │ UNUserNotification │ │ (Live Activities) │ │ (Timer finished) │ └──────────────────────┘ └──────────────────────┘ │ ┌──────────▼──────────┐ │ GoodtimeInProgress │ │ (Extension) │ │ ─────────────────── │ │ • Displays UI │ │ • Button taps → │ │ NotificationCenter│ └──────────────────────┘ │ ┌──────────▼──────────┐ │ LiveActivityIntent │ │ Handler │ │ (Host app only) │ └─────────────────────┘ ``` -------------------------------- ### SettingsRepository - Backup Settings Source: https://context7.com/adrcotfas/goodtime/llms.txt Manages backup preferences, including paths and cloud sync. ```APIDOC ## SettingsRepository - Backup Settings ### Description Manages backup preferences, including paths and cloud sync. ### Set Backup Settings ```kotlin settingsRepo.setBackupSettings( BackupSettings( autoBackupEnabled = true, cloudAutoBackupEnabled = false, path = "/storage/emulated/0/Backups/Goodtime", ) ) ``` ``` -------------------------------- ### SettingsRepository - Activate Label Source: https://context7.com/adrcotfas/goodtime/llms.txt Switches the currently active label for focus sessions. ```APIDOC ## SettingsRepository - Activate Label ### Description Switches the currently active label for focus sessions. ### Activate Label by Name ```kotlin settingsRepo.activateLabelWithName("Deep Work") ``` ### Activate Default Label ```kotlin settingsRepo.activateDefaultLabel() ``` ``` -------------------------------- ### Export Data to CSV with BackupFileManager Source: https://context7.com/adrcotfas/goodtime/llms.txt Exports session data to a CSV file. The output includes columns for end time, duration, interruptions, label, notes, is_break, and is_archived. Displays a snackbar on success. ```kotlin // --- CSV export --- // Output columns: end,duration,interruptions,label,notes,is_break,is_archived // Example row: 2025-04-01T14:30:00Z,25,2,Work,Finished chapter,false,falseackupManager.exportCsv { result -> if (result == BackupPromptResult.SUCCESS) showSnackbar("CSV exported") } ``` -------------------------------- ### Restore Database from File Picker with BackupFileManager Source: https://context7.com/adrcotfas/goodtime/llms.txt Restores the SQLite database from a file selected by the user via the platform file picker. After a successful restore, the Koin DI graph is reinitialized and TimerManager is restarted. The UI should navigate to the main screen. ```kotlin // --- Restore from platform file picker --- backupManager.restore { result -> // On SUCCESS, TimerManager is restarted and UI should navigate to MainDest if (result == BackupPromptResult.SUCCESS) navigateToMain() } ``` -------------------------------- ### SettingsRepository - Update UI Settings Source: https://context7.com/adrcotfas/goodtime/llms.txt Updates UI-related settings like theme, fullscreen, and Do Not Disturb. ```APIDOC ## SettingsRepository - Update UI Settings ### Description Updates UI-related settings like theme, fullscreen, and Do Not Disturb. ### Update UI Settings ```kotlin settingsRepo.updateUiSettings { current -> current.copy( themePreference = ThemePreference.DARK, fullscreenMode = true, dndDuringWork = true, trueBlackMode = false, ) } ``` ``` -------------------------------- ### Export Data to JSON with BackupFileManager Source: https://context7.com/adrcotfas/goodtime/llms.txt Exports session data to a JSON file. The output is an array of objects, each containing keys for end time, duration, interruptions, label, notes, is_break, and archived status. Shows a snackbar on success. ```kotlin // --- JSON export --- // Output: array of objects with keys: end, duration, interruptions, label, notes, is_break, archived backupManager.exportJson { result -> if (result == BackupPromptResult.SUCCESS) showSnackbar("JSON exported") } ``` -------------------------------- ### Passing DomainTimerData and Label to Events in TimerManager Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Demonstrates how to pass `DomainTimerData` and `Label` to `Event.Start`, `Event.Pause`, and `Event.Finished` within the `TimerManager.kt` file. This ensures that these events carry the necessary data for persistence. ```kotlin // In Event.Start Event.Start( isFocus = ..., // ... existing fields ... domainTimerData = _timerData.value, label = _timerData.value.label.label, ) // In Event.Pause Event.Pause( runtimeState = updatedRuntimeState, domainTimerData = _timerData.value, label = _timerData.value.label.label, ) // In Event.Finished Event.Finished( type = type, autostartNextSession = autoStart, domainTimerData = _timerData.value, label = _timerData.value.label.label, ) // Event.Reset remains an object (no data) ``` -------------------------------- ### SettingsRepository - Update Reminder Settings Source: https://context7.com/adrcotfas/goodtime/llms.txt Configures the productivity reminder, including days and time. ```APIDOC ## SettingsRepository - Update Reminder Settings ### Description Configures the productivity reminder, including days and time. ### Update Reminder Settings ```kotlin settingsRepo.updateReminderSettings { current -> current.copy( days = listOf(1, 3), // ISO day numbers secondOfDay = LocalTime(9, 0).toSecondOfDay(), ) } ``` ``` -------------------------------- ### SettingsRepository: Update UI Settings Source: https://context7.com/adrcotfas/goodtime/llms.txt Update various UI-related preferences such as theme, fullscreen mode, and Do Not Disturb settings. Uses a copy-on-write pattern. ```kotlin settingsRepo.updateUiSettings { current -> current.copy( themePreference = ThemePreference.DARK, fullscreenMode = true, dndDuringWork = true, trueBlackMode = false, ) } ``` -------------------------------- ### SettingsRepository: Update Statistics Settings Source: https://context7.com/adrcotfas/goodtime/llms.txt Customize how statistics are displayed, including the overview type, duration, and whether to show breaks or archived data. ```kotlin settingsRepo.updateStatisticsSettings { current -> current.copy( overviewType = OverviewType.TIME, overviewDurationType = OverviewDurationType.THIS_WEEK, showBreaks = true, showArchived = false, ) } ``` -------------------------------- ### Generate Standardized Backup File Names with BackupFileManager Source: https://context7.com/adrcotfas/goodtime/llms.txt Generates standardized file names for database backups and general backup files, incorporating the current date and time for uniqueness. ```kotlin // --- Generate standardised file names --- val dbFileName = backupManager.generateDbBackupFileName() // e.g. "goodtime-2025-04-01-14-30.db" val baseFileName = backupManager.generateBackupFileName() // e.g. "goodtime-2025-04-01-14-30" ``` -------------------------------- ### SettingsRepository - Notification Alerts Source: https://context7.com/adrcotfas/goodtime/llms.txt Configures various notification alert settings. ```APIDOC ## SettingsRepository - Notification Alerts ### Description Configures various notification alert settings. ### Set Vibration Strength ```kotlin settingsRepo.setVibrationStrength(strength = 3) // 0 = off, 1-5 scale ``` ### Enable Torch ```kotlin settingsRepo.setEnableTorch(enabled = true) ``` ### Enable Flash Screen ```kotlin settingsRepo.setEnableFlashScreen(enabled = false) ``` ### Enable Insistent Notification ```kotlin settingsRepo.setInsistentNotification(enabled = false) ``` ### Set Work Finished Sound ```kotlin settingsRepo.setWorkFinishedSound(sound = null) // null = default ``` ### Set Break Finished Sound ```kotlin settingsRepo.setBreakFinishedSound(sound = "file://sounds/bell.mp3") ``` ``` -------------------------------- ### Handle Notification Action When Host App is Alive Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md This Kotlin code snippet demonstrates how a notification action is handled when the host app is running. It invokes a callback that requires the host app's process to be active. ```kotlin private fun handleNotificationAction(actionId: String) { when (actionId) { ACTION_START_NEXT -> { coroutineScope.launch { onStartNextSession?.invoke() // ← Host app must be alive } } } } ``` -------------------------------- ### SettingsRepository: Notification Alert Settings Source: https://context7.com/adrcotfas/goodtime/llms.txt Configure various notification alert behaviors, including vibration strength, torch/flash screen usage, insistence, and custom sounds for work/break completion. ```kotlin settingsRepo.setVibrationStrength(strength = 3) // 0 = off, 1-5 scale settingsRepo.setEnableTorch(enabled = true) settingsRepo.setEnableFlashScreen(enabled = false) settingsRepo.setInsistentNotification(enabled = false) settingsRepo.setWorkFinishedSound(sound = null) // null = default settingsRepo.setBreakFinishedSound(sound = "file://sounds/bell.mp3") ``` -------------------------------- ### GoodtimeApp Composable Entry Point Source: https://context7.com/adrcotfas/goodtime/llms.txt The main Composable function for the Goodtime application, responsible for initializing Compose Navigation, snackbar system, theme management, and lifecycle tracking. Navigation destinations are type-safe Kotlin objects. ```kotlin import androidx.compose.runtime.Composable // Android – called from MainActivity @Composable fun GoodtimeApp( platformContext: PlatformContext, mainViewModel: MainViewModel, onUpdateClicked: (() -> Unit)? = null, // non-null only on Google Play flavour ) // Navigation destinations available at runtime: // MainDest – main timer screen // OnboardingDest – first-launch onboarding flow // StatsDest – statistics screen // SettingsDest – settings overview // LabelsDest – label management // AddEditLabelDest – create/edit a label (carries labelName: String?) // ArchivedLabelsDest – archived labels // TimerDurationsDest – timer profile / duration settings // UserInterfaceDest – UI/theme settings // NotificationSettingsDest – notification & alert settings // BackupDest – backup & restore // AboutDest – about screen // LicensesDest, AcknowledgementsDest // ProDest – Pro upgrade screen // Example: navigate to statistics from a ViewModel or Composable // navController.navigate(StatsDest) // Example: navigate to edit an existing label // navController.navigate(AddEditLabelDest(name = "Work")) // Theme is controlled by AppSettings.uiSettings.themePreference (SYSTEM / LIGHT / DARK) // Dynamic color (Material You) is controlled by AppSettings.uiSettings.useDynamicColor ``` -------------------------------- ### LocalDataRepository - Timer Profiles Source: https://context7.com/adrcotfas/goodtime/llms.txt APIs for managing timer profiles. ```APIDOC ## LocalDataRepository - Timer Profiles ### Description APIs for managing timer profiles. ### Insert Timer Profile ```kotlin repo.insertTimerProfile(customProfile) ``` ### Insert Timer Profile and Set as Default ```kotlin repo.insertTimerProfileAndSetDefault(customProfile) ``` ### Delete Timer Profile ```kotlin repo.deleteTimerProfile(name = "Deep Work") ``` ``` -------------------------------- ### SettingsRepository - Statistics Display Source: https://context7.com/adrcotfas/goodtime/llms.txt Configures how statistics are displayed in the app. ```APIDOC ## SettingsRepository - Statistics Display ### Description Configures how statistics are displayed in the app. ### Update Statistics Settings ```kotlin settingsRepo.updateStatisticsSettings { current -> current.copy( overviewType = OverviewType.TIME, overviewDurationType = OverviewDurationType.THIS_WEEK, showBreaks = true, showArchived = false, ) } ``` ``` -------------------------------- ### LocalDataRepository: Timer Profile Operations Source: https://context7.com/adrcotfas/goodtime/llms.txt Manage timer profiles, including insertion and deletion. Can insert a profile and set it as the default. ```kotlin repo.insertTimerProfile(customProfile) repo.insertTimerProfileAndSetDefault(customProfile) // also sets as active profile repo.deleteTimerProfile(name = "Deep Work") ``` -------------------------------- ### SettingsRepository - Update Timer Style Source: https://context7.com/adrcotfas/goodtime/llms.txt Updates the visual style of the timer, including font size and display options. ```APIDOC ## SettingsRepository - Update Timer Style ### Description Updates the visual style of the timer, including font size and display options. ### Update Timer Style ```kotlin settingsRepo.updateTimerStyle { current -> current.copy( colorIndex = 5, fontWeight = 300, minutesOnly = false, showStreak = true, showBreakBudget = true, ) } ``` ``` -------------------------------- ### Restore Database from Specific File Path with BackupFileManager Source: https://context7.com/adrcotfas/goodtime/llms.txt Restores the SQLite database from a specified file path. This is useful for restoring after a cloud download. On success, navigates the UI to the main screen. ```kotlin // --- Restore from a known file path (used after cloud download) --- val restoreResult: BackupPromptResult = backupManager.restoreFromFile(filePath = "/tmp/goodtime-backup-2025-04-01.db") if (restoreResult == BackupPromptResult.SUCCESS) navigateToMain() ``` -------------------------------- ### Time Source Implementation in Swift Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Calculates elapsed real-time and wall-clock time in milliseconds using Swift's `CACurrentMediaTime` and `Date`. ```swift // Swift - Extension let elapsedRealtime = CACurrentMediaTime() * 1000 // milliseconds let wallClock = Date().timeIntervalSince1970 * 1000 // milliseconds ``` -------------------------------- ### DomainTimerData Fields and Usage Source: https://context7.com/adrcotfas/goodtime/llms.txt Defines the immutable snapshot of timer data, including active label, runtime state, and break data. Shows how to access base time, break budget, session type, and sessions before a long break. ```kotlin // DomainTimerData fields data class DomainTimerData( val isReady: Boolean, // false until label/profile is loaded val label: DomainLabel, // active label + timer profile val longBreakData: LongBreakData, val breakBudgetData: BreakBudgetData, val runtime: TimerRuntimeState, // all mutable clock values val completedMinutes: Long, // updated when a session finishes ) // Read the remaining/elapsed display time val baseTime: Long = timerData.getBaseTime(timeProvider) // e.g. for a paused countdown: baseTime = timeAtPause (millis remaining) // for a running count-up: baseTime = elapsedRealtime - startTime - timeSpentPaused // Compute the break budget for a count-up profile val budget: Duration = timerData.getBreakBudget(timeProvider.elapsedRealtime()) // During RUNNING focus: budget grows at rate 1/workBreakRatio per second of work // Check session type val isCountdown: Boolean = timerData.isCurrentSessionCountdown() // true for countdown profiles, or when type != FOCUS (breaks are always countdown) // Determine sessions before next long break val sessionsLeft: Int = timerData.inUseSessionsBeforeLongBreak() // 0 if long break is disabled or profile is count-up // TimerState extension properties timerData.state.isRunning // TimerState.RUNNING timerData.state.isPaused // TimerState.PAUSED timerData.state.isActive // isRunning || isPaused timerData.state.isFinished // TimerState.FINISHED timerData.state.isReset // TimerState.RESET ``` -------------------------------- ### Handling Event.Reset by Reading UserDefaults Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Provides an alternative approach for handling the `Event.Reset` scenario by reading the current state from UserDefaults before it's cleared. This ensures data is not lost when the `TimerManager` resets the state. ```kotlin is Event.Reset -> { // Read current state from UserDefaults before it's cleared by TimerManager val currentState = userDefaultsStorage.readTimerState() val currentLabel = getCurrentLabel() // Need a way to get this if (currentState != null && currentLabel != null) { maybeStoreAndClearSession(currentState, currentLabel) } else { userDefaultsStorage.clearTimerState() } } ``` -------------------------------- ### LocalDataRepository - Sessions Source: https://context7.com/adrcotfas/goodtime/llms.txt APIs for inserting, observing, updating, and deleting finished sessions. ```APIDOC ## LocalDataRepository - Sessions ### Description APIs for inserting, observing, updating, and deleting finished sessions. ### Insert Session ```kotlin val id: Long = repo.insertSession(session) ``` ### Observe All Sessions ```kotlin repo.selectAllSessions().collect { sessions -> println("Total: ${sessions.size} sessions") } ``` ### Observe Sessions by Label ```kotlin repo.selectSessionsByLabel("Work").collect { ... } ``` ### Observe Sessions After Timestamp ```kotlin repo.selectSessionsAfter(startOfWeekMillis).collect { ... } ``` ### Update Session ```kotlin repo.updateSession(id = id, newSession = session.copy(notes = "Updated note")) ``` ### Bulk Relabel Sessions ```kotlin repo.updateSessionsLabelByIds(newLabel = "Personal", ids = listOf(1L, 2L, 3L)) ``` ### Delete Sessions ```kotlin repo.deleteSessions(ids = listOf(4L, 5L)) ``` ``` -------------------------------- ### Timer State Data Structure Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Represents the complete state of the timer, including session details and settings. This structure is Codable for persistence and is used by the extension to manage the Live Activity. Label information is not included as it's set by the host app. ```swift // Full timer state (minimal - extension doesn't need label info) struct TimerState: Codable { let version: Int // MARK: - Current Session State let timerType: String // "focus" | "shortBreak" | "longBreak" let state: String // "running" | "paused" | "stopped" | "reset" let startTime: Double // elapsedRealtime (milliseconds) let endTime: Double // elapsedRealtime (milliseconds) let lastStartTime: Double // elapsedRealtime (milliseconds) let timeSpentPaused: Double // milliseconds let timeAtPause: Double // elapsedRealtime (milliseconds) let lastPauseTime: Double // elapsedRealtime (milliseconds) // MARK: - Settings (for extension to start next session) let focusDuration: Int // seconds let shortBreakDuration: Int // seconds let longBreakDuration: Int // seconds let sessionsBeforeLongBreak: Int let currentStreakCount: Int // NOTE: Label info (name, color, duration, countdown) is NOT stored here // because the extension cannot change the label. Label is set when // starting a session from the host app and remains constant throughout // the session chain. The extension only needs to continue with the // current session's settings. // MARK: - Metadata let lastUpdateTime: Double // wall-clock (for conflict resolution) let isCountdown: Bool // convenience let pendingAction: String? // "start_break" | "start_focus" | nil (from notification) } ``` -------------------------------- ### Configure Reminder Settings via SettingsRepository Source: https://context7.com/adrcotfas/goodtime/llms.txt Updates reminder settings, including the days of the week and the specific time of day for notifications. This action automatically triggers a reschedule of reminders. ```kotlin // Configure reminders via SettingsRepository (triggers automatic reschedule): settingsRepo.updateReminderSettings { current -> current.copy( days = listOf(1, 2, 3, 4, 5), // Mon–Fri secondOfDay = LocalTime(8, 30).toSecondOfDay() ) } ``` -------------------------------- ### SettingsRepository: Update Timer Style Source: https://context7.com/adrcotfas/goodtime/llms.txt Modify the visual style of the timer, including font size, color, and display options like showing streaks or break budgets. ```kotlin settingsRepo.updateTimerStyle { current -> current.copy( colorIndex = 5, fontWeight = 300, minutesOnly = false, showStreak = true, showBreakBudget = true, ) } ``` -------------------------------- ### iOS App Lifecycle with .foreground Notification Action Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Illustrates the sequence of events when a user taps a notification action while the app is killed. It highlights the importance of writing pending actions to UserDefaults while the app is still in the inactive state. ```markdown User taps "Start Break" action (app killed, timer finished) │ ▼ System launches app (Not Running → Inactive) │ ▼ userNotificationCenter(_: didReceive:) called │ ← App is still "inactive" here │ ├─ Write "pendingAction: start_break" to UserDefaults ├─ Store completed session if needed │ ▼ call completionHandler() │ ▼ App becomes Active │ ▼ TimerStateRestoration.restoreTimerState() │ ├─ Reads UserDefaults state ├─ Checks for pendingAction ├─ If "start_break": timerManager.finish() + timerManager.next() ├─ If "start_focus": timerManager.finish() + timerManager.next() ├─ Clears pendingAction └─ Continues normal restoration ``` -------------------------------- ### Event Data Structures for UserDefaults Storage Source: https://github.com/adrcotfas/goodtime/blob/master/docs/architecture/IOS_LIVE_ACTIVITY_HOST_APP_KILLED_DESIGN.md Defines data classes for events that include full data for UserDefaults storage, ensuring all necessary information is available for persistence. ```kotlin data class Start( val isFocus: Boolean, val runtimeState: TimerRuntimeState = TimerRuntimeState(), // NEW: Add full data for UserDefaults storage val domainTimerData: DomainTimerData? = null, val label: Label? = null, ) : Event() data class Pause( val runtimeState: TimerRuntimeState = TimerRuntimeState(), // NEW: Add full data for UserDefaults storage val domainTimerData: DomainTimerData? = null, val label: Label? = null, ) : Event() data class AddOneMinute( val endTime: Long, ) : Event() data class Finished( val type: TimerType, val autostartNextSession: Boolean = false, // NEW: Add full data for UserDefaults storage val domainTimerData: DomainTimerData? = null, val label: Label? = null, ) : Event() data object Reset : Event() // Note: Reset uses current timerData from TimerManager data class SendToBackground( val isTimerRunning: Boolean, val endTime: Long, ) : Event() data object BringToForeground : Event() data object UpdateActiveLabel : Event() } ```