diff --git a/FloconAndroid/datastores-no-op/build.gradle.kts b/FloconAndroid/datastores-no-op/build.gradle.kts index 31aafa279..859f94eac 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) @@ -29,8 +31,11 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } diff --git a/FloconAndroid/datastores/build.gradle.kts b/FloconAndroid/datastores/build.gradle.kts index 47a353b61..5f784f502 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) @@ -29,8 +31,11 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } diff --git a/FloconAndroid/flocon-base/build.gradle.kts b/FloconAndroid/flocon-base/build.gradle.kts index d92d2d3e4..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) @@ -6,10 +8,8 @@ plugins { kotlin { androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } @@ -68,6 +68,7 @@ android { ) } } + compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 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..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 @@ -3,48 +3,110 @@ 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() + ) + } + + 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 +) { + private var mode: DeeplinkVariable.Mode = DeeplinkVariable.Mode.Input + + var description: String? = null + + fun autoComplete(suggestions: List) { + mode = DeeplinkVariable.Mode.AutoComplete(suggestions) } - fun build() : List { - return parameters.values.toList() + internal fun build(): DeeplinkVariable { + return DeeplinkVariable( + name = name, + mode = mode, + description = description + ) } + +} + +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() 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 build(): List { - return deeplinks.toList() + fun deeplink(link: String, block: DeeplinkLinkBuilder.() -> Unit = {}) { + val deeplink = DeeplinkLinkBuilder(link).apply(block) + .build() + + deeplinks.add(deeplink) } + + 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-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..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) @@ -6,10 +8,8 @@ plugins { kotlin { androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } diff --git a/FloconAndroid/flocon/build.gradle.kts b/FloconAndroid/flocon/build.gradle.kts index fc071c989..337f2b1e9 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) @@ -8,10 +10,8 @@ plugins { kotlin { androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } @@ -77,14 +77,12 @@ kotlin { } } - buildConfig { packageName("io.github.openflocon.flocondesktop") 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/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/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/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..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 @@ -4,22 +4,50 @@ 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, + 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) { + 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..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 @@ -1,22 +1,65 @@ +@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") + data 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 mode: Mode = Mode.Input, + val description: String? = null +) { + + @Serializable + @JsonClassDiscriminator("type") + sealed interface Mode { + + @Serializable + @SerialName("input") + data object Input : Mode + + @Serializable + @SerialName("auto_complete") + data class AutoComplete(val suggestions: List) : Mode + + } + +} + @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..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.7.1" +kotlinxSerialization = "1.8.0" ktor = "3.2.3" lifecycleRuntimeKtx = "2.9.1" activityCompose = "1.10.1" diff --git a/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts b/FloconAndroid/grpc/grpc-interceptor-base/build.gradle.kts index cc2faf5f2..e43bb3211 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) @@ -29,8 +31,11 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" +} + +kotlin { + compilerOptions { + 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 250472452..cc99dcd78 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) @@ -29,8 +31,11 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } @@ -41,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 698b2e7e7..c39e77e0f 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) @@ -29,8 +31,11 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } 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..3f2d23cd9 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) @@ -6,10 +8,8 @@ plugins { kotlin { androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } diff --git a/FloconAndroid/ktor-interceptor/build.gradle.kts b/FloconAndroid/ktor-interceptor/build.gradle.kts index 0c849b57c..b7d332a7f 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) @@ -6,10 +8,8 @@ plugins { kotlin { androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } diff --git a/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts b/FloconAndroid/okhttp-interceptor-no-op/build.gradle.kts index 5fa0e7246..897fd4fc9 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) @@ -29,9 +31,6 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" - } } dependencies { @@ -39,6 +38,11 @@ dependencies { implementation(libs.okhttp3.okhttp) } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } +} mavenPublishing { publishToMavenCentral(automaticRelease = true) diff --git a/FloconAndroid/okhttp-interceptor/build.gradle.kts b/FloconAndroid/okhttp-interceptor/build.gradle.kts index a7657f35c..118ccbcbd 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) @@ -29,8 +31,11 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } 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..562891197 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) @@ -57,15 +58,18 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "11" - } buildFeatures { compose = true buildConfig = true } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } +} + val useMaven = false dependencies { if(useMaven) { 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..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 @@ -5,19 +5,27 @@ import io.github.openflocon.flocon.plugins.deeplinks.deeplinks fun initializeDeeplinks() { Flocon.deeplinks { - 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", + variable("test_variable") + 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("[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/FloconAndroid/sample-multiplatform/build.gradle.kts b/FloconAndroid/sample-multiplatform/build.gradle.kts index 46eafe40d..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) @@ -10,20 +12,12 @@ plugins { kotlin { androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } - jvm("desktop") { - compilations.all { - kotlinOptions { - jvmTarget = "11" - } - } - } + jvm("desktop") listOf( iosX64(), 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/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..bc8281d25 --- /dev/null +++ b/FloconDesktop/composeApp/schemas/io.github.openflocon.flocondesktop.common.db.AppDatabase/80.json @@ -0,0 +1,1887 @@ +{ + "formatVersion": 1, + "database": { + "version": 80, + "identityHash": "894da66ff62fe4653dcff3fea6827e2e", + "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, `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", + "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": "displayName", + "columnName": "displayName", + "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, '894da66ff62fe4653dcff3fea6827e2e')" + ] + } +} \ 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..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,8 +21,11 @@ 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 +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 @@ -50,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, @@ -64,6 +67,7 @@ import kotlinx.coroutines.Dispatchers SuccessQueryEntity::class, FavoriteQueryEntity::class, DeeplinkEntity::class, + DeeplinkVariableEntity::class, AnalyticsItemEntity::class, NetworkFilterEntity::class, NetworkSettingsEntity::class, @@ -74,13 +78,14 @@ import kotlinx.coroutines.Dispatchers DeviceAppEntity::class, DatabaseTableEntity::class, CrashReportEntity::class, - DatabaseQueryLogEntity::class, - ], + DatabaseQueryLogEntity::class + ] ) @TypeConverters( DashboardConverters::class, MapStringsConverters::class, ListStringsConverters::class, + ModeConverter::class ) abstract class AppDatabase : RoomDatabase() { abstract val networkDao: FloconNetworkDao @@ -91,6 +96,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..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 @@ -6,48 +6,72 @@ 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.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 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.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, + observeCurrentDeviceDeeplinkUseCase: ObserveCurrentDeviceDeeplinkUseCase, + 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, + 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, + "" + ), + mode = when (val m = variable.mode) { + DeeplinkVariableDomainModel.Mode.Input -> + DeeplinkVariableViewState.Mode.Input + + is DeeplinkVariableDomainModel.Mode.AutoComplete -> + DeeplinkVariableViewState.Mode.AutoComplete(m.suggestions) + } + ) + } ) + } + .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,10 +96,12 @@ class DeepLinkViewModel( 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 } } @@ -84,6 +110,10 @@ 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..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,15 +12,19 @@ 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): DeeplinkViewState = DeeplinkViewState( +internal fun mapToUi( + deepLink: DeeplinkDomainModel, + isHistory: Boolean, + variableValues: Map +): DeeplinkViewState = DeeplinkViewState( label = deepLink.label, description = deepLink.description, deeplinkId = deepLink.id, @@ -28,11 +32,19 @@ 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, + variableValues = variableValues + ) + } ) -internal fun parseDeeplinkString(input: String, deepLink: DeeplinkDomainModel): List { +internal fun parseDeeplinkString( + input: String, + deepLink: DeeplinkDomainModel, + variableValues: Map +): List { val regex = "\\[([^\\[\\]]*)\\]".toRegex() // Regex pour trouver [quelquechose] val result = mutableListOf() var lastIndex = 0 @@ -50,12 +62,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 = variableValues[parameter.variableName] ?: "{${parameter.variableName}}" + ) + } ) - ) + } 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/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..c016c9967 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/model/DeeplinkVariableViewState.kt @@ -0,0 +1,18 @@ +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, + 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/DeeplinkItemView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkItemView.kt index 6b382cc34..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,60 +140,77 @@ 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, + ), ) } } @@ -201,37 +218,38 @@ private fun TextFieldPart( @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, ) } } @@ -241,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..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 @@ -3,22 +3,34 @@ 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.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 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 @@ -26,35 +38,40 @@ 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 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) + 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 - ) { + FloconFeature(modifier = modifier) { + // Top bar: freeform input only FloconPageTopBar( modifier = Modifier.fillMaxWidth(), filterBar = { @@ -65,49 +82,173 @@ private fun DeeplinkScreen( } ) - Box( - modifier = Modifier - .fillMaxSize() - .clip(FloconTheme.shapes.medium) - .background(FloconTheme.colorPalette.primary) + // Main body — two rows of equal weight + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxSize() ) { - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(all = 8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), + // 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(), + ) + } + } + } + + // Bottom half: History + DeeplinkScrollablePanel( + title = "History", + modifier = Modifier + .weight(0.5f) + .fillMaxWidth() + .clip(FloconTheme.shapes.medium) + .background(FloconTheme.colorPalette.primary) ) { - itemsIndexed(deepLinks) { index, item -> + items(history) { item -> DeeplinkItemView( submit = submit, removeFromHistory = removeFromHistory, item = item, + 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() { FloconTheme { DeeplinkScreen( - deepLinks = listOf( - previewDeeplinkViewState(), - previewDeeplinkViewState(), - previewDeeplinkViewState(), - ), + state = + DeeplinkScreenState( + deepLinks = listOf( + previewDeeplinkViewState(), + previewDeeplinkViewState(), + previewDeeplinkViewState().copy(isHistory = true), + previewDeeplinkViewState().copy(isHistory = true), + ), + variables = + listOf( + DeeplinkVariableViewState( + name = "userId", + description = null, + value = "" + ), + DeeplinkVariableViewState( + name = "env", + description = null, + value = "staging", + mode = + DeeplinkVariableViewState.Mode + .AutoComplete( + listOf( + "dev", + "staging", + "prod" + ) + ), + ), + ), + ), 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..f5e7c06e6 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/deeplinks/view/DeeplinkVariablesPanelView.kt @@ -0,0 +1,217 @@ +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 +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.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 +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) }, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DeeplinkVariableChip( + variable: DeeplinkVariableViewState, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + var value by remember(variable.name) { mutableStateOf(variable.value) } + + 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, + ) + + when (val mode = variable.mode) { + DeeplinkVariableViewState.Mode.Input -> { + VariableInputField( + value = value, + placeholder = variable.name, + onValueChange = { + value = it + onValueChange(it) + }, + ) + } + + 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 = "", + 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/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..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 @@ -6,15 +6,44 @@ 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.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 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() } + } + polymorphic(DeeplinkVariableEntity.Mode::class) { + subclass(DeeplinkVariableEntity.Mode.Input::class) + subclass(DeeplinkVariableEntity.Mode.AutoComplete::class) + + defaultDeserializer { DeeplinkVariableEntity.Mode.Input.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/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/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..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 @@ -1,27 +1,35 @@ 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, + mode = when (mode) { + is DeeplinkVariableEntity.Mode.AutoComplete -> DeeplinkVariableDomainModel.Mode.AutoComplete(mode.suggestions) + DeeplinkVariableEntity.Mode.Input -> DeeplinkVariableDomainModel.Mode.Input }, + description = description ) fun DeeplinkDomainModel.toEntity( @@ -48,26 +56,68 @@ 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, + mode = when (val mode = mode) { + is DeeplinkVariableDomainModel.Mode.AutoComplete -> DeeplinkVariableEntity.Mode.AutoComplete(mode.suggestions) + DeeplinkVariableDomainModel.Mode.Input -> DeeplinkVariableEntity.Mode.Input + } +) + 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..632202853 --- /dev/null +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/deeplink/models/DeeplinkVariableEntity.kt @@ -0,0 +1,55 @@ +@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 +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator + +@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, + 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/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..29394e25d --- /dev/null +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/deeplink/models/DeeplinkVariableReceivedDataModel.kt @@ -0,0 +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 820b5e21a..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 @@ -1,24 +1,46 @@ 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, + mode = when (val mode = it.mode) { + is DeeplinkVariableReceivedDataModel.Mode.AutoComplete -> DeeplinkVariableDomainModel.Mode.AutoComplete(mode.suggestions) + DeeplinkVariableReceivedDataModel.Mode.Input -> DeeplinkVariableDomainModel.Mode.Input + }, + 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..e1c377c19 --- /dev/null +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/deeplink/models/DeeplinkVariableDomainModel.kt @@ -0,0 +1,16 @@ +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 + + } +} 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) }