diff --git a/clients/compose/build.gradle.kts b/clients/compose/build.gradle.kts index fe10b7f..475c289 100644 --- a/clients/compose/build.gradle.kts +++ b/clients/compose/build.gradle.kts @@ -13,7 +13,7 @@ plugins { alias(libs.plugins.buildconfig) } -version = PhoebenchVersion(1, 2, 1, "autoscroll") +version = PhoebenchVersion(1, 2, 2, "csv-export") if((version as PhoebenchVersion) < (rootProject.version as PhoebenchVersion)) throw GradleException("UI Clients version must be at least as high as protocol/common version") @@ -96,6 +96,7 @@ kotlin { implementation(libs.lucide) implementation(libs.koala) implementation(libs.kolor) + implementation(libs.filekit) } desktopMain.dependencies { implementation(compose.desktop.currentOs) diff --git a/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/PlatformAPI.kt b/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/PlatformAPI.kt index 24597b5..5a55c16 100644 --- a/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/PlatformAPI.kt +++ b/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/PlatformAPI.kt @@ -37,4 +37,6 @@ expect fun drag(data: T, serializer: KSerializer): DragAndDropTransferDat inline fun drop(data: DragAndDropEvent): T? = drop(data, serializer()) -expect fun drop(data: DragAndDropEvent, serializer: KSerializer): T? \ No newline at end of file +expect fun drop(data: DragAndDropEvent, serializer: KSerializer): T? + +expect suspend fun onDownload(contents: String, nameHint: String, extension: String) \ No newline at end of file diff --git a/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/data/ProjectVM.kt b/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/data/ProjectVM.kt index 5ce55af..710c4ee 100644 --- a/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/data/ProjectVM.kt +++ b/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/data/ProjectVM.kt @@ -1,6 +1,5 @@ package com.jaytux.phoebench.clients.data -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel @@ -13,6 +12,7 @@ import com.jaytux.phoebench.clients.hexString import com.jaytux.phoebench.clients.immutable import com.jaytux.phoebench.clients.insort import com.jaytux.phoebench.clients.insortIdx +import com.jaytux.phoebench.clients.onDownload import com.jaytux.phoebench.clients.replace import com.jaytux.phoebench.clients.withScope import com.jaytux.phoebench.common.BenchmarkResponse @@ -21,7 +21,7 @@ import com.jaytux.phoebench.common.VersionResponse import com.jaytux.phoebench.common.ProjectEvent import com.jaytux.phoebench.common.ServerMonitorEvent import com.jaytux.phoebench.common.TimeUnit -import com.jaytux.phoebench.common.fold +import io.ktor.http.quote import kotlinx.coroutines.Job import kotlin.time.Clock import kotlin.time.Instant @@ -322,4 +322,31 @@ class ProjectVM( fun clearMonitor() = withScope { _repo.deleteMonitorLogs().snackOnError() } + + fun triggerCsvDownload() = withScope { + data class CsvEntry(val id: Uuid, val bench: Uuid, val benchName: String, val runner: Uuid, val runnerName: String, val runnerHex: String, val input: String, val hardware: String, val timing: List) + + val benchmarks = _benchmarks.value.flatMap { bench -> + bench.entries.map { + CsvEntry( + id = it.id, bench = bench.id, benchName = bench.name, + runner = it.label.id, runnerName = it.label.name, runnerHex = it.label.colorStr, + input = it.input, hardware = it.hardware, timing = it.measurements + ) + } + } + val dataCount = benchmarks.maxOf { it.timing.size } + + val header = listOf("id", "benchmark-id", "benchmark-name", "runner-id", "runner-name", "runner-hex", "input", "hardware") + List(dataCount) { "entry-$it" } + val body = benchmarks.map { + listOf( + it.id.toHexDashString(), it.bench.toHexDashString(), it.benchName.quote(), + it.runner.toHexDashString(), it.runnerName.quote(), it.runnerHex, + it.input.quote(), it.hardware.quote() + ) + List(dataCount) { i -> it.timing.getOrNull(i)?.toString() ?: "" } + } + + val csv = "${header.joinToString(",")}\n${body.joinToString("\n"){ it.joinToString(",") }}" + onDownload(csv, name.value ?: "PhoeBench-project", "csv") + } } \ No newline at end of file diff --git a/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/ui/ProjectView.kt b/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/ui/ProjectView.kt index a198893..7372eb4 100644 --- a/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/ui/ProjectView.kt +++ b/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/ui/ProjectView.kt @@ -65,6 +65,7 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) { var editing by remember { mutableStateOf(false) } var deleting by remember { mutableStateOf(false) } var addingBenchmark by remember { mutableStateOf(false) } + var downloading by remember { mutableStateOf(false) } Column(Modifier.padding(all = 15.dp)) { Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) { @@ -80,8 +81,10 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) { IconButton({ vm.openMonitor() }) { Icon(Lucide.SquareTerminal, "Open monitor") } + IconButton({ downloading = true }) { + Icon(Lucide.FileDown, "Download CSV") + } } - } owner?.let { Text("${if(public) "Public" else "Private"} project by user $it") } Spacer(Modifier.height(15.dp)) @@ -121,6 +124,8 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) { addingBenchmark = false } + if(downloading) vm.triggerCsvDownload() + monitor?.let { MonitorDialog(name ?: "Unnamed Project", it, vm::closeMonitor, vm::clearMonitor) } } diff --git a/clients/compose/src/desktopMain/kotlin/com/jaytux/phoebench/clients/PlatformAPI.desktop.kt b/clients/compose/src/desktopMain/kotlin/com/jaytux/phoebench/clients/PlatformAPI.desktop.kt index 7486d2f..41c1c13 100644 --- a/clients/compose/src/desktopMain/kotlin/com/jaytux/phoebench/clients/PlatformAPI.desktop.kt +++ b/clients/compose/src/desktopMain/kotlin/com/jaytux/phoebench/clients/PlatformAPI.desktop.kt @@ -6,6 +6,10 @@ import androidx.compose.ui.draganddrop.* import androidx.compose.ui.platform.ClipEntry import com.github.javakeyring.Keyring import com.jaytux.phoebench.clients.ui.DefaultServerSelect +import io.github.vinceglb.filekit.FileKit +import io.github.vinceglb.filekit.dialogs.FileKitDialogException +import io.github.vinceglb.filekit.dialogs.openFileSaver +import io.github.vinceglb.filekit.writeString import io.ktor.client.* import io.ktor.client.engine.okhttp.* import kotlinx.serialization.KSerializer @@ -77,4 +81,14 @@ actual fun drag(data: T, serializer: KSerializer): DragAndDropTransferDat actual fun drop(data: DragAndDropEvent, serializer: KSerializer): T? = if(data.awtTransferable.isDataFlavorSupported(DataFlavor.stringFlavor)) Json.decodeFromString(serializer, data.awtTransferable.getTransferData(DataFlavor.stringFlavor) as String) - else null \ No newline at end of file + else null + +actual suspend fun onDownload(contents: String, nameHint: String, extension: String) { + try { + val file = FileKit.openFileSaver(suggestedName = nameHint, defaultExtension = extension) + file?.writeString(contents) + } + catch(e: FileKitDialogException) { + SnackProvider.get().send("Failed to save CSV: ${e.message}") + } +} \ No newline at end of file diff --git a/clients/compose/src/wasmJsMain/kotlin/com/jaytux/phoebench/clients/PlatformAPI.wasmJs.kt b/clients/compose/src/wasmJsMain/kotlin/com/jaytux/phoebench/clients/PlatformAPI.wasmJs.kt index 4853cd0..de378a4 100644 --- a/clients/compose/src/wasmJsMain/kotlin/com/jaytux/phoebench/clients/PlatformAPI.wasmJs.kt +++ b/clients/compose/src/wasmJsMain/kotlin/com/jaytux/phoebench/clients/PlatformAPI.wasmJs.kt @@ -7,6 +7,8 @@ import androidx.compose.ui.draganddrop.DragAndDropTransferData import androidx.compose.ui.draganddrop.domDataTransferOrNull import androidx.compose.ui.platform.ClipEntry import com.jaytux.phoebench.clients.ui.constantServerSelect +import io.github.vinceglb.filekit.FileKit +import io.github.vinceglb.filekit.download import io.ktor.client.* import io.ktor.client.engine.js.* import kotlinx.browser.window @@ -93,4 +95,8 @@ actual fun drag(data: T, serializer: KSerializer): DragAndDropTransferDat actual fun drop(data: DragAndDropEvent, serializer: KSerializer): T? = data.transferData?.domDataTransferOrNull?.getData("application/json")?.let { Json.decodeFromString(serializer, it) - } \ No newline at end of file + } + +actual suspend fun onDownload(contents: String, nameHint: String, extension: String) { + FileKit.download(contents.encodeToByteArray(), fileName = "$nameHint.$extension") +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a5e18c..3c27c57 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ shadow = "9.3.0" clikt = "5.0.3" buildconfig = "6.0.10" process = "1.5.1" +filekit = "0.17.0" [libraries] androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } @@ -91,6 +92,7 @@ java-keystore = { module = "com.github.javakeyring:java-keyring", version.ref = clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } process = { module = "com.github.pgreze:kotlin-process", version.ref = "process" } +filekit = { module = "io.github.vinceglb:filekit-dialogs-compose", version.ref = "filekit" } [plugins] composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }