Created
February 28, 2023 08:24
-
-
Save necatisozer/634002c1ad32e1bac84ff344d032d138 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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