diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt index e0bb694..7e2458e 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt @@ -43,7 +43,8 @@ class MainActivity : AppCompatActivity() { setContent { CompositionLocalProvider( - LocalLifecycleOwner provides lifecycleOwner + LocalLifecycleOwner provides lifecycleOwner, + LocalEdgeToEdge provides true ) { App( di = di diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/App.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/App.kt index 98c2028..a885168 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/App.kt @@ -17,6 +17,7 @@ import dev.icerock.moko.resources.compose.asFont import org.kodein.di.DI val LocalDarkMode = compositionLocalOf { error("No dark mode state provided") } +val LocalEdgeToEdge = staticCompositionLocalOf { false } val LocalDI = compositionLocalOf { error("No dependency injection provided") } val LocalHaze = compositionLocalOf { error("No Haze state provided") } val LocalPaddingValues = compositionLocalOf { null } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt index cde107f..27e0679 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt @@ -7,6 +7,9 @@ import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.composed @@ -168,4 +171,21 @@ fun LazyListState.isScrollingUp(): Boolean { } } }.value +} + +/** + * Checks if the modal is currently expanded or a swipe action is in progress to be expanded. + */ +@OptIn(ExperimentalMaterial3Api::class) +fun SheetState.isFullyExpandedOrTargeted(forceFullExpand: Boolean = false): Boolean { + val checkState = if (this.hasExpandedState) { + SheetValue.Expanded + } else { + if (forceFullExpand) { + return false + } + SheetValue.PartiallyExpanded + } + + return this.targetValue == checkState } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt index 02056be..4175c17 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/dialog/character/CharacterDialog.kt @@ -1,19 +1,16 @@ package dev.datlag.aniflow.ui.navigation.screen.medium.dialog.character -import androidx.compose.foundation.Image +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Bloodtype -import androidx.compose.material.icons.filled.Cake -import androidx.compose.material.icons.filled.Man4 -import androidx.compose.material.icons.filled.Translate +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -23,25 +20,44 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import coil3.compose.rememberAsyncImagePainter +import dev.datlag.aniflow.LocalEdgeToEdge import dev.datlag.aniflow.SharedRes import dev.datlag.aniflow.anilist.CharacterStateMachine import dev.datlag.aniflow.common.htmlToAnnotatedString +import dev.datlag.aniflow.common.isFullyExpandedOrTargeted import dev.datlag.aniflow.common.preferred import dev.datlag.aniflow.common.preferredName import dev.datlag.aniflow.ui.navigation.screen.medium.component.TranslateButton import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import dev.icerock.moko.resources.compose.stringResource -@OptIn(ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable fun CharacterDialog(component: CharacterComponent) { - AlertDialog( + val sheetState = rememberModalBottomSheetState() + val insets = if (LocalEdgeToEdge.current) { + BottomSheetDefaults.windowInsets.only(WindowInsetsSides.Bottom) + } else { + BottomSheetDefaults.windowInsets + } + + ModalBottomSheet( onDismissRequest = component::dismiss, - icon = { + windowInsets = insets, + sheetState = sheetState + ) { + val name by component.name.collectAsStateWithLifecycle() + + Box( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + contentAlignment = Alignment.Center + ) { val image by component.image.collectAsStateWithLifecycle() AsyncImage( - modifier = Modifier.size(80.dp).clip(CircleShape), + modifier = Modifier + .size(80.dp) + .clip(CircleShape), model = image.large, error = rememberAsyncImagePainter( model = image.medium, @@ -51,142 +67,135 @@ fun CharacterDialog(component: CharacterComponent) { alignment = Alignment.Center, contentDescription = component.initialChar.preferredName() ) - }, - title = { - val name by component.name.collectAsStateWithLifecycle() - Text( - text = name.preferred(), - style = MaterialTheme.typography.headlineMedium, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - fontWeight = FontWeight.SemiBold, - softWrap = true - ) - }, - text = { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally + this@ModalBottomSheet.AnimatedVisibility( + modifier = Modifier.align(Alignment.CenterStart), + visible = sheetState.isFullyExpandedOrTargeted(forceFullExpand = true), + enter = fadeIn(), + exit = fadeOut() ) { - val description by component.description.collectAsStateWithLifecycle() - val translatedDescription by component.translatedDescription.collectAsStateWithLifecycle() - - FlowRow( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), - horizontalArrangement = Arrangement.SpaceAround + IconButton( + onClick = component::dismiss ) { - val gender by component.gender.collectAsStateWithLifecycle() - val bloodType by component.bloodType.collectAsStateWithLifecycle() - val birthDate by component.birthDate.collectAsStateWithLifecycle() + Icon( + imageVector = Icons.Default.ArrowBackIosNew, + contentDescription = stringResource(SharedRes.strings.close) + ) + } + } + } - bloodType?.let { - Column( - verticalArrangement = Arrangement.SpaceEvenly, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - imageVector = Icons.Filled.Bloodtype, - contentDescription = null - ) - Text( - text = stringResource(SharedRes.strings.blood_type), - style = MaterialTheme.typography.labelSmall, - ) - Text( - text = it, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold - ) - } - } + Text( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(vertical = 8.dp), + text = name.preferred(), + style = MaterialTheme.typography.headlineMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.SemiBold, + softWrap = true + ) - gender?.let { - Column( - verticalArrangement = Arrangement.SpaceEvenly, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - imageVector = Icons.Filled.Man4, - contentDescription = null - ) - Text( - text = stringResource(SharedRes.strings.gender), - style = MaterialTheme.typography.labelSmall, - ) - Text( - text = it, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold - ) - } - } + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val description by component.description.collectAsStateWithLifecycle() + val translatedDescription by component.translatedDescription.collectAsStateWithLifecycle() - birthDate?.let { - Column( - verticalArrangement = Arrangement.SpaceEvenly, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - imageVector = Icons.Filled.Cake, - contentDescription = null - ) - Text( - text = stringResource(SharedRes.strings.birth_date), - style = MaterialTheme.typography.labelSmall, - ) - Text( - text = it.format(), - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold - ) - } + FlowRow( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), + horizontalArrangement = Arrangement.SpaceAround + ) { + val gender by component.gender.collectAsStateWithLifecycle() + val bloodType by component.bloodType.collectAsStateWithLifecycle() + val birthDate by component.birthDate.collectAsStateWithLifecycle() + + bloodType?.let { + Column( + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Filled.Bloodtype, + contentDescription = null + ) + Text( + text = stringResource(SharedRes.strings.blood_type), + style = MaterialTheme.typography.labelSmall, + ) + Text( + text = it, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) } } - (translatedDescription ?: description)?.let { - Text( - modifier = Modifier.verticalScroll(rememberScrollState()), - text = it.htmlToAnnotatedString() - ) - } ?: run { - val state by component.state.collectAsStateWithLifecycle() + gender?.let { + Column( + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Filled.Man4, + contentDescription = null + ) + Text( + text = stringResource(SharedRes.strings.gender), + style = MaterialTheme.typography.labelSmall, + ) + Text( + text = it, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + } + } - if (state is CharacterStateMachine.State.Loading) { - CircularProgressIndicator() + birthDate?.let { + Column( + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Filled.Cake, + contentDescription = null + ) + Text( + text = stringResource(SharedRes.strings.birth_date), + style = MaterialTheme.typography.labelSmall, + ) + Text( + text = it.format(), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) } - // ToDo("Display something went wrong") } } - }, - dismissButton = { - val state by component.state.collectAsStateWithLifecycle() - val description by component.description.collectAsStateWithLifecycle() - description?.let { - TranslateButton( - text = it, - ) { text -> + (translatedDescription ?: description)?.let { + TranslateButton(description ?: "") { text -> component.descriptionTranslation(text) } + Text( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(bottom = 16.dp), + text = it.htmlToAnnotatedString() + ) } ?: run { - if (state is CharacterStateMachine.State.Error) { - TextButton( - onClick = component::retry - ) { - Text(text = stringResource(SharedRes.strings.retry)) - } + val state by component.state.collectAsStateWithLifecycle() + + if (state is CharacterStateMachine.State.Loading) { + CircularProgressIndicator() } - } - }, - confirmButton = { - TextButton( - onClick = component::dismiss - ) { - Text(text = stringResource(SharedRes.strings.close)) + // ToDo("Display something went wrong") } } - ) + } } \ No newline at end of file