Database reorganize, protocol version bump, migrations
This commit is contained in:
@@ -13,7 +13,10 @@ plugins {
|
||||
alias(libs.plugins.buildconfig)
|
||||
}
|
||||
|
||||
version = "1.0.1"
|
||||
version = rootProject.version as PhoebenchVersion
|
||||
|
||||
if((version as PhoebenchVersion) < (rootProject.version as PhoebenchVersion))
|
||||
throw GradleException("UI Clients version must be at least as high as protocol/common version")
|
||||
val versionDirectory = layout.buildDirectory.dir("generated/sources/version")
|
||||
|
||||
val generateVersion = tasks.register<Task>("clientVersion") {
|
||||
@@ -128,7 +131,7 @@ compose.desktop {
|
||||
nativeDistributions {
|
||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||
packageName = "com.jaytux.phoebench.clients"
|
||||
packageVersion = version.toString()
|
||||
packageVersion = (version as PhoebenchVersion).safeString()
|
||||
|
||||
jvmArgs += listOf("--enable-native-access=ALL-UNNAMED")
|
||||
includeAllModules = true
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.jaytux.phoebench.clients
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.draganddrop.DragAndDropEvent
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTransferData
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.serializer
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
interface IStore {
|
||||
@@ -26,4 +30,12 @@ expect suspend fun String.toClipEntry(): ClipEntry
|
||||
expect fun format(f: Float): String
|
||||
|
||||
@Composable
|
||||
expect fun ServerSelect(server: String, onUpdate: (String) -> Unit)
|
||||
expect fun ServerSelect(server: String, onUpdate: (String) -> Unit)
|
||||
|
||||
inline fun <reified T> drag(data: T) = drag(data, serializer<T>())
|
||||
|
||||
expect fun <T> drag(data: T, serializer: KSerializer<T>): DragAndDropTransferData
|
||||
|
||||
inline fun <reified T> drop(data: DragAndDropEvent): T? = drop(data, serializer<T>())
|
||||
|
||||
expect fun <T> drop(data: DragAndDropEvent, serializer: KSerializer<T>): T?
|
||||
@@ -85,8 +85,21 @@ inline fun <T, X : Comparable<X>> List<T>.insort(elem: T, crossinline sortBy: (T
|
||||
return toMutableList().apply { add(index, elem) }
|
||||
}
|
||||
|
||||
fun List<Float>.geomean(): Float {
|
||||
return (size.toFloat() / sumOf { 1.0 / it.toDouble() }).toFloat()
|
||||
inline fun <T, X : Comparable<X>> List<T>.insortIdx(elem: T, crossinline sortBy: (T) -> X): Pair<List<T>, Int?> {
|
||||
val insertionPoint = binarySearchBy(sortBy(elem), selector = sortBy)
|
||||
if(insertionPoint >= 0) return this to null
|
||||
val index = -insertionPoint - 1
|
||||
|
||||
return toMutableList().apply { add(index, elem) } to index
|
||||
}
|
||||
|
||||
inline fun <T> List<T>.replace(crossinline finder: (T) -> Boolean, crossinline update: (T) -> T): List<T> {
|
||||
val idx = indexOfFirst { finder(it) }
|
||||
if(idx == -1) return this
|
||||
|
||||
val res = ArrayList(this)
|
||||
res[idx] = update(res[idx])
|
||||
return res
|
||||
}
|
||||
|
||||
infix fun <T1, T2, T3> Pair<T1, T2>.app(t3: T3) = Triple(first, second, t3)
|
||||
@@ -122,4 +135,6 @@ fun <T1, T2, T3, T4> List<Tuple4<T1, T2, T3, T4>>.unzip(): Tuple4<List<T1>, List
|
||||
}
|
||||
|
||||
return l1 to l2 app l3 app l4
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> List<T>.removeAt(idx: Int): List<T> = subList(0, idx) + subList(idx + 1, size)
|
||||
@@ -17,6 +17,7 @@ import com.jaytux.phoebench.common.Either
|
||||
import com.jaytux.phoebench.common.HomeEvent
|
||||
import com.jaytux.phoebench.common.HomeResponse
|
||||
import com.jaytux.phoebench.common.InviteListResponse
|
||||
import com.jaytux.phoebench.common.Migrate0To1Request
|
||||
import com.jaytux.phoebench.common.Routes
|
||||
import com.jaytux.phoebench.common.UserListResponse
|
||||
import com.jaytux.phoebench.common.value
|
||||
@@ -40,11 +41,14 @@ class HomeVM(
|
||||
private val _users = mutableStateOf(listOf<UserListResponse.UserData>())
|
||||
private val _invites = mutableStateOf(listOf<InviteListResponse.Invite>())
|
||||
|
||||
private val _stepsNeeded = mutableStateOf(listOf<HomeResponse.NeededMigration>())
|
||||
|
||||
val username = _username.immutable()
|
||||
val isAdmin = _isAdmin.immutable()
|
||||
val projectLimit = _projectLimit.immutable()
|
||||
val ownProjects = _ownProjects.immutable()
|
||||
val publicProjects = _publicProjects.immutable()
|
||||
val stepsNeeded = _stepsNeeded.immutable()
|
||||
|
||||
val invites = _invites.immutable()
|
||||
val users = _users.immutable()
|
||||
@@ -81,17 +85,22 @@ class HomeVM(
|
||||
withScope {
|
||||
resetAdmin()
|
||||
_repo.getHome().snackOr {
|
||||
println("Received home response $it")
|
||||
_username.value = it.username
|
||||
_isAdmin.value = it.isAdmin
|
||||
_projectLimit.value = it.projectLimit
|
||||
_ownProjects.value = it.ownProjects.sortedBy { p -> p.name }
|
||||
_publicProjects.value = it.publicProjects.sortedBy { p -> p.name }
|
||||
refreshAdmin()
|
||||
refreshFrom(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshFrom(it: HomeResponse) {
|
||||
println("Received home response $it")
|
||||
_username.value = it.username
|
||||
_isAdmin.value = it.isAdmin
|
||||
_projectLimit.value = it.projectLimit
|
||||
_ownProjects.value = it.ownProjects.sortedBy { p -> p.name }
|
||||
_publicProjects.value = it.publicProjects.sortedBy { p -> p.name }
|
||||
_stepsNeeded.value = it.neededMigrations
|
||||
refreshAdmin()
|
||||
}
|
||||
|
||||
fun refreshAdmin() {
|
||||
if(_isAdmin.value) {
|
||||
_adminJob = withScope {
|
||||
@@ -212,4 +221,12 @@ class HomeVM(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun migrate(v0to1: List<Migrate0To1Request.MigrateProject>) {
|
||||
withScope {
|
||||
_repo.migrate0To1(v0to1).snackOr {
|
||||
refreshFrom(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.jaytux.phoebench.common.HomeResponse
|
||||
import com.jaytux.phoebench.common.InviteListResponse
|
||||
import com.jaytux.phoebench.common.InviteRequest
|
||||
import com.jaytux.phoebench.common.LogoutRequest
|
||||
import com.jaytux.phoebench.common.Migrate0To1Request
|
||||
import com.jaytux.phoebench.common.ProjectRequest
|
||||
import com.jaytux.phoebench.common.Routes
|
||||
import com.jaytux.phoebench.common.UserListResponse
|
||||
@@ -31,6 +32,8 @@ interface IHomeRepo {
|
||||
|
||||
suspend fun newProject(name: String, isPublic: Boolean): Either<ErrorResponse, Unit>
|
||||
|
||||
suspend fun migrate0To1(conversions: List<Migrate0To1Request.MigrateProject>): Either<ErrorResponse, HomeResponse>
|
||||
|
||||
companion object {
|
||||
class Default(private val _client: Client) : IHomeRepo {
|
||||
override suspend fun getHome(): Either<ErrorResponse, HomeResponse> =
|
||||
@@ -62,6 +65,9 @@ interface IHomeRepo {
|
||||
|
||||
override suspend fun newProject(name: String, isPublic: Boolean): Either<ErrorResponse, Unit> =
|
||||
_client.callRoute(Routes.Project.new, ProjectRequest(name, isPublic)).ignoreValue()
|
||||
|
||||
override suspend fun migrate0To1(conversions: List<Migrate0To1Request.MigrateProject>): Either<ErrorResponse, HomeResponse> =
|
||||
_client.callRoute(Routes.migrate0To1, Migrate0To1Request(conversions))
|
||||
}
|
||||
|
||||
fun default(client: Client) = Default(client)
|
||||
|
||||
+19
-3
@@ -4,10 +4,13 @@ import androidx.compose.ui.graphics.Color
|
||||
import com.jaytux.phoebench.clients.Client
|
||||
import com.jaytux.phoebench.clients.hexString
|
||||
import com.jaytux.phoebench.clients.ignoreValue
|
||||
import com.jaytux.phoebench.common.BenchmarkRequest
|
||||
import com.jaytux.phoebench.common.BenchmarkResponse
|
||||
import com.jaytux.phoebench.common.Either
|
||||
import com.jaytux.phoebench.common.EntryRequest
|
||||
import com.jaytux.phoebench.common.EntryResponse
|
||||
import com.jaytux.phoebench.common.ErrorResponse
|
||||
import com.jaytux.phoebench.common.PartialBenchmarkRequest
|
||||
import com.jaytux.phoebench.common.VersionRequest
|
||||
import com.jaytux.phoebench.common.VersionResponse
|
||||
import com.jaytux.phoebench.common.PartialProjectRequest
|
||||
@@ -23,11 +26,15 @@ interface IProjectRepo {
|
||||
suspend fun update(name: String? = null, isPublic: Boolean? = null): Either<ErrorResponse, Unit>
|
||||
suspend fun delete(): Either<ErrorResponse, Unit>
|
||||
|
||||
suspend fun newBenchmark(name: String): Either<ErrorResponse, BenchmarkResponse>
|
||||
suspend fun updateBenchmark(id: Uuid, name: String? = null): Either<ErrorResponse, Unit>
|
||||
suspend fun deleteBenchmark(id: Uuid): Either<ErrorResponse, Unit>
|
||||
|
||||
suspend fun newVersion(name: String, color: Color, meta: String): Either<ErrorResponse, VersionResponse>
|
||||
suspend fun updateVersion(id: Uuid, name: String? = null, color: Color? = null, meta: String? = null): Either<ErrorResponse, Unit>
|
||||
suspend fun deleteVersion(id: Uuid): Either<ErrorResponse, Unit>
|
||||
|
||||
suspend fun newEntry(label: Uuid, timestamp: Instant, warmups: List<Float>, measurements: List<Float>,
|
||||
suspend fun newEntry(benchmark: Uuid, label: Uuid, timestamp: Instant, warmups: List<Float>, measurements: List<Float>,
|
||||
unit: TimeUnit, input: String, hardware: String): Either<ErrorResponse, EntryResponse>
|
||||
suspend fun deleteEntry(id: Uuid): Either<ErrorResponse, Unit>
|
||||
|
||||
@@ -42,6 +49,15 @@ interface IProjectRepo {
|
||||
override suspend fun delete(): Either<ErrorResponse, Unit> =
|
||||
_client.callRoute(Routes.Project.delete, _projectId).ignoreValue()
|
||||
|
||||
override suspend fun newBenchmark(name: String): Either<ErrorResponse, BenchmarkResponse> =
|
||||
_client.callRoute(Routes.Benchmark.new, BenchmarkRequest(name, _projectId))
|
||||
|
||||
override suspend fun updateBenchmark(id: Uuid, name: String?): Either<ErrorResponse, Unit> =
|
||||
_client.callRoute(Routes.Benchmark.update, id to PartialBenchmarkRequest(name = name)).ignoreValue()
|
||||
|
||||
override suspend fun deleteBenchmark(id: Uuid): Either<ErrorResponse, Unit> =
|
||||
_client.callRoute(Routes.Benchmark.delete, id).ignoreValue()
|
||||
|
||||
override suspend fun newVersion(name: String, color: Color, meta: String): Either<ErrorResponse, VersionResponse> =
|
||||
_client.callRoute(Routes.Version.new, VersionRequest(name, color.hexString(), meta, _projectId))
|
||||
|
||||
@@ -51,10 +67,10 @@ interface IProjectRepo {
|
||||
override suspend fun deleteVersion(id: Uuid): Either<ErrorResponse, Unit> =
|
||||
_client.callRoute(Routes.Version.delete, id).ignoreValue()
|
||||
|
||||
override suspend fun newEntry(label: Uuid, timestamp: Instant, warmups: List<Float>,
|
||||
override suspend fun newEntry(benchmark: Uuid, label: Uuid, timestamp: Instant, warmups: List<Float>,
|
||||
measurements: List<Float>, unit: TimeUnit, input: String, hardware: String
|
||||
): Either<ErrorResponse, EntryResponse> =
|
||||
_client.callRoute(Routes.Entry.new, EntryRequest(label, timestamp, _projectId, warmups, measurements, unit, input, hardware))
|
||||
_client.callRoute(Routes.Entry.new, EntryRequest(label, timestamp, benchmark, warmups, measurements, unit, input, hardware))
|
||||
|
||||
override suspend fun deleteEntry(id: Uuid): Either<ErrorResponse, Unit> =
|
||||
_client.callRoute(Routes.Entry.delete, id).ignoreValue()
|
||||
|
||||
+96
-23
@@ -11,7 +11,10 @@ import com.jaytux.phoebench.clients.SnackProvider.Companion.snackOr
|
||||
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.replace
|
||||
import com.jaytux.phoebench.clients.withScope
|
||||
import com.jaytux.phoebench.common.BenchmarkResponse
|
||||
import com.jaytux.phoebench.common.EntryResponse
|
||||
import com.jaytux.phoebench.common.VersionResponse
|
||||
import com.jaytux.phoebench.common.ProjectEvent
|
||||
@@ -57,9 +60,20 @@ class ProjectVM(
|
||||
data class Entry(val id: Uuid, val label: Version, val timeStamp: Instant, val warmups: List<Float>,
|
||||
val measurements: List<Float>, val nativeUnit: TimeUnit, val input: String, val hardware: String) {
|
||||
companion object {
|
||||
fun fromResponse(it: EntryResponse, map: Map<Uuid, Version>) = Entry(
|
||||
it.id, map[it.labelId] ?: Version.invalid, it.timestamp,
|
||||
inline fun fromResponse(it: EntryResponse, map: Map<Uuid, Version>, addInput: (String) -> Unit, addHardware: (String) -> Unit) = Entry(
|
||||
it.id, map[it.versionId] ?: Version.invalid, it.timestamp,
|
||||
it.warmups, it.measurements, it.unit, it.input, it.hardware
|
||||
).also { addInput(it.input); addHardware(it.hardware) }
|
||||
}
|
||||
}
|
||||
|
||||
data class Benchmark(val id: Uuid, val name: String, val entries: List<Entry>) {
|
||||
fun add(entry: Entry) = copy(entries = entries.insort(entry) { it.timeStamp })
|
||||
fun remove(entry: Uuid) = copy(entries = entries.filter { it.id != entry })
|
||||
|
||||
companion object {
|
||||
inline fun fromResponse(it: BenchmarkResponse, map: Map<Uuid, Version>, addInput: (String) -> Unit, addHardware: (String) -> Unit) = Benchmark(
|
||||
it.id, it.name, it.entries.map { er -> Entry.fromResponse(er, map, addInput, addHardware) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -69,7 +83,8 @@ class ProjectVM(
|
||||
private val _public = mutableStateOf(false)
|
||||
private val _editable = mutableStateOf(false)
|
||||
private val _labels = mutableStateOf(mapOf<Uuid, Version>())
|
||||
private val _entries = mutableStateOf(listOf<Entry>())
|
||||
private val _benchmarks = mutableStateOf(listOf<Benchmark>())
|
||||
private val _currentBenchmark = mutableStateOf(0)
|
||||
private val _inputs = mutableStateOf(setOf<String>())
|
||||
private val _hardware = mutableStateOf(setOf<String>())
|
||||
|
||||
@@ -78,7 +93,8 @@ class ProjectVM(
|
||||
val public = _public.immutable()
|
||||
val editable = _editable.immutable()
|
||||
val versions = _labels.immutable()
|
||||
val entries = _entries.immutable()
|
||||
val benchmarks = _benchmarks.immutable()
|
||||
val currentBenchmark = _currentBenchmark.immutable()
|
||||
val inputs = _inputs.immutable()
|
||||
val hardware = _hardware.immutable()
|
||||
|
||||
@@ -102,14 +118,10 @@ class ProjectVM(
|
||||
_owner.value = it.owner.name
|
||||
_public.value = it.isPublic
|
||||
_editable.value = it.isEditable
|
||||
_labels.value = it.usedLabels.associate { l -> l.id to Version.fromResponse(l) }
|
||||
_labels.value = it.versions.associate { l -> l.id to Version.fromResponse(l) }
|
||||
val localInputs = mutableSetOf<String>()
|
||||
val localHardware = mutableSetOf<String>()
|
||||
_entries.value = it.entries.map { e ->
|
||||
localInputs += e.input
|
||||
localHardware += e.hardware
|
||||
Entry.fromResponse(e, _labels.value)
|
||||
}
|
||||
_benchmarks.value = it.benchmarks.map { b -> Benchmark.fromResponse(b, _labels.value, localInputs::add, localHardware::add) }
|
||||
_inputs.value = localInputs
|
||||
_hardware.value = localHardware
|
||||
}
|
||||
@@ -119,13 +131,12 @@ class ProjectVM(
|
||||
private fun handleProjectEvent(event: ProjectEvent) {
|
||||
when(event) {
|
||||
ProjectEvent.Deleted -> back()
|
||||
is ProjectEvent.EntryDeleted -> _entries.value = _entries.value.filter { it.id != event.id }
|
||||
is ProjectEvent.EntryDeleted -> _benchmarks.value = _benchmarks.value.replace({ it.id == event.benchmarkId }) { it.remove(event.id) }
|
||||
is ProjectEvent.VersionChanged -> _labels.value += (event.label.id to Version.fromResponse(event.label))
|
||||
is ProjectEvent.VersionDeleted -> _labels.value = _labels.value.filter { it.value.id != event.id }
|
||||
is ProjectEvent.NewEntry -> {
|
||||
_entries.value = _entries.value.insort(Entry.fromResponse(event.entry, _labels.value), Entry::timeStamp)
|
||||
_inputs.value += event.entry.input
|
||||
_hardware.value += event.entry.hardware
|
||||
_benchmarks.value = _benchmarks.value.replace({ it.id == event.entry.benchmarkId }) { it.add(Entry.fromResponse(event.entry, _labels.value,
|
||||
{ input -> _inputs.value += input }, { hw -> _hardware.value += hw })) }
|
||||
}
|
||||
is ProjectEvent.NewVersion -> _labels.value += (event.label.id to Version.fromResponse(event.label))
|
||||
is ProjectEvent.Updated -> {
|
||||
@@ -133,6 +144,23 @@ class ProjectVM(
|
||||
_owner.value = event.changes.owner.name
|
||||
_public.value = event.changes.isPublic
|
||||
}
|
||||
|
||||
is ProjectEvent.BenchmarkChanged -> _benchmarks.value = _benchmarks.value.replace({ it.id == event.benchmark.id }) { it.copy(name = event.benchmark.name) }
|
||||
is ProjectEvent.BenchmarkDeleted -> {
|
||||
val idx = _benchmarks.value.indexOfFirst { it.id == event.id }
|
||||
if(idx == _currentBenchmark.value) _currentBenchmark.value = 0
|
||||
else if(idx < _currentBenchmark.value) _currentBenchmark.value -= 1
|
||||
_benchmarks.value = _benchmarks.value.filter { it.id != event.id }
|
||||
}
|
||||
is ProjectEvent.NewBenchmark -> {
|
||||
val (updated, idx) = _benchmarks.value.insortIdx(
|
||||
Benchmark.fromResponse(event.benchmark, _labels.value, { i -> _inputs.value += i }, { hw -> _hardware.value += hw })
|
||||
) { it.name }
|
||||
if(idx != null) {
|
||||
if(idx < _currentBenchmark.value) _currentBenchmark.value += 1
|
||||
}
|
||||
_benchmarks.value = updated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,24 +211,69 @@ class ProjectVM(
|
||||
}
|
||||
}
|
||||
|
||||
fun mkEntry(label: Version, warmups: List<Float>, measurements: List<Float>, unit: TimeUnit, input: String, hardware: String) {
|
||||
fun mkBenchmark(name: String) {
|
||||
withScope {
|
||||
_repo.newEntry(label.id, Clock.System.now(), warmups, measurements, unit, input, hardware).snackOr {
|
||||
_repo.newBenchmark(name).snackOr { bench ->
|
||||
if(_job == null) {
|
||||
_entries.value += Entry.fromResponse(it, _labels.value)
|
||||
_inputs.value += input
|
||||
_hardware.value += hardware
|
||||
val (updated, idx) = _benchmarks.value.insortIdx(
|
||||
Benchmark.fromResponse(bench, _labels.value, { i -> _inputs.value += i }, { hw -> _hardware.value += hw })
|
||||
) { it.name }
|
||||
_currentBenchmark.value?.let { curr ->
|
||||
if(idx != null) {
|
||||
if(idx < curr) _currentBenchmark.value = curr + 1
|
||||
}
|
||||
}
|
||||
_benchmarks.value = updated
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteEntry(id: Uuid) {
|
||||
fun updateBenchmark(id: Uuid, name: String?) {
|
||||
withScope {
|
||||
_repo.deleteEntry(id).snackOr {
|
||||
if(_job == null)
|
||||
_entries.value = _entries.value.filter { it.id != id }
|
||||
val old = _benchmarks.value.find { it.id == id } ?: return@withScope
|
||||
_repo.updateVersion(id, name).snackOr {
|
||||
if(_job == null) {
|
||||
_benchmarks.value = _benchmarks.value.replace({ it.id == id }) { it.copy(name = name ?: old.name) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteBenchmark(id: Uuid) {
|
||||
withScope {
|
||||
_repo.deleteBenchmark(id).snackOr {
|
||||
if(_job == null) {
|
||||
val idx = _benchmarks.value.indexOfFirst { it.id == id }
|
||||
if(idx == _currentBenchmark.value) _currentBenchmark.value = 0
|
||||
else if(idx < _currentBenchmark.value) _currentBenchmark.value -= 1
|
||||
_benchmarks.value = _benchmarks.value.filter { it.id != id }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun mkEntry(label: Version, warmups: List<Float>, measurements: List<Float>, unit: TimeUnit, input: String, hardware: String) {
|
||||
withScope {
|
||||
val current = _benchmarks.value.getOrNull(_currentBenchmark.value) ?: return@withScope
|
||||
_repo.newEntry(current.id, label.id, Clock.System.now(), warmups, measurements, unit, input, hardware).snackOr { entry ->
|
||||
if(_job == null) {
|
||||
_benchmarks.value = _benchmarks.value.replace({ it.id == entry.benchmarkId }) { it.add(Entry.fromResponse(entry, _labels.value,
|
||||
{ input -> _inputs.value += input }, { hw -> _hardware.value += hw })) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun previousBenchmark() {
|
||||
_currentBenchmark.value = maxOf(0, minOf(_currentBenchmark.value - 1, _benchmarks.value.size - 1))
|
||||
}
|
||||
|
||||
fun nextBenchmark() {
|
||||
_currentBenchmark.value = maxOf(0, minOf(_currentBenchmark.value + 1, _benchmarks.value.size - 1))
|
||||
}
|
||||
|
||||
fun selectBenchmark(id: Uuid) {
|
||||
_currentBenchmark.value = maxOf(_benchmarks.value.indexOfFirst { it.id == id }, 0)
|
||||
}
|
||||
}
|
||||
@@ -320,7 +320,19 @@ fun ConfirmLogoutDialog(onCancel: () -> Unit, onLogout: (everywhere: Boolean) ->
|
||||
@Composable
|
||||
fun HomeView(onSelectProject: (Uuid) -> Unit) {
|
||||
val vm = viewModel { HomeVM() }
|
||||
val todo by vm.stepsNeeded
|
||||
|
||||
todo.firstOrNull()?.let {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
when (it) {
|
||||
is HomeResponse.NeededMigration.Migrate0To1 -> Migrate0To1View(vm, it)
|
||||
}
|
||||
}
|
||||
} ?: HomePanel(vm, onSelectProject)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomePanel(vm: HomeVM, onSelectProject: (Uuid) -> Unit) {
|
||||
val auth = AuthProvider.get()
|
||||
val username by vm.username
|
||||
val isAdmin by vm.isAdmin
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
package com.jaytux.phoebench.clients.ui
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.draganddrop.dragAndDropSource
|
||||
import androidx.compose.foundation.draganddrop.dragAndDropTarget
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draganddrop.DragAndDropEvent
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTarget
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.composables.icons.lucide.Cross
|
||||
import com.composables.icons.lucide.Lucide
|
||||
import com.composables.icons.lucide.Plus
|
||||
import com.composables.icons.lucide.X
|
||||
import com.jaytux.phoebench.clients.app
|
||||
import com.jaytux.phoebench.clients.data.HomeVM
|
||||
import com.jaytux.phoebench.clients.drag
|
||||
import com.jaytux.phoebench.clients.drop
|
||||
import com.jaytux.phoebench.clients.removeAt
|
||||
import com.jaytux.phoebench.clients.replace
|
||||
import com.jaytux.phoebench.common.HomeResponse
|
||||
import com.jaytux.phoebench.common.Migrate0To1Request
|
||||
|
||||
@Composable
|
||||
fun Migrate0To1View(vm: HomeVM, input: HomeResponse.NeededMigration.Migrate0To1) {
|
||||
var isAdding by remember { mutableStateOf(false) }
|
||||
|
||||
var projectsToDo by remember { mutableStateOf(input.projects) }
|
||||
var remapped by remember { mutableStateOf(listOf<Triple<String, Boolean, List<HomeResponse.NeededMigration.Migrate0To1.OldProject>>>()) }
|
||||
|
||||
Surface(Modifier.fillMaxSize(0.85f), color = MaterialTheme.colorScheme.surfaceVariant, tonalElevation = 10.dp, shape = MaterialTheme.shapes.large) {
|
||||
Column(Modifier.padding(15.dp)) {
|
||||
Text("Data Migration", style = MaterialTheme.typography.headlineMedium)
|
||||
Text("The server you connected to has been updated, requiring some decisions from you.")
|
||||
Text("This new version allows you to organise different benchmarks together in one project. That way, they will share installed versions.")
|
||||
Text("Please create the new projects you would like to use, and drag the existing projects onto them as benchmarks. You can also drag a benchmark onto an empty space to have it become a top-level project containing a benchmark with the same name.")
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
Row(Modifier.weight(1f)) {
|
||||
var showTargets by remember { mutableStateOf(false) }
|
||||
|
||||
val globalTarget = remember {
|
||||
object : DragAndDropTarget {
|
||||
override fun onStarted(event: DragAndDropEvent) {
|
||||
showTargets = true
|
||||
super.onStarted(event)
|
||||
}
|
||||
|
||||
override fun onEnded(event: DragAndDropEvent) {
|
||||
showTargets = false
|
||||
super.onEnded(event)
|
||||
}
|
||||
|
||||
override fun onDrop(event: DragAndDropEvent): Boolean {
|
||||
val got = drop<HomeResponse.NeededMigration.Migrate0To1.OldProject>(event) ?: return false
|
||||
|
||||
println("DROP ONTO global")
|
||||
projectsToDo = projectsToDo.filter { it.id != got.id }
|
||||
remapped += got.name to false app listOf(got)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
val startMod = if(showTargets) Modifier.border(BorderStroke(3.dp, MaterialTheme.colorScheme.onSurface)) else Modifier
|
||||
|
||||
Surface(startMod.weight(0.66f).fillMaxHeight().dragAndDropTarget({ true }, globalTarget), tonalElevation = 10.dp, shape = MaterialTheme.shapes.large) {
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
Text("New projects", style = MaterialTheme.typography.headlineSmall)
|
||||
|
||||
LazyVerticalGrid(GridCells.Adaptive(minSize = 350.dp), Modifier.weight(1f)) {
|
||||
itemsIndexed(remapped) { idx, (name, public, old) ->
|
||||
val target = remember {
|
||||
object : DragAndDropTarget {
|
||||
override fun onStarted(event: DragAndDropEvent) {
|
||||
showTargets = true
|
||||
super.onStarted(event)
|
||||
}
|
||||
|
||||
override fun onEnded(event: DragAndDropEvent) {
|
||||
showTargets = false
|
||||
super.onEnded(event)
|
||||
}
|
||||
|
||||
override fun onDrop(event: DragAndDropEvent): Boolean {
|
||||
val got = drop<HomeResponse.NeededMigration.Migrate0To1.OldProject>(event) ?: return false
|
||||
|
||||
println("DROP ONTO $name")
|
||||
projectsToDo = projectsToDo.filter { it.id != got.id }
|
||||
remapped = remapped.replace({ it.first == name }) { it.first to it.second app (it.third + got) }
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val toggle = { public: Boolean ->
|
||||
remapped = remapped.replace({ it.first == name }) { it.first to public app it.third }
|
||||
}
|
||||
|
||||
Surface(startMod.padding(5.dp).dragAndDropTarget({ true }, target), color = MaterialTheme.colorScheme.tertiaryContainer, tonalElevation = 4.dp, shadowElevation = 1.dp, shape = MaterialTheme.shapes.medium) {
|
||||
Row(Modifier.padding(5.dp).width(IntrinsicSize.Min)) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(name)
|
||||
SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) {
|
||||
SegmentedButton(!public, { toggle(false) }, SegmentedButtonDefaults.itemShape(0, 2)) { Text("Private") }
|
||||
SegmentedButton(public, { toggle(true) }, SegmentedButtonDefaults.itemShape(1, 2)) { Text("Public") }
|
||||
}
|
||||
Column(Modifier.padding(start = 5.dp)) {
|
||||
old.forEach { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
||||
}
|
||||
}
|
||||
|
||||
IconButton({ projectsToDo += old; remapped = remapped.removeAt(idx) }) {
|
||||
Icon(Lucide.X, "Remove")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(remapped.isNotEmpty()) {
|
||||
item {
|
||||
Surface(startMod.padding(5.dp).dragAndDropTarget({ true }, globalTarget), color = MaterialTheme.colorScheme.tertiaryContainer, tonalElevation = 4.dp, shadowElevation = 1.dp, shape = MaterialTheme.shapes.medium) {
|
||||
Text("Create new...", Modifier.padding(vertical = 15.dp), fontStyle = FontStyle.Italic, color = LocalContentColor.current.copy(alpha = 0.66f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
Row {
|
||||
Box(Modifier.weight(1f))
|
||||
IconButton({ isAdding = true }) {
|
||||
Icon(Lucide.Plus, "Add project")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(10.dp))
|
||||
|
||||
Surface(Modifier.weight(0.33f).fillMaxHeight(), tonalElevation = 10.dp, shape = MaterialTheme.shapes.large) {
|
||||
LazyColumn(Modifier.padding(10.dp).fillMaxHeight()) {
|
||||
stickyHeader {
|
||||
Text("Existing projects", style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
items(projectsToDo) {
|
||||
Column {
|
||||
Surface(Modifier.padding(5.dp).fillMaxWidth().dragAndDropSource { _ -> drag(it) }, tonalElevation = 15.dp, shape = MaterialTheme.shapes.medium) {
|
||||
Text("${it.name} (${it.versions.size} versions)", Modifier.padding(10.dp))
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
Row {
|
||||
Box(Modifier.weight(1f))
|
||||
|
||||
Button({ vm.migrate(remapped.map { (name, public, old) -> Migrate0To1Request.MigrateProject(name, public, old.map { it.id }) }) },
|
||||
enabled = projectsToDo.isEmpty()) {
|
||||
Text("Perform migration")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(isAdding) AddProjectDialog({ isAdding = false }) { name, public ->
|
||||
remapped += name to public app listOf()
|
||||
isAdding = false
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddProjectDialog(onCancel: () -> Unit, onAdd: (name: String, public: Boolean) -> Unit) {
|
||||
Dialog(onDismissRequest = onCancel) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
var public by remember { mutableStateOf(false) }
|
||||
|
||||
Surface(Modifier.padding(15.dp).widthIn(400.dp), shape = MaterialTheme.shapes.medium) {
|
||||
Column(Modifier.padding(8.dp).width(IntrinsicSize.Min)) {
|
||||
Text("Create new project", Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.headlineMedium)
|
||||
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text("Name") })
|
||||
Spacer(Modifier.height(10.dp))
|
||||
SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) {
|
||||
SegmentedButton(!public, { public = false }, SegmentedButtonDefaults.itemShape(0, 2)) { Text("Private") }
|
||||
SegmentedButton(public, { public = true }, SegmentedButtonDefaults.itemShape(1, 2)) { Text("Public") }
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
CancelConfirmXRow(onCancel, {
|
||||
onAdd(name.trim(), public)
|
||||
}, confirmText = "Create", canConfirm = name.trim().isNotBlank())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
-19
@@ -50,13 +50,13 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
||||
val owner by vm.owner
|
||||
val public by vm.public
|
||||
val editable by vm.editable
|
||||
val labels by vm.versions
|
||||
val versions by vm.versions
|
||||
val benchmarks by vm.benchmarks
|
||||
val currentBenchmark by vm.currentBenchmark
|
||||
|
||||
var editing by remember { mutableStateOf(false) }
|
||||
var deleting by remember { mutableStateOf(false) }
|
||||
var addOpen by remember { mutableStateOf(false) }
|
||||
var addingLabel by remember { mutableStateOf(false) }
|
||||
var addingData by remember { mutableStateOf(false) }
|
||||
var addingBenchmark by remember { mutableStateOf(false) }
|
||||
|
||||
Column(Modifier.padding(all = 15.dp)) {
|
||||
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -73,9 +73,79 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
||||
}
|
||||
owner?.let { Text("${if(public) "Public" else "Private"} project by user $it") }
|
||||
Spacer(Modifier.height(15.dp))
|
||||
Surface(Modifier.fillMaxSize().padding(8.dp), tonalElevation = 10.dp, shape = MaterialTheme.shapes.medium) {
|
||||
benchmarks.getOrNull(currentBenchmark)?.let { curr ->
|
||||
ProjectPanel(vm, editable, versions, benchmarks, currentBenchmark, curr) { addingBenchmark = true }
|
||||
} ?: run {
|
||||
Surface(Modifier.padding(15.dp), shape = MaterialTheme.shapes.medium) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Text("No benchmarks yet...", Modifier.align(Alignment.Center), fontStyle = FontStyle.Italic)
|
||||
|
||||
if(addingBenchmark) {
|
||||
CompositionLocalProvider(LocalIndication provides NoFeedbackIndication()) {
|
||||
Box(Modifier.clickable { addingBenchmark = false }.fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.align(Alignment.BottomEnd).padding(25.dp)) {
|
||||
IconButton({ addingBenchmark = true }, Modifier.scale(1.5f), shape = MaterialTheme.shapes.medium) {
|
||||
Icon(Lucide.Plus, "Add benchmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(editing) ProjectEditDialog(name ?: "Unnamed Project", public, { editing = false }) { name, public ->
|
||||
vm.update(name, public)
|
||||
}
|
||||
|
||||
if(deleting) ConfirmDeleteProjectDialog(name ?: "Unnamed Project", { deleting = false }) {
|
||||
vm.delete()
|
||||
}
|
||||
|
||||
if(addingBenchmark) AddBenchmarkDialog({ addingBenchmark = false }) {
|
||||
vm.mkBenchmark(it)
|
||||
addingBenchmark = false
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProjectPanel(vm: ProjectVM, editable: Boolean, versions: Map<Uuid, ProjectVM.Version>, benchmarks: List<ProjectVM.Benchmark>, currentIndex: Int, currentBenchmark: ProjectVM.Benchmark, onAddBenchmark: () -> Unit) {
|
||||
var addOpen by remember { mutableStateOf(false) }
|
||||
var addingLabel by remember { mutableStateOf(false) }
|
||||
var addingData by remember { mutableStateOf(false) }
|
||||
|
||||
Surface(Modifier.fillMaxSize().padding(8.dp), tonalElevation = 10.dp, shape = MaterialTheme.shapes.medium) {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
Surface(Modifier.fillMaxWidth(), tonalElevation = 10.dp, shape = MaterialTheme.shapes.medium) {
|
||||
Row(Modifier.padding(15.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton({ vm.previousBenchmark() }, enabled = currentIndex > 0) {
|
||||
Icon(Lucide.ChevronLeft, "Previous benchmark")
|
||||
}
|
||||
|
||||
Box(Modifier.weight(1f)) {
|
||||
var isOpen by remember { mutableStateOf(false) }
|
||||
Row(Modifier.clickable { isOpen = !isOpen }, verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(currentBenchmark.name, style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Icon(Lucide.ChevronDown, "Select benchmark...")
|
||||
}
|
||||
|
||||
DropdownMenu(isOpen, { isOpen = false }) {
|
||||
benchmarks.forEach { b ->
|
||||
DropdownMenuItem({ Text(b.name, style = MaterialTheme.typography.headlineSmall) }, { vm.selectBenchmark(b.id); isOpen = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton({ vm.nextBenchmark() }, enabled = currentIndex < benchmarks.size - 1) {
|
||||
Icon(Lucide.ChevronRight, "Previous benchmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.fillMaxSize().padding(15.dp)) {
|
||||
ProjectPlotArea(vm)
|
||||
ProjectPlotArea(vm, currentBenchmark)
|
||||
|
||||
if(editable) {
|
||||
if(addOpen) {
|
||||
@@ -88,9 +158,22 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
||||
if (addOpen) {
|
||||
var lblWidth by mutableStateOf(1000.dp)
|
||||
var dataWidth by mutableStateOf(1000.dp)
|
||||
var benchWidth by mutableStateOf(1000.dp)
|
||||
val density = LocalDensity.current
|
||||
|
||||
Column(Modifier.width(maxOf(lblWidth, dataWidth)), horizontalAlignment = Alignment.End) {
|
||||
Column(Modifier.width(maxOf(lblWidth, dataWidth, benchWidth)), horizontalAlignment = Alignment.End) {
|
||||
Surface(Modifier.onGloballyPositioned {
|
||||
benchWidth = with(density) { it.size.width.toDp() }
|
||||
}, shape = MaterialTheme.shapes.small, shadowElevation = 10.dp) {
|
||||
Row(Modifier.clickable(onClick = onAddBenchmark).padding(15.dp)) {
|
||||
Icon(Lucide.TestTubeDiagonal, "Add benchmark")
|
||||
Spacer(Modifier.width(5.dp))
|
||||
Text("Benchmark")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(15.dp))
|
||||
|
||||
Surface(Modifier.onGloballyPositioned {
|
||||
lblWidth = with(density) { it.size.width.toDp() }
|
||||
}, shape = MaterialTheme.shapes.small, shadowElevation = 10.dp) {
|
||||
@@ -126,14 +209,6 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
if(editing) ProjectEditDialog(name ?: "Unnamed Project", public, { editing = false }) { name, public ->
|
||||
vm.update(name, public)
|
||||
}
|
||||
|
||||
if(deleting) ConfirmDeleteProjectDialog(name ?: "Unnamed Project", { deleting = false }) {
|
||||
vm.delete()
|
||||
}
|
||||
|
||||
if(addingLabel) AddVersionDialog({ addingLabel = false; addOpen = false }) { name, color, meta ->
|
||||
vm.mkLabel(name, color, meta)
|
||||
addOpen = false
|
||||
@@ -142,7 +217,7 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
||||
val usedInputs by vm.inputs
|
||||
val usedHardware by vm.hardware
|
||||
|
||||
if(addingData) AddEntryDialog(labels, usedInputs, usedHardware,
|
||||
if(addingData) AddEntryDialog(versions, usedInputs, usedHardware,
|
||||
onCancel = { addingData = false; addOpen = false },
|
||||
onAddLbl = { name, lbl, meta -> vm.mkLabel(name, lbl, meta) }
|
||||
) { label, warmups, measurements, unit, input, hardware ->
|
||||
@@ -159,7 +234,7 @@ fun ProjectEditDialog(name: String, public: Boolean, onCancel: () -> Unit, onSav
|
||||
|
||||
Surface(Modifier.padding(15.dp), shape = MaterialTheme.shapes.medium) {
|
||||
Column(Modifier.padding(8.dp).width(IntrinsicSize.Min)) {
|
||||
Text("Create project", Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.headlineMedium)
|
||||
Text("Modify project", Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.headlineMedium)
|
||||
OutlinedTextField(updName, { updName = it }, Modifier.fillMaxWidth(), label = { Text("Name") })
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Row(Modifier.align(Alignment.End), verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -193,6 +268,25 @@ fun ConfirmDeleteProjectDialog(name: String, onCancel: () -> Unit, onDelete: ()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddBenchmarkDialog(onCancel: () -> Unit, onAdd: (name: String) -> Unit) {
|
||||
Dialog(onDismissRequest = onCancel) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
|
||||
Surface(Modifier.padding(15.dp).widthIn(400.dp), shape = MaterialTheme.shapes.medium) {
|
||||
Column(Modifier.padding(8.dp).width(IntrinsicSize.Min)) {
|
||||
Text("Create benchmark", Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.headlineMedium)
|
||||
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text("Name") })
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
CancelConfirmXRow(onCancel, {
|
||||
onAdd(name.trim())
|
||||
}, confirmText = "Create", canConfirm = name.trim().isNotBlank())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddVersionDialog(onCancel: () -> Unit, onAdd: (name: String, color: Color, meta: String) -> Unit) {
|
||||
Dialog(onDismissRequest = onCancel) {
|
||||
@@ -408,8 +502,8 @@ fun EditVersionDialog(current: ProjectVM.Version, onCancel: () -> Unit, onUpdate
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProjectPlotArea(vm: ProjectVM){
|
||||
val entries by vm.entries
|
||||
fun ProjectPlotArea(vm: ProjectVM, benchmark: ProjectVM.Benchmark) {
|
||||
val entries = benchmark.entries
|
||||
val versions by vm.versions
|
||||
val editable by vm.editable
|
||||
val allInputs by vm.inputs
|
||||
|
||||
+20
-1
@@ -2,6 +2,11 @@ package com.jaytux.phoebench.clients
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.draganddrop.DragAndDropEvent
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTransferAction
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTransferData
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTransferable
|
||||
import androidx.compose.ui.draganddrop.awtTransferable
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import com.github.javakeyring.Keyring
|
||||
import com.jaytux.phoebench.clients.ui.DefaultServerSelect
|
||||
@@ -10,6 +15,7 @@ import io.ktor.client.engine.okhttp.*
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.serializer
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@@ -62,4 +68,17 @@ actual suspend fun String.toClipEntry(): ClipEntry =
|
||||
actual fun format(f: Float): String = String.format("%.03f", f)
|
||||
|
||||
@Composable
|
||||
actual fun ServerSelect(server: String, onUpdate: (String) -> Unit) = DefaultServerSelect(server, onUpdate)
|
||||
actual fun ServerSelect(server: String, onUpdate: (String) -> Unit) = DefaultServerSelect(server, onUpdate)
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
actual fun <T> drag(data: T, serializer: KSerializer<T>): DragAndDropTransferData =
|
||||
DragAndDropTransferData(
|
||||
DragAndDropTransferable(StringSelection(Json.encodeToString(serializer, data))),
|
||||
supportedActions = listOf(DragAndDropTransferAction.Move)
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
actual fun <T> drop(data: DragAndDropEvent, serializer: KSerializer<T>): T? =
|
||||
if(data.awtTransferable.isDataFlavorSupported(DataFlavor.stringFlavor))
|
||||
Json.decodeFromString(serializer, data.awtTransferable.getTransferData(DataFlavor.stringFlavor) as String)
|
||||
else null
|
||||
+22
-1
@@ -1,6 +1,10 @@
|
||||
package com.jaytux.phoebench.clients
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.draganddrop.DragAndDropEvent
|
||||
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.ktor.client.*
|
||||
@@ -10,8 +14,10 @@ import kotlinx.coroutines.await
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.serializer
|
||||
import org.w3c.dom.DataTransfer
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
object WasmJsStore : IStore {
|
||||
@@ -74,4 +80,19 @@ lateinit var config: Config
|
||||
@Composable
|
||||
actual fun ServerSelect(server: String, onUpdate: (String) -> Unit) {
|
||||
constantServerSelect(config.server)(server, onUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalWasmJsInterop::class)
|
||||
@JsFun("() => new DataTransfer()")
|
||||
private external fun createDataTransfer(): DataTransfer
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
actual fun <T> drag(data: T, serializer: KSerializer<T>): DragAndDropTransferData = DragAndDropTransferData(
|
||||
createDataTransfer().also { it.setData("application/json", Json.encodeToString(serializer, data)) }
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
actual fun <T> drop(data: DragAndDropEvent, serializer: KSerializer<T>): T? =
|
||||
data.transferData?.domDataTransferOrNull?.getData("application/json")?.let {
|
||||
Json.decodeFromString(serializer, it)
|
||||
}
|
||||
Reference in New Issue
Block a user