From 09a2a318aad1da6f22e275f757c20ab8b8cc7079 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Fri, 6 Mar 2026 13:22:36 +0100 Subject: [PATCH 01/12] feat: Improve deeplink wip --- .../datastores-no-op/build.gradle.kts | 7 +- FloconAndroid/datastores/build.gradle.kts | 7 +- FloconAndroid/flocon-base/build.gradle.kts | 12 +-- .../io/github/openflocon/flocon/FloconApp.kt | 2 +- .../deeplinks/FloconDeeplinksPlugin.kt | 78 +++++++++++----- .../plugins/deeplinks/model/DeeplinkModel.kt | 19 +++- FloconAndroid/flocon-no-op/build.gradle.kts | 12 +-- FloconAndroid/flocon/build.gradle.kts | 12 +-- .../openflocon/flocon/core/FloconEncoder.kt | 11 +++ .../flocon/plugins/deeplinks/Mapping.kt | 40 +++++++-- .../deeplinks/model/DeeplinksRemote.kt | 36 ++++++-- FloconAndroid/gradle/libs.versions.toml | 8 +- .../grpc-interceptor-base/build.gradle.kts | 7 +- .../grpc-interceptor-lite/build.gradle.kts | 7 +- .../grpc/grpc-interceptor/build.gradle.kts | 7 +- .../xcschemes/xcschememanagement.plist | 5 ++ .../xcschemes/iosApp.xcscheme | 32 +++++++ .../xcschemes/xcschememanagement.plist | 14 +++ .../ktor-interceptor-no-op/build.gradle.kts | 12 +-- .../ktor-interceptor/build.gradle.kts | 12 +-- .../okhttp-interceptor-no-op/build.gradle.kts | 7 +- .../okhttp-interceptor/build.gradle.kts | 7 +- .../github/openflocon/flocon/okhttp/Utils.kt | 60 +++++++------ .../sample-android-only/build.gradle.kts | 7 +- .../deeplinks/InitializeDeeplinks.kt | 20 ++--- .../sample-multiplatform/build.gradle.kts | 20 +---- .../78.json | 88 ++++++++++++++++++- .../flocondesktop/common/db/AppDatabase.kt | 8 +- .../flocondesktop/common/db/RoomModule.kt | 3 + .../features/deeplinks/DeepLinkViewModel.kt | 9 +- .../features/deeplinks/mapper/Mapper.kt | 35 ++++++-- .../features/deeplinks/model/DeeplinkPart.kt | 10 ++- .../deeplinks/view/DeeplinkItemView.kt | 2 + .../datasource/DeeplinkLocalDataSource.kt | 9 +- .../datasource/DeeplinkRemoteDataSource.kt | 4 +- .../repository/DeeplinkRepositoryImpl.kt | 17 ++-- .../io/github/openflocon/data/local/DI.kt | 22 +++++ .../openflocon/data/local/deeplink/DI.kt | 6 +- .../deeplink/dao/FloconDeeplinkVariableDao.kt | 85 ++++++++++++++++++ .../datasource/LocalDeeplinkDataSourceRoom.kt | 60 +++++++++---- .../data/local/deeplink/mapper/Mapper.kt | 76 ++++++++++++---- .../local/deeplink/models/DeeplinkEntity.kt | 34 +++++-- .../deeplink/models/DeeplinkVariableEntity.kt | 35 ++++++++ .../DeeplinkRemoteDataSourceImpl.kt | 4 +- .../models/DeeplinkReceivedDataModel.kt | 31 ++++++- .../DeeplinkVariableReceivedDataModel.kt | 9 ++ .../models/DeeplinksReceivedDataModel.kt | 42 ++++++--- .../deeplink/models/DeeplinkDomainModel.kt | 18 +++- .../models/DeeplinkVariableDomainModel.kt | 6 ++ .../domain/deeplink/models/Deeplinks.kt | 6 ++ .../deeplink/repository/DeeplinkRepository.kt | 3 +- .../usecase/ExecuteDeeplinkUseCase.kt | 47 +++++----- .../ObserveCurrentDeviceDeeplinkUseCase.kt | 6 +- 53 files changed, 841 insertions(+), 295 deletions(-) create mode 100644 FloconAndroid/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/rteyssandier.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/rteyssandier.xcuserdatad/xcschemes/iosApp.xcscheme create mode 100644 FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/rteyssandier.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/dao/FloconDeeplinkVariableDao.kt create mode 100644 FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/models/DeeplinkVariableEntity.kt create mode 100644 FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinkVariableReceivedDataModel.kt create mode 100644 FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/DeeplinkVariableDomainModel.kt create mode 100644 FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/Deeplinks.kt diff --git a/FloconAndroid/datastores-no-op/build.gradle.kts b/FloconAndroid/datastores-no-op/build.gradle.kts index 31aafa279..ba7c2215c 100644 --- a/FloconAndroid/datastores-no-op/build.gradle.kts +++ b/FloconAndroid/datastores-no-op/build.gradle.kts @@ -26,11 +26,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } diff --git a/FloconAndroid/datastores/build.gradle.kts b/FloconAndroid/datastores/build.gradle.kts index 47a353b61..443e4f57c 100644 --- a/FloconAndroid/datastores/build.gradle.kts +++ b/FloconAndroid/datastores/build.gradle.kts @@ -26,11 +26,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } diff --git a/FloconAndroid/flocon-base/build.gradle.kts b/FloconAndroid/flocon-base/build.gradle.kts index d92d2d3e4..8e3f0bf2b 100644 --- a/FloconAndroid/flocon-base/build.gradle.kts +++ b/FloconAndroid/flocon-base/build.gradle.kts @@ -5,13 +5,7 @@ plugins { } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } + androidTarget() jvm() @@ -69,8 +63,8 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt index ccb26c119..2add1f3ba 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/FloconApp.kt @@ -35,7 +35,7 @@ abstract class FloconApp { val crashReporterPlugin: FloconCrashReporterPlugin } - open val client: FloconApp.Client? = null + open val client: Client? = null abstract val isInitialized : StateFlow diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt index 60d61a803..504a1869f 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt @@ -3,38 +3,76 @@ package io.github.openflocon.flocon.plugins.deeplinks import io.github.openflocon.flocon.FloconApp import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel -class ParameterBuilder { - val parameters: MutableMap = mutableMapOf() +class DeeplinkLinkBuilder internal constructor( + private val link: String +) { + private val parameters: MutableMap = mutableMapOf() + + var label: String? = null + var description: String? = null infix fun String.withAutoComplete(suggestions: List) { - parameters[this] = DeeplinkModel.Parameter(paramName = this, suggestions.distinct()) + parameters[this] = DeeplinkModel.Parameter.AutoComplete( + paramName = this, + suggestions.distinct() + ) } - fun build() : List { - return parameters.values.toList() + infix fun String.withVariable(variableName: String) { + parameters[this] = DeeplinkModel.Parameter.Variable( + paramName = this, + variableName = variableName + ) } + + fun build() = DeeplinkModel( + link = link, + label = label, + description = description, + parameters = parameters.values + .toList() + ) + } +class DeeplinkVariableBuilder internal constructor( + private val name: String +) { + var description: String? = null + + internal fun build(): DeeplinkVariable { + return DeeplinkVariable( + name = name, + description = description + ) + } + +} + +data class DeeplinkVariable( + val name: String, + val description: String? = null +) + class DeeplinkBuilder { + private val variables = mutableListOf() private val deeplinks = mutableListOf() - fun deeplink( - link: String, - label: String? = null, - description: String? = null, - parameters: (ParameterBuilder.() -> Unit)? = null, - ) { - deeplinks.add( - DeeplinkModel( - link = link, - label = label, - description = description, - parameters = parameters?.let { ParameterBuilder().apply(parameters).build() } ?: emptyList() - ) - ) + fun variable(name: String, block: DeeplinkVariableBuilder.() -> Unit = {}) { + val variable = DeeplinkVariableBuilder(name).apply(block) + .build() + + variables.add(variable) + } + + fun deeplink(link: String, block: DeeplinkLinkBuilder.() -> Unit = {}) { + val deeplink = DeeplinkLinkBuilder(link).apply(block) + .build() + + deeplinks.add(deeplink) } - fun build(): List { + internal fun build(): List { return deeplinks.toList() } } diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt index 6574aad82..29533742c 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinkModel.kt @@ -6,8 +6,19 @@ data class DeeplinkModel( val description: String? = null, val parameters: List, ) { - data class Parameter( - val paramName: String, - val autoComplete: List, - ) + + sealed interface Parameter { + val paramName: String + + data class AutoComplete( + override val paramName: String, + val autoComplete: List + ) : Parameter + + data class Variable( + override val paramName: String, + val variableName: String + ) : Parameter + + } } \ No newline at end of file diff --git a/FloconAndroid/flocon-no-op/build.gradle.kts b/FloconAndroid/flocon-no-op/build.gradle.kts index dedd7d824..fde9da46d 100644 --- a/FloconAndroid/flocon-no-op/build.gradle.kts +++ b/FloconAndroid/flocon-no-op/build.gradle.kts @@ -5,13 +5,7 @@ plugins { } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } + androidTarget() jvm() @@ -70,8 +64,8 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index fc071c989..6ce6b5b1c 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -7,13 +7,7 @@ plugins { } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } + androidTarget() jvm() @@ -106,8 +100,8 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt index ffb059d6c..68aab88d3 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/core/FloconEncoder.kt @@ -1,11 +1,22 @@ package io.github.openflocon.flocon.core +import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkParameterRemote import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass internal object FloconEncoder { val json = Json { ignoreUnknownKeys = true isLenient = true encodeDefaults = false + + serializersModule = SerializersModule { + polymorphic(DeeplinkParameterRemote::class) { + subclass(DeeplinkParameterRemote.AutoComplete::class) + subclass(DeeplinkParameterRemote.Variable::class) + } + } } } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt index dd57bb442..71f6f7973 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt @@ -4,22 +4,46 @@ import io.github.openflocon.flocon.core.FloconEncoder import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkModel import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkParameterRemote import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkRemote +import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinkVariableRemote import io.github.openflocon.flocon.plugins.deeplinks.model.DeeplinksRemote -import kotlinx.serialization.encodeToString -internal fun toDeeplinksJson(deeplinks: List): String { - val dto = DeeplinksRemote(deeplinks.map { it.toRemote() }) - return FloconEncoder.json.encodeToString(dto) +internal fun toDeeplinksJson( + deeplinks: List, + variables: List +): String { + val dto = DeeplinksRemote( + deeplinks = deeplinks.map(DeeplinkModel::toRemote), + variables = variables.map(DeeplinkVariable::toRemote) + ) + + return FloconEncoder.json + .encodeToString( + serializer = DeeplinksRemote.serializer(), + value = dto + ) } internal fun DeeplinkModel.toRemote(): DeeplinkRemote = DeeplinkRemote( label = label, link = link, description = description, - parameters = parameters.map { it.toRemote() } + parameters = parameters.map(DeeplinkModel.Parameter::toRemote) ) -internal fun DeeplinkModel.Parameter.toRemote(): DeeplinkParameterRemote = DeeplinkParameterRemote( - paramName = paramName, - autoComplete = autoComplete +internal fun DeeplinkVariable.toRemote(): DeeplinkVariableRemote = DeeplinkVariableRemote( + name = name, + description = description ) + +internal fun DeeplinkModel.Parameter.toRemote(): DeeplinkParameterRemote = when (this) { + is DeeplinkModel.Parameter.AutoComplete -> DeeplinkParameterRemote.AutoComplete( + name = paramName, + autoComplete = autoComplete + ) + + is DeeplinkModel.Parameter.Variable -> DeeplinkParameterRemote.Variable( + name = paramName, + variableName = variableName + ) +} + diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt index 06cd4cf38..aaff16098 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt @@ -1,22 +1,48 @@ +@file:OptIn(ExperimentalSerializationApi::class) + package io.github.openflocon.flocon.plugins.deeplinks.model +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator @Serializable -internal class DeeplinkParameterRemote( - val paramName: String, - val autoComplete: List, -) +@JsonClassDiscriminator("type") +internal sealed interface DeeplinkParameterRemote { + val name: String + + @Serializable + @SerialName("auto_complete") + class AutoComplete( + override val name: String, + val autoComplete: List + ) : DeeplinkParameterRemote + + @Serializable + @SerialName("variable") + data class Variable( + override val name: String, + val variableName: String + ) : DeeplinkParameterRemote +} @Serializable internal class DeeplinkRemote( val label: String? = null, val link: String, val description: String? = null, - val parameters: List, + val parameters: List +) + +@Serializable +internal data class DeeplinkVariableRemote( + val name: String, + val description: String? = null ) @Serializable internal class DeeplinksRemote( val deeplinks: List, + val variables: List ) diff --git a/FloconAndroid/gradle/libs.versions.toml b/FloconAndroid/gradle/libs.versions.toml index d68aa0315..6458cf276 100644 --- a/FloconAndroid/gradle/libs.versions.toml +++ b/FloconAndroid/gradle/libs.versions.toml @@ -4,15 +4,15 @@ apollo = "4.0.0" coilCompose = "3.2.0" compose = "1.9.0" datastorePreferences = "1.1.7" -kotlin = "2.1.0" +kotlin = "2.2.0" mavenPublish = "0.34.0" coreKtx = "1.16.0" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" kotlinxCoroutinesBom = "1.10.2" -kotlinxSerialization = "1.7.1" -ktor = "3.2.3" +kotlinxSerialization = "1.9.0" +ktor = "3.4.1" lifecycleRuntimeKtx = "2.9.1" activityCompose = "1.10.1" composeBom = "2025.06.01" @@ -26,7 +26,7 @@ grpc = "1.70.0" protobufPlugin = "0.9.5" grpcKotlin = "1.4.3" protobuf = "4.26.1" -ksp = "2.1.0-1.0.29" +ksp = "2.3.6" processPhoenix = "3.0.0" sqlite = "2.5.2" sqliteJdbc = "3.50.3.0" diff --git a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts index cc2faf5f2..81df01b2d 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts @@ -26,11 +26,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } diff --git a/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts index 250472452..2058eb760 100644 --- a/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts @@ -26,11 +26,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } diff --git a/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts index 698b2e7e7..517cedee0 100644 --- a/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts @@ -26,11 +26,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } diff --git a/FloconAndroid/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/rteyssandier.xcuserdatad/xcschemes/xcschememanagement.plist b/FloconAndroid/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/rteyssandier.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 000000000..ee3458dd7 --- /dev/null +++ b/FloconAndroid/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/rteyssandier.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/rteyssandier.xcuserdatad/xcschemes/iosApp.xcscheme b/FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/rteyssandier.xcuserdatad/xcschemes/iosApp.xcscheme new file mode 100644 index 000000000..6054ff55c --- /dev/null +++ b/FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/rteyssandier.xcuserdatad/xcschemes/iosApp.xcscheme @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/rteyssandier.xcuserdatad/xcschemes/xcschememanagement.plist b/FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/rteyssandier.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 000000000..fa59f97d5 --- /dev/null +++ b/FloconAndroid/iosApp/iosApp.xcodeproj/xcuserdata/rteyssandier.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + iosApp.xcscheme + + orderHint + 0 + + + + diff --git a/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts b/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts index 231a6dd58..5f3ea524e 100644 --- a/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts @@ -5,13 +5,7 @@ plugins { } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } + androidTarget() jvm() @@ -71,8 +65,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } diff --git a/FloconAndroid/ktor-interceptor/build.gradle.kts b/FloconAndroid/ktor-interceptor/build.gradle.kts index 0c849b57c..84c1c918c 100644 --- a/FloconAndroid/ktor-interceptor/build.gradle.kts +++ b/FloconAndroid/ktor-interceptor/build.gradle.kts @@ -5,13 +5,7 @@ plugins { } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } + androidTarget() jvm() @@ -74,8 +68,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } diff --git a/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts b/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts index 5fa0e7246..03e7d4d25 100644 --- a/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts @@ -26,11 +26,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } diff --git a/FloconAndroid/okhttp-interceptor/build.gradle.kts b/FloconAndroid/okhttp-interceptor/build.gradle.kts index a7657f35c..b4643a064 100644 --- a/FloconAndroid/okhttp-interceptor/build.gradle.kts +++ b/FloconAndroid/okhttp-interceptor/build.gradle.kts @@ -26,11 +26,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } diff --git a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Utils.kt b/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Utils.kt index 2ebeaa8d3..7d2d7f4e2 100644 --- a/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Utils.kt +++ b/FloconAndroid/okhttp-interceptor/src/main/kotlin/io/github/openflocon/flocon/okhttp/Utils.kt @@ -54,48 +54,52 @@ internal fun getHttpMessage(httpCode: Int): String { } } - internal fun MediaType?.charsetOrUtf8(): Charset = this?.charset() ?: Charsets.UTF_8 +// Try / catch fix crash internal fun extractResponseBodyInfo( response: Response, responseHeaders: Map ): Pair { - val responseBody = response.body ?: return null to null - - var bodyString: String? = null - var bodySize: Long? = null + return try { + val responseBody = response.body ?: return null to null - val source = responseBody.source() - source.request(Long.MAX_VALUE) // Buffer the entire body, otherwise we have an empty string + var bodyString: String? = null + var bodySize: Long? = null - var buffer = source.buffer - if (buffer.size <= 0L) { - // Do not attempt to read empty bodies, it would throw a EOFException in GzipSource - return "" to 0 - } + val source = responseBody.source() + source.request(Long.MAX_VALUE) // Buffer the entire body, otherwise we have an empty string - val charset = responseBody.contentType().charsetOrUtf8() - bodySize = buffer.size - if (responseHeaders.isGzipped()) { - GzipSource(buffer.clone()).use { gzippedResponseBody -> - buffer = Buffer() - buffer.writeAll(gzippedResponseBody) + var buffer = source.buffer + if (buffer.size <= 0L) { + // Do not attempt to read empty bodies, it would throw a EOFException in GzipSource + return "" to 0 } - bodyString = buffer.clone().readString(charset) - } else if (responseHeaders.isBrotli()) { - BrotliInputStream(buffer.clone().inputStream()).source().buffer().use { brotliResponseBody -> - buffer = Buffer() - buffer.writeAll(brotliResponseBody) + val charset = responseBody.contentType().charsetOrUtf8() + bodySize = buffer.size + if (responseHeaders.isGzipped()) { + GzipSource(buffer.clone()).use { gzippedResponseBody -> + buffer = Buffer() + buffer.writeAll(gzippedResponseBody) + } + + bodyString = buffer.clone().readString(charset) + } else if (responseHeaders.isBrotli()) { + BrotliInputStream(buffer.clone().inputStream()).source().buffer().use { brotliResponseBody -> + buffer = Buffer() + buffer.writeAll(brotliResponseBody) + } + + bodyString = buffer.clone().readString(charset) + } else { + bodyString = buffer.clone().readString(charset) } - bodyString = buffer.clone().readString(charset) - } else { - bodyString = buffer.clone().readString(charset) + bodyString to bodySize + } catch (_: Exception) { + null to null } - - return bodyString to bodySize } internal fun extractRequestBodyInfo( diff --git a/FloconAndroid/sample-android-only/build.gradle.kts b/FloconAndroid/sample-android-only/build.gradle.kts index 1d056d1c6..7c903e608 100644 --- a/FloconAndroid/sample-android-only/build.gradle.kts +++ b/FloconAndroid/sample-android-only/build.gradle.kts @@ -54,11 +54,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } buildFeatures { compose = true diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/deeplinks/InitializeDeeplinks.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/deeplinks/InitializeDeeplinks.kt index 8e7b635ef..f36c6b00a 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/deeplinks/InitializeDeeplinks.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/deeplinks/InitializeDeeplinks.kt @@ -5,19 +5,17 @@ import io.github.openflocon.flocon.plugins.deeplinks.deeplinks fun initializeDeeplinks() { Flocon.deeplinks { + variable("test_variable") deeplink("flocon://home") deeplink("flocon://test") - deeplink( - "flocon://user/[userId]", - label = "User", - parameters = { - "userId" withAutoComplete listOf("Florent", "David", "Guillaume") - } - ) - deeplink( - "flocon://post/[postId]?comment=[commentText]", - label = "Post", + deeplink("flocon://user/[userId]") { + label = "User" + "userId" withAutoComplete listOf("Florent", "David", "Guillaume") + } + deeplink("flocon://post/[postId]?comment=[commentText]") { + label = "Post" description = "Open a post and send a comment" - ) + "commentText" withVariable "test_variable" + } } } \ No newline at end of file diff --git a/FloconAndroid/sample-multiplatform/build.gradle.kts b/FloconAndroid/sample-multiplatform/build.gradle.kts index 46eafe40d..ea5cb46e1 100644 --- a/FloconAndroid/sample-multiplatform/build.gradle.kts +++ b/FloconAndroid/sample-multiplatform/build.gradle.kts @@ -9,21 +9,9 @@ plugins { } kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } + androidTarget() - jvm("desktop") { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } + jvm("desktop") listOf( iosX64(), @@ -148,8 +136,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } buildFeatures { diff --git a/FloconDesktop/composeApp/schemas/io.github.openflocon.flocondesktop.common.db.AppDatabase/78.json b/FloconDesktop/composeApp/schemas/io.github.openflocon.flocondesktop.common.db.AppDatabase/78.json index 0f5fbc706..089111fde 100644 --- a/FloconDesktop/composeApp/schemas/io.github.openflocon.flocondesktop.common.db.AppDatabase/78.json +++ b/FloconDesktop/composeApp/schemas/io.github.openflocon.flocondesktop.common.db.AppDatabase/78.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 78, - "identityHash": "f14b7328eecced6a5e4af2cccc035c0a", + "identityHash": "fadb38ec8474f4083b36f3e1e0e12e70", "entities": [ { "tableName": "FloconNetworkCallEntity", @@ -1049,6 +1049,90 @@ } ] }, + { + "tableName": "DeeplinkVariableEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT, `isHistory` INTEGER NOT NULL, FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "isHistory", + "columnName": "isHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_DeeplinkVariableEntity_deviceId_packageName", + "unique": false, + "columnNames": [ + "deviceId", + "packageName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DeeplinkVariableEntity_deviceId_packageName` ON `${TABLE_NAME}` (`deviceId`, `packageName`)" + }, + { + "name": "index_DeeplinkVariableEntity_deviceId_name", + "unique": true, + "columnNames": [ + "deviceId", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DeeplinkVariableEntity_deviceId_name` ON `${TABLE_NAME}` (`deviceId`, `name`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, { "tableName": "AnalyticsItemEntity", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` TEXT NOT NULL, `analyticsTableId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `appInstance` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `createdAtFormatted` TEXT NOT NULL, `eventName` TEXT NOT NULL, `propertiesColumnsNames` TEXT NOT NULL, `propertiesValues` TEXT NOT NULL, PRIMARY KEY(`itemId`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", @@ -1785,7 +1869,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f14b7328eecced6a5e4af2cccc035c0a')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fadb38ec8474f4083b36f3e1e0e12e70')" ] } } \ No newline at end of file diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt index cd253768b..1b122cad3 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt @@ -22,7 +22,9 @@ import io.github.openflocon.data.local.database.models.SuccessQueryEntity import io.github.openflocon.data.local.database.dao.DatabaseQueryLogDao import io.github.openflocon.data.local.database.models.DatabaseQueryLogEntity import io.github.openflocon.data.local.deeplink.dao.FloconDeeplinkDao +import io.github.openflocon.data.local.deeplink.dao.FloconDeeplinkVariableDao import io.github.openflocon.data.local.deeplink.models.DeeplinkEntity +import io.github.openflocon.data.local.deeplink.models.DeeplinkVariableEntity import io.github.openflocon.data.local.device.datasource.dao.DevicesDao import io.github.openflocon.data.local.device.datasource.model.DeviceAppEntity import io.github.openflocon.data.local.device.datasource.model.DeviceEntity @@ -64,6 +66,7 @@ import kotlinx.coroutines.Dispatchers SuccessQueryEntity::class, FavoriteQueryEntity::class, DeeplinkEntity::class, + DeeplinkVariableEntity::class, AnalyticsItemEntity::class, NetworkFilterEntity::class, NetworkSettingsEntity::class, @@ -74,8 +77,8 @@ import kotlinx.coroutines.Dispatchers DeviceAppEntity::class, DatabaseTableEntity::class, CrashReportEntity::class, - DatabaseQueryLogEntity::class, - ], + DatabaseQueryLogEntity::class + ] ) @TypeConverters( DashboardConverters::class, @@ -91,6 +94,7 @@ abstract class AppDatabase : RoomDatabase() { abstract val imageDao: FloconImageDao abstract val queryDao: QueryDao abstract val deeplinkDao: FloconDeeplinkDao + abstract val deeplinkVariableDao: FloconDeeplinkVariableDao abstract val analyticsDao: FloconAnalyticsDao abstract val networkFilterDao: NetworkFilterDao abstract val networkMocksDao: NetworkMocksDao diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/RoomModule.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/RoomModule.kt index 9cbc26c8e..a13d98aa7 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/RoomModule.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/RoomModule.kt @@ -31,6 +31,9 @@ val roomModule = single { get().deeplinkDao } + single { + get().deeplinkVariableDao + } single { get().analyticsDao } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt index 3bf5ec1ec..b25a94f92 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt @@ -38,7 +38,7 @@ class DeepLinkViewModel( ) .mapLatest { (deepLinks, history) -> mapToUi( - deepLinks = deepLinks, + deepLinks = deepLinks.deeplinks, history = history, ) } @@ -76,6 +76,7 @@ class DeepLinkViewModel( when (it) { is DeeplinkPart.Text -> it.value is DeeplinkPart.TextField -> values[it] ?: "" + is DeeplinkPart.Variable -> TODO() } } @@ -84,6 +85,12 @@ class DeepLinkViewModel( deeplinkId = viewState.deeplinkId, saveIntoHistory = viewState.deeplinkId == -1L || numberOfTextFields != 0 ) + .alsoFailure { + it.printStackTrace() + feedbackDisplayer.displayMessage( + message = "Error while sending deeplink" + ) + } } } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/mapper/Mapper.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/mapper/Mapper.kt index 6ac2ed183..081a18286 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/mapper/Mapper.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/mapper/Mapper.kt @@ -20,7 +20,10 @@ internal fun mapToUi( mapToUi(it.model, isHistory = it.isHistory) } -internal fun mapToUi(deepLink: DeeplinkDomainModel, isHistory: Boolean): DeeplinkViewState = DeeplinkViewState( +internal fun mapToUi( + deepLink: DeeplinkDomainModel, + isHistory: Boolean +): DeeplinkViewState = DeeplinkViewState( label = deepLink.label, description = deepLink.description, deeplinkId = deepLink.id, @@ -28,11 +31,17 @@ internal fun mapToUi(deepLink: DeeplinkDomainModel, isHistory: Boolean): Deeplin parts = if (isHistory) { listOf(DeeplinkPart.Text(deepLink.link)) } else { - parseDeeplinkString(deepLink.link, deepLink = deepLink) + parseDeeplinkString( + input = deepLink.link, + deepLink = deepLink + ) }, ) -internal fun parseDeeplinkString(input: String, deepLink: DeeplinkDomainModel): List { +internal fun parseDeeplinkString( + input: String, + deepLink: DeeplinkDomainModel +): List { val regex = "\\[([^\\[\\]]*)\\]".toRegex() // Regex pour trouver [quelquechose] val result = mutableListOf() var lastIndex = 0 @@ -50,12 +59,22 @@ internal fun parseDeeplinkString(input: String, deepLink: DeeplinkDomainModel): } // 2. Ajouter la partie "TextField" - result.add( - DeeplinkPart.TextField( - label = value, - autoComplete = deepLink.parameters.find { it.paramName == value }?.autoComplete + val parameter = deepLink.parameters.find { it.name == value } + + if (parameter != null) { + result.add( + when (parameter) { + is DeeplinkDomainModel.Parameter.AutoComplete -> DeeplinkPart.TextField( + label = value, + autoComplete = parameter.autoComplete + ) + + is DeeplinkDomainModel.Parameter.Variable -> DeeplinkPart.Variable( + value = value // TODO Change + ) + } ) - ) + } lastIndex = range.last + 1 } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkPart.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkPart.kt index c01a4596e..17355e639 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkPart.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkPart.kt @@ -4,12 +4,20 @@ import androidx.compose.runtime.Immutable @Immutable sealed interface DeeplinkPart { + @Immutable - data class Text(val value: String) : DeeplinkPart + data class Text( + val value: String + ) : DeeplinkPart @Immutable data class TextField( val label: String, val autoComplete: List?, ) : DeeplinkPart + + @Immutable + data class Variable( + val value: String + ) : DeeplinkPart } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkItemView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkItemView.kt index 6b382cc34..11ec111d9 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkItemView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkItemView.kt @@ -196,6 +196,8 @@ private fun TextFieldPart( ), ) } + + is DeeplinkPart.Variable -> TODO() } } diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/datasource/DeeplinkLocalDataSource.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/datasource/DeeplinkLocalDataSource.kt index e8b08d446..f1fc77643 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/datasource/DeeplinkLocalDataSource.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/datasource/DeeplinkLocalDataSource.kt @@ -1,14 +1,18 @@ package io.github.openflocon.data.core.deeplink.datasource import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel +import io.github.openflocon.domain.deeplink.models.Deeplinks import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel import kotlinx.coroutines.flow.Flow interface DeeplinkLocalDataSource { - suspend fun update(deviceIdAndPackageNameDomainModel: DeviceIdAndPackageNameDomainModel, deeplinks: List) + suspend fun update( + deviceIdAndPackageNameDomainModel: DeviceIdAndPackageNameDomainModel, + deeplinks: Deeplinks + ) - fun observe(deviceIdAndPackageNameDomainModel: DeviceIdAndPackageNameDomainModel): Flow> + fun observe(deviceIdAndPackageNameDomainModel: DeviceIdAndPackageNameDomainModel): Flow fun observeHistory(deviceIdAndPackageNameDomainModel: DeviceIdAndPackageNameDomainModel): Flow> @@ -26,4 +30,5 @@ interface DeeplinkLocalDataSource { deeplinkId: Long, deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel ): DeeplinkDomainModel? + } diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/datasource/DeeplinkRemoteDataSource.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/datasource/DeeplinkRemoteDataSource.kt index 5fcc161dd..c23f1de49 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/datasource/DeeplinkRemoteDataSource.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/datasource/DeeplinkRemoteDataSource.kt @@ -1,9 +1,9 @@ package io.github.openflocon.data.core.deeplink.datasource -import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel +import io.github.openflocon.domain.deeplink.models.Deeplinks import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel interface DeeplinkRemoteDataSource { - fun getItems(message: FloconIncomingMessageDomainModel): List + fun getItems(message: FloconIncomingMessageDomainModel): Deeplinks? } diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/repository/DeeplinkRepositoryImpl.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/repository/DeeplinkRepositoryImpl.kt index 778dfd4e6..8e8bc6d0b 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/repository/DeeplinkRepositoryImpl.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/deeplink/repository/DeeplinkRepositoryImpl.kt @@ -5,6 +5,7 @@ import io.github.openflocon.data.core.deeplink.datasource.DeeplinkRemoteDataSour import io.github.openflocon.domain.Protocol import io.github.openflocon.domain.common.DispatcherProvider import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel +import io.github.openflocon.domain.deeplink.models.Deeplinks import io.github.openflocon.domain.deeplink.repository.DeeplinkRepository import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel @@ -28,13 +29,13 @@ class DeeplinkRepositoryImpl( ) { when (message.method) { Protocol.FromDevice.Deeplink.Method.GetDeeplinks -> { - val items = remote.getItems(message) + val deeplinks = remote.getItems(message) ?: return - println(items.toString()) + println(deeplinks.toString()) localDeeplinkDataSource.update( deviceIdAndPackageNameDomainModel = deviceIdAndPackageName, - deeplinks = items + deeplinks = deeplinks ) } } @@ -47,8 +48,9 @@ class DeeplinkRepositoryImpl( // no op } - override fun observe(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): Flow> = localDeeplinkDataSource.observe(deviceIdAndPackageName) - .flowOn(dispatcherProvider.data) + override fun observe(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): Flow = + localDeeplinkDataSource.observe(deviceIdAndPackageName) + .flowOn(dispatcherProvider.data) override suspend fun getDeeplinkById( deeplinkId: Long, @@ -57,8 +59,9 @@ class DeeplinkRepositoryImpl( localDeeplinkDataSource.getDeeplinkById(deeplinkId, deviceIdAndPackageName) } - override fun observeHistory(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): Flow> = localDeeplinkDataSource.observeHistory(deviceIdAndPackageName) - .flowOn(dispatcherProvider.data) + override fun observeHistory(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): Flow> = + localDeeplinkDataSource.observeHistory(deviceIdAndPackageName) + .flowOn(dispatcherProvider.data) override suspend fun addToHistory( item: DeeplinkDomainModel, diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/DI.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/DI.kt index 585122602..a6b74ba5d 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/DI.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/DI.kt @@ -6,15 +6,37 @@ import io.github.openflocon.data.local.crashreporter.crashReporterLocalModule import io.github.openflocon.data.local.dashboard.dashboardModule import io.github.openflocon.data.local.database.databaseModule import io.github.openflocon.data.local.deeplink.deeplinkModule +import io.github.openflocon.data.local.deeplink.models.DeeplinkEntity import io.github.openflocon.data.local.device.deviceModule import io.github.openflocon.data.local.files.filesModule import io.github.openflocon.data.local.images.imagesModule import io.github.openflocon.data.local.network.networkModule import io.github.openflocon.data.local.sharedpreference.sharedPreferenceModule import io.github.openflocon.data.local.table.tableModule +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass +import org.koin.core.qualifier.named import org.koin.dsl.module +internal val JSON = named("local_json") + val dataLocalModule = module { + single(JSON) { + Json { + ignoreUnknownKeys = true + + serializersModule = SerializersModule { + polymorphic(DeeplinkEntity.Parameter::class) { + subclass(DeeplinkEntity.Parameter.AutoComplete::class) + subclass(DeeplinkEntity.Parameter.Variable::class) + + defaultDeserializer { DeeplinkEntity.Parameter.AutoComplete.serializer() } + } + } + } + } includes( adbModule, analyticsModule, diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/DI.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/DI.kt index 99b7cea8b..33534eab2 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/DI.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/DI.kt @@ -1,11 +1,13 @@ package io.github.openflocon.data.local.deeplink import io.github.openflocon.data.core.deeplink.datasource.DeeplinkLocalDataSource +import io.github.openflocon.data.local.JSON import io.github.openflocon.data.local.deeplink.datasource.LocalDeeplinkDataSourceRoom -import org.koin.core.module.dsl.singleOf import org.koin.dsl.bind import org.koin.dsl.module internal val deeplinkModule = module { - singleOf(::LocalDeeplinkDataSourceRoom) bind DeeplinkLocalDataSource::class + single { + LocalDeeplinkDataSourceRoom(deeplinkDao = get(), deeplinkVariableDao = get(), json = get(JSON)) + } bind DeeplinkLocalDataSource::class } diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/dao/FloconDeeplinkVariableDao.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/dao/FloconDeeplinkVariableDao.kt new file mode 100644 index 000000000..3fc6904ba --- /dev/null +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/dao/FloconDeeplinkVariableDao.kt @@ -0,0 +1,85 @@ +package io.github.openflocon.data.local.deeplink.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import io.github.openflocon.data.local.deeplink.models.DeeplinkVariableEntity +import io.github.openflocon.domain.device.models.DeviceId +import kotlinx.coroutines.flow.Flow + +@Dao +interface FloconDeeplinkVariableDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(deeplink: DeeplinkVariableEntity) + + @Query( + """ + DELETE FROM DeeplinkVariableEntity + WHERE deviceId = :deviceId + AND packageName = :packageName + AND isHistory = false + """, + ) + suspend fun deleteAll(deviceId: String, packageName: String) + + @Transaction + suspend fun updateAll( + deviceId: DeviceId, + packageName: String, + variables: List, + ) { + deleteAll(deviceId = deviceId, packageName = packageName) + variables.forEach { insert(deeplink = it) } + } + + @Query( + """ + SELECT * + FROM DeeplinkVariableEntity + WHERE deviceId = :deviceId + AND packageName = :packageName + AND isHistory = false + ORDER BY id ASC + """, + ) + fun observeAll(deviceId: String, packageName: String): Flow> + + @Query( + """ + SELECT * + FROM DeeplinkVariableEntity + WHERE deviceId = :deviceId + AND packageName = :packageName + AND isHistory = true + ORDER BY id DESC + """, + ) + fun observeHistory(deviceId: String, packageName: String): Flow> + + @Query( + """ + DELETE + FROM DeeplinkVariableEntity + WHERE deviceId = :deviceId + AND id = :deeplinkId + AND packageName = :packageName + AND isHistory = :isHistory + """, + ) + suspend fun delete(deviceId: String, packageName: String, deeplinkId: Long, isHistory: Boolean) + + @Query( + """ + SELECT * + FROM DeeplinkVariableEntity + WHERE deviceId = :deviceId + AND packageName = :packageName + AND id = :deeplinkId + LIMIT 1 + """, + ) + suspend fun getById(deviceId: String, packageName: String, deeplinkId: Long): DeeplinkVariableEntity? +} diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/datasource/LocalDeeplinkDataSourceRoom.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/datasource/LocalDeeplinkDataSourceRoom.kt index 6ec2a42b6..3bad7ecd2 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/datasource/LocalDeeplinkDataSourceRoom.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/datasource/LocalDeeplinkDataSourceRoom.kt @@ -2,50 +2,80 @@ package io.github.openflocon.data.local.deeplink.datasource import io.github.openflocon.data.core.deeplink.datasource.DeeplinkLocalDataSource import io.github.openflocon.data.local.deeplink.dao.FloconDeeplinkDao +import io.github.openflocon.data.local.deeplink.dao.FloconDeeplinkVariableDao import io.github.openflocon.data.local.deeplink.mapper.toDomainModel import io.github.openflocon.data.local.deeplink.mapper.toDomainModels import io.github.openflocon.data.local.deeplink.mapper.toEntities import io.github.openflocon.data.local.deeplink.mapper.toEntity import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel +import io.github.openflocon.domain.deeplink.models.Deeplinks import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEmpty import kotlinx.serialization.json.Json internal class LocalDeeplinkDataSourceRoom( private val deeplinkDao: FloconDeeplinkDao, + private val deeplinkVariableDao: FloconDeeplinkVariableDao, private val json: Json, ) : DeeplinkLocalDataSource { override suspend fun update( deviceIdAndPackageNameDomainModel: DeviceIdAndPackageNameDomainModel, - deeplinks: List + deeplinks: Deeplinks ) { deeplinkDao.updateAll( deviceId = deviceIdAndPackageNameDomainModel.deviceId, packageName = deviceIdAndPackageNameDomainModel.packageName, deeplinks = toEntities( - deeplinks = deeplinks, + deeplinks = deeplinks.deeplinks, deviceIdAndPackageName = deviceIdAndPackageNameDomainModel, json = json, - ), + ) + ) + deeplinkVariableDao.updateAll( + deviceId = deviceIdAndPackageNameDomainModel.deviceId, + packageName = deviceIdAndPackageNameDomainModel.packageName, + variables = toEntities( + variables = deeplinks.variables, + deviceIdAndPackageName = deviceIdAndPackageNameDomainModel + ) ) } - override fun observe(deviceIdAndPackageNameDomainModel: DeviceIdAndPackageNameDomainModel): Flow> = deeplinkDao.observeAll( - deviceId = deviceIdAndPackageNameDomainModel.deviceId, - packageName = deviceIdAndPackageNameDomainModel.packageName, - ) - .map { toDomainModels(it, json = json) } - .distinctUntilChanged() + override fun observe( + deviceIdAndPackageNameDomainModel: DeviceIdAndPackageNameDomainModel + ): Flow = combine( + deeplinkDao.observeAll( + deviceId = deviceIdAndPackageNameDomainModel.deviceId, + packageName = deviceIdAndPackageNameDomainModel.packageName, + ) + .map { toDomainModels(entities = it, json = json) } + .distinctUntilChanged(), + deeplinkVariableDao.observeAll( + deviceId = deviceIdAndPackageNameDomainModel.deviceId, + packageName = deviceIdAndPackageNameDomainModel.packageName, + ) + .map { toDomainModels(entities = it) } + .distinctUntilChanged() + ) { deeplinks, variables -> + Deeplinks( + deeplinks = deeplinks, + variables = variables + ) + } + .onEmpty { emit(Deeplinks(emptyList(), emptyList())) } - override fun observeHistory(deviceIdAndPackageNameDomainModel: DeviceIdAndPackageNameDomainModel): Flow> = deeplinkDao.observeHistory( - deviceId = deviceIdAndPackageNameDomainModel.deviceId, - packageName = deviceIdAndPackageNameDomainModel.packageName, - ) - .map { toDomainModels(it, json = json) } - .distinctUntilChanged() + override fun observeHistory(deviceIdAndPackageNameDomainModel: DeviceIdAndPackageNameDomainModel): Flow> = + deeplinkDao.observeHistory( + deviceId = deviceIdAndPackageNameDomainModel.deviceId, + packageName = deviceIdAndPackageNameDomainModel.packageName, + ) + .map { toDomainModels(it, json = json) } + .distinctUntilChanged() override suspend fun addToHistory( deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/mapper/Mapper.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/mapper/Mapper.kt index 0c2f7c10a..20b664b97 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/mapper/Mapper.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/mapper/Mapper.kt @@ -1,27 +1,31 @@ package io.github.openflocon.data.local.deeplink.mapper import io.github.openflocon.data.local.deeplink.models.DeeplinkEntity -import io.github.openflocon.data.local.deeplink.models.DeeplinkParameterEntity +import io.github.openflocon.data.local.deeplink.models.DeeplinkVariableEntity import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel +import io.github.openflocon.domain.deeplink.models.DeeplinkVariableDomainModel import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json fun DeeplinkEntity.toDomainModel( - json: Json, + json: Json ): DeeplinkDomainModel = DeeplinkDomainModel( label = this.label, link = this.link, description = this.description, id = this.id, parameters = try { - this.parametersAsJson?.let { - json.decodeFromString>(it) - }?.map { it.toDomain() } ?: emptyList() + json.decodeFromString>(parametersAsJson) + .map(DeeplinkEntity.Parameter::toDomain) } catch (t: Throwable) { t.printStackTrace() emptyList() - }, + } +) + +fun DeeplinkVariableEntity.toDomainModel(): DeeplinkVariableDomainModel = DeeplinkVariableDomainModel( + name = name, + description = description ) fun DeeplinkDomainModel.toEntity( @@ -48,26 +52,64 @@ fun DeeplinkDomainModel.toEntity( ) } -// Pour une liste +fun DeeplinkVariableDomainModel.toEntity( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + isHistory: Boolean +): DeeplinkVariableEntity = DeeplinkVariableEntity( + deviceId = deviceIdAndPackageName.deviceId, + name = name, + packageName = deviceIdAndPackageName.packageName, + description = description, + isHistory = isHistory +) + fun toDomainModels( entities: List, json: Json, ): List = entities.map { it.toDomainModel(json = json) } +fun toDomainModels( + entities: List +): List = entities.map(DeeplinkVariableEntity::toDomainModel) + fun toEntities( deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, deeplinks: List, json: Json, ): List = deeplinks.map { - it.toEntity(deviceIdAndPackageName = deviceIdAndPackageName, isHistory = false, json = json,) + it.toEntity(deviceIdAndPackageName = deviceIdAndPackageName, isHistory = false, json = json) } -private fun DeeplinkDomainModel.Parameter.toEntity() = DeeplinkParameterEntity( - paramName = paramName, - autoComplete = autoComplete, -) +fun toEntities( + deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel, + variables: List, +): List = variables.map { + it.toEntity( + deviceIdAndPackageName = deviceIdAndPackageName, + isHistory = false + ) +} -private fun DeeplinkParameterEntity.toDomain() = DeeplinkDomainModel.Parameter( - paramName = paramName, - autoComplete = autoComplete, -) +private fun DeeplinkDomainModel.Parameter.toEntity() = when (this) { + is DeeplinkDomainModel.Parameter.AutoComplete -> DeeplinkEntity.Parameter.AutoComplete( + name = name, + autoComplete = autoComplete + ) + + is DeeplinkDomainModel.Parameter.Variable -> DeeplinkEntity.Parameter.Variable( + name = name, + variableName = variableName + ) +} + +private fun DeeplinkEntity.Parameter.toDomain() = when (this) { + is DeeplinkEntity.Parameter.AutoComplete -> DeeplinkDomainModel.Parameter.AutoComplete( + name = name, + autoComplete = autoComplete + ) + + is DeeplinkEntity.Parameter.Variable -> DeeplinkDomainModel.Parameter.Variable( + name = name, + variableName = variableName + ) +} diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/models/DeeplinkEntity.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/models/DeeplinkEntity.kt index 796545986..fc5bf181e 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/models/DeeplinkEntity.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/models/DeeplinkEntity.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalSerializationApi::class) + package io.github.openflocon.data.local.deeplink.models import androidx.room.Entity @@ -5,7 +7,10 @@ import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey import io.github.openflocon.data.local.device.datasource.model.DeviceAppEntity +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator @Entity( indices = [ @@ -31,10 +36,27 @@ data class DeeplinkEntity( val description: String?, val parametersAsJson: String, val isHistory: Boolean, -) +) { + + @Serializable + @JsonClassDiscriminator("type") + sealed interface Parameter { + val name: String + + @Serializable + @SerialName("auto_complete") + data class AutoComplete( + override val name: String, + val autoComplete: List + ) : Parameter + + @Serializable + @SerialName("variable") + data class Variable( + override val name: String, + val variableName: String + ) : Parameter + + } +} -@Serializable -data class DeeplinkParameterEntity( - val paramName: String, - val autoComplete: List, -) diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/models/DeeplinkVariableEntity.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/models/DeeplinkVariableEntity.kt new file mode 100644 index 000000000..23a3c187c --- /dev/null +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/models/DeeplinkVariableEntity.kt @@ -0,0 +1,35 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package io.github.openflocon.data.local.deeplink.models + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import io.github.openflocon.data.local.device.datasource.model.DeviceAppEntity +import kotlinx.serialization.ExperimentalSerializationApi + +@Entity( + indices = [ + Index(value = ["deviceId", "packageName"]), + Index(value = ["deviceId", "name"], unique = true), + ], + foreignKeys = [ + ForeignKey( + entity = DeviceAppEntity::class, + parentColumns = ["deviceId", "packageName"], + childColumns = ["deviceId", "packageName"], + onDelete = ForeignKey.CASCADE + ) + ], +) +data class DeeplinkVariableEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val deviceId: String, + val packageName: String, + val name: String, + val description: String?, + val isHistory: Boolean +) + diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/datasource/DeeplinkRemoteDataSourceImpl.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/datasource/DeeplinkRemoteDataSourceImpl.kt index 5eef9e852..15bb1c564 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/datasource/DeeplinkRemoteDataSourceImpl.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/datasource/DeeplinkRemoteDataSourceImpl.kt @@ -5,6 +5,7 @@ import com.flocon.data.remote.deeplink.models.DeeplinksReceivedDataModel import com.flocon.data.remote.deeplink.models.toDomain import io.github.openflocon.data.core.deeplink.datasource.DeeplinkRemoteDataSource import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel +import io.github.openflocon.domain.deeplink.models.Deeplinks import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel import kotlinx.serialization.json.Json @@ -12,7 +13,6 @@ internal class DeeplinkRemoteDataSourceImpl( private val json: Json, ) : DeeplinkRemoteDataSource { - override fun getItems(message: FloconIncomingMessageDomainModel): List = json.safeDecodeFromString(message.body) + override fun getItems(message: FloconIncomingMessageDomainModel): Deeplinks? = json.safeDecodeFromString(message.body) ?.toDomain() - .orEmpty() } diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinkReceivedDataModel.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinkReceivedDataModel.kt index 6f9424d0e..53ceed265 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinkReceivedDataModel.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinkReceivedDataModel.kt @@ -1,6 +1,11 @@ +@file:OptIn(ExperimentalSerializationApi::class) + package com.flocon.data.remote.deeplink.models +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator @Serializable internal data class DeeplinkReceivedDataModel( @@ -9,9 +14,27 @@ internal data class DeeplinkReceivedDataModel( val description: String? = null, val parameters: List = emptyList(), ) { + @Serializable - data class Parameter( - val paramName: String, - val autoComplete: List, - ) + @JsonClassDiscriminator("type") + sealed interface Parameter { + val name: String + + @Serializable + @SerialName("auto_complete") + data class AutoComplete( + override val name: String, + val autoComplete: List + ) : Parameter + + @Serializable + @SerialName("variable") + data class Variable( + override val name: String, + val variableName: String + ) : Parameter + + } + + } diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinkVariableReceivedDataModel.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinkVariableReceivedDataModel.kt new file mode 100644 index 000000000..442325001 --- /dev/null +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinkVariableReceivedDataModel.kt @@ -0,0 +1,9 @@ +package com.flocon.data.remote.deeplink.models + +import kotlinx.serialization.Serializable + +@Serializable +internal data class DeeplinkVariableReceivedDataModel( + val name: String, + val description: String? = null +) diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinksReceivedDataModel.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinksReceivedDataModel.kt index 820b5e21a..1f5512ba5 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinksReceivedDataModel.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinksReceivedDataModel.kt @@ -1,24 +1,42 @@ package com.flocon.data.remote.deeplink.models import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel +import io.github.openflocon.domain.deeplink.models.DeeplinkVariableDomainModel +import io.github.openflocon.domain.deeplink.models.Deeplinks import kotlinx.serialization.Serializable @Serializable internal data class DeeplinksReceivedDataModel( val deeplinks: List, + val variables: List ) -internal fun DeeplinksReceivedDataModel.toDomain(): List = deeplinks.map { - DeeplinkDomainModel( - label = it.label, - link = it.link, - description = it.description, - parameters = it.parameters.map { - DeeplinkDomainModel.Parameter( - paramName = it.paramName, - autoComplete = it.autoComplete, - ) - }, - id = 0, // will be created by the DB later +internal fun DeeplinksReceivedDataModel.toDomain(): Deeplinks = Deeplinks( + deeplinks = deeplinks.map { + DeeplinkDomainModel( + label = it.label, + link = it.link, + description = it.description, + parameters = it.parameters.map(DeeplinkReceivedDataModel.Parameter::toDomain), + id = 0, // will be created by the DB later + ) + }, + variables = variables.map { + DeeplinkVariableDomainModel( + name = it.name, + description = it.description + ) + } +) + +private fun DeeplinkReceivedDataModel.Parameter.toDomain() = when (this) { + is DeeplinkReceivedDataModel.Parameter.AutoComplete -> DeeplinkDomainModel.Parameter.AutoComplete( + name = name, + autoComplete = autoComplete + ) + + is DeeplinkReceivedDataModel.Parameter.Variable -> DeeplinkDomainModel.Parameter.Variable( + name = name, + variableName = variableName ) } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/DeeplinkDomainModel.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/DeeplinkDomainModel.kt index fa20b78c2..b693e47ce 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/DeeplinkDomainModel.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/DeeplinkDomainModel.kt @@ -7,8 +7,18 @@ data class DeeplinkDomainModel( val description: String?, val parameters: List, ) { - data class Parameter( - val paramName: String, - val autoComplete: List, - ) + sealed interface Parameter { + val name: String + + data class AutoComplete( + override val name: String, + val autoComplete: List + ) : Parameter + + data class Variable( + override val name: String, + val variableName: String + ) : Parameter + + } } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/DeeplinkVariableDomainModel.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/DeeplinkVariableDomainModel.kt new file mode 100644 index 000000000..1339cb9c6 --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/DeeplinkVariableDomainModel.kt @@ -0,0 +1,6 @@ +package io.github.openflocon.domain.deeplink.models + +data class DeeplinkVariableDomainModel( + val name: String, + val description: String? +) diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/Deeplinks.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/Deeplinks.kt new file mode 100644 index 000000000..69f8f235c --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/Deeplinks.kt @@ -0,0 +1,6 @@ +package io.github.openflocon.domain.deeplink.models + +data class Deeplinks( + val deeplinks: List, + val variables: List +) diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/repository/DeeplinkRepository.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/repository/DeeplinkRepository.kt index 6b4765720..dc717446a 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/repository/DeeplinkRepository.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/repository/DeeplinkRepository.kt @@ -1,11 +1,12 @@ package io.github.openflocon.domain.deeplink.repository import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel +import io.github.openflocon.domain.deeplink.models.Deeplinks import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel import kotlinx.coroutines.flow.Flow interface DeeplinkRepository { - fun observe(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): Flow> + fun observe(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): Flow suspend fun getDeeplinkById(deeplinkId: Long, deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): DeeplinkDomainModel? fun observeHistory(deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel): Flow> suspend fun addToHistory(item: DeeplinkDomainModel, deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel) diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/usecase/ExecuteDeeplinkUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/usecase/ExecuteDeeplinkUseCase.kt index fdc5bb595..06ad9bbc9 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/usecase/ExecuteDeeplinkUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/usecase/ExecuteDeeplinkUseCase.kt @@ -2,6 +2,8 @@ package io.github.openflocon.domain.deeplink.usecase import io.github.openflocon.domain.adb.ExecuteAdbCommandUseCase import io.github.openflocon.domain.adb.model.AdbCommandTargetDomainModel +import io.github.openflocon.domain.common.Either +import io.github.openflocon.domain.common.Failure import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel import io.github.openflocon.domain.deeplink.repository.DeeplinkRepository import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase @@ -12,8 +14,8 @@ class ExecuteDeeplinkUseCase( private val addToDeeplinkHistoryUseCase: AddToDeeplinkHistoryUseCase, private val deeplinkRepository: DeeplinkRepository, ) { - suspend operator fun invoke(deeplink: String, deeplinkId: Long, saveIntoHistory: Boolean) { - val current = getCurrentDeviceIdAndPackageNameUseCase() ?: return + suspend operator fun invoke(deeplink: String, deeplinkId: Long, saveIntoHistory: Boolean): Either { + val current = getCurrentDeviceIdAndPackageNameUseCase() ?: return Failure(IllegalStateException("No device")) // must been done before executing the deeplink, because the new launch overrides the list of deeplinks in the DB val originalModel = if (deeplinkId == -1L) { @@ -22,31 +24,32 @@ class ExecuteDeeplinkUseCase( deeplinkRepository.getDeeplinkById(deeplinkId = deeplinkId, current) } - executeAdbCommandUseCase( + return executeAdbCommandUseCase( target = AdbCommandTargetDomainModel.Device(current.deviceId), command = "shell am start -W -a android.intent.action.VIEW -d \"$deeplink\" ${current.packageName}", - ).alsoSuccess { - if (saveIntoHistory) { - originalModel?.let { model -> - // from an existing deeplink - addToDeeplinkHistoryUseCase( - item = model.copy( - link = deeplink, + ) + .alsoSuccess { + if (saveIntoHistory) { + originalModel?.let { model -> + // from an existing deeplink + addToDeeplinkHistoryUseCase( + item = model.copy( + link = deeplink, + ) ) - ) - } ?: run { - // from freeform - addToDeeplinkHistoryUseCase( - item = DeeplinkDomainModel( - link = deeplink, - label = null, - description = null, - id = 0, // will be created by the DB - parameters = emptyList(), + } ?: run { + // from freeform + addToDeeplinkHistoryUseCase( + item = DeeplinkDomainModel( + link = deeplink, + label = null, + description = null, + id = 0, // will be created by the DB + parameters = emptyList(), + ) ) - ) + } } } - } } } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/usecase/ObserveCurrentDeviceDeeplinkUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/usecase/ObserveCurrentDeviceDeeplinkUseCase.kt index 09f8c6b38..1ad54d12d 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/usecase/ObserveCurrentDeviceDeeplinkUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/usecase/ObserveCurrentDeviceDeeplinkUseCase.kt @@ -1,6 +1,6 @@ package io.github.openflocon.domain.deeplink.usecase -import io.github.openflocon.domain.deeplink.models.DeeplinkDomainModel +import io.github.openflocon.domain.deeplink.models.Deeplinks import io.github.openflocon.domain.deeplink.repository.DeeplinkRepository import io.github.openflocon.domain.device.usecase.ObserveCurrentDeviceIdAndPackageNameUseCase import kotlinx.coroutines.flow.Flow @@ -11,9 +11,9 @@ class ObserveCurrentDeviceDeeplinkUseCase( private val deeplinkRepository: DeeplinkRepository, private val observeCurrentDeviceIdAndPackageNameUseCase: ObserveCurrentDeviceIdAndPackageNameUseCase, ) { - operator fun invoke(): Flow> = observeCurrentDeviceIdAndPackageNameUseCase().flatMapLatest { current -> + operator fun invoke(): Flow = observeCurrentDeviceIdAndPackageNameUseCase().flatMapLatest { current -> if (current == null) { - flowOf(emptyList()) + flowOf(Deeplinks(emptyList(), emptyList())) } else { deeplinkRepository.observe(deviceIdAndPackageName = current) } From 51fea7ea285e526655cc0607e2f9f69bbb9f7954 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Fri, 6 Mar 2026 14:24:40 +0100 Subject: [PATCH 02/12] fix: View --- .../deeplinks/FloconDeeplinksPlugin.kt | 19 +- .../deeplinks/FloconDeeplinksPlugin.kt | 18 +- .../features/deeplinks/DeepLinkViewModel.kt | 72 +++--- .../features/deeplinks/mapper/Mapper.kt | 21 +- .../deeplinks/model/DeeplinkScreenState.kt | 9 + .../model/DeeplinkVariableViewState.kt | 10 + .../deeplinks/view/DeeplinkItemView.kt | 233 ++++++++++-------- .../features/deeplinks/view/DeeplinkScreen.kt | 77 ++++-- .../view/DeeplinkVariablesPanelView.kt | 145 +++++++++++ 9 files changed, 425 insertions(+), 179 deletions(-) create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkScreenState.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkVariableViewState.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkVariablesPanelView.kt diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt index 504a1869f..448b40468 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt @@ -72,17 +72,26 @@ class DeeplinkBuilder { deeplinks.add(deeplink) } - internal fun build(): List { - return deeplinks.toList() - } + internal fun deeplinks(): List = deeplinks.toList() + internal fun variables(): List = variables.toList() } fun FloconApp.deeplinks(deeplinksBlock: DeeplinkBuilder.() -> Unit) { this.client?.deeplinksPlugin?.let { - it.registerDeeplinks(DeeplinkBuilder().apply(deeplinksBlock).build()) + val builder = DeeplinkBuilder().apply(deeplinksBlock) + + it.registerDeeplinks( + deeplinks = builder.deeplinks(), + variables = builder.variables() + ) } } interface FloconDeeplinksPlugin { - fun registerDeeplinks(deeplinks: List) + + fun registerDeeplinks( + deeplinks: List, + variables: List + ) + } \ No newline at end of file diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt index 481ef24e0..b65bc40fc 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt @@ -14,6 +14,7 @@ internal class FloconDeeplinksPluginImpl( ) : FloconPlugin, FloconDeeplinksPlugin { private val deeplinks = MutableStateFlow?>(null) + private val variables = MutableStateFlow?>(null) override fun onMessageReceived( messageFromServer: FloconMessageFromServer, @@ -24,20 +25,25 @@ internal class FloconDeeplinksPluginImpl( override fun onConnectedToServer() { // on connected, send known dashboard deeplinks.value?.let { - registerDeeplinks(it) + registerDeeplinks(it, variables.value.orEmpty()) } } - override fun registerDeeplinks(deeplinks: List) { - this.deeplinks.update { - deeplinks - } + override fun registerDeeplinks( + deeplinks: List, + variables: List + ) { + this.deeplinks.update { deeplinks } + this.variables.update { variables } try { sender.send( plugin = Protocol.FromDevice.Deeplink.Plugin, method = Protocol.FromDevice.Deeplink.Method.GetDeeplinks, - body = toDeeplinksJson(deeplinks) + body = toDeeplinksJson( + deeplinks = deeplinks, + variables = variables + ) ) } catch (t: Throwable) { FloconLogger.logError("deeplink mapping error", t) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt index b25a94f92..4801a9b4b 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt @@ -6,20 +6,23 @@ import flocondesktop.composeapp.generated.resources.Res import flocondesktop.composeapp.generated.resources.deeplink_removed import flocondesktop.composeapp.generated.resources.fill_deeplink_parts import io.github.openflocon.domain.common.DispatcherProvider -import io.github.openflocon.domain.common.combines import io.github.openflocon.domain.deeplink.usecase.ExecuteDeeplinkUseCase import io.github.openflocon.domain.deeplink.usecase.ObserveCurrentDeviceDeeplinkHistoryUseCase import io.github.openflocon.domain.deeplink.usecase.ObserveCurrentDeviceDeeplinkUseCase import io.github.openflocon.domain.deeplink.usecase.RemoveFromDeeplinkHistoryUseCase import io.github.openflocon.domain.feedback.FeedbackDisplayer +import io.github.openflocon.flocondesktop.common.utils.stateInWhileSubscribed import io.github.openflocon.flocondesktop.features.deeplinks.mapper.mapToUi import io.github.openflocon.flocondesktop.features.deeplinks.model.DeeplinkPart +import io.github.openflocon.flocondesktop.features.deeplinks.model.DeeplinkScreenState +import io.github.openflocon.flocondesktop.features.deeplinks.model.DeeplinkVariableViewState import io.github.openflocon.flocondesktop.features.deeplinks.model.DeeplinkViewState -import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString @@ -27,27 +30,40 @@ class DeepLinkViewModel( private val dispatcherProvider: DispatcherProvider, private val feedbackDisplayer: FeedbackDisplayer, private val observeCurrentDeviceDeeplinkUseCase: ObserveCurrentDeviceDeeplinkUseCase, - private val observeCurrentDeviceDeeplinkHistoryUseCase: ObserveCurrentDeviceDeeplinkHistoryUseCase, + private val observeCurrentDeviceDeeplinkHistoryUseCase: + ObserveCurrentDeviceDeeplinkHistoryUseCase, private val executeDeeplinkUseCase: ExecuteDeeplinkUseCase, private val removeFromDeeplinkHistoryUseCase: RemoveFromDeeplinkHistoryUseCase, ) : ViewModel() { - val deepLinks: StateFlow> = combines( + private val variableValues = MutableStateFlow>(emptyMap()) + + val state: StateFlow = combine( observeCurrentDeviceDeeplinkUseCase(), - observeCurrentDeviceDeeplinkHistoryUseCase() - ) - .mapLatest { (deepLinks, history) -> - mapToUi( - deepLinks = deepLinks.deeplinks, + observeCurrentDeviceDeeplinkHistoryUseCase(), + variableValues.asStateFlow() + ) { deepLinks, history, variablesValues -> + DeeplinkScreenState( + deepLinks = mapToUi( history = history, - ) - } - .flowOn(dispatcherProvider.viewModel) - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = emptyList(), + deepLinks = deepLinks.deeplinks, + variableValues = variablesValues + ), + variables = deepLinks.variables.map { variable -> + DeeplinkVariableViewState( + name = variable.name, + description = variable.description, + value = variablesValues.getOrDefault(variable.name, ""), + ) + } ) + } + .onEach { println(it) } + .stateInWhileSubscribed(DeeplinkScreenState(emptyList(), emptyList())) + + fun setVariable(name: String, value: String) { + variableValues.update { current -> current + (name to value) } + } fun removeFromHistory(viewState: DeeplinkViewState) { viewModelScope.launch(dispatcherProvider.viewModel) { @@ -72,13 +88,15 @@ class DeepLinkViewModel( return@launch } - val deeplink = viewState.parts.joinToString(separator = "") { - when (it) { - is DeeplinkPart.Text -> it.value - is DeeplinkPart.TextField -> values[it] ?: "" - is DeeplinkPart.Variable -> TODO() + val currentVariableValues = variableValues.value + val deeplink = + viewState.parts.joinToString(separator = "") { + when (it) { + is DeeplinkPart.Text -> it.value + is DeeplinkPart.TextField -> values[it] ?: "" + is DeeplinkPart.Variable -> currentVariableValues[it.value] ?: it.value + } } - } executeDeeplinkUseCase( deeplink = deeplink, @@ -87,9 +105,7 @@ class DeepLinkViewModel( ) .alsoFailure { it.printStackTrace() - feedbackDisplayer.displayMessage( - message = "Error while sending deeplink" - ) + feedbackDisplayer.displayMessage(message = "Error while sending deeplink") } } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/mapper/Mapper.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/mapper/Mapper.kt index 081a18286..1c505f675 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/mapper/Mapper.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/mapper/Mapper.kt @@ -12,17 +12,18 @@ data class DeeplinkItem( internal fun mapToUi( history: List, deepLinks: List, + variableValues: Map ): List = buildList { addAll(history.map { DeeplinkItem(model = it, isHistory = true) }) addAll(deepLinks.map { DeeplinkItem(model = it, isHistory = false) }) -}.distinctBy { it.model.link } - .map { - mapToUi(it.model, isHistory = it.isHistory) - } +} + .distinctBy { it.model.link } + .map { mapToUi(deepLink = it.model, isHistory = it.isHistory, variableValues = variableValues) } internal fun mapToUi( deepLink: DeeplinkDomainModel, - isHistory: Boolean + isHistory: Boolean, + variableValues: Map ): DeeplinkViewState = DeeplinkViewState( label = deepLink.label, description = deepLink.description, @@ -33,14 +34,16 @@ internal fun mapToUi( } else { parseDeeplinkString( input = deepLink.link, - deepLink = deepLink + deepLink = deepLink, + variableValues = variableValues ) - }, + } ) internal fun parseDeeplinkString( input: String, - deepLink: DeeplinkDomainModel + deepLink: DeeplinkDomainModel, + variableValues: Map ): List { val regex = "\\[([^\\[\\]]*)\\]".toRegex() // Regex pour trouver [quelquechose] val result = mutableListOf() @@ -70,7 +73,7 @@ internal fun parseDeeplinkString( ) is DeeplinkDomainModel.Parameter.Variable -> DeeplinkPart.Variable( - value = value // TODO Change + value = variableValues[parameter.variableName] ?: "{${parameter.variableName}}" ) } ) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkScreenState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkScreenState.kt new file mode 100644 index 000000000..02a02fe84 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkScreenState.kt @@ -0,0 +1,9 @@ +package io.github.openflocon.flocondesktop.features.deeplinks.model + +import androidx.compose.runtime.Immutable + +@Immutable +data class DeeplinkScreenState( + val deepLinks: List, + val variables: List, +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkVariableViewState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkVariableViewState.kt new file mode 100644 index 000000000..37aed82ab --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkVariableViewState.kt @@ -0,0 +1,10 @@ +package io.github.openflocon.flocondesktop.features.deeplinks.model + +import androidx.compose.runtime.Immutable + +@Immutable +data class DeeplinkVariableViewState( + val name: String, + val description: String?, + val value: String, +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkItemView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkItemView.kt index 11ec111d9..69818fab3 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkItemView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkItemView.kt @@ -47,82 +47,81 @@ import org.jetbrains.compose.ui.tooling.preview.Preview @Composable fun DeeplinkItemView( - item: DeeplinkViewState, - submit: (DeeplinkViewState, values: Map) -> Unit, - removeFromHistory: (DeeplinkViewState) -> Unit, - modifier: Modifier = Modifier, + item: DeeplinkViewState, + submit: (DeeplinkViewState, values: Map) -> Unit, + removeFromHistory: (DeeplinkViewState) -> Unit, + variableValues: Map = emptyMap(), + modifier: Modifier = Modifier, ) { val values = remember(item.deeplinkId) { mutableStateMapOf() } Column( - modifier = modifier - .padding(vertical = 4.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier.padding(vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), ) { item.label?.let { Text( - text = item.label, - modifier = Modifier.padding(start = 4.dp), - style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold), - color = FloconTheme.colorPalette.onPrimary + text = item.label, + modifier = Modifier.padding(start = 4.dp), + style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold), + color = FloconTheme.colorPalette.onPrimary ) } Row( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Row( - modifier = Modifier - .weight(1f) - .clip(FloconTheme.shapes.medium) - .background( - if (item.isHistory) FloconTheme.colorPalette.accent - else FloconTheme.colorPalette.surface - ) - .padding(horizontal = 12.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.weight(1f) + .clip(FloconTheme.shapes.medium) + .background( + if (item.isHistory) FloconTheme.colorPalette.accent + else FloconTheme.colorPalette.surface + ) + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, ) { item.parts.fastForEach { part -> TextFieldPart( - part = part, - onFieldValueChanged = { field, value -> - values.put(field, value) - }, + part = part, + variableValues = variableValues, + onFieldValueChanged = { field, value -> values.put(field, value) }, ) } } if (item.isHistory) { FloconIconTonalButton( - onClick = { removeFromHistory(item) }, - containerColor = FloconTheme.colorPalette.tertiary, + onClick = { removeFromHistory(item) }, + containerColor = FloconTheme.colorPalette.tertiary, ) { FloconIcon( - imageVector = Icons.Default.Delete, + imageVector = Icons.Default.Delete, ) } } FloconIconTonalButton( - onClick = { submit(item, values.toMap()) }, - containerColor = FloconTheme.colorPalette.tertiary, + onClick = { submit(item, values.toMap()) }, + containerColor = FloconTheme.colorPalette.tertiary, ) { FloconIcon( - imageVector = Icons.AutoMirrored.Filled.Send, + imageVector = Icons.AutoMirrored.Filled.Send, ) } } item.description?.let { Text( - text = it, - style = FloconTheme.typography.bodySmall.copy( - fontWeight = FontWeight.Light, - fontStyle = FontStyle.Italic, - ), - color = FloconTheme.colorPalette.onSurface, - modifier = Modifier.padding(start = 4.dp), + text = it, + style = + FloconTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Light, + fontStyle = FontStyle.Italic, + ), + color = FloconTheme.colorPalette.onSurface, + modifier = Modifier.padding(start = 4.dp), ) } } @@ -131,8 +130,9 @@ fun DeeplinkItemView( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun TextFieldPart( - part: DeeplinkPart, - onFieldValueChanged: (DeeplinkPart.TextField, value: String) -> Unit + part: DeeplinkPart, + variableValues: Map, + onFieldValueChanged: (DeeplinkPart.TextField, value: String) -> Unit ) { when (part) { is DeeplinkPart.TextField -> { @@ -140,100 +140,116 @@ private fun TextFieldPart( var value by remember { mutableStateOf("") } var isExpanded by remember { mutableStateOf(false) } - LaunchedEffect(part, value) { - onFieldValueChangedCallback(part, value) - } + LaunchedEffect(part, value) { onFieldValueChangedCallback(part, value) } - val filteredAutoComplete = remember(value, part.autoComplete) { - part.autoComplete?.filter { it.contains(value, ignoreCase = true) } - } + val filteredAutoComplete = + remember(value, part.autoComplete) { + part.autoComplete?.filter { it.contains(value, ignoreCase = true) } + } ExposedDropdownMenuBox( - expanded = isExpanded, - onExpandedChange = { isExpanded = it }, + expanded = isExpanded, + onExpandedChange = { isExpanded = it }, ) { DeeplinkTextField( - value = value, - onValueChange = { - value = it - isExpanded = filteredAutoComplete?.isNotEmpty() == true - }, - modifier = Modifier.menuAnchor(PrimaryEditable), - label = part.label, + value = value, + onValueChange = { + value = it + isExpanded = filteredAutoComplete?.isNotEmpty() == true + }, + modifier = Modifier.menuAnchor(PrimaryEditable), + label = part.label, ) // The dropdown menu if (filteredAutoComplete?.isNotEmpty() == true) { ExposedDropdownMenu( - modifier = Modifier.widthIn(min = 200.dp), - expanded = isExpanded, - onDismissRequest = { isExpanded = false }, + modifier = Modifier.widthIn(min = 200.dp), + expanded = isExpanded, + onDismissRequest = { isExpanded = false }, ) { filteredAutoComplete.forEach { item -> Text( - text = item, - style = FloconTheme.typography.bodySmall, - modifier = Modifier - .clickable { - value = item - isExpanded = false - } - .padding( - vertical = 4.dp, - horizontal = 8.dp, - ) + text = item, + style = FloconTheme.typography.bodySmall, + modifier = + Modifier.clickable { + value = item + isExpanded = false + } + .padding( + vertical = 4.dp, + horizontal = 8.dp, + ) ) } } } } } - is DeeplinkPart.Text -> { Text( - part.value, - style = FloconTheme.typography.bodySmall.copy( - color = FloconTheme.colorPalette.onSurface, - ), + part.value, + style = + FloconTheme.typography.bodySmall.copy( + color = FloconTheme.colorPalette.onSurface, + ), + ) + } + is DeeplinkPart.Variable -> { + val resolved = variableValues[part.value] + Text( + text = resolved.takeIf { !it.isNullOrEmpty() } ?: part.value, + style = + FloconTheme.typography.bodySmall.copy( + color = + if (resolved.isNullOrEmpty()) + FloconTheme.colorPalette.onSurface.copy( + alpha = 0.4f + ) + else FloconTheme.colorPalette.onSurface, + fontWeight = + if (resolved.isNullOrEmpty()) FontWeight.Normal + else FontWeight.Bold, + ), ) } - - is DeeplinkPart.Variable -> TODO() } } @Composable private fun DeeplinkTextField( - modifier: Modifier = Modifier, - label: String, - value: String, - onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + label: String, + value: String, + onValueChange: (String) -> Unit, ) { val isValueEmpty = value.isEmpty() Box( - modifier = modifier.background( - color = FloconTheme.colorPalette.primary, - shape = RoundedCornerShape(2.dp), - ).padding(horizontal = 2.dp, vertical = 2.dp) - .width(IntrinsicSize.Min), + modifier = + modifier.background( + color = FloconTheme.colorPalette.primary, + shape = RoundedCornerShape(2.dp), + ) + .padding(horizontal = 2.dp, vertical = 2.dp) + .width(IntrinsicSize.Min), ) { Text( - text = label, - style = FloconTheme.typography.bodySmall, - color = FloconTheme.colorPalette.onSurface.copy(alpha = 0.45f), - modifier = Modifier.graphicsLayer { - alpha = if (isValueEmpty) 1f else 0f - }, + text = label, + style = FloconTheme.typography.bodySmall, + color = FloconTheme.colorPalette.onSurface.copy(alpha = 0.45f), + modifier = Modifier.graphicsLayer { alpha = if (isValueEmpty) 1f else 0f }, ) BasicTextField( - textStyle = FloconTheme.typography.bodySmall.copy( - color = FloconTheme.colorPalette.onSurface, - fontWeight = FontWeight.Bold, - ), - maxLines = 1, - value = value, - cursorBrush = SolidColor(FloconTheme.colorPalette.onSurface), - onValueChange = onValueChange, + textStyle = + FloconTheme.typography.bodySmall.copy( + color = FloconTheme.colorPalette.onSurface, + fontWeight = FontWeight.Bold, + ), + maxLines = 1, + value = value, + cursorBrush = SolidColor(FloconTheme.colorPalette.onSurface), + onValueChange = onValueChange, ) } } @@ -243,12 +259,13 @@ private fun DeeplinkTextField( private fun DeeplinkItemViewPreview() { FloconTheme { DeeplinkItemView( - modifier = Modifier.background( - FloconTheme.colorPalette.primary, - ), - submit = { _, _ -> }, - item = previewDeeplinkViewState(), - removeFromHistory = {}, + modifier = + Modifier.background( + FloconTheme.colorPalette.primary, + ), + submit = { _, _ -> }, + item = previewDeeplinkViewState(), + removeFromHistory = {}, ) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkScreen.kt index 62e345784..1724b0030 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkScreen.kt @@ -3,10 +3,12 @@ package io.github.openflocon.flocondesktop.features.deeplinks.view import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState @@ -19,6 +21,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.openflocon.flocondesktop.features.deeplinks.DeepLinkViewModel import io.github.openflocon.flocondesktop.features.deeplinks.model.DeeplinkPart +import io.github.openflocon.flocondesktop.features.deeplinks.model.DeeplinkScreenState +import io.github.openflocon.flocondesktop.features.deeplinks.model.DeeplinkVariableViewState import io.github.openflocon.flocondesktop.features.deeplinks.model.DeeplinkViewState import io.github.openflocon.flocondesktop.features.deeplinks.model.previewDeeplinkViewState import io.github.openflocon.library.designsystem.FloconTheme @@ -32,44 +36,53 @@ import org.koin.compose.viewmodel.koinViewModel @Composable fun DeeplinkScreen(modifier: Modifier = Modifier) { val viewModel: DeepLinkViewModel = koinViewModel() - val deepLinks by viewModel.deepLinks.collectAsStateWithLifecycle() + val state by viewModel.state.collectAsStateWithLifecycle() DeeplinkScreen( - deepLinks = deepLinks, + state = state, submit = viewModel::submit, removeFromHistory = viewModel::removeFromHistory, + setVariable = viewModel::setVariable, modifier = modifier, ) } @Composable private fun DeeplinkScreen( - deepLinks: List, + state: DeeplinkScreenState, submit: (DeeplinkViewState, values: Map) -> Unit, removeFromHistory: (DeeplinkViewState) -> Unit, + setVariable: (name: String, value: String) -> Unit, modifier: Modifier = Modifier, ) { val listState = rememberLazyListState() val scrollAdapter = rememberFloconScrollbarAdapter(listState) - FloconFeature( - modifier = modifier - ) { + FloconFeature(modifier = modifier) { FloconPageTopBar( modifier = Modifier.fillMaxWidth(), filterBar = { - DeeplinkFreeformItemView( - submit = submit, - modifier = Modifier.fillMaxWidth(), - ) + Column { + if (state.variables.isNotEmpty()) { + DeeplinkVariablesPanelView( + variables = state.variables, + onVariableChanged = setVariable, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp), + ) + } + DeeplinkFreeformItemView( + submit = submit, + modifier = Modifier.fillMaxWidth(), + ) + } } ) Box( - modifier = Modifier - .fillMaxSize() - .clip(FloconTheme.shapes.medium) - .background(FloconTheme.colorPalette.primary) + modifier = + Modifier.fillMaxSize() + .clip(FloconTheme.shapes.medium) + .background(FloconTheme.colorPalette.primary) ) { LazyColumn( state = listState, @@ -77,19 +90,19 @@ private fun DeeplinkScreen( contentPadding = PaddingValues(all = 8.dp), verticalArrangement = Arrangement.spacedBy(4.dp), ) { - itemsIndexed(deepLinks) { index, item -> + itemsIndexed(state.deepLinks) { _, item -> DeeplinkItemView( submit = submit, removeFromHistory = removeFromHistory, item = item, + variableValues = state.variables.associate { it.name to it.value }, modifier = Modifier.fillMaxWidth(), ) } } FloconVerticalScrollbar( adapter = scrollAdapter, - modifier = Modifier.fillMaxHeight() - .align(Alignment.TopEnd) + modifier = Modifier.fillMaxHeight().align(Alignment.TopEnd) ) } } @@ -100,14 +113,32 @@ private fun DeeplinkScreen( private fun DeeplinkScreenPreview() { FloconTheme { DeeplinkScreen( - deepLinks = listOf( - previewDeeplinkViewState(), - previewDeeplinkViewState(), - previewDeeplinkViewState(), - ), + state = + DeeplinkScreenState( + deepLinks = + listOf( + previewDeeplinkViewState(), + previewDeeplinkViewState(), + previewDeeplinkViewState(), + ), + variables = + listOf( + DeeplinkVariableViewState( + name = "userId", + description = null, + value = "" + ), + DeeplinkVariableViewState( + name = "env", + description = null, + value = "staging" + ), + ), + ), submit = { _, _ -> }, - modifier = Modifier.fillMaxSize(), removeFromHistory = {}, + setVariable = { _, _ -> }, + modifier = Modifier.fillMaxSize(), ) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkVariablesPanelView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkVariablesPanelView.kt new file mode 100644 index 000000000..55115d717 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkVariablesPanelView.kt @@ -0,0 +1,145 @@ +package io.github.openflocon.flocondesktop.features.deeplinks.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.graphics.SolidColor +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import io.github.openflocon.flocondesktop.features.deeplinks.model.DeeplinkVariableViewState +import io.github.openflocon.library.designsystem.FloconTheme +import org.jetbrains.compose.ui.tooling.preview.Preview + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun DeeplinkVariablesPanelView( + variables: List, + onVariableChanged: (name: String, value: String) -> Unit, + modifier: Modifier = Modifier, +) { + if (variables.isEmpty()) return + + FlowRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + variables.forEach { variable -> + DeeplinkVariableChip( + variable = variable, + onValueChange = { onVariableChanged(variable.name, it) }, + ) + } + } +} + +@Composable +private fun DeeplinkVariableChip( + variable: DeeplinkVariableViewState, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + var value by remember(variable.name) { mutableStateOf(variable.value) } + val isValueEmpty = value.isEmpty() + + Row( + modifier = + modifier.background( + color = FloconTheme.colorPalette.surface, + shape = RoundedCornerShape(4.dp), + ) + .padding(horizontal = 6.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = "${variable.name}:", + style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold), + color = FloconTheme.colorPalette.onSurface, + ) + + Box( + modifier = + Modifier.background( + color = FloconTheme.colorPalette.primary, + shape = RoundedCornerShape(2.dp), + ) + .padding(horizontal = 4.dp, vertical = 2.dp) + .width(IntrinsicSize.Min), + ) { + Text( + text = if (isValueEmpty) variable.name else value, + style = FloconTheme.typography.bodySmall, + color = + FloconTheme.colorPalette.onSurface.copy( + alpha = if (isValueEmpty) 0.4f else 0f + ), + modifier = Modifier.graphicsLayer { alpha = if (isValueEmpty) 1f else 0f }, + ) + BasicTextField( + value = value, + onValueChange = { + value = it + onValueChange(it) + }, + maxLines = 1, + textStyle = + FloconTheme.typography.bodySmall.copy( + color = FloconTheme.colorPalette.onSurface, + fontWeight = FontWeight.Bold, + ), + cursorBrush = SolidColor(FloconTheme.colorPalette.onSurface), + ) + } + } +} + +@Composable +@Preview +private fun DeeplinkVariablesPanelViewPreview() { + FloconTheme { + DeeplinkVariablesPanelView( + modifier = + Modifier.background(FloconTheme.colorPalette.primary) + .fillMaxWidth() + .padding(8.dp), + variables = + listOf( + DeeplinkVariableViewState( + name = "userId", + description = "The user id", + value = "" + ), + DeeplinkVariableViewState( + name = "env", + description = null, + value = "staging" + ), + DeeplinkVariableViewState( + name = "token", + description = null, + value = "" + ), + ), + onVariableChanged = { _, _ -> }, + ) + } +} From c852480d5af6831e0e7dc054f4980691f76a6fad Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Fri, 6 Mar 2026 14:46:35 +0100 Subject: [PATCH 03/12] feat: Improve UI --- .../deeplinks/FloconDeeplinksPlugin.kt | 17 +- .../flocon/plugins/deeplinks/Mapping.kt | 8 +- .../deeplinks/model/DeeplinksRemote.kt | 19 +- .../deeplinks/InitializeDeeplinks.kt | 18 +- .../80.json | 1881 +++++++++++++++++ .../flocondesktop/common/db/AppDatabase.kt | 4 +- .../features/deeplinks/DeepLinkViewModel.kt | 116 +- .../model/DeeplinkVariableViewState.kt | 16 +- .../view/DeeplinkVariablesPanelView.kt | 218 +- .../io/github/openflocon/data/local/DI.kt | 7 + .../data/local/deeplink/ModeConverter.kt | 20 + .../data/local/deeplink/mapper/Mapper.kt | 10 +- .../deeplink/models/DeeplinkVariableEntity.kt | 24 +- .../DeeplinkVariableReceivedDataModel.kt | 21 +- .../models/DeeplinksReceivedDataModel.kt | 4 + .../models/DeeplinkVariableDomainModel.kt | 12 +- 16 files changed, 2256 insertions(+), 139 deletions(-) create mode 100644 FloconDesktop/composeApp/schemas/io.github.openflocon.flocondesktop.common.db.AppDatabase/80.json create mode 100644 FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/ModeConverter.kt diff --git a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt index 448b40468..e1bfbad78 100644 --- a/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt +++ b/FloconAndroid/flocon-base/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/FloconDeeplinksPlugin.kt @@ -38,11 +38,18 @@ class DeeplinkLinkBuilder internal constructor( class DeeplinkVariableBuilder internal constructor( private val name: String ) { + private var mode: DeeplinkVariable.Mode = DeeplinkVariable.Mode.Input + var description: String? = null + fun autoComplete(suggestions: List) { + mode = DeeplinkVariable.Mode.AutoComplete(suggestions) + } + internal fun build(): DeeplinkVariable { return DeeplinkVariable( name = name, + mode = mode, description = description ) } @@ -51,8 +58,16 @@ class DeeplinkVariableBuilder internal constructor( data class DeeplinkVariable( val name: String, + val mode: Mode = Mode.Input, val description: String? = null -) +) { + + sealed interface Mode { + object Input : Mode + data class AutoComplete(val suggestions: List) : Mode + } + +} class DeeplinkBuilder { private val variables = mutableListOf() diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt index 71f6f7973..4dede8700 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/Mapping.kt @@ -13,7 +13,7 @@ internal fun toDeeplinksJson( ): String { val dto = DeeplinksRemote( deeplinks = deeplinks.map(DeeplinkModel::toRemote), - variables = variables.map(DeeplinkVariable::toRemote) + variables = variables.map(DeeplinkVariable::toRemote) ) return FloconEncoder.json @@ -32,7 +32,11 @@ internal fun DeeplinkModel.toRemote(): DeeplinkRemote = DeeplinkRemote( internal fun DeeplinkVariable.toRemote(): DeeplinkVariableRemote = DeeplinkVariableRemote( name = name, - description = description + mode = when (val mode = mode) { + is DeeplinkVariable.Mode.AutoComplete -> DeeplinkVariableRemote.Mode.AutoComplete(suggestions = mode.suggestions) + DeeplinkVariable.Mode.Input -> DeeplinkVariableRemote.Mode.Input + }, + description = description, ) internal fun DeeplinkModel.Parameter.toRemote(): DeeplinkParameterRemote = when (this) { diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt index aaff16098..0f229b039 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt @@ -38,8 +38,25 @@ internal class DeeplinkRemote( @Serializable internal data class DeeplinkVariableRemote( val name: String, + val mode: Mode = Mode.Input, val description: String? = null -) +) { + + @Serializable + @JsonClassDiscriminator("type") + sealed interface Mode { + + @Serializable + @SerialName("input") + object Input : Mode + + @Serializable + @SerialName("auto_complete") + data class AutoComplete(val suggestions: List) : Mode + + } + +} @Serializable internal class DeeplinksRemote( diff --git a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/deeplinks/InitializeDeeplinks.kt b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/deeplinks/InitializeDeeplinks.kt index f36c6b00a..b456c7acd 100644 --- a/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/deeplinks/InitializeDeeplinks.kt +++ b/FloconAndroid/sample-android-only/src/main/java/io/github/openflocon/flocon/myapplication/deeplinks/InitializeDeeplinks.kt @@ -6,16 +6,26 @@ import io.github.openflocon.flocon.plugins.deeplinks.deeplinks fun initializeDeeplinks() { Flocon.deeplinks { variable("test_variable") - deeplink("flocon://home") - deeplink("flocon://test") - deeplink("flocon://user/[userId]") { + variable("host") { + description = "Host variable" + autoComplete(listOf("flocon", "flocon2", "flocon3")) + } + deeplink("[host]://home") { + "host" withVariable "host" + } + deeplink("[host]://test") { + "host" withVariable "host" + } + deeplink("[host]://user/[userId]") { label = "User" "userId" withAutoComplete listOf("Florent", "David", "Guillaume") + "host" withVariable "host" } - deeplink("flocon://post/[postId]?comment=[commentText]") { + deeplink("[host]://post/[postId]?comment=[commentText]") { label = "Post" description = "Open a post and send a comment" "commentText" withVariable "test_variable" + "host" withVariable "host" } } } \ No newline at end of file diff --git a/FloconDesktop/composeApp/schemas/io.github.openflocon.flocondesktop.common.db.AppDatabase/80.json b/FloconDesktop/composeApp/schemas/io.github.openflocon.flocondesktop.common.db.AppDatabase/80.json new file mode 100644 index 000000000..27058f405 --- /dev/null +++ b/FloconDesktop/composeApp/schemas/io.github.openflocon.flocondesktop.common.db.AppDatabase/80.json @@ -0,0 +1,1881 @@ +{ + "formatVersion": 1, + "database": { + "version": 80, + "identityHash": "5b43a0b8688e41bbe05233fd1aee26a9", + "entities": [ + { + "tableName": "FloconNetworkCallEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`callId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `appInstance` INTEGER NOT NULL, `type` TEXT NOT NULL, `isReplayed` INTEGER NOT NULL, `request_url` TEXT NOT NULL, `request_method` TEXT NOT NULL, `request_startTime` INTEGER NOT NULL, `request_startTimeFormatted` TEXT NOT NULL, `request_byteSizeFormatted` TEXT NOT NULL, `request_requestHeaders` TEXT NOT NULL, `request_requestBody` TEXT, `request_requestByteSize` INTEGER NOT NULL, `request_isMocked` INTEGER NOT NULL, `request_domainFormatted` TEXT NOT NULL, `request_methodFormatted` TEXT NOT NULL, `request_queryFormatted` TEXT NOT NULL, `request_graphql_query` TEXT, `request_graphql_operationType` TEXT, `request_websocket_event` TEXT, `response_durationMs` REAL, `response_durationFormatted` TEXT, `response_responseContentType` TEXT, `response_responseBody` TEXT, `response_responseHeaders` TEXT, `response_responseByteSize` INTEGER, `response_responseByteSizeFormatted` TEXT, `response_responseError` TEXT, `response_isImage` INTEGER, `response_statusFormatted` TEXT, `response_graphql_isSuccess` INTEGER, `response_graphql_responseHttpCode` INTEGER, `response_http_responseHttpCode` INTEGER, `response_grpc_responseStatus` TEXT, PRIMARY KEY(`callId`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "callId", + "columnName": "callId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appInstance", + "columnName": "appInstance", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isReplayed", + "columnName": "isReplayed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "request.url", + "columnName": "request_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "request.method", + "columnName": "request_method", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "request.startTime", + "columnName": "request_startTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "request.startTimeFormatted", + "columnName": "request_startTimeFormatted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "request.byteSizeFormatted", + "columnName": "request_byteSizeFormatted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "request.requestHeaders", + "columnName": "request_requestHeaders", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "request.requestBody", + "columnName": "request_requestBody", + "affinity": "TEXT" + }, + { + "fieldPath": "request.requestByteSize", + "columnName": "request_requestByteSize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "request.isMocked", + "columnName": "request_isMocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "request.domainFormatted", + "columnName": "request_domainFormatted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "request.methodFormatted", + "columnName": "request_methodFormatted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "request.queryFormatted", + "columnName": "request_queryFormatted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "request.graphql.query", + "columnName": "request_graphql_query", + "affinity": "TEXT" + }, + { + "fieldPath": "request.graphql.operationType", + "columnName": "request_graphql_operationType", + "affinity": "TEXT" + }, + { + "fieldPath": "request.websocket.event", + "columnName": "request_websocket_event", + "affinity": "TEXT" + }, + { + "fieldPath": "response.durationMs", + "columnName": "response_durationMs", + "affinity": "REAL" + }, + { + "fieldPath": "response.durationFormatted", + "columnName": "response_durationFormatted", + "affinity": "TEXT" + }, + { + "fieldPath": "response.responseContentType", + "columnName": "response_responseContentType", + "affinity": "TEXT" + }, + { + "fieldPath": "response.responseBody", + "columnName": "response_responseBody", + "affinity": "TEXT" + }, + { + "fieldPath": "response.responseHeaders", + "columnName": "response_responseHeaders", + "affinity": "TEXT" + }, + { + "fieldPath": "response.responseByteSize", + "columnName": "response_responseByteSize", + "affinity": "INTEGER" + }, + { + "fieldPath": "response.responseByteSizeFormatted", + "columnName": "response_responseByteSizeFormatted", + "affinity": "TEXT" + }, + { + "fieldPath": "response.responseError", + "columnName": "response_responseError", + "affinity": "TEXT" + }, + { + "fieldPath": "response.isImage", + "columnName": "response_isImage", + "affinity": "INTEGER" + }, + { + "fieldPath": "response.statusFormatted", + "columnName": "response_statusFormatted", + "affinity": "TEXT" + }, + { + "fieldPath": "response.graphql.isSuccess", + "columnName": "response_graphql_isSuccess", + "affinity": "INTEGER" + }, + { + "fieldPath": "response.graphql.responseHttpCode", + "columnName": "response_graphql_responseHttpCode", + "affinity": "INTEGER" + }, + { + "fieldPath": "response.http.responseHttpCode", + "columnName": "response_http_responseHttpCode", + "affinity": "INTEGER" + }, + { + "fieldPath": "response.grpc.responseStatus", + "columnName": "response_grpc_responseStatus", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "callId" + ] + }, + "indices": [ + { + "name": "index_FloconNetworkCallEntity_deviceId_packageName", + "unique": false, + "columnNames": [ + "deviceId", + "packageName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FloconNetworkCallEntity_deviceId_packageName` ON `${TABLE_NAME}` (`deviceId`, `packageName`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "FileEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `path` TEXT NOT NULL, `parentPath` TEXT NOT NULL, `size` INTEGER NOT NULL, `lastModifiedTimestamp` INTEGER NOT NULL, FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDirectory", + "columnName": "isDirectory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentPath", + "columnName": "parentPath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastModifiedTimestamp", + "columnName": "lastModifiedTimestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_FileEntity_deviceId_packageName", + "unique": false, + "columnNames": [ + "deviceId", + "packageName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FileEntity_deviceId_packageName` ON `${TABLE_NAME}` (`deviceId`, `packageName`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "FileOptionsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `withFoldersSize` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `packageName`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "withFoldersSize", + "columnName": "withFoldersSize", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "packageName" + ] + }, + "indices": [ + { + "name": "index_FileOptionsEntity_deviceId_packageName", + "unique": false, + "columnNames": [ + "deviceId", + "packageName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FileOptionsEntity_deviceId_packageName` ON `${TABLE_NAME}` (`deviceId`, `packageName`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "DashboardEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`dashboardId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, PRIMARY KEY(`dashboardId`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "dashboardId", + "columnName": "dashboardId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "dashboardId" + ] + }, + "indices": [ + { + "name": "index_DashboardEntity_dashboardId", + "unique": false, + "columnNames": [ + "dashboardId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DashboardEntity_dashboardId` ON `${TABLE_NAME}` (`dashboardId`)" + }, + { + "name": "index_DashboardEntity_deviceId_packageName", + "unique": false, + "columnNames": [ + "deviceId", + "packageName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DashboardEntity_deviceId_packageName` ON `${TABLE_NAME}` (`deviceId`, `packageName`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "DashboardContainerEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dashboardId` TEXT NOT NULL, `containerOrder` INTEGER NOT NULL, `containerConfig` TEXT NOT NULL, `name` TEXT NOT NULL, FOREIGN KEY(`dashboardId`) REFERENCES `DashboardEntity`(`dashboardId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dashboardId", + "columnName": "dashboardId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "containerOrder", + "columnName": "containerOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "containerConfig", + "columnName": "containerConfig", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_DashboardContainerEntity_dashboardId", + "unique": false, + "columnNames": [ + "dashboardId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DashboardContainerEntity_dashboardId` ON `${TABLE_NAME}` (`dashboardId`)" + }, + { + "name": "index_DashboardContainerEntity_dashboardId_containerOrder", + "unique": true, + "columnNames": [ + "dashboardId", + "containerOrder" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DashboardContainerEntity_dashboardId_containerOrder` ON `${TABLE_NAME}` (`dashboardId`, `containerOrder`)" + } + ], + "foreignKeys": [ + { + "table": "DashboardEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "dashboardId" + ], + "referencedColumns": [ + "dashboardId" + ] + } + ] + }, + { + "tableName": "DashboardElementEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `containerId` INTEGER NOT NULL, `elementOrder` INTEGER NOT NULL, `elementAsJson` TEXT NOT NULL, FOREIGN KEY(`containerId`) REFERENCES `DashboardContainerEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "containerId", + "columnName": "containerId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "elementOrder", + "columnName": "elementOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "elementAsJson", + "columnName": "elementAsJson", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_DashboardElementEntity_containerId", + "unique": false, + "columnNames": [ + "containerId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DashboardElementEntity_containerId` ON `${TABLE_NAME}` (`containerId`)" + }, + { + "name": "index_DashboardElementEntity_containerId_elementOrder", + "unique": true, + "columnNames": [ + "containerId", + "elementOrder" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DashboardElementEntity_containerId_elementOrder` ON `${TABLE_NAME}` (`containerId`, `elementOrder`)" + } + ], + "foreignKeys": [ + { + "table": "DashboardContainerEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "containerId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "TableEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `name` TEXT NOT NULL, FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_TableEntity_deviceId_packageName_name", + "unique": true, + "columnNames": [ + "deviceId", + "packageName", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TableEntity_deviceId_packageName_name` ON `${TABLE_NAME}` (`deviceId`, `packageName`, `name`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "TableItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` TEXT NOT NULL, `tableId` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `columnsNames` TEXT NOT NULL, `values` TEXT NOT NULL, PRIMARY KEY(`itemId`), FOREIGN KEY(`tableId`) REFERENCES `TableEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "itemId", + "columnName": "itemId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tableId", + "columnName": "tableId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "columnsNames", + "columnName": "columnsNames", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "values", + "columnName": "values", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "itemId" + ] + }, + "indices": [ + { + "name": "index_TableItemEntity_tableId", + "unique": false, + "columnNames": [ + "tableId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TableItemEntity_tableId` ON `${TABLE_NAME}` (`tableId`)" + } + ], + "foreignKeys": [ + { + "table": "TableEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "tableId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "DeviceImageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `url` TEXT NOT NULL, `time` INTEGER NOT NULL, `headersJsonEncoded` TEXT NOT NULL, PRIMARY KEY(`deviceId`, `packageName`, `url`, `time`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "headersJsonEncoded", + "columnName": "headersJsonEncoded", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "packageName", + "url", + "time" + ] + }, + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "SuccessQueryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `databaseId` TEXT NOT NULL, `queryString` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "databaseId", + "columnName": "databaseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "queryString", + "columnName": "queryString", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_SuccessQueryEntity_deviceId_packageName_databaseId_queryString", + "unique": true, + "columnNames": [ + "deviceId", + "packageName", + "databaseId", + "queryString" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SuccessQueryEntity_deviceId_packageName_databaseId_queryString` ON `${TABLE_NAME}` (`deviceId`, `packageName`, `databaseId`, `queryString`)" + }, + { + "name": "index_SuccessQueryEntity_databaseId", + "unique": false, + "columnNames": [ + "databaseId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SuccessQueryEntity_databaseId` ON `${TABLE_NAME}` (`databaseId`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "FavoriteQueryEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `databaseId` TEXT NOT NULL, `queryString` TEXT NOT NULL, `title` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "databaseId", + "columnName": "databaseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "queryString", + "columnName": "queryString", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_FavoriteQueryEntity_databaseId", + "unique": false, + "columnNames": [ + "databaseId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FavoriteQueryEntity_databaseId` ON `${TABLE_NAME}` (`databaseId`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "DeeplinkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `link` TEXT NOT NULL, `label` TEXT, `description` TEXT, `parametersAsJson` TEXT NOT NULL, `isHistory` INTEGER NOT NULL, FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "parametersAsJson", + "columnName": "parametersAsJson", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isHistory", + "columnName": "isHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_DeeplinkEntity_deviceId_packageName", + "unique": false, + "columnNames": [ + "deviceId", + "packageName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DeeplinkEntity_deviceId_packageName` ON `${TABLE_NAME}` (`deviceId`, `packageName`)" + }, + { + "name": "index_DeeplinkEntity_deviceId_link", + "unique": true, + "columnNames": [ + "deviceId", + "link" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DeeplinkEntity_deviceId_link` ON `${TABLE_NAME}` (`deviceId`, `link`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "DeeplinkVariableEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT, `isHistory` INTEGER NOT NULL, `mode` TEXT NOT NULL, FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "isHistory", + "columnName": "isHistory", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "mode", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_DeeplinkVariableEntity_deviceId_packageName", + "unique": false, + "columnNames": [ + "deviceId", + "packageName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DeeplinkVariableEntity_deviceId_packageName` ON `${TABLE_NAME}` (`deviceId`, `packageName`)" + }, + { + "name": "index_DeeplinkVariableEntity_deviceId_name", + "unique": true, + "columnNames": [ + "deviceId", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DeeplinkVariableEntity_deviceId_name` ON `${TABLE_NAME}` (`deviceId`, `name`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "AnalyticsItemEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` TEXT NOT NULL, `analyticsTableId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `appInstance` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `createdAtFormatted` TEXT NOT NULL, `eventName` TEXT NOT NULL, `propertiesColumnsNames` TEXT NOT NULL, `propertiesValues` TEXT NOT NULL, PRIMARY KEY(`itemId`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "itemId", + "columnName": "itemId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "analyticsTableId", + "columnName": "analyticsTableId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appInstance", + "columnName": "appInstance", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAtFormatted", + "columnName": "createdAtFormatted", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventName", + "columnName": "eventName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "propertiesColumnsNames", + "columnName": "propertiesColumnsNames", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "propertiesValues", + "columnName": "propertiesValues", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "itemId" + ] + }, + "indices": [ + { + "name": "index_AnalyticsItemEntity_deviceId_packageName_analyticsTableId", + "unique": false, + "columnNames": [ + "deviceId", + "packageName", + "analyticsTableId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AnalyticsItemEntity_deviceId_packageName_analyticsTableId` ON `${TABLE_NAME}` (`deviceId`, `packageName`, `analyticsTableId`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "NetworkFilterEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `columnName` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `itemsAsJson` TEXT NOT NULL, PRIMARY KEY(`deviceId`, `columnName`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "columnName", + "columnName": "columnName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemsAsJson", + "columnName": "itemsAsJson", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "columnName" + ] + }, + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "NetworkSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `valueAsJson` TEXT NOT NULL, PRIMARY KEY(`deviceId`, `packageName`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "valueAsJson", + "columnName": "valueAsJson", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "packageName" + ] + }, + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "MockNetworkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mockId` TEXT NOT NULL, `deviceId` TEXT, `packageName` TEXT, `isEnabled` INTEGER NOT NULL, `response` TEXT NOT NULL, `expectation_urlPattern` TEXT NOT NULL, `expectation_method` TEXT NOT NULL, PRIMARY KEY(`mockId`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "mockId", + "columnName": "mockId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT" + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "response", + "columnName": "response", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "expectation.urlPattern", + "columnName": "expectation_urlPattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "expectation.method", + "columnName": "expectation_method", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "mockId" + ] + }, + "indices": [ + { + "name": "index_MockNetworkEntity_deviceId_packageName", + "unique": false, + "columnNames": [ + "deviceId", + "packageName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MockNetworkEntity_deviceId_packageName` ON `${TABLE_NAME}` (`deviceId`, `packageName`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "DeviceWithSerialEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `serial` TEXT NOT NULL, PRIMARY KEY(`deviceId`), FOREIGN KEY(`deviceId`) REFERENCES `DeviceEntity`(`deviceId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serial", + "columnName": "serial", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId" + ] + }, + "foreignKeys": [ + { + "table": "DeviceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId" + ], + "referencedColumns": [ + "deviceId" + ] + } + ] + }, + { + "tableName": "BadQualityConfigEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `isEnabled` INTEGER NOT NULL, `errorProbability` REAL NOT NULL, `errors` TEXT NOT NULL, `triggerProbability` REAL NOT NULL, `minLatencyMs` INTEGER NOT NULL, `maxLatencyMs` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "errorProbability", + "columnName": "errorProbability", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "errors", + "columnName": "errors", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latency.triggerProbability", + "columnName": "triggerProbability", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "latency.minLatencyMs", + "columnName": "minLatencyMs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latency.maxLatencyMs", + "columnName": "maxLatencyMs", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_BadQualityConfigEntity_deviceId_packageName", + "unique": false, + "columnNames": [ + "deviceId", + "packageName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_BadQualityConfigEntity_deviceId_packageName` ON `${TABLE_NAME}` (`deviceId`, `packageName`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "DeviceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `deviceName` TEXT NOT NULL, `platform` TEXT NOT NULL, PRIMARY KEY(`deviceId`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceName", + "columnName": "deviceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "platform", + "columnName": "platform", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId" + ] + } + }, + { + "tableName": "DeviceAppEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `iconEncoded` TEXT, `lastAppInstance` INTEGER NOT NULL, `floconVersionOnDevice` TEXT NOT NULL, PRIMARY KEY(`deviceId`, `packageName`), FOREIGN KEY(`deviceId`) REFERENCES `DeviceEntity`(`deviceId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iconEncoded", + "columnName": "iconEncoded", + "affinity": "TEXT" + }, + { + "fieldPath": "lastAppInstance", + "columnName": "lastAppInstance", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "floconVersionOnDevice", + "columnName": "floconVersionOnDevice", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "packageName" + ] + }, + "foreignKeys": [ + { + "table": "DeviceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId" + ], + "referencedColumns": [ + "deviceId" + ] + } + ] + }, + { + "tableName": "DatabaseTableEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `databaseId` TEXT NOT NULL, `tableName` TEXT NOT NULL, `columnsAsString` TEXT NOT NULL, PRIMARY KEY(`deviceId`, `packageName`, `databaseId`, `tableName`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "databaseId", + "columnName": "databaseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tableName", + "columnName": "tableName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "columnsAsString", + "columnName": "columnsAsString", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "packageName", + "databaseId", + "tableName" + ] + }, + "indices": [ + { + "name": "index_DatabaseTableEntity_databaseId", + "unique": false, + "columnNames": [ + "databaseId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DatabaseTableEntity_databaseId` ON `${TABLE_NAME}` (`databaseId`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "CrashReportEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`crashId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `exceptionType` TEXT NOT NULL, `exceptionMessage` TEXT NOT NULL, `stackTrace` TEXT NOT NULL, PRIMARY KEY(`crashId`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "crashId", + "columnName": "crashId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exceptionType", + "columnName": "exceptionType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "exceptionMessage", + "columnName": "exceptionMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stackTrace", + "columnName": "stackTrace", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "crashId" + ] + }, + "indices": [ + { + "name": "index_CrashReportEntity_deviceId_packageName", + "unique": false, + "columnNames": [ + "deviceId", + "packageName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_CrashReportEntity_deviceId_packageName` ON `${TABLE_NAME}` (`deviceId`, `packageName`)" + } + ], + "foreignKeys": [ + { + "table": "DeviceAppEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "deviceId", + "packageName" + ], + "referencedColumns": [ + "deviceId", + "packageName" + ] + } + ] + }, + { + "tableName": "DatabaseQueryLogEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `dbName` TEXT NOT NULL, `sqlQuery` TEXT NOT NULL, `bindArgs` TEXT, `timestamp` INTEGER NOT NULL, `isTransaction` INTEGER NOT NULL, `deviceId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `appInstance` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dbName", + "columnName": "dbName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sqlQuery", + "columnName": "sqlQuery", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bindArgs", + "columnName": "bindArgs", + "affinity": "TEXT" + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTransaction", + "columnName": "isTransaction", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appInstance", + "columnName": "appInstance", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5b43a0b8688e41bbe05233fd1aee26a9')" + ] + } +} \ No newline at end of file diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt index 1b122cad3..6de2f3937 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt @@ -21,6 +21,7 @@ import io.github.openflocon.data.local.database.models.FavoriteQueryEntity import io.github.openflocon.data.local.database.models.SuccessQueryEntity import io.github.openflocon.data.local.database.dao.DatabaseQueryLogDao import io.github.openflocon.data.local.database.models.DatabaseQueryLogEntity +import io.github.openflocon.data.local.deeplink.ModeConverter import io.github.openflocon.data.local.deeplink.dao.FloconDeeplinkDao import io.github.openflocon.data.local.deeplink.dao.FloconDeeplinkVariableDao import io.github.openflocon.data.local.deeplink.models.DeeplinkEntity @@ -52,7 +53,7 @@ import io.github.openflocon.flocondesktop.common.db.converters.MapStringsConvert import kotlinx.coroutines.Dispatchers @Database( - version = 79, + version = 80, entities = [ FloconNetworkCallEntity::class, FileEntity::class, @@ -84,6 +85,7 @@ import kotlinx.coroutines.Dispatchers DashboardConverters::class, MapStringsConverters::class, ListStringsConverters::class, + ModeConverter::class ) abstract class AppDatabase : RoomDatabase() { abstract val networkDao: FloconNetworkDao diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt index 4801a9b4b..76646f431 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt @@ -6,6 +6,7 @@ import flocondesktop.composeapp.generated.resources.Res import flocondesktop.composeapp.generated.resources.deeplink_removed import flocondesktop.composeapp.generated.resources.fill_deeplink_parts import io.github.openflocon.domain.common.DispatcherProvider +import io.github.openflocon.domain.deeplink.models.DeeplinkVariableDomainModel import io.github.openflocon.domain.deeplink.usecase.ExecuteDeeplinkUseCase import io.github.openflocon.domain.deeplink.usecase.ObserveCurrentDeviceDeeplinkHistoryUseCase import io.github.openflocon.domain.deeplink.usecase.ObserveCurrentDeviceDeeplinkUseCase @@ -27,39 +28,58 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString class DeepLinkViewModel( - private val dispatcherProvider: DispatcherProvider, - private val feedbackDisplayer: FeedbackDisplayer, - private val observeCurrentDeviceDeeplinkUseCase: ObserveCurrentDeviceDeeplinkUseCase, - private val observeCurrentDeviceDeeplinkHistoryUseCase: - ObserveCurrentDeviceDeeplinkHistoryUseCase, - private val executeDeeplinkUseCase: ExecuteDeeplinkUseCase, - private val removeFromDeeplinkHistoryUseCase: RemoveFromDeeplinkHistoryUseCase, + private val dispatcherProvider: DispatcherProvider, + private val feedbackDisplayer: FeedbackDisplayer, + private val observeCurrentDeviceDeeplinkUseCase: ObserveCurrentDeviceDeeplinkUseCase, + private val observeCurrentDeviceDeeplinkHistoryUseCase: + ObserveCurrentDeviceDeeplinkHistoryUseCase, + private val executeDeeplinkUseCase: ExecuteDeeplinkUseCase, + private val removeFromDeeplinkHistoryUseCase: RemoveFromDeeplinkHistoryUseCase, ) : ViewModel() { private val variableValues = MutableStateFlow>(emptyMap()) - val state: StateFlow = combine( - observeCurrentDeviceDeeplinkUseCase(), - observeCurrentDeviceDeeplinkHistoryUseCase(), - variableValues.asStateFlow() - ) { deepLinks, history, variablesValues -> - DeeplinkScreenState( - deepLinks = mapToUi( - history = history, - deepLinks = deepLinks.deeplinks, - variableValues = variablesValues - ), - variables = deepLinks.variables.map { variable -> - DeeplinkVariableViewState( - name = variable.name, - description = variable.description, - value = variablesValues.getOrDefault(variable.name, ""), - ) - } - ) - } - .onEach { println(it) } - .stateInWhileSubscribed(DeeplinkScreenState(emptyList(), emptyList())) + val state: StateFlow = + combine( + observeCurrentDeviceDeeplinkUseCase(), + observeCurrentDeviceDeeplinkHistoryUseCase(), + variableValues.asStateFlow() + ) { deepLinks, history, variablesValues -> + DeeplinkScreenState( + deepLinks = + mapToUi( + history = history, + deepLinks = deepLinks.deeplinks, + variableValues = variablesValues + ), + variables = + deepLinks.variables.map { variable -> + DeeplinkVariableViewState( + name = variable.name, + description = variable.description, + value = + variablesValues.getOrDefault( + variable.name, + "" + ), + mode = + when (val m = variable.mode) { + DeeplinkVariableDomainModel.Mode + .Input -> + DeeplinkVariableViewState + .Mode.Input + is DeeplinkVariableDomainModel.Mode.AutoComplete -> + DeeplinkVariableViewState + .Mode.AutoComplete( + m.suggestions + ) + }, + ) + } + ) + } + .onEach { println(it) } + .stateInWhileSubscribed(DeeplinkScreenState(emptyList(), emptyList())) fun setVariable(name: String, value: String) { variableValues.update { current -> current + (name to value) } @@ -68,11 +88,11 @@ class DeepLinkViewModel( fun removeFromHistory(viewState: DeeplinkViewState) { viewModelScope.launch(dispatcherProvider.viewModel) { removeFromDeeplinkHistoryUseCase( - deeplinkId = viewState.deeplinkId, + deeplinkId = viewState.deeplinkId, ) feedbackDisplayer.displayMessage( - getString(Res.string.deeplink_removed), - type = FeedbackDisplayer.MessageType.Error, + getString(Res.string.deeplink_removed), + type = FeedbackDisplayer.MessageType.Error, ) } } @@ -82,31 +102,31 @@ class DeepLinkViewModel( val numberOfTextFields = viewState.parts.count { it is DeeplinkPart.TextField } if (numberOfTextFields != values.values.filterNot { it.isBlank() }.size) { feedbackDisplayer.displayMessage( - getString(Res.string.fill_deeplink_parts), - type = FeedbackDisplayer.MessageType.Error, + getString(Res.string.fill_deeplink_parts), + type = FeedbackDisplayer.MessageType.Error, ) return@launch } val currentVariableValues = variableValues.value val deeplink = - viewState.parts.joinToString(separator = "") { - when (it) { - is DeeplinkPart.Text -> it.value - is DeeplinkPart.TextField -> values[it] ?: "" - is DeeplinkPart.Variable -> currentVariableValues[it.value] ?: it.value + viewState.parts.joinToString(separator = "") { + when (it) { + is DeeplinkPart.Text -> it.value + is DeeplinkPart.TextField -> values[it] ?: "" + is DeeplinkPart.Variable -> currentVariableValues[it.value] ?: it.value + } } - } executeDeeplinkUseCase( - deeplink = deeplink, - deeplinkId = viewState.deeplinkId, - saveIntoHistory = viewState.deeplinkId == -1L || numberOfTextFields != 0 - ) - .alsoFailure { - it.printStackTrace() - feedbackDisplayer.displayMessage(message = "Error while sending deeplink") - } + deeplink = deeplink, + deeplinkId = viewState.deeplinkId, + saveIntoHistory = viewState.deeplinkId == -1L || numberOfTextFields != 0 + ) + .alsoFailure { + it.printStackTrace() + feedbackDisplayer.displayMessage(message = "Error while sending deeplink") + } } } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkVariableViewState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkVariableViewState.kt index 37aed82ab..c016c9967 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkVariableViewState.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkVariableViewState.kt @@ -4,7 +4,15 @@ import androidx.compose.runtime.Immutable @Immutable data class DeeplinkVariableViewState( - val name: String, - val description: String?, - val value: String, -) + val name: String, + val description: String?, + val value: String, + val mode: Mode = Mode.Input, +) { + @Immutable + sealed interface Mode { + data object Input : Mode + + @Immutable data class AutoComplete(val suggestions: List) : Mode + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkVariablesPanelView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkVariablesPanelView.kt index 55115d717..f5e7c06e6 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkVariablesPanelView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkVariablesPanelView.kt @@ -1,6 +1,7 @@ package io.github.openflocon.flocondesktop.features.deeplinks.view import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -10,8 +11,12 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType.Companion.PrimaryEditable +import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -31,115 +36,182 @@ import org.jetbrains.compose.ui.tooling.preview.Preview @OptIn(ExperimentalLayoutApi::class) @Composable fun DeeplinkVariablesPanelView( - variables: List, - onVariableChanged: (name: String, value: String) -> Unit, - modifier: Modifier = Modifier, + variables: List, + onVariableChanged: (name: String, value: String) -> Unit, + modifier: Modifier = Modifier, ) { if (variables.isEmpty()) return FlowRow( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), ) { variables.forEach { variable -> DeeplinkVariableChip( - variable = variable, - onValueChange = { onVariableChanged(variable.name, it) }, + variable = variable, + onValueChange = { onVariableChanged(variable.name, it) }, ) } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun DeeplinkVariableChip( - variable: DeeplinkVariableViewState, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, + variable: DeeplinkVariableViewState, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, ) { var value by remember(variable.name) { mutableStateOf(variable.value) } - val isValueEmpty = value.isEmpty() Row( - modifier = - modifier.background( - color = FloconTheme.colorPalette.surface, - shape = RoundedCornerShape(4.dp), - ) - .padding(horizontal = 6.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier.background( + color = FloconTheme.colorPalette.surface, + shape = RoundedCornerShape(4.dp), + ) + .padding(horizontal = 6.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { Text( - text = "${variable.name}:", - style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold), - color = FloconTheme.colorPalette.onSurface, + text = "${variable.name}:", + style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold), + color = FloconTheme.colorPalette.onSurface, ) - Box( - modifier = - Modifier.background( - color = FloconTheme.colorPalette.primary, - shape = RoundedCornerShape(2.dp), - ) - .padding(horizontal = 4.dp, vertical = 2.dp) - .width(IntrinsicSize.Min), - ) { - Text( - text = if (isValueEmpty) variable.name else value, - style = FloconTheme.typography.bodySmall, - color = - FloconTheme.colorPalette.onSurface.copy( - alpha = if (isValueEmpty) 0.4f else 0f - ), - modifier = Modifier.graphicsLayer { alpha = if (isValueEmpty) 1f else 0f }, - ) - BasicTextField( + when (val mode = variable.mode) { + DeeplinkVariableViewState.Mode.Input -> { + VariableInputField( value = value, + placeholder = variable.name, onValueChange = { value = it onValueChange(it) }, - maxLines = 1, - textStyle = - FloconTheme.typography.bodySmall.copy( - color = FloconTheme.colorPalette.onSurface, - fontWeight = FontWeight.Bold, - ), - cursorBrush = SolidColor(FloconTheme.colorPalette.onSurface), - ) + ) + } + + is DeeplinkVariableViewState.Mode.AutoComplete -> { + var isExpanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = isExpanded, + onExpandedChange = { isExpanded = it }, + ) { + VariableInputField( + value = value, + placeholder = variable.name, + onValueChange = { + value = it + onValueChange(it) + isExpanded = mode.suggestions.isNotEmpty() + }, + readOnly = true, + modifier = Modifier.menuAnchor(PrimaryEditable), + ) + + if (mode.suggestions.isNotEmpty()) { + ExposedDropdownMenu( + modifier = Modifier.widthIn(min = 150.dp), + expanded = isExpanded, + onDismissRequest = { isExpanded = false }, + ) { + mode.suggestions.forEach { suggestion -> + Text( + text = suggestion, + style = FloconTheme.typography.bodySmall, + modifier = Modifier.clickable { + value = suggestion + onValueChange(suggestion) + isExpanded = false + } + .padding( + vertical = 4.dp, + horizontal = 8.dp + ) + ) + } + } + } + } + } } } } +@Composable +private fun VariableInputField( + value: String, + placeholder: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + readOnly: Boolean = false +) { + val isValueEmpty = value.isEmpty() + + Box( + modifier = modifier.background( + color = FloconTheme.colorPalette.primary, + shape = RoundedCornerShape(2.dp), + ) + .padding(horizontal = 4.dp, vertical = 2.dp) + .width(IntrinsicSize.Min), + ) { + Text( + text = placeholder, + style = FloconTheme.typography.bodySmall, + color = FloconTheme.colorPalette.onSurface.copy(alpha = 0.4f), + modifier = Modifier.graphicsLayer { alpha = if (isValueEmpty) 1f else 0f }, + ) + BasicTextField( + value = value, + onValueChange = onValueChange, + maxLines = 1, + textStyle = FloconTheme.typography.bodySmall.copy( + color = FloconTheme.colorPalette.onSurface, + fontWeight = FontWeight.Bold, + ), + readOnly = readOnly, + cursorBrush = SolidColor(FloconTheme.colorPalette.onSurface), + ) + } +} + @Composable @Preview private fun DeeplinkVariablesPanelViewPreview() { FloconTheme { DeeplinkVariablesPanelView( - modifier = - Modifier.background(FloconTheme.colorPalette.primary) - .fillMaxWidth() - .padding(8.dp), - variables = - listOf( - DeeplinkVariableViewState( - name = "userId", - description = "The user id", - value = "" - ), - DeeplinkVariableViewState( - name = "env", - description = null, - value = "staging" - ), - DeeplinkVariableViewState( - name = "token", - description = null, - value = "" - ), - ), - onVariableChanged = { _, _ -> }, + modifier = + Modifier.background(FloconTheme.colorPalette.primary) + .fillMaxWidth() + .padding(8.dp), + variables = + listOf( + DeeplinkVariableViewState( + name = "userId", + description = "The user id", + value = "", + mode = DeeplinkVariableViewState.Mode.Input, + ), + DeeplinkVariableViewState( + name = "env", + description = null, + value = "staging", + mode = + DeeplinkVariableViewState.Mode.AutoComplete( + listOf("dev", "staging", "prod") + ), + ), + DeeplinkVariableViewState( + name = "token", + description = null, + value = "", + mode = DeeplinkVariableViewState.Mode.Input, + ), + ), + onVariableChanged = { _, _ -> }, ) } } diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/DI.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/DI.kt index a6b74ba5d..eb4804066 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/DI.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/DI.kt @@ -7,6 +7,7 @@ import io.github.openflocon.data.local.dashboard.dashboardModule import io.github.openflocon.data.local.database.databaseModule import io.github.openflocon.data.local.deeplink.deeplinkModule import io.github.openflocon.data.local.deeplink.models.DeeplinkEntity +import io.github.openflocon.data.local.deeplink.models.DeeplinkVariableEntity import io.github.openflocon.data.local.device.deviceModule import io.github.openflocon.data.local.files.filesModule import io.github.openflocon.data.local.images.imagesModule @@ -34,6 +35,12 @@ val dataLocalModule = module { defaultDeserializer { DeeplinkEntity.Parameter.AutoComplete.serializer() } } + polymorphic(DeeplinkVariableEntity.Mode::class) { + subclass(DeeplinkVariableEntity.Mode.Input::class) + subclass(DeeplinkVariableEntity.Mode.AutoComplete::class) + + defaultDeserializer { DeeplinkVariableEntity.Mode.Input.serializer() } + } } } } diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/ModeConverter.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/ModeConverter.kt new file mode 100644 index 000000000..603399051 --- /dev/null +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/ModeConverter.kt @@ -0,0 +1,20 @@ +package io.github.openflocon.data.local.deeplink + +import androidx.room.TypeConverter +import io.github.openflocon.data.local.JSON +import io.github.openflocon.data.local.deeplink.models.DeeplinkVariableEntity +import kotlinx.serialization.json.Json +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class ModeConverter : KoinComponent { + + private val json by inject(JSON) + + @TypeConverter + fun fromContainerConfig(value: DeeplinkVariableEntity.Mode): String = json.encodeToString(value) + + @TypeConverter + fun toContainerConfig(value: String): DeeplinkVariableEntity.Mode = json.decodeFromString(value) + +} diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/mapper/Mapper.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/mapper/Mapper.kt index 20b664b97..51a02e194 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/mapper/Mapper.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/mapper/Mapper.kt @@ -25,6 +25,10 @@ fun DeeplinkEntity.toDomainModel( fun DeeplinkVariableEntity.toDomainModel(): DeeplinkVariableDomainModel = DeeplinkVariableDomainModel( name = name, + mode = when (mode) { + is DeeplinkVariableEntity.Mode.AutoComplete -> DeeplinkVariableDomainModel.Mode.AutoComplete(mode.suggestions) + DeeplinkVariableEntity.Mode.Input -> DeeplinkVariableDomainModel.Mode.Input + }, description = description ) @@ -60,7 +64,11 @@ fun DeeplinkVariableDomainModel.toEntity( name = name, packageName = deviceIdAndPackageName.packageName, description = description, - isHistory = isHistory + isHistory = isHistory, + mode = when (val mode = mode) { + is DeeplinkVariableDomainModel.Mode.AutoComplete -> DeeplinkVariableEntity.Mode.AutoComplete(mode.suggestions) + DeeplinkVariableDomainModel.Mode.Input -> DeeplinkVariableEntity.Mode.Input + } ) fun toDomainModels( diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/models/DeeplinkVariableEntity.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/models/DeeplinkVariableEntity.kt index 23a3c187c..632202853 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/models/DeeplinkVariableEntity.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/models/DeeplinkVariableEntity.kt @@ -8,6 +8,9 @@ import androidx.room.Index import androidx.room.PrimaryKey import io.github.openflocon.data.local.device.datasource.model.DeviceAppEntity import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator @Entity( indices = [ @@ -30,6 +33,23 @@ data class DeeplinkVariableEntity( val packageName: String, val name: String, val description: String?, - val isHistory: Boolean -) + val isHistory: Boolean, + val mode: Mode = Mode.Input +) { + + @Serializable + @JsonClassDiscriminator("type") + sealed interface Mode { + + @Serializable + @SerialName("input") + object Input : Mode + + @Serializable + @SerialName("auto_complete") + data class AutoComplete(val suggestions: List) : Mode + + } + +} diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinkVariableReceivedDataModel.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinkVariableReceivedDataModel.kt index 442325001..29394e25d 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinkVariableReceivedDataModel.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinkVariableReceivedDataModel.kt @@ -1,9 +1,28 @@ package com.flocon.data.remote.deeplink.models +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator @Serializable internal data class DeeplinkVariableReceivedDataModel( val name: String, + val mode: Mode = Mode.Input, val description: String? = null -) +) { + + @Serializable + @JsonClassDiscriminator("type") + sealed interface Mode { + + @Serializable + @SerialName("input") + object Input : Mode + + @Serializable + @SerialName("auto_complete") + data class AutoComplete(val suggestions: List) : Mode + + } + +} diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinksReceivedDataModel.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinksReceivedDataModel.kt index 1f5512ba5..a2ae9a44a 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinksReceivedDataModel.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinksReceivedDataModel.kt @@ -24,6 +24,10 @@ internal fun DeeplinksReceivedDataModel.toDomain(): Deeplinks = Deeplinks( variables = variables.map { DeeplinkVariableDomainModel( name = it.name, + mode = when (val mode = it.mode) { + is DeeplinkVariableReceivedDataModel.Mode.AutoComplete -> DeeplinkVariableDomainModel.Mode.AutoComplete(mode.suggestions) + DeeplinkVariableReceivedDataModel.Mode.Input -> DeeplinkVariableDomainModel.Mode.Input + }, description = it.description ) } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/DeeplinkVariableDomainModel.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/DeeplinkVariableDomainModel.kt index 1339cb9c6..e1c377c19 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/DeeplinkVariableDomainModel.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/DeeplinkVariableDomainModel.kt @@ -2,5 +2,15 @@ package io.github.openflocon.domain.deeplink.models data class DeeplinkVariableDomainModel( val name: String, + val mode: Mode, val description: String? -) +) { + + sealed interface Mode { + + object Input : Mode + + data class AutoComplete(val suggestions: List) : Mode + + } +} From 9c9001db2ff04c8a3632501ee0630dee452f7814 Mon Sep 17 00:00:00 2001 From: Raphael TEYSSANDIER Date: Fri, 6 Mar 2026 15:01:32 +0100 Subject: [PATCH 04/12] feat: Improve UI --- .../features/deeplinks/DeepLinkViewModel.kt | 125 ++++++------ .../features/deeplinks/view/DeeplinkScreen.kt | 178 ++++++++++++++---- 2 files changed, 200 insertions(+), 103 deletions(-) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt index 76646f431..35c8e2835 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/DeepLinkViewModel.kt @@ -22,64 +22,52 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString class DeepLinkViewModel( - private val dispatcherProvider: DispatcherProvider, - private val feedbackDisplayer: FeedbackDisplayer, - private val observeCurrentDeviceDeeplinkUseCase: ObserveCurrentDeviceDeeplinkUseCase, - private val observeCurrentDeviceDeeplinkHistoryUseCase: - ObserveCurrentDeviceDeeplinkHistoryUseCase, - private val executeDeeplinkUseCase: ExecuteDeeplinkUseCase, - private val removeFromDeeplinkHistoryUseCase: RemoveFromDeeplinkHistoryUseCase, + private val dispatcherProvider: DispatcherProvider, + private val feedbackDisplayer: FeedbackDisplayer, + observeCurrentDeviceDeeplinkUseCase: ObserveCurrentDeviceDeeplinkUseCase, + observeCurrentDeviceDeeplinkHistoryUseCase: ObserveCurrentDeviceDeeplinkHistoryUseCase, + private val executeDeeplinkUseCase: ExecuteDeeplinkUseCase, + private val removeFromDeeplinkHistoryUseCase: RemoveFromDeeplinkHistoryUseCase, ) : ViewModel() { private val variableValues = MutableStateFlow>(emptyMap()) - val state: StateFlow = - combine( - observeCurrentDeviceDeeplinkUseCase(), - observeCurrentDeviceDeeplinkHistoryUseCase(), - variableValues.asStateFlow() - ) { deepLinks, history, variablesValues -> - DeeplinkScreenState( - deepLinks = - mapToUi( - history = history, - deepLinks = deepLinks.deeplinks, - variableValues = variablesValues - ), - variables = - deepLinks.variables.map { variable -> - DeeplinkVariableViewState( - name = variable.name, - description = variable.description, - value = - variablesValues.getOrDefault( - variable.name, - "" - ), - mode = - when (val m = variable.mode) { - DeeplinkVariableDomainModel.Mode - .Input -> - DeeplinkVariableViewState - .Mode.Input - is DeeplinkVariableDomainModel.Mode.AutoComplete -> - DeeplinkVariableViewState - .Mode.AutoComplete( - m.suggestions - ) - }, - ) - } - ) + val state: StateFlow = combine( + observeCurrentDeviceDeeplinkUseCase(), + observeCurrentDeviceDeeplinkHistoryUseCase(), + variableValues.asStateFlow() + ) { deepLinks, history, variablesValues -> + DeeplinkScreenState( + deepLinks = mapToUi( + history = history, + deepLinks = deepLinks.deeplinks, + variableValues = variablesValues + ), + variables = deepLinks.variables.map { variable -> + DeeplinkVariableViewState( + name = variable.name, + description = variable.description, + value = variablesValues.getOrDefault( + variable.name, + "" + ), + mode = when (val m = variable.mode) { + DeeplinkVariableDomainModel.Mode.Input -> + DeeplinkVariableViewState.Mode.Input + + is DeeplinkVariableDomainModel.Mode.AutoComplete -> + DeeplinkVariableViewState.Mode.AutoComplete(m.suggestions) } - .onEach { println(it) } - .stateInWhileSubscribed(DeeplinkScreenState(emptyList(), emptyList())) + ) + } + ) + } + .stateInWhileSubscribed(DeeplinkScreenState(emptyList(), emptyList())) fun setVariable(name: String, value: String) { variableValues.update { current -> current + (name to value) } @@ -88,11 +76,11 @@ class DeepLinkViewModel( fun removeFromHistory(viewState: DeeplinkViewState) { viewModelScope.launch(dispatcherProvider.viewModel) { removeFromDeeplinkHistoryUseCase( - deeplinkId = viewState.deeplinkId, + deeplinkId = viewState.deeplinkId, ) feedbackDisplayer.displayMessage( - getString(Res.string.deeplink_removed), - type = FeedbackDisplayer.MessageType.Error, + getString(Res.string.deeplink_removed), + type = FeedbackDisplayer.MessageType.Error, ) } } @@ -102,31 +90,30 @@ class DeepLinkViewModel( val numberOfTextFields = viewState.parts.count { it is DeeplinkPart.TextField } if (numberOfTextFields != values.values.filterNot { it.isBlank() }.size) { feedbackDisplayer.displayMessage( - getString(Res.string.fill_deeplink_parts), - type = FeedbackDisplayer.MessageType.Error, + getString(Res.string.fill_deeplink_parts), + type = FeedbackDisplayer.MessageType.Error, ) return@launch } val currentVariableValues = variableValues.value - val deeplink = - viewState.parts.joinToString(separator = "") { - when (it) { - is DeeplinkPart.Text -> it.value - is DeeplinkPart.TextField -> values[it] ?: "" - is DeeplinkPart.Variable -> currentVariableValues[it.value] ?: it.value - } - } + val deeplink = viewState.parts.joinToString(separator = "") { + when (it) { + is DeeplinkPart.Text -> it.value + is DeeplinkPart.TextField -> values[it] ?: "" + is DeeplinkPart.Variable -> currentVariableValues[it.value] ?: it.value + } + } executeDeeplinkUseCase( - deeplink = deeplink, - deeplinkId = viewState.deeplinkId, - saveIntoHistory = viewState.deeplinkId == -1L || numberOfTextFields != 0 - ) - .alsoFailure { - it.printStackTrace() - feedbackDisplayer.displayMessage(message = "Error while sending deeplink") - } + deeplink = deeplink, + deeplinkId = viewState.deeplinkId, + saveIntoHistory = viewState.deeplinkId == -1L || numberOfTextFields != 0 + ) + .alsoFailure { + it.printStackTrace() + feedbackDisplayer.displayMessage(message = "Error while sending deeplink") + } } } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkScreen.kt index 1724b0030..c24a36e08 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkScreen.kt @@ -5,18 +5,26 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.openflocon.flocondesktop.features.deeplinks.DeepLinkViewModel @@ -30,7 +38,6 @@ import io.github.openflocon.library.designsystem.components.FloconFeature import io.github.openflocon.library.designsystem.components.FloconPageTopBar import io.github.openflocon.library.designsystem.components.FloconVerticalScrollbar import io.github.openflocon.library.designsystem.components.rememberFloconScrollbarAdapter -import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel @Composable @@ -55,59 +62,153 @@ private fun DeeplinkScreen( setVariable: (name: String, value: String) -> Unit, modifier: Modifier = Modifier, ) { - val listState = rememberLazyListState() - val scrollAdapter = rememberFloconScrollbarAdapter(listState) + val variableValues by remember(state.variables) { + derivedStateOf { state.variables.associate { it.name to it.value } } + } + val deepLinks by remember(state.deepLinks) { + derivedStateOf { state.deepLinks.filter { !it.isHistory } } + } + val history by remember(state.deepLinks) { derivedStateOf { state.deepLinks.filter { it.isHistory } } } FloconFeature(modifier = modifier) { + // Top bar: freeform input only FloconPageTopBar( modifier = Modifier.fillMaxWidth(), filterBar = { - Column { - if (state.variables.isNotEmpty()) { - DeeplinkVariablesPanelView( - variables = state.variables, - onVariableChanged = setVariable, - modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp), + DeeplinkFreeformItemView( + submit = submit, + modifier = Modifier.fillMaxWidth(), + ) + } + ) + + // Main body — two rows of equal weight + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxSize() + ) { + // Top half: variables (left) | deeplinks (right) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .weight(0.5f) + .fillMaxWidth() + ) { + // Left: Variables + DeeplinkPanel( + title = "Variables", + modifier = Modifier + .weight(0.3f) + .fillMaxHeight() + .clip(FloconTheme.shapes.medium) + .background(FloconTheme.colorPalette.primary), + ) { + DeeplinkVariablesPanelView( + variables = state.variables, + onVariableChanged = setVariable, + modifier = Modifier.fillMaxWidth().padding(8.dp), + ) + } + + // Right: Deeplinks (non-history) + DeeplinkScrollablePanel( + title = "Deeplinks", + modifier = Modifier + .weight(0.7f) + .fillMaxHeight() + .clip(FloconTheme.shapes.medium) + .background(FloconTheme.colorPalette.primary), + ) { + items(deepLinks) { item -> + DeeplinkItemView( + submit = submit, + removeFromHistory = removeFromHistory, + item = item, + variableValues = variableValues, + modifier = Modifier.fillMaxWidth(), ) } - DeeplinkFreeformItemView( - submit = submit, - modifier = Modifier.fillMaxWidth(), - ) } } - ) - Box( - modifier = - Modifier.fillMaxSize() + // Bottom half: History + DeeplinkScrollablePanel( + title = "History", + modifier = Modifier + .weight(0.5f) + .fillMaxWidth() .clip(FloconTheme.shapes.medium) .background(FloconTheme.colorPalette.primary) - ) { - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(all = 8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), ) { - itemsIndexed(state.deepLinks) { _, item -> + items(history) { item -> DeeplinkItemView( submit = submit, removeFromHistory = removeFromHistory, item = item, - variableValues = state.variables.associate { it.name to it.value }, + variableValues = variableValues, modifier = Modifier.fillMaxWidth(), ) } } + } + } +} + +/** Panel with a header label and freeform content slot. */ +@Composable +private fun DeeplinkPanel( + title: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Column(modifier = modifier) { + DeeplinkPanelHeader(title) + content() + } +} + +/** Panel with a header label and a scrollable LazyColumn content slot. */ +@Composable +private fun DeeplinkScrollablePanel( + title: String, + modifier: Modifier = Modifier, + content: LazyListScope.() -> Unit, +) { + val listState = rememberLazyListState() + val scrollAdapter = rememberFloconScrollbarAdapter(listState) + + Column(modifier = modifier) { + DeeplinkPanelHeader(title) + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(all = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + content = content, + ) FloconVerticalScrollbar( adapter = scrollAdapter, - modifier = Modifier.fillMaxHeight().align(Alignment.TopEnd) + modifier = Modifier.fillMaxHeight().align(Alignment.TopEnd), ) } } } +@Composable +private fun DeeplinkPanelHeader(title: String) { + Text( + text = title, + style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold), + color = FloconTheme.colorPalette.onPrimary, + modifier = Modifier + .fillMaxWidth() + .background(FloconTheme.colorPalette.primary) + .padding(horizontal = 12.dp, vertical = 6.dp), + ) + HorizontalDivider(color = FloconTheme.colorPalette.surface) +} + @Composable @Preview private fun DeeplinkScreenPreview() { @@ -115,12 +216,12 @@ private fun DeeplinkScreenPreview() { DeeplinkScreen( state = DeeplinkScreenState( - deepLinks = - listOf( - previewDeeplinkViewState(), - previewDeeplinkViewState(), - previewDeeplinkViewState(), - ), + deepLinks = listOf( + previewDeeplinkViewState(), + previewDeeplinkViewState(), + previewDeeplinkViewState().copy(isHistory = true), + previewDeeplinkViewState().copy(isHistory = true), + ), variables = listOf( DeeplinkVariableViewState( @@ -131,7 +232,16 @@ private fun DeeplinkScreenPreview() { DeeplinkVariableViewState( name = "env", description = null, - value = "staging" + value = "staging", + mode = + DeeplinkVariableViewState.Mode + .AutoComplete( + listOf( + "dev", + "staging", + "prod" + ) + ), ), ), ), From 70037930d4c3cdb1f99d8f426fdf84aae58e6455 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Sat, 7 Mar 2026 23:59:55 +0100 Subject: [PATCH 05/12] fix: JVM --- FloconAndroid/datastores-no-op/build.gradle.kts | 8 ++++++++ FloconAndroid/datastores/build.gradle.kts | 8 ++++++++ FloconAndroid/flocon/build.gradle.kts | 3 ++- .../grpc/grpc-interceptor-base/build.gradle.kts | 8 ++++++++ .../grpc/grpc-interceptor-lite/build.gradle.kts | 9 ++++++++- FloconAndroid/grpc/grpc-interceptor/build.gradle.kts | 8 ++++++++ FloconAndroid/ktor-interceptor-no-op/build.gradle.kts | 2 ++ FloconAndroid/ktor-interceptor/build.gradle.kts | 2 ++ FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts | 7 +++++++ FloconAndroid/okhttp-interceptor/build.gradle.kts | 8 ++++++++ FloconAndroid/sample-android-only/build.gradle.kts | 7 +++++++ 11 files changed, 68 insertions(+), 2 deletions(-) diff --git a/FloconAndroid/datastores-no-op/build.gradle.kts b/FloconAndroid/datastores-no-op/build.gradle.kts index ba7c2215c..657362646 100644 --- a/FloconAndroid/datastores-no-op/build.gradle.kts +++ b/FloconAndroid/datastores-no-op/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -31,6 +33,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + dependencies { implementation(project(":flocon-base")) diff --git a/FloconAndroid/datastores/build.gradle.kts b/FloconAndroid/datastores/build.gradle.kts index 443e4f57c..37689886a 100644 --- a/FloconAndroid/datastores/build.gradle.kts +++ b/FloconAndroid/datastores/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -31,6 +33,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + dependencies { implementation(project(":flocon-base")) diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index 6ce6b5b1c..b689b76cf 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) @@ -71,7 +73,6 @@ kotlin { } } - buildConfig { packageName("io.github.openflocon.flocondesktop") diff --git a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts index 81df01b2d..9915e76c8 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -31,6 +33,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + dependencies { implementation(project(":flocon-base")) diff --git a/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts index 2058eb760..8e4738e9f 100644 --- a/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -31,6 +33,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + dependencies { api(project(":grpc:grpc-interceptor-base")) @@ -38,7 +46,6 @@ dependencies { implementation(libs.gson) } - mavenPublishing { publishToMavenCentral(automaticRelease = true) diff --git a/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts index 517cedee0..6feb409cf 100644 --- a/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -31,6 +33,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + dependencies { api(project(":grpc:grpc-interceptor-base")) diff --git a/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts b/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts index 5f3ea524e..3e313610c 100644 --- a/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) diff --git a/FloconAndroid/ktor-interceptor/build.gradle.kts b/FloconAndroid/ktor-interceptor/build.gradle.kts index 84c1c918c..3dd5b48e4 100644 --- a/FloconAndroid/ktor-interceptor/build.gradle.kts +++ b/FloconAndroid/ktor-interceptor/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) diff --git a/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts b/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts index 03e7d4d25..84440c5fb 100644 --- a/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -36,6 +38,11 @@ dependencies { implementation(libs.okhttp3.okhttp) } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} mavenPublishing { publishToMavenCentral(automaticRelease = true) diff --git a/FloconAndroid/okhttp-interceptor/build.gradle.kts b/FloconAndroid/okhttp-interceptor/build.gradle.kts index b4643a064..649558b76 100644 --- a/FloconAndroid/okhttp-interceptor/build.gradle.kts +++ b/FloconAndroid/okhttp-interceptor/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) @@ -31,6 +33,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + dependencies { implementation(project(":flocon-base")) diff --git a/FloconAndroid/sample-android-only/build.gradle.kts b/FloconAndroid/sample-android-only/build.gradle.kts index 7c903e608..55ed7af12 100644 --- a/FloconAndroid/sample-android-only/build.gradle.kts +++ b/FloconAndroid/sample-android-only/build.gradle.kts @@ -1,4 +1,5 @@ import com.google.protobuf.gradle.id +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.application) @@ -63,6 +64,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + val useMaven = false dependencies { if(useMaven) { From d4ed6289584ab2c98ef364a27e247c9a9bf64f70 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Mon, 9 Mar 2026 20:22:27 +0100 Subject: [PATCH 06/12] fix: Revert to 11 --- FloconAndroid/datastores-no-op/build.gradle.kts | 6 +++--- FloconAndroid/datastores/build.gradle.kts | 6 +++--- FloconAndroid/flocon-base/build.gradle.kts | 4 ++-- FloconAndroid/flocon-no-op/build.gradle.kts | 4 ++-- FloconAndroid/flocon/build.gradle.kts | 4 ++-- FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts | 6 +++--- FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts | 6 +++--- FloconAndroid/grpc/grpc-interceptor/build.gradle.kts | 6 +++--- FloconAndroid/ktor-interceptor-no-op/build.gradle.kts | 4 ++-- FloconAndroid/ktor-interceptor/build.gradle.kts | 4 ++-- FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts | 6 +++--- FloconAndroid/okhttp-interceptor/build.gradle.kts | 6 +++--- FloconAndroid/sample-android-only/build.gradle.kts | 6 +++--- FloconAndroid/sample-multiplatform/build.gradle.kts | 4 ++-- 14 files changed, 36 insertions(+), 36 deletions(-) diff --git a/FloconAndroid/datastores-no-op/build.gradle.kts b/FloconAndroid/datastores-no-op/build.gradle.kts index 657362646..859f94eac 100644 --- a/FloconAndroid/datastores-no-op/build.gradle.kts +++ b/FloconAndroid/datastores-no-op/build.gradle.kts @@ -28,14 +28,14 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } } kotlin { compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_11) } } diff --git a/FloconAndroid/datastores/build.gradle.kts b/FloconAndroid/datastores/build.gradle.kts index 37689886a..5f784f502 100644 --- a/FloconAndroid/datastores/build.gradle.kts +++ b/FloconAndroid/datastores/build.gradle.kts @@ -28,14 +28,14 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } } kotlin { compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_11) } } diff --git a/FloconAndroid/flocon-base/build.gradle.kts b/FloconAndroid/flocon-base/build.gradle.kts index 8e3f0bf2b..0cbd1bb78 100644 --- a/FloconAndroid/flocon-base/build.gradle.kts +++ b/FloconAndroid/flocon-base/build.gradle.kts @@ -63,8 +63,8 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } } diff --git a/FloconAndroid/flocon-no-op/build.gradle.kts b/FloconAndroid/flocon-no-op/build.gradle.kts index fde9da46d..d94c56561 100644 --- a/FloconAndroid/flocon-no-op/build.gradle.kts +++ b/FloconAndroid/flocon-no-op/build.gradle.kts @@ -64,8 +64,8 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } } diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index b689b76cf..bcabc819e 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -101,8 +101,8 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") diff --git a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts index 9915e76c8..e43bb3211 100644 --- a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts @@ -28,14 +28,14 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } } kotlin { compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_11) } } diff --git a/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts index 8e4738e9f..cc99dcd78 100644 --- a/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor-lite/build.gradle.kts @@ -28,14 +28,14 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } } kotlin { compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_11) } } diff --git a/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts index 6feb409cf..c39e77e0f 100644 --- a/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts +++ b/FloconAndroid/grpc/grpc-interceptor/build.gradle.kts @@ -28,14 +28,14 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } } kotlin { compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_11) } } diff --git a/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts b/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts index 3e313610c..0f1c45b49 100644 --- a/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts @@ -67,8 +67,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } } diff --git a/FloconAndroid/ktor-interceptor/build.gradle.kts b/FloconAndroid/ktor-interceptor/build.gradle.kts index 3dd5b48e4..73f545abb 100644 --- a/FloconAndroid/ktor-interceptor/build.gradle.kts +++ b/FloconAndroid/ktor-interceptor/build.gradle.kts @@ -70,8 +70,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } } diff --git a/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts b/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts index 84440c5fb..897fd4fc9 100644 --- a/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts @@ -28,8 +28,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } } @@ -40,7 +40,7 @@ dependencies { kotlin { compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_11) } } diff --git a/FloconAndroid/okhttp-interceptor/build.gradle.kts b/FloconAndroid/okhttp-interceptor/build.gradle.kts index 649558b76..118ccbcbd 100644 --- a/FloconAndroid/okhttp-interceptor/build.gradle.kts +++ b/FloconAndroid/okhttp-interceptor/build.gradle.kts @@ -28,14 +28,14 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } } kotlin { compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_11) } } diff --git a/FloconAndroid/sample-android-only/build.gradle.kts b/FloconAndroid/sample-android-only/build.gradle.kts index 55ed7af12..562891197 100644 --- a/FloconAndroid/sample-android-only/build.gradle.kts +++ b/FloconAndroid/sample-android-only/build.gradle.kts @@ -55,8 +55,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } buildFeatures { compose = true @@ -66,7 +66,7 @@ android { kotlin { compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_11) } } diff --git a/FloconAndroid/sample-multiplatform/build.gradle.kts b/FloconAndroid/sample-multiplatform/build.gradle.kts index ea5cb46e1..7c6842162 100644 --- a/FloconAndroid/sample-multiplatform/build.gradle.kts +++ b/FloconAndroid/sample-multiplatform/build.gradle.kts @@ -136,8 +136,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } buildFeatures { From be27e2e9de25ae017fd7b2622543b9db3dfeeb2e Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Mon, 9 Mar 2026 20:23:39 +0100 Subject: [PATCH 07/12] fix: Discussion --- .../flocon/plugins/deeplinks/model/DeeplinksRemote.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt index 0f229b039..e6e9015cc 100644 --- a/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt +++ b/FloconAndroid/flocon/src/commonMain/kotlin/io/github/openflocon/flocon/plugins/deeplinks/model/DeeplinksRemote.kt @@ -14,7 +14,7 @@ internal sealed interface DeeplinkParameterRemote { @Serializable @SerialName("auto_complete") - class AutoComplete( + data class AutoComplete( override val name: String, val autoComplete: List ) : DeeplinkParameterRemote @@ -48,7 +48,7 @@ internal data class DeeplinkVariableRemote( @Serializable @SerialName("input") - object Input : Mode + data object Input : Mode @Serializable @SerialName("auto_complete") From db5d0408b6a388f49d21df7f496d02923e318f35 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Mon, 9 Mar 2026 20:38:08 +0100 Subject: [PATCH 08/12] fix: Revert versionning --- FloconAndroid/flocon-base/build.gradle.kts | 9 ++++++++- FloconAndroid/flocon/build.gradle.kts | 7 +++++-- FloconAndroid/gradle/libs.versions.toml | 6 +++--- FloconAndroid/ktor-interceptor/build.gradle.kts | 6 +++++- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/FloconAndroid/flocon-base/build.gradle.kts b/FloconAndroid/flocon-base/build.gradle.kts index 0cbd1bb78..4c63dfe76 100644 --- a/FloconAndroid/flocon-base/build.gradle.kts +++ b/FloconAndroid/flocon-base/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) @@ -5,7 +7,11 @@ plugins { } kotlin { - androidTarget() + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } jvm() @@ -62,6 +68,7 @@ android { ) } } + compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index bcabc819e..337f2b1e9 100644 --- a/FloconAndroid/flocon/build.gradle.kts +++ b/FloconAndroid/flocon/build.gradle.kts @@ -9,7 +9,11 @@ plugins { } kotlin { - androidTarget() + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } jvm() @@ -79,7 +83,6 @@ buildConfig { buildConfigField("APP_VERSION", System.getenv("PROJECT_VERSION_NAME") ?: project.property("floconVersion") as String) } - android { namespace = "io.github.openflocon.flocon" compileSdk = 36 diff --git a/FloconAndroid/gradle/libs.versions.toml b/FloconAndroid/gradle/libs.versions.toml index 6458cf276..2b6541ed6 100644 --- a/FloconAndroid/gradle/libs.versions.toml +++ b/FloconAndroid/gradle/libs.versions.toml @@ -4,7 +4,7 @@ apollo = "4.0.0" coilCompose = "3.2.0" compose = "1.9.0" datastorePreferences = "1.1.7" -kotlin = "2.2.0" +kotlin = "2.1.0" mavenPublish = "0.34.0" coreKtx = "1.16.0" junit = "4.13.2" @@ -12,7 +12,7 @@ junitVersion = "1.2.1" espressoCore = "3.6.1" kotlinxCoroutinesBom = "1.10.2" kotlinxSerialization = "1.9.0" -ktor = "3.4.1" +ktor = "3.2.3" lifecycleRuntimeKtx = "2.9.1" activityCompose = "1.10.1" composeBom = "2025.06.01" @@ -26,7 +26,7 @@ grpc = "1.70.0" protobufPlugin = "0.9.5" grpcKotlin = "1.4.3" protobuf = "4.26.1" -ksp = "2.3.6" +ksp = "2.1.0-1.0.29" processPhoenix = "3.0.0" sqlite = "2.5.2" sqliteJdbc = "3.50.3.0" diff --git a/FloconAndroid/ktor-interceptor/build.gradle.kts b/FloconAndroid/ktor-interceptor/build.gradle.kts index 73f545abb..b7d332a7f 100644 --- a/FloconAndroid/ktor-interceptor/build.gradle.kts +++ b/FloconAndroid/ktor-interceptor/build.gradle.kts @@ -7,7 +7,11 @@ plugins { } kotlin { - androidTarget() + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } jvm() From 7c848f59dfe8e367955f5510ea8d2677068711b9 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Mon, 9 Mar 2026 20:47:34 +0100 Subject: [PATCH 09/12] fix: Version --- FloconAndroid/gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FloconAndroid/gradle/libs.versions.toml b/FloconAndroid/gradle/libs.versions.toml index 2b6541ed6..32ea39e80 100644 --- a/FloconAndroid/gradle/libs.versions.toml +++ b/FloconAndroid/gradle/libs.versions.toml @@ -11,7 +11,7 @@ junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" kotlinxCoroutinesBom = "1.10.2" -kotlinxSerialization = "1.9.0" +kotlinxSerialization = "1.8.0" ktor = "3.2.3" lifecycleRuntimeKtx = "2.9.1" activityCompose = "1.10.1" From 9e0064115c64e86bad7d431110f5699e879ed522 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Mon, 9 Mar 2026 21:15:54 +0100 Subject: [PATCH 10/12] fix: Version --- FloconAndroid/flocon-no-op/build.gradle.kts | 8 +++++++- FloconAndroid/ktor-interceptor-no-op/build.gradle.kts | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/FloconAndroid/flocon-no-op/build.gradle.kts b/FloconAndroid/flocon-no-op/build.gradle.kts index d94c56561..963d4a6d6 100644 --- a/FloconAndroid/flocon-no-op/build.gradle.kts +++ b/FloconAndroid/flocon-no-op/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) @@ -5,7 +7,11 @@ plugins { } kotlin { - androidTarget() + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } jvm() diff --git a/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts b/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts index 0f1c45b49..3f2d23cd9 100644 --- a/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts +++ b/FloconAndroid/ktor-interceptor-no-op/build.gradle.kts @@ -7,7 +7,11 @@ plugins { } kotlin { - androidTarget() + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } jvm() From 7a704332e735f516704ed718838cdf764834a12b Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Mon, 9 Mar 2026 21:23:57 +0100 Subject: [PATCH 11/12] fix: Version --- FloconAndroid/sample-multiplatform/build.gradle.kts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/FloconAndroid/sample-multiplatform/build.gradle.kts b/FloconAndroid/sample-multiplatform/build.gradle.kts index 7c6842162..e2aa52888 100644 --- a/FloconAndroid/sample-multiplatform/build.gradle.kts +++ b/FloconAndroid/sample-multiplatform/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.application) @@ -9,7 +11,11 @@ plugins { } kotlin { - androidTarget() + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } jvm("desktop") From bcd46404208942cbae09ad9a611fd3164bbedc16 Mon Sep 17 00:00:00 2001 From: doTTTTT Date: Wed, 25 Mar 2026 22:39:32 +0100 Subject: [PATCH 12/12] fix: Schema --- .../80.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/FloconDesktop/composeApp/schemas/io.github.openflocon.flocondesktop.common.db.AppDatabase/80.json b/FloconDesktop/composeApp/schemas/io.github.openflocon.flocondesktop.common.db.AppDatabase/80.json index 27058f405..bc8281d25 100644 --- a/FloconDesktop/composeApp/schemas/io.github.openflocon.flocondesktop.common.db.AppDatabase/80.json +++ b/FloconDesktop/composeApp/schemas/io.github.openflocon.flocondesktop.common.db.AppDatabase/80.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 80, - "identityHash": "5b43a0b8688e41bbe05233fd1aee26a9", + "identityHash": "894da66ff62fe4653dcff3fea6827e2e", "entities": [ { "tableName": "FloconNetworkCallEntity", @@ -1345,7 +1345,7 @@ }, { "tableName": "MockNetworkEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mockId` TEXT NOT NULL, `deviceId` TEXT, `packageName` TEXT, `isEnabled` INTEGER NOT NULL, `response` TEXT NOT NULL, `expectation_urlPattern` TEXT NOT NULL, `expectation_method` TEXT NOT NULL, PRIMARY KEY(`mockId`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mockId` TEXT NOT NULL, `deviceId` TEXT, `packageName` TEXT, `isEnabled` INTEGER NOT NULL, `response` TEXT NOT NULL, `displayName` TEXT NOT NULL, `expectation_urlPattern` TEXT NOT NULL, `expectation_method` TEXT NOT NULL, PRIMARY KEY(`mockId`), FOREIGN KEY(`deviceId`, `packageName`) REFERENCES `DeviceAppEntity`(`deviceId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "mockId", @@ -1375,6 +1375,12 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, { "fieldPath": "expectation.urlPattern", "columnName": "expectation_urlPattern", @@ -1875,7 +1881,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5b43a0b8688e41bbe05233fd1aee26a9')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '894da66ff62fe4653dcff3fea6827e2e')" ] } } \ No newline at end of file