Further search result design adjustments

This commit is contained in:
MM20
2024-06-29 21:57:31 +02:00
parent 7258d456d3
commit 304ca12e65
18 changed files with 775 additions and 336 deletions
@@ -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<ToolbarAction>()
@@ -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)
@@ -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<LauncherIcon?> {
return iconService.getIcon(shortcut, size)
fun getChildIcon(child: SavableSearchable, size: Int): Flow<LauncherIcon?> {
return iconService.getIcon(child, size)
}
fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> {
return favoritesService.isPinned(shortcut)
fun isChildPinned(child: SavableSearchable): Flow<Boolean> {
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) {
@@ -193,6 +193,8 @@ fun ListItem(
},
onLongClick = { onShowDetails(true) }),
website = item,
onBack = { onShowDetails(false) },
showDetails = showDetails,
)
}
}
@@ -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<ToolbarAction>()
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),
@@ -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<ToolbarAction>()
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<ToolbarAction>()
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,
)
}
}
@@ -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"),