package com.suno.android.ui.screens.home.profile.edit import android.Manifest import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia import androidx.activity.result.contract.ActivityResultContracts.TakePicture import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.suno.android.common_core_utils.model.Url import com.suno.android.common_core_utils.model.UserHandle import com.suno.android.common_ui.components.bottom_sheet.AvatarImageBottomSheet import com.suno.android.common_ui.components.bottom_sheet.TextEditingBottomSheet import com.suno.android.common_ui.components.buttons.EditProfileOption import com.suno.android.common_ui.components.images.ProfileAvatarImage import com.suno.android.common_ui.components.omni.CircleIconButton import com.suno.android.common_ui.components.preview.SunoPreview import com.suno.android.common_ui.theme.ExtendedTheme import com.suno.android.preview.FakeUser import com.suno.android.utils.mvi.MviEffectHandler import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import com.suno.android.common_res.R as CommonResR import com.suno.android.common_ui.R as CommonUiR @Composable fun EditProfileScreen( vm: EditProfileScreenVM = hiltViewModel(), onBackPressed: () -> Unit, ) { val viewState by vm.state.collectAsStateWithLifecycle() EditProfileScreen( viewState = viewState, sendEvent = vm::sendEvent, effects = vm.effects, onBackPressed = onBackPressed, ) } @Composable fun EditProfileScreen( viewState: EditProfileScreenState, sendEvent: (EditProfileScreenEvent) -> Unit, effects: SharedFlow, onBackPressed: () -> Unit, ) { val avatarImageSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) val changeNameSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) val changeUserHandleSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) var showAvatarImageBottomSheet by remember { mutableStateOf(false) } var showChangeNameBottomSheet by remember { mutableStateOf(false) } var showChangeUserHandleBottomSheet by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() val cameraPermission = rememberPermissionState(Manifest.permission.CAMERA) { granted -> sendEvent( EditProfileScreenEvent.OnCameraPermissionResult(isGranted = granted), ) } val takePicture = rememberLauncherForActivityResult( contract = TakePicture(), ) { success -> sendEvent(EditProfileScreenEvent.OnCameraResult(success)) } val pickMedia = rememberLauncherForActivityResult( contract = PickVisualMedia(), ) { uri -> sendEvent(EditProfileScreenEvent.OnImagePickerResult(uri)) } // Handle MVI effects MviEffectHandler(effects) { effect -> when (effect) { is EditProfileScreenEffect.LaunchCamera -> { takePicture.launch(effect.uri) } EditProfileScreenEffect.LaunchImagePicker -> { pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) } EditProfileScreenEffect.RequestTakePhoto -> { sendEvent(EditProfileScreenEvent.OnCameraRequested) } } } // We need this launched effect for the initial setting of the modal sheet state so it'll animate properly for // FIRST launch LaunchedEffect(showAvatarImageBottomSheet) { // Sheet is now in composition, animate it if (showAvatarImageBottomSheet) avatarImageSheetState.show() } // Bidirectional sync for sheet state so it'll animate LaunchedEffect(avatarImageSheetState.isVisible) { showAvatarImageBottomSheet = avatarImageSheetState.isVisible } LaunchedEffect(showChangeNameBottomSheet) { if (showChangeNameBottomSheet) { changeNameSheetState.show() } } LaunchedEffect(changeNameSheetState.isVisible) { showChangeNameBottomSheet = changeNameSheetState.isVisible } LaunchedEffect(showChangeUserHandleBottomSheet) { if (showChangeUserHandleBottomSheet) changeUserHandleSheetState.show() } LaunchedEffect(changeUserHandleSheetState.isVisible) { showChangeUserHandleBottomSheet = changeUserHandleSheetState.isVisible } Scaffold( topBar = { CenterAlignedTopAppBar( modifier = Modifier .background(ExtendedTheme.colors.backgroundPrimary) .padding(horizontal = 12.dp, vertical = 8.dp), colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = ExtendedTheme.colors.backgroundPrimary, ), title = { Text( style = ExtendedTheme.typography.largeBody, fontSize = 20.sp, fontWeight = FontWeight.W500, text = stringResource(CommonResR.string.edit_profile), ) }, navigationIcon = { CircleIconButton( vectorDrawableResId = CommonUiR.drawable.arrow_left, contentDescription = stringResource(CommonResR.string.back), onClick = onBackPressed, ) }, ) }, ) { innerPadding -> EditProfileScreen( avatarUrl = viewState.avatarImageUrl, displayName = viewState.displayName, userHandle = viewState.userHandle?.handle.orEmpty(), canEditProfilePhoto = viewState.isAddProfilePictureGateEnabled, onAvatarClick = { showAvatarImageBottomSheet = true }, onNameClick = { showChangeNameBottomSheet = true }, onUsernameClick = { showChangeUserHandleBottomSheet = true }, modifier = Modifier.padding(innerPadding), ) if (viewState.isPhotoUploading) { LinearProgressIndicator( color = ExtendedTheme.colors.sunoPink, trackColor = ExtendedTheme.colors.borderPrimary.copy(alpha = .1f), modifier = Modifier .padding(innerPadding) .fillMaxWidth(), ) } } if (showChangeUserHandleBottomSheet) { TextEditingBottomSheet( sheetState = changeUserHandleSheetState, headerName = stringResource(CommonResR.string.username_entry_navigation_title), confirmButtonName = stringResource(id = CommonResR.string.save), value = viewState.userHandle?.handle.orEmpty(), maxLines = 1, onConfirm = { newValue -> sendEvent(EditProfileScreenEvent.OnChangeUserHandle(UserHandle(newValue))) showChangeUserHandleBottomSheet = false scope.launch { changeUserHandleSheetState.hide() } }, onDismiss = { showChangeUserHandleBottomSheet = false }, ) } if (showChangeNameBottomSheet) { TextEditingBottomSheet( sheetState = changeNameSheetState, headerName = stringResource(CommonResR.string.name_entry_navigation_title), maxLines = 1, confirmButtonName = stringResource(id = CommonResR.string.save), value = viewState.displayName, onConfirm = { newValue -> sendEvent(EditProfileScreenEvent.OnChangeDisplayName(newValue)) showChangeNameBottomSheet = false scope.launch { changeNameSheetState.hide() } }, onDismiss = { showChangeNameBottomSheet = false }, ) } if (showAvatarImageBottomSheet) { AvatarImageBottomSheet( sheetState = avatarImageSheetState, headerName = stringResource(CommonResR.string.change_profile_picture), onDismissRequest = { scope.launch { avatarImageSheetState.hide() } }, scope = scope, onChooseFromGallery = { sendEvent(EditProfileScreenEvent.OnImagePickerRequested) scope.launch { avatarImageSheetState.hide() } }, onTakePhoto = { if (cameraPermission.status.isGranted) { sendEvent(EditProfileScreenEvent.OnCameraRequested) } else { cameraPermission.launchPermissionRequest() } scope.launch { avatarImageSheetState.hide() } }, ) } } @Composable internal fun EditProfileScreen( avatarUrl: Url?, displayName: String, userHandle: String, canEditProfilePhoto: Boolean, onAvatarClick: () -> Unit, onNameClick: () -> Unit, onUsernameClick: () -> Unit, modifier: Modifier = Modifier, ) { Column( modifier = modifier .padding(horizontal = 16.dp) .fillMaxSize() .background(ExtendedTheme.colors.backgroundPrimary), verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { if (canEditProfilePhoto) { ProfilePictureWithEdit( avatarUrl = avatarUrl, displayName = displayName, onClick = onAvatarClick, modifier = Modifier.padding(vertical = 16.dp), ) } else { Box( modifier = Modifier.padding(vertical = 16.dp), ) { ProfileAvatarImage( modifier = Modifier .size(100.dp), avatarUrl = avatarUrl, displayName = displayName, ) } } EditProfileOption( titleText = stringResource(CommonResR.string.name), valueText = displayName, onClick = onNameClick, ) EditProfileOption( titleText = stringResource(CommonResR.string.username), valueText = userHandle, onClick = onUsernameClick, ) } } @Composable internal fun ProfilePictureWithEdit( avatarUrl: Url?, displayName: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Box( modifier = modifier .clip(CircleShape) .clickable( onClickLabel = stringResource(CommonResR.string.edit_profile), onClick = onClick, ), contentAlignment = Alignment.Center, ) { ProfileAvatarImage( modifier = Modifier .size(100.dp), avatarUrl = avatarUrl, displayName = displayName, ) Spacer( modifier = Modifier .matchParentSize() .background(ExtendedTheme.colors.backgroundPrimary.copy(alpha = .2f)), ) Icon( imageVector = ImageVector.vectorResource(id = CommonUiR.drawable.pencil), contentDescription = null, tint = ExtendedTheme.colors.textPrimary, modifier = Modifier.size(32.dp), ) } } @PreviewLightDark @Composable private fun EditProfileContent_Preview() { SunoPreview { EditProfileScreen( viewState = EditProfileScreenState( user = FakeUser.Mikhail, isAddProfilePictureGateEnabled = true, ), sendEvent = {}, effects = MutableSharedFlow(), onBackPressed = {}, ) } } @PreviewLightDark @Composable private fun ProfilePictureWithEdit_Preview() { SunoPreview { Row { ProfilePictureWithEdit( avatarUrl = null, displayName = "", onClick = {}, ) ProfilePictureWithEdit( avatarUrl = Url(""), displayName = "", onClick = {}, ) } } }