Reuse search result item viewmodels

This commit is contained in:
MM20
2023-04-21 13:37:00 +02:00
parent d1482ad112
commit df1aa8c119
29 changed files with 658 additions and 481 deletions
@@ -0,0 +1,69 @@
package de.mm20.launcher2.ui.launcher.search
import android.util.LruCache
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
class ListItemViewModelStore() {
private val cache = ViewModelCache()
operator fun <T: ListItemViewModel>get(key: String, modelClass: Class<T>): T {
val cachedInstance = cache[key]
if (cachedInstance != null) {
return cachedInstance as T
}
val newInstance = modelClass.getDeclaredConstructor().newInstance()
cache.put(key, newInstance)
return newInstance
}
inline operator fun <reified T: ListItemViewModel>get(key: String): T {
return get(key, T::class.java)
}
fun clear() {
cache.evictAll()
}
}
private class ViewModelCache: LruCache<String, ListItemViewModel>(500) {
override fun entryRemoved(
evicted: Boolean,
key: String?,
oldValue: ListItemViewModel?,
newValue: ListItemViewModel?
) {
super.entryRemoved(evicted, key, oldValue, newValue)
oldValue?.clear()
}
}
/**
* Knock-off of Android's ViewModel class but not tied to a lifecycle.
* This is useful for view models that are not tied to a lifecycle, e.g. for list items.
*/
open class ListItemViewModel {
val viewModelScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
fun clear() {
viewModelScope.coroutineContext.cancel()
onCleared()
}
protected open fun onCleared() {
}
}
val LocalListItemViewModelStore = staticCompositionLocalOf { ListItemViewModelStore() }
@Composable
inline fun <reified T: ListItemViewModel>listItemViewModel(key: String): T {
val store = LocalListItemViewModelStore.current
return remember(key) {
store[key]
}
}
@@ -1,5 +1,10 @@
package de.mm20.launcher2.ui.launcher.search.apps
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import androidx.compose.animation.*
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
@@ -20,7 +25,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.rememberAsyncImagePainter
import com.google.accompanist.flowlayout.FlowRow
import de.mm20.launcher2.search.data.LauncherApp
@@ -28,6 +36,8 @@ import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.*
import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
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
@@ -43,7 +53,13 @@ fun AppItem(
app: LauncherApp,
onBack: () -> Unit
) {
val viewModel = remember { AppItemVM(app) }
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${app.key}")
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
LaunchedEffect(app) {
viewModel.init(app, iconSize.toInt())
}
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val snackbarHostState = LocalSnackbarHostState.current
@@ -63,7 +79,7 @@ fun AppItem(
style = MaterialTheme.typography.titleMedium
)
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
val tags by viewModel.tags.collectAsState(emptyList())
if (tags.isNotEmpty()) {
Text(
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
@@ -98,7 +114,7 @@ fun AppItem(
mainAxisSpacing = 12.dp,
crossAxisSpacing = 0.dp
) {
val notifications by viewModel.notifications.collectAsState(initial = emptyList())
val notifications by viewModel.notifications.collectAsState(emptyList())
for (not in notifications) {
val title =
@@ -126,7 +142,9 @@ fun AppItem(
viewModel.clearNotification(not)
},
onClick = {
viewModel.openNotification(not)
try {
not.notification.contentIntent?.send()
} catch (e: PendingIntent.CanceledException) {}
}
)
}
@@ -173,9 +191,8 @@ fun AppItem(
}
}
}
val badge by viewModel.badge.collectAsState(null)
val iconSize = 84.dp.toPixels().toInt()
val icon by remember(app) { viewModel.getIcon(iconSize) }.collectAsState(null)
val badge by viewModel.badge.collectAsStateWithLifecycle(null)
val icon by viewModel.icon.collectAsStateWithLifecycle()
ShapedLauncherIcon(
size = 84.dp,
modifier = Modifier
@@ -213,7 +230,7 @@ fun AppItem(
label = stringResource(R.string.menu_app_info),
icon = Icons.Rounded.Info
) {
viewModel.openAppInfo(context)
app.openAppInfo(context)
})
toolbarActions.add(
@@ -240,7 +257,7 @@ fun AppItem(
icon = Icons.Rounded.Share
) {
scope.launch {
viewModel.shareApkFile(context)
app.shareApkFile(context)
}
}
} else {
@@ -252,7 +269,10 @@ fun AppItem(
label = stringResource(R.string.menu_share_store_link, storeDetails.label),
icon = Icons.Rounded.Link,
action = {
viewModel.shareStoreLink(context, storeDetails.url)
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_TEXT, storeDetails.url)
shareIntent.type = "text/plain"
context.startActivity(Intent.createChooser(shareIntent, null))
}
),
DefaultToolbarAction(
@@ -260,7 +280,7 @@ fun AppItem(
icon = Icons.Rounded.Android
) {
scope.launch {
viewModel.shareApkFile(context)
app.shareApkFile(context)
}
}
)
@@ -268,13 +288,13 @@ fun AppItem(
}
toolbarActions.add(shareAction)
if (viewModel.canUninstall) {
if (app.canUninstall) {
toolbarActions.add(
DefaultToolbarAction(
label = stringResource(R.string.menu_uninstall),
icon = Icons.Rounded.Delete,
) {
viewModel.uninstall(context)
app.uninstall(context)
onBack()
}
)
@@ -371,4 +391,4 @@ fun AppItemGridPopup(
)
}
}
}
}
@@ -1,137 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.apps
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.*
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Process
import android.service.notification.StatusBarNotification
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.koin.core.component.inject
class AppItemVM(
private val app: LauncherApp
) : SearchableItemVM(app) {
private val notificationRepository: NotificationRepository by inject()
private val appShortcutRepository: AppShortcutRepository by inject()
val notifications =
notificationRepository.notifications.map { it.filter { it.packageName == app.`package` } }
fun clearNotification(notification: StatusBarNotification) {
notificationRepository.cancelNotification(notification)
}
fun openAppInfo(context: Context) {
val launcherApps = context.getSystemService<LauncherApps>()!!
launcherApps.startAppDetailsActivity(
ComponentName(app.`package`, app.activity),
app.getUser(),
null,
null
)
}
suspend fun shareApkFile(context: Context) {
val launcherApps = context.getSystemService<LauncherApps>()!!
val fileCopy = java.io.File(
context.cacheDir,
"${app.`package`}-${app.version}.apk"
)
withContext(Dispatchers.IO) {
try {
val user = (app as? LauncherApp)?.getUser()
val info = if (user != null) {
launcherApps.getApplicationInfo(app.`package`, 0, user)
} else {
context.packageManager.getApplicationInfo(app.`package`, 0)
}
val file = java.io.File(info.publicSourceDir)
try {
file.copyTo(fileCopy, false)
} catch (e: FileAlreadyExistsException) {
// Do nothing. If the file is already there we don't have to copy it again.
}
} catch (e: PackageManager.NameNotFoundException) {
CrashReporter.logException(e)
}
}
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val uri = FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".fileprovider",
fileCopy
)
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
shareIntent.type = "application/vnd.android.package-archive"
withContext(Dispatchers.Main) {
context.startActivity(Intent.createChooser(shareIntent, null))
}
}
fun shareStoreLink(context: Context, url: String) {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_TEXT, url)
shareIntent.type = "text/plain"
context.startActivity(Intent.createChooser(shareIntent, null))
}
val canUninstall = app.flags and ApplicationInfo.FLAG_SYSTEM == 0 && (app as? LauncherApp)?.getUser() == Process.myUserHandle()
fun uninstall(context: Context) {
val intent = Intent(Intent.ACTION_DELETE)
intent.data = Uri.parse("package:" + app.`package`)
context.startActivity(intent)
}
fun openNotification(notification: StatusBarNotification) {
try {
notification.notification.contentIntent?.send()
} catch (e: PendingIntent.CanceledException) {}
}
fun getShortcutIcon(context: Context, shortcut: ShortcutInfo) : Drawable? {
val launcherApps = context.getSystemService<LauncherApps>() ?: return null
return launcherApps.getShortcutIconDrawable(shortcut, 0)
}
val shortcuts = flow {
emit(appShortcutRepository.getShortcutsForActivity(app.launcherActivityInfo, 5))
}
fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> {
return searchableRepository.isPinned(shortcut)
}
fun pinShortcut(shortcut: AppShortcut) {
favoritesService.pinItem(shortcut)
}
fun unpinShortcut(shortcut: AppShortcut) {
favoritesService.unpinItem(shortcut)
}
fun launchShortcut(context: Context, shortcut: AppShortcut) {
shortcut.launch(context, null)
}
}
@@ -1,16 +1,46 @@
package de.mm20.launcher2.ui.launcher.search.calendar
import androidx.compose.animation.*
import android.content.Context
import android.text.format.DateUtils
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.with
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Notes
import androidx.compose.material.icons.rounded.OpenInNew
import androidx.compose.material.icons.rounded.People
import androidx.compose.material.icons.rounded.Place
import androidx.compose.material.icons.rounded.Schedule
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.Icon
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@@ -29,9 +59,13 @@ import de.mm20.launcher2.ui.component.DefaultToolbarAction
import de.mm20.launcher2.ui.component.Toolbar
import de.mm20.launcher2.ui.component.ToolbarAction
import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import de.mm20.launcher2.ui.launcher.search.listItemViewModel
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
import de.mm20.launcher2.ui.locals.LocalGridSettings
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
import kotlinx.coroutines.launch
import palettes.TonalPalette
@@ -44,7 +78,12 @@ fun CalendarItem(
onBack: () -> Unit
) {
val context = LocalContext.current
val viewModel = remember(calendar.key) { CalendarItemVM(calendar) }
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${calendar.key}")
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
LaunchedEffect(calendar) {
viewModel.init(calendar, iconSize.toInt())
}
val lifecycleOwner = LocalLifecycleOwner.current
val snackbarHostState = LocalSnackbarHostState.current
@@ -54,9 +93,11 @@ fun CalendarItem(
Row(
modifier = modifier
.drawBehind {
val color = TonalPalette.fromInt(calendar.color).tone(
if (darkMode) 80 else 40
)
val color = TonalPalette
.fromInt(calendar.color)
.tone(
if (darkMode) 80 else 40
)
drawRect(Color(color), Offset.Zero, this.size.copy(width = 8.dp.toPx()))
}
.padding(start = 8.dp),
@@ -82,12 +123,12 @@ fun CalendarItem(
AnimatedVisibility(!showDetails) {
Text(
modifier = Modifier.padding(top = 2.dp),
text = viewModel.getSummary(context),
text = calendar.getSummary(context),
style = MaterialTheme.typography.bodySmall
)
}
AnimatedVisibility(showDetails) {
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
val tags by viewModel.tags.collectAsState(emptyList())
if (tags.isNotEmpty()) {
Text(
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
@@ -112,7 +153,7 @@ fun CalendarItem(
contentDescription = null
)
Text(
text = viewModel.formatTime(context),
text = calendar.formatTime(context),
style = MaterialTheme.typography.bodySmall
)
}
@@ -136,7 +177,7 @@ fun CalendarItem(
if (calendar.attendees.isNotEmpty()) {
Row(
Modifier
.fillMaxWidth(),
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@@ -156,7 +197,7 @@ fun CalendarItem(
modifier = Modifier
.fillMaxWidth()
.clickable {
viewModel.openLocation(context)
calendar.openLocation(context)
}
) {
Icon(
@@ -213,11 +254,14 @@ fun CalendarItem(
onBack()
lifecycleOwner.lifecycleScope.launch {
val result = snackbarHostState.showSnackbar(
message = context.getString(R.string.msg_item_hidden, calendar.label),
message = context.getString(
R.string.msg_item_hidden,
calendar.label
),
actionLabel = context.getString(R.string.action_undo),
duration = SnackbarDuration.Short,
)
if(result == SnackbarResult.ActionPerformed) {
)
if (result == SnackbarResult.ActionPerformed) {
viewModel.unhide()
}
}
@@ -296,4 +340,53 @@ fun CalendarItemGridPopup(
)
}
}
}
private fun CalendarEvent.formatTime(context: Context): String {
if (allDay) return DateUtils.formatDateRange(
context,
startTime,
endTime,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY
)
return DateUtils.formatDateRange(
context,
startTime,
endTime,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_WEEKDAY
)
}
private fun CalendarEvent.getSummary(context: Context): String {
val isToday =
DateUtils.isToday(startTime) && DateUtils.isToday(endTime)
return if (isToday) {
if (allDay) {
context.getString(R.string.calendar_event_allday)
} else {
DateUtils.formatDateRange(
context,
startTime,
endTime,
DateUtils.FORMAT_SHOW_TIME
)
}
} else {
if (allDay) {
DateUtils.formatDateRange(
context,
startTime,
endTime,
DateUtils.FORMAT_SHOW_DATE
)
} else {
DateUtils.formatDateRange(
context,
startTime,
endTime,
DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE
)
}
}
}
@@ -1,82 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.calendar
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.text.format.DateUtils
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import java.net.URLEncoder
class CalendarItemVM(
private val calendarEvent: CalendarEvent
) : SearchableItemVM(calendarEvent) {
fun getSummary(context: Context): String {
val isToday =
DateUtils.isToday(calendarEvent.startTime) && DateUtils.isToday(calendarEvent.endTime)
return if (isToday) {
if (calendarEvent.allDay) {
context.getString(R.string.calendar_event_allday)
} else {
DateUtils.formatDateRange(
context,
calendarEvent.startTime,
calendarEvent.endTime,
DateUtils.FORMAT_SHOW_TIME
)
}
} else {
if (calendarEvent.allDay) {
DateUtils.formatDateRange(
context,
calendarEvent.startTime,
calendarEvent.endTime,
DateUtils.FORMAT_SHOW_DATE
)
} else {
DateUtils.formatDateRange(
context,
calendarEvent.startTime,
calendarEvent.endTime,
DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE
)
}
}
}
fun formatTime(context: Context): String {
if (calendarEvent.allDay) return DateUtils.formatDateRange(
context,
calendarEvent.startTime,
calendarEvent.endTime,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY
)
return DateUtils.formatDateRange(
context,
calendarEvent.startTime,
calendarEvent.endTime,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_WEEKDAY
)
}
fun openLocation(context: Context) {
context.tryStartActivity(
Intent(Intent.ACTION_VIEW)
.setData(
Uri.parse(
"geo:0,0?q=${
URLEncoder.encode(
calendarEvent.location,
"utf8"
)
}"
)
)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
}
}
@@ -1,60 +1,105 @@
package de.mm20.launcher2.ui.launcher.search.common
import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
import android.graphics.drawable.Drawable
import android.service.notification.StatusBarNotification
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.geometry.Rect
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.getSystemService
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.badges.BadgeService
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.files.FileRepository
import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.LauncherShortcut
import de.mm20.launcher2.services.favorites.FavoritesService
import de.mm20.launcher2.services.tags.TagsService
import de.mm20.launcher2.ui.launcher.search.ListItemViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
abstract class SearchableItemVM(
private val searchable: SavableSearchable
) : KoinComponent {
protected val favoritesService: FavoritesService by inject()
protected val searchableRepository: SearchableRepository by inject()
protected val badgeService: BadgeService by inject()
protected val iconService: IconService by inject()
protected val customAttributesRepository: CustomAttributesRepository by inject()
class SearchableItemVM : ListItemViewModel(), KoinComponent {
private val favoritesService: FavoritesService by inject()
private val badgeService: BadgeService by inject()
private val iconService: IconService by inject()
private val tagsService: TagsService by inject()
private val notificationRepository: NotificationRepository by inject()
private val appShortcutRepository: AppShortcutRepository by inject()
private val fileRepository: FileRepository by inject()
private val searchable = MutableStateFlow<SavableSearchable?>(null)
private val iconSize = MutableStateFlow<Int>(0)
fun init(searchable: SavableSearchable, iconSize: Int) {
this.searchable.value = searchable
this.iconSize.value = iconSize
}
val isPinned = searchable.flatMapLatest {
if (it == null) emptyFlow() else favoritesService.isPinned(it)
}
val isPinned = searchableRepository.isPinned(searchable)
fun pin() {
favoritesService.pinItem(searchable)
searchable.value?.let { favoritesService.pinItem(it) }
}
fun unpin() {
favoritesService.unpinItem(searchable)
searchable.value?.let { favoritesService.unpinItem(it) }
}
val isHidden = searchable.flatMapLatest {
if (it == null) emptyFlow() else favoritesService.isHidden(it)
}
val isHidden = searchableRepository.isHidden(searchable)
fun hide() {
searchableRepository.upsert(searchable, hidden = true, pinned = false)
searchable.value?.let { favoritesService.hideItem(it) }
}
fun unhide() {
searchableRepository.update(searchable, hidden = false)
searchable.value?.let { favoritesService.unhideItem(it) }
}
val badge = badgeService.getBadge(searchable)
val badge = searchable.flatMapLatest {
if (it == null) emptyFlow() else badgeService.getBadge(it)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun getIcon(size: Int): Flow<LauncherIcon> {
return iconService.getIcon(searchable, size)
val icon = searchable.combine(iconSize) { sh, sz -> sh to sz }.flatMapLatest { (s, size) ->
if (s == null || size == 0) emptyFlow() else iconService.getIcon(s, size)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val tags = searchable.flatMapLatest {
if (it == null) emptyFlow() else tagsService.getTags(it)
}
fun getTags(): Flow<List<String>> {
return customAttributesRepository.getTags(searchable)
val notifications = searchable.flatMapLatest { searchable ->
if (searchable !is LauncherApp) emptyFlow()
else notificationRepository.notifications.map { it.filter { it.packageName == searchable.`package` } }
}
val shortcuts = searchable.map {
if (it !is LauncherApp) emptyList()
else appShortcutRepository.getShortcutsForActivity(it.launcherActivityInfo, 5)
}
open fun launch(context: Context, bounds: Rect? = null): Boolean {
val searchable = searchable.value ?: return false
val view = (context as? AppCompatActivity)?.window?.decorView
val options = if (bounds != null && view != null) {
ActivityOptionsCompat.makeScaleUpAnimation(
@@ -72,8 +117,47 @@ abstract class SearchableItemVM(
favoritesService.reportLaunch(searchable)
return true
} else if (searchable is LauncherApp || searchable is AppShortcut) {
searchableRepository.delete(searchable)
favoritesService.reset(searchable)
}
return false
}
fun clearNotification(notification: StatusBarNotification) {
notificationRepository.cancelNotification(notification)
}
fun getShortcutIcon(context: Context, shortcut: ShortcutInfo): Drawable? {
val launcherApps = context.getSystemService<LauncherApps>() ?: return null
return launcherApps.getShortcutIconDrawable(shortcut, 0)
}
fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> {
return favoritesService.isPinned(shortcut)
}
fun pinShortcut(shortcut: AppShortcut) {
favoritesService.pinItem(shortcut)
}
fun unpinShortcut(shortcut: AppShortcut) {
favoritesService.unpinItem(shortcut)
}
fun launchShortcut(context: Context, shortcut: AppShortcut) {
shortcut.launch(context, null)
}
fun delete() {
val searchable = searchable.value ?: return
if (searchable is File) fileRepository.deleteFile(searchable)
if (searchable is LauncherShortcut) appShortcutRepository.removePinnedShortcut(searchable)
favoritesService.reset(searchable)
}
public override fun onCleared() {
super.onCleared()
Log.d("SearchableItemVM", "onCleared: ${searchable.value?.key}")
}
}
@@ -35,6 +35,8 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.AppShortcut
@@ -51,8 +53,10 @@ import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.apps.AppItemGridPopup
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItemGridPopup
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import de.mm20.launcher2.ui.launcher.search.contacts.ContactItemGridPopup
import de.mm20.launcher2.ui.launcher.search.files.FileItemGridPopup
import de.mm20.launcher2.ui.launcher.search.listItemViewModel
import de.mm20.launcher2.ui.launcher.search.shortcut.ShortcutItemGridPopup
import de.mm20.launcher2.ui.launcher.search.website.WebsiteItemGridPopup
import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaItemGridPopup
@@ -71,7 +75,12 @@ fun GridItem(
showLabels: Boolean = true,
highlight: Boolean = false
) {
val viewModel = remember(item.key) { GridItemVM(item) }
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${item.key}")
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
LaunchedEffect(item, iconSize) {
viewModel.init(item, iconSize.toInt())
}
val context = LocalContext.current
@@ -79,9 +88,8 @@ fun GridItem(
var bounds by remember { mutableStateOf(Rect.Zero) }
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
val badge by remember(item.key) { viewModel.badge }.collectAsState(null)
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
val icon by remember(item.key) { viewModel.getIcon(iconSize.toInt()) }.collectAsState(null)
val badge by remember(item.key) { viewModel.badge }.collectAsStateWithLifecycle()
val icon by viewModel.icon.collectAsStateWithLifecycle()
val launchOnPress = !item.preferDetailsOverLaunch
@@ -1,8 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.common.grid
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
class GridItemVM(
searchable: SavableSearchable
): SearchableItemVM(searchable)
@@ -8,13 +8,19 @@ import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.*
import de.mm20.launcher2.ui.component.InnerCard
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import de.mm20.launcher2.ui.launcher.search.contacts.ContactItem
import de.mm20.launcher2.ui.launcher.search.files.FileItem
import de.mm20.launcher2.ui.launcher.search.listItemViewModel
import de.mm20.launcher2.ui.launcher.search.shortcut.AppShortcutItem
import de.mm20.launcher2.ui.locals.LocalGridSettings
@Composable
fun ListItem(
@@ -25,7 +31,12 @@ fun ListItem(
var showDetails by remember { mutableStateOf(false) }
val context = LocalContext.current
val viewModel = remember(item.key) { ListItemVM(item) }
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${item.key}")
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
LaunchedEffect(item, iconSize) {
viewModel.init(item, iconSize.toInt())
}
var bounds by remember { mutableStateOf(Rect.Zero) }
InnerCard(
@@ -1,8 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.common.list
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
class ListItemVM(
searchable: SavableSearchable
): SearchableItemVM(searchable)
@@ -1,17 +1,47 @@
package de.mm20.launcher2.ui.launcher.search.contacts
import androidx.compose.animation.*
import android.content.Intent
import android.net.Uri
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.*
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.with
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Call
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Email
import androidx.compose.material.icons.rounded.OpenInNew
import androidx.compose.material.icons.rounded.Place
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.Icon
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
@@ -21,15 +51,23 @@ 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.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
import de.mm20.launcher2.ui.component.*
import de.mm20.launcher2.ui.component.Chip
import de.mm20.launcher2.ui.component.DefaultToolbarAction
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.Toolbar
import de.mm20.launcher2.ui.component.ToolbarAction
import de.mm20.launcher2.ui.icons.Telegram
import de.mm20.launcher2.ui.icons.WhatsApp
import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
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
@@ -45,7 +83,13 @@ fun ContactItem(
onBack: () -> Unit
) {
val context = LocalContext.current
val viewModel = remember(contact) { ContactItemVM(contact) }
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${contact.key}")
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
LaunchedEffect(contact, iconSize) {
viewModel.init(contact, iconSize.toInt())
}
val lifecycleOwner = LocalLifecycleOwner.current
val snackbarHostState = LocalSnackbarHostState.current
@@ -58,8 +102,7 @@ fun ContactItem(
Row(
verticalAlignment = Alignment.CenterVertically
) {
val iconSize = 48.dp.toPixels().toInt()
val icon by remember(contact) { viewModel.getIcon(iconSize) }.collectAsState(null)
val icon by viewModel.icon.collectAsStateWithLifecycle()
val padding by transition.animateDp(label = "iconPadding") {
if (it) 16.dp else 8.dp
}
@@ -92,7 +135,7 @@ fun ContactItem(
)
}
AnimatedVisibility(showDetails) {
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
val tags by viewModel.tags.collectAsState(emptyList())
if (tags.isNotEmpty()) {
Text(
modifier = Modifier.padding(top = 1.dp),
@@ -125,7 +168,10 @@ fun ContactItem(
modifier = Modifier.padding(end = 16.dp),
text = it.label,
onClick = {
viewModel.contact(context, it)
context.tryStartActivity(
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setData(Uri.parse(it.data))
)
}
)
}
@@ -149,7 +195,10 @@ fun ContactItem(
modifier = Modifier.padding(end = 16.dp),
text = it.label,
onClick = {
viewModel.contact(context, it)
context.tryStartActivity(
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setData(Uri.parse(it.data))
)
}
)
}
@@ -173,7 +222,10 @@ fun ContactItem(
modifier = Modifier.padding(end = 16.dp),
text = it.label,
onClick = {
viewModel.contact(context, it)
context.tryStartActivity(
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setData(Uri.parse(it.data))
)
}
)
}
@@ -197,7 +249,10 @@ fun ContactItem(
modifier = Modifier.padding(end = 16.dp),
text = it.label,
onClick = {
viewModel.contact(context, it)
context.tryStartActivity(
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setData(Uri.parse(it.data))
)
}
)
}
@@ -221,7 +276,10 @@ fun ContactItem(
modifier = Modifier.padding(end = 16.dp),
text = it.label,
onClick = {
viewModel.contact(context, it)
context.tryStartActivity(
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setData(Uri.parse(it.data))
)
}
)
}
@@ -271,11 +329,14 @@ fun ContactItem(
onBack()
lifecycleOwner.lifecycleScope.launch {
val result = snackbarHostState.showSnackbar(
message = context.getString(R.string.msg_item_hidden, contact.label),
message = context.getString(
R.string.msg_item_hidden,
contact.label
),
actionLabel = context.getString(R.string.action_undo),
duration = SnackbarDuration.Short,
)
if(result == SnackbarResult.ActionPerformed) {
)
if (result == SnackbarResult.ActionPerformed) {
viewModel.unhide()
}
}
@@ -1,20 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.contacts
import android.content.Context
import android.content.Intent
import android.net.Uri
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.ContactInfo
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
class ContactItemVM(
val contact: Contact
) : SearchableItemVM(contact) {
fun contact(context: Context, contactInfo: ContactInfo) {
context.tryStartActivity(
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setData(Uri.parse(contactInfo.data))
)
}
}
@@ -19,7 +19,9 @@ 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.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
@@ -29,6 +31,8 @@ import de.mm20.launcher2.ui.component.Toolbar
import de.mm20.launcher2.ui.component.ToolbarAction
import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
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
@@ -46,7 +50,13 @@ fun FileItem(
onBack: () -> Unit
) {
val context = LocalContext.current
val viewModel = remember(file.key) { FileItemVM(file) }
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${file.key}")
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
LaunchedEffect(file) {
viewModel.init(file, iconSize.toInt())
}
val lifecycleOwner = LocalLifecycleOwner.current
val snackbarHostState = LocalSnackbarHostState.current
@@ -85,7 +95,7 @@ fun FileItem(
}
AnimatedVisibility(showDetails) {
Column {
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
val tags by viewModel.tags.collectAsState(emptyList())
if (tags.isNotEmpty()) {
Text(
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
@@ -128,8 +138,7 @@ fun FileItem(
}
}
val iconSize = 48.dp.toPixels().toInt()
val icon by remember(file) { viewModel.getIcon(iconSize) }.collectAsState(null)
val icon by viewModel.icon.collectAsStateWithLifecycle()
val badge by viewModel.badge.collectAsState(null)
val padding by transition.animateDp(label = "iconPadding") {
if (it) 16.dp else 8.dp
@@ -180,17 +189,17 @@ fun FileItem(
)
)
if (viewModel.canShare) {
if (file.canShare) {
toolbarActions.add(DefaultToolbarAction(
label = stringResource(R.string.menu_share),
icon = Icons.Rounded.Share,
action = {
viewModel.share(context)
file.share(context)
}
))
}
if (viewModel.canDelete) {
if (file.isDeletable) {
var showConfirmDialog by remember { mutableStateOf(false) }
toolbarActions.add(DefaultToolbarAction(
label = stringResource(R.string.menu_delete),
@@ -1,36 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.files
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import de.mm20.launcher2.files.FileRepository
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.LocalFile
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import org.koin.core.component.inject
class FileItemVM(
private val file: File
) : SearchableItemVM(file) {
private val fileRepository: FileRepository by inject()
val canShare = file is LocalFile
val canDelete = file.isDeletable
fun delete() {
fileRepository.deleteFile(file)
}
fun share(context: Context) {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val uri = FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".fileprovider",
java.io.File(file.path)
)
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
shareIntent.type = file.mimeType
context.startActivity(Intent.createChooser(shareIntent, null))
}
}
@@ -1,6 +1,9 @@
package de.mm20.launcher2.ui.launcher.search.shortcut
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
@@ -16,8 +19,13 @@ 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.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.LauncherShortcut
import de.mm20.launcher2.search.data.LegacyShortcut
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
import de.mm20.launcher2.ui.component.DefaultToolbarAction
@@ -26,6 +34,8 @@ import de.mm20.launcher2.ui.component.Toolbar
import de.mm20.launcher2.ui.component.ToolbarAction
import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
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
@@ -42,9 +52,15 @@ fun AppShortcutItem(
showDetails: Boolean = false,
onBack: () -> Unit
) {
val viewModel = remember { ShortcutItemVM(shortcut) }
val context = LocalContext.current
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${shortcut.key}")
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
LaunchedEffect(shortcut, iconSize) {
viewModel.init(shortcut, iconSize.toInt())
}
val lifecycleOwner = LocalLifecycleOwner.current
val snackbarHostState = LocalSnackbarHostState.current
@@ -70,7 +86,7 @@ fun AppShortcutItem(
)
AnimatedVisibility(showDetails) {
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
val tags by viewModel.tags.collectAsState(emptyList())
if (tags.isNotEmpty()) {
Text(
modifier = Modifier.padding(top = 1.dp, bottom = 2.dp),
@@ -95,8 +111,7 @@ fun AppShortcutItem(
}
val badge by viewModel.badge.collectAsState(null)
val size by animateDpAsState(if (showDetails) 84.dp else 48.dp)
val iconSize = 84.dp.toPixels().toInt()
val icon by remember(shortcut.key) { viewModel.getIcon(iconSize) }.collectAsState(null)
val icon by viewModel.icon.collectAsStateWithLifecycle()
val padding by transition.animateDp(label = "iconPadding") {
if (it) 16.dp else 8.dp
@@ -136,13 +151,21 @@ fun AppShortcutItem(
toolbarActions.add(favAction)
}
toolbarActions.add(
DefaultToolbarAction(
label = stringResource(R.string.menu_app_info),
icon = Icons.Rounded.Info
) {
viewModel.openAppInfo(context)
})
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
@@ -152,7 +175,7 @@ fun AppShortcutItem(
action = { sheetManager.showCustomizeSearchableModal(shortcut) }
))
if (viewModel.canDelete) {
if (shortcut is LauncherShortcut && shortcut.launcherShortcut.isPinned) {
toolbarActions.add(DefaultToolbarAction(
label = stringResource(R.string.menu_delete),
icon = Icons.Rounded.Delete,
@@ -215,7 +238,7 @@ fun AppShortcutItem(
text = { Text(stringResource(R.string.alert_delete_shortcut, shortcut.label)) },
confirmButton = {
TextButton(onClick = {
viewModel.deleteShortcut()
viewModel.delete()
requestDelete = false
}) {
Text(stringResource(android.R.string.ok))
@@ -278,4 +301,11 @@ fun ShortcutItemGridPopup(
)
}
}
}
}
val AppShortcut.packageName: String?
get() = when (this) {
is LegacyShortcut -> intent.`package`
is LauncherShortcut -> launcherShortcut.`package`
else -> null
}
@@ -1,41 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.shortcut
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.LauncherShortcut
import de.mm20.launcher2.search.data.LegacyShortcut
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class ShortcutItemVM(private val shortcut: AppShortcut) : SearchableItemVM(shortcut), KoinComponent {
private val shortcutRepository: AppShortcutRepository by inject()
val canDelete = shortcut is LauncherShortcut && shortcut.launcherShortcut.isPinned
fun openAppInfo(context: Context) {
val packageName = when(shortcut) {
is LegacyShortcut -> shortcut.intent.`package` ?: return
is LauncherShortcut -> shortcut.launcherShortcut.`package`
else -> return
}
context.tryStartActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:$packageName")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
)
}
fun deleteShortcut() {
if (!canDelete) return
if (shortcut is LauncherShortcut) shortcutRepository.removePinnedShortcut(shortcut)
searchableRepository.delete(shortcut)
}
}
@@ -16,6 +16,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import de.mm20.launcher2.search.data.Website
import de.mm20.launcher2.ui.component.DefaultToolbarAction
@@ -23,8 +24,12 @@ import de.mm20.launcher2.ui.component.Toolbar
import de.mm20.launcher2.ui.component.ToolbarAction
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
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
@Composable
fun WebsiteItem(
@@ -33,7 +38,13 @@ fun WebsiteItem(
onBack: (() -> Unit)? = null
) {
val context = LocalContext.current
val viewModel = remember(website) { WebsiteItemVM(website) }
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${website.key}")
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
LaunchedEffect(website, iconSize) {
viewModel.init(website, iconSize.toInt())
}
Column(
modifier = modifier.clickable {
@@ -57,7 +68,7 @@ fun WebsiteItem(
text = website.labelOverride ?: website.label,
style = MaterialTheme.typography.titleLarge
)
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
val tags by viewModel.tags.collectAsState(emptyList())
if (tags.isNotEmpty()) {
Text(
modifier = Modifier.padding(top = 2.dp, bottom = 2.dp),
@@ -102,7 +113,7 @@ fun WebsiteItem(
label = stringResource(R.string.menu_share),
icon= Icons.Rounded.Share,
action = {
viewModel.share(context)
website.share(context)
}
)
)
@@ -1,21 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.website
import android.content.Context
import android.content.Intent
import de.mm20.launcher2.search.data.Website
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
class WebsiteItemVM(
private val website: Website
) : SearchableItemVM(website) {
fun share(context: Context) {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(
Intent.EXTRA_TEXT,
"${website.label}\n\n${website.description}\n\n${website.url}"
)
shareIntent.type = "text/plain"
context.startActivity(Intent.createChooser(shareIntent, null))
}
}
@@ -16,6 +16,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import de.mm20.launcher2.search.data.Wikipedia
import de.mm20.launcher2.ui.R
@@ -23,8 +24,12 @@ import de.mm20.launcher2.ui.component.DefaultToolbarAction
import de.mm20.launcher2.ui.component.Toolbar
import de.mm20.launcher2.ui.component.ToolbarAction
import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
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.utils.htmlToAnnotatedString
@Composable
@@ -34,7 +39,13 @@ fun WikipediaItem(
onBack: (() -> Unit)? = null
) {
val context = LocalContext.current
val viewModel = remember(wikipedia) { WikipediaItemVM(wikipedia) }
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${wikipedia.key}")
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
LaunchedEffect(wikipedia, iconSize) {
viewModel.init(wikipedia, iconSize.toInt())
}
Column(
modifier = modifier.clickable {
@@ -59,7 +70,7 @@ fun WikipediaItem(
text = wikipedia.label,
style = MaterialTheme.typography.titleLarge
)
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
val tags by viewModel.tags.collectAsState(emptyList())
if (tags.isNotEmpty()) {
Text(
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
@@ -110,7 +121,7 @@ fun WikipediaItem(
label = stringResource(R.string.menu_share),
icon = Icons.Rounded.Share,
action = {
viewModel.share(context)
wikipedia.share(context)
}
)
)
@@ -1,24 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.wikipedia
import android.content.Context
import android.content.Intent
import androidx.core.text.HtmlCompat
import de.mm20.launcher2.search.data.Wikipedia
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
class WikipediaItemVM(
private val wikipedia: Wikipedia
) : SearchableItemVM(wikipedia) {
fun share(context: Context) {
val text = HtmlCompat.fromHtml(wikipedia.text, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(
Intent.EXTRA_TEXT, "${wikipedia.label}\n\n" +
"${text.substring(0, 200)}\n\n" +
"${wikipedia.wikipediaUrl}/wiki?curid=${wikipedia.id}"
)
shareIntent.type = "text/plain"
context.startActivity(Intent.createChooser(shareIntent, null))
}
}