diff --git a/apps/flipcash/features/scanner/build.gradle.kts b/apps/flipcash/features/scanner/build.gradle.kts index 6bf13d2c9..b02728203 100644 --- a/apps/flipcash/features/scanner/build.gradle.kts +++ b/apps/flipcash/features/scanner/build.gradle.kts @@ -23,5 +23,6 @@ dependencies { implementation(project(":libs:vibrator:bindings")) implementation(project(":ui:biometrics")) implementation(project(":ui:scanner")) + implementation(libs.androidx.camerax.view) implementation(libs.androidx.foundation.layout) } diff --git a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt index c652482b0..85f26c509 100644 --- a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt +++ b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt @@ -1,6 +1,7 @@ package com.flipcash.app.scanner.internal import android.annotation.SuppressLint +import androidx.camera.view.PreviewView import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -63,6 +64,9 @@ internal fun Scanner() { var isPinching by remember { mutableStateOf(false) } var zoomRatio by remember { mutableFloatStateOf(1f) } + // Exposed by CodeScanner so the tip card can snapshot the live feed for its blurred backdrop. + var previewView by remember { mutableStateOf(null) } + LaunchedEffect(biometricsState, previewing) { if (previewing == true) { focusManager.clearFocus() @@ -80,6 +84,7 @@ internal fun Scanner() { isPaused = isPaused, isPinching = isPinching, zoomRatio = zoomRatio, + previewView = previewView, onAction = { when (it) { ScannerDecorItem.Give -> { @@ -111,6 +116,7 @@ internal fun Scanner() { cameraAvailable = true previewing = it }, + onPreviewViewChanged = { previewView = it }, onCodeScanned = { result -> when (result) { is CodeScanResult.QrCode -> { diff --git a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/ScannableContainer.kt b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/ScannableContainer.kt index 540f127ac..801b1fd23 100644 --- a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/ScannableContainer.kt +++ b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/ScannableContainer.kt @@ -8,7 +8,10 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.ExitTransition import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import android.app.ActivityManager +import android.os.Build import androidx.compose.animation.togetherWith +import androidx.camera.view.PreviewView import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -16,6 +19,7 @@ import androidx.compose.material.DismissState import androidx.compose.material.DismissValue import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.derivedStateOf @@ -27,12 +31,19 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.setValue import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import com.flipcash.app.bills.AnimatedScannable +import com.flipcash.app.bills.components.cards.LocalTipCardBackdrop +import com.flipcash.app.bills.components.cards.LocalTipCardBaseAlpha +import com.flipcash.app.bills.components.cards.LocalTipCardColor +import com.flipcash.app.bills.components.cards.TipCardOpaqueFallback import com.flipcash.app.core.android.extensions.launchAppSettings import com.flipcash.app.core.bill.Scannable import com.flipcash.app.core.tipping.LocalTipCoordinator @@ -68,6 +79,7 @@ internal fun ScannableContainer( isPaused: Boolean, isPinching: Boolean = false, zoomRatio: Float = 1f, + previewView: PreviewView? = null, scannerView: @Composable () -> Unit, onAction: (ScannerDecorItem) -> Unit ) { @@ -109,6 +121,18 @@ internal fun ScannableContainer( val autoStart = state.autoStartCamera == true var cameraStarted by remember { mutableStateOf(autoStart) } + // Window-space origin of the scanner surface, used to align the blurred camera backdrop under + // the tip card wherever it sits. + var containerOriginInWindow by remember { mutableStateOf(Offset.Zero) } + + // The frosted camera backdrop snapshots the feed (a GPU→CPU readback). Gate it to devices that + // can absorb that — API 31+ for the blur, and not low-RAM. Everywhere else the card falls back + // to an opaque approximation of the frosted tone. + val supportsFrostedTipCard = remember { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + context.getSystemService(ActivityManager::class.java)?.isLowRamDevice != true + } + OnLifecycleEvent { _, event -> if (event == Lifecycle.Event.ON_STOP && !autoStart) { cameraStarted = false @@ -119,6 +143,7 @@ internal fun ScannableContainer( modifier = Modifier .fillMaxSize() .then(modifier) + .onGloballyPositioned { containerOriginInWindow = it.positionInWindow() } .testTag("scanner_view") ) { val availableUpdate by LocalAppUpdater.current.availableUpdate.collectAsStateWithLifecycle() @@ -262,31 +287,50 @@ internal fun ScannableContainer( label = "billVerticalBias", ) - AnimatedScannable( - modifier = Modifier.fillMaxSize(), - dismissState = billDismissState, - dismissed = dismissed, - contentPadding = PaddingValues( - start = CodeTheme.dimens.inset, - end = CodeTheme.dimens.inset, - top = CodeTheme.dimens.grid.x2, - bottom = billBottomInset, - ), - scannableAlignment = BiasAlignment(horizontalBias = 0f, verticalBias = billVerticalBias), - bill = updatedBillState.bill, - transitionSpec = { - when (updatedState.billResult) { - BillDeterminationResult.None -> EnterTransition.None - Grabbed -> AnimationUtils.animationBillEnterGrabbed - PutInWallet -> AnimationUtils.animationBillEnterGive - } togetherWith when (updatedState.billResult) { - BillDeterminationResult.None -> ExitTransition.None - Grabbed -> AnimationUtils.animationBillExitGrabbed - PutInWallet -> AnimationUtils.animationBillExitReturned - } - } + // Frosted-camera backdrop behind the tip card. Snapshots the feed once (see + // rememberCameraTipCardBackdrop) only while a tip card is up and only on capable devices. + val isTipCard = updatedBillState.bill is Scannable.TipCard + val tipCardBackdrop = rememberCameraTipCardBackdrop( + previewView = previewView, + enabled = isTipCard && supportsFrostedTipCard, + containerOriginInWindow = containerOriginInWindow, ) + // Where the frosted backdrop is unavailable, render the tip card as an opaque approximation + // of the frosted tone instead of a translucent panel over the (stutter-prone) live camera. + val useOpaqueFallback = isTipCard && !supportsFrostedTipCard + + CompositionLocalProvider( + LocalTipCardBackdrop provides tipCardBackdrop, + LocalTipCardColor provides if (useOpaqueFallback) TipCardOpaqueFallback else Color.Unspecified, + LocalTipCardBaseAlpha provides if (useOpaqueFallback) 1f else LocalTipCardBaseAlpha.current, + ) { + AnimatedScannable( + modifier = Modifier.fillMaxSize(), + dismissState = billDismissState, + dismissed = dismissed, + contentPadding = PaddingValues( + start = CodeTheme.dimens.inset, + end = CodeTheme.dimens.inset, + top = CodeTheme.dimens.grid.x2, + bottom = billBottomInset, + ), + scannableAlignment = BiasAlignment(horizontalBias = 0f, verticalBias = billVerticalBias), + bill = updatedBillState.bill, + transitionSpec = { + when (updatedState.billResult) { + BillDeterminationResult.None -> EnterTransition.None + Grabbed -> AnimationUtils.animationBillEnterGrabbed + PutInWallet -> AnimationUtils.animationBillEnterGive + } togetherWith when (updatedState.billResult) { + BillDeterminationResult.None -> ExitTransition.None + Grabbed -> AnimationUtils.animationBillExitGrabbed + PutInWallet -> AnimationUtils.animationBillExitReturned + } + } + ) + } + // Below-bill content, owned by the scannable type (see `overlays/ScannableOverlays`). // `displayedScannable` retains the last shown scannable so an overlay can still animate // OUT as `bill` returns to null on dismiss (the overlay stays mounted; only `visible` flips). diff --git a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/TipCardCameraBackdrop.kt b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/TipCardCameraBackdrop.kt new file mode 100644 index 000000000..90e748ba1 --- /dev/null +++ b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/TipCardCameraBackdrop.kt @@ -0,0 +1,97 @@ +package com.flipcash.app.scanner.internal.bills + +import androidx.camera.view.PreviewView +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import com.flipcash.app.bills.components.cards.TipCardBlurRadius +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive + +// How long to keep trying for a usable frame after the card appears (the stream may not be ready +// on the very first attempt), and the gap between attempts. +private const val CAPTURE_TIMEOUT_MS = 1_000L +private const val CAPTURE_RETRY_MS = 50L + +/** + * Builds the frosted-camera backdrop for a tip card. When [enabled], it takes a SINGLE frozen + * snapshot of the live [previewView] (camera only — the translucent card composited on top is + * never captured, so there is no feedback loop) and returns a composable that draws that blurred + * frame aligned to the card's bounds. Returns null until a frame is captured. + * + * A single snapshot is deliberate: [PreviewView.getBitmap] is a main-thread GPU→CPU readback, and + * doing it continuously stutters the live preview even on flagship hardware. A tip card is a static + * modal, so one frozen frame — captured as the card animates in — is enough. + * + * Haze can't blur the camera (it's an AndroidView outside the Compose layer), so we feed it the + * pixels ourselves via [PreviewView.getBitmap] and blur with [Modifier.blur]. + * + * @param containerOriginInWindow window-space origin of the full-screen scanner surface, used to + * line the camera frame up under the card wherever the card sits. + */ +@Composable +internal fun rememberCameraTipCardBackdrop( + previewView: PreviewView?, + enabled: Boolean, + containerOriginInWindow: Offset, +): (@Composable BoxScope.() -> Unit)? { + var frame by remember(previewView) { mutableStateOf(null) } + + LaunchedEffect(previewView, enabled) { + if (previewView == null || !enabled) { + frame = null + return@LaunchedEffect + } + // Grab one frame, retrying briefly until the stream yields a bitmap, then stop — no ongoing + // readback, so the live preview is never starved. + var waited = 0L + while (isActive && frame == null && waited < CAPTURE_TIMEOUT_MS) { + runCatching { previewView.bitmap }.getOrNull()?.let { frame = it.asImageBitmap() } + if (frame == null) { + delay(CAPTURE_RETRY_MS) + waited += CAPTURE_RETRY_MS + } + } + } + + val current = frame ?: return null + return { + CameraBackdropLayer( + frame = current, + containerOriginInWindow = containerOriginInWindow, + modifier = Modifier.matchParentSize(), + ) + } +} + +@Composable +private fun CameraBackdropLayer( + frame: ImageBitmap, + containerOriginInWindow: Offset, + modifier: Modifier = Modifier, +) { + var selfOriginInWindow by remember { mutableStateOf(Offset.Zero) } + Box( + modifier + .onGloballyPositioned { selfOriginInWindow = it.positionInWindow() } + .blur(TipCardBlurRadius) + .drawBehind { + // The frame fills the scanner surface; translate it so the slice behind the card + // (this layer) lines up with what the camera is actually showing there. + drawImage(frame, topLeft = containerOriginInWindow - selfOriginInWindow) + } + ) +} diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipCardScreen.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipCardScreen.kt index 4e4f155da..d613c6bb2 100644 --- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipCardScreen.kt +++ b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipCardScreen.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.bills.ScannableRenderer -import com.flipcash.app.bills.components.cards.LocalTipCardColor +import com.flipcash.app.bills.components.cards.LocalTipCardBaseAlpha import com.flipcash.app.core.bill.Scannable import com.flipcash.app.core.tipping.TipResult import com.flipcash.app.core.tipping.TipStep @@ -101,7 +101,7 @@ internal fun TipCardScreen() { Spacer(Modifier.weight(1f)) state.tipCard?.let { CompositionLocalProvider( - LocalTipCardColor provides Color(0xFF101011) + LocalTipCardBaseAlpha provides 0.36f ) { ScannableRenderer( scannable = it, diff --git a/apps/flipcash/shared/bills/build.gradle.kts b/apps/flipcash/shared/bills/build.gradle.kts index 82828d665..95cd54e93 100644 --- a/apps/flipcash/shared/bills/build.gradle.kts +++ b/apps/flipcash/shared/bills/build.gradle.kts @@ -14,4 +14,5 @@ dependencies { implementation(project(":apps:flipcash:shared:common-ui")) implementation(libs.androidx.datastore) + implementation(libs.bundles.haze) } diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/ScannableRenderer.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/ScannableRenderer.kt index c20b4faa3..3618a1f84 100644 --- a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/ScannableRenderer.kt +++ b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/ScannableRenderer.kt @@ -2,14 +2,12 @@ package com.flipcash.app.bills import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewWrapper import com.flipcash.app.bills.components.bills.CashBill import com.flipcash.app.bills.components.bills.GoldBar -import com.flipcash.app.bills.components.cards.LocalTipCardColor import com.flipcash.app.bills.components.cards.TipCard import com.flipcash.app.core.bill.Scannable import com.flipcash.app.theme.FlipcashThemeWrapper diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/cards/TipCard.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/cards/TipCard.kt index 243da3f9a..62bf75c36 100644 --- a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/cards/TipCard.kt +++ b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/components/cards/TipCard.kt @@ -2,6 +2,8 @@ package com.flipcash.app.bills.components.cards import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -15,7 +17,6 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Alignment @@ -24,6 +25,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.flipcash.app.bills.components.ScannableCode import com.flipcash.services.models.UserProfile @@ -32,8 +34,43 @@ import com.flipcash.shared.common.ui.ContactAvatar import com.getcode.theme.CodeTheme import com.getcode.theme.xxl + +/** Alpha applied to the card fill so its translucency is identical at every call site. */ +private const val TipCardAlpha = 0.82f +/** + * Base color the tip card is tinted with. Callers may override it, but the alpha is applied + * uniformly by [TipCard] itself (see [TipCardAlpha]) so the card reads identically everywhere — + * the differing per-usage opacities were a bug. + */ +val LocalTipCardBaseAlpha = staticCompositionLocalOf { TipCardAlpha } + +/** + * Optional backdrop rendered as the card's bottom layer, clipped to the card's rounded bounds and + * sitting under the translucent fill. Used to give the card a "frosted glass" look over rich + * content behind it (e.g. the live camera in the scanner). Null means no backdrop — the card is + * just its translucent fill. + */ +val LocalTipCardBackdrop = staticCompositionLocalOf<(@Composable BoxScope.() -> Unit)?> { null } + + + +/** Blur radius used for the card's backdrop, tuned to read like the design's 40px background blur. */ +val TipCardBlurRadius: Dp = 24.dp + +/** + * Base tint color for the card. Defaults to the theme's tip-card color; overridden on the opaque + * fallback path (see [TipCardOpaqueFallback]). + */ val LocalTipCardColor = staticCompositionLocalOf { Color.Unspecified } +/** + * Opaque stand-in for the frosted-glass tone, used on devices where the live blurred-camera + * backdrop is disabled (pre-API-31 / low-RAM). Rendered at full opacity so the card stays cheap and + * never shows the stutter-prone live feed through a translucent fill. Tune to taste to match the + * average frosted output. + */ +val TipCardOpaqueFallback = Color(0xFF1A1A1C) + @OptIn(ExperimentalLayoutApi::class) @Composable internal fun TipCard( @@ -42,7 +79,11 @@ internal fun TipCard( modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.Center, ) { - val cardColor = LocalTipCardColor.current.takeOrElse { CodeTheme.colors.tipCardColor } + val fillColor = LocalTipCardColor.current + .takeOrElse { CodeTheme.colors.tipCardColor } + .copy(alpha = LocalTipCardBaseAlpha.current) + val backdrop = LocalTipCardBackdrop.current + BoxWithConstraints( modifier = modifier .windowInsetsPadding(WindowInsets.statusBarsIgnoringVisibility) @@ -52,48 +93,55 @@ internal fun TipCard( val mW = this.maxWidth val codeSize = remember { mW * 0.65f } - Column( - modifier = Modifier - .background( - color = cardColor, - shape = CodeTheme.shapes.xxl, - ) - .padding(vertical = CodeTheme.dimens.grid.x8, horizontal = CodeTheme.dimens.grid.x7) - .heightIn(0.dp, 800.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x4) + Box( + modifier = Modifier.clip(CodeTheme.shapes.xxl) ) { - if (payloadData.isNotEmpty()) { - ScannableCode( - modifier = Modifier.size(codeSize), - data = payloadData, - icon = null, - ) + // Backdrop (blurred content behind the card) sits under the translucent fill. It is + // clipped to the rounded card bounds by the parent's clip. + if (backdrop != null) { + Box(modifier = Modifier.matchParentSize(), content = backdrop) } - Row( - verticalAlignment = Alignment.CenterVertically, + Column( + modifier = Modifier + .background(color = fillColor) + .padding(vertical = CodeTheme.dimens.grid.x8, horizontal = CodeTheme.dimens.grid.x7) + .heightIn(0.dp, 800.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x4) ) { - Text( - text = stringResource(R.string.label_tip), - style = CodeTheme.typography.textMedium, - color = CodeTheme.colors.textMain, - ) - - ContactAvatar( - modifier = Modifier - .padding(start = CodeTheme.dimens.grid.x2, end = CodeTheme.dimens.grid.x1) - .size(CodeTheme.dimens.staticGrid.x5) - .clip(CircleShape), - userProfile = user - ) - - Text( - text = user.displayName.orEmpty(), - style = CodeTheme.typography.textMedium, - color = CodeTheme.colors.textMain, - ) + if (payloadData.isNotEmpty()) { + ScannableCode( + modifier = Modifier.size(codeSize), + data = payloadData, + icon = null, + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.label_tip), + style = CodeTheme.typography.textMedium, + color = CodeTheme.colors.textMain, + ) + + ContactAvatar( + modifier = Modifier + .padding(start = CodeTheme.dimens.grid.x2, end = CodeTheme.dimens.grid.x1) + .size(CodeTheme.dimens.staticGrid.x5) + .clip(CircleShape), + userProfile = user + ) + + Text( + text = user.displayName.orEmpty(), + style = CodeTheme.typography.textMedium, + color = CodeTheme.colors.textMain, + ) + } } } } -} \ No newline at end of file +} diff --git a/ui/scanner/src/main/kotlin/com/getcode/ui/scanner/CodeScanner.kt b/ui/scanner/src/main/kotlin/com/getcode/ui/scanner/CodeScanner.kt index 9205d1677..19381b0e7 100644 --- a/ui/scanner/src/main/kotlin/com/getcode/ui/scanner/CodeScanner.kt +++ b/ui/scanner/src/main/kotlin/com/getcode/ui/scanner/CodeScanner.kt @@ -64,6 +64,9 @@ fun CodeScanner( onPreviewStateChanged: (Boolean) -> Unit, onCodeScanned: (CodeScanResult) -> Unit, onError: (Throwable) -> Unit = { }, + // Surfaces the underlying PreviewView so callers can snapshot the live feed (e.g. to render a + // blurred camera backdrop behind an overlay). Non-null while mounted, null on dispose. + onPreviewViewChanged: (PreviewView?) -> Unit = { }, ) { val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current @@ -74,6 +77,11 @@ fun CodeScanner( } } + DisposableEffect(previewView) { + onPreviewViewChanged(previewView) + onDispose { onPreviewViewChanged(null) } + } + val preview = remember { Preview.Builder().build().apply { surfaceProvider = previewView.surfaceProvider } } diff --git a/ui/theme/src/main/kotlin/com/getcode/theme/Theme.kt b/ui/theme/src/main/kotlin/com/getcode/theme/Theme.kt index 3decfbd3e..0c530e6d7 100644 --- a/ui/theme/src/main/kotlin/com/getcode/theme/Theme.kt +++ b/ui/theme/src/main/kotlin/com/getcode/theme/Theme.kt @@ -52,7 +52,7 @@ internal val CodeDefaultColorScheme = ColorScheme( toggleUncheckedTrackColor = BrandToggleUncheckedTrackColor, cashBill = CashBill, cashBillDecorColor = CashBillDecor, - tipCard = Brand, + tipCard = Color.Black, betaIndicator = BetaIndicator, bannerThemed = Brand, bannerError = Error,