Skip to content

Instantly share code, notes, and snippets.

@necatisozer
Created February 28, 2023 08:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save necatisozer/634002c1ad32e1bac84ff344d032d138 to your computer and use it in GitHub Desktop.
Save necatisozer/634002c1ad32e1bac84ff344d032d138 to your computer and use it in GitHub Desktop.
/*
* Copyright 2023 Lyrebird Studio
*/
package com.lyrebirdstudio.facelab.ui.photoedit
import android.graphics.Matrix
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.times
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.toIntRect
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.window.DialogProperties
import androidx.core.graphics.values
import com.lyrebirdstudio.facelab.R
import com.lyrebirdstudio.facelab.analytics.LocalAnalytics
import com.lyrebirdstudio.facelab.analytics.trackCommon1Event
import com.lyrebirdstudio.facelab.analytics.trackCommonEventWith2
import com.lyrebirdstudio.facelab.analytics.trackUxCamEvent
import com.lyrebirdstudio.facelab.core.util.tryCatching
import com.lyrebirdstudio.facelab.data.Fail
import com.lyrebirdstudio.facelab.data.Gender
import com.lyrebirdstudio.facelab.data.Loading
import com.lyrebirdstudio.facelab.data.Success
import com.lyrebirdstudio.facelab.data.network.InternetNotConnectedException
import com.lyrebirdstudio.facelab.data.photoprocess.BoundingBox
import com.lyrebirdstudio.facelab.data.photoprocess.Category
import com.lyrebirdstudio.facelab.data.photoprocess.ExcessiveUseException
import com.lyrebirdstudio.facelab.data.photoprocess.FaceDetail
import com.lyrebirdstudio.facelab.data.photoprocess.Filter
import com.lyrebirdstudio.facelab.data.photoprocess.FilterType
import com.lyrebirdstudio.facelab.data.user.LocalSessionTracker
import com.lyrebirdstudio.facelab.data.value
import com.lyrebirdstudio.facelab.sdk.uxcam.occludeSensitiveComposable
import com.lyrebirdstudio.facelab.theme.FaceLabTheme3
import com.lyrebirdstudio.facelab.theme.components.FaceLabAlertDialog3
import com.lyrebirdstudio.facelab.theme.components.FaceLabBackground
import com.lyrebirdstudio.facelab.theme.components.FaceLabButton3
import com.lyrebirdstudio.facelab.theme.components.FaceLabButtonDefaults
import com.lyrebirdstudio.facelab.theme.components.FaceLabCard3
import com.lyrebirdstudio.facelab.theme.components.FaceLabGradientButton3
import com.lyrebirdstudio.facelab.theme.components.FaceLabLargeGradientButton3
import com.lyrebirdstudio.facelab.theme.components.FaceLabSmallButton3
import com.lyrebirdstudio.facelab.theme.components.FaceLabTextButton3
import com.lyrebirdstudio.facelab.theme.components.FaceLabTopAppBar3
import com.lyrebirdstudio.facelab.ui.components.FaceLabImage
import com.lyrebirdstudio.facelab.ui.components.FaceLabLinearProgressIndicator
import com.lyrebirdstudio.facelab.ui.components.rememberFaceLabImagePainter
import com.lyrebirdstudio.facelab.ui.dimensions.rememberDimensions
import com.lyrebirdstudio.facelab.ui.utils.annotatedStringResource
import com.lyrebirdstudio.facelab.ui.utils.drawIntoNativeCanvas
import com.lyrebirdstudio.facelab.ui.utils.gesture.detectPointerTransformGestures
import com.lyrebirdstudio.facelab.ui.utils.mapOffset
import com.lyrebirdstudio.facelab.ui.utils.mapRect
import com.lyrebirdstudio.facelab.ui.utils.minimumTouchTargetSize
import com.lyrebirdstudio.facelab.ui.utils.onDpSizeChanged
import com.lyrebirdstudio.facelab.ui.utils.plus
import com.lyrebirdstudio.facelab.ui.utils.round
import com.lyrebirdstudio.facelab.ui.utils.tint
import com.lyrebirdstudio.facelab.ui.utils.toComposeSize
import com.lyrebirdstudio.facelab.ui.utils.toRect
import com.lyrebirdstudio.facelab.util.graphics.inverted
import dev.burnoo.compose.rememberpreference.rememberBooleanPreference
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.delay
@Composable
fun PhotoEditScreen(
uiState: PhotoEditUiState,
onBack: () -> Unit,
onProcessProClick: () -> Unit,
onProcessTryAgainClick: () -> Unit,
onProcessChoosePhotoClick: () -> Unit,
onFaceSelect: (FaceDetail) -> Unit,
onFaceConfirm: (FaceDetail) -> Unit,
onFaceSelectConfirm: () -> Unit,
onFaceSelection: (saveChanges: Boolean) -> Unit,
onCategoryClick: (Category) -> Unit,
onFilterSelect: (Filter) -> Unit,
onFilterSelectCancel: () -> Unit,
onFilterDeselect: () -> Unit,
onFilterApplyClick: () -> Unit,
onFilterCancel: () -> Unit,
onGenderClick: (Gender) -> Unit,
onUndoClick: () -> Unit,
onRedoClick: () -> Unit,
onSave: () -> Unit,
onProcessAgreementDismiss: () -> Unit,
onProcessAgreementConfirm: () -> Unit,
onExcessiveUseDialogDismiss: () -> Unit,
onExcessiveUseDialogConfirm: () -> Unit,
modifier: Modifier = Modifier
) {
val analytics = LocalAnalytics.current
// Gender menu states
var genderMenuExpanded by remember { mutableStateOf(false) }
var genderMenuTipVisible by remember { mutableStateOf(false) }
var genderMenuTipShown by
rememberBooleanPreference(keyName = "genderMenuTipShown220117", defaultValue = false)
// Gender menu tip logic
LaunchedEffect(uiState.state) {
delay(500.milliseconds)
if (uiState.state is PhotoEditUiState.State.Edit && genderMenuTipShown == false) {
genderMenuTipVisible = true
genderMenuTipShown = true
}
}
// Compare button tip states
var compareButtonTipVisible by remember { mutableStateOf(false) }
var compareButtonTipShown by
rememberBooleanPreference(keyName = "compareButtonTipShown220117", defaultValue = false)
// Compare button tip logic
LaunchedEffect(uiState.appliedFilters) {
delay(500.milliseconds)
if (uiState.appliedFilters.isNotEmpty() && compareButtonTipShown == false) {
compareButtonTipVisible = true
compareButtonTipShown = true
}
}
// Menu & tips logic
val collapseAllMenusAndTips: () -> Unit = remember {
{
if (genderMenuExpanded) {
analytics.trackCommonEventWith2("editChangeGenderExit")
}
genderMenuExpanded = false
genderMenuTipVisible = false
compareButtonTipVisible = false
}
}
// Dialog states
var exitDialogVisible by remember { mutableStateOf(false) }
var saveDialogVisible by remember { mutableStateOf(false) }
val dialogVisible = exitDialogVisible || saveDialogVisible
// Back logic
val backEnabled =
dialogVisible.not() &&
uiState.selectedCategoryId == null &&
(uiState.state is PhotoEditUiState.State.Process.Loading ||
uiState.state is PhotoEditUiState.State.FaceSelection ||
uiState.state is PhotoEditUiState.State.Edit)
val onBackAttempt: () -> Unit = {
collapseAllMenusAndTips()
if (uiState.selectedCategoryId != null) {
onFilterCancel()
} else if (
uiState.state is PhotoEditUiState.State.Edit &&
(uiState.savedFilters.isNotEmpty() ||
uiState.appliedFilters.isNotEmpty() ||
uiState.revertedFilters.isNotEmpty())
) {
exitDialogVisible = true
} else {
onBack()
}
}
BackHandler {
if (backEnabled) {
onBackAttempt()
}
}
val saveEnabled =
when {
dialogVisible -> false
uiState.state !is PhotoEditUiState.State.Edit -> false
uiState.appliedFilter == null ->
uiState.savedFilters.isNotEmpty() || uiState.appliedFilters.isNotEmpty()
uiState.appliedFilter is Success -> true
else -> false
}
val dimensions = rememberDimensions()
Column(modifier = modifier) {
PhotoEditTopAppBar2(
onBackClick = onBackAttempt,
backEnabled = backEnabled,
undoVisible = uiState.selectedCategoryId == null,
undoEnabled = backEnabled && uiState.appliedFilters.isNotEmpty(),
onUndoClick = {
collapseAllMenusAndTips()
onUndoClick()
},
redoVisible = uiState.selectedCategoryId == null,
redoEnabled = backEnabled && uiState.revertedFilters.isNotEmpty(),
onRedoClick = {
collapseAllMenusAndTips()
onRedoClick()
},
saveEnabled = saveEnabled,
onSaveClick = {
collapseAllMenusAndTips()
onSave()
},
)
FaceLabBackground(modifier = Modifier.weight(1f)) {
var categoriesSize by remember { mutableStateOf(DpSize.Zero) }
var filtersSize by remember { mutableStateOf(DpSize.Zero) }
if (uiState.sampledImage != null && uiState.sampledImageSize != null) {
ImageLayout(
state = uiState.state,
baseImage = uiState.baseImage!!,
editedImage = uiState.editedImage,
imageSize = uiState.sampledImageSize.toComposeSize(),
faces = uiState.faceDetails,
selectedFaceId = uiState.selectedFaceId,
onFaceSelect = onFaceSelect,
onFaceConfirm = onFaceConfirm,
onClick = collapseAllMenusAndTips,
modifier =
Modifier.occludeSensitiveComposable("edit")
.matchParentSize()
.padding(
bottom =
if (categoriesSize.height > 0.dp) {
categoriesSize.height
} else {
92.dp +
WindowInsets.navigationBars
.asPaddingValues()
.calculateBottomPadding()
},
),
)
}
if (
(uiState.state is PhotoEditUiState.State.FaceSelection ||
uiState.state is PhotoEditUiState.State.Edit) &&
uiState.selectedCategoryId == null &&
uiState.faceDetails != null &&
uiState.faceDetails.size > 1
) {
CircleIconButton(
painter = painterResource(id = R.drawable.change_face),
onClick = {
analytics.trackCommonEventWith2("editChangeFaceClick")
if (uiState.appliedFilters.isNotEmpty()) {
saveDialogVisible = true
analytics.trackUxCamEvent("editChangeFaceDialogView")
} else {
onFaceSelection(false)
}
},
enabled = genderMenuExpanded.not(),
selected = uiState.state is PhotoEditUiState.State.FaceSelection,
modifier = Modifier.padding(dimensions.margin).align(Alignment.TopStart),
)
}
if (
uiState.state is PhotoEditUiState.State.Edit &&
uiState.selectedCategoryId == null &&
uiState.selectedGender != null &&
uiState.appliedFilters.isEmpty() &&
uiState.revertedFilters.isEmpty()
) {
GenderMenu(
gender = uiState.selectedGender!!,
onGenderClick = {
collapseAllMenusAndTips()
onGenderClick(it)
},
expanded = genderMenuExpanded,
tipVisible = genderMenuTipVisible,
onTipClick = {
genderMenuTipVisible = false
genderMenuExpanded = true
},
onExpandClick = { genderMenuExpanded = true },
onCollapseClick = { genderMenuExpanded = false },
modifier = Modifier.padding(dimensions.margin).align(Alignment.TopEnd),
)
}
if (uiState.state is PhotoEditUiState.State.Edit) {
val bottomPadding by
animateDpAsState(
when (uiState.selectedCategoryId) {
null -> categoriesSize.height
else -> filtersSize.height
},
)
CompareButton(
enabled =
uiState.appliedFilter is Success || uiState.appliedFilters.isNotEmpty(),
tipVisible = compareButtonTipVisible,
modifier =
Modifier.padding(dimensions.margin)
.padding(bottom = bottomPadding)
.align(Alignment.BottomEnd),
)
}
if (uiState.state is PhotoEditUiState.State.Process.Loading) {
ProcessLoadingLayout(state = uiState.state)
}
AnimatedBottomContent(
visible =
uiState.isUserPro == false && uiState.state is PhotoEditUiState.State.Process,
) {
ProcessProCard(
isFocused = uiState.state is PhotoEditUiState.State.Process.Loading,
onProClick = onProcessProClick,
)
}
if (uiState.state is PhotoEditUiState.State.Process.Error) {
ProcessErrorLayout(
state = uiState.state,
onProcessTryAgainClick = onProcessTryAgainClick,
onProcessChoosePhotoClick = onProcessChoosePhotoClick,
onBackClick = onBackAttempt,
onExcessiveUseDialogConfirm = onExcessiveUseDialogConfirm,
onExcessiveUseDialogDismiss = onExcessiveUseDialogDismiss,
)
}
AnimatedBottomContent(
visible = uiState.state is PhotoEditUiState.State.FaceSelection,
) {
FaceSelectionApplyCard(onContinueClick = onFaceSelectConfirm)
}
AnimatedBottomContent(
visible =
uiState.state is PhotoEditUiState.State.Edit &&
uiState.selectedCategoryId == null,
modifier = Modifier.onDpSizeChanged { categoriesSize = it },
) {
if (uiState.categories != null && uiState.selectedFace != null) {
CategoriesLayout(
categories = uiState.categories,
gender = uiState.selectedGender!!,
onCategoryClick = {
collapseAllMenusAndTips()
onCategoryClick(it)
},
)
}
}
AnimatedBottomContent(
visible =
uiState.state is PhotoEditUiState.State.Edit &&
uiState.selectedCategoryId != null,
modifier = Modifier.onDpSizeChanged { filtersSize = it },
) {
if (uiState.selectedCategoryId != null && uiState.selectedFace != null) {
FiltersLayout(
filters = uiState.selectedCategory!!.filters,
selectedFilterId = uiState.selectedFilterId,
gender = uiState.selectedGender!!,
onFilterSelect = {
collapseAllMenusAndTips()
onFilterSelect(it)
},
onFilterDeselect = {
collapseAllMenusAndTips()
onFilterDeselect()
},
onApplyClick = {
collapseAllMenusAndTips()
onFilterApplyClick()
},
onCancelClick = {
collapseAllMenusAndTips()
onFilterCancel()
},
showSaveInsteadOfApply = uiState.selectedCategoryId == "cartoon",
onSaveClick = {
collapseAllMenusAndTips()
onSave()
},
)
}
}
if (uiState.appliedFilter is Loading) {
var progress by remember { mutableStateOf(0f) }
ProgressLayout(
text = stringResource(id = R.string.photo_edit_apply_filter_progress_text),
progress = progress,
cancelable = uiState.filterApplyCancelable,
onCancel = {
analytics.trackCommonEventWith2(
"editProcessingCancel",
"filterId" to uiState.selectedFilterId,
)
onFilterSelectCancel()
},
)
LaunchedEffect(Unit) { progress = 0.95f }
} else if (uiState.appliedFilter is Fail) {
FilterSelectErrorLayout(
isUserPro = uiState.isUserPro == true,
error = uiState.appliedFilter.error,
onTryAgainClick = { onFilterSelect(uiState.selectedFilter!!) },
onBackClick = onFilterSelectCancel,
onExcessiveUseDialogConfirm = onExcessiveUseDialogConfirm,
onExcessiveUseDialogDismiss = onExcessiveUseDialogDismiss,
)
}
if (exitDialogVisible) {
ExitDialog(onConfirm = onBack, onDismiss = { exitDialogVisible = false })
}
if (saveDialogVisible) {
SaveDialog(
onSave = {
onFaceSelection(true)
saveDialogVisible = false
},
onDiscard = {
onFaceSelection(false)
saveDialogVisible = false
},
onDismiss = { saveDialogVisible = false },
)
}
if (uiState.processAgreementVisible) {
ProcessAgreementDialog(
onDismiss = onProcessAgreementDismiss,
onConfirm = onProcessAgreementConfirm,
)
}
}
}
}
@Composable
private fun ProcessAgreementDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) {
val sessionTracker = LocalSessionTracker.current
val uriHandler = LocalUriHandler.current
FaceLabAlertDialog3(
onDismissRequest = {},
buttons = {
Column {
FaceLabButton3(onClick = onConfirm, modifier = Modifier.fillMaxWidth()) {
Text(
text = stringResource(id = R.string.cloud_processing_dialog_confirm_button),
)
}
Spacer(modifier = Modifier.height(8.dp))
FaceLabTextButton3(onClick = onDismiss, modifier = Modifier.fillMaxWidth()) {
Text(
text = stringResource(id = R.string.cloud_processing_dialog_dismiss_button),
)
}
}
},
title = {
Text(
text = stringResource(id = R.string.cloud_processing_dialog_title),
modifier = Modifier.fillMaxWidth(),
)
},
text = {
val text = annotatedStringResource(id = R.string.cloud_processing_dialog_text)
ClickableText(
text = text,
style = LocalTextStyle.current,
modifier = Modifier.fillMaxWidth(),
) { offset ->
text.getStringAnnotations("URL", offset, offset).firstOrNull()?.let {
sessionTracker.allowShortBreakForAnotherApp()
tryCatching { uriHandler.openUri(it.item) }
}
}
},
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PhotoEditTopAppBar2(
onBackClick: () -> Unit,
backEnabled: Boolean,
undoVisible: Boolean,
undoEnabled: Boolean,
onUndoClick: () -> Unit,
redoVisible: Boolean,
redoEnabled: Boolean,
onRedoClick: () -> Unit,
saveEnabled: Boolean,
onSaveClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val analytics = LocalAnalytics.current
FaceLabTopAppBar3(
title = {
Row {
if (undoVisible) {
IconButton(
onClick = {
analytics.trackCommonEventWith2("editUndoRedoClick", "type" to "undo")
onUndoClick()
},
enabled = undoEnabled,
) {
Icon(
painter = painterResource(id = R.drawable.arrow_undo),
contentDescription = null,
)
}
}
if (redoVisible) {
IconButton(
onClick = {
analytics.trackCommonEventWith2("editUndoRedoClick", "type" to "redo")
onRedoClick()
},
enabled = redoEnabled,
) {
Icon(
painter = painterResource(id = R.drawable.arrow_redo),
contentDescription = null,
)
}
}
}
},
navigationIcon = {
Box {
IconButton(onClick = onBackClick, enabled = backEnabled) {
Icon(
painter = painterResource(id = R.drawable.arrow_back),
contentDescription = null,
)
}
if (backEnabled.not()) {
Box(
modifier =
Modifier.clickable(
onClick = {
analytics.trackCommonEventWith2("editDisabledBack")
},
interactionSource = remember { MutableInteractionSource() },
indication = null,
)
.minimumTouchTargetSize(),
)
}
}
},
actions = {
FaceLabSmallButton3(
onClick = onSaveClick,
enabled = saveEnabled,
modifier = Modifier.padding(horizontal = 8.dp),
) {
Text(text = stringResource(id = R.string.photo_edit_save_button))
}
},
modifier = modifier,
)
}
@Composable
private fun ImageLayout(
state: PhotoEditUiState.State,
baseImage: Any,
editedImage: String?,
imageSize: IntSize,
faces: List<FaceDetail>?,
selectedFaceId: String?,
onFaceSelect: (FaceDetail) -> Unit,
onFaceConfirm: (FaceDetail) -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val baseImagePainter = rememberFaceLabImagePainter(baseImage)
var initialImageMatrix by remember { mutableStateOf(Matrix()) }
var imageMatrix by remember { mutableStateOf(Matrix()) }
var canvasSize by remember { mutableStateOf(IntSize.Zero) }
val editedImagePainter = rememberFaceLabImagePainter(editedImage)
var showBaseImage by remember { mutableStateOf(false) }
val editedImageAlpha by
animateFloatAsState(if (editedImage == null || showBaseImage) 0f else 1f)
LaunchedEffect(canvasSize, imageSize) {
val scaleFactor =
ContentScale.Fit.computeScaleFactor(
srcSize = imageSize.toSize(),
dstSize = canvasSize.toSize(),
)
val alignedOffset =
Alignment.Center.align(
size = (imageSize.toSize() * scaleFactor).round(),
space = canvasSize,
layoutDirection = LayoutDirection.Ltr,
)
initialImageMatrix =
Matrix().apply {
setScale(scaleFactor.scaleX, scaleFactor.scaleY)
postTranslate(alignedOffset.x.toFloat(), alignedOffset.y.toFloat())
}
imageMatrix = initialImageMatrix
}
val zoomModifier =
Modifier.pointerInput(Unit) {
detectPointerTransformGestures(
onGesture = { centroid, pan, zoom, _, mainPointer, _ ->
imageMatrix =
Matrix(imageMatrix).apply {
postScale(zoom, zoom, centroid.x, centroid.y)
val minScale = initialImageMatrix.values()[Matrix.MSCALE_X]
val maxScale = minScale * 5f
val scale = values()[Matrix.MSCALE_X]
if (scale < minScale) {
postScale(
minScale / scale,
minScale / scale,
centroid.x,
centroid.y,
)
} else if (scale > maxScale) {
postScale(
maxScale / scale,
maxScale / scale,
centroid.x,
centroid.y,
)
}
postTranslate(pan.x, pan.y)
val canvasRect = canvasSize.toIntRect()
val imageRect = mapRect(imageSize.toIntRect().toRect())
var dx = 0f
var dy = 0f
if (canvasRect.width < imageRect.width) {
if (imageRect.left > canvasRect.left) {
dx = canvasRect.left - imageRect.left
}
if (imageRect.right < canvasRect.right) {
dx = canvasRect.right - imageRect.right
}
} else {
dx = (canvasRect.width - imageRect.width) / 2 - imageRect.left
}
if (canvasRect.height < imageRect.height) {
if (imageRect.top > canvasRect.top) {
dy = canvasRect.top - imageRect.top
}
if (imageRect.bottom < canvasRect.bottom) {
dy = canvasRect.bottom - imageRect.bottom
}
} else {
dy = (canvasRect.height - imageRect.height) / 2 - imageRect.top
}
postTranslate(dx, dy)
}
mainPointer.consume()
},
)
}
val faceSelectionModifier =
Modifier.pointerInput(imageSize, faces, selectedFaceId, onFaceSelect, imageMatrix) {
detectTapGestures(
onDoubleTap = { offset ->
val realOffset = imageMatrix.inverted().mapOffset(offset)
val clickedFace =
faces?.findLast {
it.boundingBox.calculateRect(imageSize).contains(realOffset)
}
if (clickedFace != null) {
onFaceConfirm(clickedFace)
}
},
onTap = { offset ->
val realOffset = imageMatrix.inverted().mapOffset(offset)
val clickedFace =
faces?.findLast {
it.boundingBox.calculateRect(imageSize).contains(realOffset)
}
if (clickedFace != null && clickedFace.faceId != selectedFaceId) {
onFaceSelect(clickedFace)
}
},
)
}
val compareModifier =
Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = {
showBaseImage = true
onClick()
tryAwaitRelease()
showBaseImage = false
},
)
}
Box(modifier = modifier) {
Canvas(
Modifier.onSizeChanged { canvasSize = it }
.matchParentSize()
.clipToBounds()
.then(zoomModifier)
.then(
when (state) {
is PhotoEditUiState.State.FaceSelection -> faceSelectionModifier
is PhotoEditUiState.State.Edit -> compareModifier
else -> Modifier
},
),
) {
drawIntoNativeCanvas {
concat(imageMatrix)
with(baseImagePainter) { draw(imageSize.toSize()) }
with(editedImagePainter) { draw(imageSize.toSize(), alpha = editedImageAlpha) }
if (state is PhotoEditUiState.State.FaceSelection) {
faces?.forEach { face ->
val faceRect = face.boundingBox.calculateRect(imageSize)
val selected = face.faceId == selectedFaceId
val strokeWidth =
imageMatrix
.inverted()
.mapRadius(if (selected) 4.dp.toPx() else 2.dp.toPx())
val colorAlpha = if (selected) 1f else 0.5f
drawRect(
color = Color.White.copy(alpha = colorAlpha),
topLeft = Offset(x = faceRect.left, y = faceRect.top),
size = Size(width = faceRect.width, height = faceRect.height),
style = Stroke(width = strokeWidth),
)
drawRect(
color = Color.Black.copy(alpha = colorAlpha),
topLeft =
Offset(
x = faceRect.left - strokeWidth / 2,
y = faceRect.top - strokeWidth / 2,
),
size =
Size(
width = faceRect.width + strokeWidth,
height = faceRect.height + strokeWidth,
),
style = Stroke(width = strokeWidth / 4),
)
}
}
}
}
}
}
@Composable
private fun ProcessLoadingLayout(state: PhotoEditUiState.State.Process.Loading) {
val text: String =
when (state) {
PhotoEditUiState.State.Process.Loading.Preparing ->
stringResource(id = R.string.photo_edit_process_preparing)
PhotoEditUiState.State.Process.Loading.Analyzing ->
stringResource(id = R.string.photo_edit_process_analyzing)
PhotoEditUiState.State.Process.Loading.AlmostReady ->
stringResource(id = R.string.photo_edit_process_almost_ready)
}
val progress: Float =
when (state) {
PhotoEditUiState.State.Process.Loading.Preparing -> 0f
PhotoEditUiState.State.Process.Loading.Analyzing -> 0.9f
PhotoEditUiState.State.Process.Loading.AlmostReady -> 1f
}
ProgressLayout(
text = text,
progress = progress,
)
}
@Composable
private fun ProgressLayout(
text: String,
progress: Float,
cancelable: Boolean = false,
onCancel: (() -> Unit)? = null,
) {
val dimensions = rememberDimensions()
ScrimSurface(onClick = {}) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier.fillMaxSize()
.padding(bottom = dimensions.margin * 2)
.navigationBarsPadding(),
) {
Text(text = text, color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.W400)
Spacer(modifier = Modifier.height(dimensions.margin / 2))
val animatedProgress by
animateFloatAsState(
targetValue = progress,
animationSpec =
tween(
durationMillis = if (progress == 1f) 1000 else 5000,
easing = LinearEasing,
),
)
FaceLabLinearProgressIndicator(
progress = animatedProgress,
brush =
Brush.horizontalGradient(
listOf(Color.White, Color.White),
),
backgroundColor = Color.White.copy(alpha = 0.5f),
)
if (cancelable) {
Spacer(modifier = Modifier.height(dimensions.margin))
FaceLabButton3(
onClick = { onCancel?.invoke() },
colors =
FaceLabButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.Black,
),
) {
Text(stringResource(R.string.photo_edit_apply_filter_progress_cancel_button))
}
}
}
}
}
@Composable
private fun ScrimSurface(
modifier: Modifier = Modifier,
onClick: () -> Unit,
content: @Composable () -> Unit
) {
Surface(
modifier =
modifier.clickable(
onClick = onClick,
interactionSource = remember { MutableInteractionSource() },
indication = null,
),
color = Color.Black.copy(alpha = 0.4f),
content = content,
)
}
@Composable
private fun ProcessProCard(
isFocused: Boolean,
onProClick: () -> Unit,
modifier: Modifier = Modifier
) {
val dimensions = rememberDimensions()
FaceLabCard3(
shape = RoundedCornerShape(topStart = 40.dp, topEnd = 40.dp),
elevation = CardDefaults.cardElevation(if (isFocused) 3.dp else 0.dp),
modifier = modifier,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier.fillMaxWidth().padding(dimensions.margin * 2).navigationBarsPadding(),
) {
Image(
painter = painterResource(id = R.drawable.ic_pro),
contentDescription = null,
modifier =
Modifier.tint(Brush.horizontalGradient(FaceLabTheme3.colorScheme.gradient1)),
)
Text(
text = stringResource(id = R.string.photo_edit_process_pro_card_title),
style = FaceLabTheme3.typography.subheadL,
)
Spacer(modifier = Modifier.height(dimensions.margin / 2))
Text(
text = stringResource(id = R.string.photo_edit_process_pro_card_description),
style = FaceLabTheme3.typography.bodyM,
)
Spacer(modifier = Modifier.height(dimensions.margin * 2))
FaceLabLargeGradientButton3(onClick = onProClick) {
Text(text = stringResource(id = R.string.photo_edit_process_pro_card_button))
}
}
}
}
@Composable
private fun ProcessErrorLayout(
state: PhotoEditUiState.State.Process.Error,
onProcessTryAgainClick: () -> Unit,
onProcessChoosePhotoClick: () -> Unit,
onBackClick: () -> Unit,
onExcessiveUseDialogConfirm: () -> Unit,
onExcessiveUseDialogDismiss: () -> Unit,
) {
val analytics = LocalAnalytics.current
when (state) {
PhotoEditUiState.State.Process.Error.ConnectionError -> {
val data =
EditDialogData(
image = painterResource(id = R.drawable.no_internet),
title =
stringResource(id = R.string.photo_edit_process_error_no_internet_title),
text =
stringResource(
id = R.string.photo_edit_process_error_no_internet_description,
),
positiveButton =
stringResource(id = R.string.photo_edit_process_error_button_try_again),
onPositiveButtonClick = {
analytics.trackCommon1Event("faceProcessErrorClick", "btn" to "tryAgain")
onProcessTryAgainClick()
},
negativeButton =
stringResource(id = R.string.photo_edit_process_error_button_back),
onNegativeButtonClick = {
analytics.trackCommon1Event("faceProcessErrorClick", "btn" to "goBack")
onBackClick()
},
)
EditDialog(data = data, onDismissRequest = {})
}
PhotoEditUiState.State.Process.Error.NoFaceFoundError -> {
val data =
EditDialogData(
image = painterResource(id = R.drawable.no_face),
title = stringResource(id = R.string.photo_edit_process_error_no_face_title),
text =
stringResource(id = R.string.photo_edit_process_error_no_face_description),
positiveButton =
stringResource(
id = R.string.photo_edit_process_error_no_face_button_choose_photo,
),
onPositiveButtonClick = {
analytics.trackCommon1Event("faceProcessErrorClick", "btn" to "choosePhoto")
onProcessChoosePhotoClick()
},
)
EditDialog(data = data, onDismissRequest = {})
}
is PhotoEditUiState.State.Process.Error.ExcessiveUseError -> {
if (state.upgradable) {
EditExcessiveUseUpgradeDialog(
returnHome = true,
onConfirm = onExcessiveUseDialogConfirm,
onDismiss = onExcessiveUseDialogDismiss,
)
} else {
EditExcessiveUseDialog(returnHome = true, onDismiss = onExcessiveUseDialogDismiss)
}
}
PhotoEditUiState.State.Process.Error.UnknownError -> {
val data =
EditDialogData(
image = painterResource(id = R.drawable.error2),
title = stringResource(id = R.string.photo_edit_process_error_unknown_title),
text =
stringResource(id = R.string.photo_edit_process_error_unknown_description),
positiveButton =
stringResource(id = R.string.photo_edit_process_error_button_try_again),
onPositiveButtonClick = {
analytics.trackCommon1Event("faceProcessErrorClick", "btn" to "tryAgain")
onProcessTryAgainClick()
},
negativeButton =
stringResource(id = R.string.photo_edit_process_error_button_back),
onNegativeButtonClick = {
analytics.trackCommon1Event("faceProcessErrorClick", "btn" to "goBack")
onBackClick()
},
)
EditDialog(data = data, onDismissRequest = {})
}
}
}
@Composable
private fun FaceSelectionApplyCard(onContinueClick: () -> Unit, modifier: Modifier = Modifier) {
val dimensions = rememberDimensions()
val analytics = LocalAnalytics.current
FaceLabCard3(
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp),
modifier = modifier,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier.fillMaxWidth().padding(dimensions.margin * 1.5f).navigationBarsPadding(),
) {
Text(
text = stringResource(id = R.string.photo_edit_face_selection_description),
color = LocalContentColor.current.copy(alpha = 0.7f),
style = FaceLabTheme3.typography.bodyM,
)
Spacer(modifier = Modifier.height(dimensions.margin * 1.5f))
FaceLabButton3(
onClick = {
analytics.trackCommonEventWith2("editChangeFaceContinue")
onContinueClick()
},
modifier = Modifier.fillMaxWidth().padding(horizontal = dimensions.margin * 1.5f),
) {
Text(text = stringResource(id = R.string.photo_edit_face_selection_button))
}
}
}
}
@Composable
private fun ExitDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
val data =
EditDialogData(
title = stringResource(id = R.string.photo_edit_exit_dialog_title),
text = stringResource(id = R.string.photo_edit_exit_dialog_text),
positiveButton = stringResource(id = R.string.photo_edit_exit_dialog_confirm_button),
onPositiveButtonClick = onConfirm,
negativeButton = stringResource(id = R.string.photo_edit_exit_dialog_dismiss_button),
onNegativeButtonClick = onDismiss,
)
EditDialog(
data = data,
onDismissRequest = onDismiss,
alignment = Alignment.Bottom,
)
}
@Composable
private fun SaveDialog(
onSave: () -> Unit,
onDiscard: () -> Unit,
onDismiss: () -> Unit,
) {
val analytics = LocalAnalytics.current
val data =
EditDialogData(
title = stringResource(id = R.string.photo_edit_save_dialog_title),
text = stringResource(id = R.string.photo_edit_save_dialog_text),
positiveButton = stringResource(id = R.string.photo_edit_save_dialog_save_button),
onPositiveButtonClick = {
analytics.trackCommonEventWith2("editChangeFaceDialog", "btn" to "save")
onSave()
},
negativeButton = stringResource(id = R.string.photo_edit_save_dialog_discard_button),
onNegativeButtonClick = {
analytics.trackCommonEventWith2("editChangeFaceDialog", "btn" to "discard")
onDiscard()
},
)
EditDialog(
data = data,
onDismissRequest = onDismiss,
alignment = Alignment.Bottom,
)
}
@Composable
private fun CategoriesLayout(
categories: List<Category>,
gender: Gender,
onCategoryClick: (Category) -> Unit,
modifier: Modifier = Modifier
) {
val categoriesByGender =
remember(categories, gender) { categories.filter { it.gender.contains(gender) } }
LazyRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
contentPadding =
PaddingValues(horizontal = 8.dp) + WindowInsets.navigationBars.asPaddingValues(),
modifier = modifier,
) {
items(categoriesByGender) { category ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier =
Modifier.clip(RoundedCornerShape(12.dp))
.clickable(onClick = { onCategoryClick(category) })
.padding(top = 8.dp, bottom = 12.dp)
.widthIn(min = 88.dp),
) {
FaceLabImage(
data =
when (gender) {
Gender.Male -> category.maleIcon
Gender.Female -> category.femaleIcon
},
contentDescription = null,
colorFilter = ColorFilter.tint(LocalContentColor.current),
modifier = Modifier.size(52.dp),
)
Text(
text = category.title.value().orEmpty(),
style = FaceLabTheme3.typography.labelSM,
)
}
}
}
}
@Composable
private fun FiltersLayout(
filters: List<Filter>,
selectedFilterId: String?,
gender: Gender,
onFilterSelect: (Filter) -> Unit,
onFilterDeselect: () -> Unit,
onApplyClick: () -> Unit,
onCancelClick: () -> Unit,
showSaveInsteadOfApply: Boolean,
onSaveClick: () -> Unit,
modifier: Modifier = Modifier
) {
val filtersByGender =
remember(filters, gender) { filters.filter { it.gender.contains(gender) } }
Box(modifier = modifier) {
FaceLabCard3(
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp),
modifier = Modifier.align(Alignment.BottomCenter),
) {
Box(modifier = Modifier.fillMaxWidth().navigationBarsPadding().height(93.dp))
}
Column(modifier = Modifier.navigationBarsPadding()) {
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
verticalAlignment = Alignment.Bottom,
) {
items(filtersByGender) { filter ->
val selected = filter.id == selectedFilterId
val imageSize by animateDpAsState(targetValue = if (selected) 78.dp else 72.dp)
Box(
modifier =
Modifier.padding(top = 78.dp - imageSize)
.clip(RoundedCornerShape(12.dp))
.clickable(
onClick = {
if (selected) onFilterDeselect() else onFilterSelect(filter)
},
)
.padding(4.dp),
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val borderModifier =
Modifier.border(
width = 2.dp,
color = FaceLabTheme3.colorScheme.primary,
shape = RoundedCornerShape(12.dp),
)
.border(
width = 3.dp,
color = FaceLabTheme3.colorScheme.onPrimary,
shape = RoundedCornerShape(12.dp),
)
FaceLabImage(
data =
when (gender) {
Gender.Male -> filter.maleIcon
Gender.Female -> filter.femaleIcon
},
contentDescription = null,
modifier =
Modifier.then(if (selected) borderModifier else Modifier)
.clip(RoundedCornerShape(12.dp))
.size(imageSize),
)
if (selected) {
Text(
text = filter.translations.value().orEmpty(),
color = FaceLabTheme3.colorScheme.primary,
style = FaceLabTheme3.typography.labelMM,
)
} else {
Text(
text = filter.translations.value().orEmpty(),
color = FaceLabTheme3.colorScheme.onSurface.copy(alpha = 0.6f),
style = FaceLabTheme3.typography.labelM,
)
}
}
if (filter.type == FilterType.Pro) {
Image(
painter = painterResource(id = R.drawable.pro_badge),
contentDescription = null,
modifier = Modifier.align(Alignment.TopEnd),
)
}
}
}
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
) {
FaceLabTextButton3(onClick = onCancelClick) {
Text(
text = stringResource(id = R.string.photo_edit_filter_cancel_button),
color = LocalContentColor.current.copy(alpha = 0.6f),
)
}
if (showSaveInsteadOfApply) {
FaceLabTextButton3(
onClick = onSaveClick,
enabled = selectedFilterId != null,
) {
Text(text = stringResource(id = R.string.photo_edit_filter_save_button))
}
} else {
FaceLabTextButton3(
onClick = onApplyClick,
enabled = selectedFilterId != null,
) {
Text(text = stringResource(id = R.string.photo_edit_filter_apply_button))
}
}
}
}
}
}
@Composable
private fun FilterSelectErrorLayout(
isUserPro: Boolean,
error: Throwable,
onTryAgainClick: () -> Unit,
onBackClick: () -> Unit,
onExcessiveUseDialogConfirm: () -> Unit,
onExcessiveUseDialogDismiss: () -> Unit,
) {
when (error) {
is InternetNotConnectedException -> {
val data =
EditDialogData(
image = painterResource(id = R.drawable.no_internet),
title =
stringResource(id = R.string.photo_edit_process_error_no_internet_title),
text =
stringResource(
id = R.string.photo_edit_process_error_no_internet_description,
),
positiveButton =
stringResource(id = R.string.photo_edit_process_error_button_try_again),
onPositiveButtonClick = onTryAgainClick,
negativeButton =
stringResource(id = R.string.photo_edit_process_error_button_back),
onNegativeButtonClick = onBackClick,
)
EditDialog(data = data, onDismissRequest = {})
}
is ExcessiveUseException -> {
if (isUserPro) {
EditExcessiveUseDialog(returnHome = false, onDismiss = onExcessiveUseDialogDismiss)
} else {
EditExcessiveUseUpgradeDialog(
returnHome = false,
onConfirm = onExcessiveUseDialogConfirm,
onDismiss = onExcessiveUseDialogDismiss,
)
}
}
else -> {
val data =
EditDialogData(
image = painterResource(id = R.drawable.error2),
title = stringResource(id = R.string.photo_edit_process_error_unknown_title),
text =
stringResource(id = R.string.photo_edit_process_error_unknown_description),
positiveButton =
stringResource(id = R.string.photo_edit_process_error_button_try_again),
onPositiveButtonClick = onTryAgainClick,
negativeButton =
stringResource(id = R.string.photo_edit_process_error_button_back),
onNegativeButtonClick = onBackClick,
)
EditDialog(data = data, onDismissRequest = {})
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun GenderMenu(
gender: Gender,
onGenderClick: (Gender) -> Unit,
expanded: Boolean,
tipVisible: Boolean,
onTipClick: () -> Unit,
onExpandClick: () -> Unit,
onCollapseClick: () -> Unit,
modifier: Modifier = Modifier
) {
val analytics = LocalAnalytics.current
Box(contentAlignment = Alignment.TopEnd, modifier = modifier.animateContentSize()) {
if (expanded) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier.width(IntrinsicSize.Max)
.clip(RoundedCornerShape(18.dp))
.background(FaceLabTheme3.colorScheme.primary.copy(alpha = 0.7f)),
) {
Surface(
onClick = onCollapseClick,
color = FaceLabTheme3.colorScheme.primary,
contentColor = FaceLabTheme3.colorScheme.onPrimary,
modifier = Modifier.height(31.dp).fillMaxWidth(),
) {
Icon(
painter = painterResource(id = R.drawable.close),
contentDescription = null,
)
}
Spacer(modifier = Modifier.height(4.dp))
GenderMenuItem(
Gender.Female,
selected = gender == Gender.Female,
onGenderClick = {
analytics.trackCommonEventWith2("editGenderChanged", "btn" to "female")
onGenderClick(it)
},
modifier = Modifier.padding(horizontal = 4.dp).fillMaxWidth(),
)
Spacer(modifier = Modifier.height(4.dp))
GenderMenuItem(
Gender.Male,
selected = gender == Gender.Male,
onGenderClick = {
analytics.trackCommonEventWith2("editGenderChanged", "btn" to "male")
onGenderClick(it)
},
modifier = Modifier.padding(horizontal = 4.dp).fillMaxWidth(),
)
Spacer(modifier = Modifier.height(4.dp))
}
} else if (tipVisible) {
GenderMenuTip(
onClick = {
analytics.trackCommonEventWith2("editChangeGenderClick")
analytics.trackUxCamEvent("editChangeGenderClick")
onTipClick()
},
)
} else {
CircleIconButton(
painter = painterResource(id = R.drawable.gender_icon),
onClick = {
analytics.trackCommonEventWith2("editChangeGenderClick")
onExpandClick()
},
)
}
}
}
@Composable
private fun GenderMenuItem(
gender: Gender,
selected: Boolean,
onGenderClick: (Gender) -> Unit,
modifier: Modifier = Modifier
) {
FaceLabCard3(
onClick = { onGenderClick(gender) },
enabled = selected.not(),
shape = RoundedCornerShape(12.dp),
colors =
CardDefaults.cardColors(
containerColor = Color.Transparent,
contentColor =
FaceLabTheme3.colorScheme.onPrimary.copy(alpha = if (selected) 1f else 0.6f),
disabledContainerColor = FaceLabTheme3.colorScheme.primary.copy(alpha = 0.7f),
disabledContentColor =
FaceLabTheme3.colorScheme.onPrimary.copy(alpha = if (selected) 1f else 0.6f),
),
modifier = modifier,
) {
val drawableId =
when (gender) {
Gender.Male -> R.drawable.category_male
Gender.Female -> R.drawable.category_female
}
val textId =
when (gender) {
Gender.Male -> R.string.photo_edit_gender_male
Gender.Female -> R.string.photo_edit_gender_female
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(4.dp),
) {
Icon(painter = painterResource(id = drawableId), contentDescription = null)
Text(
text = stringResource(id = textId),
style =
if (selected) {
FaceLabTheme3.typography.labelMM
} else {
FaceLabTheme3.typography.labelM
},
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun GenderMenuTip(onClick: () -> Unit, modifier: Modifier = Modifier) {
Surface(
onClick = onClick,
shape = RoundedCornerShape(18.dp),
color = FaceLabTheme3.colorScheme.primary.copy(alpha = 0.7f),
contentColor = FaceLabTheme3.colorScheme.onPrimary,
modifier = modifier,
) {
Row {
Text(
text = stringResource(id = R.string.photo_edit_gender_menu_tip),
textAlign = TextAlign.Start,
style = FaceLabTheme3.typography.bodyS,
modifier = Modifier.widthIn(max = 200.dp).padding(8.dp),
)
Box(
modifier =
Modifier.clip(CircleShape)
.background(FaceLabTheme3.colorScheme.primary)
.size(36.dp)
.align(Alignment.Top),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.gender_icon),
contentDescription = null,
tint = FaceLabTheme3.colorScheme.onPrimary,
)
}
}
}
}
@Composable
private fun CompareButton(enabled: Boolean, tipVisible: Boolean, modifier: Modifier = Modifier) {
Box(modifier = modifier.animateContentSize()) {
if (tipVisible) {
CompareButtonTip()
} else {
Box(
modifier =
Modifier.alpha(
if (enabled) 1f else FaceLabTheme3.disabledAlpha,
)
.background(
color = FaceLabTheme3.colorScheme.surface,
shape = CircleShape,
)
.size(36.dp),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.bxa),
contentDescription = null,
tint = FaceLabTheme3.colorScheme.onSurface,
)
}
}
}
}
@Composable
private fun CompareButtonTip(modifier: Modifier = Modifier) {
Row(
modifier =
modifier
.clip(RoundedCornerShape(18.dp))
.background(FaceLabTheme3.colorScheme.primary.copy(alpha = 0.7f)),
) {
Text(
text = stringResource(id = R.string.photo_edit_compare_button_tip),
color = FaceLabTheme3.colorScheme.onPrimary,
textAlign = TextAlign.Start,
style = FaceLabTheme3.typography.bodyS,
modifier = Modifier.widthIn(max = 210.dp).padding(8.dp),
)
Box(
modifier =
Modifier.clip(CircleShape)
.background(FaceLabTheme3.colorScheme.primary)
.size(36.dp)
.align(Alignment.Bottom),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.bxa),
contentDescription = null,
tint = FaceLabTheme3.colorScheme.onPrimary,
)
}
}
}
@Composable
private fun EditExcessiveUseUpgradeDialog(
returnHome: Boolean,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
FaceLabAlertDialog3(
onDismissRequest = {},
image = {
Image(
painter = painterResource(id = R.drawable.excessive_use_dialog),
contentDescription = null,
modifier =
Modifier.clip(RoundedCornerShape(20.dp)).aspectRatio(724 / 400f).fillMaxWidth(),
)
},
title = { Text(text = stringResource(id = R.string.excessive_use_upgrade_dialog_title)) },
text = { Text(text = stringResource(id = R.string.excessive_use_upgrade_dialog_text)) },
buttons = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
FaceLabTextButton3(onClick = onDismiss, modifier = Modifier.weight(1f)) {
Text(
stringResource(
when {
returnHome -> R.string.excessive_use_dialog_return_home_button
else -> R.string.excessive_use_dialog_cancel_button
},
),
)
}
FaceLabGradientButton3(onClick = onConfirm, modifier = Modifier.weight(1f)) {
Text(
stringResource(id = R.string.excessive_use_dialog_upgrade_button),
)
}
}
},
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
)
}
@Composable
private fun EditExcessiveUseDialog(returnHome: Boolean, onDismiss: () -> Unit) {
FaceLabAlertDialog3(
onDismissRequest = {},
image = {
Icon(painter = painterResource(id = R.drawable.error2), contentDescription = null)
},
title = { Text(text = stringResource(id = R.string.excessive_use_dialog_title)) },
text = { Text(text = stringResource(id = R.string.excessive_use_dialog_text)) },
buttons = {
FaceLabButton3(onClick = onDismiss, modifier = Modifier.fillMaxWidth()) {
Text(
stringResource(
when {
returnHome -> R.string.excessive_use_dialog_return_home_button
else -> R.string.excessive_use_dialog_cancel_button
},
),
)
}
},
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
)
}
@Composable
private fun EditDialog(
data: EditDialogData,
onDismissRequest: () -> Unit,
alignment: Alignment.Vertical = Alignment.CenterVertically,
) {
FaceLabAlertDialog3(
onDismissRequest = onDismissRequest,
buttons = {
Column {
if (data.positiveButton != null) {
FaceLabButton3(
onClick = { data.onPositiveButtonClick?.invoke() },
modifier = Modifier.fillMaxWidth(),
) {
Text(text = data.positiveButton)
}
}
if (data.negativeButton != null) {
Spacer(modifier = Modifier.height(8.dp))
FaceLabTextButton3(
onClick = { data.onNegativeButtonClick?.invoke() },
modifier = Modifier.fillMaxWidth(),
) {
Text(text = data.negativeButton)
}
}
}
},
image = data.image?.let { { Icon(painter = data.image, contentDescription = null) } },
title = data.title?.let { { Text(text = data.title) } },
text = data.text?.let { { Text(text = data.text) } },
alignment = alignment,
)
}
private class EditDialogData(
val image: Painter? = null,
val title: String? = null,
val text: String? = null,
val positiveButton: String? = null,
val onPositiveButtonClick: (() -> Unit)? = null,
val negativeButton: String? = null,
val onNegativeButtonClick: (() -> Unit)? = null,
)
@Composable
private fun CircleIconButton(
painter: Painter,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
selected: Boolean = false,
) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
IconButton(
onClick = onClick,
enabled = enabled,
modifier =
modifier
.alpha(if (selected) 0.6f else if (enabled) 1f else FaceLabTheme3.disabledAlpha)
.background(
color =
if (selected) {
FaceLabTheme3.colorScheme.primary
} else {
FaceLabTheme3.colorScheme.surface
},
shape = CircleShape,
)
.size(36.dp),
) {
Icon(
painter = painter,
contentDescription = null,
tint =
if (selected) {
FaceLabTheme3.colorScheme.onPrimary
} else {
FaceLabTheme3.colorScheme.onSurface
},
)
}
}
}
@Composable
fun BoxScope.AnimatedBottomContent(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + slideInVertically(initialOffsetY = { it }),
exit: ExitTransition = fadeOut() + slideOutVertically(targetOffsetY = { it }),
content: @Composable AnimatedVisibilityScope.() -> Unit
) {
AnimatedVisibility(
visible = visible,
modifier = modifier.align(Alignment.BottomCenter),
enter = enter,
exit = exit,
content = content,
)
}
private fun BoundingBox.calculateRect(imageSize: IntSize) =
Rect(
left = left * imageSize.width,
top = top * imageSize.height,
right = (left + width) * imageSize.width,
bottom = (top + height) * imageSize.height,
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment