Author SHA1 Message Date
herel 9c9716a352 fix(build): Remove existing app before desktop install
cp -R into an existing /Applications/myicek.app nested the new bundle
inside the old one (myicek.app/myicek.app) and failed on the read-only
legal files of the previous install. Delete the previous bundle first.
2026-08-31 16:41:30 +02:00
herel f2f5a15967 build: Bypass Homebrew JDK vendor check for desktop packaging
Compose Desktop's checkRuntime task rejects Homebrew JDK distributions
because their bundled runtimes may be broken. Disable the vendor check
to keep packaging with the current toolchain.

Refs JetBrains/compose-multiplatform#3107
2026-08-31 16:41:30 +02:00
herel 22c0a61e32 Merge pull request 'fix(desktop): Persist window size and position across restarts' (#2) from desktop-window-size into main
Reviewed-on: #2
2026-08-11 15:13:45 +00:00
herel 057be15822 fix(desktop): Persist window size and position across restarts
The desktop window always reset to its default size after restart
because no WindowState was persisted. Add WindowStateStorage backed
by multiplatform-settings (java.util.prefs on desktop) to save and
restore width, height, and position. Use snapshotFlow inside the
Window composition to persist changes continuously, with an explicit
Preferences.flush() to ensure values reach disk before exit.

Also add 'desktop-install' Makefile target to rebuild and copy the
app to /Applications/myicek.app.
2026-08-11 17:11:58 +02:00
5 changed files with 154 additions and 2 deletions
+6 -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,11 @@ android:
desktop: desktop:
./gradlew composeApp:run ./gradlew composeApp:run
desktop-install:
./gradlew composeApp:packageDistributionForCurrentOS
rm -rf /Applications/myicek.app
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())
} }
} }
+1
View File
@@ -2,3 +2,4 @@ kotlin.code.style=official
android.useAndroidX=true android.useAndroidX=true
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
org.gradle.jvmargs=-Xmx4096m org.gradle.jvmargs=-Xmx4096m
compose.desktop.packaging.checkJdkVendor=false