diff --git a/CHANGELOG.md b/CHANGELOG.md index b461555..7d4defa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ Add entries under **Unreleased** in each pull request. The *Bump version* workfl ## [Unreleased] +### Added +- `DeckTrigger.FloatingButton(icon = Icons.Default.Build)` sets the floating button's icon from an `ImageVector`; the composable `icon` slot is now documented too. +- `DeckStyle(titleIcon = …)` replaces the bug icon next to the drawer title; `titleIcon = null` shows the title without an icon. + +### Changed +- The SharedPreferences browser lists only the app's own files. Files written by libraries and the system (WebView, Firebase, Google Play services, AndroidX, DevDeck) are behind a *Show library files* switch. + ## [1.1.0] - 2026-10-27 ### Added diff --git a/README.md b/README.md index 71e2138..d3525f8 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,12 @@ OkHttpClient.Builder().apply { DebugTools.interceptors().forEach(::addIntercepto - The swipe is detected without taking touches, so taps, scrolls and system back keep working. - On an emulator, a mouse can't do a two-finger swipe: use Ctrl + Shift + D or the floating button. - If your app uses two-finger horizontal pans (maps, canvases), turn the swipe off. +- To use your own icon on the floating button, pass an `ImageVector` or any composable: + +```kotlin +trigger = DeckTrigger.FloatingButton(icon = Icons.Default.Build) +trigger = DeckTrigger.FloatingButton(icon = { Icon(painterResource(R.drawable.ic_debug), contentDescription = "Open debug menu") }) +``` The drawer header shows the app version, plus chips for a new test build, an active recording (with a *Stop* button) and any network or memory simulation. @@ -168,7 +174,7 @@ DevDeck.install( | `UiToolsPlugin` | User Interface | Slow animations, keylines, screenshots, long screenshots, recording, gallery | ✓ | | `AppearancePlugin` | User Interface | Dark mode and app language overrides, system language and display settings | ✓ | | `PerformancePlugin` | System | Frame rate, jank and memory overlay, memory details | ✓ | -| `StoragePlugin` | System | SharedPreferences browser and editor, storage usage | ✓ | +| `StoragePlugin` | System | SharedPreferences browser and editor (the app's own files first; library files such as WebView or Firebase behind a switch), storage usage | ✓ | | `DevicePlugin` | System | Permissions, developer options | ✓ | | `InfoPlugin` | Info | App info, device info, shareable snapshot, bug report, version | ✓ | @@ -307,6 +313,13 @@ DevDeck.updateConfig { it.copy(plugins = it.plugins + cachePlugin) } `DeckStyle` also sets the panel width and colors; `collapsibleSections = false` shows every section open, and `oneSectionAtATime = true` closes the others when one opens. +The icon next to the title is `titleIcon`: pass your own composable, or `null` to show the title without an icon. `Icon` picks up the tile's colour. The floating button's icon is set on the trigger (see [Opening the drawer](#opening-the-drawer)). + +```kotlin +style = DeckStyle(titleIcon = { Icon(painterResource(R.drawable.ic_logo), contentDescription = null) }) +style = DeckStyle(titleIcon = null) +``` + **Payment screens:** DevDeck skips Adyen, Primer, Netcetera 3DS, Cardinal Commerce, Stripe, Braintree, Checkout.com and PayPal activities automatically (`DeckExclusions.PAYMENT_SDKS`). Add your own with `excludedActivityPrefixes`, and set `DevDeck.isEnabled = false` during 3DS inside your own screens. --- diff --git a/devdeck/api/devdeck.api b/devdeck/api/devdeck.api index b43f2c9..2a37abe 100644 --- a/devdeck/api/devdeck.api +++ b/devdeck/api/devdeck.api @@ -132,11 +132,14 @@ public final class io/devdeck/DeckStateKt { public final class io/devdeck/DeckStyle { public static final field $stable I public synthetic fun (Ljava/lang/String;Ljava/lang/String;FFJJJLandroidx/compose/material3/ColorScheme;Landroidx/compose/material3/ColorScheme;IZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;FFJJJLandroidx/compose/material3/ColorScheme;Landroidx/compose/material3/ColorScheme;IZZLkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;FFJJJLandroidx/compose/material3/ColorScheme;Landroidx/compose/material3/ColorScheme;IZZLkotlin/jvm/functions/Function2;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (Ljava/lang/String;Ljava/lang/String;FFJJJLandroidx/compose/material3/ColorScheme;Landroidx/compose/material3/ColorScheme;IZZLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component10 ()I public final fun component11 ()Z public final fun component12 ()Z + public final fun component13 ()Lkotlin/jvm/functions/Function2; public final fun component2 ()Ljava/lang/String; public final fun component3-D9Ej5fM ()F public final fun component4 ()F @@ -145,8 +148,10 @@ public final class io/devdeck/DeckStyle { public final fun component7-0d7_KjU ()J public final fun component8 ()Landroidx/compose/material3/ColorScheme; public final fun component9 ()Landroidx/compose/material3/ColorScheme; - public final fun copy-JPBzAow (Ljava/lang/String;Ljava/lang/String;FFJJJLandroidx/compose/material3/ColorScheme;Landroidx/compose/material3/ColorScheme;IZZ)Lio/devdeck/DeckStyle; + public final synthetic fun copy-JPBzAow (Ljava/lang/String;Ljava/lang/String;FFJJJLandroidx/compose/material3/ColorScheme;Landroidx/compose/material3/ColorScheme;IZZ)Lio/devdeck/DeckStyle; public static synthetic fun copy-JPBzAow$default (Lio/devdeck/DeckStyle;Ljava/lang/String;Ljava/lang/String;FFJJJLandroidx/compose/material3/ColorScheme;Landroidx/compose/material3/ColorScheme;IZZILjava/lang/Object;)Lio/devdeck/DeckStyle; + public final fun copy-WDFtIgw (Ljava/lang/String;Ljava/lang/String;FFJJJLandroidx/compose/material3/ColorScheme;Landroidx/compose/material3/ColorScheme;IZZLkotlin/jvm/functions/Function2;)Lio/devdeck/DeckStyle; + public static synthetic fun copy-WDFtIgw$default (Lio/devdeck/DeckStyle;Ljava/lang/String;Ljava/lang/String;FFJJJLandroidx/compose/material3/ColorScheme;Landroidx/compose/material3/ColorScheme;IZZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/devdeck/DeckStyle; public fun equals (Ljava/lang/Object;)Z public final fun getAnimationDurationMillis ()I public final fun getCardColor-0d7_KjU ()J @@ -159,6 +164,7 @@ public final class io/devdeck/DeckStyle { public final fun getScrimColor-0d7_KjU ()J public final fun getSubtitle ()Ljava/lang/String; public final fun getTitle ()Ljava/lang/String; + public final fun getTitleIcon ()Lkotlin/jvm/functions/Function2; public final fun getWidthFraction ()F public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -172,6 +178,8 @@ public final class io/devdeck/DeckTrigger$FloatingButton : io/devdeck/DeckTrigge public fun ()V public fun (Landroidx/compose/ui/Alignment;ZLkotlin/jvm/functions/Function2;)V public synthetic fun (Landroidx/compose/ui/Alignment;ZLkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Landroidx/compose/ui/graphics/vector/ImageVector;Landroidx/compose/ui/Alignment;Z)V + public synthetic fun (Landroidx/compose/ui/graphics/vector/ImageVector;Landroidx/compose/ui/Alignment;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Landroidx/compose/ui/Alignment; public final fun component2 ()Z public final fun component3 ()Lkotlin/jvm/functions/Function2; diff --git a/devdeck/src/main/kotlin/io/devdeck/DeckPanel.kt b/devdeck/src/main/kotlin/io/devdeck/DeckPanel.kt index b6bf470..52aa77f 100644 --- a/devdeck/src/main/kotlin/io/devdeck/DeckPanel.kt +++ b/devdeck/src/main/kotlin/io/devdeck/DeckPanel.kt @@ -41,6 +41,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -143,7 +144,7 @@ private fun DeckPageContent( ) { Column(Modifier.fillMaxSize().background(background).windowInsetsPadding(WindowInsets.safeDrawing.only(insetSides))) { if (page == null) { - DeckHeader(title = style.title, subtitle = style.subtitle, onClose = state::close) + DeckHeader(title = style.title, subtitle = style.subtitle, icon = style.titleIcon, onClose = state::close) } else { DeckPageBar(title = page.title, onBack = { state.navigateBack() }, onClose = state::close) } @@ -172,7 +173,7 @@ private const val PAGE_TRANSITION_MILLIS = 280 private const val FOCUS_TIMEOUT_MILLIS = 2_000L @Composable -private fun DeckHeader(title: String, subtitle: String?, onClose: () -> Unit) { +private fun DeckHeader(title: String, subtitle: String?, icon: (@Composable () -> Unit)?, onClose: () -> Unit) { val context = LocalContext.current val version = remember(context) { appVersionInfo(context)?.let { "${it.versionName} (${it.versionCode})" } } val warning = LocalDeckColors.current?.warning ?: MaterialTheme.colorScheme.error @@ -181,13 +182,15 @@ private fun DeckHeader(title: String, subtitle: String?, onClose: () -> Unit) { val heldBytes = MemorySimulator.heldBytes Column(Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp, top = 12.dp, bottom = 12.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { - Box( - Modifier.size(44.dp).background(MaterialTheme.colorScheme.primary, RoundedCornerShape(14.dp)), - contentAlignment = Alignment.Center, - ) { - Icon(DeckIcons.BugReport, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimary) + if (icon != null) { + Box( + Modifier.size(44.dp).background(MaterialTheme.colorScheme.primary, RoundedCornerShape(14.dp)), + contentAlignment = Alignment.Center, + ) { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimary, content = icon) + } + Spacer(Modifier.width(12.dp)) } - Spacer(Modifier.width(12.dp)) Column(Modifier.weight(1f)) { Text( text = title, diff --git a/devdeck/src/main/kotlin/io/devdeck/DeckStyle.kt b/devdeck/src/main/kotlin/io/devdeck/DeckStyle.kt index 75047cf..6c4c841 100644 --- a/devdeck/src/main/kotlin/io/devdeck/DeckStyle.kt +++ b/devdeck/src/main/kotlin/io/devdeck/DeckStyle.kt @@ -1,11 +1,18 @@ package io.devdeck import androidx.compose.material3.ColorScheme +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import io.devdeck.internal.DeckIcons +/** + * How the drawer looks. [titleIcon] is drawn in the tile next to the title, tinted for that tile (use `Icon` so it + * picks up the colour); pass `null` to show the title without an icon. + */ @Immutable public data class DeckStyle( val title: String = "Debug menu", @@ -20,4 +27,57 @@ public data class DeckStyle( val animationDurationMillis: Int = 250, val collapsibleSections: Boolean = true, val oneSectionAtATime: Boolean = false, -) + val titleIcon: (@Composable () -> Unit)? = DefaultTitleIcon, +) { + + @Deprecated("Kept for binary compatibility", level = DeprecationLevel.HIDDEN) + public constructor( + title: String = "Debug menu", + subtitle: String? = null, + maxWidth: Dp = 360.dp, + widthFraction: Float = 0.85f, + scrimColor: Color = Color.Black.copy(alpha = 0.4f), + containerColor: Color = Color.Unspecified, + cardColor: Color = Color.Unspecified, + lightColorScheme: ColorScheme? = null, + darkColorScheme: ColorScheme? = null, + animationDurationMillis: Int = 250, + collapsibleSections: Boolean = true, + oneSectionAtATime: Boolean = false, + ) : this( + title, subtitle, maxWidth, widthFraction, scrimColor, containerColor, cardColor, lightColorScheme, darkColorScheme, + animationDurationMillis, collapsibleSections, oneSectionAtATime, DefaultTitleIcon, + ) + + @Deprecated("Kept for binary compatibility", level = DeprecationLevel.HIDDEN) + public fun copy( + title: String = this.title, + subtitle: String? = this.subtitle, + maxWidth: Dp = this.maxWidth, + widthFraction: Float = this.widthFraction, + scrimColor: Color = this.scrimColor, + containerColor: Color = this.containerColor, + cardColor: Color = this.cardColor, + lightColorScheme: ColorScheme? = this.lightColorScheme, + darkColorScheme: ColorScheme? = this.darkColorScheme, + animationDurationMillis: Int = this.animationDurationMillis, + collapsibleSections: Boolean = this.collapsibleSections, + oneSectionAtATime: Boolean = this.oneSectionAtATime, + ): DeckStyle = copy( + title = title, + subtitle = subtitle, + maxWidth = maxWidth, + widthFraction = widthFraction, + scrimColor = scrimColor, + containerColor = containerColor, + cardColor = cardColor, + lightColorScheme = lightColorScheme, + darkColorScheme = darkColorScheme, + animationDurationMillis = animationDurationMillis, + collapsibleSections = collapsibleSections, + oneSectionAtATime = oneSectionAtATime, + titleIcon = titleIcon, + ) +} + +private val DefaultTitleIcon: @Composable () -> Unit = { Icon(DeckIcons.BugReport, contentDescription = null) } diff --git a/devdeck/src/main/kotlin/io/devdeck/DeckTrigger.kt b/devdeck/src/main/kotlin/io/devdeck/DeckTrigger.kt index 5761846..fd1eda3 100644 --- a/devdeck/src/main/kotlin/io/devdeck/DeckTrigger.kt +++ b/devdeck/src/main/kotlin/io/devdeck/DeckTrigger.kt @@ -1,17 +1,33 @@ package io.devdeck +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.vector.ImageVector @Immutable public sealed interface DeckTrigger { public data object None : DeckTrigger + /** + * A round button that opens the drawer. [icon] replaces the default bug icon with any composable, for example + * `{ Icon(painterResource(R.drawable.ic_debug), contentDescription = "Open debug menu") }`. + */ public data class FloatingButton( val alignment: Alignment = Alignment.CenterEnd, val draggable: Boolean = true, val icon: (@Composable () -> Unit)? = null, - ) : DeckTrigger + ) : DeckTrigger { + + /** Shows [icon] (for example `Icons.Default.Build`) instead of the default bug icon. */ + public constructor( + icon: ImageVector, + alignment: Alignment = Alignment.CenterEnd, + draggable: Boolean = true, + ) : this(alignment, draggable, { Icon(icon, contentDescription = OPEN_DESCRIPTION) }) + } } + +internal const val OPEN_DESCRIPTION = "Open debug menu" diff --git a/devdeck/src/main/kotlin/io/devdeck/DevDeckOverlay.kt b/devdeck/src/main/kotlin/io/devdeck/DevDeckOverlay.kt index e36e203..5b965c1 100644 --- a/devdeck/src/main/kotlin/io/devdeck/DevDeckOverlay.kt +++ b/devdeck/src/main/kotlin/io/devdeck/DevDeckOverlay.kt @@ -213,7 +213,7 @@ private fun FloatingTrigger(trigger: DeckTrigger.FloatingButton, onClick: () -> .size(TriggerSize), ) { Box(contentAlignment = Alignment.Center) { - trigger.icon?.invoke() ?: Icon(DeckIcons.BugReport, contentDescription = "Open debug menu") + trigger.icon?.invoke() ?: Icon(DeckIcons.BugReport, contentDescription = OPEN_DESCRIPTION) } } if (UpdateChecker.availableUpdate != null) { diff --git a/devdeck/src/main/kotlin/io/devdeck/storage/LibraryPreferences.kt b/devdeck/src/main/kotlin/io/devdeck/storage/LibraryPreferences.kt new file mode 100644 index 0000000..c5e98cd --- /dev/null +++ b/devdeck/src/main/kotlin/io/devdeck/storage/LibraryPreferences.kt @@ -0,0 +1,36 @@ +package io.devdeck.storage + +/** + * Tells the app's own SharedPreferences files apart from the ones libraries and the system write into the same folder + * (WebView, Firebase, Google Play services, AndroidX, DevDeck itself). + */ +internal object LibraryPreferences { + + /** True when [name] belongs to a library rather than to the app with package [packageName]. */ + fun isLibrary(name: String, packageName: String): Boolean { + if (LIBRARY_PREFIXES.any { name.startsWith(it) }) return true + // Files named like a package (com.google.foo, androidx.work.util) belong to whoever owns that package. + if (!QUALIFIED_NAME.matches(name)) return false + return !name.startsWith(organisation(packageName)) + } + + /** The first two segments of the package, so com.example.app and com.example.app.debug both own com.example.*. */ + private fun organisation(packageName: String): String = packageName.split('.').take(2).joinToString(".", postfix = ".") + + private val QUALIFIED_NAME = Regex("^[a-z][a-z0-9_]*(\\.[A-Za-z0-9_]+)+.*") + + private val LIBRARY_PREFIXES = listOf( + "devdeck_", + "androidx.", + "android.", + "com.android.", + "com.google.", + "WebView", + "Firebase", + "PersistedInstallation", + "com.facebook.", + "com.crashlytics", + "com.datadog.", + "io.sentry", + ) +} diff --git a/devdeck/src/main/kotlin/io/devdeck/storage/SharedPreferencesBrowser.kt b/devdeck/src/main/kotlin/io/devdeck/storage/SharedPreferencesBrowser.kt index e7a5c49..d683474 100644 --- a/devdeck/src/main/kotlin/io/devdeck/storage/SharedPreferencesBrowser.kt +++ b/devdeck/src/main/kotlin/io/devdeck/storage/SharedPreferencesBrowser.kt @@ -13,9 +13,11 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -48,7 +50,12 @@ public fun SharedPreferencesBrowser(modifier: Modifier = Modifier, include: (Str val loaded by produceState?>(null, context, version) { value = withContext(Dispatchers.IO) { StorageFiles.sharedPreferencesNames(context) } } - val names = remember(loaded, include) { loaded.orEmpty().filter(include) } + // Library files (WebView, Firebase, DevDeck itself) are hidden unless asked for, so the app's own files stand out. + var showLibraryFiles by rememberSaveable { mutableStateOf(false) } + val (libraryNames, appNames) = remember(loaded, include) { + loaded.orEmpty().filter(include).partition { LibraryPreferences.isLibrary(it, context.packageName) } + } + val names = if (showLibraryFiles) appNames + libraryNames else appNames Column(modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth().padding(start = 16.dp, end = 4.dp), verticalAlignment = Alignment.CenterVertically) { Text( @@ -74,6 +81,15 @@ public fun SharedPreferencesBrowser(modifier: Modifier = Modifier, include: (Str } if (loaded != null && names.isEmpty()) TextItem("No SharedPreferences files") InfoGroup(dividerThreshold = 2) { names.forEach { name -> key(name, version) { PreferenceFileRow(name) } } } + if (libraryNames.isNotEmpty()) { + SectionDivider() + SwitchItem( + title = "Show library files", + subtitle = "${libraryNames.size} from libraries and the system, such as WebView or Firebase", + checked = showLibraryFiles, + onCheckedChange = { showLibraryFiles = it }, + ) + } } } diff --git a/devdeck/src/test/kotlin/io/devdeck/storage/LibraryPreferencesTest.kt b/devdeck/src/test/kotlin/io/devdeck/storage/LibraryPreferencesTest.kt new file mode 100644 index 0000000..85afad4 --- /dev/null +++ b/devdeck/src/test/kotlin/io/devdeck/storage/LibraryPreferencesTest.kt @@ -0,0 +1,34 @@ +package io.devdeck.storage + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class LibraryPreferencesTest { + + private fun isLibrary(name: String) = LibraryPreferences.isLibrary(name, packageName = "com.example.shop.debug") + + @Test + fun `keeps the app's own files`() { + listOf("user_session", "settings", "onboarding", "com.example.shop.debug_preferences", "com.example.cart").forEach { + assertFalse(it, isLibrary(it)) + } + } + + @Test + fun `hides library and system files`() { + listOf( + "WebViewChromiumPrefs", + "WebViewProfilePrefsDefault", + "FirebaseHeartBeatW0RFRkFVTFRd", + "PersistedInstallation.W0RFRkFVTFRd", + "com.google.android.gms.measurement.prefs", + "com.google.firebase.crashlytics", + "androidx.work.util", + "devdeck_settings", + "com.facebook.sdk.appEventPreferences", + "io.sentry.prefs", + "org.example.analytics", + ).forEach { assertTrue(it, isLibrary(it)) } + } +} diff --git a/sample/src/main/kotlin/io/devdeck/sample/ui/DrawerControls.kt b/sample/src/main/kotlin/io/devdeck/sample/ui/DrawerControls.kt index 8c51276..f168c41 100644 --- a/sample/src/main/kotlin/io/devdeck/sample/ui/DrawerControls.kt +++ b/sample/src/main/kotlin/io/devdeck/sample/ui/DrawerControls.kt @@ -25,7 +25,14 @@ fun DrawerControls(options: DrawerOptions, modifier: Modifier = Modifier) { ControlSwitch("Two-finger swipe to open", options.twoFingerSwipe) { options.twoFingerSwipe = it } ControlSwitch("Ctrl + Shift + D shortcut", options.keyboardShortcut) { options.keyboardShortcut = it } ControlSwitch("Floating button", options.floatingButton) { options.floatingButton = it } + ControlSwitch("Custom floating button icon", options.customIcon, enabled = options.floatingButton) { options.customIcon = it } ControlSwitch("One section open at a time", options.oneSectionAtATime) { options.oneSectionAtATime = it } + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + Text("Title icon", modifier = Modifier.padding(end = 8.dp)) + TitleIcon.entries.forEach { icon -> + FilterChip(selected = options.titleIcon == icon, onClick = { options.titleIcon = icon }, label = { Text(icon.name) }) + } + } Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { Text("Side", modifier = Modifier.padding(end = 8.dp)) DeckSide.entries.forEach { side -> diff --git a/sample/src/main/kotlin/io/devdeck/sample/ui/DrawerOptions.kt b/sample/src/main/kotlin/io/devdeck/sample/ui/DrawerOptions.kt index bb33bf0..52b5410 100644 --- a/sample/src/main/kotlin/io/devdeck/sample/ui/DrawerOptions.kt +++ b/sample/src/main/kotlin/io/devdeck/sample/ui/DrawerOptions.kt @@ -1,15 +1,19 @@ package io.devdeck.sample.ui +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.res.painterResource import io.devdeck.DeckGestures import io.devdeck.DeckShortcut import io.devdeck.DeckSide +import io.devdeck.DeckStyle import io.devdeck.DeckTrigger +import io.devdeck.sample.R @Stable class DrawerOptions { @@ -17,6 +21,8 @@ class DrawerOptions { var twoFingerSwipe by mutableStateOf(true) var keyboardShortcut by mutableStateOf(true) var floatingButton by mutableStateOf(true) + var customIcon by mutableStateOf(false) + var titleIcon by mutableStateOf(TitleIcon.Default) var side by mutableStateOf(DeckSide.End) var oneSectionAtATime by mutableStateOf(false) @@ -27,8 +33,31 @@ class DrawerOptions { ) val trigger: DeckTrigger - get() = if (floatingButton) DeckTrigger.FloatingButton() else DeckTrigger.None + get() = when { + !floatingButton -> DeckTrigger.None + customIcon -> customIconTrigger + else -> DeckTrigger.FloatingButton() + } } +enum class TitleIcon { Default, Custom, None } + +/** The drawer style with the sample's title icon choice applied. */ +fun DrawerOptions.style(base: DeckStyle): DeckStyle = base.copy( + oneSectionAtATime = oneSectionAtATime, + titleIcon = when (titleIcon) { + TitleIcon.Default -> base.titleIcon + TitleIcon.Custom -> wrenchIcon + TitleIcon.None -> null + }, +) + +private val wrenchIcon: @Composable () -> Unit = { Icon(painterResource(R.drawable.ic_wrench), contentDescription = null) } + +// Any composable works as the icon, here a drawable from the app's own resources. +private val customIconTrigger = DeckTrigger.FloatingButton( + icon = { Icon(painterResource(R.drawable.ic_wrench), contentDescription = "Open debug menu") }, +) + @Composable fun rememberDrawerOptions(): DrawerOptions = remember { DrawerOptions() } diff --git a/sample/src/main/kotlin/io/devdeck/sample/ui/SampleScreen.kt b/sample/src/main/kotlin/io/devdeck/sample/ui/SampleScreen.kt index be4b229..ac234ef 100644 --- a/sample/src/main/kotlin/io/devdeck/sample/ui/SampleScreen.kt +++ b/sample/src/main/kotlin/io/devdeck/sample/ui/SampleScreen.kt @@ -34,7 +34,7 @@ fun SampleScreen(plugins: List, flagStore: FeatureFlagStore, onOpenL side = options.side, gestures = options.gestures, trigger = options.trigger, - style = sampleDeckStyle.copy(oneSectionAtATime = options.oneSectionAtATime), + style = options.style(sampleDeckStyle), ) { Scaffold { padding -> Column( diff --git a/sample/src/main/res/drawable/ic_wrench.xml b/sample/src/main/res/drawable/ic_wrench.xml new file mode 100644 index 0000000..bdff867 --- /dev/null +++ b/sample/src/main/res/drawable/ic_wrench.xml @@ -0,0 +1,11 @@ + + + + +