From 44109d9eda74ee29329db3ca7b248547abb76f96 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Thu, 18 Dec 2025 20:22:46 +0900 Subject: [PATCH 1/3] migrate to m3e list item --- app/build.gradle.kts | 10 + .../ui/screen/settings/AccountsScreen.kt | 63 ++- .../ui/screen/settings/AiConfigScreen.kt | 64 ++- .../ui/screen/settings/AppLoggingScreen.kt | 38 +- .../ui/screen/settings/AppearanceScreen.kt | 431 +++++++++--------- .../screen/settings/LocalFilterEditDialog.kt | 73 ++- .../ui/screen/settings/LocalFilterScreen.kt | 19 +- .../ui/screen/settings/SettingsScreen.kt | 346 ++++++-------- .../flare/ui/screen/settings/StorageScreen.kt | 57 +-- compose-ui/build.gradle.kts | 5 + .../flare/ui/theme/PlatformShapes.android.kt | 50 +- 11 files changed, 552 insertions(+), 604 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3ad148ae5..d3cd8acb2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -247,3 +247,13 @@ extensions.getByType(com.android.build.api.variant.AndroidComponentsExtension::c GenerateDeepLinkManifestTask::manifest ) } + +kotlin { + sourceSets { + all { + languageSettings { + optIn("androidx.compose.material3.ExperimentalMaterial3ExpressiveApi") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AccountsScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AccountsScreen.kt index 86da9ba85..c77858de7 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AccountsScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AccountsScreen.kt @@ -2,7 +2,6 @@ package dev.dimension.flare.ui.screen.settings import androidx.compose.animation.core.AnimationConstants 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.fillMaxSize @@ -10,12 +9,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemColors import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.ListItemShapes import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton +import androidx.compose.material3.SegmentedListItem import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text @@ -48,7 +49,6 @@ import dev.dimension.flare.ui.component.FlareScaffold import dev.dimension.flare.ui.component.RichText import dev.dimension.flare.ui.component.ThemeIconData import dev.dimension.flare.ui.component.ThemedIcon -import dev.dimension.flare.ui.component.listCard import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.isError @@ -102,7 +102,7 @@ internal fun AccountsScreen( modifier = Modifier .padding(horizontal = screenHorizontalPadding), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { when (val accountState = state.accounts) { // TODO: show error @@ -113,12 +113,7 @@ internal fun AccountsScreen( userState = UiState.Loading(), onClick = {}, toLogin = {}, - modifier = - Modifier - .listCard( - index = it, - totalCount = 3, - ), + shapes = ListItemDefaults.segmentedShapes(it, 3), ) } } @@ -140,12 +135,6 @@ internal fun AccountsScreen( } } SwipeToDismissBox( - modifier = - Modifier - .listCard( - index = index, - totalCount = accountState.data.size, - ), state = swipeState, backgroundContent = { if (swipeState.dismissDirection != SwipeToDismissBoxValue.Settled) { @@ -170,6 +159,7 @@ internal fun AccountsScreen( AccountItem( modifier = Modifier, userState = data, + shapes = ListItemDefaults.segmentedShapes(index, accountState.data.size), onClick = { state.setActiveAccount(it) }, @@ -193,6 +183,7 @@ internal fun AccountsScreen( } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun AccountItem( userState: UiState, @@ -206,22 +197,26 @@ fun AccountItem( supportingContent: @Composable (UiUserV2) -> Unit = { Text(text = it.handle, maxLines = 1) }, - tonalElevation: Dp = ListItemDefaults.Elevation, - shadowElevation: Dp = ListItemDefaults.Elevation, avatarSize: Dp = AvatarComponentDefaults.size, colors: ListItemColors = ListItemDefaults.colors(), + shapes: ListItemShapes = ListItemDefaults.shapes(), ) { userState .onSuccess { data -> - ListItem( - headlineContent = { + SegmentedListItem( + modifier = modifier, + onClick = { + onClick.invoke(data.key) + }, + shapes = shapes, + content = { headlineContent.invoke(data) }, - modifier = - modifier - .clickable { - onClick.invoke(data.key) - }, +// modifier = +// modifier +// .clickable { +// onClick.invoke(data.key) +// }, leadingContent = { AvatarComponent(data = data.avatar, size = avatarSize) }, @@ -232,12 +227,12 @@ fun AccountItem( supportingContent.invoke(data) }, colors = colors, - shadowElevation = shadowElevation, - tonalElevation = tonalElevation, ) }.onLoading { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = {}, + shapes = shapes, + content = { Text(text = "Loading...", modifier = Modifier.placeholder(true)) }, modifier = modifier, @@ -252,12 +247,12 @@ fun AccountItem( Text(text = "Loading...", modifier = Modifier.placeholder(true)) }, colors = colors, - shadowElevation = shadowElevation, - tonalElevation = tonalElevation, ) }.onError { throwable -> - ListItem( - headlineContent = { + SegmentedListItem( + onClick = {}, + shapes = shapes, + content = { if (throwable is LoginExpiredException) { Text(text = stringResource(id = R.string.login_expired, throwable.accountKey.toString())) } else { @@ -291,8 +286,6 @@ fun AccountItem( null }, colors = colors, - shadowElevation = shadowElevation, - tonalElevation = tonalElevation, ) } } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AiConfigScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AiConfigScreen.kt index a3dca1873..fb67a9820 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AiConfigScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AiConfigScreen.kt @@ -1,6 +1,5 @@ package dev.dimension.flare.ui.screen.settings -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -13,9 +12,10 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SegmentedListItem import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -50,8 +50,9 @@ import dev.dimension.flare.ui.model.onLoading import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.presenter.settings.FlareServerProviderPresenter -import dev.dimension.flare.ui.theme.listCardContainer -import dev.dimension.flare.ui.theme.listCardItem +import dev.dimension.flare.ui.theme.first +import dev.dimension.flare.ui.theme.item +import dev.dimension.flare.ui.theme.last import dev.dimension.flare.ui.theme.screenHorizontalPadding import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest @@ -88,23 +89,20 @@ internal fun AiConfigScreen(onBack: () -> Unit) { Modifier .verticalScroll(rememberScrollState()) .padding(it) - .padding(horizontal = screenHorizontalPadding) - .listCardContainer(), - verticalArrangement = Arrangement.spacedBy(2.dp), + .padding(horizontal = screenHorizontalPadding), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { - ListItem( - modifier = - Modifier - .listCardItem() - .clickable { - state.setShowServerDialog(true) - }, + SegmentedListItem( + onClick = { + state.setShowServerDialog(true) + }, + shapes = ListItemDefaults.first(), overlineContent = { Text( text = stringResource(id = R.string.settings_ai_config_server), ) }, - headlineContent = { + content = { state.currentServer.onSuccess { Text( text = it, @@ -118,8 +116,14 @@ internal fun AiConfigScreen(onBack: () -> Unit) { ) }, ) - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.update { + copy(translation = !state.aiConfig.translation) + } + }, + shapes = ListItemDefaults.item(), + content = { Text( text = stringResource(id = R.string.settings_ai_config_entable_translation), ) @@ -139,17 +143,15 @@ internal fun AiConfigScreen(onBack: () -> Unit) { }, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.update { - copy(translation = !state.aiConfig.translation) - } - }, ) - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.update { + copy(tldr = !state.aiConfig.tldr) + } + }, + shapes = ListItemDefaults.last(), + content = { Text( text = stringResource(id = R.string.settings_ai_config_enable_tldr), ) @@ -169,14 +171,6 @@ internal fun AiConfigScreen(onBack: () -> Unit) { }, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.update { - copy(tldr = !state.aiConfig.tldr) - } - }, ) } } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppLoggingScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppLoggingScreen.kt index 60c950832..3d6a1fc70 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppLoggingScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppLoggingScreen.kt @@ -1,7 +1,6 @@ package dev.dimension.flare.ui.screen.settings import android.widget.Toast -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -12,8 +11,9 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SegmentedListItem import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -36,12 +36,11 @@ import dev.dimension.flare.ui.component.BackButton import dev.dimension.flare.ui.component.FAIcon import dev.dimension.flare.ui.component.FlareLargeFlexibleTopAppBar import dev.dimension.flare.ui.component.FlareScaffold -import dev.dimension.flare.ui.component.listCard import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.presenter.settings.DevModePresenter import dev.dimension.flare.ui.screen.media.saveByteArrayToDownloads -import dev.dimension.flare.ui.theme.listCardContainer import dev.dimension.flare.ui.theme.screenHorizontalPadding +import dev.dimension.flare.ui.theme.single import moe.tlaster.precompose.molecule.producePresenter import kotlin.time.Clock @@ -106,11 +105,15 @@ internal fun AppLoggingScreen(onBack: () -> Unit) { modifier = Modifier .padding(horizontal = screenHorizontalPadding), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { item { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.setEnabled(!state.enabled) + }, + shapes = ListItemDefaults.single(), + content = { Text(stringResource(R.string.settings_app_logging_enable_network_logging)) }, trailingContent = { @@ -121,28 +124,21 @@ internal fun AppLoggingScreen(onBack: () -> Unit) { }, ) }, - modifier = - Modifier - .listCardContainer() - .clickable { - state.setEnabled(!state.enabled) - }, ) } item { Spacer(modifier = Modifier.height(12.dp)) } itemsIndexed(state.messages) { index, it -> - ListItem( - headlineContent = { + SegmentedListItem( + selected = selectedMessage == it, + onClick = { + selectedMessage = it + }, + shapes = ListItemDefaults.segmentedShapes(index, state.messages.size), + content = { Text(it, maxLines = 3) }, - modifier = - Modifier - .listCard(index = index, totalCount = state.messages.size) - .clickable { - selectedMessage = it - }, ) } } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceScreen.kt index 41831f1e5..b0a73624b 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceScreen.kt @@ -3,10 +3,10 @@ package dev.dimension.flare.ui.screen.settings import android.os.Build import androidx.compose.animation.AnimatedVisibility 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.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -18,8 +18,10 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.ListItemShapes import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedListItem import androidx.compose.material3.Slider import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -33,7 +35,6 @@ 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.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -61,13 +62,16 @@ import dev.dimension.flare.ui.component.FlareLargeFlexibleTopAppBar import dev.dimension.flare.ui.component.FlareScaffold import dev.dimension.flare.ui.component.platform.isBigScreen import dev.dimension.flare.ui.component.status.StatusItem +import dev.dimension.flare.ui.model.isSuccess import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.presenter.home.ActiveAccountPresenter import dev.dimension.flare.ui.presenter.home.UserState import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.presenter.settings.AppearancePresenter import dev.dimension.flare.ui.presenter.settings.AppearanceState -import dev.dimension.flare.ui.theme.listCardContainer +import dev.dimension.flare.ui.theme.first +import dev.dimension.flare.ui.theme.item +import dev.dimension.flare.ui.theme.last import dev.dimension.flare.ui.theme.listCardItem import dev.dimension.flare.ui.theme.screenHorizontalPadding import kotlinx.collections.immutable.ImmutableMap @@ -111,10 +115,7 @@ internal fun AppearanceScreen( verticalArrangement = Arrangement.spacedBy(16.dp), ) { Column( - modifier = - Modifier - .listCardContainer(), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { SingleChoiceSettingsItem( headline = { Text(text = stringResource(id = R.string.settings_appearance_theme)) }, @@ -131,10 +132,16 @@ internal fun AppearanceScreen( copy(theme = it) } }, - modifier = Modifier.listCardItem(), + shapes = ListItemDefaults.first(), ) - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.updateSettings { + copy(pureColorMode = !pureColorMode) + } + }, + shapes = ListItemDefaults.item(), + content = { Text(text = stringResource(id = R.string.settings_appearance_theme_pure_color)) }, supportingContent = { @@ -150,18 +157,16 @@ internal fun AppearanceScreen( }, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.updateSettings { - copy(pureColorMode = !pureColorMode) - } - }, ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.updateSettings { + copy(dynamicTheme = !dynamicTheme) + } + }, + shapes = ListItemDefaults.item(), + content = { Text(text = stringResource(id = R.string.settings_appearance_dynamic_theme)) }, supportingContent = { @@ -177,19 +182,15 @@ internal fun AppearanceScreen( }, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.updateSettings { - copy(dynamicTheme = !dynamicTheme) - } - }, ) } AnimatedVisibility(visible = !appearanceSettings.dynamicTheme || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + toColorPicker.invoke() + }, + shapes = ListItemDefaults.item(), + content = { Text(text = stringResource(id = R.string.settings_appearance_theme_color)) }, supportingContent = { @@ -205,12 +206,6 @@ internal fun AppearanceScreen( ).size(36.dp), ) }, - modifier = - Modifier - .listCardItem() - .clickable { - toColorPicker.invoke() - }, ) } @@ -228,7 +223,7 @@ internal fun AppearanceScreen( copy(bottomBarStyle = it) } }, - modifier = Modifier.listCardItem(), + shapes = ListItemDefaults.item(), ) SingleChoiceSettingsItem( @@ -249,99 +244,102 @@ internal fun AppearanceScreen( copy(bottomBarBehavior = it) } }, - modifier = Modifier.listCardItem(), + shapes = ListItemDefaults.item(), ) var fontSizeDiff by remember { mutableFloatStateOf(appearanceSettings.fontSizeDiff) } - Column( - modifier = - Modifier - .listCardItem() - .background(MaterialTheme.colorScheme.surface) - .padding(vertical = 8.dp, horizontal = 16.dp), - ) { - Text( - text = stringResource(id = R.string.settings_appearance_font_size_diff), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = stringResource(id = R.string.settings_appearance_font_size_diff_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Row( - horizontalArrangement = - Arrangement.spacedBy( - 8.dp, - Alignment.CenterHorizontally, - ), - ) { - IconButton( - onClick = { - if (fontSizeDiff > -4f) { - fontSizeDiff -= 1f - hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentTick) - state.updateSettings { - copy( - fontSizeDiff = fontSizeDiff, - lineHeightDiff = fontSizeDiff * 2, - ) - } - } - }, - enabled = fontSizeDiff > -4f, - ) { - FAIcon( - FontAwesomeIcons.Solid.Minus, - contentDescription = stringResource(R.string.settings_appearance_font_size_diff_decrease), + SegmentedListItem( + onClick = {}, + shapes = ListItemDefaults.item(), + content = { + Column { + Text( + text = stringResource(id = R.string.settings_appearance_font_size_diff), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, ) } - Slider( - value = fontSizeDiff, - onValueChange = { - fontSizeDiff = it - hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentTick) - }, - onValueChangeFinished = { - state.updateSettings { - copy( - fontSizeDiff = fontSizeDiff, - lineHeightDiff = fontSizeDiff * 2, + }, + supportingContent = { + Column { + Text( + text = stringResource(id = R.string.settings_appearance_font_size_diff_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Row { + IconButton( + onClick = { + if (fontSizeDiff > -4f) { + fontSizeDiff -= 1f + hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentTick) + state.updateSettings { + copy( + fontSizeDiff = fontSizeDiff, + lineHeightDiff = fontSizeDiff * 2, + ) + } + } + }, + enabled = fontSizeDiff > -4f, + ) { + FAIcon( + FontAwesomeIcons.Solid.Minus, + contentDescription = stringResource(R.string.settings_appearance_font_size_diff_decrease), ) } - }, - valueRange = -4f..4f, - steps = 7, - modifier = - Modifier - .weight(1f), - ) - IconButton( - onClick = { - if (fontSizeDiff < 4f) { - fontSizeDiff += 1f - hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentTick) - state.updateSettings { - copy( - fontSizeDiff = fontSizeDiff, - lineHeightDiff = fontSizeDiff * 2, - ) - } + Slider( + value = fontSizeDiff, + onValueChange = { + fontSizeDiff = it + hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentTick) + }, + onValueChangeFinished = { + state.updateSettings { + copy( + fontSizeDiff = fontSizeDiff, + lineHeightDiff = fontSizeDiff * 2, + ) + } + }, + valueRange = -4f..4f, + steps = 7, + modifier = + Modifier + .weight(1f), + ) + IconButton( + onClick = { + if (fontSizeDiff < 4f) { + fontSizeDiff += 1f + hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentTick) + state.updateSettings { + copy( + fontSizeDiff = fontSizeDiff, + lineHeightDiff = fontSizeDiff * 2, + ) + } + } + }, + enabled = fontSizeDiff < 4f, + ) { + FAIcon( + FontAwesomeIcons.Solid.Plus, + contentDescription = stringResource(R.string.settings_appearance_font_size_diff_increase), + ) } - }, - enabled = fontSizeDiff < 4f, - ) { - FAIcon( - FontAwesomeIcons.Solid.Plus, - contentDescription = stringResource(R.string.settings_appearance_font_size_diff_increase), - ) + } } - } - } - - ListItem( - headlineContent = { + }, + ) + SegmentedListItem( + onClick = { + state.updateSettings { + copy(inAppBrowser = !inAppBrowser) + } + }, + shapes = ListItemDefaults.last(), + content = { Text(text = stringResource(id = R.string.settings_appearance_in_app_browser)) }, supportingContent = { @@ -357,31 +355,26 @@ internal fun AppearanceScreen( }, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.updateSettings { - copy(inAppBrowser = !inAppBrowser) - } - }, ) } Column( - modifier = - Modifier - .listCardContainer(), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { state.sampleStatus.onSuccess { - StatusItem( - it, - modifier = - Modifier - .listCardItem() - .background(MaterialTheme.colorScheme.surface), - ) + SegmentedListItem( + onClick = {}, + shapes = ListItemDefaults.first(), + contentPadding = PaddingValues(0.dp), + ) { + StatusItem( + it, + modifier = + Modifier + .listCardItem() + .background(MaterialTheme.colorScheme.surface), + ) + } } SingleChoiceSettingsItem( headline = { Text(text = stringResource(id = R.string.settings_appearance_avatar_shape)) }, @@ -397,10 +390,21 @@ internal fun AppearanceScreen( copy(avatarShape = it) } }, - modifier = Modifier.listCardItem(), + shapes = + if (state.sampleStatus.isSuccess) { + ListItemDefaults.item() + } else { + ListItemDefaults.first() + }, ) - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.updateSettings { + copy(fullWidthPost = !fullWidthPost) + } + }, + shapes = ListItemDefaults.item(), + content = { Text(text = stringResource(id = R.string.settings_appearance_full_width_post)) }, supportingContent = { @@ -416,14 +420,6 @@ internal fun AppearanceScreen( }, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.updateSettings { - copy(fullWidthPost = !fullWidthPost) - } - }, ) SingleChoiceSettingsItem( headline = { Text(text = stringResource(id = R.string.settings_appearance_post_action_style)) }, @@ -442,10 +438,17 @@ internal fun AppearanceScreen( copy(postActionStyle = it) } }, + shapes = ListItemDefaults.item(), ) AnimatedVisibility(appearanceSettings.postActionStyle != PostActionStyle.Hidden) { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.updateSettings { + copy(showNumbers = !showNumbers) + } + }, + shapes = ListItemDefaults.item(), + content = { Text(text = stringResource(id = R.string.settings_appearance_show_numbers)) }, supportingContent = { @@ -461,18 +464,16 @@ internal fun AppearanceScreen( }, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.updateSettings { - copy(showNumbers = !showNumbers) - } - }, ) } - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.updateSettings { + copy(showLinkPreview = !showLinkPreview) + } + }, + shapes = ListItemDefaults.item(), + content = { Text(text = stringResource(id = R.string.settings_appearance_show_link_previews)) }, supportingContent = { @@ -488,18 +489,16 @@ internal fun AppearanceScreen( }, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.updateSettings { - copy(showLinkPreview = !showLinkPreview) - } - }, ) AnimatedVisibility(visible = appearanceSettings.showLinkPreview) { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.updateSettings { + copy(compatLinkPreview = !compatLinkPreview) + } + }, + shapes = ListItemDefaults.item(), + content = { Text(text = stringResource(id = R.string.settings_appearance_compat_link_previews)) }, supportingContent = { @@ -515,18 +514,16 @@ internal fun AppearanceScreen( }, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.updateSettings { - copy(compatLinkPreview = !compatLinkPreview) - } - }, ) } - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.updateSettings { + copy(showMedia = !showMedia) + } + }, + shapes = ListItemDefaults.item(), + content = { Text(text = stringResource(id = R.string.settings_appearance_show_media)) }, supportingContent = { @@ -542,18 +539,16 @@ internal fun AppearanceScreen( }, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.updateSettings { - copy(showMedia = !showMedia) - } - }, ) AnimatedVisibility(appearanceSettings.showMedia) { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.updateSettings { + copy(showSensitiveContent = !showSensitiveContent) + } + }, + shapes = ListItemDefaults.item(), + content = { Text(text = stringResource(id = R.string.settings_appearance_show_cw_img)) }, supportingContent = { @@ -569,19 +564,17 @@ internal fun AppearanceScreen( }, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.updateSettings { - copy(showSensitiveContent = !showSensitiveContent) - } - }, ) } AnimatedVisibility(appearanceSettings.showMedia) { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.updateSettings { + copy(expandMediaSize = !expandMediaSize) + } + }, + shapes = ListItemDefaults.item(), + content = { Text(text = stringResource(id = R.string.settings_appearance_expand_media)) }, supportingContent = { @@ -597,14 +590,6 @@ internal fun AppearanceScreen( }, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.updateSettings { - copy(expandMediaSize = !expandMediaSize) - } - }, ) } AnimatedVisibility(appearanceSettings.showMedia) { @@ -624,6 +609,7 @@ internal fun AppearanceScreen( } }, modifier = Modifier.listCardItem(), + shapes = ListItemDefaults.last(), ) } } @@ -639,20 +625,21 @@ private fun SingleChoiceSettingsItem( items: ImmutableMap, selected: T, onSelected: (T) -> Unit, + shapes: ListItemShapes, modifier: Modifier = Modifier, ) { val isBigScreen = isBigScreen() var showMenu by remember { mutableStateOf(false) } - - ListItem( - modifier = - modifier - .clickable { - if (!isBigScreen) { - showMenu = true - } - }, - headlineContent = headline, + SegmentedListItem( + modifier = modifier, + checked = showMenu, + onCheckedChange = { + if (!isBigScreen) { + showMenu = it + } + }, + shapes = shapes, + content = headline, supportingContent = supporting, trailingContent = { if (isBigScreen) { diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/LocalFilterEditDialog.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/LocalFilterEditDialog.kt index bb1d24893..15e73b0fd 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/LocalFilterEditDialog.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/LocalFilterEditDialog.kt @@ -1,7 +1,6 @@ package dev.dimension.flare.ui.screen.settings import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -10,9 +9,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SegmentedListItem import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -39,9 +39,11 @@ import dev.dimension.flare.ui.model.UiKeywordFilter import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.presenter.settings.LocalFilterPresenter -import dev.dimension.flare.ui.theme.listCardContainer -import dev.dimension.flare.ui.theme.listCardItem +import dev.dimension.flare.ui.theme.first +import dev.dimension.flare.ui.theme.item +import dev.dimension.flare.ui.theme.last import dev.dimension.flare.ui.theme.screenHorizontalPadding +import dev.dimension.flare.ui.theme.single import moe.tlaster.precompose.molecule.producePresenter @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @@ -109,13 +111,14 @@ internal fun LocalFilterEditDialog( modifier = Modifier.fillMaxWidth(), ) Column( - modifier = - Modifier - .listCardContainer(), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.setForTimeline(!state.forTimeline) + }, + shapes = ListItemDefaults.first(), + content = { Text(text = stringResource(id = R.string.local_filter_for_timeline)) }, trailingContent = { @@ -124,15 +127,13 @@ internal fun LocalFilterEditDialog( onCheckedChange = state::setForTimeline, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.setForTimeline(!state.forTimeline) - }, ) - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.setForNotification(!state.forNotification) + }, + shapes = ListItemDefaults.item(), + content = { Text(text = stringResource(id = R.string.local_filter_for_notification)) }, trailingContent = { @@ -141,15 +142,13 @@ internal fun LocalFilterEditDialog( onCheckedChange = state::setForNotification, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.setForNotification(!state.forNotification) - }, ) - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.setForSearch(!state.forSearch) + }, + shapes = ListItemDefaults.last(), + content = { Text(text = stringResource(id = R.string.local_filter_for_search)) }, trailingContent = { @@ -158,18 +157,17 @@ internal fun LocalFilterEditDialog( onCheckedChange = state::setForSearch, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.setForSearch(!state.forSearch) - }, ) } Spacer(modifier = Modifier.weight(1f)) if (keyword != null) { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.delete() + onBack() + }, + shapes = ListItemDefaults.single(), + content = { Text( text = stringResource(id = R.string.local_filter_delete), color = MaterialTheme.colorScheme.error, @@ -182,13 +180,6 @@ internal fun LocalFilterEditDialog( tint = MaterialTheme.colorScheme.error, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.delete() - onBack() - }, ) } } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/LocalFilterScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/LocalFilterScreen.kt index ed1956549..b5c178064 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/LocalFilterScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/LocalFilterScreen.kt @@ -5,7 +5,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.SegmentedListItem import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -14,7 +15,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import compose.icons.FontAwesomeIcons import compose.icons.fontawesomeicons.Solid import compose.icons.fontawesomeicons.solid.Pen @@ -24,7 +24,6 @@ import dev.dimension.flare.ui.component.BackButton import dev.dimension.flare.ui.component.FAIcon import dev.dimension.flare.ui.component.FlareLargeFlexibleTopAppBar import dev.dimension.flare.ui.component.FlareScaffold -import dev.dimension.flare.ui.component.listCard import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.presenter.settings.LocalFilterPresenter @@ -73,13 +72,15 @@ internal fun LocalFilterScreen( modifier = Modifier .padding(horizontal = screenHorizontalPadding), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { state.items.onSuccess { list -> items(list.size) { index -> val item = list[index] - ListItem( - headlineContent = { + SegmentedListItem( + onClick = {}, + shapes = ListItemDefaults.segmentedShapes(index, list.size), + content = { Text(text = item.keyword) }, // supportingContent = { @@ -95,12 +96,6 @@ internal fun LocalFilterScreen( ) } }, - modifier = - Modifier - .listCard( - index = index, - totalCount = list.size, - ), ) } } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt index 4996033bf..1f066637e 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt @@ -2,7 +2,6 @@ package dev.dimension.flare.ui.screen.settings import android.content.Intent import android.provider.Settings -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -12,7 +11,8 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.SegmentedListItem import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -46,14 +46,17 @@ import dev.dimension.flare.ui.component.FlareLargeFlexibleTopAppBar import dev.dimension.flare.ui.component.FlareScaffold import dev.dimension.flare.ui.component.ThemeIconData import dev.dimension.flare.ui.component.ThemedIcon +import dev.dimension.flare.ui.model.isSuccess import dev.dimension.flare.ui.model.onError import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.presenter.home.ActiveAccountPresenter import dev.dimension.flare.ui.presenter.home.UserState import dev.dimension.flare.ui.presenter.invoke -import dev.dimension.flare.ui.theme.listCardContainer -import dev.dimension.flare.ui.theme.listCardItem +import dev.dimension.flare.ui.theme.first +import dev.dimension.flare.ui.theme.item +import dev.dimension.flare.ui.theme.last import dev.dimension.flare.ui.theme.screenHorizontalPadding +import dev.dimension.flare.ui.theme.single import moe.tlaster.precompose.molecule.producePresenter @Composable @@ -126,98 +129,78 @@ internal fun SettingsScreen( ) { state.user .onSuccess { - Column( - modifier = - Modifier - .listCardContainer(), - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - AccountItem( - userState = state.user, - avatarSize = 40.dp, - onClick = { - toAccounts.invoke() - }, - supportingContent = { - Text(text = stringResource(id = R.string.settings_accounts_title)) - }, - toLogin = { - toAccounts.invoke() - }, - modifier = - Modifier - .listCardItem(), - ) - } + AccountItem( + userState = state.user, + avatarSize = 40.dp, + onClick = { + toAccounts.invoke() + }, + supportingContent = { + Text(text = stringResource(id = R.string.settings_accounts_title)) + }, + toLogin = { + toAccounts.invoke() + }, + shapes = ListItemDefaults.single(), + ) }.onError { - Column( - modifier = - Modifier - .listCardContainer(), - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - ListItem( - headlineContent = { - Text(text = stringResource(id = R.string.settings_accounts_title)) - }, - modifier = - Modifier - .listCardItem() - .clickable { - toAccounts.invoke() - }, - leadingContent = { - ThemedIcon( - imageVector = FontAwesomeIcons.Solid.CircleUser, - contentDescription = null, - color = ThemeIconData.Color.ImperialMagenta, - ) - }, - supportingContent = { - Text(text = stringResource(id = R.string.settings_accounts_title)) - }, - ) - } + SegmentedListItem( + onClick = { + toAccounts.invoke() + }, + shapes = ListItemDefaults.single(), + content = { + Text(text = stringResource(id = R.string.settings_accounts_title)) + }, + leadingContent = { + ThemedIcon( + imageVector = FontAwesomeIcons.Solid.CircleUser, + contentDescription = null, + color = ThemeIconData.Color.ImperialMagenta, + ) + }, + supportingContent = { + Text(text = stringResource(id = R.string.settings_accounts_title)) + }, + ) } state.user .onError { - Column( - modifier = - Modifier - .listCardContainer(), - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - ListItem( - headlineContent = { - Text(text = stringResource(id = R.string.settings_guest_setting_title)) - }, - modifier = - Modifier - .listCardItem() - .clickable { - toGuestSettings.invoke() - }, - leadingContent = { - ThemedIcon( - imageVector = FontAwesomeIcons.Solid.Globe, - contentDescription = stringResource(id = R.string.settings_guest_setting_title), - color = ThemeIconData.Color.SapphireBlue, - ) - }, - supportingContent = { - Text(text = stringResource(id = R.string.settings_guest_setting_description)) - }, - ) - } + SegmentedListItem( + onClick = { + toGuestSettings.invoke() + }, + shapes = ListItemDefaults.single(), + content = { + Text(text = stringResource(id = R.string.settings_guest_setting_title)) + }, + leadingContent = { + ThemedIcon( + imageVector = FontAwesomeIcons.Solid.Globe, + contentDescription = stringResource(id = R.string.settings_guest_setting_title), + color = ThemeIconData.Color.SapphireBlue, + ) + }, + supportingContent = { + Text(text = stringResource(id = R.string.settings_guest_setting_description)) + }, + ) } Column( - modifier = - Modifier.listCardContainer(), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + toAppearance.invoke() + }, + shapes = + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.TIRAMISU && !state.user.isSuccess) { + ListItemDefaults.single() + } else { + ListItemDefaults.first() + }, + content = { Text(text = stringResource(id = R.string.settings_appearance_title)) }, leadingContent = { @@ -230,16 +213,27 @@ internal fun SettingsScreen( supportingContent = { Text(text = stringResource(id = R.string.settings_appearance_subtitle)) }, - modifier = - Modifier - .listCardItem() - .clickable { - toAppearance.invoke() - }, ) if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + try { + val intent = + Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply { + data = "package:${BuildConfig.APPLICATION_ID}".toUri() + } + context.startActivity(intent) + } catch (e: Exception) { + e.printStackTrace() + } + }, + shapes = + if (state.user.isSuccess) { + ListItemDefaults.item() + } else { + ListItemDefaults.last() + }, + content = { Text(text = stringResource(id = R.string.settings_language_title)) }, leadingContent = { @@ -252,25 +246,15 @@ internal fun SettingsScreen( supportingContent = { Text(text = stringResource(id = R.string.settings_language_description)) }, - modifier = - Modifier - .listCardItem() - .clickable { - try { - val intent = - Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply { - data = "package:${BuildConfig.APPLICATION_ID}".toUri() - } - context.startActivity(intent) - } catch (e: Exception) { - e.printStackTrace() - } - }, ) } state.user.onSuccess { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + toTabCustomization.invoke() + }, + shapes = ListItemDefaults.last(), + content = { Text(text = stringResource(id = R.string.settings_side_panel)) }, leadingContent = { @@ -283,25 +267,20 @@ internal fun SettingsScreen( supportingContent = { Text(text = stringResource(id = R.string.settings_side_panel_description)) }, - modifier = - Modifier - .listCardItem() - .clickable { - toTabCustomization.invoke() - }, ) } } state.user.onSuccess { Column( - modifier = - Modifier - .listCardContainer(), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + toLocalFilter.invoke() + }, + shapes = ListItemDefaults.first(), + content = { Text(text = stringResource(id = R.string.settings_local_filter_title)) }, leadingContent = { @@ -314,23 +293,15 @@ internal fun SettingsScreen( supportingContent = { Text(text = stringResource(id = R.string.settings_local_filter_description)) }, - modifier = - Modifier - .listCardItem() - .clickable { - toLocalFilter.invoke() - }, ) - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + toLocalHistory.invoke() + }, + shapes = ListItemDefaults.item(), + content = { Text(text = stringResource(id = R.string.settings_local_history_title)) }, - modifier = - Modifier - .listCardItem() - .clickable { - toLocalHistory.invoke() - }, leadingContent = { ThemedIcon( imageVector = FontAwesomeIcons.Solid.ClockRotateLeft, @@ -342,8 +313,12 @@ internal fun SettingsScreen( Text(text = stringResource(id = R.string.settings_local_history_description)) }, ) - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + toStorage.invoke() + }, + shapes = ListItemDefaults.last(), + content = { Text(text = stringResource(id = R.string.settings_storage_title)) }, leadingContent = { @@ -356,12 +331,6 @@ internal fun SettingsScreen( supportingContent = { Text(text = stringResource(id = R.string.settings_storage_subtitle)) }, - modifier = - Modifier - .listCardItem() - .clickable { - toStorage.invoke() - }, ) } // ListItem( @@ -383,43 +352,35 @@ internal fun SettingsScreen( // }, // ) } + SegmentedListItem( + onClick = { + toAiConfig.invoke() + }, + shapes = ListItemDefaults.single(), + content = { + Text(text = stringResource(id = R.string.settings_ai_config_title)) + }, + leadingContent = { + ThemedIcon( + imageVector = FontAwesomeIcons.Solid.Robot, + contentDescription = stringResource(id = R.string.settings_ai_config_title), + color = ThemeIconData.Color.ForestGreen, + ) + }, + supportingContent = { + Text(text = stringResource(id = R.string.settings_ai_config_description)) + }, + ) Column( - modifier = - Modifier - .listCardContainer(), - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - ListItem( - headlineContent = { - Text(text = stringResource(id = R.string.settings_ai_config_title)) - }, - leadingContent = { - ThemedIcon( - imageVector = FontAwesomeIcons.Solid.Robot, - contentDescription = stringResource(id = R.string.settings_ai_config_title), - color = ThemeIconData.Color.ForestGreen, - ) - }, - supportingContent = { - Text(text = stringResource(id = R.string.settings_ai_config_description)) - }, - modifier = - Modifier - .listCardItem() - .clickable { - toAiConfig.invoke() - }, - ) - } - Column( - modifier = - Modifier - .listCardContainer(), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { if (BuildConfig.DEBUG) { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + toColorSpace.invoke() + }, + shapes = ListItemDefaults.first(), + content = { Text(text = "Color Space") }, leadingContent = { @@ -429,16 +390,19 @@ internal fun SettingsScreen( color = ThemeIconData.Color.CharcoalGrey, ) }, - modifier = - Modifier - .listCardItem() - .clickable { - toColorSpace.invoke() - }, ) } - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + toAbout.invoke() + }, + shapes = + if (BuildConfig.DEBUG) { + ListItemDefaults.last() + } else { + ListItemDefaults.single() + }, + content = { Text(text = stringResource(id = R.string.settings_about_title)) }, leadingContent = { @@ -451,12 +415,6 @@ internal fun SettingsScreen( supportingContent = { Text(text = stringResource(id = R.string.settings_about_subtitle)) }, - modifier = - Modifier - .listCardItem() - .clickable { - toAbout.invoke() - }, ) } } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/StorageScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/StorageScreen.kt index f25507cd5..9de795ecf 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/StorageScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/StorageScreen.kt @@ -1,14 +1,14 @@ package dev.dimension.flare.ui.screen.settings import android.content.Context -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.SegmentedListItem import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -20,7 +20,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import coil3.imageLoader import compose.icons.FontAwesomeIcons import compose.icons.fontawesomeicons.Solid @@ -36,8 +35,9 @@ import dev.dimension.flare.ui.component.ThemedIcon import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.presenter.settings.StoragePresenter import dev.dimension.flare.ui.presenter.settings.StorageState -import dev.dimension.flare.ui.theme.listCardContainer -import dev.dimension.flare.ui.theme.listCardItem +import dev.dimension.flare.ui.theme.first +import dev.dimension.flare.ui.theme.item +import dev.dimension.flare.ui.theme.last import dev.dimension.flare.ui.theme.screenHorizontalPadding import moe.tlaster.precompose.molecule.producePresenter @@ -71,12 +71,15 @@ internal fun StorageScreen( Modifier .verticalScroll(rememberScrollState()) .padding(it) - .padding(horizontal = screenHorizontalPadding) - .listCardContainer(), - verticalArrangement = Arrangement.spacedBy(2.dp), + .padding(horizontal = screenHorizontalPadding), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.clearImageCache() + }, + shapes = ListItemDefaults.first(), + content = { Text(text = stringResource(id = R.string.settings_storage_clear_image_cache)) }, supportingContent = { @@ -88,12 +91,6 @@ internal fun StorageScreen( ), ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.clearImageCache() - }, leadingContent = { ThemedIcon( FontAwesomeIcons.Solid.Images, @@ -102,8 +99,12 @@ internal fun StorageScreen( ) }, ) - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + state.clearCacheDatabase() + }, + shapes = ListItemDefaults.item(), + content = { Text(text = stringResource(id = R.string.settings_storage_clear_database)) }, supportingContent = { @@ -116,12 +117,6 @@ internal fun StorageScreen( ), ) }, - modifier = - Modifier - .listCardItem() - .clickable { - state.clearCacheDatabase() - }, leadingContent = { ThemedIcon( FontAwesomeIcons.Solid.Database, @@ -130,8 +125,12 @@ internal fun StorageScreen( ) }, ) - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + toAppLog.invoke() + }, + shapes = ListItemDefaults.last(), + content = { Text(text = stringResource(id = R.string.settings_storage_app_log)) }, supportingContent = { @@ -140,12 +139,6 @@ internal fun StorageScreen( stringResource(id = R.string.settings_storage_app_log_description), ) }, - modifier = - Modifier - .listCardItem() - .clickable { - toAppLog.invoke() - }, leadingContent = { ThemedIcon( FontAwesomeIcons.Solid.Envelope, diff --git a/compose-ui/build.gradle.kts b/compose-ui/build.gradle.kts index fcc6592d4..43be3e2a0 100644 --- a/compose-ui/build.gradle.kts +++ b/compose-ui/build.gradle.kts @@ -36,6 +36,11 @@ kotlin { } sourceSets { + all { + languageSettings { + optIn("androidx.compose.material3.ExperimentalMaterial3ExpressiveApi") + } + } val commonMain by getting { dependencies { implementation(projects.shared) diff --git a/compose-ui/src/androidMain/kotlin/dev/dimension/flare/ui/theme/PlatformShapes.android.kt b/compose-ui/src/androidMain/kotlin/dev/dimension/flare/ui/theme/PlatformShapes.android.kt index 8e57580ee..128885bd9 100644 --- a/compose-ui/src/androidMain/kotlin/dev/dimension/flare/ui/theme/PlatformShapes.android.kt +++ b/compose-ui/src/androidMain/kotlin/dev/dimension/flare/ui/theme/PlatformShapes.android.kt @@ -4,6 +4,8 @@ import android.os.Build import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.ListItemShapes import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -34,28 +36,20 @@ internal actual object PlatformShapes { actual val topCardShape: Shape @Composable - get() = - listCardItemShape.copy( - topStart = listCardContainerShape.topStart, - topEnd = listCardContainerShape.topEnd, - ) + get() = ListItemDefaults.first().shape actual val bottomCardShape: Shape @Composable - get() = - listCardItemShape.copy( - bottomStart = listCardContainerShape.bottomStart, - bottomEnd = listCardContainerShape.bottomEnd, - ) + get() = ListItemDefaults.last().shape @OptIn(ExperimentalMaterial3ExpressiveApi::class) actual val listCardContainerShape: CornerBasedShape @Composable - get() = MaterialTheme.shapes.largeIncreased + get() = ListItemDefaults.single().shape as? CornerBasedShape ?: MaterialTheme.shapes.large actual val listCardItemShape: CornerBasedShape @Composable - get() = MaterialTheme.shapes.extraSmall + get() = ListItemDefaults.item().shape as? CornerBasedShape ?: MaterialTheme.shapes.extraSmall actual val dmShapeFromMe: CornerBasedShape @Composable get() = @@ -119,3 +113,35 @@ private fun Modifier.compatClip(shape: Shape): Modifier { } } } + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +public fun ListItemDefaults.first(): ListItemShapes = ListItemDefaults.segmentedShapes(0, 2) + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +public fun ListItemDefaults.item(): ListItemShapes = ListItemDefaults.segmentedShapes(1, 3) + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +public fun ListItemDefaults.last(): ListItemShapes = ListItemDefaults.segmentedShapes(1, 2) + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +public fun ListItemDefaults.single(): ListItemShapes { + val first = first() + val last = last() + val firstShape = first.shape + val lastShape = last.shape + if (firstShape is CornerBasedShape && lastShape is CornerBasedShape) { + return first.copy( + shape = + firstShape.copy( + bottomStart = lastShape.bottomStart, + bottomEnd = lastShape.bottomEnd, + ), + ) + } else { + return ListItemDefaults.segmentedShapes(0, 1) + } +} From 8d8291bb6cab310d40ace27b4719c98867b17ae1 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Mon, 5 Jan 2026 16:21:51 +0900 Subject: [PATCH 2/3] update list item usage --- .../ui/component/NavigationSuiteScaffold2.kt | 53 +++--- .../flare/ui/screen/dm/DMListScreen.kt | 3 +- .../ui/screen/home/AccountSelectionModal.kt | 12 +- .../flare/ui/screen/home/HomeScreen.kt | 52 +++--- .../flare/ui/screen/home/TabSettingScreen.kt | 19 +-- .../flare/ui/screen/list/ListScreen.kt | 3 +- .../flare/ui/screen/rss/RssSourcesScreen.kt | 4 +- .../ui/screen/settings/AccountsScreen.kt | 127 ++++++++++---- .../ui/screen/settings/AppLoggingScreen.kt | 3 +- .../ui/screen/settings/AppearanceScreen.kt | 3 - .../ui/screen/settings/LocalFilterScreen.kt | 3 +- .../ui/screen/settings/TabCustomizeScreen.kt | 161 +++++++++--------- .../platform/PlatformListItem.android.kt | 29 ++++ .../flare/ui/theme/PlatformShapes.android.kt | 60 ++----- .../flare/ui/component/UiListItemComponent.kt | 121 +++++++++---- .../flare/ui/component/dm/DmListItem.kt | 18 +- .../ui/component/platform/PlatformListItem.kt | 14 ++ .../ui/screen/bluesky/BlueskyFeedWithTabs.kt | 8 +- .../flare/ui/screen/rss/RssListWithTabs.kt | 19 +-- .../ui/screen/settings/AboutScreenContent.kt | 86 ++++------ .../platform/PlatformListItem.ios.kt | 34 ++++ .../platform/PlatformListItem.jvm.kt | 34 ++++ .../flare/ui/presenter/home/UserPresenter.kt | 43 +++-- 23 files changed, 543 insertions(+), 366 deletions(-) diff --git a/app/src/main/java/dev/dimension/flare/ui/component/NavigationSuiteScaffold2.kt b/app/src/main/java/dev/dimension/flare/ui/component/NavigationSuiteScaffold2.kt index bcf504ffb..02dc4a71b 100644 --- a/app/src/main/java/dev/dimension/flare/ui/component/NavigationSuiteScaffold2.kt +++ b/app/src/main/java/dev/dimension/flare/ui/component/NavigationSuiteScaffold2.kt @@ -9,7 +9,6 @@ import androidx.compose.animation.animateContentSize import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -37,12 +36,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FloatingToolbarDefaults.floatingToolbarVerticalNestedScroll import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalWideNavigationRail import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationDrawerItem import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.SegmentedListItem import androidx.compose.material3.ShortNavigationBarItem import androidx.compose.material3.Surface import androidx.compose.material3.WideNavigationRailState @@ -83,6 +83,7 @@ import dev.dimension.flare.R import dev.dimension.flare.data.model.BottomBarBehavior import dev.dimension.flare.data.model.BottomBarStyle import dev.dimension.flare.data.model.LocalAppearanceSettings +import dev.dimension.flare.ui.theme.segmentedShapes2 import soup.compose.material.motion.animation.materialElevationScaleIn import soup.compose.material.motion.animation.materialElevationScaleOut @@ -244,26 +245,26 @@ fun NavigationSuiteScaffold2( } if (layoutType == NavigationSuiteType.NavigationBar) { Column( - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { secondaryScope.itemList.forEachIndexed { index, it -> - ListItem( - headlineContent = { + SegmentedListItem( + onClick = { + it.onClick() + }, + shapes = + ListItemDefaults.segmentedShapes2( + index, + secondaryScope.itemsCount, + ), + content = { it.label?.invoke() }, leadingContent = it.icon, trailingContent = it.badge, modifier = it.modifier - .padding(horizontal = 16.dp) - .listCard( - index = index, - totalCount = secondaryScope.itemsCount, - ).clickable( - enabled = it.enabled, - onClick = it.onClick, - interactionSource = it.interactionSource, - ), + .padding(horizontal = 16.dp), ) } } @@ -286,26 +287,26 @@ fun NavigationSuiteScaffold2( Spacer(modifier = Modifier.weight(1f)) if (layoutType == NavigationSuiteType.NavigationBar) { Column( - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { footerScope.itemList.forEachIndexed { index, it -> - ListItem( - headlineContent = { + SegmentedListItem( + content = { it.label?.invoke() }, + onClick = { + it.onClick() + }, + shapes = + ListItemDefaults.segmentedShapes2( + index, + footerScope.itemsCount, + ), leadingContent = it.icon, trailingContent = it.badge, modifier = it.modifier - .padding(horizontal = 16.dp) - .listCard( - index = index, - totalCount = footerScope.itemsCount, - ).clickable( - enabled = it.enabled, - onClick = it.onClick, - interactionSource = it.interactionSource, - ), + .padding(horizontal = 16.dp), ) } } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/dm/DMListScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/dm/DMListScreen.kt index 0f0f22ec0..164b38690 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/dm/DMListScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/dm/DMListScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -97,7 +98,7 @@ internal fun DMListScreen( modifier = Modifier .padding(horizontal = screenHorizontalPadding), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { dmList( data = state.items, diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/home/AccountSelectionModal.kt b/app/src/main/java/dev/dimension/flare/ui/screen/home/AccountSelectionModal.kt index b0a1be228..8baba9bb9 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/home/AccountSelectionModal.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/home/AccountSelectionModal.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -14,13 +15,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import dev.dimension.flare.R -import dev.dimension.flare.ui.component.listCard import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.presenter.settings.AccountsPresenter import dev.dimension.flare.ui.route.Route import dev.dimension.flare.ui.screen.settings.AccountItem import dev.dimension.flare.ui.theme.screenHorizontalPadding +import dev.dimension.flare.ui.theme.segmentedShapes2 import moe.tlaster.precompose.molecule.producePresenter @Composable @@ -37,18 +38,13 @@ internal fun AccountSelectionModal( val state by producePresenter { presenter() } state.accounts.onSuccess { Column( - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { for (index in 0 until it.size) { val (accountKey, data) = it[index] AccountItem( - modifier = - Modifier - .listCard( - index = index, - totalCount = it.size, - ), userState = data, + shapes = ListItemDefaults.segmentedShapes2(index, it.size), onClick = { state.setActiveAccount(it) onBack.invoke() diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt index e42e51bd3..baffa852c 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt @@ -38,12 +38,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.retain.retain import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -114,32 +114,30 @@ internal fun HomeScreen(afterInit: () -> Unit) { val wideNavigationRailState = rememberWideNavigationRailState() state.tabs .onSuccess { tabs -> - val topLevelBackStack by producePresenter( - key = "home_top_level_back_stack_${tabs.all.first().key}", - useImmediateClock = true, - ) { - TopLevelBackStack( - getDirection(tabs.all.first()), - ) - } - - fun navigate(route: Route) { - topLevelBackStack.addTopLevel(route) - scope.launch { - wideNavigationRailState.collapse() + val topLevelBackStack = + retain( + "home_top_level_back_stack_${tabs.all.first().key}", + ) { + TopLevelBackStack( + getDirection(tabs.all.first()), + ) } - } - val currentRoute by remember { - derivedStateOf { - topLevelBackStack.topLevelKey - } - } - val accountType by remember { - derivedStateOf { - currentRoute.accountTypeOr(AccountType.Active) + val topLevelBackStackState by rememberUpdatedState(topLevelBackStack) + + val navigate = + remember(topLevelBackStack, wideNavigationRailState) { + { route: Route -> + topLevelBackStack.addTopLevel(route) + scope.launch { + wideNavigationRailState.collapse() + } + Unit + } } - } + + val currentRoute = topLevelBackStack.topLevelKey + val accountType = currentRoute.accountTypeOr(AccountType.Active) val userState by producePresenter(key = "home_account_type_$accountType") { userPresenter(accountType) } @@ -177,7 +175,7 @@ internal fun HomeScreen(afterInit: () -> Unit) { userState, layoutType, currentRoute, - ::navigate, + navigate, ) }, navigationSuiteItems = { @@ -300,10 +298,10 @@ internal fun HomeScreen(afterInit: () -> Unit) { } }, navigate = { - topLevelBackStack.add(it) + topLevelBackStackState.add(it) }, onBack = { - topLevelBackStack.removeLast() + topLevelBackStackState.removeLast() }, ) } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/home/TabSettingScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/home/TabSettingScreen.kt index 345b910bb..d93c78d51 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/home/TabSettingScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/home/TabSettingScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -51,6 +52,7 @@ import dev.dimension.flare.ui.screen.settings.EditTabDialog import dev.dimension.flare.ui.screen.settings.TabAddBottomSheet import dev.dimension.flare.ui.screen.settings.TabCustomItem import dev.dimension.flare.ui.theme.screenHorizontalPadding +import dev.dimension.flare.ui.theme.segmentedShapes2 import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map @@ -129,7 +131,7 @@ internal fun TabSettingScreen( modifier = Modifier .padding(horizontal = screenHorizontalPadding), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { state.enableMixedTimeline.onSuccess { enabled -> if (state.currentTabs.size > 1) { @@ -166,6 +168,7 @@ internal fun TabSettingScreen( itemsIndexed(state.currentTabs, key = { _, item -> item.key }) { index, item -> TabCustomItem( item = item, + shapes = ListItemDefaults.segmentedShapes2(index, state.currentTabs.size), deleteTab = { if (it is TimelineTabItem) { state.deleteTab(item) @@ -178,12 +181,6 @@ internal fun TabSettingScreen( }, reorderableLazyColumnState = reorderableLazyColumnState, canSwipeToDelete = state.canSwipeToDelete, - modifier = - Modifier - .listCard( - index = index, - totalCount = state.currentTabs.size, - ), ) } } @@ -243,7 +240,7 @@ private fun presenter( object { val currentTabs = cacheTabs val allTabsState = allTabsState - val canSwipeToDelete = cacheTabs.size > 1 + val canSwipeToDelete = true val showAddTab = showAddTab val selectedEditTab = selectedEditTab val enableMixedTimeline = enableMixedTimeline @@ -285,16 +282,10 @@ private fun presenter( } fun deleteTab(tab: TimelineTabItem) { - if (cacheTabs.size <= 1) { - return - } cacheTabs.removeIf { it.key == tab.key } } fun deleteTab(key: String) { - if (cacheTabs.size <= 1) { - return - } cacheTabs.removeIf { it.key == key } } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/list/ListScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/list/ListScreen.kt index 235fce70a..ab4f501a5 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/list/ListScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/list/ListScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -109,7 +110,7 @@ internal fun ListScreen( modifier = Modifier .padding(horizontal = screenHorizontalPadding), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { uiListWithTabs( state = state, diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/rss/RssSourcesScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/rss/RssSourcesScreen.kt index ca5f9559a..c13c065d9 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/rss/RssSourcesScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/rss/RssSourcesScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -21,7 +22,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import compose.icons.FontAwesomeIcons import compose.icons.fontawesomeicons.Solid import compose.icons.fontawesomeicons.solid.FileExport @@ -105,7 +105,7 @@ internal fun RssSourcesScreen( modifier = Modifier .padding(horizontal = screenHorizontalPadding), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), contentPadding = contentPadding, ) { rssListWithTabs( diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AccountsScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AccountsScreen.kt index c77858de7..d92b7cb9d 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AccountsScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AccountsScreen.kt @@ -4,15 +4,20 @@ import androidx.compose.animation.core.AnimationConstants import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.IconButton import androidx.compose.material3.ListItemColors import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.ListItemElevation import androidx.compose.material3.ListItemShapes import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton @@ -26,7 +31,9 @@ import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -35,8 +42,10 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import compose.icons.FontAwesomeIcons import compose.icons.fontawesomeicons.Solid +import compose.icons.fontawesomeicons.solid.EllipsisVertical import compose.icons.fontawesomeicons.solid.FaceSadTear import compose.icons.fontawesomeicons.solid.Plus +import compose.icons.fontawesomeicons.solid.Trash import dev.dimension.flare.R import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -58,6 +67,7 @@ import dev.dimension.flare.ui.model.onLoading import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.theme.screenHorizontalPadding +import dev.dimension.flare.ui.theme.segmentedShapes2 import io.github.fornewid.placeholder.material3.placeholder import kotlinx.coroutines.delay import moe.tlaster.precompose.molecule.producePresenter @@ -104,30 +114,15 @@ internal fun AccountsScreen( .padding(horizontal = screenHorizontalPadding), verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { - when (val accountState = state.accounts) { - // TODO: show error - is UiState.Error -> Unit - is UiState.Loading -> { - items(3) { - AccountItem( - userState = UiState.Loading(), - onClick = {}, - toLogin = {}, - shapes = ListItemDefaults.segmentedShapes(it, 3), - ) - } - } - - is UiState.Success -> { - items(accountState.data.size, key = { index -> - accountState.data[index] - .first.accountKey - .toString() - }) { index -> - val (account, data) = accountState.data[index] + state.accounts + .onSuccess { accountState -> + itemsIndexed(accountState.toImmutableList()) { index, (account, data) -> val swipeState = rememberSwipeToDismissBoxState() - + val shape = ListItemDefaults.segmentedShapes2(index, accountState.size) + var showMenu by remember { mutableStateOf(false) } + val isSwiping = + swipeState.dismissDirection != SwipeToDismissBoxValue.Settled LaunchedEffect(swipeState.settledValue) { if (swipeState.settledValue != SwipeToDismissBoxValue.Settled) { delay(AnimationConstants.DefaultDurationMillis.toLong()) @@ -142,7 +137,7 @@ internal fun AccountsScreen( modifier = Modifier .fillMaxSize() - .background(color = MaterialTheme.colorScheme.error) + .background(color = MaterialTheme.colorScheme.error, shape = shape.draggedShape) .padding(16.dp), contentAlignment = Alignment.CenterEnd, ) { @@ -157,28 +152,80 @@ internal fun AccountsScreen( enableDismissFromEndToStart = data.isSuccess || data.isError, ) { AccountItem( - modifier = Modifier, + selected = isSwiping, userState = data, - shapes = ListItemDefaults.segmentedShapes(index, accountState.data.size), + shapes = shape, onClick = { state.setActiveAccount(it) }, + onLongClick = { + showMenu = true + }, toLogin = toLogin, trailingContent = { user -> - state.activeAccount.onSuccess { - RadioButton( - selected = it.accountKey == user.key, + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + state.activeAccount.onSuccess { + RadioButton( + selected = it.accountKey == user.key, + onClick = { + state.setActiveAccount(user.key) + }, + ) + } + IconButton( onClick = { - state.setActiveAccount(user.key) + showMenu = true }, - ) + ) { + FAIcon( + FontAwesomeIcons.Solid.EllipsisVertical, + contentDescription = stringResource(id = R.string.more), + ) + DropdownMenu( + expanded = showMenu, + onDismissRequest = { + showMenu = false + }, + ) { + DropdownMenuItem( + text = { + Text( + text = stringResource(id = R.string.settings_accounts_remove), + color = MaterialTheme.colorScheme.error, + ) + }, + onClick = { + showMenu = false + state.logout(account.accountKey) + }, + leadingIcon = { + FAIcon( + imageVector = FontAwesomeIcons.Solid.Trash, + contentDescription = stringResource(id = R.string.settings_accounts_remove), + tint = MaterialTheme.colorScheme.error, + ) + }, + ) + } + } } }, ) } } + }.onLoading { + items(3) { + AccountItem( + userState = UiState.Loading(), + onClick = {}, + toLogin = {}, + shapes = ListItemDefaults.segmentedShapes2(it, 3), + ) + } } - } } } } @@ -198,16 +245,24 @@ fun AccountItem( Text(text = it.handle, maxLines = 1) }, avatarSize: Dp = AvatarComponentDefaults.size, - colors: ListItemColors = ListItemDefaults.colors(), + colors: ListItemColors = ListItemDefaults.segmentedColors(), shapes: ListItemShapes = ListItemDefaults.shapes(), + elevation: ListItemElevation = ListItemDefaults.elevation(), + selected: Boolean = false, + onLongClick: (() -> Unit)? = null, + onLongClickLabel: String? = null, ) { userState .onSuccess { data -> SegmentedListItem( + selected = selected, + elevation = elevation, modifier = modifier, onClick = { onClick.invoke(data.key) }, + onLongClick = onLongClick, + onLongClickLabel = onLongClickLabel, shapes = shapes, content = { headlineContent.invoke(data) @@ -230,7 +285,11 @@ fun AccountItem( ) }.onLoading { SegmentedListItem( + selected = selected, onClick = {}, + onLongClick = onLongClick, + onLongClickLabel = onLongClickLabel, + elevation = elevation, shapes = shapes, content = { Text(text = "Loading...", modifier = Modifier.placeholder(true)) @@ -250,7 +309,11 @@ fun AccountItem( ) }.onError { throwable -> SegmentedListItem( + selected = selected, onClick = {}, + onLongClick = onLongClick, + onLongClickLabel = onLongClickLabel, + elevation = elevation, shapes = shapes, content = { if (throwable is LoginExpiredException) { diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppLoggingScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppLoggingScreen.kt index 3d6a1fc70..300a06561 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppLoggingScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppLoggingScreen.kt @@ -40,6 +40,7 @@ import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.presenter.settings.DevModePresenter import dev.dimension.flare.ui.screen.media.saveByteArrayToDownloads import dev.dimension.flare.ui.theme.screenHorizontalPadding +import dev.dimension.flare.ui.theme.segmentedShapes2 import dev.dimension.flare.ui.theme.single import moe.tlaster.precompose.molecule.producePresenter import kotlin.time.Clock @@ -135,7 +136,7 @@ internal fun AppLoggingScreen(onBack: () -> Unit) { onClick = { selectedMessage = it }, - shapes = ListItemDefaults.segmentedShapes(index, state.messages.size), + shapes = ListItemDefaults.segmentedShapes2(index, state.messages.size), content = { Text(it, maxLines = 3) }, diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceScreen.kt index b0a73624b..097351940 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/AppearanceScreen.kt @@ -72,7 +72,6 @@ import dev.dimension.flare.ui.presenter.settings.AppearanceState import dev.dimension.flare.ui.theme.first import dev.dimension.flare.ui.theme.item import dev.dimension.flare.ui.theme.last -import dev.dimension.flare.ui.theme.listCardItem import dev.dimension.flare.ui.theme.screenHorizontalPadding import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf @@ -371,7 +370,6 @@ internal fun AppearanceScreen( it, modifier = Modifier - .listCardItem() .background(MaterialTheme.colorScheme.surface), ) } @@ -608,7 +606,6 @@ internal fun AppearanceScreen( copy(videoAutoplay = it) } }, - modifier = Modifier.listCardItem(), shapes = ListItemDefaults.last(), ) } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/LocalFilterScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/LocalFilterScreen.kt index b5c178064..bfd01f8d4 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/LocalFilterScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/LocalFilterScreen.kt @@ -29,6 +29,7 @@ import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.presenter.settings.LocalFilterPresenter import dev.dimension.flare.ui.presenter.settings.LocalFilterState import dev.dimension.flare.ui.theme.screenHorizontalPadding +import dev.dimension.flare.ui.theme.segmentedShapes2 import moe.tlaster.precompose.molecule.producePresenter @OptIn(ExperimentalMaterial3Api::class) @@ -79,7 +80,7 @@ internal fun LocalFilterScreen( val item = list[index] SegmentedListItem( onClick = {}, - shapes = ListItemDefaults.segmentedShapes(index, list.size), + shapes = ListItemDefaults.segmentedShapes2(index, list.size), content = { Text(text = item.keyword) }, diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/TabCustomizeScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/TabCustomizeScreen.kt index ce85bb5a9..d6874613e 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/TabCustomizeScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/TabCustomizeScreen.kt @@ -23,8 +23,10 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.ListItemShapes import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface +import androidx.compose.material3.SegmentedListItem import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text @@ -65,13 +67,13 @@ import dev.dimension.flare.ui.component.FlareLargeFlexibleTopAppBar import dev.dimension.flare.ui.component.FlareScaffold import dev.dimension.flare.ui.component.TabIcon import dev.dimension.flare.ui.component.TabTitle -import dev.dimension.flare.ui.component.listCard import dev.dimension.flare.ui.model.UiProfile import dev.dimension.flare.ui.model.collectAsUiState import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.presenter.invoke import dev.dimension.flare.ui.presenter.list.PinnableTimelineTabPresenter import dev.dimension.flare.ui.theme.screenHorizontalPadding +import dev.dimension.flare.ui.theme.segmentedShapes2 import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope @@ -180,7 +182,7 @@ internal fun TabCustomizeScreen( modifier = Modifier .padding(horizontal = screenHorizontalPadding), - verticalArrangement = Arrangement.spacedBy(2.dp), + verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), ) { itemsIndexed( state.tabs, @@ -188,18 +190,13 @@ internal fun TabCustomizeScreen( ) { index, item -> TabCustomItem( item = item, + shapes = ListItemDefaults.segmentedShapes2(index, state.tabs.size), deleteTab = state::deleteTab, editTab = { state.setEditTab(it) }, reorderableLazyColumnState = reorderableLazyColumnState, canSwipeToDelete = state.canSwipeToDelete, - modifier = - Modifier - .listCard( - index = index, - totalCount = state.tabs.size, - ), ) } } @@ -263,6 +260,7 @@ internal fun LazyItemScope.TabCustomItem( reorderableLazyColumnState: ReorderableLazyListState, canSwipeToDelete: Boolean, modifier: Modifier = Modifier, + shapes: ListItemShapes = ListItemDefaults.shapes(), ) { val haptics = LocalHapticFeedback.current val swipeState = @@ -284,87 +282,88 @@ internal fun LazyItemScope.TabCustomItem( ) + fadeOut(), ) { val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp) - Surface( - shadowElevation = elevation, + val isSwiping = + swipeState.dismissDirection != SwipeToDismissBoxValue.Settled + SwipeToDismissBox( + state = swipeState, + enableDismissFromEndToStart = canSwipeToDelete, + enableDismissFromStartToEnd = canSwipeToDelete, + backgroundContent = { + val alignment = + when (swipeState.dismissDirection) { + SwipeToDismissBoxValue.StartToEnd -> Alignment.CenterStart + SwipeToDismissBoxValue.EndToStart -> Alignment.CenterEnd + SwipeToDismissBoxValue.Settled -> Alignment.Center + } + if (swipeState.dismissDirection != SwipeToDismissBoxValue.Settled) { + Box( + modifier = + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.error, shape = shapes.draggedShape) + .padding(16.dp), + ) { + FAIcon( + imageVector = FontAwesomeIcons.Solid.Trash, + contentDescription = stringResource(id = R.string.tab_settings_remove), + modifier = + Modifier + .align(alignment), + tint = MaterialTheme.colorScheme.onError, + ) + } + } + }, ) { - SwipeToDismissBox( - state = swipeState, - enableDismissFromEndToStart = canSwipeToDelete, - enableDismissFromStartToEnd = canSwipeToDelete, - backgroundContent = { - val alignment = - when (swipeState.dismissDirection) { - SwipeToDismissBoxValue.StartToEnd -> Alignment.CenterStart - SwipeToDismissBoxValue.EndToStart -> Alignment.CenterEnd - SwipeToDismissBoxValue.Settled -> Alignment.Center + SegmentedListItem( + elevation = ListItemDefaults.elevation(elevation), + selected = isDragging || isSwiping, + onClick = {}, + shapes = shapes, + content = { + TabTitle(item.metaData.title) + }, + leadingContent = { + TabIcon( + item, + ) + }, + trailingContent = { + Row { + IconButton( + onClick = { + editTab.invoke(item) + }, + ) { + FAIcon( + FontAwesomeIcons.Solid.Pen, + contentDescription = stringResource(id = R.string.tab_settings_edit), + ) } - if (swipeState.dismissDirection != SwipeToDismissBoxValue.Settled) { - Box( + IconButton( modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.error) - .padding(16.dp), + Modifier.draggableHandle( + onDragStarted = { + haptics.performHapticFeedback( + HapticFeedbackType.Confirm, + ) + }, + onDragStopped = { + haptics.performHapticFeedback( + HapticFeedbackType.Confirm, + ) + }, + ), + onClick = {}, ) { FAIcon( - imageVector = FontAwesomeIcons.Solid.Trash, - contentDescription = stringResource(id = R.string.tab_settings_remove), - modifier = - Modifier -// .size(24.dp) - .align(alignment), - tint = MaterialTheme.colorScheme.onError, + FontAwesomeIcons.Solid.Bars, + contentDescription = stringResource(id = R.string.tab_settings_drag), ) } } }, - ) { - ListItem( - headlineContent = { - TabTitle(item.metaData.title) - }, - leadingContent = { - TabIcon( - item, - ) - }, - trailingContent = { - Row { - IconButton( - onClick = { - editTab.invoke(item) - }, - ) { - FAIcon( - FontAwesomeIcons.Solid.Pen, - contentDescription = stringResource(id = R.string.tab_settings_edit), - ) - } - IconButton( - modifier = - Modifier.draggableHandle( - onDragStarted = { - haptics.performHapticFeedback( - HapticFeedbackType.Confirm, - ) - }, - onDragStopped = { - haptics.performHapticFeedback( - HapticFeedbackType.Confirm, - ) - }, - ), - onClick = {}, - ) { - FAIcon( - FontAwesomeIcons.Solid.Bars, - contentDescription = stringResource(id = R.string.tab_settings_drag), - ) - } - } - }, - ) - } + ) } } } diff --git a/compose-ui/src/androidMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.android.kt b/compose-ui/src/androidMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.android.kt index 2b3427707..9d03338d0 100644 --- a/compose-ui/src/androidMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.android.kt +++ b/compose-ui/src/androidMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.android.kt @@ -1,8 +1,11 @@ package dev.dimension.flare.ui.component.platform import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.SegmentedListItem import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import dev.dimension.flare.ui.theme.segmentedShapes2 @Composable internal actual fun PlatformListItem( @@ -20,3 +23,29 @@ internal actual fun PlatformListItem( trailingContent = trailingContent, ) } + +@Composable +internal actual fun PlatformSegmentedListItem( + headlineContent: @Composable (() -> Unit), + index: Int, + totalCount: Int, + modifier: Modifier, + leadingContent: @Composable (() -> Unit), + supportingContent: @Composable (() -> Unit), + trailingContent: @Composable (() -> Unit), + onClick: () -> Unit, + onLongClick: (() -> Unit)?, + onLongClickLabel: String?, +) { + SegmentedListItem( + content = headlineContent, + shapes = ListItemDefaults.segmentedShapes2(index, totalCount), + modifier = modifier, + leadingContent = leadingContent, + supportingContent = supportingContent, + trailingContent = trailingContent, + onClick = onClick, + onLongClick = onLongClick, + onLongClickLabel = onLongClickLabel, + ) +} diff --git a/compose-ui/src/androidMain/kotlin/dev/dimension/flare/ui/theme/PlatformShapes.android.kt b/compose-ui/src/androidMain/kotlin/dev/dimension/flare/ui/theme/PlatformShapes.android.kt index 128885bd9..83c93ba9a 100644 --- a/compose-ui/src/androidMain/kotlin/dev/dimension/flare/ui/theme/PlatformShapes.android.kt +++ b/compose-ui/src/androidMain/kotlin/dev/dimension/flare/ui/theme/PlatformShapes.android.kt @@ -1,6 +1,5 @@ package dev.dimension.flare.ui.theme -import android.os.Build import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -8,15 +7,7 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemShapes import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.Outline -import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.drawscope.clipPath -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -78,42 +69,6 @@ public object ListCardShapes { public fun bottomCard(): Shape = PlatformShapes.bottomCardShape } -@Composable -public fun Modifier.listCardContainer(): Modifier = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - this.clip(PlatformShapes.listCardContainerShape) - } else { - this.compatClip(PlatformShapes.listCardContainerShape) - } - -@Composable -public fun Modifier.listCardItem(): Modifier = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - this.clip(PlatformShapes.listCardItemShape) - } else { - this.compatClip(PlatformShapes.listCardItemShape) - } - -@Composable -private fun Modifier.compatClip(shape: Shape): Modifier { - val layoutDirection = LocalLayoutDirection.current - val density = LocalDensity.current - return this.drawWithContent { - val outline = shape.createOutline(size, layoutDirection, density) - - val clipPath = - when (outline) { - is Outline.Rectangle -> Path().apply { addRect(outline.rect) } - is Outline.Rounded -> Path().apply { addRoundRect(outline.roundRect) } - is Outline.Generic -> outline.path - } - - clipPath(path = clipPath) { - this@drawWithContent.drawContent() - } - } -} - @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable public fun ListItemDefaults.first(): ListItemShapes = ListItemDefaults.segmentedShapes(0, 2) @@ -126,6 +81,21 @@ public fun ListItemDefaults.item(): ListItemShapes = ListItemDefaults.segmentedS @Composable public fun ListItemDefaults.last(): ListItemShapes = ListItemDefaults.segmentedShapes(1, 2) +@Composable +public fun ListItemDefaults.segmentedShapes2( + index: Int, + count: Int, +): ListItemShapes { + if (count == 1) { + return ListItemDefaults.single() + } else { + return ListItemDefaults.segmentedShapes( + index = index, + count = count, + ) + } +} + @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable public fun ListItemDefaults.single(): ListItemShapes { diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt index a247cd357..20946ef8e 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/UiListItemComponent.kt @@ -28,6 +28,7 @@ import dev.dimension.flare.compose.ui.list_empty import dev.dimension.flare.compose.ui.list_error import dev.dimension.flare.ui.common.itemsIndexed import dev.dimension.flare.ui.component.platform.PlatformListItem +import dev.dimension.flare.ui.component.platform.PlatformSegmentedListItem import dev.dimension.flare.ui.component.platform.PlatformText import dev.dimension.flare.ui.component.platform.placeholder import dev.dimension.flare.ui.model.UiList @@ -96,12 +97,8 @@ public fun LazyListScope.uiListItemComponent( onClicked = onClicked, item = item, trailingContent = trailingContent, - modifier = - Modifier - .listCard( - index = index, - totalCount = itemCount, - ), + index = index, + totalCount = itemCount, ) } } @@ -111,24 +108,99 @@ public fun UiListItem( onClicked: ((UiList) -> Unit)?, item: UiList, trailingContent: @Composable (RowScope.(UiList) -> Unit), + index: Int, + totalCount: Int, modifier: Modifier = Modifier, ) { - Column( - modifier = - modifier - .background(PlatformTheme.colorScheme.card) - .let { - if (onClicked == null) { - it + if (item.description?.takeIf { it.isNotEmpty() } != null) { + Column( + modifier = + modifier + .listCard( + index = index, + totalCount = totalCount, + ).background(PlatformTheme.colorScheme.card) + .let { + if (onClicked == null) { + it + } else { + it + .clickable { + onClicked(item) + } + } + }, + ) { + PlatformListItem( + headlineContent = { + PlatformText(text = item.title) + }, + leadingContent = { + if (item.avatar != null) { + NetworkImage( + model = item.avatar, + contentDescription = item.title, + modifier = + Modifier + .size(AvatarComponentDefaults.size) + .clip(PlatformTheme.shapes.medium), + ) } else { - it - .clickable { - onClicked(item) - } + FAIcon( + imageVector = FontAwesomeIcons.Solid.Rss, + contentDescription = null, + modifier = + Modifier + .size(AvatarComponentDefaults.size) + .background( + color = PlatformTheme.colorScheme.primaryContainer, + shape = PlatformTheme.shapes.medium, + ).padding(8.dp), + tint = PlatformTheme.colorScheme.onPrimaryContainer, + ) + } + }, + supportingContent = { + if (item.creator != null) { + PlatformText( + text = + stringResource( + Res.string.feeds_discover_feeds_created_by, + item.creator?.handle ?: "Unknown", + ), + style = PlatformTheme.typography.caption, + color = PlatformTheme.colorScheme.caption, + ) + } + }, + trailingContent = { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + trailingContent.invoke(this, item) } }, - ) { - PlatformListItem( + ) + item.description?.takeIf { it.isNotEmpty() }?.let { + PlatformText( + text = it, + modifier = + Modifier + .background(PlatformTheme.colorScheme.card) + .fillMaxWidth() + .padding(bottom = 8.dp) + .padding(horizontal = screenHorizontalPadding), + ) + } + } + } else { + PlatformSegmentedListItem( + modifier = modifier, + index = index, + totalCount = totalCount, + onClick = { + onClicked?.invoke(item) + }, headlineContent = { PlatformText(text = item.title) }, @@ -178,17 +250,6 @@ public fun UiListItem( } }, ) - item.description?.takeIf { it.isNotEmpty() }?.let { - PlatformText( - text = it, - modifier = - Modifier - .background(PlatformTheme.colorScheme.card) - .fillMaxWidth() - .padding(bottom = 8.dp) - .padding(horizontal = screenHorizontalPadding), - ) - } } } diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/dm/DmListItem.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/dm/DmListItem.kt index dcdb56134..5e3fc5027 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/dm/DmListItem.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/dm/DmListItem.kt @@ -1,7 +1,6 @@ package dev.dimension.flare.ui.component.dm 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 @@ -35,7 +34,7 @@ import dev.dimension.flare.ui.component.FAIcon import dev.dimension.flare.ui.component.ItemPlaceHolder import dev.dimension.flare.ui.component.RichText import dev.dimension.flare.ui.component.listCard -import dev.dimension.flare.ui.component.platform.PlatformListItem +import dev.dimension.flare.ui.component.platform.PlatformSegmentedListItem import dev.dimension.flare.ui.component.platform.PlatformText import dev.dimension.flare.ui.model.UiDMRoom import dev.dimension.flare.ui.theme.PlatformTheme @@ -107,7 +106,12 @@ public fun LazyListScope.dmList( } }, itemContent = { index, itemCount, item -> - PlatformListItem( + PlatformSegmentedListItem( + onClick = { + onItemClicked.invoke(item.key) + }, + index = index, + totalCount = itemCount, headlineContent = { Row( horizontalArrangement = Arrangement.spacedBy(4.dp), @@ -223,14 +227,6 @@ public fun LazyListScope.dmList( ) } }, - modifier = - Modifier - .listCard( - index = index, - totalCount = itemCount, - ).clickable { - onItemClicked.invoke(item.key) - }, ) }, ) diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.kt index 5f18f6171..a49d44234 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.kt @@ -11,3 +11,17 @@ internal expect fun PlatformListItem( supportingContent: @Composable () -> Unit = {}, trailingContent: @Composable () -> Unit = {}, ) + +@Composable +internal expect fun PlatformSegmentedListItem( + headlineContent: @Composable () -> Unit, + index: Int, + totalCount: Int, + modifier: Modifier = Modifier, + leadingContent: @Composable () -> Unit = {}, + supportingContent: @Composable () -> Unit = {}, + trailingContent: @Composable () -> Unit = {}, + onClick: () -> Unit, + onLongClick: (() -> Unit)? = null, + onLongClickLabel: String? = null, +) diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedWithTabs.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedWithTabs.kt index d07495028..dd7ceefec 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedWithTabs.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedWithTabs.kt @@ -116,12 +116,8 @@ public fun LazyListScope.popularBlueskyFeedWithTabs( } } }, - modifier = - Modifier - .listCard( - index = index, - totalCount = itemCount, - ), + index = index, + totalCount = itemCount, ) } } diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/rss/RssListWithTabs.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/rss/RssListWithTabs.kt index 0cc8e7c69..8e8cfb415 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/rss/RssListWithTabs.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/rss/RssListWithTabs.kt @@ -1,7 +1,6 @@ package dev.dimension.flare.ui.screen.rss import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -33,11 +32,10 @@ import dev.dimension.flare.compose.ui.tab_settings_add import dev.dimension.flare.compose.ui.tab_settings_remove import dev.dimension.flare.ui.component.FAIcon import dev.dimension.flare.ui.component.NetworkImage -import dev.dimension.flare.ui.component.listCard import dev.dimension.flare.ui.component.platform.PlatformDropdownMenu import dev.dimension.flare.ui.component.platform.PlatformDropdownMenuItem import dev.dimension.flare.ui.component.platform.PlatformIconButton -import dev.dimension.flare.ui.component.platform.PlatformListItem +import dev.dimension.flare.ui.component.platform.PlatformSegmentedListItem import dev.dimension.flare.ui.component.platform.PlatformText import dev.dimension.flare.ui.model.UiRssSource import dev.dimension.flare.ui.model.onSuccess @@ -77,15 +75,12 @@ public fun LazyListScope.rssListWithTabs( itemsIndexed( state.sources, ) { index, it -> - PlatformListItem( - modifier = - Modifier - .listCard( - index = index, - totalCount = state.sources.size, - ).clickable { - onClicked.invoke(it) - }, + PlatformSegmentedListItem( + onClick = { + onClicked.invoke(it) + }, + index = index, + totalCount = state.sources.size, headlineContent = { it.title?.let { PlatformText(text = it) diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/settings/AboutScreenContent.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/settings/AboutScreenContent.kt index 0012c7acf..e53fee30e 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/settings/AboutScreenContent.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/settings/AboutScreenContent.kt @@ -2,7 +2,6 @@ package dev.dimension.flare.ui.screen.settings import androidx.compose.foundation.Image import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -41,10 +40,8 @@ import dev.dimension.flare.compose.ui.settings_about_source_code import dev.dimension.flare.compose.ui.settings_about_telegram import dev.dimension.flare.compose.ui.settings_about_telegram_description import dev.dimension.flare.compose.ui.settings_privacy_policy -import dev.dimension.flare.ui.component.listCardContainer -import dev.dimension.flare.ui.component.listCardItem import dev.dimension.flare.ui.component.platform.PlatformIcon -import dev.dimension.flare.ui.component.platform.PlatformListItem +import dev.dimension.flare.ui.component.platform.PlatformSegmentedListItem import dev.dimension.flare.ui.component.platform.PlatformText import dev.dimension.flare.ui.theme.PlatformTheme import org.jetbrains.compose.resources.painterResource @@ -97,12 +94,9 @@ public fun AboutScreenContent( ) Spacer(Modifier.height(16.dp)) Column( - modifier = - Modifier - .listCardContainer(), verticalArrangement = Arrangement.spacedBy(2.dp), ) { - PlatformListItem( + PlatformSegmentedListItem( headlineContent = { PlatformText(text = stringResource(resource = Res.string.settings_about_source_code)) }, @@ -111,12 +105,11 @@ public fun AboutScreenContent( text = "https://github.com/DimensionDev/Flare", ) }, - modifier = - Modifier - .listCardItem() - .clickable { - uriHandler.openUri("https://github.com/DimensionDev/Flare") - }, + onClick = { + uriHandler.openUri("https://github.com/DimensionDev/Flare") + }, + index = 0, + totalCount = 6, leadingContent = { PlatformIcon( imageVector = FontAwesomeIcons.Brands.Github, @@ -125,7 +118,7 @@ public fun AboutScreenContent( ) }, ) - PlatformListItem( + PlatformSegmentedListItem( headlineContent = { PlatformText(text = stringResource(resource = Res.string.settings_about_telegram)) }, @@ -134,12 +127,11 @@ public fun AboutScreenContent( text = stringResource(resource = Res.string.settings_about_telegram_description), ) }, - modifier = - Modifier - .listCardItem() - .clickable { - uriHandler.openUri("https://t.me/+0UtcP6_qcDoyOWE1") - }, + onClick = { + uriHandler.openUri("https://t.me/+0UtcP6_qcDoyOWE1") + }, + index = 1, + totalCount = 6, leadingContent = { PlatformIcon( imageVector = FontAwesomeIcons.Brands.Telegram, @@ -148,7 +140,7 @@ public fun AboutScreenContent( ) }, ) - PlatformListItem( + PlatformSegmentedListItem( headlineContent = { PlatformText(text = stringResource(resource = Res.string.settings_about_discord)) }, @@ -157,12 +149,11 @@ public fun AboutScreenContent( text = stringResource(resource = Res.string.settings_about_discord_description), ) }, - modifier = - Modifier - .listCardItem() - .clickable { - uriHandler.openUri("https://discord.gg/De9NhXBryT") - }, + onClick = { + uriHandler.openUri("https://discord.gg/De9NhXBryT") + }, + index = 2, + totalCount = 6, leadingContent = { PlatformIcon( imageVector = FontAwesomeIcons.Brands.Discord, @@ -171,7 +162,7 @@ public fun AboutScreenContent( ) }, ) - PlatformListItem( + PlatformSegmentedListItem( headlineContent = { PlatformText(text = stringResource(resource = Res.string.settings_about_line)) }, @@ -180,12 +171,11 @@ public fun AboutScreenContent( text = stringResource(resource = Res.string.settings_about_line_description), ) }, - modifier = - Modifier - .listCardItem() - .clickable { - uriHandler.openUri("https://line.me/ti/g/hf95HyGJ9k") - }, + onClick = { + uriHandler.openUri("https://line.me/ti/g/hf95HyGJ9k") + }, + index = 3, + totalCount = 6, leadingContent = { PlatformIcon( imageVector = FontAwesomeIcons.Brands.Line, @@ -194,7 +184,7 @@ public fun AboutScreenContent( ) }, ) - PlatformListItem( + PlatformSegmentedListItem( headlineContent = { PlatformText(text = stringResource(resource = Res.string.settings_about_localization)) }, @@ -203,12 +193,11 @@ public fun AboutScreenContent( text = stringResource(resource = Res.string.settings_about_localization_description), ) }, - modifier = - Modifier - .listCardItem() - .clickable { - uriHandler.openUri("https://crowdin.com/project/flareapp") - }, + onClick = { + uriHandler.openUri("https://crowdin.com/project/flareapp") + }, + index = 4, + totalCount = 6, leadingContent = { PlatformIcon( imageVector = FontAwesomeIcons.Solid.Language, @@ -217,7 +206,7 @@ public fun AboutScreenContent( ) }, ) - PlatformListItem( + PlatformSegmentedListItem( headlineContent = { PlatformText(text = stringResource(resource = Res.string.settings_privacy_policy)) }, @@ -226,12 +215,11 @@ public fun AboutScreenContent( text = "https://legal.mask.io/maskbook", ) }, - modifier = - Modifier - .listCardItem() - .clickable { - uriHandler.openUri("https://legal.mask.io/maskbook/") - }, + onClick = { + uriHandler.openUri("https://legal.mask.io/maskbook/") + }, + index = 5, + totalCount = 6, leadingContent = { PlatformIcon( imageVector = FontAwesomeIcons.Solid.Lock, diff --git a/compose-ui/src/iosMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.ios.kt b/compose-ui/src/iosMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.ios.kt index 78d825729..492a55478 100644 --- a/compose-ui/src/iosMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.ios.kt +++ b/compose-ui/src/iosMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.ios.kt @@ -1,6 +1,7 @@ package dev.dimension.flare.ui.component.platform import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -9,6 +10,7 @@ import androidx.compose.ui.unit.dp import com.slapps.cupertino.LocalContentColor import com.slapps.cupertino.ProvideTextStyle import com.slapps.cupertino.theme.CupertinoTheme +import dev.dimension.flare.ui.component.listCard import dev.dimension.flare.ui.component.status.ListComponent import dev.dimension.flare.ui.theme.PlatformTheme @@ -43,3 +45,35 @@ internal actual fun PlatformListItem( }, ) } + +@Composable +internal actual fun PlatformSegmentedListItem( + headlineContent: @Composable (() -> Unit), + index: Int, + totalCount: Int, + modifier: Modifier, + leadingContent: @Composable (() -> Unit), + supportingContent: @Composable (() -> Unit), + trailingContent: @Composable (() -> Unit), + onClick: () -> Unit, + onLongClick: (() -> Unit)?, + onLongClickLabel: String?, +) { + // iOS does not have a native segmented list item, so we use the regular list item + PlatformListItem( + headlineContent = headlineContent, + modifier = + modifier + .listCard( + index = index, + totalCount = totalCount, + ).combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + onLongClickLabel = onLongClickLabel, + ), + leadingContent = leadingContent, + supportingContent = supportingContent, + trailingContent = trailingContent, + ) +} diff --git a/compose-ui/src/jvmMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.jvm.kt b/compose-ui/src/jvmMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.jvm.kt index 2590875d2..6af8bb051 100644 --- a/compose-ui/src/jvmMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.jvm.kt +++ b/compose-ui/src/jvmMain/kotlin/dev/dimension/flare/ui/component/platform/PlatformListItem.jvm.kt @@ -1,10 +1,12 @@ package dev.dimension.flare.ui.component.platform import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import dev.dimension.flare.ui.component.listCard import dev.dimension.flare.ui.component.status.ListComponent import dev.dimension.flare.ui.theme.PlatformTheme @@ -35,3 +37,35 @@ internal actual fun PlatformListItem( }, ) } + +@Composable +internal actual fun PlatformSegmentedListItem( + headlineContent: @Composable (() -> Unit), + index: Int, + totalCount: Int, + modifier: Modifier, + leadingContent: @Composable (() -> Unit), + supportingContent: @Composable (() -> Unit), + trailingContent: @Composable (() -> Unit), + onClick: () -> Unit, + onLongClick: (() -> Unit)?, + onLongClickLabel: String?, +) { + // JVM does not have a native segmented list item, so we use the regular list item + PlatformListItem( + headlineContent = headlineContent, + modifier = + modifier + .listCard( + index = index, + totalCount = totalCount, + ).combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + onLongClickLabel = onLongClickLabel, + ), + leadingContent = leadingContent, + supportingContent = supportingContent, + trailingContent = trailingContent, + ) +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/UserPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/UserPresenter.kt index c62aec941..f36dd9ed5 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/UserPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/UserPresenter.kt @@ -1,19 +1,23 @@ package dev.dimension.flare.ui.presenter.home import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import dev.dimension.flare.common.collectAsState +import androidx.compose.runtime.getValue import dev.dimension.flare.data.datasource.microblog.AuthenticatedMicroblogDataSource import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.NoActiveAccountException -import dev.dimension.flare.data.repository.accountServiceProvider +import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.UiUserV2 -import dev.dimension.flare.ui.model.flatMap +import dev.dimension.flare.ui.model.flattenUiState import dev.dimension.flare.ui.model.map import dev.dimension.flare.ui.model.toUi import dev.dimension.flare.ui.presenter.PresenterBase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -24,25 +28,32 @@ public class UserPresenter( KoinComponent { private val accountRepository: AccountRepository by inject() - @Composable - override fun body(): UserState { - val user = - accountServiceProvider(accountType = accountType, repository = accountRepository).flatMap { service -> + @OptIn(ExperimentalCoroutinesApi::class) + private val dataFlow by lazy { + accountServiceFlow(accountType, accountRepository) + .flatMapLatest { service -> val userId = userKey?.id ?: if (service is AuthenticatedMicroblogDataSource) { service.accountKey.id } else { null - } ?: throw NoActiveAccountException - remember(service, userKey) { - service.userById(userId) - }.collectAsState() - .toUi() - .map { - it as UiUserV2 - } + } + if (userId == null) { + flowOf(UiState.Error(NoActiveAccountException)) + } else { + service.userById(userId).toUi() + } + }.map { + it.map { + it as UiUserV2 + } } + } + + @Composable + override fun body(): UserState { + val user by dataFlow.flattenUiState() return object : UserState { override val user = user From 43c5041c21405f911536597c5d03f5422dac6d1d Mon Sep 17 00:00:00 2001 From: Tlaster Date: Mon, 5 Jan 2026 16:26:44 +0900 Subject: [PATCH 3/3] fix build --- compose-ui/build.gradle.kts | 8 +- .../flare/ui/screen/list/UiListWithTabs.kt | 89 ++++++++++--------- 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/compose-ui/build.gradle.kts b/compose-ui/build.gradle.kts index 43be3e2a0..8fd672f87 100644 --- a/compose-ui/build.gradle.kts +++ b/compose-ui/build.gradle.kts @@ -36,11 +36,6 @@ kotlin { } sourceSets { - all { - languageSettings { - optIn("androidx.compose.material3.ExperimentalMaterial3ExpressiveApi") - } - } val commonMain by getting { dependencies { implementation(projects.shared) @@ -72,6 +67,9 @@ kotlin { } } val androidMain by getting { + languageSettings { + optIn("androidx.compose.material3.ExperimentalMaterial3ExpressiveApi") + } dependencies { implementation(libs.compose.placeholder.material3) implementation(libs.material3.adaptive) diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/list/UiListWithTabs.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/list/UiListWithTabs.kt index 36446ea0c..546278536 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/list/UiListWithTabs.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/list/UiListWithTabs.kt @@ -1,6 +1,7 @@ package dev.dimension.flare.ui.screen.list import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -77,50 +78,52 @@ public fun LazyListScope.uiListWithTabs( mutableStateOf(false) } PlatformIconButton(onClick = { showDropdown = true }) { - FAIcon( - imageVector = FontAwesomeIcons.Solid.EllipsisVertical, - contentDescription = stringResource(Res.string.more), - ) - PlatformDropdownMenu( - expanded = showDropdown, - onDismissRequest = { showDropdown = false }, - ) { - PlatformDropdownMenuItem( - text = { - PlatformText( - text = stringResource(Res.string.list_edit), - ) - }, - onClick = { - editList(item) - showDropdown = false - }, - leadingIcon = { - FAIcon( - imageVector = FontAwesomeIcons.Solid.Pen, - contentDescription = stringResource(Res.string.list_edit), - ) - }, - ) - PlatformDropdownMenuItem( - text = { - PlatformText( - text = stringResource(Res.string.list_delete), - color = PlatformTheme.colorScheme.error, - ) - }, - onClick = { - deleteList(item) - showDropdown = false - }, - leadingIcon = { - FAIcon( - imageVector = FontAwesomeIcons.Solid.Trash, - contentDescription = stringResource(Res.string.list_delete), - tint = PlatformTheme.colorScheme.error, - ) - }, + Box { + FAIcon( + imageVector = FontAwesomeIcons.Solid.EllipsisVertical, + contentDescription = stringResource(Res.string.more), ) + PlatformDropdownMenu( + expanded = showDropdown, + onDismissRequest = { showDropdown = false }, + ) { + PlatformDropdownMenuItem( + text = { + PlatformText( + text = stringResource(Res.string.list_edit), + ) + }, + onClick = { + editList(item) + showDropdown = false + }, + leadingIcon = { + FAIcon( + imageVector = FontAwesomeIcons.Solid.Pen, + contentDescription = stringResource(Res.string.list_edit), + ) + }, + ) + PlatformDropdownMenuItem( + text = { + PlatformText( + text = stringResource(Res.string.list_delete), + color = PlatformTheme.colorScheme.error, + ) + }, + onClick = { + deleteList(item) + showDropdown = false + }, + leadingIcon = { + FAIcon( + imageVector = FontAwesomeIcons.Solid.Trash, + contentDescription = stringResource(Res.string.list_delete), + tint = PlatformTheme.colorScheme.error, + ) + }, + ) + } } } }