From 065ac64f9f03f338f1dd5dffd6f7e72808a6f226 Mon Sep 17 00:00:00 2001 From: Yuriy Elistratov Date: Mon, 27 Jul 2026 01:04:13 +0400 Subject: [PATCH 1/3] build: Update version of version catalog and add konfeature-ui --- buildSrc/build.gradle.kts | 2 +- buildSrc/settings.gradle.kts | 5 +---- buildSrc/src/main/kotlin/Project.kt | 2 +- buildSrc/src/main/kotlin/convention-publish.gradle.kts | 5 +---- buildSrc/src/main/kotlin/internal/Versions.kt | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- plugins/plugin-konfeature/build.gradle.kts | 1 + settings.gradle.kts | 4 +++- 8 files changed, 10 insertions(+), 13 deletions(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 9650b00c..d3f0c122 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -18,7 +18,7 @@ kotlin { dependencies { implementation(rmr.infrastructure.publish) implementation(rmr.infrastructure.android) - implementation(libs.publish.gradlePlugin) + implementation(stack.publish.gradlePlugin) implementation(stack.gradle.android.cacheFixGradlePlugin) implementation(stack.kotlin.gradlePlugin) implementation(stack.kotlin.composeCompiler.gradlePlugin) diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts index b1a63539..64d47418 100644 --- a/buildSrc/settings.gradle.kts +++ b/buildSrc/settings.gradle.kts @@ -30,7 +30,7 @@ dependencyResolutionManagement { } versionCatalogs { - val version = "2026.02.23" // Keep it in sync with root settings.gradle.kts + val version = "2026.07.10" // Keep it in sync with root settings.gradle.kts create("rmr") { from("com.redmadrobot.versions:versions-redmadrobot:$version") } @@ -40,8 +40,5 @@ dependencyResolutionManagement { create("stack") { from("com.redmadrobot.versions:versions-stack:$version") } - create("libs") { - from(files("../gradle/libs.versions.toml")) - } } } diff --git a/buildSrc/src/main/kotlin/Project.kt b/buildSrc/src/main/kotlin/Project.kt index 2d5d1a2b..de802a53 100644 --- a/buildSrc/src/main/kotlin/Project.kt +++ b/buildSrc/src/main/kotlin/Project.kt @@ -1,7 +1,7 @@ object Project { const val MIN_SDK = 23 const val TARGET_SDK = 36 - const val COMPILE_SDK = 36 + const val COMPILE_SDK = 37 object BuildTypes { const val release = "release" diff --git a/buildSrc/src/main/kotlin/convention-publish.gradle.kts b/buildSrc/src/main/kotlin/convention-publish.gradle.kts index ccdfdb42..460dbb10 100644 --- a/buildSrc/src/main/kotlin/convention-publish.gradle.kts +++ b/buildSrc/src/main/kotlin/convention-publish.gradle.kts @@ -1,15 +1,12 @@ import com.redmadrobot.build.dsl.* import com.vanniktech.maven.publish.AndroidSingleVariantLibrary -import com.vanniktech.maven.publish.JavadocJar -import com.vanniktech.maven.publish.KotlinJvm -import com.vanniktech.maven.publish.SonatypeHost plugins { id("com.vanniktech.maven.publish") } mavenPublishing { - publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true) + publishToMavenCentral(automaticRelease = true) signAllPublications() // disable Javadoc because of Dokka / JDK 17: "PermittedSubclasses requires ASM9" during compilation configure(AndroidSingleVariantLibrary(variant = "release", sourcesJar = true, publishJavadocJar = false,)) diff --git a/buildSrc/src/main/kotlin/internal/Versions.kt b/buildSrc/src/main/kotlin/internal/Versions.kt index 207f80d3..8bb0c5a4 100644 --- a/buildSrc/src/main/kotlin/internal/Versions.kt +++ b/buildSrc/src/main/kotlin/internal/Versions.kt @@ -3,5 +3,5 @@ package internal internal object Versions { const val MIN_SDK = 23 const val TARGET_SDK = 36 - const val COMPILE_SDK = TARGET_SDK + const val COMPILE_SDK = 37 } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e111328..c61a118f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/plugins/plugin-konfeature/build.gradle.kts b/plugins/plugin-konfeature/build.gradle.kts index 61929f16..c3e0228f 100644 --- a/plugins/plugin-konfeature/build.gradle.kts +++ b/plugins/plugin-konfeature/build.gradle.kts @@ -10,6 +10,7 @@ android { dependencies { implementation(rmr.konfeature) + implementation(rmr.konfeature.ui) implementation(androidx.core) implementation(androidx.lifecycle.runtime) implementation(stack.timber) diff --git a/settings.gradle.kts b/settings.gradle.kts index 25282f3c..8e489c21 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,9 +31,11 @@ dependencyResolutionManagement { } versionCatalogs { - val version = "2026.02.23" // Keep it in sync with buildSrc/settings.gradle.kts + val version = "2026.07.10" // Keep it in sync with buildSrc/settings.gradle.kts create("rmr") { from("com.redmadrobot.versions:versions-redmadrobot:$version") + version("konfeature", "1.1.0") // Remove with update version + library("konfeature-ui", "com.redmadrobot.konfeature:konfeature-ui:1.1.0") // Remove with update version } create("androidx") { from("com.redmadrobot.versions:versions-androidx:$version") From 8a3c9dc70fcbb4926bcf70f8fcc931d67e2d28bd Mon Sep 17 00:00:00 2001 From: Yuriy Elistratov Date: Mon, 27 Jul 2026 15:23:05 +0400 Subject: [PATCH 2/3] feature: Use konfeature-ui for plugin konfeature --- panel-no-op/build.gradle.kts | 1 + .../KonfeatureDebugPanelInterceptor.kt | 17 - .../plugin/konfeature/KonfeaturePlugin.kt | 6 +- .../redmadrobot/debug/uikit/theme/Theme.kt | 12 + plugins/plugin-konfeature/build.gradle.kts | 3 - .../KonfeatureDebugPanelInterceptor.kt | 137 ------ .../plugin/konfeature/KonfeaturePlugin.kt | 35 +- .../konfeature/KonfeaturePluginContainer.kt | 14 - .../konfeature/ui/AnimatedFilterItem.kt | 32 -- .../konfeature/ui/EditConfigValueDialog.kt | 266 ------------ .../plugin/konfeature/ui/KonfeatureScreen.kt | 403 ------------------ .../konfeature/ui/KonfeatureViewModel.kt | 228 ---------- .../konfeature/ui/data/EditDialogState.kt | 7 - .../konfeature/ui/data/KonfeatureItem.kt | 42 -- .../konfeature/ui/data/KonfeatureViewState.kt | 21 - .../plugin/konfeature/util/JsonConverter.kt | 36 -- .../src/main/res/drawable/icon_clear.xml | 10 - .../src/main/res/drawable/icon_edit.xml | 10 - .../res/drawable/icon_keyboard_arrow_down.xml | 10 - .../res/drawable/icon_keyboard_arrow_up.xml | 10 - .../src/main/res/drawable/icon_search.xml | 10 - .../src/main/res/values/strings.xml | 25 -- sample/build.gradle.kts | 4 + .../com/redmadrobot/debug_sample/App.kt | 28 +- .../storage/TestKonfeatureProvider.kt | 16 +- settings.gradle.kts | 1 + 26 files changed, 76 insertions(+), 1308 deletions(-) delete mode 100644 panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeatureDebugPanelInterceptor.kt delete mode 100644 plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeatureDebugPanelInterceptor.kt delete mode 100644 plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeaturePluginContainer.kt delete mode 100644 plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/AnimatedFilterItem.kt delete mode 100644 plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/EditConfigValueDialog.kt delete mode 100644 plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureScreen.kt delete mode 100644 plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureViewModel.kt delete mode 100644 plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/data/EditDialogState.kt delete mode 100644 plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/data/KonfeatureItem.kt delete mode 100644 plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/data/KonfeatureViewState.kt delete mode 100644 plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/util/JsonConverter.kt delete mode 100644 plugins/plugin-konfeature/src/main/res/drawable/icon_clear.xml delete mode 100644 plugins/plugin-konfeature/src/main/res/drawable/icon_edit.xml delete mode 100644 plugins/plugin-konfeature/src/main/res/drawable/icon_keyboard_arrow_down.xml delete mode 100644 plugins/plugin-konfeature/src/main/res/drawable/icon_keyboard_arrow_up.xml delete mode 100644 plugins/plugin-konfeature/src/main/res/drawable/icon_search.xml delete mode 100644 plugins/plugin-konfeature/src/main/res/values/strings.xml diff --git a/panel-no-op/build.gradle.kts b/panel-no-op/build.gradle.kts index 073f46e1..a199b667 100644 --- a/panel-no-op/build.gradle.kts +++ b/panel-no-op/build.gradle.kts @@ -40,4 +40,5 @@ dependencies { implementation(stack.okhttp) implementation(stack.kotlinx.coroutines.android) implementation(rmr.konfeature) + implementation(rmr.konfeature.ui.noop) } diff --git a/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeatureDebugPanelInterceptor.kt b/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeatureDebugPanelInterceptor.kt deleted file mode 100644 index 34e46a23..00000000 --- a/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeatureDebugPanelInterceptor.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.redmadrobot.debug.plugin.konfeature - -import android.content.Context -import com.redmadrobot.konfeature.source.FeatureValueSource -import com.redmadrobot.konfeature.source.Interceptor - -/** - * No-op implementation of [KonfeatureDebugPanelInterceptor] for release builds. - * - * [intercept] always returns `null`, so no flag overrides are applied. - */ -@Suppress("UnusedPrivateProperty") -public class KonfeatureDebugPanelInterceptor(context: Context) : Interceptor { - override val name: String = "NoopDebugPanelInterceptor" - - override fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any? = null -} diff --git a/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeaturePlugin.kt b/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeaturePlugin.kt index 0736427b..f3aca666 100644 --- a/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeaturePlugin.kt +++ b/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeaturePlugin.kt @@ -1,14 +1,16 @@ package com.redmadrobot.debug.plugin.konfeature import com.redmadrobot.konfeature.Konfeature +import com.redmadrobot.konfeature.ui.KonfeatureDebugStore /** * No-op implementation of [KonfeaturePlugin] for release builds. * - * Performs no actions; only mirrors the public constructor signature. + * Performs no actions; only mirrors the public constructor signature. The overrides store and + * interceptor come from `konfeature-ui-noop` in release builds. */ @Suppress("UnusedPrivateProperty") public class KonfeaturePlugin( - private val debugPanelInterceptor: KonfeatureDebugPanelInterceptor, private val konfeature: Konfeature, + private val store: KonfeatureDebugStore, ) diff --git a/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/Theme.kt b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/Theme.kt index 8a43f784..0a7c80e1 100644 --- a/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/Theme.kt +++ b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/theme/Theme.kt @@ -10,11 +10,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalContext import com.redmadrobot.debug.uikit.theme.model.ThemeMode internal val LocalColors = compositionLocalOf { LightDebugPanelColors } internal val LocalTypography = compositionLocalOf { DebugPanelTypographyTokens() } +internal val LocalIsDarkTheme = staticCompositionLocalOf { false } /** * Entry point for accessing Debug Panel design tokens. @@ -35,6 +37,15 @@ public object DebugPanelTheme { @Composable @ReadOnlyComposable get() = LocalTypography.current + + /** + * Whether the theme is currently resolved to dark, taking the selected [ThemeMode] into account + * (for [ThemeMode.System] this reflects the system setting). + */ + public val isDarkTheme: Boolean + @Composable + @ReadOnlyComposable + get() = LocalIsDarkTheme.current } /** @@ -84,6 +95,7 @@ public fun DebugPanelTheme( CompositionLocalProvider( LocalColors provides panelColors, LocalTypography provides panelTypography, + LocalIsDarkTheme provides isDarkTheme, ) { MaterialTheme( colorScheme = materialColorScheme, diff --git a/plugins/plugin-konfeature/build.gradle.kts b/plugins/plugin-konfeature/build.gradle.kts index c3e0228f..8aa7c5ac 100644 --- a/plugins/plugin-konfeature/build.gradle.kts +++ b/plugins/plugin-konfeature/build.gradle.kts @@ -11,7 +11,4 @@ android { dependencies { implementation(rmr.konfeature) implementation(rmr.konfeature.ui) - implementation(androidx.core) - implementation(androidx.lifecycle.runtime) - implementation(stack.timber) } diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeatureDebugPanelInterceptor.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeatureDebugPanelInterceptor.kt deleted file mode 100644 index 27057085..00000000 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeatureDebugPanelInterceptor.kt +++ /dev/null @@ -1,137 +0,0 @@ -package com.redmadrobot.debug.plugin.konfeature - -import android.content.Context -import android.content.SharedPreferences -import androidx.core.content.edit -import com.redmadrobot.debug.plugin.konfeature.util.JsonConverter -import com.redmadrobot.konfeature.source.FeatureValueSource -import com.redmadrobot.konfeature.source.Interceptor -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import org.json.JSONObject -import timber.log.Timber - -/** - * [Interceptor] for Konfeature that allows overriding feature flag values through the debug panel. - * - * Overridden values are stored in SharedPreferences and applied each time a flag is read through Konfeature. - * - * Must be passed to the [KonfeaturePlugin] constructor and also registered in Konfeature during its configuration. - * - * Example: - * ``` - * val interceptor = KonfeatureDebugPanelInterceptor(context) - * - * val konfeature = konfeature { - * addInterceptor(interceptor) - * // ... - * } - * - * KonfeaturePlugin(interceptor, konfeature) - * ``` - * - * @param context context for accessing SharedPreferences - */ -public class KonfeatureDebugPanelInterceptor(context: Context) : Interceptor { - private val preferences by lazy { - context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE) - } - - private val _valuesFlow = MutableStateFlow(emptyMap()) - - internal val valuesFlow = _valuesFlow.asStateFlow() - - private val mutex = Mutex() - - override val name: String = "DebugPanelInterceptor" - - init { - CoroutineScope(Dispatchers.IO).launch { - _valuesFlow.value = mutex.withLock { fetchValues(preferences) } - } - } - - override fun intercept(valueSource: FeatureValueSource, key: String, value: Any): Any? { - return _valuesFlow.value[key] - ?.let { convertTypeIfNeeded(it, value) } - ?.takeIf { it != value } - } - - /* - * map debugValue from Int to Long, - * from Float to Double, - * from Long to Double if value is Double - */ - private fun convertTypeIfNeeded(debugValue: Any, value: Any): Any { - var result = when { - debugValue is Int -> debugValue.toLong() - debugValue is Float -> debugValue.toDouble() - else -> debugValue - } - if (result is Long && value is Double) { - result = result.toDouble() - } - return result - } - - internal suspend fun setValue(key: String, value: Any) { - _valuesFlow.update { it + (key to value) } - updateValues(_valuesFlow.value) - } - - internal suspend fun resetValue(key: String) { - _valuesFlow.update { it - key } - updateValues(_valuesFlow.value) - } - - internal suspend fun resetAllValues() { - _valuesFlow.value = emptyMap() - updateValues(_valuesFlow.value) - } - - private suspend fun updateValues(map: Map) { - coroutineScope { - launch(Dispatchers.IO) { - mutex.withLock { updateValues(preferences, map) } - } - } - } - - @Suppress("TooGenericExceptionCaught") - private fun fetchValues(preferences: SharedPreferences): Map { - return try { - val jsonValues = preferences.getString(KEY, EMPTY_MAP) ?: EMPTY_MAP - JsonConverter.toMap(JSONObject(jsonValues)) - } catch (error: Exception) { - Timber.tag(TAG).e(error, "cant fetch debug values") - preferences.edit(commit = true) { remove(KEY) } - emptyMap() - } - } - - @Suppress("TooGenericExceptionCaught") - private fun updateValues(preferences: SharedPreferences, map: Map) { - try { - val jsonValues = JSONObject(map).toString() - preferences.edit(commit = true) { - putString(KEY, jsonValues) - } - } catch (error: Exception) { - Timber.tag(TAG).e(error, "cant update debug values") - } - } - - private companion object { - private const val EMPTY_MAP = "{}" - private const val FILE_NAME = "debug_panel_interceptor_values" - private const val KEY = "values" - private const val TAG = "DebugPanelInterceptor" - } -} diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeaturePlugin.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeaturePlugin.kt index 3987ea54..a685e13d 100644 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeaturePlugin.kt +++ b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeaturePlugin.kt @@ -4,33 +4,50 @@ import androidx.compose.runtime.Composable import com.redmadrobot.debug.core.internal.CommonContainer import com.redmadrobot.debug.core.internal.PluginDependencyContainer import com.redmadrobot.debug.core.plugin.Plugin -import com.redmadrobot.debug.plugin.konfeature.ui.KonfeatureScreen +import com.redmadrobot.debug.uikit.theme.DebugPanelTheme import com.redmadrobot.konfeature.Konfeature +import com.redmadrobot.konfeature.ui.KonfeatureDebugPanel +import com.redmadrobot.konfeature.ui.KonfeatureDebugStore +import com.redmadrobot.konfeature.ui.presentation.theme.KonfeatureTheme /** * Plugin for viewing and overriding feature flags from the Konfeature library. * - * Allows changing feature flag values at runtime without rebuilding the app. - * Overridden values are stored in SharedPreferences and survive app restarts. + * The screen, feature list and persistence are provided by the `konfeature-ui` library. This + * plugin only embeds [KonfeatureDebugPanel] into the debug panel using the library's default + * palette, and keeps it in sync with the debug panel's light/dark theme mode. Boolean values are + * toggled inline by the panel; other value types are shown read-only. * - * @param debugPanelInterceptor interceptor through which overrides are applied - * @param konfeature Konfeature instance used to obtain the list of features + * Overridden values are persisted by [store] (DataStore) and survive app restarts. * - * @see KonfeatureDebugPanelInterceptor + * @param konfeature Konfeature instance whose feature configs are displayed + * @param store store backing the overrides; create it once via [KonfeatureDebugStore.create] and + * reuse the same instance for [com.redmadrobot.konfeature.ui.KonfeatureDebugInterceptor] + * + * @see KonfeatureDebugStore */ public class KonfeaturePlugin( - private val debugPanelInterceptor: KonfeatureDebugPanelInterceptor, private val konfeature: Konfeature, + private val store: KonfeatureDebugStore, ) : Plugin() { override fun getName(): String = NAME + /** + * The plugin has no dependencies of its own — the screen, state and persistence are provided by + * the `konfeature-ui` library — so an empty container is returned. + */ override fun getPluginContainer(commonContainer: CommonContainer): PluginDependencyContainer { - return KonfeaturePluginContainer(konfeature, debugPanelInterceptor) + return object : PluginDependencyContainer {} } @Composable override fun content() { - KonfeatureScreen() + KonfeatureTheme(isDarkTheme = DebugPanelTheme.isDarkTheme) { + KonfeatureDebugPanel( + konfeature = konfeature, + store = store, + ) + } } private companion object { diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeaturePluginContainer.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeaturePluginContainer.kt deleted file mode 100644 index 5bb84746..00000000 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeaturePluginContainer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.redmadrobot.debug.plugin.konfeature - -import com.redmadrobot.debug.core.internal.PluginDependencyContainer -import com.redmadrobot.debug.plugin.konfeature.ui.KonfeatureViewModel -import com.redmadrobot.konfeature.Konfeature - -internal class KonfeaturePluginContainer( - private val konfeature: Konfeature, - private val debugPanelInterceptor: KonfeatureDebugPanelInterceptor, -) : PluginDependencyContainer { - fun createKonfeatureViewModel(): KonfeatureViewModel { - return KonfeatureViewModel(konfeature, debugPanelInterceptor) - } -} diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/AnimatedFilterItem.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/AnimatedFilterItem.kt deleted file mode 100644 index 38711261..00000000 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/AnimatedFilterItem.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.redmadrobot.debug.plugin.konfeature.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.lazy.LazyItemScope -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -private const val ENTER_DURATION_MILLIS = 220 -private const val EXIT_DURATION_MILLIS = 180 - -@Composable -internal fun LazyItemScope.AnimatedFilterItem( - visible: Boolean, - modifier: Modifier = Modifier, - content: @Composable () -> Unit, -) { - AnimatedVisibility( - visible = visible, - enter = fadeIn(animationSpec = tween(durationMillis = ENTER_DURATION_MILLIS)) + - expandVertically(animationSpec = tween(durationMillis = ENTER_DURATION_MILLIS)), - exit = fadeOut(animationSpec = tween(durationMillis = EXIT_DURATION_MILLIS)) + - shrinkVertically(animationSpec = tween(durationMillis = EXIT_DURATION_MILLIS)), - modifier = modifier.animateItem(), - ) { - content() - } -} diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/EditConfigValueDialog.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/EditConfigValueDialog.kt deleted file mode 100644 index 192e8b7d..00000000 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/EditConfigValueDialog.kt +++ /dev/null @@ -1,266 +0,0 @@ -package com.redmadrobot.debug.plugin.konfeature.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -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.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import com.redmadrobot.debug.plugin.konfeature.R -import com.redmadrobot.debug.plugin.konfeature.ui.data.EditDialogState -import com.redmadrobot.debug.uikit.components.PanelDialog -import com.redmadrobot.debug.uikit.components.PanelStyledTextField -import com.redmadrobot.debug.uikit.components.PanelToggle -import com.redmadrobot.debug.uikit.theme.DebugPanelShapes -import com.redmadrobot.debug.uikit.theme.DebugPanelTheme - -@Composable -internal fun EditConfigValueDialog( - state: EditDialogState, - onValueChange: (key: String, value: Any) -> Unit, - onValueReset: (key: String) -> Unit, - onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, -) { - val initialValue = state.value - var value by remember { mutableStateOf(state.value) } - var isInputEmpty by remember { mutableStateOf(false) } - val saveEnabled by remember { - derivedStateOf { !isInputEmpty && initialValue != value } - } - - PanelDialog(title = state.key, onDismiss = onDismissRequest, modifier = modifier) { - when (initialValue) { - is Boolean -> BooleanEditInput( - value = initialValue, - onValueChange = { value = it }, - ) - - is Long -> LongEditInput( - value = initialValue, - onValueChange = { value = it }, - onEmptyInput = { isInputEmpty = it }, - ) - - is Double -> DoubleEditInput( - value = initialValue, - onValueChange = { value = it }, - onEmptyInput = { isInputEmpty = it }, - ) - - is String -> StringEditInput( - value = initialValue, - onValueChange = { value = it }, - ) - } - Spacer(modifier = Modifier.height(20.dp)) - EditConfigValueButtons( - state = state, - saveEnabled = saveEnabled, - value = value, - onValueChange = onValueChange, - onValueReset = onValueReset, - onDismissRequest = onDismissRequest, - ) - } -} - -@Composable -private fun EditConfigValueButtons( - state: EditDialogState, - saveEnabled: Boolean, - value: Any, - onValueChange: (key: String, value: Any) -> Unit, - onValueReset: (key: String) -> Unit, - onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(space = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - CloseButton(onDismissRequest = onDismissRequest) - Spacer(modifier = Modifier.weight(weight = 1f)) - if (state.isDebugSource) { - DebugSourceButton( - onClick = { - onValueReset.invoke(state.key) - onDismissRequest.invoke() - } - ) - } - SaveButton( - saveEnabled = saveEnabled, - onClick = { - onValueChange.invoke(state.key, value) - onDismissRequest.invoke() - } - ) - } -} - -@Composable -private fun SaveButton( - saveEnabled: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Button( - modifier = modifier, - onClick = onClick, - enabled = saveEnabled, - shape = DebugPanelShapes.medium, - ) { - Text( - text = stringResource(R.string.konfeature_plugin_save), - style = DebugPanelTheme.typography.labelLarge, - ) - } -} - -@Composable -private fun DebugSourceButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - OutlinedButton( - modifier = modifier, - onClick = onClick, - shape = DebugPanelShapes.medium, - colors = ButtonDefaults.outlinedButtonColors( - contentColor = DebugPanelTheme.colors.content.error, - ), - ) { - Text( - text = stringResource(R.string.konfeature_plugin_reset), - style = DebugPanelTheme.typography.labelLarge, - ) - } -} - -@Composable -private fun CloseButton(onDismissRequest: () -> Unit, modifier: Modifier = Modifier) { - OutlinedButton( - modifier = modifier, - onClick = onDismissRequest, - shape = DebugPanelShapes.medium, - ) { - Text( - text = stringResource(R.string.konfeature_plugin_close), - style = DebugPanelTheme.typography.labelLarge, - ) - } -} - -@Composable -private fun BooleanEditInput( - value: Boolean, - onValueChange: (Any) -> Unit, - modifier: Modifier = Modifier, -) { - var checked by remember { mutableStateOf(value) } - - Row( - modifier = modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(R.string.konfeature_plugin_edit_dialog_hint_boolean), - style = DebugPanelTheme.typography.bodyMedium, - color = DebugPanelTheme.colors.content.primary, - modifier = Modifier.weight(weight = 1f), - ) - PanelToggle( - checked = checked, - onCheckedChange = { newChecked -> - checked = newChecked - onValueChange(newChecked) - }, - ) - } -} - -@Composable -private fun LongEditInput( - value: Long, - onValueChange: (Any) -> Unit, - onEmptyInput: (Boolean) -> Unit, - modifier: Modifier = Modifier, -) { - var text by remember { mutableStateOf(value.toString()) } - - PanelStyledTextField( - value = text, - onValueChange = { newText -> - val newValue = newText.toLongOrNull() - if (newValue != null || newText.isEmpty()) { - text = newText - newValue?.let(onValueChange) - onEmptyInput(newText.isEmpty()) - } - }, - label = stringResource(R.string.konfeature_plugin_edit_dialog_hint_long), - modifier = modifier, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - ) -} - -@Composable -private fun DoubleEditInput( - value: Double, - onValueChange: (Any) -> Unit, - onEmptyInput: (Boolean) -> Unit, - modifier: Modifier = Modifier, -) { - var text by remember { mutableStateOf(value.toBigDecimal().toPlainString()) } - - PanelStyledTextField( - value = text, - onValueChange = { newText -> - val newValue = newText.toDoubleOrNull() - if (newValue != null || newText.isEmpty()) { - text = newText - newValue?.let(onValueChange) - onEmptyInput(newText.isEmpty()) - } - }, - label = stringResource(R.string.konfeature_plugin_edit_dialog_hint_double), - modifier = modifier, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - ) -} - -@Composable -private fun StringEditInput( - value: String, - onValueChange: (Any) -> Unit, - modifier: Modifier = Modifier, -) { - var text by remember { mutableStateOf(value) } - - PanelStyledTextField( - value = text, - onValueChange = { newText -> - text = newText - onValueChange(newText) - }, - label = stringResource(R.string.konfeature_plugin_edit_dialog_hint_string), - modifier = modifier, - ) -} diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureScreen.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureScreen.kt deleted file mode 100644 index 9fe84776..00000000 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureScreen.kt +++ /dev/null @@ -1,403 +0,0 @@ -package com.redmadrobot.debug.plugin.konfeature.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.redmadrobot.debug.core.extension.getPlugin -import com.redmadrobot.debug.core.extension.provideViewModel -import com.redmadrobot.debug.plugin.konfeature.KonfeaturePlugin -import com.redmadrobot.debug.plugin.konfeature.KonfeaturePluginContainer -import com.redmadrobot.debug.plugin.konfeature.R -import com.redmadrobot.debug.plugin.konfeature.ui.data.KonfeatureItem -import com.redmadrobot.debug.plugin.konfeature.ui.data.KonfeatureViewState -import com.redmadrobot.debug.uikit.components.PanelSearchBar -import com.redmadrobot.debug.uikit.components.PanelToggle -import com.redmadrobot.debug.uikit.theme.DebugPanelDimensions -import com.redmadrobot.debug.uikit.theme.DebugPanelShapes -import com.redmadrobot.debug.uikit.theme.DebugPanelTheme -import com.redmadrobot.debug.uikit.theme.MonoFontFamily - -@Composable -internal fun KonfeatureScreen( - viewModel: KonfeatureViewModel = provideViewModel { - getPlugin() - .getContainer() - .createKonfeatureViewModel() - }, -) { - val state by viewModel.state.collectAsState() - - KonfeatureLayout( - state = state, - onRefreshClick = viewModel::onRefreshClick, - onResetAllClick = viewModel::onResetAllClick, - onCollapseAllClick = viewModel::onCollapseAllClick, - onHeaderClick = viewModel::onConfigHeaderClick, - onEditClick = viewModel::onEditClick, - onSearchQueryChange = viewModel::onSearchQueryChanged, - onBooleanToggle = viewModel::onValueChanged, - ) - - state.editDialogState?.let { dialogState -> - EditConfigValueDialog( - state = dialogState, - onValueChange = viewModel::onValueChanged, - onValueReset = viewModel::onValueReset, - onDismissRequest = viewModel::onEditDialogCloseClicked, - ) - } -} - -@Composable -internal fun KonfeatureLayout( - state: KonfeatureViewState, - onEditClick: (key: String, value: Any, isDebugSource: Boolean) -> Unit, - onRefreshClick: () -> Unit, - onCollapseAllClick: () -> Unit, - onResetAllClick: () -> Unit, - onHeaderClick: (String) -> Unit, - onSearchQueryChange: (String) -> Unit, - onBooleanToggle: (String, Boolean) -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier - .fillMaxSize() - .background(color = DebugPanelTheme.colors.background.primary) - ) { - ToolbarChips( - onRefreshClick = onRefreshClick, - onCollapseAllClick = onCollapseAllClick, - onResetAllClick = onResetAllClick, - ) - PanelSearchBar( - query = state.searchQuery, - onQueryChange = onSearchQueryChange, - placeholder = stringResource(R.string.konfeature_plugin_search_hint), - modifier = Modifier.padding(horizontal = 12.dp), - ) - AnimatedVisibility(visible = state.shouldShowEmptySearchItemsHint) { - Text( - text = stringResource(R.string.konfeature_plugin_search_empty), - style = DebugPanelTheme.typography.bodyMedium, - color = DebugPanelTheme.colors.content.tertiary, - modifier = Modifier.padding(all = 16.dp), - ) - } - LazyColumn(modifier = Modifier.weight(weight = 1f)) { - konfeatureItems( - state = state, - onHeaderClick = onHeaderClick, - onEditClick = onEditClick, - onBooleanToggle = onBooleanToggle - ) - } - } -} - -private fun LazyListScope.konfeatureItems( - state: KonfeatureViewState, - onHeaderClick: (String) -> Unit, - onEditClick: (key: String, value: Any, isDebugSource: Boolean) -> Unit, - onBooleanToggle: (String, Boolean) -> Unit, -) { - items( - items = state.filteredItems, - key = { item -> item.itemKey }, - ) { item -> - val isMatchingFilter = item.itemKey in state.matchingKeys - - when (item) { - is KonfeatureItem.Config -> { - val isCollapsed = !state.isSearchActive && item.name in state.collapsedConfigs - val overrideCount = state.values.count { value -> - value.configName == item.name && value.isDebugSource - } - AnimatedFilterItem(visible = isMatchingFilter) { - ConfigGroupHeader( - name = item.description.takeIf { it.isNotEmpty() } ?: item.name, - overrideCount = overrideCount, - isCollapsed = isCollapsed, - onClick = { onHeaderClick(item.name) }, - ) - } - } - - is KonfeatureItem.Value -> { - val isVisible = isMatchingFilter && (state.isSearchActive || item.configName !in state.collapsedConfigs) - AnimatedFilterItem(visible = isVisible) { - ConfigValueItem( - item = item, - onEditClick = onEditClick, - onBooleanToggle = onBooleanToggle, - ) - } - } - } - } -} - -@Composable -private fun ToolbarChips( - onRefreshClick: () -> Unit, - onCollapseAllClick: () -> Unit, - onResetAllClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 12.dp), - horizontalArrangement = Arrangement.spacedBy(space = 8.dp), - ) { - ActionChip( - label = stringResource(R.string.konfeature_plugin_refresh), - onClick = onRefreshClick, - ) - ActionChip( - label = stringResource(R.string.konfeature_plugin_collapse_all), - onClick = onCollapseAllClick, - ) - ActionChip( - label = stringResource(R.string.konfeature_plugin_reset_all), - onClick = onResetAllClick, - ) - } -} - -@Composable -private fun ActionChip( - label: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Text( - text = label, - style = DebugPanelTheme.typography.labelLarge, - color = DebugPanelTheme.colors.content.secondary, - modifier = modifier - .clip(shape = DebugPanelShapes.medium) - .border( - width = 1.dp, - color = DebugPanelTheme.colors.stroke.primary, - shape = DebugPanelShapes.medium, - ) - .clickable(onClick = onClick) - .padding(horizontal = 12.dp, vertical = 4.dp), - ) -} - -@Composable -private fun ConfigGroupHeader( - name: String, - overrideCount: Int, - isCollapsed: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .fillMaxWidth() - .clip(shape = DebugPanelShapes.medium) - .clickable(onClick = onClick) - .padding(all = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(space = 4.dp), - ) { - Icon( - painter = painterResource( - id = if (isCollapsed) { - R.drawable.icon_keyboard_arrow_up - } else { - R.drawable.icon_keyboard_arrow_down - } - ), - contentDescription = null, - tint = DebugPanelTheme.colors.content.tertiary, - modifier = Modifier.size(size = DebugPanelDimensions.iconSizeSmall), - ) - Text( - text = name, - style = DebugPanelTheme.typography.titleMedium, - color = DebugPanelTheme.colors.content.primary, - modifier = Modifier.weight(weight = 1f), - ) - if (overrideCount > 0) { - Text( - text = overrideCount.toString(), - style = DebugPanelTheme.typography.labelSmall, - color = DebugPanelTheme.colors.content.accent, - modifier = Modifier - .background( - color = DebugPanelTheme.colors.surface.tertiary, - shape = DebugPanelShapes.small, - ) - .padding(horizontal = 8.dp, vertical = 2.dp), - ) - } - } -} - -@Composable -private fun ConfigValueItem( - item: KonfeatureItem.Value, - onEditClick: (key: String, value: Any, isDebugSource: Boolean) -> Unit, - onBooleanToggle: (String, Boolean) -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .fillMaxWidth() - .padding(start = 32.dp, end = 8.dp) - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(space = 8.dp), - ) { - ValueInfoColumn( - item = item, - modifier = Modifier.weight(weight = 1f), - ) - - when { - item.value is Boolean -> PanelToggle( - checked = item.value, - onCheckedChange = { newValue -> onBooleanToggle(item.key, newValue) }, - ) - - item.editAvailable -> EditButton( - onClick = { onEditClick(item.key, item.value, item.isDebugSource) }, - ) - } - } -} - -@Composable -private fun ValueInfoColumn( - item: KonfeatureItem.Value, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier) { - Text( - text = item.key, - style = DebugPanelTheme.typography.bodyMedium.copy(fontFamily = MonoFontFamily), - color = DebugPanelTheme.colors.content.secondary, - ) - if (item.description.isNotEmpty()) { - Text( - modifier = Modifier.padding(top = 4.dp), - text = item.description, - style = DebugPanelTheme.typography.bodyMedium.copy(fontFamily = MonoFontFamily), - color = DebugPanelTheme.colors.content.tertiary, - ) - } - if (item.value is Boolean) { - ValueSourceLabel(item = item, modifier = Modifier.padding(top = 8.dp)) - } else { - ValueWithSource(item = item) - } - } -} - -@Composable -private fun ValueWithSource( - item: KonfeatureItem.Value, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier.padding(top = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(space = 8.dp), - ) { - Text( - text = formatValue(value = item.value), - style = DebugPanelTheme.typography.labelMedium, - color = sourceColor(item = item), - ) - ValueSourceLabel(item = item) - } -} - -@Composable -private fun ValueSourceLabel( - item: KonfeatureItem.Value, - modifier: Modifier = Modifier, -) { - if (item.isDebugSource || item.sourceName != "Default") { - SourceLabel( - source = item.sourceName, - isDebug = item.isDebugSource, - modifier = modifier, - ) - } -} - -@Composable -private fun EditButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - IconButton( - onClick = onClick, - modifier = modifier.size(size = DebugPanelDimensions.iconSizeLarge), - ) { - Icon( - painter = painterResource(R.drawable.icon_edit), - contentDescription = null, - tint = DebugPanelTheme.colors.content.accent, - modifier = Modifier.size(size = DebugPanelDimensions.iconSizeSmall), - ) - } -} - -@Composable -private fun SourceLabel( - source: String, - isDebug: Boolean, - modifier: Modifier = Modifier, -) { - Text( - text = source, - style = DebugPanelTheme.typography.labelMedium, - color = if (isDebug) { - DebugPanelTheme.colors.content.teal - } else { - DebugPanelTheme.colors.source.remoteText - }, - modifier = modifier, - ) -} - -private fun formatValue(value: Any): String { - return if (value is String) "\"$value\"" else value.toString() -} - -@Composable -private fun sourceColor(item: KonfeatureItem.Value): Color = when { - item.isDebugSource -> DebugPanelTheme.colors.content.teal - item.sourceName != "Default" -> DebugPanelTheme.colors.source.remoteText - else -> DebugPanelTheme.colors.content.tertiary -} diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureViewModel.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureViewModel.kt deleted file mode 100644 index 530489d9..00000000 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureViewModel.kt +++ /dev/null @@ -1,228 +0,0 @@ -package com.redmadrobot.debug.plugin.konfeature.ui - -import androidx.compose.ui.graphics.Color -import androidx.lifecycle.viewModelScope -import com.redmadrobot.debug.core.internal.PluginViewModel -import com.redmadrobot.debug.plugin.konfeature.KonfeatureDebugPanelInterceptor -import com.redmadrobot.debug.plugin.konfeature.ui.data.EditDialogState -import com.redmadrobot.debug.plugin.konfeature.ui.data.KonfeatureItem -import com.redmadrobot.debug.plugin.konfeature.ui.data.KonfeatureViewState -import com.redmadrobot.konfeature.FeatureConfigSpec -import com.redmadrobot.konfeature.FeatureValueSpec -import com.redmadrobot.konfeature.Konfeature -import com.redmadrobot.konfeature.source.FeatureValueSource -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -private const val SEARCH_QUERY_DELAY_MILLIS = 500L - -internal class KonfeatureViewModel( - private val konfeature: Konfeature, - private val debugPanelInterceptor: KonfeatureDebugPanelInterceptor, -) : PluginViewModel() { - private val _state = MutableStateFlow(KonfeatureViewState()) - private val _searchQueryFlow = MutableStateFlow("") - - val state: StateFlow = _state.asStateFlow() - - init { - observeKonfeatureValues() - observeSearchQuery() - } - - fun onValueChanged(key: String, value: Any) { - viewModelScope.launch { - debugPanelInterceptor.setValue(key, value) - } - } - - fun onValueReset(key: String) { - viewModelScope.launch { - debugPanelInterceptor.resetValue(key) - } - } - - fun onConfigHeaderClick(configName: String) { - _state.update { state -> - val newCollapsedConfigs = if (configName in state.collapsedConfigs) { - state.collapsedConfigs - configName - } else { - state.collapsedConfigs + configName - } - state.copy(collapsedConfigs = newCollapsedConfigs) - } - } - - fun onRefreshClick() { - viewModelScope.launch { updateItems() } - } - - fun onResetAllClick() { - viewModelScope.launch { - debugPanelInterceptor.resetAllValues() - } - } - - fun onCollapseAllClick() { - _state.update { state -> state.copy(collapsedConfigs = state.configs.keys) } - } - - fun onEditClick(key: String, value: Any, isDebugSource: Boolean) { - _state.update { it.copy(editDialogState = EditDialogState(key, value, isDebugSource)) } - } - - fun onEditDialogCloseClicked() { - _state.update { it.copy(editDialogState = null) } - } - - fun onSearchQueryChanged(query: String) { - _state.update { state -> state.copy(searchQuery = query) } - _searchQueryFlow.update { query } - } - - private fun observeKonfeatureValues() { - debugPanelInterceptor.valuesFlow - .onEach { updateItems() } - .launchIn(viewModelScope) - } - - @OptIn(FlowPreview::class) - private fun observeSearchQuery() { - _searchQueryFlow - .debounce(timeoutMillis = SEARCH_QUERY_DELAY_MILLIS) - .onEach { query -> - _state.update { state -> - state.copy(matchingKeys = computeMatchingKeys(state.values, query)) - } - } - .launchIn(viewModelScope) - } - - private suspend fun updateItems() { - val (configs, values) = withContext(Dispatchers.IO) { getItems(konfeature) } - val searchQuery = _searchQueryFlow.value - val items = buildItems(configs, values) - val matchingKeys = computeMatchingKeys(values, searchQuery) - - _state.update { state -> - state.copy( - configs = configs, - values = values, - filteredItems = items, - matchingKeys = matchingKeys, - ) - } - } - - private fun buildItems( - configs: Map, - values: List, - ): List { - return buildList { - var previousValue: KonfeatureItem.Value? = null - for (value in values) { - if (previousValue?.configName != value.configName) { - configs[value.configName]?.let { config -> add(config) } - } - add(value) - previousValue = value - } - } - } - - private fun getItems(konfeature: Konfeature): Pair, List> { - val configs = mutableMapOf() - val values = mutableListOf() - - konfeature.spec.fold(configs to values) { acc, configSpec -> - acc.apply { - configs[configSpec.name] = createConfigItem(configSpec) - configSpec.values.mapTo(values) { valueSpec -> - createConfigValueItem( - configName = configSpec.name, - valueSpec = valueSpec, - konfeature = konfeature - ) - } - } - } - - return configs to values - } - - private fun createConfigItem(config: FeatureConfigSpec): KonfeatureItem.Config { - return KonfeatureItem.Config( - name = config.name, - description = config.description - ) - } - - private fun createConfigValueItem( - configName: String, - valueSpec: FeatureValueSpec, - konfeature: Konfeature, - ): KonfeatureItem.Value { - val configValue = konfeature.getValue(valueSpec) - - val sourceColor = when (configValue.source) { - FeatureValueSource.Default -> Color.Gray - is FeatureValueSource.Interceptor -> Color.Red - is FeatureValueSource.Source -> Color.Green - } - - return KonfeatureItem.Value( - key = valueSpec.key, - value = configValue.value, - configName = configName, - sourceName = getSourceName(configValue.source), - sourceColor = sourceColor, - description = valueSpec.description, - isDebugSource = isDebugSource(configValue.source), - ) - } - - private fun isDebugSource(source: FeatureValueSource): Boolean { - return (source as? FeatureValueSource.Interceptor)?.name == debugPanelInterceptor.name - } - - private fun getSourceName(source: FeatureValueSource): String { - return when (source) { - FeatureValueSource.Default -> "Default" - is FeatureValueSource.Interceptor -> source.name - is FeatureValueSource.Source -> source.name - else -> "Unknown" - } - } - - private suspend fun computeMatchingKeys( - values: List, - query: String, - ): Set { - return withContext(Dispatchers.Default) { - if (query.isBlank()) { - values.toMatchingKeys() - } else { - val matchingValues = values.filter { it.key.contains(query, ignoreCase = true) } - matchingValues.toMatchingKeys() - } - } - } - - private fun List.toMatchingKeys(): Set { - return this.flatMapTo(destination = mutableSetOf()) { value -> - listOf( - "${KonfeatureItem.ITEM_KEY_PREFIX_CONFIG}${value.configName}", - "${KonfeatureItem.ITEM_KEY_PREFIX_VALUE}${value.key}" - ) - } - } -} diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/data/EditDialogState.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/data/EditDialogState.kt deleted file mode 100644 index 0a5fe92c..00000000 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/data/EditDialogState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.redmadrobot.debug.plugin.konfeature.ui.data - -internal class EditDialogState( - val key: String, - val value: Any, - val isDebugSource: Boolean -) diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/data/KonfeatureItem.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/data/KonfeatureItem.kt deleted file mode 100644 index f79289b0..00000000 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/data/KonfeatureItem.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.redmadrobot.debug.plugin.konfeature.ui.data - -import androidx.compose.ui.graphics.Color - -internal sealed interface KonfeatureItem { - val itemKey: String - - companion object { - const val ITEM_KEY_PREFIX_CONFIG = "config_" - const val ITEM_KEY_PREFIX_VALUE = "value_" - } - - data class Config( - val name: String, - val description: String, - ) : KonfeatureItem { - override val itemKey: String - get() = "$ITEM_KEY_PREFIX_CONFIG$name" - } - - data class Value( - val key: String, - val configName: String, - val value: Any, - val description: String, - val sourceName: String, - val sourceColor: Color, - val isDebugSource: Boolean - ) : KonfeatureItem { - override val itemKey: String - get() = "$ITEM_KEY_PREFIX_VALUE$key" - - val editAvailable: Boolean - get() = when (value) { - is Boolean, - is String, - is Long, - is Double -> true - else -> false - } - } -} diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/data/KonfeatureViewState.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/data/KonfeatureViewState.kt deleted file mode 100644 index 024edcf3..00000000 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/data/KonfeatureViewState.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.redmadrobot.debug.plugin.konfeature.ui.data - -internal data class KonfeatureViewState( - val searchQuery: String = "", - val collapsedConfigs: Set = emptySet(), - val configs: Map = emptyMap(), - val values: List = emptyList(), - val filteredItems: List = emptyList(), - val matchingKeys: Set = emptySet(), - val editDialogState: EditDialogState? = null -) { - val isSearchActive: Boolean - get() = searchQuery.isNotBlank() - val shouldShowEmptySearchItemsHint: Boolean - get() { - val isNotMatchingKeys = matchingKeys.none { key -> - key.startsWith(prefix = KonfeatureItem.ITEM_KEY_PREFIX_VALUE) - } - return isSearchActive && isNotMatchingKeys - } -} diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/util/JsonConverter.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/util/JsonConverter.kt deleted file mode 100644 index be1e94bf..00000000 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/util/JsonConverter.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.redmadrobot.debug.plugin.konfeature.util - -import org.json.JSONArray -import org.json.JSONObject - -internal object JsonConverter { - fun toMap(jsonobj: JSONObject): Map { - val map = mutableMapOf() - val keys = jsonobj.keys() - while (keys.hasNext()) { - val key = keys.next() - var value = jsonobj[key] - if (value is JSONArray) { - value = toList(value) - } else if (value is JSONObject) { - value = toMap(value) - } - map[key] = value - } - return map - } - - fun toList(array: JSONArray): List { - val list = mutableListOf() - for (i in 0 until array.length()) { - var value = array[i] - if (value is JSONArray) { - value = toList(value) - } else if (value is JSONObject) { - value = toMap(value) - } - list.add(value) - } - return list - } -} diff --git a/plugins/plugin-konfeature/src/main/res/drawable/icon_clear.xml b/plugins/plugin-konfeature/src/main/res/drawable/icon_clear.xml deleted file mode 100644 index 95cc170f..00000000 --- a/plugins/plugin-konfeature/src/main/res/drawable/icon_clear.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/plugins/plugin-konfeature/src/main/res/drawable/icon_edit.xml b/plugins/plugin-konfeature/src/main/res/drawable/icon_edit.xml deleted file mode 100644 index a50b9d79..00000000 --- a/plugins/plugin-konfeature/src/main/res/drawable/icon_edit.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/plugins/plugin-konfeature/src/main/res/drawable/icon_keyboard_arrow_down.xml b/plugins/plugin-konfeature/src/main/res/drawable/icon_keyboard_arrow_down.xml deleted file mode 100644 index 10cedc2e..00000000 --- a/plugins/plugin-konfeature/src/main/res/drawable/icon_keyboard_arrow_down.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/plugins/plugin-konfeature/src/main/res/drawable/icon_keyboard_arrow_up.xml b/plugins/plugin-konfeature/src/main/res/drawable/icon_keyboard_arrow_up.xml deleted file mode 100644 index 61f92c2a..00000000 --- a/plugins/plugin-konfeature/src/main/res/drawable/icon_keyboard_arrow_up.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/plugins/plugin-konfeature/src/main/res/drawable/icon_search.xml b/plugins/plugin-konfeature/src/main/res/drawable/icon_search.xml deleted file mode 100644 index afe352ee..00000000 --- a/plugins/plugin-konfeature/src/main/res/drawable/icon_search.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/plugins/plugin-konfeature/src/main/res/values/strings.xml b/plugins/plugin-konfeature/src/main/res/values/strings.xml deleted file mode 100644 index 3fa00691..00000000 --- a/plugins/plugin-konfeature/src/main/res/values/strings.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - Edit: %s - - Boolean value: - Long value: - Double value: - String value: - - key: %s - source: %s - value: %s - - Reset - Save - Close - - Refresh - Collapse All - Reset All - - Search by key… - Clear search - No toggles found - diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index ac256d8f..f0c0bbb7 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -41,5 +41,9 @@ dependencies { debugImplementation(project(":plugins:plugin-konfeature")) releaseImplementation(project(":panel-no-op")) + // Konfeature debug store & interceptor (referenced from the app's main source set) + debugImplementation(rmr.konfeature.ui) + releaseImplementation(rmr.konfeature.ui.noop) + implementation(stack.retrofit) } diff --git a/sample/src/main/kotlin/com/redmadrobot/debug_sample/App.kt b/sample/src/main/kotlin/com/redmadrobot/debug_sample/App.kt index 168c76a5..61c4b7c0 100644 --- a/sample/src/main/kotlin/com/redmadrobot/debug_sample/App.kt +++ b/sample/src/main/kotlin/com/redmadrobot/debug_sample/App.kt @@ -3,18 +3,29 @@ package com.redmadrobot.debug_sample import android.app.Application import com.redmadrobot.debug.core.DebugPanel import com.redmadrobot.debug.plugin.aboutapp.AboutAppPlugin -import com.redmadrobot.debug.plugin.konfeature.KonfeatureDebugPanelInterceptor import com.redmadrobot.debug.plugin.konfeature.KonfeaturePlugin import com.redmadrobot.debug.plugin.servers.ServersPlugin import com.redmadrobot.debug_sample.debug_data.DebugAboutAppInfoProvider import com.redmadrobot.debug_sample.debug_data.DebugServersProvider import com.redmadrobot.debug_sample.storage.TestKonfeatureProvider +import com.redmadrobot.konfeature.Logger +import com.redmadrobot.konfeature.ui.KonfeatureDebugInterceptor +import com.redmadrobot.konfeature.ui.KonfeatureDebugStore +import kotlinx.coroutines.runBlocking +import timber.log.Timber class App : Application() { override fun onCreate() { super.onCreate() - val debugPanelInterceptor = KonfeatureDebugPanelInterceptor(this) + val konfeatureLogger = konfeatureLogger() + val store = runBlocking { + KonfeatureDebugStore.create( + path = filesDir.resolve("konfeature_debug.preferences_pb").absolutePath, + logger = konfeatureLogger, + ) + } + val debugInterceptor = KonfeatureDebugInterceptor(store) DebugPanel.initialize( application = this, @@ -23,8 +34,8 @@ class App : Application() { preInstalledServers = DebugServersProvider().provideData() ), KonfeaturePlugin( - debugPanelInterceptor = debugPanelInterceptor, - konfeature = TestKonfeatureProvider.create(debugPanelInterceptor), + konfeature = TestKonfeatureProvider.create(debugInterceptor, konfeatureLogger), + store = store, ), AboutAppPlugin( appInfoList = DebugAboutAppInfoProvider.provideData() @@ -32,4 +43,13 @@ class App : Application() { ) ) } + + private fun konfeatureLogger(): Logger = object : Logger { + override fun log(severity: Logger.Severity, message: String) { + when (severity) { + Logger.Severity.WARNING -> Timber.tag("Konfeature").w(message) + Logger.Severity.INFO -> Timber.tag("Konfeature").i(message) + } + } + } } diff --git a/sample/src/main/kotlin/com/redmadrobot/debug_sample/storage/TestKonfeatureProvider.kt b/sample/src/main/kotlin/com/redmadrobot/debug_sample/storage/TestKonfeatureProvider.kt index c9a58a5c..10b68949 100644 --- a/sample/src/main/kotlin/com/redmadrobot/debug_sample/storage/TestKonfeatureProvider.kt +++ b/sample/src/main/kotlin/com/redmadrobot/debug_sample/storage/TestKonfeatureProvider.kt @@ -1,22 +1,21 @@ package com.redmadrobot.debug_sample.storage -import com.redmadrobot.debug.plugin.konfeature.KonfeatureDebugPanelInterceptor import com.redmadrobot.konfeature.FeatureConfig import com.redmadrobot.konfeature.Konfeature import com.redmadrobot.konfeature.Logger import com.redmadrobot.konfeature.builder.konfeature import com.redmadrobot.konfeature.source.FeatureSource import com.redmadrobot.konfeature.source.SourceSelectionStrategy -import timber.log.Timber +import com.redmadrobot.konfeature.ui.KonfeatureDebugInterceptor internal object TestKonfeatureProvider { - fun create(debugPanelInterceptor: KonfeatureDebugPanelInterceptor): Konfeature { + fun create(debugInterceptor: KonfeatureDebugInterceptor, logger: Logger): Konfeature { return konfeature { register(FeatureConfig1()) register(FeatureConfig2()) register(FeatureConfig3()) - addInterceptor(debugPanelInterceptor) + addInterceptor(debugInterceptor) addSource(object : FeatureSource { override val name: String = "SampleFeatureSource" @@ -24,14 +23,7 @@ internal object TestKonfeatureProvider { return key == "boolean_feature_2" } }) - setLogger(object : Logger { - override fun log(severity: Logger.Severity, message: String) { - when (severity) { - Logger.Severity.WARNING -> Timber.tag("Konfeature").w(message) - Logger.Severity.INFO -> Timber.tag("Konfeature").i(message) - } - } - }) + setLogger(logger) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 8e489c21..ca2d15ab 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,6 +36,7 @@ dependencyResolutionManagement { from("com.redmadrobot.versions:versions-redmadrobot:$version") version("konfeature", "1.1.0") // Remove with update version library("konfeature-ui", "com.redmadrobot.konfeature:konfeature-ui:1.1.0") // Remove with update version + library("konfeature-ui-noop", "com.redmadrobot.konfeature:konfeature-ui-noop:1.1.0") // Remove with update version } create("androidx") { from("com.redmadrobot.versions:versions-androidx:$version") From e0a20dba1759e56f1adcb2c957944304c7ed7e05 Mon Sep 17 00:00:00 2001 From: Yuriy Elistratov Date: Tue, 28 Jul 2026 19:21:47 +0400 Subject: [PATCH 3/3] feature: Refactor logic of init konfeature plugin --- .../com/redmadrobot/debug/core/DebugPanel.kt | 2 +- .../internal/PluginDependencyContainer.kt | 9 +- .../konfeature/KonfeatureDebugPanelConfig.kt | 52 +++++++++++ .../plugin/konfeature/KonfeaturePlugin.kt | 5 +- .../konfeature/KonfeatureDebugPanelConfig.kt | 90 +++++++++++++++++++ .../plugin/konfeature/KonfeaturePlugin.kt | 27 ++++-- .../com/redmadrobot/debug_sample/App.kt | 14 ++- .../storage/TestKonfeatureProvider.kt | 7 +- 8 files changed, 182 insertions(+), 24 deletions(-) create mode 100644 panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeatureDebugPanelConfig.kt create mode 100644 plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeatureDebugPanelConfig.kt diff --git a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/DebugPanel.kt b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/DebugPanel.kt index 7aa98903..0d587a19 100644 --- a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/DebugPanel.kt +++ b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/DebugPanel.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.emptyFlow * application = this, * plugins = listOf( * ServersPlugin(preInstalledServers), - * KonfeaturePlugin(interceptor, konfeature), + * KonfeaturePlugin(konfeature = konfeature, config = config), * ) * ) * ``` diff --git a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/internal/PluginDependencyContainer.kt b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/internal/PluginDependencyContainer.kt index 4a6072a0..1b0f5a7b 100644 --- a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/internal/PluginDependencyContainer.kt +++ b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/internal/PluginDependencyContainer.kt @@ -11,4 +11,11 @@ import com.redmadrobot.debug.core.annotation.DebugPanelInternal * @see CommonContainer */ @DebugPanelInternal -public interface PluginDependencyContainer +public interface PluginDependencyContainer { + /** + * Shared no-op container for plugins that have no dependencies of their own. Return it from + * [com.redmadrobot.debug.core.plugin.Plugin.getPluginContainer] instead of allocating an empty + * anonymous implementation. + */ + public object Empty : PluginDependencyContainer +} diff --git a/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeatureDebugPanelConfig.kt b/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeatureDebugPanelConfig.kt new file mode 100644 index 00000000..a287909f --- /dev/null +++ b/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeatureDebugPanelConfig.kt @@ -0,0 +1,52 @@ +package com.redmadrobot.debug.plugin.konfeature + +import android.content.Context +import com.redmadrobot.konfeature.Logger +import com.redmadrobot.konfeature.builder.KonfeatureBuilder +import com.redmadrobot.konfeature.ui.KonfeatureDebugInterceptor +import com.redmadrobot.konfeature.ui.KonfeatureDebugStore + +/** + * No-op counterpart of the `plugin-konfeature` `KonfeatureDebugPanelConfig`, for release builds. + * + * Mirrors the real public API so call sites compile unchanged, but the bundled store and + * interceptor come from `konfeature-ui-noop`: the store holds no overrides and the interceptor + * never overrides a value. + */ +@Suppress("UnusedPrivateProperty") +public class KonfeatureDebugPanelConfig private constructor( + internal val store: KonfeatureDebugStore, + internal val interceptor: KonfeatureDebugInterceptor, +) { + public companion object { + /** Default DataStore file name; accepted only to match the real API. */ + public const val DEFAULT_PATH: String = "konfeature_debug.preferences_pb" + + /** Returns a no-op config. [context], [path] and [logger] are accepted only for API parity. */ + public suspend fun create( + context: Context, + path: String = DEFAULT_PATH, + logger: Logger? = null, + ): KonfeatureDebugPanelConfig { + val store = KonfeatureDebugStore.create( + path = context.filesDir.resolve(path).absolutePath, + logger = logger, + ) + return KonfeatureDebugPanelConfig( + store = store, + interceptor = KonfeatureDebugInterceptor(store), + ) + } + } +} + +/** + * No-op counterpart of the `plugin-konfeature` `applyDebugPanelConfig`. Registers the no-op + * interceptor (which never overrides a value) so the builder stays valid; changes nothing else. + */ +public fun KonfeatureBuilder.applyDebugPanelConfig( + config: KonfeatureDebugPanelConfig, +): KonfeatureBuilder { + addInterceptor(config.interceptor) + return this +} diff --git a/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeaturePlugin.kt b/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeaturePlugin.kt index f3aca666..ee9cddd0 100644 --- a/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeaturePlugin.kt +++ b/panel-no-op/src/main/kotlin/com/redmadrobot/debug/noop/plugin/konfeature/KonfeaturePlugin.kt @@ -1,16 +1,15 @@ package com.redmadrobot.debug.plugin.konfeature import com.redmadrobot.konfeature.Konfeature -import com.redmadrobot.konfeature.ui.KonfeatureDebugStore /** * No-op implementation of [KonfeaturePlugin] for release builds. * * Performs no actions; only mirrors the public constructor signature. The overrides store and - * interceptor come from `konfeature-ui-noop` in release builds. + * interceptor come from `konfeature-ui-noop` in release builds via [KonfeatureDebugPanelConfig]. */ @Suppress("UnusedPrivateProperty") public class KonfeaturePlugin( private val konfeature: Konfeature, - private val store: KonfeatureDebugStore, + private val config: KonfeatureDebugPanelConfig, ) diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeatureDebugPanelConfig.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeatureDebugPanelConfig.kt new file mode 100644 index 00000000..ee5a843c --- /dev/null +++ b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeatureDebugPanelConfig.kt @@ -0,0 +1,90 @@ +package com.redmadrobot.debug.plugin.konfeature + +import android.content.Context +import com.redmadrobot.konfeature.Logger +import com.redmadrobot.konfeature.builder.KonfeatureBuilder +import com.redmadrobot.konfeature.ui.KonfeatureDebugInterceptor +import com.redmadrobot.konfeature.ui.KonfeatureDebugStore + +/** + * Everything [KonfeaturePlugin] needs to override feature values, bundled into a single object: the + * persistent [KonfeatureDebugStore] and the [KonfeatureDebugInterceptor] backed by that same store. + * + * Bundling them guarantees the interceptor feeding your Konfeature instance and the panel showing + * the overrides share one store — the wiring callers previously had to get right by hand. + * + * Create it once on startup, attach it to your Konfeature instance with [applyDebugPanelConfig], + * then hand the same config to [KonfeaturePlugin]: + * + * ```kotlin + * val config = KonfeatureDebugPanelConfig.create(context) + * val konfeature = konfeature { + * register(MyFeatureConfig()) + * applyDebugPanelConfig(config) + * } + * KonfeaturePlugin(konfeature = konfeature, config = config) + * ``` + * + * @see applyDebugPanelConfig + */ +public class KonfeatureDebugPanelConfig private constructor( + internal val store: KonfeatureDebugStore, + internal val interceptor: KonfeatureDebugInterceptor, +) { + /** + * Set to `true` by [applyDebugPanelConfig]. [KonfeaturePlugin] checks it to fail fast when a + * config was created but never attached to a Konfeature instance — otherwise the panel would + * display feature values it could never actually override. + */ + internal var isAttached: Boolean = false + + public companion object { + /** Default DataStore file name, resolved under `context.filesDir`. */ + public const val DEFAULT_PATH: String = "konfeature_debug.preferences_pb" + + /** + * Creates the store (completing its initial load of persisted overrides) and the + * interceptor backed by it. + * + * Call once on startup from any suitable coroutine scope and reuse the returned instance + * for both [applyDebugPanelConfig] and [KonfeaturePlugin]. + * + * @param context used to resolve [path] under the app's files directory. + * @param path file name for the DataStore file, relative to `context.filesDir`. + * @param logger optional logger forwarded to [KonfeatureDebugStore.create]. + */ + public suspend fun create( + context: Context, + path: String = DEFAULT_PATH, + logger: Logger? = null, + ): KonfeatureDebugPanelConfig { + val store = KonfeatureDebugStore.create( + path = context.filesDir.resolve(path).absolutePath, + logger = logger, + ) + return KonfeatureDebugPanelConfig( + store = store, + interceptor = KonfeatureDebugInterceptor(store), + ) + } + } +} + +/** + * Registers the debug panel's interceptor on this builder and marks [config] as attached so + * [KonfeaturePlugin] can verify the wiring. + * + * ```kotlin + * val konfeature = konfeature { + * register(MyFeatureConfig()) + * applyDebugPanelConfig(config) + * } + * ``` + */ +public fun KonfeatureBuilder.applyDebugPanelConfig( + config: KonfeatureDebugPanelConfig, +): KonfeatureBuilder { + addInterceptor(config.interceptor) + config.isAttached = true + return this +} diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeaturePlugin.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeaturePlugin.kt index a685e13d..b44a72be 100644 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeaturePlugin.kt +++ b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/KonfeaturePlugin.kt @@ -7,7 +7,6 @@ import com.redmadrobot.debug.core.plugin.Plugin import com.redmadrobot.debug.uikit.theme.DebugPanelTheme import com.redmadrobot.konfeature.Konfeature import com.redmadrobot.konfeature.ui.KonfeatureDebugPanel -import com.redmadrobot.konfeature.ui.KonfeatureDebugStore import com.redmadrobot.konfeature.ui.presentation.theme.KonfeatureTheme /** @@ -18,18 +17,30 @@ import com.redmadrobot.konfeature.ui.presentation.theme.KonfeatureTheme * palette, and keeps it in sync with the debug panel's light/dark theme mode. Boolean values are * toggled inline by the panel; other value types are shown read-only. * - * Overridden values are persisted by [store] (DataStore) and survive app restarts. + * Overridden values are persisted by [config]'s store (DataStore) and survive app restarts. + * + * Pass the same [config] you attached to [konfeature] via + * [applyDebugPanelConfig]; the constructor fails fast if it was never attached, since an unattached + * config means the panel would show values it can never override. * * @param konfeature Konfeature instance whose feature configs are displayed - * @param store store backing the overrides; create it once via [KonfeatureDebugStore.create] and - * reuse the same instance for [com.redmadrobot.konfeature.ui.KonfeatureDebugInterceptor] + * @param config the config created via [KonfeatureDebugPanelConfig.create] and attached to + * [konfeature] with [applyDebugPanelConfig] * - * @see KonfeatureDebugStore + * @see KonfeatureDebugPanelConfig */ public class KonfeaturePlugin( private val konfeature: Konfeature, - private val store: KonfeatureDebugStore, + private val config: KonfeatureDebugPanelConfig, ) : Plugin() { + init { + check(config.isAttached) { + "KonfeatureDebugPanelConfig was not attached to a Konfeature instance. " + + "Call konfeature { applyDebugPanelConfig(config) } before passing the config to " + + "KonfeaturePlugin." + } + } + override fun getName(): String = NAME /** @@ -37,7 +48,7 @@ public class KonfeaturePlugin( * the `konfeature-ui` library — so an empty container is returned. */ override fun getPluginContainer(commonContainer: CommonContainer): PluginDependencyContainer { - return object : PluginDependencyContainer {} + return PluginDependencyContainer.Empty } @Composable @@ -45,7 +56,7 @@ public class KonfeaturePlugin( KonfeatureTheme(isDarkTheme = DebugPanelTheme.isDarkTheme) { KonfeatureDebugPanel( konfeature = konfeature, - store = store, + store = config.store, ) } } diff --git a/sample/src/main/kotlin/com/redmadrobot/debug_sample/App.kt b/sample/src/main/kotlin/com/redmadrobot/debug_sample/App.kt index 61c4b7c0..34215e05 100644 --- a/sample/src/main/kotlin/com/redmadrobot/debug_sample/App.kt +++ b/sample/src/main/kotlin/com/redmadrobot/debug_sample/App.kt @@ -3,14 +3,13 @@ package com.redmadrobot.debug_sample import android.app.Application import com.redmadrobot.debug.core.DebugPanel import com.redmadrobot.debug.plugin.aboutapp.AboutAppPlugin +import com.redmadrobot.debug.plugin.konfeature.KonfeatureDebugPanelConfig import com.redmadrobot.debug.plugin.konfeature.KonfeaturePlugin import com.redmadrobot.debug.plugin.servers.ServersPlugin import com.redmadrobot.debug_sample.debug_data.DebugAboutAppInfoProvider import com.redmadrobot.debug_sample.debug_data.DebugServersProvider import com.redmadrobot.debug_sample.storage.TestKonfeatureProvider import com.redmadrobot.konfeature.Logger -import com.redmadrobot.konfeature.ui.KonfeatureDebugInterceptor -import com.redmadrobot.konfeature.ui.KonfeatureDebugStore import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -19,13 +18,12 @@ class App : Application() { super.onCreate() val konfeatureLogger = konfeatureLogger() - val store = runBlocking { - KonfeatureDebugStore.create( - path = filesDir.resolve("konfeature_debug.preferences_pb").absolutePath, + val debugPanelConfig = runBlocking { + KonfeatureDebugPanelConfig.create( + context = this@App, logger = konfeatureLogger, ) } - val debugInterceptor = KonfeatureDebugInterceptor(store) DebugPanel.initialize( application = this, @@ -34,8 +32,8 @@ class App : Application() { preInstalledServers = DebugServersProvider().provideData() ), KonfeaturePlugin( - konfeature = TestKonfeatureProvider.create(debugInterceptor, konfeatureLogger), - store = store, + konfeature = TestKonfeatureProvider.create(debugPanelConfig, konfeatureLogger), + config = debugPanelConfig, ), AboutAppPlugin( appInfoList = DebugAboutAppInfoProvider.provideData() diff --git a/sample/src/main/kotlin/com/redmadrobot/debug_sample/storage/TestKonfeatureProvider.kt b/sample/src/main/kotlin/com/redmadrobot/debug_sample/storage/TestKonfeatureProvider.kt index 10b68949..696e873f 100644 --- a/sample/src/main/kotlin/com/redmadrobot/debug_sample/storage/TestKonfeatureProvider.kt +++ b/sample/src/main/kotlin/com/redmadrobot/debug_sample/storage/TestKonfeatureProvider.kt @@ -1,21 +1,22 @@ package com.redmadrobot.debug_sample.storage +import com.redmadrobot.debug.plugin.konfeature.KonfeatureDebugPanelConfig +import com.redmadrobot.debug.plugin.konfeature.applyDebugPanelConfig import com.redmadrobot.konfeature.FeatureConfig import com.redmadrobot.konfeature.Konfeature import com.redmadrobot.konfeature.Logger import com.redmadrobot.konfeature.builder.konfeature import com.redmadrobot.konfeature.source.FeatureSource import com.redmadrobot.konfeature.source.SourceSelectionStrategy -import com.redmadrobot.konfeature.ui.KonfeatureDebugInterceptor internal object TestKonfeatureProvider { - fun create(debugInterceptor: KonfeatureDebugInterceptor, logger: Logger): Konfeature { + fun create(debugPanelConfig: KonfeatureDebugPanelConfig, logger: Logger): Konfeature { return konfeature { register(FeatureConfig1()) register(FeatureConfig2()) register(FeatureConfig3()) - addInterceptor(debugInterceptor) + applyDebugPanelConfig(debugPanelConfig) addSource(object : FeatureSource { override val name: String = "SampleFeatureSource"