refactor(ui): establish modular Compose foundation

This commit is contained in:
2026-07-10 18:19:24 +02:00
parent 382a015688
commit a15be5787d
68 changed files with 3103 additions and 2167 deletions

View File

@@ -1,37 +1,42 @@
package com.vnidrop.app
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vnidrop.app.core.rememberFileSystemService
import com.vnidrop.app.notifications.JvmLocalNotificationService
import java.net.NetworkInterface
class JVMPlatform : Platform {
override val name: String = "Java ${System.getProperty("java.version")}"
override val defaultCoreDataDir: String =
System.getProperty("user.home") + "/.vnidrop"
override val defaultReceiveDir: String =
System.getProperty("user.home") + "/Downloads"
override val deviceInfo: DeviceInfo = DeviceInfo(
deviceName = System.getenv("COMPUTERNAME")
?: System.getenv("HOSTNAME")
?: System.getProperty("user.name"),
deviceModel = listOfNotNull(
System.getProperty("os.arch"),
System.getProperty("java.vm.name"),
).joinToString(" | ").ifBlank { null },
operatingSystem = listOfNotNull(
System.getProperty("os.name"),
System.getProperty("os.version"),
).joinToString(" ").ifBlank { name },
network = activeNetworkSummary(),
@Composable
fun rememberJvmAppDependencies(): AppDependencies {
val fileSystemService = rememberFileSystemService()
return remember(fileSystemService) {
AppDependencies(
environment = PlatformEnvironment(
name = "Java ${System.getProperty("java.version")}",
appVersion = AppDependencies::class.java.`package`.implementationVersion ?: "0.1.0",
defaultCoreDataDir = System.getProperty("user.home") + "/.vnidrop",
defaultUsername = System.getenv("COMPUTERNAME") ?: System.getenv("HOSTNAME") ?: System.getProperty("user.name") ?: "Receiver",
),
deviceInfoProvider = JvmDeviceInfoProvider,
fileSystemService = fileSystemService,
localNotificationService = JvmLocalNotificationService(),
)
}
}
private object JvmDeviceInfoProvider : DeviceInfoProvider {
override suspend fun load(): DeviceInfo = DeviceInfo(
deviceName = System.getenv("COMPUTERNAME") ?: System.getenv("HOSTNAME") ?: System.getProperty("user.name"),
deviceModel = listOfNotNull(System.getProperty("os.arch"), System.getProperty("java.vm.name"))
.joinToString(" | ").ifBlank { null },
operatingSystem = listOfNotNull(System.getProperty("os.name"), System.getProperty("os.version"))
.joinToString(" ").ifBlank { "Java ${System.getProperty("java.version")}" },
network = runCatching {
NetworkInterface.getNetworkInterfaces().asSequence()
.filter { it.isUp && !it.isLoopback }
.map { it.displayName }
.firstOrNull()
}.getOrNull(),
batteryLevel = null,
)
}
actual fun getPlatform(): Platform = JVMPlatform()
private fun activeNetworkSummary(): String? =
runCatching {
NetworkInterface.getNetworkInterfaces()
.asSequence()
.filter { it.isUp && !it.isLoopback }
.map { it.displayName }
.firstOrNull()
}.getOrNull()

View File

@@ -44,15 +44,6 @@ actual fun rememberReceiveFolderPicker(
}
}
actual suspend fun sharePickedFile(
repository: CoreRepository,
file: PickedShareFile,
transferName: String,
senderName: String,
) {
repository.sharePath(file.value, transferName, senderName)
}
private fun openPicker(
onError: (String) -> Unit,
block: () -> Unit,

View File

@@ -27,4 +27,11 @@ private class JvmFileSystemService : FileSystemService {
}
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSink? = null
override suspend fun sharePickedFile(
repository: CoreGateway,
file: PickedShareFile,
transferName: String,
senderName: String,
): Result<Share> = repository.sharePath(file.value, transferName, senderName)
}

View File

@@ -0,0 +1,63 @@
package com.vnidrop.app.notifications
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.awt.Color
import java.awt.Frame
import java.awt.Graphics2D
import java.awt.SystemTray
import java.awt.TrayIcon
import java.awt.image.BufferedImage
class JvmLocalNotificationService : LocalNotificationService {
private val _permission = MutableStateFlow(
if (SystemTray.isSupported()) NotificationPermission.Granted else NotificationPermission.Unsupported,
)
override val permission: StateFlow<NotificationPermission> = _permission.asStateFlow()
private var trayIcon: TrayIcon? = null
override suspend fun refreshPermission(): NotificationPermission = permission.value
override suspend fun requestPermission(): NotificationPermission = permission.value
override suspend fun openSettings(): Result<Unit> = Result.failure(
UnsupportedOperationException("Notification settings are not available on this platform"),
)
override suspend fun publish(notification: LocalNotification): Result<Unit> = runCatching {
check(permission.value == NotificationPermission.Granted) { "System notifications are not supported" }
val icon = trayIcon ?: createTrayIcon().also {
SystemTray.getSystemTray().add(it)
trayIcon = it
}
icon.displayMessage(notification.title, notification.body, TrayIcon.MessageType.INFO)
}
override suspend fun cancel(id: String) = Unit
override suspend fun cancelAll() {
trayIcon?.let { SystemTray.getSystemTray().remove(it) }
trayIcon = null
}
private fun createTrayIcon(): TrayIcon {
val image = BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB)
val graphics = image.graphics as Graphics2D
try {
graphics.color = Color(83, 82, 237)
graphics.fillOval(2, 2, 28, 28)
graphics.color = Color.WHITE
graphics.fillRect(14, 8, 4, 16)
} finally {
graphics.dispose()
}
return TrayIcon(image, "VniDrop").apply {
isImageAutoSize = true
addActionListener {
Frame.getFrames().firstOrNull { it.isDisplayable }?.let { frame ->
frame.isVisible = true
frame.toFront()
}
}
}
}
}