Build native UI shell foundation

This commit is contained in:
2026-07-04 10:41:51 +02:00
parent 89a99582a5
commit 6bcfc534a0
34 changed files with 1998 additions and 794 deletions

View File

@@ -0,0 +1,56 @@
package com.vnidrop.app.logging
import java.io.File
import java.nio.charset.StandardCharsets
actual fun createPlatformLogStore(appDataDir: String, policy: LogRotationPolicy): PlatformLogStore =
JvmPlatformLogStore(appDataDir, policy)
actual fun platformNowMillis(): Long = System.currentTimeMillis()
private class JvmPlatformLogStore(
appDataDir: String,
private val policy: LogRotationPolicy,
) : PlatformLogStore {
private val directory = File(appDataDir, "logs")
private val activeFile = File(directory, "app.log")
override val logDirectory: String = directory.absolutePath
@Synchronized
override fun append(line: String) {
directory.mkdirs()
val bytes = line.toByteArray(StandardCharsets.UTF_8)
if (policy.shouldRotate(activeFile.length(), bytes.size.toLong())) {
rotate()
}
activeFile.appendBytes(bytes)
}
@Synchronized
override fun listLogFiles(): List<LogFileInfo> {
directory.mkdirs()
return directory
.listFiles { file -> file.isFile && file.name.startsWith("app") && file.name.endsWith(".log") }
.orEmpty()
.sortedByDescending { it.lastModified() }
.map { file -> LogFileInfo(file.name, file.absolutePath, file.length(), file.lastModified()) }
}
private fun rotate() {
if (policy.maxFiles == 0) {
activeFile.delete()
return
}
File(directory, "app.${policy.maxFiles}.log").delete()
for (index in policy.maxFiles - 1 downTo 1) {
val source = File(directory, "app.$index.log")
if (source.exists()) {
source.renameTo(File(directory, "app.${index + 1}.log"))
}
}
if (activeFile.exists()) {
activeFile.renameTo(File(directory, "app.1.log"))
}
}
}

View File

@@ -0,0 +1,51 @@
package com.vnidrop.app.platform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import java.awt.Color
import java.awt.EventQueue
import java.awt.Window
import javax.swing.JFrame
@Composable
actual fun PlatformSystemAppearance(isDarkTheme: Boolean) {
SideEffect {
DesktopSystemAppearance.apply(isDarkTheme)
}
}
internal object DesktopSystemAppearance {
private const val MAC_APPEARANCE_PROPERTY = "apple.awt.application.appearance"
private const val TRANSPARENT_TITLE_BAR_PROPERTY = "apple.awt.transparentTitleBar"
fun apply(isDarkTheme: Boolean) {
if (!isMacOs()) return
System.setProperty(MAC_APPEARANCE_PROPERTY, macOsAppearanceName(isDarkTheme))
EventQueue.invokeLater {
Window.getWindows().forEach { window ->
applyWindowChrome(window, isDarkTheme)
}
}
}
internal fun macOsAppearanceName(isDarkTheme: Boolean): String =
if (isDarkTheme) "NSAppearanceNameDarkAqua" else "NSAppearanceNameAqua"
internal fun titlebarBackground(isDarkTheme: Boolean): Color =
if (isDarkTheme) Color(0x12, 0x12, 0x12) else Color(0xF8, 0xF8, 0xF8)
private fun applyWindowChrome(window: Window, isDarkTheme: Boolean) {
val background = titlebarBackground(isDarkTheme)
window.background = background
(window as? JFrame)?.rootPane?.let { rootPane ->
// Keep the native macOS controls and drag behavior, but let the
// decorated titlebar blend with the app's resolved light/dark surface.
rootPane.putClientProperty(TRANSPARENT_TITLE_BAR_PROPERTY, true)
rootPane.background = background
rootPane.contentPane.background = background
}
}
private fun isMacOs(): Boolean =
System.getProperty("os.name").startsWith("Mac", ignoreCase = true)
}