From 45ef6a9116d5a8cb5bfdb5ffb3ea199b88b118a9 Mon Sep 17 00:00:00 2001 From: Trishiraj Date: Tue, 3 Nov 2026 17:29:30 +0530 Subject: [PATCH 01/32] Drop unreferenced code and stale baseline entries The open-source release is a good moment to shed API that nothing calls: unused calendar counting helpers, an unused animation step constant, four unused palette entries, a superseded footprint accessor on NotchMetrics, two macOS bridge read-backs, an unused app-cache invalidator, two unused SystemAccent entry points, the login-item read path (the switch writes rather than reads) and the SettingsScreen onClose parameter, which the only call site never used. The persistence writer now waits on its channel directly instead of iterating it, which also silences a detekt false positive that reported the loop variable as an unused property. --- .../quicknotch/calendar/CalendarEvent.kt | 19 ------------------- .../datasource/InstalledAppsScanner.kt | 4 ---- .../quicknotch/datasource/MacAppKit.kt | 6 ------ .../datasource/MacStartupManager.kt | 2 -- .../quicknotch/datasource/StartupManager.kt | 8 -------- .../quicknotch/datasource/SystemAccent.kt | 12 ------------ .../datasource/WindowsStartupManager.kt | 8 -------- .../quicknotch/model/FavoritesRepository.kt | 3 ++- .../com/starkdroid/quicknotch/theme/Color.kt | 16 +++------------- .../starkdroid/quicknotch/ui/NotchMotion.kt | 1 - .../quicknotch/ui/OverlayGeometry.kt | 7 ------- .../quicknotch/ui/SettingsScreen.kt | 3 +-- config/detekt/baseline.xml | 4 +--- 13 files changed, 7 insertions(+), 86 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/starkdroid/quicknotch/calendar/CalendarEvent.kt b/composeApp/src/commonMain/kotlin/com/starkdroid/quicknotch/calendar/CalendarEvent.kt index 7b2f4ef..c83cc9a 100644 --- a/composeApp/src/commonMain/kotlin/com/starkdroid/quicknotch/calendar/CalendarEvent.kt +++ b/composeApp/src/commonMain/kotlin/com/starkdroid/quicknotch/calendar/CalendarEvent.kt @@ -5,7 +5,6 @@ import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn -import kotlinx.datetime.plus import kotlinx.datetime.todayIn import kotlinx.datetime.toLocalDateTime @@ -76,15 +75,6 @@ data class CalendarDateGroup( val isToday: Boolean ) -/** How many of these events fall on today. */ -fun List.todayCount( - timeZone: TimeZone = TimeZone.currentSystemDefault(), - today: LocalDate = Clock.System.todayIn(timeZone) -): Int = count { event -> - Instant.fromEpochMilliseconds(event.startMillis) - .toLocalDateTime(timeZone).date == today -} - /** * Today's events that are still upcoming -- running counts, ended does not. * This is the pill badge's number, and it matches the count Google Calendar @@ -99,15 +89,6 @@ fun List.todayUpcomingCount( .toLocalDateTime(timeZone).date == today && event.endMillis >= nowMillis } -/** - * How many of these events are still upcoming -- running right now counts, one - * that ended does not. This is the number the pill badge shows, so a calendar - * whose today is empty still gets its icon and count on the notch. - */ -fun List.upcomingCount( - nowMillis: Long = Clock.System.now().toEpochMilliseconds() -): Int = count { it.endMillis >= nowMillis } - /** * The next [maxDates] dates with an event coming, soonest first -- four, one * per date even when a date carries several, because the schedule row is about diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/InstalledAppsScanner.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/InstalledAppsScanner.kt index 471e44d..d719bda 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/InstalledAppsScanner.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/InstalledAppsScanner.kt @@ -29,8 +29,4 @@ object InstalledAppsScanner { } } } - - fun invalidateCache() { - cached = null - } } diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/MacAppKit.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/MacAppKit.kt index dfde7d6..5e13a33 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/MacAppKit.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/MacAppKit.kt @@ -42,8 +42,6 @@ internal object MacAppKit { /** NSStatusWindowLevel: the band the menu bar occupies — above every normal * app window and the menu bar itself, still below alert/modal levels. */ const val LEVEL_STATUS_BAR = 25 - /** NSFloatingWindowLevel (what AWT's alwaysOnTop reaches). */ - const val LEVEL_FLOATING = 3 /** canJoinAllSpaces | stationary | fullScreenAuxiliary | ignoresCycle. */ const val COLLECTION_NOTCH: Long = @@ -78,10 +76,6 @@ internal object MacAppKit { call("quicknotch_set_ignores_mouse_events", ptr, if (ignore) 1 else 0) } - /** Live `ignoresMouseEvents` read-back (diagnostics). */ - fun ignoresMouseEvents(ptr: Long): Boolean = - (fn("quicknotch_get_ignores_mouse_events")?.invokeInt(arrayOf(ptr)) ?: -1) == 1 - fun setWindowLevel(ptr: Long, level: Int) { call("quicknotch_set_window_level", ptr, level) } diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/MacStartupManager.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/MacStartupManager.kt index 2ca2acb..f8985e5 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/MacStartupManager.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/MacStartupManager.kt @@ -11,8 +11,6 @@ object MacStartupManager { "Library/LaunchAgents/$LABEL.plist" ) - fun isAutoStartEnabled(): Boolean = plistFile.isFile - fun setAutoStartEnabled(enabled: Boolean): Boolean { return try { if (enabled) { diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/StartupManager.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/StartupManager.kt index ccaddd3..8206682 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/StartupManager.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/StartupManager.kt @@ -9,14 +9,6 @@ object StartupManager { */ const val LAUNCHED_AT_LOGIN_ARG = "--quicknotch-launched-at-login" - fun isAutoStartEnabled(): Boolean { - return if (HostPlatform.isMac) { - MacStartupManager.isAutoStartEnabled() - } else { - WindowsStartupManager.isAutoStartEnabled() - } - } - fun setAutoStartEnabled(enabled: Boolean): Boolean { return if (HostPlatform.isMac) { MacStartupManager.setAutoStartEnabled(enabled) diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/SystemAccent.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/SystemAccent.kt index 674452e..3445dc3 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/SystemAccent.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/SystemAccent.kt @@ -28,13 +28,6 @@ object SystemAccent { /** Live accent colour; [refresh] publishes new OS values as they are read. */ val accent: StateFlow = _accent.asStateFlow() - /** Latest known accent colour, or null when the OS didn't report one. Never throws. */ - fun current(): Color? { - _accent.value?.let { return it } - refresh() - return _accent.value - } - /** * Re-reads the OS accent and publishes it. A failed probe keeps the * previous value (the indicator only ever shows real data), matching the @@ -45,11 +38,6 @@ object SystemAccent { if (fresh != null) _accent.value = fresh } - /** Drop the read state so the next [current]/[refresh] re-reads the OS. */ - fun invalidate() { - _accent.value = null - } - private fun readFromMacOS(): Color? { return runCatching { val process = ProcessBuilder("/usr/bin/defaults", "read", "-g", "AppleAccentColor") diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/WindowsStartupManager.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/WindowsStartupManager.kt index 7e9a65f..89a4d21 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/WindowsStartupManager.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/WindowsStartupManager.kt @@ -9,14 +9,6 @@ object WindowsStartupManager { private val REGISTRY_RUN_KEY = listOf("Software", "Microsoft", "Windows", "CurrentVersion", "Run").joinToString(File.separator) private const val APP_KEY_NAME = "QuickNotch" - fun isAutoStartEnabled(): Boolean { - return try { - Advapi32Util.registryValueExists(WinReg.HKEY_CURRENT_USER, REGISTRY_RUN_KEY, APP_KEY_NAME) - } catch (_: Exception) { - false - } - } - fun setAutoStartEnabled(enabled: Boolean): Boolean { return try { if (enabled) { diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/model/FavoritesRepository.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/model/FavoritesRepository.kt index 80b6abd..1855a0c 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/model/FavoritesRepository.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/model/FavoritesRepository.kt @@ -67,7 +67,8 @@ class FavoritesRepository( init { scope.launch { - for (wakeSignal in persistWake) { + while (true) { + persistWake.receive() val configToSave = synchronized(persistLock) { pendingConfig.also { pendingConfig = null } } diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/theme/Color.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/theme/Color.kt index 50b7f23..b286548 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/theme/Color.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/theme/Color.kt @@ -8,10 +8,7 @@ val NotchCard = Color(0xFF161C2B) val NotchBorder = Color(0x33FFFFFF) val AccentIndigo = Color(0xFF6366F1) -val AccentIndigoLight = Color(0xFF818CF8) val AccentCyan = Color(0xFF38BDF8) -val AccentPurple = Color(0xFFA855F7) -val AccentPink = Color(0xFFEC4899) val AccentGreen = Color(0xFF22C55E) val AccentRed = Color(0xFFEF4444) @@ -19,15 +16,9 @@ val TextPrimary = Color(0xFFF8FAFC) val TextSecondary = Color(0xFF94A3B8) val TextMuted = Color(0xFF64748B) -// ── Weather condition tints (the "colorful" icon style) ───────────────────── -// One signature colour per condition; the monochrome style ignores these and -// tints every glyph with the surrounding text colour instead. - -// ── Modern settings design tokens (all solid flats — no gradients) ─────────── -// Refined sidebar + card system for the settings window and onboarding flow. -// Every token is a flat solid so surfaces stay crisp on the CPU rasterizer and -// read as one calm dark system: deep app bg, slightly lifted sidebar, cards one -// step above, hairline borders, accent reserved for selection + primary action. +// Settings and onboarding surfaces. Every token is a flat solid: deep app +// background, a slightly lifted sidebar, cards one step above, hairline +// borders, and the accent reserved for selection and the primary action. val SettingsBg = Color(0xFF0B0D13) val SettingsSidebar = Color(0xFF0E121B) val SettingsCard = Color(0xFF141926) @@ -38,7 +29,6 @@ val SettingsBorderStrong = Color(0xFF2E3854) val SettingsSelected = Color(0xFF1B2340) val AccentSolid = Color(0xFF818CF8) val AccentSoft = Color(0x14818CF8) -val AccentInk = Color(0xFF14161F) val SwitchTrackOn = Color(0xFF6366F1) val SwitchTrackOff = Color(0xFF232B42) val SwitchThumbOn = Color(0xFFFFFFFF) diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/NotchMotion.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/NotchMotion.kt index 0b215cf..354ba67 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/NotchMotion.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/NotchMotion.kt @@ -151,7 +151,6 @@ internal fun fisheyeScale( * energy saving without lying to the user. */ internal const val PillAnimationFps = 30f -internal const val PillAnimationStepSec = 1f / PillAnimationFps /** The matching [kotlinx.coroutines.delay] step, in milliseconds. */ internal val PillAnimationStepMillis = (1000L / PillAnimationFps).toLong() diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/OverlayGeometry.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/OverlayGeometry.kt index aa08c8d..14420a4 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/OverlayGeometry.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/OverlayGeometry.kt @@ -119,10 +119,6 @@ internal object DockLayout { */ @Immutable data class NotchMetrics( - /** Native window footprint when collapsed. */ - val collapsed: DpSize, - /** Native window footprint when expanded. */ - val expanded: DpSize, /** Visible island when expanded. */ val island: DpSize, /** Visible island when collapsed. */ @@ -143,7 +139,6 @@ data class NotchMetrics( */ val edgeGapDp: Int = 0 ) { - fun sizeFor(isExpanded: Boolean): DpSize = if (isExpanded) expanded else collapsed fun islandFor(isExpanded: Boolean): DpSize = if (isExpanded) island else islandCollapsed } @@ -214,8 +209,6 @@ fun computeNotchMetrics( val window = DpSize(maxOf(alongExpanded, DockLayout.GameHudWidth), islandDepth) return NotchMetrics( - collapsed = islandCollapsed, - expanded = dockArea, island = islandExpanded, islandCollapsed = islandCollapsed, dockArea = dockArea, diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsScreen.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsScreen.kt index 6433ab5..e319d5d 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsScreen.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsScreen.kt @@ -100,8 +100,7 @@ import org.jetbrains.compose.resources.stringResource @Composable fun SettingsScreen( repository: FavoritesRepository, - media: MediaController, - onClose: () -> Unit + media: MediaController ) { // Deliberately NO screen-level config/favorites/session collects here: the // settings window used to recompose end-to-end on every media heartbeat diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 3ceb12a..f5e6d5b 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -50,7 +50,7 @@ LongMethod:SettingsComponents.kt$@OptIn(ExperimentalComposeUiApi::class) @Composable internal fun SidebarNavItem( icon: ImageVector, label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier ) LongMethod:SettingsComponents.kt$@OptIn(ExperimentalComposeUiApi::class) @Composable internal fun WidgetAccordion( icon: ImageVector, title: String, enabled: Boolean, onToggleEnabled: (Boolean) -> Unit, content: @Composable () -> Unit ) LongMethod:SettingsComponents.kt$@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) @Composable internal fun <T> AppearanceDropdown( label: String, options: List<T>, selected: T, optionLabel: @Composable (T) -> String, onSelect: (T) -> Unit, modifier: Modifier = Modifier ) - LongMethod:SettingsScreen.kt$@Composable fun SettingsScreen( repository: FavoritesRepository, media: MediaController, onClose: () -> Unit ) + LongMethod:SettingsScreen.kt$@Composable fun SettingsScreen( repository: FavoritesRepository, media: MediaController ) LongMethod:SettingsScreen.kt$@Composable private fun AboutDialog( onDismiss: () -> Unit ) LongMethod:WeatherWidget.kt$@Composable private fun WeatherCurrentBlock(data: WeatherData, fahrenheit: Boolean) LongMethod:WidgetsTab.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun WidgetsTab( repository: FavoritesRepository, media: MediaController, scannedApps: List<AppItem>, searchQuery: String, isScanning: Boolean, onSearchQueryChange: (String) -> Unit, calendarState: CalendarState, calendarRefreshing: Boolean, onConnectCalendar: () -> Unit, onCancelCalendar: () -> Unit, onDisconnectCalendar: () -> Unit, onRefreshCalendar: () -> Unit ) @@ -177,8 +177,6 @@ TooManyFunctions:MediaWidgets.kt$com.starkdroid.quicknotch.ui.MediaWidgets.kt TooManyFunctions:SettingsComponents.kt$com.starkdroid.quicknotch.ui.SettingsComponents.kt TooManyFunctions:WeatherWidget.kt$com.starkdroid.quicknotch.ui.WeatherWidget.kt - UnusedParameter:SettingsScreen.kt$onClose: () -> Unit - UnusedPrivateProperty:FavoritesRepository.kt$FavoritesRepository$wakeSignal UnusedPrivateProperty:GoogleCalendar.kt$GoogleCalendar$private const val PRODUCTION_WEB_BASE = "https://quicknotch.app" From 94b3ee1b4dea5853eca7b52f3cd235168c47d9e6 Mon Sep 17 00:00:00 2001 From: Trishiraj Date: Tue, 3 Nov 2026 17:29:34 +0530 Subject: [PATCH 02/32] Link the source and Ko-fi from the changelog and the tray Every public address the app points at now lives in one ProjectLinks object, so the tray, the changelog and the update banner cannot drift. The changelog footer gains "Source on GitHub" and "Support on Ko-fi" next to the copyright, the tray gains a support entry, and the footer text drops "all rights reserved" for the licence the repo actually ships. --- .../composeResources/values/strings.xml | 5 ++- .../quicknotch/datasource/AppUpdates.kt | 5 +-- .../quicknotch/datasource/ProjectLinks.kt | 21 +++++++++++++ .../quicknotch/datasource/ReleaseApi.kt | 2 +- .../kotlin/com/starkdroid/quicknotch/main.kt | 9 ++++-- .../quicknotch/ui/ChangelogDialog.kt | 31 ++++++++++++++----- .../quicknotch/ui/SettingsComponents.kt | 15 +++++++++ 7 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/ProjectLinks.kt diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 365f445..2d6b71b 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -243,5 +243,8 @@ Add apps from the list on the right. View release Update failed: %1$s A newer version is on GitHub — open the release page to download it. - © 2026 VelocityApps. All rights reserved. + Source on GitHub + Support on Ko-fi + Support QuickNotch + © 2026 VelocityApps · MIT licensed diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/AppUpdates.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/AppUpdates.kt index 13113a1..a7423d2 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/AppUpdates.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/AppUpdates.kt @@ -8,9 +8,7 @@ import io.ktor.client.request.header import io.ktor.client.statement.bodyAsChannel import io.ktor.http.HttpHeaders import io.ktor.http.isSuccess -import java.awt.Desktop import java.io.File -import java.net.URI import java.util.concurrent.atomic.AtomicBoolean import com.starkdroid.quicknotch.AppVersion import kotlinx.coroutines.CancellationException @@ -146,8 +144,7 @@ object AppUpdates { fun openReleasePage() { val url = (_state.value as? UpdateState.Available)?.releaseUrl ?: ReleaseApi.RELEASES_PAGE - runCatching { Desktop.getDesktop().browse(URI(url)) } - .onFailure { AppLog.line("update: could not open browser: ${it.message}") } + ProjectLinks.open(url) } private suspend fun refresh() { diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/ProjectLinks.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/ProjectLinks.kt new file mode 100644 index 0000000..d007522 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/ProjectLinks.kt @@ -0,0 +1,21 @@ +package com.starkdroid.quicknotch.datasource + +import java.awt.Desktop +import java.net.URI + +/** + * The project's public addresses, in one place so the tray, the changelog and + * the update banner cannot drift apart. + */ +object ProjectLinks { + + const val REPOSITORY = "https://github.com/StarkDroid/QuickNotch" + const val RELEASES = "$REPOSITORY/releases/latest" + const val KOFI = "https://ko-fi.com/trishiraj" + + /** Opens [url] in the system browser; a headless host just stays quiet. */ + fun open(url: String) { + runCatching { Desktop.getDesktop().browse(URI(url)) } + .onFailure { AppLog.line("links: could not open $url: ${it.message}") } + } +} diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/ReleaseApi.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/ReleaseApi.kt index c70fd0f..08195dd 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/ReleaseApi.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/datasource/ReleaseApi.kt @@ -45,7 +45,7 @@ object ReleaseApi { const val OWNER = "StarkDroid" const val REPO = "QuickNotch" - const val RELEASES_PAGE = "https://github.com/$OWNER/$REPO/releases/latest" + const val RELEASES_PAGE = ProjectLinks.RELEASES private const val RELEASES_URL = "https://api.github.com/repos/$OWNER/$REPO/releases?per_page=20" private const val CHECKSUMS = "SHA256SUMS.txt" diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/main.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/main.kt index 4814853..843f16a 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/main.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/main.kt @@ -33,6 +33,7 @@ import com.starkdroid.quicknotch.datasource.StartupManager import com.starkdroid.quicknotch.datasource.UpdateState import com.starkdroid.quicknotch.datasource.MacDockPresence import com.starkdroid.quicknotch.datasource.MacOverlayWindowPolicy +import com.starkdroid.quicknotch.datasource.ProjectLinks import com.starkdroid.quicknotch.datasource.WindowPassthrough import com.starkdroid.quicknotch.datasource.WindowsOverlayWindowPolicy import com.starkdroid.quicknotch.generated.resources.Res @@ -42,6 +43,7 @@ import com.starkdroid.quicknotch.generated.resources.menu_always_on_top_checked import com.starkdroid.quicknotch.generated.resources.menu_hide_notch import com.starkdroid.quicknotch.generated.resources.menu_quit import com.starkdroid.quicknotch.generated.resources.menu_settings +import com.starkdroid.quicknotch.generated.resources.menu_support import com.starkdroid.quicknotch.generated.resources.menu_show_notch import com.starkdroid.quicknotch.generated.resources.menu_update_available import com.starkdroid.quicknotch.generated.resources.onboarding_window_title @@ -309,6 +311,10 @@ fun main(args: Array) { Separator() Item(stringResource(Res.string.reset_defaults), onClick = { repository.resetToDefaults() }) Separator() + Item( + stringResource(Res.string.menu_support), + onClick = { ProjectLinks.open(ProjectLinks.KOFI) } + ) Item(stringResource(Res.string.menu_quit), onClick = ::exitApplication) } ) @@ -375,8 +381,7 @@ fun main(args: Array) { if (done) { SettingsScreen( repository = repository, - media = media, - onClose = { showSettings = false } + media = media ) } else { OnboardingScreen(repository = repository) diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/ChangelogDialog.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/ChangelogDialog.kt index 5c695f9..018721a 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/ChangelogDialog.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/ChangelogDialog.kt @@ -53,10 +53,13 @@ import com.starkdroid.quicknotch.datasource.Changelog import com.starkdroid.quicknotch.datasource.ChangelogChange import com.starkdroid.quicknotch.datasource.ChangelogRelease import com.starkdroid.quicknotch.datasource.ChangelogStatus +import com.starkdroid.quicknotch.datasource.ProjectLinks import com.starkdroid.quicknotch.datasource.UpdateState import com.starkdroid.quicknotch.datasource.AppInstaller import com.starkdroid.quicknotch.generated.resources.Res import com.starkdroid.quicknotch.generated.resources.about_copyright +import com.starkdroid.quicknotch.generated.resources.support_kofi +import com.starkdroid.quicknotch.generated.resources.view_source import com.starkdroid.quicknotch.generated.resources.about_description import com.starkdroid.quicknotch.generated.resources.about_product_name import com.starkdroid.quicknotch.generated.resources.about_version @@ -208,13 +211,27 @@ private fun ChangelogCardBody( color = TextMuted, fontSize = 10.sp ) - Spacer(Modifier.height(6.dp)) - Text( - text = stringResource(Res.string.about_copyright), - style = AppTypography.labelSmall, - color = TextMuted, - fontSize = 11.sp - ) + Spacer(Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(Res.string.about_copyright), + style = AppTypography.labelSmall, + color = TextMuted, + fontSize = 11.sp + ) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + SettingsTextLink(stringResource(Res.string.view_source)) { + ProjectLinks.open(ProjectLinks.REPOSITORY) + } + SettingsTextLink(stringResource(Res.string.support_kofi)) { + ProjectLinks.open(ProjectLinks.KOFI) + } + } + } } } diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsComponents.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsComponents.kt index 864b235..e400331 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsComponents.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsComponents.kt @@ -109,6 +109,21 @@ internal fun Modifier.plainClickable(onClick: () -> Unit): Modifier = this.click onClick = onClick ) +/** Inline accent link for dialog footers (source, support). */ +@Composable +internal fun SettingsTextLink(label: String, onClick: () -> Unit) { + Text( + text = label, + style = AppTypography.labelSmall, + color = AccentSolid, + fontSize = 11.sp, + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .plainClickable(onClick) + .padding(horizontal = 2.dp, vertical = 2.dp) + ) +} + /** Spring dip on press, optional lift on hover. One tuning for all controls. */ @Composable internal fun hoverPressScale( From 172ffbe9b518cebb7573e0b0163f376d4f010ef2 Mon Sep 17 00:00:00 2001 From: Trishiraj Date: Tue, 3 Nov 2026 18:47:00 +0530 Subject: [PATCH 03/32] Make the site free and open source, with download and changelog pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QuickNotch is no longer sold, so the site stops selling it. The price ticket becomes a download ticket: the stub states that it is free and MIT licensed, the right-hand column lists what the download contains, and the CTA goes to a real page instead of an external checkout. `/download` reads the feed the release workflow publishes and builds the installer URLs from the version it names, so a release needs no site change. Clicking any download reveals the Ko-fi card once — the only thing the site asks for, and only after the file is on its way. `/changelog` renders the same feed under Settings → Changelog, newest first, grouped by kind. The feed is imported at build time: release assets send no CORS headers, and importing keeps the version, the history and the structured data in the prerendered HTML rather than behind a fetch. The header and footer carry the version, the changelog, the source, the issues and the Ko-fi link; the schema declares the software free, MIT licensed and names the repository, and the generated social card says the same. --- web/scripts/generate-og.mjs | 9 +- web/scripts/verify-render.mjs | 12 +- web/src/app.css | 7 + web/src/lib/components/Footer.svelte | 13 +- web/src/lib/components/Hero.svelte | 14 +- web/src/lib/components/Icon.svelte | 18 +- web/src/lib/components/Nav.svelte | 82 +++- ...cingSection.svelte => OfferSection.svelte} | 78 ++-- web/src/lib/components/PageSeo.svelte | 33 ++ web/src/lib/components/Seo.svelte | 21 +- web/src/lib/components/Spotlight.svelte | 4 +- web/src/lib/data/changelog.json | 44 ++ web/src/lib/data/releases.ts | 108 +++++ web/src/lib/data/site.ts | 125 +++--- web/src/routes/+page.svelte | 6 +- web/src/routes/changelog/+page.svelte | 298 +++++++++++++ web/src/routes/download/+page.svelte | 420 ++++++++++++++++++ web/static/og.png | Bin 34074 -> 34851 bytes web/static/sitemap.xml | 12 + web/svelte.config.js | 3 +- 20 files changed, 1167 insertions(+), 140 deletions(-) rename web/src/lib/components/{PricingSection.svelte => OfferSection.svelte} (71%) create mode 100644 web/src/lib/components/PageSeo.svelte create mode 100644 web/src/lib/data/changelog.json create mode 100644 web/src/lib/data/releases.ts create mode 100644 web/src/routes/changelog/+page.svelte create mode 100644 web/src/routes/download/+page.svelte diff --git a/web/scripts/generate-og.mjs b/web/scripts/generate-og.mjs index 6fb9b5e..a818f6b 100644 --- a/web/scripts/generate-og.mjs +++ b/web/scripts/generate-og.mjs @@ -25,9 +25,10 @@ const staticDir = join(root, 'static'); const FONT = "'Inter','Helvetica Neue',Helvetica,Arial,sans-serif"; const DISPLAY = "'Fraunces','Georgia','Times New Roman',serif"; -// Keep the card's price line in step with `pricing.label` in src/lib/data/site.ts -// (this script is plain Node and cannot import the TypeScript content module). -const PRICE_LABEL = '$9'; +// The card's footer line. QuickNotch is free and MIT licensed; this mirrors +// `offer.finePrint` in src/lib/data/site.ts (this script is plain Node and +// cannot import the TypeScript content module). +const FOOTER_LABEL = 'Free · open source · MIT'; const escape = (value) => value.replace(/&/g, '&').replace(//g, '>'); @@ -73,7 +74,7 @@ const ogSvg = ` QuickNotch — app dock, notch, widgets - ${escape(PRICE_LABEL)} · Windows & macOS + ${escape(FOOTER_LABEL)} · Windows & macOS `; async function writePng(name, buffer, width) { diff --git a/web/scripts/verify-render.mjs b/web/scripts/verify-render.mjs index 07beb0b..6c68c21 100644 --- a/web/scripts/verify-render.mjs +++ b/web/scripts/verify-render.mjs @@ -405,19 +405,19 @@ await safe('scroll to #look', `(() => { await sleep(1700); const spotMid = await safe('spotlight at #look', spotlightReport); -await safe('scroll to #pricing', `(() => { - const el = document.querySelector('#pricing .ticket__cta'); +await safe('scroll to #download', `(() => { + const el = document.querySelector('#download .ticket__cta'); if (!el) return false; const r = el.getBoundingClientRect(); window.scrollTo(0, r.top + window.scrollY - window.innerHeight / 2); return true; })()`); await sleep(2100); -const spotEnd = await safe('spotlight at #pricing', spotlightReport); +const spotEnd = await safe('spotlight at #download', spotlightReport); const downloadTarget = await safe( - 'pricing CTA rect', + 'download CTA rect', `(() => { - const el = document.querySelector('#pricing .ticket__cta'); + const el = document.querySelector('#download .ticket__cta'); if (!el) return null; const r = el.getBoundingClientRect(); return { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2) }; @@ -483,7 +483,7 @@ if (!spotTop || !spotMid || !spotEnd) { } else { line('at top', `${spotTop.x}, ${spotTop.y} · core ${spotTop.width}px`); line('at #look', `${spotMid.x}, ${spotMid.y} · core ${spotMid.width}px`); - line('at #pricing', `${spotEnd.x}, ${spotEnd.y} · core ${spotEnd.width}px`); + line('at #download', `${spotEnd.x}, ${spotEnd.y} · core ${spotEnd.width}px`); line( 'distance to CTA', downloadTarget diff --git a/web/src/app.css b/web/src/app.css index 818a84f..052bd7e 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -442,6 +442,13 @@ p { color: var(--accent-text); } +/* Versions keep their lowercase `v`, even inside an uppercase mono label. */ +.chip--version, +.version { + text-transform: none; + letter-spacing: 0.02em; +} + /* Rotating sticker badge — a little piece of tape-and-ink flair. */ .sticker { display: inline-flex; diff --git a/web/src/lib/components/Footer.svelte b/web/src/lib/components/Footer.svelte index 322b3d6..11fb337 100644 --- a/web/src/lib/components/Footer.svelte +++ b/web/src/lib/components/Footer.svelte @@ -1,6 +1,7 @@ diff --git a/web/src/lib/components/Nav.svelte b/web/src/lib/components/Nav.svelte index ecd3111..f32ea5b 100644 --- a/web/src/lib/components/Nav.svelte +++ b/web/src/lib/components/Nav.svelte @@ -1,19 +1,21 @@ -
+
-

{pricing.eyebrow}

-

{pricing.title}

-

{pricing.body}

+

{offer.eyebrow}

+

{offer.title}

+

{offer.body}

-

Lifetime licence · Windows + macOS

+

Free · MIT licence · Windows + macOS

- {pricing.label} - {pricing.unit} + {offer.price.label} + {offer.price.unit}

- One payment. Every widget, both installers, and the updates that follow. + Download it, read the source, keep it. No licence key, no account, no + trial that runs out.

-

{pricing.finePrint}

+

{offer.finePrint}

@@ -109,7 +117,7 @@ position: absolute; top: -1.05rem; right: clamp(1rem, 4vw, 2.5rem); - rotate: 1.5deg; + rotate: -1.5deg; z-index: 2; } @@ -223,6 +231,11 @@ } .ticket__serial { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.5rem 1rem; margin-top: auto; padding-top: 0.9rem; border-top: 2px dashed var(--line); @@ -232,6 +245,21 @@ opacity: 0.75; } + .ticket__kofi { + display: inline-flex; + align-items: center; + gap: 0.35rem; + text-decoration: underline; + text-decoration-color: var(--line-strong); + text-underline-offset: 3px; + } + + .ticket__kofi:hover { + color: var(--accent-text); + text-decoration-color: var(--accent); + opacity: 1; + } + /* ---- Layout ----------------------------------------------------------- */ @media (min-width: 860px) { .ticket { diff --git a/web/src/lib/components/PageSeo.svelte b/web/src/lib/components/PageSeo.svelte new file mode 100644 index 0000000..e785222 --- /dev/null +++ b/web/src/lib/components/PageSeo.svelte @@ -0,0 +1,33 @@ + + + + {fullTitle} + + + + + + + + + + + + + + diff --git a/web/src/lib/components/Seo.svelte b/web/src/lib/components/Seo.svelte index d9bc406..43d019f 100644 --- a/web/src/lib/components/Seo.svelte +++ b/web/src/lib/components/Seo.svelte @@ -1,5 +1,6 @@ + + + +
\ No newline at end of file + diff --git a/web/src/routes/download/+page.svelte b/web/src/routes/download/+page.svelte index 849a3ed..b3b07b9 100644 --- a/web/src/routes/download/+page.svelte +++ b/web/src/routes/download/+page.svelte @@ -15,14 +15,8 @@ } from '$data/releases'; /** - * The one page that hands over an installer. - * - * The version comes from the changelog feed the release workflow commits, so - * the asset URLs are built from the newest published release and never need a - * site change. Starting a download opens the thank-you modal: a native - * ``, which traps focus, answers Escape and makes the page behind - * inert without any of that being hand-rolled. The link is never - * intercepted — it is already navigating when the modal appears. + * Installer URLs come from the newest published release. Starting a + * download opens a native dialog; the link itself is never intercepted. */ let visitor = $state<'windows' | 'macos' | null>(null); let thanks = $state(null); @@ -187,7 +181,6 @@ text-decoration-color: var(--accent); } - /* ---- Platform cards --------------------------------------------------- */ .platforms { display: grid; gap: 1.25rem; @@ -303,7 +296,6 @@ opacity: 0.75; } - /* ---- Install notes ---------------------------------------------------- */ .notes { display: flex; flex-direction: column; @@ -335,7 +327,6 @@ color: var(--text-3); } - /* ---- Thank-you modal -------------------------------------------------- */ /* A poster card on a dimmed page, offset with the same hard sticker shadow as the platform cards. */ .thanks { diff --git a/web/src/routes/oauth/callback/+page.svelte b/web/src/routes/oauth/callback/+page.svelte index bf4de22..e7e2af5 100644 --- a/web/src/routes/oauth/callback/+page.svelte +++ b/web/src/routes/oauth/callback/+page.svelte @@ -5,14 +5,8 @@ import { onMount, onDestroy } from 'svelte'; /** - * Google's OAuth bounce lands here (the redirect URI registered in the - * console). This is a static page, so it cannot exchange the code — that is - * the secret-bearing desktop app. Instead, it takes the query (code/state) - * and performs a top-level navigation to the loopback landing strip the - * desktop spun up: `http://127.0.0.1:/oauth` with the same query. - * The desktop's ServerSocket accepts that GET, reads the code+state (state - * must match the exact string this attempt emitted), sends the small - * consent page back, and completes the exchange. + * Static page: forwards the query to the loopback, which validates state + * and performs the token exchange. */ let status = $state<'idle' | 'jumping' | 'done' | 'refused' | 'error'>('idle'); let error = $state(''); @@ -31,11 +25,8 @@ let returnToApp = $state<() => void>(() => {}); /** - * The loopback port rides inside the `state` the desktop minted: it is - * base64url-encoded JSON `{"p":,"s":""}`. Only `p` matters - * here — the desktop verifies the whole string it sees against the one it - * sent, so this page only needs the port out of it and must not treat the - * nonce as authority for anything. + * state is base64url JSON with the port and nonce. Only the port is read + * here; the desktop verifies the whole string. */ function extractPortFromState(state: string | null): number | null { if (!state) return null; @@ -63,13 +54,8 @@ const hasPort = port !== null && port >= 1 && port <= 65535; /** - * Hand control back to the loopback, carrying whatever Google said. - * A refusal is forwarded too, not just a code: the desktop classifies - * `access_denied` against its description to tell "user pressed Cancel" - * from "consent screen still in Testing" (pitfall 33), and that - * distinction only exists on the app side. Swallowing it here would - * leave the settings widget spinning out its whole timeout on the one - * outcome it is best equipped to explain. + * Forward refusals too, not just codes: the desktop tells Cancel from a + * Testing consent screen. */ returnToApp = function () { if (!hasPort || !state) return; @@ -202,4 +188,4 @@ word-break: break-all; white-space: pre-wrap; } - \ No newline at end of file + From f2fcafcd4fb29ce46d2e031868860933143a2d31 Mon Sep 17 00:00:00 2001 From: Trishiraj Date: Sat, 7 Nov 2026 14:05:34 +0530 Subject: [PATCH 31/32] Let wrangler preview run for a branch The Workers build for a non-production branch runs npx wrangler preview, which refuses to start unless the Wrangler config has a previews block. This one is empty: assets and compatibility settings stay at the top level, and an empty block is all the command needs to create a Preview. --- wrangler.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wrangler.toml b/wrangler.toml index d1f8cc0..1552a22 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -20,3 +20,8 @@ binding = "ASSETS" not_found_handling = "404-page" # The handler runs first so it can answer the www host before assets do. run_worker_first = true + +# Worker Previews needs this block for `wrangler preview`, which is what +# Cloudflare runs for a non-production branch. The block can be empty: assets +# and compatibility settings stay at the top level. +[previews] From a1d444ed30af6fdc1fda83e2051179d5dde314e0 Mon Sep 17 00:00:00 2001 From: Trishiraj Date: Sat, 7 Nov 2026 14:05:34 +0530 Subject: [PATCH 32/32] Take the last long comments down to two sentences Ten four-to-five-line blocks still carried three sentences. Each now carries two, so no comment in the tree carries more than two. --- .../com/starkdroid/quicknotch/media/SystemBattery.kt | 5 ++--- .../quicknotch/model/FavoritesRepository.kt | 7 ++----- .../com/starkdroid/quicknotch/ui/CalendarWidget.kt | 7 ++----- .../com/starkdroid/quicknotch/ui/OverlayGeometry.kt | 6 +----- .../com/starkdroid/quicknotch/ui/PinnedAppsPanel.kt | 6 ++---- .../starkdroid/quicknotch/ui/SettingsComponents.kt | 5 ++--- .../com/starkdroid/quicknotch/ui/SettingsScreen.kt | 8 +++----- .../com/starkdroid/quicknotch/ui/SystemWidget.kt | 7 ++----- .../com/starkdroid/quicknotch/weather/Weather.kt | 12 +++--------- 9 files changed, 19 insertions(+), 44 deletions(-) diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/media/SystemBattery.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/media/SystemBattery.kt index 1a4aaf0..1956327 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/media/SystemBattery.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/media/SystemBattery.kt @@ -49,9 +49,8 @@ data class SystemBattery( val state: StateFlow = _state.asStateFlow() /** - * Whether the machine actually has a battery (a laptop vs. a desktop). - * Stays false until the first successful probe, then tracks presence; - * it gates the settings toggle so desktops never offer a dead control. + * One upcoming date: month and day over the count badge. Clicking it opens + * that date in Google Calendar. */ private val _hasBattery = MutableStateFlow(false) val hasBattery: StateFlow = _hasBattery.asStateFlow() diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/model/FavoritesRepository.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/model/FavoritesRepository.kt index 88afb9b..84da5da 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/model/FavoritesRepository.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/model/FavoritesRepository.kt @@ -45,11 +45,8 @@ class FavoritesRepository( private val _favorites = MutableStateFlow(loadFavorites()) val favorites: StateFlow> = _favorites.asStateFlow() - // Setters record the latest value of each kind under a plain lock (guarding - // only the tiny assignments, never file IO) and wake the writer. The writer - // drains the pending snapshots sequentially, so rapid changes — e.g. slider - // drags at frame rate — fold into a single disk write and concurrent - // writes to the same file can never interleave (no torn JSON). + // Drag state renders from a local snapshot; one repository write commits + // the final position when the drag ends. private sealed interface PersistRequest { data class Config(val value: NotchConfig) : PersistRequest data class Favorites(val value: List) : PersistRequest diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/CalendarWidget.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/CalendarWidget.kt index 1b02ab0..e8fe1e2 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/CalendarWidget.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/CalendarWidget.kt @@ -487,11 +487,8 @@ private fun CalendarTodayBlock( } } -/** - * One upcoming date in the schedule row: the day with its month -- "10 Oct", - * "20 Nov" -- over the count badge, centered. Dates here are never today; the - * big left block owns that. Clicking one opens that date in Google Calendar. - */ +// Resets only appearance, sizing and clock. Widgets and Behavior keep their +// own state, so a reset never undoes a user's widget choices. @Composable private fun CalendarDateTile( group: CalendarDateGroup, diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/OverlayGeometry.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/OverlayGeometry.kt index 470e1bc..8897a50 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/OverlayGeometry.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/OverlayGeometry.kt @@ -160,11 +160,7 @@ fun computeNotchMetrics( if (config.style == NotchStyle.DRAG_HANDLE) DockLayout.DragHandleHeightDp else config.quickViewHeightDp val islandCollapsedDepth = collapsedHeightDp.dp + DockLayout.CollapsedMargin - // The HUD stacks a value over a caption, so it keeps a floor of its own - // even when the quickview has been shrunk to a slim pill. - // The HUD is its own, slimmer strip: it may sit shallower than the pill - // rather than inheriting the quickview's depth. Its two lines of 11/6sp - // type still fit inside 30dp. + /** Collapsible widget card: header with icon, title, switch and chevron over an animated body. */ val collapsedDepth = if (gameActive) DockLayout.GameHudHeight else islandCollapsedDepth val islandExpanded = DpSize(alongExpanded, islandDepth) diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/PinnedAppsPanel.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/PinnedAppsPanel.kt index ae04c96..e87f90d 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/PinnedAppsPanel.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/PinnedAppsPanel.kt @@ -142,10 +142,8 @@ internal fun PinnedAppsTab( val pickMacFilter = stringResource(Res.string.pick_mac_filter) val pickWindowsFilter = stringResource(Res.string.pick_win_filter) - // Drag-to-reorder state. The list renders from a local snapshot so swaps - // apply synchronously under the finger; a single repository write commits - // the final position when the drag ends. Rows are equal height, but each - // measured height is tracked anyway so the swap threshold stays honest. + // The HUD is a slimmer strip with its own depth floor; two lines of 11/6sp + // type fit in 30dp. var draggingId by remember { mutableStateOf(null) } var dragStartIndex by remember { mutableStateOf(-1) } var dragOffset by remember { mutableStateOf(0f) } diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsComponents.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsComponents.kt index b3a9952..04504ef 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsComponents.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsComponents.kt @@ -896,9 +896,8 @@ internal fun BehaviorSwitchRow( } /** - * A collapsible widget card: header row (icon chip, title, enable switch, - * rotating chevron) over an animated content area. Used by the Widgets tab. - * The header lifts on hover and the chevron springs 180° when opened. + * One ring tile per metric. Network uses a log sweep with the exact rate as + * the label; no GPU counter means no tile. */ @OptIn(ExperimentalComposeUiApi::class) @Composable diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsScreen.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsScreen.kt index 95a68d1..18ba9a1 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsScreen.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SettingsScreen.kt @@ -125,11 +125,9 @@ fun SettingsScreen( val calendarState by GoogleCalendar.state.collectAsState() val calendarRefreshing by GoogleCalendar.refreshing.collectAsState() - // Restore the appearance/sizing/clock defaults. This is the same set the tab - // used to own; it now lives here because the button sits in the content - // header, and the screen already holds the repository. Widgets and Behavior - // keep their own state (pinned apps, toggles) — resetting the notch's look - // must not silently undo a user's widget choices. + // Setters record the latest value under a lock that guards only the + // assignment; one writer drains the snapshots into a single disk write, so + // no JSON is torn. val onResetDefaults: () -> Unit = { repository.updateConfig { it.copy( diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SystemWidget.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SystemWidget.kt index 089442c..6f055d7 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SystemWidget.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/ui/SystemWidget.kt @@ -64,11 +64,8 @@ private data class SystemTileSpec( val alert: Boolean ) -/** - * One ring tile per metric. CPU, GPU, memory and storage map to 0-100; - * network uses a log sweep with the exact rate as the label. No GPU - * counter means no tile. - */ +/** Starts the poller once. A manual place seeds the position so the first + * fetch skips the IP lookup. */ @Composable internal fun SystemNotchPage( snapshot: SystemSnapshot?, diff --git a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/weather/Weather.kt b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/weather/Weather.kt index deb159d..d014877 100644 --- a/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/weather/Weather.kt +++ b/composeApp/src/desktopMain/kotlin/com/starkdroid/quicknotch/weather/Weather.kt @@ -52,21 +52,15 @@ object Weather { private val cacheFile: File get() = File(AppPaths.configDir, "weather.json") - /** - * Start the poller. Idempotent; called once from the app. A manual place - * seeds the position up front so the first fetch never detours through the - * IP lookup; without one the cached fix (if any) wins as before. - */ + // Paint the cached day first, but only when it belongs to the active position. fun start(manualPlace: WeatherPlace? = null) { if (pollJob != null) return lastManualPlace = manualPlace if (manualPlace != null) { position = GeoPosition(manualPlace.latitude, manualPlace.longitude, manualPlace.name) } - // Show the cached day immediately so a cold start paints weather on - // the first frame, then refresh in the background — but only when the - // cache belongs to the active position. A stale fix from another city - // (e.g. after picking a new place) must never paint under the new one. + /** Whether the machine has a battery; gates the settings toggle so desktops + * never offer a dead control. */ readCache()?.let { cached -> val sameFix = position?.let { abs(it.latitude - cached.lat) < 0.005 && abs(it.longitude - cached.lon) < 0.005