diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 934e4849cf6..e720ad5decd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,6 +69,11 @@ android { isMinifyEnabled = true proguardFiles("proguard-rules.pro") } + create("play") { + applicationIdSuffix = ".play" + isMinifyEnabled = true + proguardFiles("proguard-rules.pro") + } } flavorDimensions += "product" @@ -134,18 +139,18 @@ licensee { dependencies { implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.core:core-splashscreen:1.0.1") - implementation("androidx.activity:activity-compose:1.9.1") - implementation(platform("androidx.compose:compose-bom:2024.08.00")) + implementation("androidx.activity:activity-compose:1.9.2") + implementation(platform("androidx.compose:compose-bom:2024.09.00")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-util") debugImplementation("androidx.compose.ui:ui-tooling") implementation("androidx.compose.animation:animation") - implementation("androidx.compose.material:material-icons-core-android:1.6.8") - implementation("androidx.compose.material3:material3:1.3.0-rc01") + implementation("androidx.compose.material:material-icons-core-android") + implementation("androidx.compose.material3:material3") implementation("androidx.compose.material3:material3-window-size-class") - implementation("androidx.navigation:navigation-compose:2.8.0-rc01") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4") + implementation("androidx.navigation:navigation-compose:2.8.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.2") implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7") diff --git a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/HomeTopBar.kt b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/HomeTopBar.kt index cd1c1021216..9c2b6afae38 100644 --- a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/HomeTopBar.kt +++ b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/HomeTopBar.kt @@ -41,10 +41,8 @@ fun HomeTopBar( ) { val (isSearchExpanded, isExpandedScreen, searchTerm, searchMode, searchedIconInfoModel, isIconPicker) = uiState - val condition = isSearchExpanded || isExpandedScreen - val offset = animateDpAsState( - targetValue = if (condition) { + targetValue = if (isSearchExpanded || isExpandedScreen) { 0.dp } else { (-100).dp @@ -59,7 +57,7 @@ fun HomeTopBar( }, searchTerm = searchTerm, onClearSearch = onClearSearch, - onChangeMode = onChangeMode, + onModeChange = onChangeMode, onSearch = onSearchIcons, iconInfoModel = it, onNavigate = onNavigate, @@ -79,7 +77,7 @@ private fun SearchBar( searchTerm: String, onSearch: (String) -> Unit, onClearSearch: () -> Unit, - onChangeMode: (SearchMode) -> Unit, + onModeChange: (SearchMode) -> Unit, onNavigate: () -> Unit, isExpandedScreen: Boolean, isIconPicker: Boolean, @@ -97,15 +95,9 @@ private fun SearchBar( LawniconsSearchBar( query = searchTerm, isQueryEmpty = searchTerm == "", - onClear = { - onClearSearch() - }, - onBack = { - onFocusChange() - }, - onQueryChange = { newValue -> - onSearch(newValue) - }, + onClear = onClearSearch, + onBack = onFocusChange, + onQueryChange = onSearch, iconInfoModel = iconInfoModel, onNavigate = onNavigate, isExpandedScreen = isExpandedScreen, @@ -114,13 +106,9 @@ private fun SearchBar( SearchContents( searchTerm = searchTerm, searchMode = searchMode, - onModeChange = { mode -> - onChangeMode(mode) - }, + onModeChange = onModeChange, iconInfo = iconInfoModel.iconInfo, - onSendResult = { - onSendResult(it) - }, + onSendResult = onSendResult, ) }, inputFieldModifier = inputFieldModifier, diff --git a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconInfoSheet.kt b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconInfoSheet.kt index e48e807a60d..c79a5685d7d 100644 --- a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconInfoSheet.kt +++ b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconInfoSheet.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode @@ -58,7 +59,7 @@ fun IconInfoSheet( skipPartiallyExpanded = true, ) - val groupedComponents = remember { + val groupedComponents = rememberSaveable { iconInfo.componentNames .groupBy { it.label } .map { (label, components) -> @@ -71,7 +72,7 @@ fun IconInfoSheet( newValue = "", ) - val shareContents = remember { getShareContents(githubName, groupedComponents) } + val shareContents = rememberSaveable { getShareContents(githubName, groupedComponents) } ModalBottomSheet( onDismissRequest = { diff --git a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconPreview.kt b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconPreview.kt index 213ae98691b..8847e7801de 100644 --- a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconPreview.kt +++ b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconPreview.kt @@ -33,11 +33,12 @@ import app.lawnchair.lawnicons.ui.util.PreviewLawnicons import app.lawnchair.lawnicons.ui.util.SampleData import kotlin.math.ln -private fun ColorScheme.iconColor(): Color { - val elevation = 3.dp - val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f - return primary.copy(alpha = alpha).compositeOver(surface) -} +val ColorScheme.iconColor: Color + get() { + val elevation = 3.dp + val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f + return primary.copy(alpha = alpha).compositeOver(surface) + } @Composable fun IconPreview( @@ -68,7 +69,7 @@ fun IconPreview( color = iconBackground ?: if (isIconInfoShown.value) { MaterialTheme.colorScheme.surfaceVariant } else { - MaterialTheme.colorScheme.iconColor() + MaterialTheme.colorScheme.iconColor }, ), ) { diff --git a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconPreviewGrid.kt b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconPreviewGrid.kt index 92c81d274b2..8dd5edfc33e 100644 --- a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconPreviewGrid.kt +++ b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconPreviewGrid.kt @@ -1,6 +1,7 @@ package app.lawnchair.lawnicons.ui.components.home import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi @@ -9,13 +10,14 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding @@ -27,15 +29,21 @@ import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -48,7 +56,7 @@ import app.lawnchair.lawnicons.ui.util.SampleData import app.lawnchair.lawnicons.ui.util.toPaddingValues import app.lawnchair.lawnicons.util.appIcon import kotlinx.collections.immutable.ImmutableList -import my.nanihadesuka.compose.LazyVerticalGridScrollbar +import my.nanihadesuka.compose.InternalLazyVerticalGridScrollbar import my.nanihadesuka.compose.ScrollbarSelectionMode import my.nanihadesuka.compose.ScrollbarSettings @@ -63,34 +71,31 @@ fun IconPreviewGrid( contentPadding: PaddingValues? = null, gridState: LazyGridState = rememberLazyGridState(), ) { - val indexOfFirstItem = remember { derivedStateOf { gridState.firstVisibleItemIndex } } - val letter = iconInfo[indexOfFirstItem.value].label[0].uppercase() + val indexOfFirstItem by remember { derivedStateOf { gridState.firstVisibleItemIndex } } + val letter = iconInfo[indexOfFirstItem].label[0].uppercase() + var thumbSelected by remember { mutableStateOf(false) } Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = modifier.fillMaxWidth(), ) { - LazyVerticalGridScrollbar( + val horizontalGridPadding = if (isExpandedScreen) 32.dp else 8.dp + Box( modifier = Modifier .widthIn(max = 640.dp) .fillMaxWidth() .statusBarsPadding() .then( - if (isExpandedScreen) Modifier.padding(top = 26.dp) else Modifier.padding(bottom = 80.dp), + if (isExpandedScreen) { + Modifier.padding(top = 26.dp) + } else { + Modifier.padding( + bottom = 80.dp, + ) + }, ), - state = gridState, - settings = ScrollbarSettings( - alwaysShowScrollbar = true, - thumbUnselectedColor = MaterialTheme.colorScheme.primaryContainer, - thumbSelectedColor = MaterialTheme.colorScheme.primary, - selectionMode = ScrollbarSelectionMode.Thumb, - ), - indicatorContent = { _, isThumbSelected -> - ScrollbarIndicator(letter, isThumbSelected) - }, ) { - val horizontalGridPadding = if (isExpandedScreen) 32.dp else 8.dp LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 80.dp), contentPadding = contentPadding ?: WindowInsets.navigationBars.toPaddingValues( @@ -106,26 +111,63 @@ fun IconPreviewGrid( GridItemSpan(maxLineSpan) }, ) { - TopAppBar() + AppBarListItem() + } + items( + items = iconInfo, + contentType = { "icon_preview" }, + ) { iconInfo -> + val scale by animateFloatAsState( + if (thumbSelected && iconInfo.label.first() + .toString() == letter + ) { + 1.1f + } else { + 1f + }, + label = "", + ) + IconPreview( + modifier = Modifier + .scale(scale), + iconInfo = iconInfo, + isIconPicker = isIconPicker, + onSendResult = onSendResult, + ) } } - items( - items = iconInfo, - contentType = { "icon_preview" }, - ) { iconInfo -> - IconPreview( - iconInfo = iconInfo, - isIconPicker = isIconPicker, - onSendResult = onSendResult, - ) - } + } + Box( + contentAlignment = Alignment.CenterEnd, + ) { + Spacer( + Modifier + .fillMaxHeight() + .width(8.dp) + .background(MaterialTheme.colorScheme.surfaceContainer) + .clip(CircleShape), + ) + InternalLazyVerticalGridScrollbar( + modifier = Modifier.offset(7.dp), + state = gridState, + settings = ScrollbarSettings( + alwaysShowScrollbar = true, + thumbUnselectedColor = MaterialTheme.colorScheme.primary, + thumbSelectedColor = MaterialTheme.colorScheme.primary, + selectionMode = ScrollbarSelectionMode.Thumb, + ), + indicatorContent = { _, isThumbSelected -> + thumbSelected = isThumbSelected + ScrollbarIndicator(letter, isThumbSelected) + }, + ) } } } } @Composable -private fun ColumnScope.ScrollbarIndicator( +private fun ScrollbarIndicator( label: String, isThumbSelected: Boolean, ) { @@ -154,7 +196,7 @@ private fun ColumnScope.ScrollbarIndicator( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TopAppBar(modifier: Modifier = Modifier) { +private fun AppBarListItem(modifier: Modifier = Modifier) { val context = LocalContext.current CenterAlignedTopAppBar( modifier = modifier, diff --git a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconRequestFAB.kt b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconRequestFAB.kt index 8e3cdee83b1..2106ec0cd94 100644 --- a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconRequestFAB.kt +++ b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/IconRequestFAB.kt @@ -37,6 +37,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -130,7 +131,7 @@ fun RequestHandler( val encodedRequestList = buildForm(requestList.replace("\n", "%20")) val directLinkEnabled = encodedRequestList.length < Constants.DIRECT_LINK_MAX_LENGTH - var sheetExpanded by remember { mutableStateOf(false) } + var sheetExpanded by rememberSaveable { mutableStateOf(false) } val sheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true, ) @@ -307,7 +308,7 @@ private fun openSnackbarFirstLaunchContent( val result = snackbarHostState .showSnackbar( message = context.getString(R.string.snackbar_request_icons_hint), - duration = SnackbarDuration.Long, + duration = SnackbarDuration.Short, ) if (result == SnackbarResult.Dismissed) { onActionPerformed() diff --git a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/OverflowMenu.kt b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/OverflowMenu.kt deleted file mode 100644 index 99caf61734b..00000000000 --- a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/OverflowMenu.kt +++ /dev/null @@ -1,58 +0,0 @@ -package app.lawnchair.lawnicons.ui.components.home - -import androidx.compose.foundation.layout.Box -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.MoreVert -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp - -@Composable -fun OverflowMenu( - modifier: Modifier = Modifier, - block: @Composable OverflowMenuScope.() -> Unit, -) { - val showMenu = remember { mutableStateOf(false) } - val overflowMenuScope = remember { OverflowMenuScopeImpl(showMenu) } - - Box( - modifier = modifier, - ) { - ClickableIcon( - imageVector = Icons.Rounded.MoreVert, - size = 52.dp, - onClick = { showMenu.value = true }, - ) - DropdownMenu( - expanded = showMenu.value, - onDismissRequest = { showMenu.value = false }, - offset = DpOffset(x = 16.dp, y = (-36).dp), - ) { - CompositionLocalProvider( - LocalContentColor - provides MaterialTheme.colorScheme.onSurface, - ) { - block(overflowMenuScope) - } - } - } -} - -interface OverflowMenuScope { - fun hideMenu() -} - -private class OverflowMenuScopeImpl(private val showState: MutableState) : - OverflowMenuScope { - override fun hideMenu() { - showState.value = false - } -} diff --git a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/search/SearchBar.kt b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/search/SearchBar.kt index 435cd245b92..c2e1d4ac3d5 100644 --- a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/search/SearchBar.kt +++ b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/search/SearchBar.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -214,8 +215,23 @@ private fun ResponsiveSearchBar( isExpandedScreen: Boolean, modifier: Modifier = Modifier, inputFieldModifier: Modifier = Modifier, - content: @Composable () -> Unit, + content: @Composable ColumnScope.() -> Unit, ) { + val inputField = + @Composable { + SearchBarDefaults.InputField( + query = query, + onQueryChange = onQueryChange, + onSearch = onSearch, + modifier = inputFieldModifier, + expanded = active, + onExpandedChange = onActiveChange, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + ) + } + if (isExpandedScreen) { Row( verticalAlignment = Alignment.CenterVertically, @@ -223,47 +239,19 @@ private fun ResponsiveSearchBar( modifier = modifier, ) { DockedSearchBar( - inputField = { - SearchBarDefaults.InputField( - query = query, - onQueryChange = onQueryChange, - onSearch = onSearch, - modifier = inputFieldModifier, - expanded = active, - onExpandedChange = onActiveChange, - placeholder = placeholder, - leadingIcon = leadingIcon, - trailingIcon = trailingIcon, - ) - }, + inputField = inputField, expanded = active, onExpandedChange = onActiveChange, - content = { - content() - }, + content = content, ) } } else { SearchBar( - inputField = { - SearchBarDefaults.InputField( - query = query, - onQueryChange = onQueryChange, - onSearch = onSearch, - modifier = inputFieldModifier, - expanded = active, - onExpandedChange = onActiveChange, - placeholder = placeholder, - leadingIcon = leadingIcon, - trailingIcon = trailingIcon, - ) - }, + inputField = inputField, expanded = active, onExpandedChange = onActiveChange, modifier = Modifier.fillMaxWidth(), - content = { - content() - }, + content = content, ) } } @@ -273,13 +261,15 @@ internal fun SearchIcon( active: Boolean, onButtonClick: () -> Unit, ) { - if (active) { - ClickableIcon( - imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - onClick = onButtonClick, - ) - } else { - Icon(Icons.Rounded.Search, contentDescription = null) + Crossfade(active, label = "") { + if (it) { + ClickableIcon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + onClick = onButtonClick, + ) + } else { + Icon(Icons.Rounded.Search, contentDescription = null) + } } } diff --git a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/search/SearchContents.kt b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/search/SearchContents.kt index c2f05d47838..bd919f75d08 100644 --- a/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/search/SearchContents.kt +++ b/app/src/main/kotlin/app/lawnchair/lawnicons/ui/components/home/search/SearchContents.kt @@ -29,7 +29,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -180,7 +180,7 @@ private fun IconInfoListItem(iconInfo: ImmutableList) { return@IconInfoListItem } - val isIconInfoAppfilterShown = remember { mutableStateOf(false) } + val isIconInfoAppfilterShown = rememberSaveable { mutableStateOf(false) } ListItem( headlineContent = { Text(it.getFirstLabelAndComponent().label) },