diff --git a/Makefile b/Makefile index f1bef40..5a6aae3 100644 --- a/Makefile +++ b/Makefile @@ -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") EMULATOR := $(shell command -v emulator 2>/dev/null || echo "$(HOME)/Library/Android/sdk/emulator/emulator") @@ -11,6 +11,10 @@ android: desktop: ./gradlew composeApp:run +desktop-install: + ./gradlew composeApp:packageDistributionForCurrentOS + cp -R composeApp/build/compose/binaries/main/app/myicek.app /Applications/myicek.app + android-usb: android $(ADB) -d install -r $(APK) $(ADB) -d shell am start -n ch.parano.myicek/.MainActivity diff --git a/composeApp/src/commonMain/kotlin/ch/parano/myicek/WindowStateStorage.kt b/composeApp/src/commonMain/kotlin/ch/parano/myicek/WindowStateStorage.kt new file mode 100644 index 0000000..27abb83 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ch/parano/myicek/WindowStateStorage.kt @@ -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 . + +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) + } +} diff --git a/composeApp/src/commonTest/kotlin/ch/parano/myicek/WindowStateStorageTest.kt b/composeApp/src/commonTest/kotlin/ch/parano/myicek/WindowStateStorageTest.kt new file mode 100644 index 0000000..c9d4c80 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/ch/parano/myicek/WindowStateStorageTest.kt @@ -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 . + +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) + } +} diff --git a/composeApp/src/desktopMain/kotlin/ch/parano/myicek/Main.kt b/composeApp/src/desktopMain/kotlin/ch/parano/myicek/Main.kt index 0ce13b1..7d86ea2 100644 --- a/composeApp/src/desktopMain/kotlin/ch/parano/myicek/Main.kt +++ b/composeApp/src/desktopMain/kotlin/ch/parano/myicek/Main.kt @@ -17,12 +17,43 @@ 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.WindowPosition import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState import ch.parano.myicek.auth.DesktopOAuthClient +import com.russhwolf.settings.Settings +import java.util.prefs.Preferences 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()) } }