Further search result design adjustments
This commit is contained in:
@@ -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)
|
||||
|
||||
+39
-22
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+247
-27
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user