fix(desktop): Persist window size and position across restarts #2

Merged
herel merged 1 commits from desktop-window-size into main 2026-08-11 15:13:46 +00:00
4 changed files with 152 additions and 2 deletions
+5 -1
View File
@@ -1,4 +1,4 @@
.PHONY: android desktop android-usb android-emulator .PHONY: android desktop desktop-install android-usb android-emulator
ADB := $(shell command -v adb 2>/dev/null || echo "$(HOME)/Library/Android/sdk/platform-tools/adb") ADB := $(shell command -v adb 2>/dev/null || echo "$(HOME)/Library/Android/sdk/platform-tools/adb")
EMULATOR := $(shell command -v emulator 2>/dev/null || echo "$(HOME)/Library/Android/sdk/emulator/emulator") EMULATOR := $(shell command -v emulator 2>/dev/null || echo "$(HOME)/Library/Android/sdk/emulator/emulator")
@@ -11,6 +11,10 @@ android:
desktop: desktop:
./gradlew composeApp:run ./gradlew composeApp:run
desktop-install:
./gradlew composeApp:packageDistributionForCurrentOS
cp -R composeApp/build/compose/binaries/main/app/myicek.app /Applications/myicek.app
android-usb: android android-usb: android
$(ADB) -d install -r $(APK) $(ADB) -d install -r $(APK)
$(ADB) -d shell am start -n ch.parano.myicek/.MainActivity $(ADB) -d shell am start -n ch.parano.myicek/.MainActivity
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// MyIce Kotlin Multiplatform — schedule/convocation viewer
// Copyright (C) 2026 parano.ch
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package ch.parano.myicek
import com.russhwolf.settings.Settings
data class SavedWindowState(
val width: Int,
val height: Int,
val x: Int,
val y: Int,
)
class WindowStateStorage(private val settings: Settings) {
companion object {
private const val WIDTH_KEY = "window_width"
private const val HEIGHT_KEY = "window_height"
private const val X_KEY = "window_x"
private const val Y_KEY = "window_y"
}
fun save(width: Int, height: Int, x: Int, y: Int) {
settings.putInt(WIDTH_KEY, width)
settings.putInt(HEIGHT_KEY, height)
settings.putInt(X_KEY, x)
settings.putInt(Y_KEY, y)
}
fun load(): SavedWindowState? {
val width = settings.getIntOrNull(WIDTH_KEY) ?: return null
val height = settings.getIntOrNull(HEIGHT_KEY) ?: return null
val x = settings.getIntOrNull(X_KEY) ?: return null
val y = settings.getIntOrNull(Y_KEY) ?: return null
return SavedWindowState(width, height, x, y)
}
}
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// MyIce Kotlin Multiplatform — schedule/convocation viewer
// Copyright (C) 2026 parano.ch
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package ch.parano.myicek
import com.russhwolf.settings.MapSettings
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
class WindowStateStorageTest {
@Test
fun saveAndLoadWindowState() {
val settings = MapSettings()
val storage = WindowStateStorage(settings)
storage.save(width = 1200, height = 800, x = 100, y = 50)
val state = storage.load()
assertEquals(1200, state?.width)
assertEquals(800, state?.height)
assertEquals(100, state?.x)
assertEquals(50, state?.y)
}
@Test
fun loadReturnsNullWhenNothingSaved() {
val settings = MapSettings()
val storage = WindowStateStorage(settings)
assertNull(storage.load())
}
@Test
fun saveOverwritesPreviousState() {
val settings = MapSettings()
val storage = WindowStateStorage(settings)
storage.save(width = 1200, height = 800, x = 100, y = 50)
storage.save(width = 1024, height = 768, x = 200, y = 75)
val state = storage.load()
assertEquals(1024, state?.width)
assertEquals(768, state?.height)
assertEquals(200, state?.x)
assertEquals(75, state?.y)
}
}
@@ -17,12 +17,43 @@
package ch.parano.myicek package ch.parano.myicek
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import ch.parano.myicek.auth.DesktopOAuthClient import ch.parano.myicek.auth.DesktopOAuthClient
import com.russhwolf.settings.Settings
import java.util.prefs.Preferences
fun main() = application { fun main() = application {
Window(onCloseRequest = ::exitApplication, title = "MyIce") { val storage = WindowStateStorage(Settings())
val saved = storage.load()
val windowState = rememberWindowState(
width = saved?.width?.dp ?: 1024.dp,
height = saved?.height?.dp ?: 768.dp,
position = if (saved != null) WindowPosition(saved.x.dp, saved.y.dp) else WindowPosition(0.dp, 0.dp),
)
Window(
onCloseRequest = ::exitApplication,
title = "MyIce",
state = windowState,
) {
LaunchedEffect(Unit) {
snapshotFlow { windowState.size to windowState.position }
.collect { (size, position) ->
storage.save(
width = size.width.value.toInt(),
height = size.height.value.toInt(),
x = position.x.value.toInt(),
y = position.y.value.toInt(),
)
Preferences.userRoot().flush()
}
}
App(oauthClient = DesktopOAuthClient()) App(oauthClient = DesktopOAuthClient())
} }
} }