Reuse search result item viewmodels
This commit is contained in:
parent
d1482ad112
commit
df1aa8c119
@ -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
|
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.*
|
||||||
import androidx.compose.animation.core.snap
|
import androidx.compose.animation.core.snap
|
||||||
import androidx.compose.animation.core.tween
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import com.google.accompanist.flowlayout.FlowRow
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import de.mm20.launcher2.search.data.LauncherApp
|
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.component.*
|
||||||
import de.mm20.launcher2.ui.ktx.toDp
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
import de.mm20.launcher2.ui.ktx.toPixels
|
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.launcher.sheets.LocalBottomSheetManager
|
||||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||||
@ -43,7 +53,13 @@ fun AppItem(
|
|||||||
app: LauncherApp,
|
app: LauncherApp,
|
||||||
onBack: () -> Unit
|
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 context = LocalContext.current
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
@ -63,7 +79,7 @@ fun AppItem(
|
|||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
|
|
||||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
val tags by viewModel.tags.collectAsState(emptyList())
|
||||||
if (tags.isNotEmpty()) {
|
if (tags.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
|
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
|
||||||
@ -98,7 +114,7 @@ fun AppItem(
|
|||||||
mainAxisSpacing = 12.dp,
|
mainAxisSpacing = 12.dp,
|
||||||
crossAxisSpacing = 0.dp
|
crossAxisSpacing = 0.dp
|
||||||
) {
|
) {
|
||||||
val notifications by viewModel.notifications.collectAsState(initial = emptyList())
|
val notifications by viewModel.notifications.collectAsState(emptyList())
|
||||||
|
|
||||||
for (not in notifications) {
|
for (not in notifications) {
|
||||||
val title =
|
val title =
|
||||||
@ -126,7 +142,9 @@ fun AppItem(
|
|||||||
viewModel.clearNotification(not)
|
viewModel.clearNotification(not)
|
||||||
},
|
},
|
||||||
onClick = {
|
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 badge by viewModel.badge.collectAsStateWithLifecycle(null)
|
||||||
val iconSize = 84.dp.toPixels().toInt()
|
val icon by viewModel.icon.collectAsStateWithLifecycle()
|
||||||
val icon by remember(app) { viewModel.getIcon(iconSize) }.collectAsState(null)
|
|
||||||
ShapedLauncherIcon(
|
ShapedLauncherIcon(
|
||||||
size = 84.dp,
|
size = 84.dp,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -213,7 +230,7 @@ fun AppItem(
|
|||||||
label = stringResource(R.string.menu_app_info),
|
label = stringResource(R.string.menu_app_info),
|
||||||
icon = Icons.Rounded.Info
|
icon = Icons.Rounded.Info
|
||||||
) {
|
) {
|
||||||
viewModel.openAppInfo(context)
|
app.openAppInfo(context)
|
||||||
})
|
})
|
||||||
|
|
||||||
toolbarActions.add(
|
toolbarActions.add(
|
||||||
@ -240,7 +257,7 @@ fun AppItem(
|
|||||||
icon = Icons.Rounded.Share
|
icon = Icons.Rounded.Share
|
||||||
) {
|
) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.shareApkFile(context)
|
app.shareApkFile(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -252,7 +269,10 @@ fun AppItem(
|
|||||||
label = stringResource(R.string.menu_share_store_link, storeDetails.label),
|
label = stringResource(R.string.menu_share_store_link, storeDetails.label),
|
||||||
icon = Icons.Rounded.Link,
|
icon = Icons.Rounded.Link,
|
||||||
action = {
|
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(
|
DefaultToolbarAction(
|
||||||
@ -260,7 +280,7 @@ fun AppItem(
|
|||||||
icon = Icons.Rounded.Android
|
icon = Icons.Rounded.Android
|
||||||
) {
|
) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.shareApkFile(context)
|
app.shareApkFile(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -268,13 +288,13 @@ fun AppItem(
|
|||||||
}
|
}
|
||||||
toolbarActions.add(shareAction)
|
toolbarActions.add(shareAction)
|
||||||
|
|
||||||
if (viewModel.canUninstall) {
|
if (app.canUninstall) {
|
||||||
toolbarActions.add(
|
toolbarActions.add(
|
||||||
DefaultToolbarAction(
|
DefaultToolbarAction(
|
||||||
label = stringResource(R.string.menu_uninstall),
|
label = stringResource(R.string.menu_uninstall),
|
||||||
icon = Icons.Rounded.Delete,
|
icon = Icons.Rounded.Delete,
|
||||||
) {
|
) {
|
||||||
viewModel.uninstall(context)
|
app.uninstall(context)
|
||||||
onBack()
|
onBack()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
|
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.animateDpAsState
|
||||||
import androidx.compose.animation.core.snap
|
import androidx.compose.animation.core.snap
|
||||||
import androidx.compose.animation.core.tween
|
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.background
|
||||||
import androidx.compose.foundation.clickable
|
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.Icons
|
||||||
import androidx.compose.material.icons.rounded.*
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material.icons.rounded.Edit
|
||||||
import androidx.compose.runtime.*
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
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.Toolbar
|
||||||
import de.mm20.launcher2.ui.component.ToolbarAction
|
import de.mm20.launcher2.ui.component.ToolbarAction
|
||||||
import de.mm20.launcher2.ui.ktx.toDp
|
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.launcher.sheets.LocalBottomSheetManager
|
||||||
import de.mm20.launcher2.ui.locals.LocalDarkTheme
|
import de.mm20.launcher2.ui.locals.LocalDarkTheme
|
||||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import palettes.TonalPalette
|
import palettes.TonalPalette
|
||||||
@ -44,7 +78,12 @@ fun CalendarItem(
|
|||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
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 lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
@ -54,9 +93,11 @@ fun CalendarItem(
|
|||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.drawBehind {
|
.drawBehind {
|
||||||
val color = TonalPalette.fromInt(calendar.color).tone(
|
val color = TonalPalette
|
||||||
if (darkMode) 80 else 40
|
.fromInt(calendar.color)
|
||||||
)
|
.tone(
|
||||||
|
if (darkMode) 80 else 40
|
||||||
|
)
|
||||||
drawRect(Color(color), Offset.Zero, this.size.copy(width = 8.dp.toPx()))
|
drawRect(Color(color), Offset.Zero, this.size.copy(width = 8.dp.toPx()))
|
||||||
}
|
}
|
||||||
.padding(start = 8.dp),
|
.padding(start = 8.dp),
|
||||||
@ -82,12 +123,12 @@ fun CalendarItem(
|
|||||||
AnimatedVisibility(!showDetails) {
|
AnimatedVisibility(!showDetails) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(top = 2.dp),
|
modifier = Modifier.padding(top = 2.dp),
|
||||||
text = viewModel.getSummary(context),
|
text = calendar.getSummary(context),
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AnimatedVisibility(showDetails) {
|
AnimatedVisibility(showDetails) {
|
||||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
val tags by viewModel.tags.collectAsState(emptyList())
|
||||||
if (tags.isNotEmpty()) {
|
if (tags.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
|
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
|
||||||
@ -112,7 +153,7 @@ fun CalendarItem(
|
|||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = viewModel.formatTime(context),
|
text = calendar.formatTime(context),
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -136,7 +177,7 @@ fun CalendarItem(
|
|||||||
if (calendar.attendees.isNotEmpty()) {
|
if (calendar.attendees.isNotEmpty()) {
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -156,7 +197,7 @@ fun CalendarItem(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
viewModel.openLocation(context)
|
calendar.openLocation(context)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -213,11 +254,14 @@ fun CalendarItem(
|
|||||||
onBack()
|
onBack()
|
||||||
lifecycleOwner.lifecycleScope.launch {
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
val result = snackbarHostState.showSnackbar(
|
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),
|
actionLabel = context.getString(R.string.action_undo),
|
||||||
duration = SnackbarDuration.Short,
|
duration = SnackbarDuration.Short,
|
||||||
)
|
)
|
||||||
if(result == SnackbarResult.ActionPerformed) {
|
if (result == SnackbarResult.ActionPerformed) {
|
||||||
viewModel.unhide()
|
viewModel.unhide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -297,3 +341,52 @@ 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
|
package de.mm20.launcher2.ui.launcher.search.common
|
||||||
|
|
||||||
import android.content.Context
|
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.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
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.badges.BadgeService
|
||||||
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
|
import de.mm20.launcher2.files.FileRepository
|
||||||
import de.mm20.launcher2.searchable.SearchableRepository
|
|
||||||
import de.mm20.launcher2.icons.IconService
|
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.SavableSearchable
|
||||||
import de.mm20.launcher2.search.data.AppShortcut
|
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.LauncherApp
|
||||||
|
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||||
import de.mm20.launcher2.services.favorites.FavoritesService
|
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.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.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
abstract class SearchableItemVM(
|
class SearchableItemVM : ListItemViewModel(), KoinComponent {
|
||||||
private val searchable: SavableSearchable
|
private val favoritesService: FavoritesService by inject()
|
||||||
) : KoinComponent {
|
private val badgeService: BadgeService by inject()
|
||||||
protected val favoritesService: FavoritesService by inject()
|
private val iconService: IconService by inject()
|
||||||
protected val searchableRepository: SearchableRepository by inject()
|
private val tagsService: TagsService by inject()
|
||||||
protected val badgeService: BadgeService by inject()
|
private val notificationRepository: NotificationRepository by inject()
|
||||||
protected val iconService: IconService by inject()
|
private val appShortcutRepository: AppShortcutRepository by inject()
|
||||||
protected val customAttributesRepository: CustomAttributesRepository 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() {
|
fun pin() {
|
||||||
favoritesService.pinItem(searchable)
|
searchable.value?.let { favoritesService.pinItem(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unpin() {
|
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() {
|
fun hide() {
|
||||||
searchableRepository.upsert(searchable, hidden = true, pinned = false)
|
searchable.value?.let { favoritesService.hideItem(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unhide() {
|
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> {
|
val icon = searchable.combine(iconSize) { sh, sz -> sh to sz }.flatMapLatest { (s, size) ->
|
||||||
return iconService.getIcon(searchable, 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>> {
|
val notifications = searchable.flatMapLatest { searchable ->
|
||||||
return customAttributesRepository.getTags(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 {
|
open fun launch(context: Context, bounds: Rect? = null): Boolean {
|
||||||
|
val searchable = searchable.value ?: return false
|
||||||
val view = (context as? AppCompatActivity)?.window?.decorView
|
val view = (context as? AppCompatActivity)?.window?.decorView
|
||||||
val options = if (bounds != null && view != null) {
|
val options = if (bounds != null && view != null) {
|
||||||
ActivityOptionsCompat.makeScaleUpAnimation(
|
ActivityOptionsCompat.makeScaleUpAnimation(
|
||||||
@ -72,8 +117,47 @@ abstract class SearchableItemVM(
|
|||||||
favoritesService.reportLaunch(searchable)
|
favoritesService.reportLaunch(searchable)
|
||||||
return true
|
return true
|
||||||
} else if (searchable is LauncherApp || searchable is AppShortcut) {
|
} else if (searchable is LauncherApp || searchable is AppShortcut) {
|
||||||
searchableRepository.delete(searchable)
|
favoritesService.reset(searchable)
|
||||||
}
|
}
|
||||||
return false
|
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.unit.dp
|
||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
import androidx.compose.ui.window.PopupProperties
|
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.SavableSearchable
|
||||||
import de.mm20.launcher2.search.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.data.AppShortcut
|
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.ktx.toPixels
|
||||||
import de.mm20.launcher2.ui.launcher.search.apps.AppItemGridPopup
|
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.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.contacts.ContactItemGridPopup
|
||||||
import de.mm20.launcher2.ui.launcher.search.files.FileItemGridPopup
|
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.shortcut.ShortcutItemGridPopup
|
||||||
import de.mm20.launcher2.ui.launcher.search.website.WebsiteItemGridPopup
|
import de.mm20.launcher2.ui.launcher.search.website.WebsiteItemGridPopup
|
||||||
import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaItemGridPopup
|
import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaItemGridPopup
|
||||||
@ -71,7 +75,12 @@ fun GridItem(
|
|||||||
showLabels: Boolean = true,
|
showLabels: Boolean = true,
|
||||||
highlight: Boolean = false
|
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
|
val context = LocalContext.current
|
||||||
|
|
||||||
@ -79,9 +88,8 @@ fun GridItem(
|
|||||||
var bounds by remember { mutableStateOf(Rect.Zero) }
|
var bounds by remember { mutableStateOf(Rect.Zero) }
|
||||||
|
|
||||||
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
val badge by remember(item.key) { viewModel.badge }.collectAsState(null)
|
val badge by remember(item.key) { viewModel.badge }.collectAsStateWithLifecycle()
|
||||||
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
|
val icon by viewModel.icon.collectAsStateWithLifecycle()
|
||||||
val icon by remember(item.key) { viewModel.getIcon(iconSize.toInt()) }.collectAsState(null)
|
|
||||||
|
|
||||||
val launchOnPress = !item.preferDetailsOverLaunch
|
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.boundsInWindow
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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.SavableSearchable
|
||||||
import de.mm20.launcher2.search.data.*
|
import de.mm20.launcher2.search.data.*
|
||||||
import de.mm20.launcher2.ui.component.InnerCard
|
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.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.contacts.ContactItem
|
||||||
import de.mm20.launcher2.ui.launcher.search.files.FileItem
|
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.launcher.search.shortcut.AppShortcutItem
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListItem(
|
fun ListItem(
|
||||||
@ -25,7 +31,12 @@ fun ListItem(
|
|||||||
var showDetails by remember { mutableStateOf(false) }
|
var showDetails by remember { mutableStateOf(false) }
|
||||||
val context = LocalContext.current
|
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) }
|
var bounds by remember { mutableStateOf(Rect.Zero) }
|
||||||
InnerCard(
|
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
|
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.animateDp
|
||||||
import androidx.compose.animation.core.snap
|
import androidx.compose.animation.core.snap
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.core.updateTransition
|
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.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.*
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material.icons.rounded.Call
|
||||||
import androidx.compose.runtime.*
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Rect
|
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.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
import de.mm20.launcher2.search.data.Contact
|
import de.mm20.launcher2.search.data.Contact
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
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.Telegram
|
||||||
import de.mm20.launcher2.ui.icons.WhatsApp
|
import de.mm20.launcher2.ui.icons.WhatsApp
|
||||||
import de.mm20.launcher2.ui.ktx.toDp
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
import de.mm20.launcher2.ui.ktx.toPixels
|
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.launcher.sheets.LocalBottomSheetManager
|
||||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||||
@ -45,7 +83,13 @@ fun ContactItem(
|
|||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
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 lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
@ -58,8 +102,7 @@ fun ContactItem(
|
|||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
val iconSize = 48.dp.toPixels().toInt()
|
val icon by viewModel.icon.collectAsStateWithLifecycle()
|
||||||
val icon by remember(contact) { viewModel.getIcon(iconSize) }.collectAsState(null)
|
|
||||||
val padding by transition.animateDp(label = "iconPadding") {
|
val padding by transition.animateDp(label = "iconPadding") {
|
||||||
if (it) 16.dp else 8.dp
|
if (it) 16.dp else 8.dp
|
||||||
}
|
}
|
||||||
@ -92,7 +135,7 @@ fun ContactItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
AnimatedVisibility(showDetails) {
|
AnimatedVisibility(showDetails) {
|
||||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
val tags by viewModel.tags.collectAsState(emptyList())
|
||||||
if (tags.isNotEmpty()) {
|
if (tags.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(top = 1.dp),
|
modifier = Modifier.padding(top = 1.dp),
|
||||||
@ -125,7 +168,10 @@ fun ContactItem(
|
|||||||
modifier = Modifier.padding(end = 16.dp),
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
text = it.label,
|
text = it.label,
|
||||||
onClick = {
|
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),
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
text = it.label,
|
text = it.label,
|
||||||
onClick = {
|
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),
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
text = it.label,
|
text = it.label,
|
||||||
onClick = {
|
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),
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
text = it.label,
|
text = it.label,
|
||||||
onClick = {
|
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),
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
text = it.label,
|
text = it.label,
|
||||||
onClick = {
|
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()
|
onBack()
|
||||||
lifecycleOwner.lifecycleScope.launch {
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
val result = snackbarHostState.showSnackbar(
|
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),
|
actionLabel = context.getString(R.string.action_undo),
|
||||||
duration = SnackbarDuration.Short,
|
duration = SnackbarDuration.Short,
|
||||||
)
|
)
|
||||||
if(result == SnackbarResult.ActionPerformed) {
|
if (result == SnackbarResult.ActionPerformed) {
|
||||||
viewModel.unhide()
|
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.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.search.data.File
|
import de.mm20.launcher2.search.data.File
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
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.component.ToolbarAction
|
||||||
import de.mm20.launcher2.ui.ktx.toDp
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
import de.mm20.launcher2.ui.ktx.toPixels
|
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.launcher.sheets.LocalBottomSheetManager
|
||||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||||
@ -46,7 +50,13 @@ fun FileItem(
|
|||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
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 lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
@ -85,7 +95,7 @@ fun FileItem(
|
|||||||
}
|
}
|
||||||
AnimatedVisibility(showDetails) {
|
AnimatedVisibility(showDetails) {
|
||||||
Column {
|
Column {
|
||||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
val tags by viewModel.tags.collectAsState(emptyList())
|
||||||
if (tags.isNotEmpty()) {
|
if (tags.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
|
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
|
||||||
@ -128,8 +138,7 @@ fun FileItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val iconSize = 48.dp.toPixels().toInt()
|
val icon by viewModel.icon.collectAsStateWithLifecycle()
|
||||||
val icon by remember(file) { viewModel.getIcon(iconSize) }.collectAsState(null)
|
|
||||||
val badge by viewModel.badge.collectAsState(null)
|
val badge by viewModel.badge.collectAsState(null)
|
||||||
val padding by transition.animateDp(label = "iconPadding") {
|
val padding by transition.animateDp(label = "iconPadding") {
|
||||||
if (it) 16.dp else 8.dp
|
if (it) 16.dp else 8.dp
|
||||||
@ -180,17 +189,17 @@ fun FileItem(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (viewModel.canShare) {
|
if (file.canShare) {
|
||||||
toolbarActions.add(DefaultToolbarAction(
|
toolbarActions.add(DefaultToolbarAction(
|
||||||
label = stringResource(R.string.menu_share),
|
label = stringResource(R.string.menu_share),
|
||||||
icon = Icons.Rounded.Share,
|
icon = Icons.Rounded.Share,
|
||||||
action = {
|
action = {
|
||||||
viewModel.share(context)
|
file.share(context)
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewModel.canDelete) {
|
if (file.isDeletable) {
|
||||||
var showConfirmDialog by remember { mutableStateOf(false) }
|
var showConfirmDialog by remember { mutableStateOf(false) }
|
||||||
toolbarActions.add(DefaultToolbarAction(
|
toolbarActions.add(DefaultToolbarAction(
|
||||||
label = stringResource(R.string.menu_delete),
|
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
|
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.*
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.layout.*
|
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.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
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.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.R
|
||||||
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
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.component.ToolbarAction
|
||||||
import de.mm20.launcher2.ui.ktx.toDp
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
import de.mm20.launcher2.ui.ktx.toPixels
|
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.launcher.sheets.LocalBottomSheetManager
|
||||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||||
@ -42,9 +52,15 @@ fun AppShortcutItem(
|
|||||||
showDetails: Boolean = false,
|
showDetails: Boolean = false,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
val viewModel = remember { ShortcutItemVM(shortcut) }
|
|
||||||
val context = LocalContext.current
|
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 lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
|
|
||||||
@ -70,7 +86,7 @@ fun AppShortcutItem(
|
|||||||
)
|
)
|
||||||
|
|
||||||
AnimatedVisibility(showDetails) {
|
AnimatedVisibility(showDetails) {
|
||||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
val tags by viewModel.tags.collectAsState(emptyList())
|
||||||
if (tags.isNotEmpty()) {
|
if (tags.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(top = 1.dp, bottom = 2.dp),
|
modifier = Modifier.padding(top = 1.dp, bottom = 2.dp),
|
||||||
@ -95,8 +111,7 @@ fun AppShortcutItem(
|
|||||||
}
|
}
|
||||||
val badge by viewModel.badge.collectAsState(null)
|
val badge by viewModel.badge.collectAsState(null)
|
||||||
val size by animateDpAsState(if (showDetails) 84.dp else 48.dp)
|
val size by animateDpAsState(if (showDetails) 84.dp else 48.dp)
|
||||||
val iconSize = 84.dp.toPixels().toInt()
|
val icon by viewModel.icon.collectAsStateWithLifecycle()
|
||||||
val icon by remember(shortcut.key) { viewModel.getIcon(iconSize) }.collectAsState(null)
|
|
||||||
|
|
||||||
val padding by transition.animateDp(label = "iconPadding") {
|
val padding by transition.animateDp(label = "iconPadding") {
|
||||||
if (it) 16.dp else 8.dp
|
if (it) 16.dp else 8.dp
|
||||||
@ -136,13 +151,21 @@ fun AppShortcutItem(
|
|||||||
toolbarActions.add(favAction)
|
toolbarActions.add(favAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
toolbarActions.add(
|
val packageName = shortcut.packageName
|
||||||
DefaultToolbarAction(
|
if (packageName != null) {
|
||||||
label = stringResource(R.string.menu_app_info),
|
toolbarActions.add(
|
||||||
icon = Icons.Rounded.Info
|
DefaultToolbarAction(
|
||||||
) {
|
label = stringResource(R.string.menu_app_info),
|
||||||
viewModel.openAppInfo(context)
|
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
|
val sheetManager = LocalBottomSheetManager.current
|
||||||
@ -152,7 +175,7 @@ fun AppShortcutItem(
|
|||||||
action = { sheetManager.showCustomizeSearchableModal(shortcut) }
|
action = { sheetManager.showCustomizeSearchableModal(shortcut) }
|
||||||
))
|
))
|
||||||
|
|
||||||
if (viewModel.canDelete) {
|
if (shortcut is LauncherShortcut && shortcut.launcherShortcut.isPinned) {
|
||||||
toolbarActions.add(DefaultToolbarAction(
|
toolbarActions.add(DefaultToolbarAction(
|
||||||
label = stringResource(R.string.menu_delete),
|
label = stringResource(R.string.menu_delete),
|
||||||
icon = Icons.Rounded.Delete,
|
icon = Icons.Rounded.Delete,
|
||||||
@ -215,7 +238,7 @@ fun AppShortcutItem(
|
|||||||
text = { Text(stringResource(R.string.alert_delete_shortcut, shortcut.label)) },
|
text = { Text(stringResource(R.string.alert_delete_shortcut, shortcut.label)) },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
viewModel.deleteShortcut()
|
viewModel.delete()
|
||||||
requestDelete = false
|
requestDelete = false
|
||||||
}) {
|
}) {
|
||||||
Text(stringResource(android.R.string.ok))
|
Text(stringResource(android.R.string.ok))
|
||||||
@ -279,3 +302,10 @@ 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.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import de.mm20.launcher2.search.data.Website
|
import de.mm20.launcher2.search.data.Website
|
||||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
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.component.ToolbarAction
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.ktx.toDp
|
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.launcher.sheets.LocalBottomSheetManager
|
||||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WebsiteItem(
|
fun WebsiteItem(
|
||||||
@ -33,7 +38,13 @@ fun WebsiteItem(
|
|||||||
onBack: (() -> Unit)? = null
|
onBack: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
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(
|
Column(
|
||||||
modifier = modifier.clickable {
|
modifier = modifier.clickable {
|
||||||
@ -57,7 +68,7 @@ fun WebsiteItem(
|
|||||||
text = website.labelOverride ?: website.label,
|
text = website.labelOverride ?: website.label,
|
||||||
style = MaterialTheme.typography.titleLarge
|
style = MaterialTheme.typography.titleLarge
|
||||||
)
|
)
|
||||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
val tags by viewModel.tags.collectAsState(emptyList())
|
||||||
if (tags.isNotEmpty()) {
|
if (tags.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(top = 2.dp, bottom = 2.dp),
|
modifier = Modifier.padding(top = 2.dp, bottom = 2.dp),
|
||||||
@ -102,7 +113,7 @@ fun WebsiteItem(
|
|||||||
label = stringResource(R.string.menu_share),
|
label = stringResource(R.string.menu_share),
|
||||||
icon= Icons.Rounded.Share,
|
icon= Icons.Rounded.Share,
|
||||||
action = {
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import de.mm20.launcher2.search.data.Wikipedia
|
import de.mm20.launcher2.search.data.Wikipedia
|
||||||
import de.mm20.launcher2.ui.R
|
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.Toolbar
|
||||||
import de.mm20.launcher2.ui.component.ToolbarAction
|
import de.mm20.launcher2.ui.component.ToolbarAction
|
||||||
import de.mm20.launcher2.ui.ktx.toDp
|
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.launcher.sheets.LocalBottomSheetManager
|
||||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||||
import de.mm20.launcher2.ui.utils.htmlToAnnotatedString
|
import de.mm20.launcher2.ui.utils.htmlToAnnotatedString
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -34,7 +39,13 @@ fun WikipediaItem(
|
|||||||
onBack: (() -> Unit)? = null
|
onBack: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
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(
|
Column(
|
||||||
modifier = modifier.clickable {
|
modifier = modifier.clickable {
|
||||||
@ -59,7 +70,7 @@ fun WikipediaItem(
|
|||||||
text = wikipedia.label,
|
text = wikipedia.label,
|
||||||
style = MaterialTheme.typography.titleLarge
|
style = MaterialTheme.typography.titleLarge
|
||||||
)
|
)
|
||||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
val tags by viewModel.tags.collectAsState(emptyList())
|
||||||
if (tags.isNotEmpty()) {
|
if (tags.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
|
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
|
||||||
@ -110,7 +121,7 @@ fun WikipediaItem(
|
|||||||
label = stringResource(R.string.menu_share),
|
label = stringResource(R.string.menu_share),
|
||||||
icon = Icons.Rounded.Share,
|
icon = Icons.Rounded.Share,
|
||||||
action = {
|
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,14 +3,18 @@ package de.mm20.launcher2.search.data
|
|||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.LauncherActivityInfo
|
import android.content.pm.LauncherActivityInfo
|
||||||
import android.content.pm.LauncherApps
|
import android.content.pm.LauncherApps
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.AdaptiveIconDrawable
|
import android.graphics.drawable.AdaptiveIconDrawable
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import de.mm20.launcher2.applications.R
|
import de.mm20.launcher2.applications.R
|
||||||
import de.mm20.launcher2.compat.PackageManagerCompat
|
import de.mm20.launcher2.compat.PackageManagerCompat
|
||||||
@ -44,6 +48,9 @@ data class LauncherApp(
|
|||||||
|
|
||||||
val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
|
val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
|
||||||
|
|
||||||
|
val canUninstall: Boolean
|
||||||
|
get() = flags and ApplicationInfo.FLAG_SYSTEM == 0 && isMainProfile
|
||||||
|
|
||||||
override val domain: String = Domain
|
override val domain: String = Domain
|
||||||
override val preferDetailsOverLaunch: Boolean = false
|
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>()!!
|
||||||
|
|
||||||
|
launcherApps.startAppDetailsActivity(
|
||||||
|
ComponentName(`package`, activity),
|
||||||
|
getUser(),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun shareApkFile(context: Context) {
|
||||||
|
val launcherApps = context.getSystemService<LauncherApps>()!!
|
||||||
|
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 {
|
companion object {
|
||||||
private fun getStoreLinkForInstaller(
|
private fun getStoreLinkForInstaller(
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package de.mm20.launcher2.search.data
|
|||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.CalendarContract
|
import android.provider.CalendarContract
|
||||||
import de.mm20.launcher2.icons.ColorLayer
|
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.icons.TextLayer
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
|
import java.net.URLEncoder
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
data class CalendarEvent(
|
data class CalendarEvent(
|
||||||
@ -57,6 +59,23 @@ data class CalendarEvent(
|
|||||||
return context.tryStartActivity(getLaunchIntent(), options)
|
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 {
|
companion object {
|
||||||
const val Domain = "calendar"
|
const val Domain = "calendar"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -129,6 +129,11 @@ interface File : SavableSearchable {
|
|||||||
|
|
||||||
val isDeletable: Boolean
|
val isDeletable: Boolean
|
||||||
get() = false
|
get() = false
|
||||||
|
|
||||||
|
val canShare: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
fun share(context: Context) {}
|
||||||
suspend fun delete(context: Context) {}
|
suspend fun delete(context: Context) {}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -335,4 +335,20 @@ data class LocalFile(
|
|||||||
return metaData
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -90,6 +90,16 @@ data class Website(
|
|||||||
return context.tryStartActivity(getLaunchIntent(), options)
|
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 {
|
companion object {
|
||||||
const val Domain = "web"
|
const val Domain = "web"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
import de.mm20.launcher2.icons.ColorLayer
|
import de.mm20.launcher2.icons.ColorLayer
|
||||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||||
import de.mm20.launcher2.icons.TintedIconLayer
|
import de.mm20.launcher2.icons.TintedIconLayer
|
||||||
@ -53,6 +54,18 @@ data class Wikipedia(
|
|||||||
return context.tryStartActivity(getLaunchIntent(), options)
|
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 {
|
companion object {
|
||||||
const val Domain = "wikipedia"
|
const val Domain = "wikipedia"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,10 @@ class FavoritesService(
|
|||||||
return searchableRepository.isPinned(searchable)
|
return searchableRepository.isPinned(searchable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isHidden(searchable: SavableSearchable): Flow<Boolean> {
|
||||||
|
return searchableRepository.isHidden(searchable)
|
||||||
|
}
|
||||||
|
|
||||||
fun pinItem(searchable: SavableSearchable) {
|
fun pinItem(searchable: SavableSearchable) {
|
||||||
searchableRepository.upsert(
|
searchableRepository.upsert(
|
||||||
searchable,
|
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) {
|
fun reportLaunch(searchable: SavableSearchable) {
|
||||||
searchableRepository.touch(searchable)
|
searchableRepository.touch(searchable)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,4 +12,5 @@ interface TagsService {
|
|||||||
fun createTag(tag: String, items: List<SavableSearchable>)
|
fun createTag(tag: String, items: List<SavableSearchable>)
|
||||||
|
|
||||||
fun updateTag(tag: String, newName: String? = null, items: List<SavableSearchable>? = null)
|
fun updateTag(tag: String, newName: String? = null, items: List<SavableSearchable>? = null)
|
||||||
|
fun getTags(it: SavableSearchable): Flow<List<String>>
|
||||||
}
|
}
|
||||||
@ -64,4 +64,8 @@ internal class TagsServiceImpl(
|
|||||||
customAttributesRepository.setItemsForTag(tag, items)
|
customAttributesRepository.setItemsForTag(tag, items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getTags(it: SavableSearchable): Flow<List<String>> {
|
||||||
|
return customAttributesRepository.getTags(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user