diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/ListItemViewModel.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/ListItemViewModel.kt new file mode 100644 index 00000000..791a5dbe --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/ListItemViewModel.kt @@ -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 get(key: String, modelClass: Class): 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 get(key: String): T { + return get(key, T::class.java) + } + + fun clear() { + cache.evictAll() + } +} + +private class ViewModelCache: LruCache(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 listItemViewModel(key: String): T { + val store = LocalListItemViewModelStore.current + return remember(key) { + store[key] + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt index 61f1f761..a47a4bf2 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt @@ -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( ) } } -} \ No newline at end of file +} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItemVM.kt deleted file mode 100644 index 0ab03661..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItemVM.kt +++ /dev/null @@ -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.startAppDetailsActivity( - ComponentName(app.`package`, app.activity), - app.getUser(), - null, - null - ) - } - - suspend fun shareApkFile(context: Context) { - val launcherApps = context.getSystemService()!! - 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() ?: return null - return launcherApps.getShortcutIconDrawable(shortcut, 0) - } - - val shortcuts = flow { - emit(appShortcutRepository.getShortcutsForActivity(app.launcherActivityInfo, 5)) - } - - fun isShortcutPinned(shortcut: AppShortcut): Flow { - 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) - } -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt index 95f67095..20fcb16a 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt @@ -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 + ) + } + } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItemVM.kt deleted file mode 100644 index ab873bce..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItemVM.kt +++ /dev/null @@ -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) - ) - } -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt index bc910bb5..718ae26b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt @@ -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(null) + private val iconSize = MutableStateFlow(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 { - 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> { - 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() ?: return null + return launcherApps.getShortcutIconDrawable(shortcut, 0) + } + + fun isShortcutPinned(shortcut: AppShortcut): Flow { + 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}") + } + + } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt index 88fe8520..4e7f0ef3 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt @@ -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 diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItemVM.kt deleted file mode 100644 index 15aa6108..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItemVM.kt +++ /dev/null @@ -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) \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt index b643bd39..ad6faa39 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt @@ -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( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItemVM.kt deleted file mode 100644 index 87b92a5e..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItemVM.kt +++ /dev/null @@ -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) \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt index 08b4f46b..f84b7e46 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt @@ -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() } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItemVM.kt deleted file mode 100644 index 51c9dde9..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItemVM.kt +++ /dev/null @@ -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)) - ) - } -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItem.kt index eb2e63dd..3c96f08d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItem.kt @@ -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), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItemVM.kt deleted file mode 100644 index a0a4d8f6..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItemVM.kt +++ /dev/null @@ -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)) - } -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt index a4994127..290069a5 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt @@ -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( ) } } -} \ No newline at end of file +} + +val AppShortcut.packageName: String? + get() = when (this) { + is LegacyShortcut -> intent.`package` + is LauncherShortcut -> launcherShortcut.`package` + else -> null + } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItemVM.kt deleted file mode 100644 index 009b7140..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItemVM.kt +++ /dev/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) - } -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItem.kt index 1a5d618f..b826d61c 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItem.kt @@ -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) } ) ) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItemVM.kt deleted file mode 100644 index 2cf77e8c..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItemVM.kt +++ /dev/null @@ -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)) - } -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/WikipediaItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/WikipediaItem.kt index 148f161e..58f24488 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/WikipediaItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/WikipediaItem.kt @@ -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) } ) ) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/WikipediaItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/WikipediaItemVM.kt deleted file mode 100644 index cf249764..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/WikipediaItemVM.kt +++ /dev/null @@ -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)) - } -} \ No newline at end of file diff --git a/data/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt b/data/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt index 1b42ef4b..9f0dcedf 100644 --- a/data/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt +++ b/data/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt @@ -3,14 +3,18 @@ package de.mm20.launcher2.search.data import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherApps import android.content.pm.PackageManager import android.graphics.drawable.AdaptiveIconDrawable +import android.net.Uri import android.os.Bundle import android.os.Process import android.os.UserHandle import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider import androidx.core.content.getSystemService import de.mm20.launcher2.applications.R import de.mm20.launcher2.compat.PackageManagerCompat @@ -44,6 +48,9 @@ data class LauncherApp( val isMainProfile = launcherActivityInfo.user == Process.myUserHandle() + val canUninstall: Boolean + get() = flags and ApplicationInfo.FLAG_SYSTEM == 0 && isMainProfile + override val domain: String = Domain override val preferDetailsOverLaunch: Boolean = false @@ -148,7 +155,60 @@ data class LauncherApp( } } + fun uninstall(context: Context) { + val intent = Intent(Intent.ACTION_DELETE) + intent.data = Uri.parse("package:$`package`") + context.startActivity(intent) + } + fun openAppInfo(context: Context) { + val launcherApps = context.getSystemService()!! + + launcherApps.startAppDetailsActivity( + ComponentName(`package`, activity), + getUser(), + null, + null + ) + } + + suspend fun shareApkFile(context: Context) { + val launcherApps = context.getSystemService()!! + val fileCopy = java.io.File( + context.cacheDir, + "${`package`}-${version}.apk" + ) + withContext(Dispatchers.IO) { + try { + val user = getUser() + val info = if (user != null) { + launcherApps.getApplicationInfo(`package`, 0, user) + } else { + context.packageManager.getApplicationInfo(`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) { + } + } + 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)) + } + } companion object { private fun getStoreLinkForInstaller( diff --git a/data/calendar/src/main/java/de/mm20/launcher2/search/data/CalendarEvent.kt b/data/calendar/src/main/java/de/mm20/launcher2/search/data/CalendarEvent.kt index 3e399fff..4598425a 100644 --- a/data/calendar/src/main/java/de/mm20/launcher2/search/data/CalendarEvent.kt +++ b/data/calendar/src/main/java/de/mm20/launcher2/search/data/CalendarEvent.kt @@ -3,6 +3,7 @@ package de.mm20.launcher2.search.data import android.content.ContentUris import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle import android.provider.CalendarContract import de.mm20.launcher2.icons.ColorLayer @@ -10,6 +11,7 @@ import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.TextLayer import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.search.SavableSearchable +import java.net.URLEncoder import java.text.SimpleDateFormat data class CalendarEvent( @@ -57,6 +59,23 @@ data class CalendarEvent( return context.tryStartActivity(getLaunchIntent(), options) } + fun openLocation(context: Context) { + context.tryStartActivity( + Intent(Intent.ACTION_VIEW) + .setData( + Uri.parse( + "geo:0,0?q=${ + URLEncoder.encode( + location, + "utf8" + ) + }" + ) + ) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } + companion object { const val Domain = "calendar" } diff --git a/data/files/src/main/java/de/mm20/launcher2/search/data/File.kt b/data/files/src/main/java/de/mm20/launcher2/search/data/File.kt index 6b0d06e1..c6fc7c63 100644 --- a/data/files/src/main/java/de/mm20/launcher2/search/data/File.kt +++ b/data/files/src/main/java/de/mm20/launcher2/search/data/File.kt @@ -129,6 +129,11 @@ interface File : SavableSearchable { val isDeletable: Boolean get() = false + + val canShare: Boolean + get() = false + + fun share(context: Context) {} suspend fun delete(context: Context) {} } \ No newline at end of file diff --git a/data/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt b/data/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt index 40a32bb0..3fca9c7b 100644 --- a/data/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt +++ b/data/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt @@ -335,4 +335,20 @@ data class LocalFile( return metaData } } + + override val canShare: Boolean + get() = !isDirectory + + override 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(path) + ) + shareIntent.putExtra(Intent.EXTRA_STREAM, uri) + shareIntent.type = mimeType + context.startActivity(Intent.createChooser(shareIntent, null)) + } } \ No newline at end of file diff --git a/data/websites/src/main/java/de/mm20/launcher2/search/data/Website.kt b/data/websites/src/main/java/de/mm20/launcher2/search/data/Website.kt index d5cadddb..8bc4922b 100644 --- a/data/websites/src/main/java/de/mm20/launcher2/search/data/Website.kt +++ b/data/websites/src/main/java/de/mm20/launcher2/search/data/Website.kt @@ -90,6 +90,16 @@ data class Website( return context.tryStartActivity(getLaunchIntent(), options) } + fun share(context: Context) { + val shareIntent = Intent(Intent.ACTION_SEND) + shareIntent.putExtra( + Intent.EXTRA_TEXT, + "${label}\n\n${description}\n\n${url}" + ) + shareIntent.type = "text/plain" + context.startActivity(Intent.createChooser(shareIntent, null)) + } + companion object { const val Domain = "web" } diff --git a/data/wikipedia/src/main/java/de/mm20/launcher2/search/data/Wikipedia.kt b/data/wikipedia/src/main/java/de/mm20/launcher2/search/data/Wikipedia.kt index f2b847dc..6fc2b4b6 100644 --- a/data/wikipedia/src/main/java/de/mm20/launcher2/search/data/Wikipedia.kt +++ b/data/wikipedia/src/main/java/de/mm20/launcher2/search/data/Wikipedia.kt @@ -7,6 +7,7 @@ import android.net.Uri import android.os.Bundle import androidx.browser.customtabs.CustomTabsIntent import androidx.core.content.ContextCompat +import androidx.core.text.HtmlCompat import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.TintedIconLayer @@ -53,6 +54,18 @@ data class Wikipedia( return context.tryStartActivity(getLaunchIntent(), options) } + fun share(context: Context) { + val text = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() + val shareIntent = Intent(Intent.ACTION_SEND) + shareIntent.putExtra( + Intent.EXTRA_TEXT, "${label}\n\n" + + "${text.substring(0, 200)}…\n\n" + + url + ) + shareIntent.type = "text/plain" + context.startActivity(Intent.createChooser(shareIntent, null)) + } + companion object { const val Domain = "wikipedia" } diff --git a/services/favorites/src/main/java/de/mm20/launcher2/services/favorites/FavoritesService.kt b/services/favorites/src/main/java/de/mm20/launcher2/services/favorites/FavoritesService.kt index 23893e34..8c5555b9 100644 --- a/services/favorites/src/main/java/de/mm20/launcher2/services/favorites/FavoritesService.kt +++ b/services/favorites/src/main/java/de/mm20/launcher2/services/favorites/FavoritesService.kt @@ -31,6 +31,10 @@ class FavoritesService( return searchableRepository.isPinned(searchable) } + fun isHidden(searchable: SavableSearchable): Flow { + return searchableRepository.isHidden(searchable) + } + fun pinItem(searchable: SavableSearchable) { searchableRepository.upsert( searchable, @@ -56,6 +60,21 @@ class FavoritesService( ) } + fun hideItem(searchable: SavableSearchable) { + searchableRepository.upsert( + searchable, + hidden = true, + pinned = false, + ) + } + + fun unhideItem(searchable: SavableSearchable) { + searchableRepository.upsert( + searchable, + hidden = false, + ) + } + fun reportLaunch(searchable: SavableSearchable) { searchableRepository.touch(searchable) } diff --git a/services/tags/src/main/java/de/mm20/launcher2/services/tags/TagsService.kt b/services/tags/src/main/java/de/mm20/launcher2/services/tags/TagsService.kt index 8f0dca28..08a0e110 100644 --- a/services/tags/src/main/java/de/mm20/launcher2/services/tags/TagsService.kt +++ b/services/tags/src/main/java/de/mm20/launcher2/services/tags/TagsService.kt @@ -12,4 +12,5 @@ interface TagsService { fun createTag(tag: String, items: List) fun updateTag(tag: String, newName: String? = null, items: List? = null) + fun getTags(it: SavableSearchable): Flow> } \ No newline at end of file diff --git a/services/tags/src/main/java/de/mm20/launcher2/services/tags/impl/TagsServiceImpl.kt b/services/tags/src/main/java/de/mm20/launcher2/services/tags/impl/TagsServiceImpl.kt index 1e956709..88ba2c2a 100644 --- a/services/tags/src/main/java/de/mm20/launcher2/services/tags/impl/TagsServiceImpl.kt +++ b/services/tags/src/main/java/de/mm20/launcher2/services/tags/impl/TagsServiceImpl.kt @@ -64,4 +64,8 @@ internal class TagsServiceImpl( customAttributesRepository.setItemsForTag(tag, items) } } + + override fun getTags(it: SavableSearchable): Flow> { + return customAttributesRepository.getTags(it) + } } \ No newline at end of file