From 2c2c88b93cec2471a2e04989b2639bbfd4111fe8 Mon Sep 17 00:00:00 2001 From: KorigamiK <72932688+KorigamiK@users.noreply.github.com> Date: Wed, 2 Apr 2025 23:29:13 +0530 Subject: [PATCH] Feat: App list view (#1170) * Feat: grid icon visibility settings and migration support * fix: remove unnecessary padding * Feat: Create List View Settings * fix FavoritesPartProvider * small fix * fix favorites * Remove useless datastore migration * Change app list default value * Revert migration 3 * Hide list icon preference if list is not enabled * Use ListResults for app list --------- Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com> --- .../mm20/launcher2/ui/base/ProvideSettings.kt | 1 - .../ui/launcher/search/SearchColumn.kt | 10 +- .../ui/launcher/search/apps/AppItem.kt | 755 ++++++++++-------- .../ui/launcher/search/apps/AppResults.kt | 304 +++---- .../launcher/search/common/grid/GridItem.kt | 17 +- .../search/common/grid/GridResults.kt | 5 +- .../launcher/search/common/list/ListItem.kt | 22 + .../ui/settings/icons/IconsSettingsScreen.kt | 20 + .../settings/icons/IconsSettingsScreenVM.kt | 8 + core/i18n/src/main/res/values/strings.xml | 4 + .../preferences/LauncherSettingsData.kt | 5 +- .../LauncherSettingsDataSerializer.kt | 4 +- .../launcher2/preferences/ui/UiSettings.kt | 17 +- 13 files changed, 674 insertions(+), 498 deletions(-) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/base/ProvideSettings.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/base/ProvideSettings.kt index bce0eb64..2ae2d0ec 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/base/ProvideSettings.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/base/ProvideSettings.kt @@ -13,7 +13,6 @@ import de.mm20.launcher2.widgets.FavoritesWidget import de.mm20.launcher2.widgets.WidgetRepository import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import org.koin.androidx.compose.inject @Composable diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt index ba6f10fa..9b9fcd8a 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt @@ -64,6 +64,7 @@ fun SearchColumn( ) { val columns = LocalGridSettings.current.columnCount + val showList = LocalGridSettings.current.showList val context = LocalContext.current val viewModel: SearchVM = viewModel() @@ -111,6 +112,7 @@ fun SearchColumn( val expandedCategory: SearchCategory? by viewModel.expandedCategory var selectedAppProfileIndex: Int by remember(isSearchEmpty) { mutableIntStateOf(0) } + var selectedAppIndex: Int by remember(website) { mutableIntStateOf(-1) } var selectedContactIndex: Int by remember(contacts) { mutableIntStateOf(-1) } var selectedFileIndex: Int by remember(files) { mutableIntStateOf(-1) } var selectedCalendarIndex: Int by remember(events) { mutableIntStateOf(-1) } @@ -193,6 +195,9 @@ fun SearchColumn( columns = columns, reverse = reverse, showProfileLockControls = hasProfilesPermission, + showList = showList, + selectedIndex = selectedAppIndex, + onSelect = { selectedAppIndex = it }, ) } else { AppResults( @@ -202,7 +207,10 @@ fun SearchColumn( selectedAppProfileIndex = it }, columns = columns, - reverse = reverse + reverse = reverse, + showList = showList, + selectedIndex = selectedAppIndex, + onSelect = { selectedAppIndex = it }, ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt index f90931b4..c49fb0f4 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt @@ -4,6 +4,7 @@ import android.app.PendingIntent import android.content.Intent import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.tween @@ -61,7 +62,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp -import androidx.compose.ui.unit.roundToIntRect import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import de.mm20.launcher2.crashreporter.CrashReporter @@ -85,11 +85,15 @@ import kotlinx.coroutines.launch fun AppItem( modifier: Modifier = Modifier, app: Application, + showDetails: Boolean, onBack: () -> Unit ) { val viewModel: SearchableItemVM = listItemViewModel(key = "search-${app.key}") val iconSize = LocalGridSettings.current.iconSize.dp.toPixels() + val badge by viewModel.badge.collectAsStateWithLifecycle(null) + val icon by viewModel.icon.collectAsStateWithLifecycle() + LaunchedEffect(app) { viewModel.init(app, iconSize.toInt()) } @@ -97,386 +101,440 @@ fun AppItem( val context = LocalContext.current val scope = rememberCoroutineScope() - Column( - modifier = modifier.verticalScroll(rememberScrollState()) - ) { - Row { - Column( - modifier = Modifier - .weight(1f) - .padding(16.dp) - ) { - Text( - text = app.labelOverride ?: app.label, - style = MaterialTheme.typography.titleMedium - ) - - if (!app.isPrivate) { - - val tags by viewModel.tags.collectAsState(emptyList()) - if (tags.isNotEmpty()) { - Text( - modifier = Modifier.padding(top = 1.dp, bottom = 4.dp), - text = tags.joinToString(separator = " #", prefix = "#"), - color = MaterialTheme.colorScheme.secondary, - style = MaterialTheme.typography.labelSmall - ) - } - - - app.versionName?.let { - Text( - text = stringResource(R.string.app_info_version, it), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(top = 4.dp), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - Text( - text = app.componentName.packageName, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(top = 1.dp), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } else { - Text( - stringResource(R.string.profile_private_profile_state_locked), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(top = 8.dp), - color = MaterialTheme.colorScheme.secondary, - ) - } - - } - val badge by viewModel.badge.collectAsStateWithLifecycle(null) - val icon by viewModel.icon.collectAsStateWithLifecycle() - ShapedLauncherIcon( - size = 48.dp, - modifier = Modifier - .padding(16.dp), - badge = { badge }, - icon = { icon }, - ) - } - val notifications by viewModel.notifications.collectAsState(emptyList()) - - AnimatedVisibility(notifications.isNotEmpty()) { - var showAllNotifications by remember { mutableStateOf(false) } - AnimatedContent( - showAllNotifications || notifications.size == 1, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(bottom = 12.dp) - .border( - 1.dp, - MaterialTheme.colorScheme.outlineVariant, - MaterialTheme.shapes.small - ) - .clip(MaterialTheme.shapes.small) - ) { showAll -> - if (showAll) { - Column( - modifier = Modifier.animateContentSize() - ) { - for ((i, not) in notifications.withIndex()) { - val icon = - remember(not.smallIcon) { not.smallIcon?.loadDrawable(context) } - - if (not.title == null && not.text == null) continue - - if (i > 0) { - HorizontalDivider() - } - Row( - verticalAlignment = Alignment.CenterVertically, + SharedTransitionLayout(modifier = modifier) { + AnimatedContent(showDetails) { showDetails -> + if (showDetails) { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + Row { + Column( + modifier = Modifier + .weight(1f) + .padding(16.dp) + ) { + Text( + text = app.labelOverride ?: app.label, + style = MaterialTheme.typography.titleMedium, modifier = Modifier - .clickable { - try { - not.contentIntent?.sendWithBackgroundPermission(context) - } catch (e: PendingIntent.CanceledException) { - CrashReporter.logException(e) - } - } - .padding(vertical = 4.dp) - ) { - Box( - modifier = Modifier - .padding(horizontal = 12.dp) - .clip(CircleShape) - .background(Color(not.color)) - .size(32.dp) - .padding(8.dp), - contentAlignment = Alignment.Center, - ) { - AsyncImage( - modifier = Modifier.fillMaxSize(), - model = icon, - contentDescription = null + .sharedBounds( + rememberSharedContentState("label"), + this@AnimatedContent, + ), + ) + + if (!app.isPrivate) { + + val tags by viewModel.tags.collectAsState(emptyList()) + if (tags.isNotEmpty()) { + Text( + modifier = Modifier.padding(top = 1.dp, bottom = 4.dp), + text = tags.joinToString(separator = " #", prefix = "#"), + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.labelSmall ) } - Column( - modifier = Modifier.weight(1f) - ) { - if (not.title != null) { - Text( - not.title!!, - style = MaterialTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - if (not.text != null) { - Text( - not.text!!, - modifier = Modifier.padding(top = 2.dp), - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + + app.versionName?.let { + Text( + text = stringResource(R.string.app_info_version, it), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(top = 4.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Text( + text = app.componentName.packageName, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(top = 1.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } else { + Text( + stringResource(R.string.profile_private_profile_state_locked), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(top = 8.dp), + color = MaterialTheme.colorScheme.secondary, + ) + } + + } + ShapedLauncherIcon( + size = 48.dp, + modifier = Modifier + .padding(16.dp), + badge = { badge }, + icon = { icon }, + ) + } + val notifications by viewModel.notifications.collectAsState(emptyList()) + + AnimatedVisibility(notifications.isNotEmpty()) { + var showAllNotifications by remember { mutableStateOf(false) } + AnimatedContent( + showAllNotifications || notifications.size == 1, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp) + .border( + 1.dp, + MaterialTheme.colorScheme.outlineVariant, + MaterialTheme.shapes.small + ) + .clip(MaterialTheme.shapes.small) + ) { showAll -> + if (showAll) { + Column( + modifier = Modifier.animateContentSize() + ) { + for ((i, not) in notifications.withIndex()) { + val icon = + remember(not.smallIcon) { + not.smallIcon?.loadDrawable( + context + ) + } + + if (not.title == null && not.text == null) continue + + if (i > 0) { + HorizontalDivider() + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable { + try { + not.contentIntent?.sendWithBackgroundPermission( + context + ) + } catch (e: PendingIntent.CanceledException) { + CrashReporter.logException(e) + } + } + .padding(vertical = 4.dp) + ) { + Box( + modifier = Modifier + .padding(horizontal = 12.dp) + .clip(CircleShape) + .background(Color(not.color)) + .size(32.dp) + .padding(8.dp), + contentAlignment = Alignment.Center, + ) { + AsyncImage( + modifier = Modifier.fillMaxSize(), + model = icon, + contentDescription = null + ) + } + Column( + modifier = Modifier.weight(1f) + ) { + if (not.title != null) { + Text( + not.title!!, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + if (not.text != null) { + Text( + not.text!!, + modifier = Modifier.padding(top = 2.dp), + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + if (not.isClearable) { + IconButton( + onClick = { + viewModel.clearNotification(not) + } + ) { + Icon(Icons.Rounded.Clear, null) + } + } + } } } - if (not.isClearable) { + } else { + Row( + modifier = Modifier + .clickable { + showAllNotifications = true + } + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Rounded.Notifications, + null, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Text( + pluralStringResource( + R.plurals.app_info_notifications, + notifications.size, + notifications.size + ), + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.weight(1f), + ) + Icon( + Icons.AutoMirrored.Rounded.NavigateNext, + null, + modifier = Modifier.padding(horizontal = 12.dp) + ) + } + } + } + } + + val shortcuts by viewModel.children.collectAsState(emptyList()) + if (shortcuts.isNotEmpty()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp) + .border( + 1.dp, + MaterialTheme.colorScheme.outlineVariant, + MaterialTheme.shapes.small + ) + .clip(MaterialTheme.shapes.small) + ) { + for ((i, shortcut) in shortcuts.withIndex()) { + val isPinned by remember(shortcut) { + viewModel.isChildPinned( + shortcut + ) + }.collectAsState( + false + ) + + val iconSizePx = 32.dp.toPixels() + + val icon by + remember { + viewModel.getChildIcon( + shortcut, + iconSizePx.toInt() + ) + }.collectAsState(null) + if (i > 0) { + HorizontalDivider() + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable { + viewModel.launchChild(context, shortcut) + } + .padding(vertical = 4.dp) + ) { + ShapedLauncherIcon( + size = 32.dp, + icon = { icon }, + shape = CircleShape, + modifier = Modifier + .padding(horizontal = 12.dp) + .size(32.dp), + ) + + Text( + shortcut.labelOverride ?: shortcut.label, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) IconButton( onClick = { - viewModel.clearNotification(not) + if (isPinned) { + viewModel.unpinChild(shortcut) + } else { + viewModel.pinChild(shortcut) + } } ) { - Icon(Icons.Rounded.Clear, null) + Icon( + if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline, + stringResource(if (isPinned) R.string.menu_favorites_unpin else R.string.menu_favorites_pin), + ) } } + } } } - } else { - Row( - modifier = Modifier - .clickable { - showAllNotifications = true - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - Icons.Rounded.Notifications, - null, - modifier = Modifier.padding(horizontal = 16.dp) - ) - Text( - pluralStringResource( - R.plurals.app_info_notifications, - notifications.size, - notifications.size - ), - style = MaterialTheme.typography.titleSmall, - modifier = Modifier.weight(1f), - ) - Icon( - Icons.AutoMirrored.Rounded.NavigateNext, - null, - modifier = Modifier.padding(horizontal = 12.dp) - ) - } - } - } - } - val shortcuts by viewModel.children.collectAsState(emptyList()) - if (shortcuts.isNotEmpty()) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(bottom = 12.dp) - .border( - 1.dp, - MaterialTheme.colorScheme.outlineVariant, - MaterialTheme.shapes.small - ) - .clip(MaterialTheme.shapes.small) - ) { - for ((i, shortcut) in shortcuts.withIndex()) { - val isPinned by remember(shortcut) { viewModel.isChildPinned(shortcut) }.collectAsState( - false + val toolbarActions = mutableListOf() + + if (LocalFavoritesEnabled.current) { + val isPinned by viewModel.isPinned.collectAsState(false) + val favAction = if (isPinned) { + DefaultToolbarAction( + label = stringResource(R.string.menu_favorites_unpin), + icon = Icons.Rounded.Star, + action = { + viewModel.unpin() + } + ) + } else { + DefaultToolbarAction( + label = stringResource(R.string.menu_favorites_pin), + icon = Icons.Rounded.StarOutline, + action = { + viewModel.pin() + }) + } + toolbarActions.add(favAction) + } + + if (!app.isPrivate) { + toolbarActions.add( + DefaultToolbarAction( + label = stringResource(R.string.menu_app_info), + icon = Icons.Rounded.Info + ) { + app.openAppDetails(context) + }) + } + + toolbarActions.add( + DefaultToolbarAction( + label = stringResource(R.string.menu_launch), + icon = Icons.AutoMirrored.Rounded.OpenInNew, + action = { + viewModel.launch(context) + } + ) ) - val iconSizePx = 32.dp.toPixels() - - val icon by - remember { - viewModel.getChildIcon( - shortcut, - iconSizePx.toInt() - ) - }.collectAsState(null) - if (i > 0) { - HorizontalDivider() + val sheetManager = LocalBottomSheetManager.current + if (!app.isPrivate) { + toolbarActions.add( + DefaultToolbarAction( + label = stringResource(R.string.menu_customize), + icon = Icons.Rounded.Tune, + action = { sheetManager.showCustomizeSearchableModal(app) } + )) } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable { - viewModel.launchChild(context, shortcut) - } - .padding(vertical = 4.dp) - ) { - ShapedLauncherIcon( - size = 32.dp, - icon = { icon }, - shape = CircleShape, - modifier = Modifier - .padding(horizontal = 12.dp) - .size(32.dp), - ) - Text( - shortcut.labelOverride ?: shortcut.label, - modifier = Modifier.weight(1f), - style = MaterialTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - IconButton( - onClick = { - if (isPinned) { - viewModel.unpinChild(shortcut) - } else { - viewModel.pinChild(shortcut) + if (!app.isPrivate) { + val storeDetails = remember(app) { app.getStoreDetails(context) } + val shareAction = if (storeDetails == null) { + DefaultToolbarAction( + label = stringResource(R.string.menu_share), + icon = Icons.Rounded.Share + ) { + scope.launch { + app.shareApkFile(context) } } - ) { - Icon( - if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline, - stringResource(if (isPinned) R.string.menu_favorites_unpin else R.string.menu_favorites_pin), + } else { + SubmenuToolbarAction( + label = stringResource(R.string.menu_share), + icon = Icons.Rounded.Share, + children = listOf( + DefaultToolbarAction( + label = stringResource( + R.string.menu_share_store_link, + storeDetails.label + ), + icon = Icons.Rounded.Link, + action = { + val shareIntent = Intent(Intent.ACTION_SEND) + shareIntent.putExtra( + Intent.EXTRA_TEXT, + storeDetails.url + ) + shareIntent.type = "text/plain" + context.startActivity( + Intent.createChooser( + shareIntent, + null + ) + ) + } + ), + DefaultToolbarAction( + label = stringResource(R.string.menu_share_apk_file), + icon = Icons.Rounded.Android + ) { + scope.launch { + app.shareApkFile(context) + } + } + ) ) } + toolbarActions.add(shareAction) } - } - } - } - - val toolbarActions = mutableListOf() - - if (LocalFavoritesEnabled.current) { - val isPinned by viewModel.isPinned.collectAsState(false) - val favAction = if (isPinned) { - DefaultToolbarAction( - label = stringResource(R.string.menu_favorites_unpin), - icon = Icons.Rounded.Star, - action = { - viewModel.unpin() + if (app.canUninstall) { + toolbarActions.add( + DefaultToolbarAction( + label = stringResource(R.string.menu_uninstall), + icon = Icons.Rounded.Delete, + ) { + app.uninstall(context) + onBack() + } + ) } - ) - } else { - DefaultToolbarAction( - label = stringResource(R.string.menu_favorites_pin), - icon = Icons.Rounded.StarOutline, - action = { - viewModel.pin() - }) - } - toolbarActions.add(favAction) - } - if (!app.isPrivate) { - toolbarActions.add( - DefaultToolbarAction( - label = stringResource(R.string.menu_app_info), - icon = Icons.Rounded.Info - ) { - app.openAppDetails(context) - }) - } - - toolbarActions.add( - DefaultToolbarAction( - label = stringResource(R.string.menu_launch), - icon = Icons.AutoMirrored.Rounded.OpenInNew, - action = { - viewModel.launch(context) - } - ) - ) - - val sheetManager = LocalBottomSheetManager.current - if (!app.isPrivate) { - toolbarActions.add(DefaultToolbarAction( - label = stringResource(R.string.menu_customize), - icon = Icons.Rounded.Tune, - action = { sheetManager.showCustomizeSearchableModal(app) } - )) - } - - if (!app.isPrivate) { - val storeDetails = remember(app) { app.getStoreDetails(context) } - val shareAction = if (storeDetails == null) { - DefaultToolbarAction( - label = stringResource(R.string.menu_share), - icon = Icons.Rounded.Share - ) { - scope.launch { - app.shareApkFile(context) - } - } - } else { - SubmenuToolbarAction( - label = stringResource(R.string.menu_share), - icon = Icons.Rounded.Share, - children = listOf( - DefaultToolbarAction( - label = stringResource( - R.string.menu_share_store_link, - storeDetails.label - ), - icon = Icons.Rounded.Link, - action = { - val shareIntent = Intent(Intent.ACTION_SEND) - shareIntent.putExtra(Intent.EXTRA_TEXT, storeDetails.url) - shareIntent.type = "text/plain" - context.startActivity(Intent.createChooser(shareIntent, null)) + Toolbar( + leftActions = listOf( + DefaultToolbarAction( + label = stringResource(id = R.string.menu_back), + icon = Icons.AutoMirrored.Rounded.ArrowBack + ) { + onBack() } ), - DefaultToolbarAction( - label = stringResource(R.string.menu_share_apk_file), - icon = Icons.Rounded.Android - ) { - scope.launch { - app.shareApkFile(context) - } - } + rightActions = toolbarActions ) - ) + } + } else { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (LocalGridSettings.current.showListIcons) { + ShapedLauncherIcon( + size = LocalGridSettings.current.iconSize.dp, + modifier = Modifier + .padding(end = 16.dp), + badge = { badge }, + icon = { icon }, + ) + } + Text( + maxLines = 1, + overflow = TextOverflow.Ellipsis, + text = app.labelOverride ?: app.label, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .sharedBounds( + rememberSharedContentState("label"), + this@AnimatedContent, + ), + ) + } } - toolbarActions.add(shareAction) } - - if (app.canUninstall) { - toolbarActions.add( - DefaultToolbarAction( - label = stringResource(R.string.menu_uninstall), - icon = Icons.Rounded.Delete, - ) { - app.uninstall(context) - onBack() - } - ) - } - - Toolbar( - leftActions = listOf( - DefaultToolbarAction( - label = stringResource(id = R.string.menu_back), - icon = Icons.AutoMirrored.Rounded.ArrowBack - ) { - onBack() - } - ), - rightActions = toolbarActions - ) } } @@ -507,6 +565,7 @@ fun AppItemGridPopup( y = lerp(-16.dp, 0.dp, animationProgress) ), app = app, + showDetails = true, onBack = onDismiss ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt index 80f858e5..81a73937 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -22,9 +22,9 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LeadingIconTab import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.Text +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -36,6 +36,8 @@ import de.mm20.launcher2.search.Application import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.launcher.search.common.grid.GridItem import de.mm20.launcher2.ui.launcher.search.common.grid.GridResults +import de.mm20.launcher2.ui.launcher.search.common.list.ListItem +import de.mm20.launcher2.ui.launcher.search.common.list.ListResults import de.mm20.launcher2.ui.layout.BottomReversed import de.mm20.launcher2.ui.locals.LocalGridSettings @@ -47,158 +49,192 @@ fun LazyListScope.AppResults( isProfileLocked: Boolean = false, onProfileLockChange: ((Profile, Boolean) -> Unit)? = null, apps: List, + selectedIndex: Int, + onSelect: (Int) -> Unit, highlightedItem: Application? = null, columns: Int, reverse: Boolean, + showList: Boolean, ) { - - GridResults( - key = "apps", - items = apps, - before = if (profiles.size > 1) { - { - Column( - verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top, + val before = if (profiles.size > 1) { + @Composable { + Column( + verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top, + ) { + PrimaryScrollableTabRow( + selectedTabIndex = selectedProfileIndex, + containerColor = Color.Transparent, + edgePadding = 16.dp, + divider = {} ) { - PrimaryScrollableTabRow( - selectedTabIndex = selectedProfileIndex, - containerColor = Color.Transparent, - edgePadding = 16.dp, - divider = {} - ) { - for ((i, profile) in profiles.withIndex()) { - LeadingIconTab( - selected = selectedProfileIndex == profiles.indexOf(profile), - text = { - Text( - when (profile.type) { - Profile.Type.Personal -> stringResource(R.string.apps_profile_main) - Profile.Type.Work -> stringResource(R.string.apps_profile_work) - Profile.Type.Private -> stringResource(R.string.apps_profile_private) - } - ) - }, - icon = { + for ((i, profile) in profiles.withIndex()) { + LeadingIconTab( + selected = selectedProfileIndex == profiles.indexOf(profile), + text = { + Text( when (profile.type) { - Profile.Type.Personal -> Icon( - Icons.Rounded.Person, - contentDescription = null - ) - - Profile.Type.Work -> Icon( - Icons.Rounded.Work, - contentDescription = null - ) - - Profile.Type.Private -> Icon( - Icons.Rounded.PrivateSpace, - contentDescription = null - ) + Profile.Type.Personal -> stringResource(R.string.apps_profile_main) + Profile.Type.Work -> stringResource(R.string.apps_profile_work) + Profile.Type.Private -> stringResource(R.string.apps_profile_private) } - }, - onClick = { - onProfileSelected(i) + ) + }, + icon = { + when (profile.type) { + Profile.Type.Personal -> Icon( + Icons.Rounded.Person, + contentDescription = null + ) + + Profile.Type.Work -> Icon( + Icons.Rounded.Work, + contentDescription = null + ) + + Profile.Type.Private -> Icon( + Icons.Rounded.PrivateSpace, + contentDescription = null + ) } - ) - } + }, + onClick = { + onProfileSelected(i) + } + ) } - HorizontalDivider() + } - val profileType = profiles[selectedProfileIndex].type - if (profileType != Profile.Type.Personal) { - if (isProfileLocked) { - Column( - modifier = Modifier - .padding(12.dp) - .fillMaxWidth() - .border(1.dp, MaterialTheme.colorScheme.outlineVariant, MaterialTheme.shapes.small) - .background(MaterialTheme.colorScheme.surfaceContainer, MaterialTheme.shapes.small) - .padding(vertical = 64.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - if (profileType == Profile.Type.Work) Icons.Rounded.WorkOff else Icons.Rounded.Lock, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.secondary, + if (!showList || isProfileLocked) { + HorizontalDivider() + } + + val profileType = profiles[selectedProfileIndex].type + if (profileType != Profile.Type.Personal) { + if (isProfileLocked) { + Column( + modifier = Modifier + .padding(12.dp) + .fillMaxWidth() + .border( + 1.dp, + MaterialTheme.colorScheme.outlineVariant, + MaterialTheme.shapes.small ) - Text( - stringResource( - if (profileType == Profile.Type.Work) R.string.profile_work_profile_state_locked - else R.string.profile_private_profile_state_locked - ), - modifier = Modifier.padding(top = 8.dp), - color = MaterialTheme.colorScheme.secondary, - style = MaterialTheme.typography.titleSmall, + .background( + MaterialTheme.colorScheme.surfaceContainer, + MaterialTheme.shapes.small ) - if (showProfileLockControls) { - Button( - modifier = Modifier.padding(top = 32.dp), - onClick = { - onProfileLockChange?.invoke( - profiles[selectedProfileIndex], - false - ) - }, - contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, - ) { - Icon( - if (profileType == Profile.Type.Work) Icons.Rounded.Work else Icons.Rounded.LockOpen, - contentDescription = null, - modifier = Modifier - .padding(end = ButtonDefaults.IconSpacing) - .size(ButtonDefaults.IconSize) + .padding(vertical = 64.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + if (profileType == Profile.Type.Work) Icons.Rounded.WorkOff else Icons.Rounded.Lock, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.secondary, + ) + Text( + stringResource( + if (profileType == Profile.Type.Work) R.string.profile_work_profile_state_locked + else R.string.profile_private_profile_state_locked + ), + modifier = Modifier.padding(top = 8.dp), + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.titleSmall, + ) + if (showProfileLockControls) { + Button( + modifier = Modifier.padding(top = 32.dp), + onClick = { + onProfileLockChange?.invoke( + profiles[selectedProfileIndex], + false ) - Text( - stringResource( - if (profileType == Profile.Type.Work) R.string.profile_work_profile_action_unlock - else R.string.profile_private_profile_action_unlock - ) + }, + contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, + ) { + Icon( + if (profileType == Profile.Type.Work) Icons.Rounded.Work else Icons.Rounded.LockOpen, + contentDescription = null, + modifier = Modifier + .padding(end = ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize) + ) + Text( + stringResource( + if (profileType == Profile.Type.Work) R.string.profile_work_profile_action_unlock + else R.string.profile_private_profile_action_unlock ) - } + ) } } - } else if (showProfileLockControls) { - FilledTonalButton( + } + } else if (showProfileLockControls) { + FilledTonalButton( + modifier = Modifier + .padding(12.dp) + .fillMaxWidth(), + onClick = { + onProfileLockChange?.invoke( + profiles[selectedProfileIndex], + true + ) + }, + contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, + ) { + Icon( + if (profileType == Profile.Type.Work) Icons.Rounded.WorkOff else Icons.Rounded.Lock, + contentDescription = null, modifier = Modifier - .padding(12.dp) - .fillMaxWidth(), - onClick = { - onProfileLockChange?.invoke( - profiles[selectedProfileIndex], - true - ) - }, - contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, - ) { - Icon( - if (profileType == Profile.Type.Work) Icons.Rounded.WorkOff else Icons.Rounded.Lock, - contentDescription = null, - modifier = Modifier - .padding(end = ButtonDefaults.IconSpacing) - .size(ButtonDefaults.IconSize) + .padding(end = ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize) + ) + Text( + stringResource( + if (profileType == Profile.Type.Work) R.string.profile_work_profile_action_lock + else R.string.profile_private_profile_action_lock ) - Text( - stringResource( - if (profileType == Profile.Type.Work) R.string.profile_work_profile_action_lock - else R.string.profile_private_profile_action_lock - ) - ) - } + ) } } } } - } else null, - itemContent = { - GridItem( - item = it, - showLabels = LocalGridSettings.current.showLabels, - highlight = it.key == highlightedItem?.key - ) - }, - reverse = reverse, - columns = columns, - ) + } + } else null + if (showList) { + ListResults( + key = "apps", + items = apps, + before = before?.let { { it() } }, + selectedIndex = selectedIndex, + itemContent = { app, showDetails, index -> + ListItem( + modifier = Modifier + .fillMaxWidth(), + item = app, + showDetails = showDetails, + onShowDetails = { onSelect(if(it) index else -1) }, + highlight = highlightedItem?.key == app.key + ) + }, + reverse = reverse, + ) + } else { + GridResults( + key = "apps", + items = apps, + before = before, + itemContent = { + GridItem( + item = it, + showLabels = LocalGridSettings.current.showLabels, + highlight = it.key == highlightedItem?.key + ) + }, + reverse = reverse, + columns = columns, + ) + } + } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt index a7187108..60e7754b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt @@ -1,6 +1,5 @@ package de.mm20.launcher2.ui.launcher.search.common.grid -import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.MutableTransitionState @@ -13,6 +12,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize @@ -110,6 +110,7 @@ fun GridItem( Column( modifier = modifier + .padding(4.dp) .combinedClickable( onClick = { if (!launchOnPress || !viewModel.launch(context, bounds)) { @@ -170,7 +171,9 @@ fun GridItem( modifier = Modifier .padding(4.dp) .onGloballyPositioned { - bounds = it.boundsInWindow().roundToIntRect() + bounds = it + .boundsInWindow() + .roundToIntRect() } then if (highlight) Modifier.background( MaterialTheme.colorScheme.surface, @@ -195,10 +198,10 @@ fun GridItem( color = MaterialTheme.colorScheme.onBackground, ) } + } - if (showPopup) { - ItemPopup(origin = bounds, searchable = item, onDismissRequest = { showPopup = false }) - } + if (showPopup) { + ItemPopup(origin = bounds, searchable = item, onDismissRequest = { showPopup = false }) } } @@ -398,7 +401,7 @@ private fun Modifier.placeOverlay( constraints.maxHeight - placeable.height, ), animationProgress.pow(2) - ).toInt() + ) ) } } @@ -410,4 +413,4 @@ private fun lerp(start: Float, stop: Float, fraction: Float): Float { private fun lerp(start: Int, stop: Int, fraction: Float): Int { return start + (fraction * (stop - start)).toInt() -} \ No newline at end of file +} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridResults.kt index 72202b43..acd159aa 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridResults.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridResults.kt @@ -83,8 +83,8 @@ fun LazyListScope.GridResults( .padding( top = if (it == 0) 8.dp else 0.dp, bottom = if (it == rows - 1) 8.dp else 0.dp, - start = 4.dp, - end = 4.dp, + start = if (columns == 1) 0.dp else 4.dp, + end = if (columns == 1) 0.dp else 4.dp, ) ) { Row { @@ -94,7 +94,6 @@ fun LazyListScope.GridResults( Box( modifier = Modifier .weight(1f) - .padding(4.dp) ) { itemContent(item) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt index c212c5db..844fa9eb 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -22,6 +23,7 @@ import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.roundToIntRect import de.mm20.launcher2.search.AppShortcut +import de.mm20.launcher2.search.Application import de.mm20.launcher2.search.Article import de.mm20.launcher2.search.CalendarEvent import de.mm20.launcher2.search.Contact @@ -30,6 +32,7 @@ import de.mm20.launcher2.search.Location import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.Website import de.mm20.launcher2.ui.ktx.toPixels +import de.mm20.launcher2.ui.launcher.search.apps.AppItem import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM import de.mm20.launcher2.ui.launcher.search.contacts.ContactItem @@ -80,6 +83,25 @@ fun ListItem( LocalContentColor provides MaterialTheme.colorScheme.onSurface ) { when (item) { + is Application -> { + AppItem( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 9999.dp) // we have infinite space, but there is an inner scroll that needs a constraint + .combinedClickable( + enabled = !showDetails, + onClick = { + if (!viewModel.launch(context, bounds)) { + onShowDetails(true) + } + }, + onLongClick = { onShowDetails(true) } + ), + app = item, + showDetails = showDetails, + onBack = { onShowDetails(false) } + ) + } is Contact -> { ContactItem( modifier = Modifier diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt index dc8543c9..09092305 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt @@ -110,6 +110,26 @@ fun IconsSettingsScreen() { viewModel.setShowLabels(it) } ) + SwitchPreference( + title = stringResource(R.string.preference_grid_list_style), + summary = stringResource(R.string.preference_grid_list_style_summary), + value = grid.showList, + onValueChanged = { + viewModel.setShowList(it) + } + ) + AnimatedVisibility( + grid.showList + ) { + SwitchPreference( + title = stringResource(R.string.preference_grid_list_icons), + summary = stringResource(R.string.preference_grid_list_icons_summary), + value = grid.showListIcons, + onValueChanged = { + viewModel.setShowListIcons(it) + } + ) + } SliderPreference( title = stringResource(R.string.preference_grid_column_count), value = grid.columnCount, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt index 80a3abd0..15f01ab5 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt @@ -53,6 +53,14 @@ class IconsSettingsScreenVM( uiSettings.setGridShowLabels(showLabels) } + fun setShowList(showList: Boolean) { + uiSettings.setGridShowList(showList) + } + + fun setShowListIcons(showIcons: Boolean) { + uiSettings.setGridShowListIcons(showIcons) + } + val iconShape = uiSettings.iconShape fun setIconShape(iconShape: IconShape) { uiSettings.setIconShape(iconShape) diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index e57c7b4f..3e9b3d06 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -557,8 +557,12 @@ Grid Icon size Number of columns + Show app results in a list + Show app icons in list Show labels Show the app name below the icon + Show applications in a list view instead of grid + Show icons in the list view Debug Troubleshooting tools Widgets diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt index 43c01e92..c8c9a933 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt @@ -1,14 +1,13 @@ package de.mm20.launcher2.preferences import android.content.Context -import de.mm20.launcher2.preferences.search.LocationSearchSettings import de.mm20.launcher2.search.SearchFilters import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class LauncherSettingsData internal constructor( - val schemaVersion: Int = 2, + val schemaVersion: Int = 3, val uiColorScheme: ColorScheme = ColorScheme.System, val uiTheme: ThemeDescriptor = ThemeDescriptor.Default, @@ -83,6 +82,8 @@ data class LauncherSettingsData internal constructor( val gridColumnCount: Int = 5, val gridIconSize: Int = 48, val gridLabels: Boolean = true, + val gridList: Boolean = false, + val gridListIcons: Boolean = true, val searchBarStyle: SearchBarStyle = SearchBarStyle.Transparent, val searchBarColors: SearchBarColors = SearchBarColors.Auto, diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsDataSerializer.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsDataSerializer.kt index cdd62f90..e6b88376 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsDataSerializer.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsDataSerializer.kt @@ -2,6 +2,7 @@ package de.mm20.launcher2.preferences import androidx.datastore.core.CorruptionException import androidx.datastore.core.Serializer +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream @@ -21,6 +22,7 @@ internal object LauncherSettingsDataSerializer : Serializer