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 cda55b06..c0eaf5be 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 @@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.launcher.search.apps import android.app.PendingIntent import android.content.Intent +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.MutableTransitionState @@ -9,64 +10,62 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.expandIn import androidx.compose.animation.shrinkOut import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.automirrored.rounded.NavigateNext +import androidx.compose.material.icons.automirrored.rounded.OpenInNew import androidx.compose.material.icons.rounded.Android -import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.Clear import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.Link -import androidx.compose.material.icons.rounded.OpenInNew +import androidx.compose.material.icons.rounded.Notifications import androidx.compose.material.icons.rounded.Share import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material.icons.rounded.Tune -import androidx.compose.material.icons.rounded.Visibility -import androidx.compose.material.icons.rounded.VisibilityOff +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.InputChip -import androidx.compose.material3.InputChipDefaults +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp import androidx.compose.ui.unit.roundToIntRect import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.lifecycleScope import coil.compose.AsyncImage -import com.google.accompanist.flowlayout.FlowRow import de.mm20.launcher2.crashreporter.CrashReporter +import de.mm20.launcher2.ktx.sendWithBackgroundPermission import de.mm20.launcher2.search.Application import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.DefaultToolbarAction @@ -80,8 +79,6 @@ import de.mm20.launcher2.ui.launcher.search.listItemViewModel import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled import de.mm20.launcher2.ui.locals.LocalGridSettings -import de.mm20.launcher2.ui.locals.LocalSnackbarHostState -import de.mm20.launcher2.ui.modifier.scale import kotlinx.coroutines.launch @Composable @@ -98,12 +95,10 @@ fun AppItem( } val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current - val snackbarHostState = LocalSnackbarHostState.current val scope = rememberCoroutineScope() Column( - modifier = modifier + modifier = modifier.verticalScroll(rememberScrollState()) ) { Row { Column( @@ -144,143 +139,215 @@ fun AppItem( overflow = TextOverflow.Ellipsis, ) - FlowRow( - modifier = Modifier - .padding(top = 12.dp) - .animateContentSize(), - mainAxisSpacing = 12.dp, - crossAxisSpacing = 0.dp - ) { - val notifications by viewModel.notifications.collectAsState(emptyList()) - for (not in notifications) { - val title = not.title?.takeIf { it.isNotBlank() } - ?: not.text?.takeIf { it.isNotBlank() } - ?: continue - - val icon = remember(not.smallIcon) { not.smallIcon?.loadDrawable(context) } - - InputChip( - modifier = Modifier.width(IntrinsicSize.Max), - selected = false, - label = { - Text( - title, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f) - ) - }, - avatar = { - Box(modifier = Modifier.background(Color(not.color))) { - AsyncImage( - modifier = Modifier - .requiredSize(InputChipDefaults.AvatarSize) - .padding(3.dp), - model = icon, - contentDescription = null - ) - } - }, - trailingIcon = if (not.isClearable) { - { - Icon( - Icons.Rounded.Clear, - null, - modifier = Modifier - .clip(CircleShape) - .size(InputChipDefaults.IconSize) - .clickable { - viewModel.clearNotification(not) - }, - ) - } - } else null, - onClick = { - try { - not.contentIntent?.send() - } catch (e: PendingIntent.CanceledException) { - CrashReporter.logException(e) - } - } - ) - } - - val shortcuts by viewModel.shortcuts.collectAsState(emptyList()) - - for (shortcut in shortcuts) { - val title = - shortcut.labelOverride ?: shortcut.label - val isPinned by remember(shortcut) { viewModel.isShortcutPinned(shortcut) }.collectAsState( - false - ) - - val iconSizePx = InputChipDefaults.AvatarSize.toPixels() - - val icon by - remember { - viewModel.getShortcutIcon( - context, - shortcut, - iconSizePx.toInt() - ) - }.collectAsState(null) - - InputChip( - modifier = Modifier.width(IntrinsicSize.Max), - selected = false, - onClick = { - viewModel.launchShortcut(context, shortcut) - }, - label = { - Text( - title, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f) - ) - }, - avatar = { - ShapedLauncherIcon( - size = InputChipDefaults.AvatarSize, - icon = { icon }, - shape = CircleShape, - ) - }, - trailingIcon = if (LocalFavoritesEnabled.current) { - { - Icon( - if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline, - null, - modifier = Modifier - .clip(CircleShape) - .requiredSize(InputChipDefaults.IconSize) - .clickable { - - if (isPinned) { - viewModel.unpinShortcut(shortcut) - } else { - viewModel.pinShortcut(shortcut) - } - }, - ) - } - } else null - ) - - } - } } val badge by viewModel.badge.collectAsStateWithLifecycle(null) val icon by viewModel.icon.collectAsStateWithLifecycle() ShapedLauncherIcon( - size = 84.dp, + 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) + } + } + } + } + } + } 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 = { + if (isPinned) { + viewModel.unpinChild(shortcut) + } else { + viewModel.pinChild(shortcut) + } + } + ) { + Icon( + if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline, + null + ) + } + } + } + } + } + } val toolbarActions = mutableListOf() @@ -316,7 +383,7 @@ fun AppItem( toolbarActions.add( DefaultToolbarAction( label = stringResource(R.string.menu_launch), - icon = Icons.Rounded.OpenInNew, + icon = Icons.AutoMirrored.Rounded.OpenInNew, action = { viewModel.launch(context) } @@ -384,7 +451,7 @@ fun AppItem( leftActions = listOf( DefaultToolbarAction( label = stringResource(id = R.string.menu_back), - icon = Icons.Rounded.ArrowBack + icon = Icons.AutoMirrored.Rounded.ArrowBack ) { onBack() } @@ -416,10 +483,6 @@ fun AppItemGridPopup( AppItem( modifier = Modifier .fillMaxWidth() - .scale( - 1 - (1 - LocalGridSettings.current.iconSize / 84f) * (1 - animationProgress), - transformOrigin = TransformOrigin(1f, 0f) - ) .offset( x = lerp(16.dp, 0.dp, animationProgress), y = lerp(-16.dp, 0.dp, animationProgress) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt index 09b7e64a..8ed28b83 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt @@ -6,7 +6,7 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.compose.ui.geometry.Rect import androidx.core.app.ActivityOptionsCompat -import androidx.customview.view.AbsSavedState +import de.mm20.launcher2.applications.AppRepository import de.mm20.launcher2.appshortcuts.AppShortcutRepository import de.mm20.launcher2.badges.BadgeService import de.mm20.launcher2.devicepose.DevicePoseProvider @@ -20,7 +20,6 @@ import de.mm20.launcher2.preferences.search.LocationSearchSettings import de.mm20.launcher2.search.AppShortcut import de.mm20.launcher2.search.Application import de.mm20.launcher2.search.File -import de.mm20.launcher2.search.Location import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.UpdatableSearchable import de.mm20.launcher2.search.UpdateResult @@ -36,12 +35,12 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes @OptIn(ExperimentalCoroutinesApi::class) @@ -51,6 +50,7 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent { private val iconService: IconService by inject() private val tagsService: TagsService by inject() private val notificationRepository: NotificationRepository by inject() + private val appRepository: AppRepository by inject() private val appShortcutRepository: AppShortcutRepository by inject() private val permissionsManager: PermissionsManager by inject() private val locationSearchSettings: LocationSearchSettings by inject() @@ -95,15 +95,30 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent { else notificationRepository.notifications.map { it.filter { it.packageName == searchable.componentName.packageName && !it.isGroupSummary } } } - val shortcuts = searchable.map { - if (it !is Application) emptyList() - else appShortcutRepository - .findMany( - componentName = it.componentName, - user = it.user, - manifest = true, - dynamic = true, - ).first() + val children = searchable.flatMapLatest { + when(it) { + is Application -> appShortcutRepository + .findMany( + componentName = it.componentName, + user = it.user, + manifest = true, + dynamic = true, + ) + is AppShortcut -> { + val packageName = it.componentName?.packageName ?: return@flatMapLatest flowOf( + emptyList() + ) + appRepository + .findOne( + packageName = packageName, + user = it.user, + ) + .map { listOfNotNull(it) } + } + else -> flowOf( + emptyList() + ) + } } fun launch(context: Context, bounds: Rect? = null): Boolean { @@ -134,24 +149,26 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent { notificationRepository.cancelNotification(notification) } - fun getShortcutIcon(context: Context, shortcut: AppShortcut, size: Int): Flow { - return iconService.getIcon(shortcut, size) + fun getChildIcon(child: SavableSearchable, size: Int): Flow { + return iconService.getIcon(child, size) } - fun isShortcutPinned(shortcut: AppShortcut): Flow { - return favoritesService.isPinned(shortcut) + fun isChildPinned(child: SavableSearchable): Flow { + return favoritesService.isPinned(child) } - fun pinShortcut(shortcut: AppShortcut) { - favoritesService.pinItem(shortcut) + fun pinChild(child: SavableSearchable) { + favoritesService.pinItem(child) } - fun unpinShortcut(shortcut: AppShortcut) { - favoritesService.unpinItem(shortcut) + fun unpinChild(child: SavableSearchable) { + favoritesService.unpinItem(child) } - fun launchShortcut(context: Context, shortcut: AppShortcut) { - shortcut.launch(context, null) + fun launchChild(context: Context, child: SavableSearchable) { + if(child.launch(context, null)) { + favoritesService.reportLaunch(child) + } } fun delete(context: Context) { 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 f2d1a163..c87d162d 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 @@ -193,6 +193,8 @@ fun ListItem( }, onLongClick = { onShowDetails(true) }), website = item, + onBack = { onShowDetails(false) }, + showDetails = showDetails, ) } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt index 9d29e4fe..cbc81028 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt @@ -5,33 +5,35 @@ import android.content.Intent import android.net.Uri import android.provider.Settings import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.animateDp -import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition import androidx.compose.animation.expandIn import androidx.compose.animation.shrinkOut +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material.icons.rounded.Tune -import androidx.compose.material.icons.rounded.Visibility -import androidx.compose.material.icons.rounded.VisibilityOff import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.InputChip +import androidx.compose.material3.InputChipDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -43,20 +45,18 @@ 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.geometry.Rect import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.roundToIntRect import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.lifecycleScope import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.search.AppShortcut import de.mm20.launcher2.ui.R -import de.mm20.launcher2.ui.animation.animateTextStyleAsState import de.mm20.launcher2.ui.component.DefaultToolbarAction import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.ShapedLauncherIcon @@ -68,9 +68,7 @@ import de.mm20.launcher2.ui.launcher.search.listItemViewModel import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled import de.mm20.launcher2.ui.locals.LocalGridSettings -import de.mm20.launcher2.ui.locals.LocalSnackbarHostState import de.mm20.launcher2.ui.modifier.scale -import kotlinx.coroutines.launch import kotlin.math.pow @Composable @@ -89,14 +87,243 @@ fun AppShortcutItem( viewModel.init(shortcut, iconSize.toInt()) } - val lifecycleOwner = LocalLifecycleOwner.current - val snackbarHostState = LocalSnackbarHostState.current + val badge by viewModel.badge.collectAsState(null) + val icon by viewModel.icon.collectAsStateWithLifecycle() var requestDelete by remember { mutableStateOf(false) } - val transition = updateTransition(showDetails, label = "AppShortcutItem") + SharedTransitionLayout( + modifier = modifier, + ) { + AnimatedContent(showDetails) { showDetails -> + if (showDetails) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + top = 16.dp, + start = 16.dp, + end = 16.dp, + bottom = 8.dp + ), + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + modifier = Modifier.sharedBounds( + rememberSharedContentState("label"), + this@AnimatedContent, + ), + text = shortcut.labelOverride ?: shortcut.label, + style = MaterialTheme.typography.titleMedium, + ) - Column( + val children by viewModel.children.collectAsState(emptyList()) + + for (app in children) { + val title = + app.labelOverride ?: app.label + val isPinned by remember(app) { viewModel.isChildPinned(app) }.collectAsState( + false + ) + + val iconSizePx = InputChipDefaults.AvatarSize.toPixels() + + val childIcon by + remember { + viewModel.getChildIcon( + app, + iconSizePx.toInt() + ) + }.collectAsState(null) + + InputChip( + modifier = Modifier.width(IntrinsicSize.Max).padding(top = 8.dp), + selected = false, + onClick = { + viewModel.launchChild(context, app) + }, + label = { + Text( + title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + }, + avatar = { + ShapedLauncherIcon( + size = InputChipDefaults.AvatarSize, + icon = { childIcon }, + shape = CircleShape, + ) + }, + trailingIcon = if (LocalFavoritesEnabled.current) { + { + Icon( + if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline, + null, + modifier = Modifier + .clip(CircleShape) + .requiredSize(InputChipDefaults.IconSize) + .clickable { + + if (isPinned) { + viewModel.unpinChild(app) + } else { + viewModel.pinChild(app) + } + }, + ) + } + } else null + ) + + } + } + ShapedLauncherIcon( + size = 48.dp, + modifier = Modifier + .sharedElement( + rememberSharedContentState("icon"), + this@AnimatedContent, + ), + badge = { badge }, + icon = { icon }, + ) + } + + if(shortcut.isUnavailable) { + MissingPermissionBanner( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp), + text = stringResource(R.string.shortcut_unavailable_description, stringResource(R.string.app_name)), + onClick = { + viewModel.requestShortcutPermission(context as AppCompatActivity) + } + ) + } + + 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) + } + + val packageName = shortcut.packageName + if (packageName != null) { + toolbarActions.add( + DefaultToolbarAction( + label = stringResource(R.string.menu_app_info), + icon = Icons.Rounded.Info + ) { + context.tryStartActivity( + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.parse("package:$packageName") + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + ) + }) + } + + + val sheetManager = LocalBottomSheetManager.current + toolbarActions.add(DefaultToolbarAction( + label = stringResource(R.string.menu_customize), + icon = Icons.Rounded.Tune, + action = { sheetManager.showCustomizeSearchableModal(shortcut) } + )) + + if (shortcut.canDelete) { + toolbarActions.add(DefaultToolbarAction( + label = stringResource(R.string.menu_delete), + icon = Icons.Rounded.Delete, + action = { requestDelete = true } + )) + } + + Toolbar( + leftActions = listOf( + DefaultToolbarAction( + label = stringResource(id = R.string.menu_back), + icon = Icons.AutoMirrored.Rounded.ArrowBack + ) { + onBack() + } + ), + rightActions = toolbarActions + ) + } + } else { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + top = 8.dp, + bottom = 8.dp, + end = 8.dp + ), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + modifier = Modifier.sharedBounds( + rememberSharedContentState("label"), + this@AnimatedContent, + ), + text = shortcut.labelOverride ?: shortcut.label, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + shortcut.appName?.let { + Text( + text = stringResource(R.string.shortcut_summary, it), + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 2.dp) + ) + } + } + ShapedLauncherIcon( + size = 48.dp, + modifier = Modifier + .padding(8.dp) + .sharedElement( + rememberSharedContentState("icon"), + this@AnimatedContent, + ), + badge = { badge }, + icon = { icon }, + ) + } + } + } + } + + /*Column( modifier = modifier ) { AnimatedVisibility(showDetails && shortcut.isUnavailable) { @@ -146,15 +373,12 @@ fun AppShortcutItem( ) } } - val badge by viewModel.badge.collectAsState(null) - val size by animateDpAsState(if (showDetails) 84.dp else 48.dp) - val icon by viewModel.icon.collectAsStateWithLifecycle() val padding by transition.animateDp(label = "iconPadding") { if (it) 16.dp else 8.dp } ShapedLauncherIcon( - size = size, + size = 48.dp, modifier = Modifier .padding(padding), badge = { badge }, @@ -231,7 +455,7 @@ fun AppShortcutItem( rightActions = toolbarActions ) } - } + }*/ if (requestDelete) { AlertDialog( @@ -278,10 +502,6 @@ fun ShortcutItemGridPopup( AppShortcutItem( modifier = Modifier .fillMaxWidth() - .scale( - 1 - (1 - LocalGridSettings.current.iconSize / 84f) * (1 - animationProgress), - transformOrigin = TransformOrigin(1f, 0f) - ) .offset( x = 16.dp * (1 - animationProgress).pow(10), y = -16.dp * (1 - animationProgress), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItem.kt index 304d1ad6..4e63da2d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItem.kt @@ -1,19 +1,21 @@ package de.mm20.launcher2.ui.launcher.search.website +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.tween import androidx.compose.animation.expandIn import androidx.compose.animation.shrinkOut import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material.icons.rounded.Edit +import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.rounded.Share import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.StarOutline @@ -26,13 +28,16 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Rect import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.roundToIntRect import coil.compose.AsyncImage +import coil.request.ImageRequest import de.mm20.launcher2.search.Website import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.DefaultToolbarAction @@ -49,6 +54,7 @@ import de.mm20.launcher2.ui.locals.LocalGridSettings fun WebsiteItem( modifier: Modifier = Modifier, website: Website, + showDetails: Boolean = false, onBack: (() -> Unit)? = null ) { val context = LocalContext.current @@ -60,96 +66,212 @@ fun WebsiteItem( viewModel.init(website, iconSize.toInt()) } - Column( - modifier = modifier.clickable { - viewModel.launch(context) - } - ) { - if (website.imageUrl != null) { - AsyncImage( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(16f / 9f) - .background(MaterialTheme.colorScheme.secondaryContainer), - model = website.imageUrl, - contentScale = ContentScale.Crop, - contentDescription = null - ) - } - Column( - modifier = Modifier.padding(16.dp), - ) { - Text( - text = website.labelOverride ?: website.label, - style = MaterialTheme.typography.titleLarge - ) - val tags by viewModel.tags.collectAsState(emptyList()) - if (tags.isNotEmpty()) { - Text( - modifier = Modifier.padding(top = 2.dp, bottom = 2.dp), - text = tags.joinToString(separator = " #", prefix = "#"), - color = MaterialTheme.colorScheme.secondary, - style = MaterialTheme.typography.labelSmall - ) - } - Text( - modifier = Modifier.padding(vertical = 8.dp), - text = website.description ?: "", - style = MaterialTheme.typography.bodySmall - ) - } - val toolbarActions = mutableListOf() + SharedTransitionScope { + AnimatedContent( + showDetails, + modifier = it then modifier, + ) { showDetails -> + Column { + if (!showDetails) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = if (website.imageUrl == null && website.description == null) Alignment.CenterVertically + else Alignment.Top, + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(16.dp) + ) { + Text( + text = website.labelOverride ?: website.label, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .sharedBounds( + rememberSharedContentState("title"), + this@AnimatedContent, + ), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) - 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() - onBack?.invoke() + Text( + modifier = Modifier + .padding(top = 4.dp, bottom = 4.dp) + .sharedBounds( + rememberSharedContentState("summary"), + this@AnimatedContent, + ), + text = website.description ?: website.url, + style = MaterialTheme.typography.bodySmall, + ) + } + if (!website.imageUrl.isNullOrEmpty()) { + AsyncImage( + modifier = Modifier + .padding(end = 12.dp, top = 12.dp, bottom = 12.dp) + .size(72.dp) + .sharedBounds( + rememberSharedContentState("image"), + this@AnimatedContent, + resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds + ) + .background( + MaterialTheme.colorScheme.secondaryContainer, + MaterialTheme.shapes.small + ) + .clip(MaterialTheme.shapes.small), + model = website.imageUrl, + contentScale = ContentScale.Crop, + contentDescription = null + ) + } else if (website.faviconUrl != null) { + AsyncImage( + modifier = Modifier + .padding(end = 16.dp, top = 12.dp, bottom = 12.dp) + .sharedElement( + rememberSharedContentState("favicon"), + this@AnimatedContent, + ) + .background( + MaterialTheme.colorScheme.secondaryContainer, + MaterialTheme.shapes.small + ) + .size(48.dp) + .padding(8.dp) + .clip(MaterialTheme.shapes.small), + model = website.faviconUrl, + contentScale = ContentScale.Crop, + contentDescription = null + ) + } + } + } else { + if (!website.imageUrl.isNullOrEmpty()) { + AsyncImage( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(16f / 9f) + .sharedBounds( + rememberSharedContentState("image"), + this@AnimatedContent, + resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds + ) + .background(MaterialTheme.colorScheme.secondaryContainer), + model = ImageRequest.Builder(context).data(website.imageUrl) + .crossfade(false).build(), + contentScale = ContentScale.Crop, + contentDescription = null + ) } - ) - } else { - DefaultToolbarAction( - label = stringResource(R.string.menu_favorites_pin), - icon = Icons.Rounded.StarOutline, - action = { - viewModel.pin() - onBack?.invoke() - }) - } - toolbarActions.add(favAction) - } - toolbarActions.add( - DefaultToolbarAction( - label = stringResource(R.string.menu_share), - icon = Icons.Rounded.Share, - action = { - website.share(context) + Row( + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.weight(1f).padding( + start = 16.dp, + end = 16.dp, + top = 16.dp, + ) + ) { + Text( + text = website.labelOverride ?: website.label, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.sharedBounds( + rememberSharedContentState("title"), + this@AnimatedContent, + ), + ) + } + if (website.imageUrl == null && website.faviconUrl != null) { + AsyncImage( + modifier = Modifier + .padding(end = 12.dp, top = 12.dp, bottom = 12.dp) + .sharedElement( + rememberSharedContentState("favicon"), + this@AnimatedContent, + ) + .background( + MaterialTheme.colorScheme.secondaryContainer, + MaterialTheme.shapes.small + ) + .size(48.dp) + .padding(8.dp) + .clip(MaterialTheme.shapes.small), + model = website.faviconUrl, + contentScale = ContentScale.Crop, + contentDescription = null + ) + } + } + + Text( + modifier = Modifier + .padding(16.dp) + .sharedBounds( + rememberSharedContentState("summary"), + this@AnimatedContent, + ), + text = website.description ?: website.url, + style = MaterialTheme.typography.bodySmall, + ) + + 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() + onBack?.invoke() + } + ) + } else { + DefaultToolbarAction( + label = stringResource(R.string.menu_favorites_pin), + icon = Icons.Rounded.StarOutline, + action = { + viewModel.pin() + onBack?.invoke() + }) + } + toolbarActions.add(favAction) + } + + toolbarActions.add( + DefaultToolbarAction( + label = stringResource(R.string.menu_share), + icon = Icons.Rounded.Share, + action = { + website.share(context) + } + ) + ) + + val sheetManager = LocalBottomSheetManager.current + toolbarActions.add(DefaultToolbarAction( + label = stringResource(R.string.menu_customize), + icon = Icons.Rounded.Tune, + action = { sheetManager.showCustomizeSearchableModal(website) } + )) + + Toolbar( + leftActions = if (onBack != null) listOf( + DefaultToolbarAction( + stringResource(id = R.string.menu_back), + icon = Icons.AutoMirrored.Rounded.ArrowBack, + action = onBack + ) + ) else emptyList(), + rightActions = toolbarActions + ) } - ) - ) - - val sheetManager = LocalBottomSheetManager.current - toolbarActions.add(DefaultToolbarAction( - label = stringResource(R.string.menu_customize), - icon = Icons.Rounded.Tune, - action = { sheetManager.showCustomizeSearchableModal(website) } - )) - - Toolbar( - leftActions = if (onBack != null) listOf( - DefaultToolbarAction( - stringResource(id = R.string.menu_back), - icon = Icons.Rounded.ArrowBack, - action = onBack - ) - ) else emptyList(), - rightActions = toolbarActions - ) + } + } } } @@ -176,7 +298,8 @@ fun WebsiteItemGridPopup( modifier = Modifier .fillMaxWidth(), website = website, - onBack = onDismiss + onBack = onDismiss, + showDetails = true, ) } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleItem.kt index 259e1ad1..01afb5fc 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleItem.kt @@ -84,7 +84,7 @@ fun ArticleItem( .padding(16.dp) ) { Text( - text = article.label, + text = article.labelOverride ?: article.label, style = MaterialTheme.typography.titleLarge, modifier = Modifier.sharedBounds( rememberSharedContentState("title"), @@ -165,7 +165,7 @@ fun ArticleItem( ) ) { Text( - text = article.label, + text = article.labelOverride ?: article.label, style = MaterialTheme.typography.titleLarge, modifier = Modifier.sharedBounds( rememberSharedContentState("title"), diff --git a/core/base/src/main/java/de/mm20/launcher2/search/AppShortcut.kt b/core/base/src/main/java/de/mm20/launcher2/search/AppShortcut.kt index 71e7cbb6..bc533a68 100644 --- a/core/base/src/main/java/de/mm20/launcher2/search/AppShortcut.kt +++ b/core/base/src/main/java/de/mm20/launcher2/search/AppShortcut.kt @@ -2,6 +2,8 @@ package de.mm20.launcher2.search import android.content.ComponentName import android.content.Context +import android.os.Process +import android.os.UserHandle import androidx.core.content.ContextCompat import de.mm20.launcher2.base.R import de.mm20.launcher2.icons.ColorLayer @@ -13,6 +15,8 @@ interface AppShortcut : SavableSearchable { val appName: String? val componentName: ComponentName? val packageName: String? + val user: UserHandle + get() = Process.myUserHandle() override val preferDetailsOverLaunch: Boolean get() = false diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 012f406a..8ef090a1 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -964,4 +964,8 @@ %1$s postal address %1$s postal addresses + + %1$s notification + %1$s notifications + \ No newline at end of file diff --git a/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt b/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt index 527098bd..975c006b 100644 --- a/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt +++ b/core/permissions/src/main/java/de/mm20/launcher2/permissions/PermissionsManager.kt @@ -233,23 +233,8 @@ internal class PermissionsManagerImpl( } override fun onResume() { - val iterator = pendingPermissionRequests.iterator() - while (iterator.hasNext()) { - when (iterator.next()) { - PermissionGroup.ExternalStorage -> { - externalStoragePermissionState.value = - checkPermissionOnce(PermissionGroup.ExternalStorage) - } - - PermissionGroup.AppShortcuts -> { - appShortcutsPermissionState.value = - checkPermissionOnce(PermissionGroup.AppShortcuts) - } - - else -> {} - } - iterator.remove() - } + externalStoragePermissionState.value = checkPermissionOnce(PermissionGroup.ExternalStorage) + appShortcutsPermissionState.value = checkPermissionOnce(PermissionGroup.AppShortcuts) } override fun reportNotificationListenerState(running: Boolean) { diff --git a/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt index fcb5f7c6..4759de89 100644 --- a/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt +++ b/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt @@ -29,6 +29,10 @@ import org.apache.commons.text.similarity.FuzzyScore import java.util.Locale interface AppRepository : SearchableRepository { + fun findOne( + packageName: String, + user: UserHandle, + ): Flow fun findMany(): Flow> } @@ -195,6 +199,17 @@ internal class AppRepositoryImpl( return LauncherApp(context, launcherActivityInfo) } + override fun findOne( + packageName: String, + user: UserHandle, + ): Flow { + return installedApps.map { + it.firstOrNull { + it.componentName.packageName == packageName && it.user == user + } + } + } + override fun findMany(): Flow> { return installedApps.map { it.toImmutableList() } } diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt index b4691d90..bd5d62a2 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt @@ -42,8 +42,11 @@ class LauncherShortcutDeserializer( val id = json.getString("id") val userSerial = json.optLong("user") + val userManager = context.getSystemService()!! + val user = userManager.getUserForSerialNumber(userSerial) ?: return null + if (!launcherApps.hasShortcutHostPermission()) { - return UnavailableShortcut(context, id, packageName, userSerial) + return UnavailableShortcut(context, id, packageName, user, userSerial) } else { val query = LauncherApps.ShortcutQuery() @@ -55,8 +58,6 @@ class LauncherShortcutDeserializer( LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER ) query.setShortcutIds(mutableListOf(id)) - val userManager = context.getSystemService()!! - val user = userManager.getUserForSerialNumber(userSerial) ?: return null val shortcuts = try { launcherApps.getShortcuts(query, user) } catch (e: IllegalStateException) { diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt index 3640fd6d..d72ec723 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt @@ -10,6 +10,7 @@ import android.content.pm.ShortcutInfo import android.graphics.drawable.AdaptiveIconDrawable import android.os.Bundle import android.os.Process +import android.os.UserHandle import android.util.Log import androidx.core.content.ContextCompat import androidx.core.content.getSystemService @@ -41,6 +42,9 @@ internal data class LauncherShortcut( override val packageName: String get() = launcherShortcut.`package` + override val user: UserHandle + get() = launcherShortcut.userHandle + constructor( context: Context, launcherShortcut: ShortcutInfo, diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/UnavailableShortcut.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/UnavailableShortcut.kt index 44d99017..6a47c23b 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/UnavailableShortcut.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/UnavailableShortcut.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.pm.PackageManager import android.os.Bundle import android.os.Process +import android.os.UserHandle import android.os.UserManager import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.StaticLauncherIcon @@ -24,15 +25,15 @@ internal class UnavailableShortcut( override val packageName: String, val shortcutId: String, val isMainProfile: Boolean, + override val user: UserHandle, val userSerial: Long, ): AppShortcut { - override val key: String get() = if (isMainProfile) { "$domain://${packageName}/${shortcutId}" } else { - "$domain://${packageName}/${shortcutId}:userSerial" + "$domain://${packageName}/${shortcutId}:$userSerial" } override val labelOverride: String? @@ -70,19 +71,19 @@ internal class UnavailableShortcut( get() = if (isMainProfile) AppProfile.Personal else AppProfile.Work companion object { - internal operator fun invoke(context: Context, id: String, packageName: String, userSerial: Long): UnavailableShortcut? { + internal operator fun invoke(context: Context, id: String, packageName: String, user: UserHandle, userSerial: Long): UnavailableShortcut? { val appInfo = try { context.packageManager.getApplicationInfo(packageName, 0) } catch (e: PackageManager.NameNotFoundException) { return null } - val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager return UnavailableShortcut( label = context.getString(R.string.shortcut_label_unavailable), appName = appInfo.loadLabel(context.packageManager).toString(), packageName = packageName, shortcutId = id, - isMainProfile = userManager.getUserForSerialNumber(userSerial) == Process.myUserHandle(), + isMainProfile = user == Process.myUserHandle(), + user = user, userSerial = userSerial, ) } diff --git a/data/notifications/src/main/java/de/mm20/launcher2/notifications/Notification.kt b/data/notifications/src/main/java/de/mm20/launcher2/notifications/Notification.kt index b8ce25af..0337699a 100644 --- a/data/notifications/src/main/java/de/mm20/launcher2/notifications/Notification.kt +++ b/data/notifications/src/main/java/de/mm20/launcher2/notifications/Notification.kt @@ -70,10 +70,10 @@ data class Notification( get() = extras.getInt(NotificationCompat.EXTRA_PROGRESS_MAX).takeIf { it > 0 } val title: String? - get() = extras.getString(NotificationCompat.EXTRA_TITLE) + get() = extras.getString(NotificationCompat.EXTRA_TITLE)?.takeIf { it.isNotBlank() } val text: String? - get() = extras.getString(NotificationCompat.EXTRA_TEXT) + get() = extras.getString(NotificationCompat.EXTRA_TEXT)?.takeIf { it.isNotBlank() } val isGroupSummary: Boolean get() = flags and NotificationCompat.FLAG_GROUP_SUMMARY != 0 diff --git a/data/websites/src/main/java/de/mm20/launcher2/websites/Website.kt b/data/websites/src/main/java/de/mm20/launcher2/websites/Website.kt index 436a560d..c66e8427 100644 --- a/data/websites/src/main/java/de/mm20/launcher2/websites/Website.kt +++ b/data/websites/src/main/java/de/mm20/launcher2/websites/Website.kt @@ -16,10 +16,10 @@ import java.util.concurrent.ExecutionException internal data class WebsiteImpl( override val label: String, override val url: String, - override val description: String, - override val imageUrl: String, - override val faviconUrl: String, - override val color: Int, + override val description: String?, + override val imageUrl: String?, + override val faviconUrl: String?, + override val color: Int?, override val labelOverride: String? = null, ) : Website { @@ -38,7 +38,7 @@ internal data class WebsiteImpl( size: Int, themed: Boolean, ): LauncherIcon? { - if (faviconUrl.isEmpty()) return null + if (faviconUrl == null) return null try { val request = ImageRequest.Builder(context) .data(faviconUrl) @@ -61,7 +61,7 @@ internal data class WebsiteImpl( } override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { - val color = if (color != 0) color else 0xFFF76F8E.toInt() + val color = color ?: 0xFFF76F8E.toInt() if (label.isNotBlank()) { return StaticLauncherIcon( foregroundLayer = TextLayer(text = label[0].toString(), color = color), diff --git a/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt b/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt index f32ab7b6..8990a520 100644 --- a/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt +++ b/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt @@ -95,10 +95,10 @@ internal class WebsiteRepository( return@withContext WebsiteImpl( label = title, url = url, - description = description, - imageUrl = image, - faviconUrl = favicon, - color = color + description = description.takeIf { it.isNotBlank() }, + imageUrl = image.takeIf { it.isNotBlank() }, + faviconUrl = favicon.takeIf { it.isNotBlank() }, + color = color.takeIf { it != 0 } ) } catch (e: IOException) { //Ignore. Not a HTML page or no connection. No result for this query diff --git a/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteSerialization.kt b/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteSerialization.kt index 31093033..003f1ccf 100644 --- a/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteSerialization.kt +++ b/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteSerialization.kt @@ -28,11 +28,11 @@ class WebsiteDeserializer: SearchableDeserializer { val json = JSONObject(serialized) return WebsiteImpl( label = json.getString("label"), - faviconUrl = json.getString("favicon"), - imageUrl = json.getString("image"), - description = json.getString("description"), + faviconUrl = json.getString("favicon").takeIf { it.isNotBlank() }, + imageUrl = json.getString("image").takeIf { it.isNotBlank() }, + description = json.getString("description").takeIf { it.isNotBlank() }, url = json.getString("url"), - color = json.getInt("color") + color = json.getInt("color").takeIf { it != 0 } ) } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 96cda4be..a2429292 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -104,7 +104,7 @@ androidx-exifinterface = { group = "androidx.exifinterface", name = "exifinterfa androidx-securitycrypto = { group = "androidx.security", name = "security-crypto", version = "1.1.0-alpha03" } androidx-datastore = { group = "androidx.datastore", name = "datastore", version = "1.0.0" } -androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version = "2.8.0-beta03" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version = "2.8.0-beta04" } materialcomponents-core = { group = "com.google.android.material", name = "material", version = "1.11.0" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version = "4.12.0" }