Further search result design adjustments
This commit is contained in:
parent
7258d456d3
commit
304ca12e65
@ -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"),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -964,4 +964,8 @@
|
||||
<item quantity="one">%1$s postal address</item>
|
||||
<item quantity="other">%1$s postal addresses</item>
|
||||
</plurals>
|
||||
<plurals name="app_info_notifications">
|
||||
<item quantity="one">%1$s notification</item>
|
||||
<item quantity="other">%1$s notifications</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@ -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) {
|
||||
|
||||
@ -29,6 +29,10 @@ import org.apache.commons.text.similarity.FuzzyScore
|
||||
import java.util.Locale
|
||||
|
||||
interface AppRepository : SearchableRepository<Application> {
|
||||
fun findOne(
|
||||
packageName: String,
|
||||
user: UserHandle,
|
||||
): Flow<Application?>
|
||||
fun findMany(): Flow<ImmutableList<Application>>
|
||||
}
|
||||
|
||||
@ -195,6 +199,17 @@ internal class AppRepositoryImpl(
|
||||
return LauncherApp(context, launcherActivityInfo)
|
||||
}
|
||||
|
||||
override fun findOne(
|
||||
packageName: String,
|
||||
user: UserHandle,
|
||||
): Flow<Application?> {
|
||||
return installedApps.map {
|
||||
it.firstOrNull {
|
||||
it.componentName.packageName == packageName && it.user == user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun findMany(): Flow<ImmutableList<Application>> {
|
||||
return installedApps.map { it.toImmutableList() }
|
||||
}
|
||||
|
||||
@ -42,8 +42,11 @@ class LauncherShortcutDeserializer(
|
||||
val id = json.getString("id")
|
||||
val userSerial = json.optLong("user")
|
||||
|
||||
val userManager = context.getSystemService<UserManager>()!!
|
||||
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<UserManager>()!!
|
||||
val user = userManager.getUserForSerialNumber(userSerial) ?: return null
|
||||
val shortcuts = try {
|
||||
launcherApps.getShortcuts(query, user)
|
||||
} catch (e: IllegalStateException) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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" }
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user