Rework edit favorites panel

This commit is contained in:
MM20 2022-09-18 15:10:04 +02:00
parent c2ccc15093
commit 86bdbecda3
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
18 changed files with 1009 additions and 296 deletions

View File

@ -239,11 +239,4 @@ val OpenSourceLicenses = arrayOf(
licenseText = R.raw.license_apache_2,
url = "https://bigbadaboom.github.io/androidsvg/"
),
OpenSourceLibrary(
name = "Compose LazyList/Grid reorder",
description = "A Jetpack Compose (Android + Desktop) modifier enabling reordering by drag and drop in a LazyList and LazyGrid.",
licenseName = R.string.apache_license_name,
licenseText = R.raw.license_apache_2,
url = "https://github.com/aclassen/ComposeReorderable"
)
)

View File

@ -17,6 +17,9 @@ interface SearchDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertSkipExisting(items: FavoritesItemEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllReplaceExisting(items: List<FavoritesItemEntity>)
@Query("SELECT * FROM Searchable " +
"WHERE ((:manuallySorted AND pinned > 1) OR " +
@ -155,4 +158,6 @@ interface SearchDao {
@Query("DELETE FROM Searchable WHERE hidden = 0")
fun deleteAllFavorites()
@Query("UPDATE Searchable SET `pinned` = 0")
fun unpinAll()
}

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.favorites
import android.content.Context
import android.util.Log
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.FavoritesItemEntity
@ -17,12 +18,32 @@ import org.koin.core.component.KoinComponent
import java.io.File
interface FavoritesRepository {
@Deprecated("Use getFavorites(java.util.List<java.lang.String>, java.util.List<java.lang.String>, boolean, boolean, boolean, java.lang.Integer) instead.")
fun getFavorites(
columns: Int,
maxRows: Int? = null,
excludeCalendarEvents: Boolean = false
): Flow<List<Searchable>>
/**
* Get favorites
* @param includeTypes Include only items of these types. Cannot be used together with excludeTypes.
* @param excludeTypes Exclude only items of these types. Cannot be used together with includeTypes.
* @param manuallySorted Include items that have been sorted manually
* @param automaticallySorted Include items that are pinned but not sorted
* @param frequentlyUsed Include items that are not pinned but most frequently used
* @param limit Maximum number of items returned.
*/
fun getFavorites(
includeTypes: List<String>? = null,
excludeTypes: List<String>? = null,
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
limit: Int = 100
): Flow<List<Searchable>>
fun getPinnedCalendarEvents(): Flow<List<Searchable>>
fun getHiddenCalendarEventKeys(): Flow<List<String>>
fun isPinned(searchable: Searchable): Flow<Boolean>
@ -32,7 +53,11 @@ interface FavoritesRepository {
fun hideItem(searchable: Searchable)
fun unhideItem(searchable: Searchable)
fun incrementLaunchCounter(searchable: Searchable)
fun saveFavorites(favorites: List<FavoritesItem>)
fun updateFavorites(
manuallySorted: List<Searchable>,
automaticallySorted: List<Searchable>,
)
fun getHiddenItems(): Flow<List<Searchable>>
fun getHiddenItemKeys(): Flow<List<String>>
fun remove(searchable: Searchable)
@ -51,6 +76,13 @@ interface FavoritesRepository {
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
/**
* Remove database entries that are invalid. This includes
* - entries that cannot be deserialized anymore
* - entries that are inconsistent (the key column is not equal to the key of the searchable)
*/
suspend fun cleanupDatabase(): Int
}
internal class FavoritesRepositoryImpl(
@ -74,21 +106,19 @@ internal class FavoritesRepositoryImpl(
excludeTypes = listOf("calendar"),
manuallySorted = true,
automaticallySorted = true,
frequentlyUsed = false,
frequentlyUsed = false,
limit = columns * (maxRows ?: 20)
)
} else {
dao.getFavorites(
manuallySorted = true,
automaticallySorted = true,
frequentlyUsed = false,
limit = columns * (maxRows ?: 20))
frequentlyUsed = false,
limit = columns * (maxRows ?: 20)
)
}.map {
it.mapNotNull {
val item = fromDatabaseEntity(it).searchable
if (item == null) {
dao.deleteByKey(it.key)
}
return@mapNotNull item
}
}
@ -99,7 +129,7 @@ internal class FavoritesRepositoryImpl(
val autoFavs = dao.getFavorites(
manuallySorted = false,
automaticallySorted = false,
frequentlyUsed = true,
frequentlyUsed = true,
limit = favCount.coerceAtMost((maxRows ?: 20) * columns) - pinned.size
).first().mapNotNull {
val item = fromDatabaseEntity(it).searchable
@ -113,6 +143,47 @@ internal class FavoritesRepositoryImpl(
}
}
override fun getFavorites(
includeTypes: List<String>?,
excludeTypes: List<String>?,
manuallySorted: Boolean,
automaticallySorted: Boolean,
frequentlyUsed: Boolean,
limit: Int
): Flow<List<Searchable>> {
val dao = database.searchDao()
val entities = when {
includeTypes == null && excludeTypes == null -> dao.getFavorites(
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
limit = limit
)
includeTypes != null && excludeTypes == null -> {
dao.getFavoritesWithTypes(
includeTypes = includeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
limit = limit
)
}
excludeTypes != null && includeTypes == null -> {
dao.getFavoritesWithoutTypes(
excludeTypes = excludeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
limit = limit
)
}
else -> throw IllegalArgumentException("You can either use includeTypes or excludeTypes, not both")
}
return entities.map {
it.mapNotNull { fromDatabaseEntity(it).searchable }
}
}
override fun getPinnedCalendarEvents(): Flow<List<CalendarEvent>> {
return database.searchDao().getFavoritesWithTypes(
includeTypes = listOf("calendar"),
@ -198,15 +269,6 @@ internal class FavoritesRepositoryImpl(
}
}
override fun saveFavorites(favorites: List<FavoritesItem>) {
scope.launch {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao()
.saveFavorites(favorites.mapNotNull { it.toDatabaseEntity() })
}
}
}
override fun getHiddenItems(): Flow<List<Searchable>> {
return database.searchDao().getHiddenItems().map {
it.mapNotNull { fromDatabaseEntity(it).searchable }
@ -240,22 +302,71 @@ internal class FavoritesRepositoryImpl(
}
}
override fun updateFavorites(
manuallySorted: List<Searchable>,
automaticallySorted: List<Searchable>
) {
val dao = database.searchDao()
scope.launch {
withContext(Dispatchers.IO) {
val keys = manuallySorted.map { it.key } + automaticallySorted.map { it.key }
val entities = dao.getFromKeys(keys)
val updatedManuallySorted = manuallySorted.mapIndexedNotNull { index, searchable ->
val entity = entities.find { searchable.key == it.key } ?: FavoritesItem(
key = searchable.key,
searchable = searchable,
launchCount = 0,
pinPosition = 0,
hidden = false,
).toDatabaseEntity() ?: return@mapIndexedNotNull null
entity.pinPosition = manuallySorted.size - index + 1
entity
}
val updatedAutomaticallySorted = automaticallySorted.mapIndexedNotNull { index, searchable ->
val entity = entities.find { searchable.key == it.key } ?: FavoritesItem(
key = searchable.key,
searchable = searchable,
launchCount = 0,
pinPosition = 0,
hidden = false,
).toDatabaseEntity() ?: return@mapIndexedNotNull null
entity.pinPosition = 1
entity
}
database.runInTransaction {
dao.unpinAll()
dao.insertAllReplaceExisting(updatedManuallySorted)
dao.insertAllReplaceExisting(updatedAutomaticallySorted)
}
}
}
}
private fun fromDatabaseEntity(entity: FavoritesItemEntity): FavoritesItem {
val deserializer: SearchableDeserializer =
getDeserializer(context, entity.serializedSearchable)
val searchable = deserializer.deserialize(entity.serializedSearchable.substringAfter("#"))
if (searchable == null) removeInvalidItem(entity.key)
return FavoritesItem(
key = entity.key,
searchable = deserializer.deserialize(entity.serializedSearchable.substringAfter("#")),
searchable = searchable,
launchCount = entity.launchCount,
pinPosition = entity.pinPosition,
hidden = entity.hidden
)
}
private fun removeInvalidItem(key: String) {
scope.launch {
database.searchDao().deleteByKey(key)
}
}
override fun getFromKeys(keys: List<String>): List<Searchable> {
val dao = database.searchDao()
return dao.getFromKeys(keys).mapNotNull { fromDatabaseEntity(it).searchable }
return dao.getFromKeys(keys)
.mapNotNull { fromDatabaseEntity(it).searchable }
}
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) {
@ -315,4 +426,26 @@ internal class FavoritesRepositoryImpl(
}
}
}
override suspend fun cleanupDatabase(): Int {
var removed = 0
val job = scope.launch {
val dao = database.backupDao()
var page = 0
do {
val favorites = dao.exportFavorites(limit = 100, offset = page * 100)
for (fav in favorites) {
val item = fromDatabaseEntity(fav)
if (item.searchable == null || item.searchable.key != item.key) {
removeInvalidItem(item.key)
removed++
Log.i("MM20", "SearchableDatabase cleanup: removed invalid item ${item.key}")
}
}
page++
} while (favorites.size == 100)
}
job.join()
return removed
}
}

View File

@ -434,6 +434,13 @@
<string name="preference_imperial_units_summary">Use degrees Fahrenheit and miles per hour</string>
<string name="preference_imperial_units">Imperial units</string>
<string name="preference_category_debug">Debug</string>
<string name="preference_category_debug_tools">Tools</string>
<string name="preference_debug_cleanup_database">Clean up database</string>
<string name="preference_debug_cleanup_database_summary">Remove broken and unused entries from the launcher database</string>
<plurals name="debug_cleanup_database_result">
<item quantity="one">%1$d entry has been removed.</item>
<item quantity="other">%1$d entries have been removed.</item>
</plurals>
<string name="preference_category_icons">Icons</string>
<string name="preference_cards">Cards</string>
<string name="preference_cards_summary">Customize card appearance</string>

View File

@ -382,10 +382,6 @@ dependencyResolutionManagement {
alias("lottie")
.to("com.airbnb.android", "lottie-compose")
.version("5.2.0")
alias("composereorderable")
.to("org.burnoutcrew.composereorderable", "reorderable")
.version("0.9.2")
}
}
}

View File

@ -112,7 +112,6 @@ dependencies {
implementation(libs.coil.compose)
implementation(libs.lottie)
implementation(libs.composereorderable)
implementation(project(":material-color-utilities"))

View File

@ -8,7 +8,9 @@ import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@ -93,12 +95,16 @@ fun ShapedLauncherIcon(
clip = currentIcon?.backgroundLayer !is TransparentLayer
this.shape = shape
}
.pointerInput(onClick, onLongClick) {
detectTapGestures(
onLongPress = { onLongClick?.invoke() },
onTap = { onClick?.invoke() },
)
},
.then(
if (onClick != null || onLongClick != null) {
Modifier.pointerInput(onClick, onLongClick) {
detectTapGestures(
onLongPress = { onLongClick?.invoke() },
onTap = { onClick?.invoke() },
)
}
} else Modifier
),
contentAlignment = Alignment.Center
) {
currentIcon?.let {
@ -124,12 +130,16 @@ fun ShapedLauncherIcon(
modifier = Modifier
.size(size * 0.33f)
.align(Alignment.BottomEnd)
.pointerInput(onClick, onLongClick) {
detectTapGestures(
onLongPress = { onLongClick?.invoke() },
onTap = { onClick?.invoke() },
)
},
.then(
if (onClick != null || onLongClick != null) {
Modifier.pointerInput(onClick, onLongClick) {
detectTapGestures(
onLongPress = { onLongClick?.invoke() },
onTap = { onClick?.invoke() },
)
}
} else Modifier
),
color = MaterialTheme.colorScheme.secondary,
shape = CircleShape
) {
@ -468,34 +478,34 @@ private val PentagonShape: Shape
}
private val TeardropShape: Shape
get() = GenericShape { size, _ ->
moveTo(0.5f * size.width, 0f)
cubicTo(
0.776f * size.width, 0f,
size.width, 0.224f * size.height,
size.width, 0.5f * size.height,
)
lineTo(
size.width, 0.88f * size.height,
)
cubicTo(
size.width, 0.946f * size.height,
0.946f * size.width, size.height,
0.88f * size.width, size.height,
)
lineTo(0.5f * size.width, size.height)
cubicTo(
0.224f * size.width, size.height,
0f, 0.776f * size.height,
0f, 0.5f * size.height,
)
cubicTo(
0f, 0.224f * size.height,
0.224f * size.width, 0f,
0.5f * size.width, 0f,
)
close()
}
get() = GenericShape { size, _ ->
moveTo(0.5f * size.width, 0f)
cubicTo(
0.776f * size.width, 0f,
size.width, 0.224f * size.height,
size.width, 0.5f * size.height,
)
lineTo(
size.width, 0.88f * size.height,
)
cubicTo(
size.width, 0.946f * size.height,
0.946f * size.width, size.height,
0.88f * size.width, size.height,
)
lineTo(0.5f * size.width, size.height)
cubicTo(
0.224f * size.width, size.height,
0f, 0.776f * size.height,
0f, 0.5f * size.height,
)
cubicTo(
0f, 0.224f * size.height,
0.224f * size.width, 0f,
0.5f * size.width, 0f,
)
close()
}
private val EasterEggShape: Shape
get() = GenericShape { size, _ ->

View File

@ -13,8 +13,6 @@ import org.koin.core.component.inject
class LauncherActivityVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
val isEditFavoritesShown = MutableLiveData(false)
private var isDarkInMode = MutableStateFlow(false)
private val dimBackgroundState = combine(
@ -46,13 +44,5 @@ class LauncherActivityVM : ViewModel(), KoinComponent {
isDarkInMode.value = darkMode
}
fun showEditFavorites() {
isEditFavoritesShown.value = true
}
fun hideEditFavorites() {
isEditFavoritesShown.value = false
}
val layout = dataStore.data.map { it.appearance.layout }.asLiveData()
}

View File

@ -42,7 +42,6 @@ import de.mm20.launcher2.ui.base.ProvideCurrentTime
import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.component.NavBarEffects
import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.launcher.modals.EditFavoritesView
import de.mm20.launcher2.ui.launcher.transitions.HomeTransitionManager
import de.mm20.launcher2.ui.launcher.transitions.LocalHomeTransitionManager
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
@ -180,29 +179,6 @@ abstract class SharedLauncherActivity(
}
}
}
var editFavoritesDialog: MaterialDialog? = null
viewModel.isEditFavoritesShown.observe(this) {
if (it) {
val view = EditFavoritesView(this@SharedLauncherActivity)
editFavoritesDialog =
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT)).show {
customView(view = view)
title(res = R.string.menu_item_edit_favs)
positiveButton(res = R.string.close) {
viewModel.hideEditFavorites()
it.dismiss()
}
onDismiss {
view.save()
viewModel.hideEditFavorites()
}
}
} else {
editFavoritesDialog?.dismiss()
editFavoritesDialog = null
}
}
}
override fun onAttachedToWindow() {

View File

@ -0,0 +1,317 @@
package de.mm20.launcher2.ui.launcher.helper
import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.*
import androidx.compose.ui.zIndex
import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.ktx.toIntOffset
import kotlinx.coroutines.*
import kotlinx.coroutines.android.awaitFrame
import kotlin.coroutines.coroutineContext
/**
* Create and remember a [LazyDragAndDropGridState]
* @param gridState the [LazyGridState] to use with the LazyGrid
* @param onDragStart callback that will be called when an item is picked up. If the return value
* is false, the drag operation will be canceled.
*/
@Composable
fun rememberLazyDragAndDropGridState(
gridState: LazyGridState = rememberLazyGridState(),
onDragStart: (item: LazyGridItemInfo) -> Boolean = { true },
onDragEnd: (item: LazyGridItemInfo) -> Unit = {},
onDragCancel: (item: LazyGridItemInfo) -> Unit = {},
onItemMove: (from: LazyGridItemInfo, to: LazyGridItemInfo) -> Unit
): LazyDragAndDropGridState {
return remember {
LazyDragAndDropGridState(
gridState,
onDragStart,
onDragEnd,
onDragCancel,
onItemMove
)
}
}
data class LazyDragAndDropGridState(
val gridState: LazyGridState,
val onDragStart: (item: LazyGridItemInfo) -> Boolean = { true },
val onDragEnd: (item: LazyGridItemInfo) -> Unit = {},
val onDragCancel: (item: LazyGridItemInfo) -> Unit = {},
val onItemMove: (from: LazyGridItemInfo, to: LazyGridItemInfo) -> Unit
) {
var draggedItem by mutableStateOf<LazyGridItemInfo?>(null)
var draggedItemAbsolutePosition by mutableStateOf<Offset?>(null)
val draggedItemOffset by derivedStateOf {
val absPos = draggedItemAbsolutePosition ?: return@derivedStateOf null
val key = draggedItem?.key ?: return@derivedStateOf null
val draggedItem = gridState.layoutInfo.visibleItemsInfo.find {
it.key == key
} ?: return@derivedStateOf null
return@derivedStateOf absPos - draggedItem.offset.toOffset()
}
var droppedItemKey by mutableStateOf<Any?>(null)
val droppedItemOffset = mutableStateOf<IntOffset>(IntOffset.Zero)
private var currentDropPosition: Int? = null
private var dropJob: Job? = null
fun startDrag(draggedItem: LazyGridItemInfo): Boolean {
if (!onDragStart(draggedItem)) return false
this.draggedItem = draggedItem
draggedItemAbsolutePosition = draggedItem.offset.toOffset()
return true
}
/**
* Move the currently dragged item to the specified drop target if the dragged item is held
* for at least 300ms over the drop target. The move operation is canceled if the dragged item
* is released or moved out of the dropTarget area during the 300ms time frame.
*/
suspend fun attemptMove(dropTarget: LazyGridItemInfo) {
if (currentDropPosition != dropTarget.index) {
coroutineScope {
dropJob?.cancelAndJoin()
dropJob = launch {
currentDropPosition = dropTarget.index
delay(300)
// Get a fresh copy of layout info because index in saved layout info might be outdated
val dragged =
gridState.layoutInfo.visibleItemsInfo.find { it.key == draggedItem?.key }
if (dragged != null) {
onItemMove(dragged, dropTarget)
}
}
}
}
}
suspend fun cancelDrag() {
draggedItem?.let { onDragCancel(it) }
afterDragEnded()
}
suspend fun endDrag() {
draggedItem?.let { onDragEnd(it) }
afterDragEnded()
}
private suspend fun afterDragEnded() {
endScrolling()
val key = draggedItem?.key
val startOffset = draggedItemOffset
draggedItem = null
draggedItemAbsolutePosition = null
currentDropPosition = null
if (key == null || startOffset == null) return
droppedItemKey = key
droppedItemOffset.value = startOffset.toIntOffset()
droppedItemOffset.animateTo(IntOffset.Zero, IntOffset.VectorConverter)
droppedItemKey = null
}
private var scrollJob: Job? = null
private var currentScrollDelta = 0.0f
/**
* Scroll the lazy grid by `delta` px per second until [endScrolling] is called
*/
suspend fun enableScrolling(delta: Float) {
if (currentScrollDelta == delta) return
coroutineScope {
scrollJob?.cancelAndJoin()
scrollJob = launch {
currentScrollDelta = delta
delay(500)
var lastFrame = awaitFrame()
while (isActive) {
val frame = awaitFrame()
val timeDelta = frame - lastFrame
gridState.scrollBy(delta * timeDelta / 1000_000_000f)
lastFrame = frame
}
}
}
}
/**
* Cancel scrolling
*/
fun endScrolling() {
currentScrollDelta = 0f
scrollJob?.cancel()
scrollJob = null
}
}
@Composable
fun LazyVerticalDragAndDropGrid(
state: LazyDragAndDropGridState,
columns: GridCells,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyGridScope.() -> Unit
) {
LazyVerticalGrid(
columns,
modifier.dragAndDrop(
state,
LocalLayoutDirection.current == LayoutDirection.Rtl,
LocalHapticFeedback.current
),
state.gridState,
contentPadding,
false,
verticalArrangement,
horizontalArrangement,
flingBehavior,
userScrollEnabled,
content,
)
}
@Composable
fun LazyHorizontalDragAndDropGrid(
state: LazyDragAndDropGridState,
rows: GridCells,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyGridScope.() -> Unit
) {
LazyHorizontalGrid(
rows,
modifier.dragAndDrop(
state,
LocalLayoutDirection.current == LayoutDirection.Rtl,
LocalHapticFeedback.current
),
state.gridState,
contentPadding,
false,
horizontalArrangement,
verticalArrangement,
flingBehavior,
userScrollEnabled,
content,
)
}
fun Modifier.dragAndDrop(
state: LazyDragAndDropGridState,
isRtl: Boolean,
hapticFeedback: HapticFeedback
) =
this then pointerInput(null) {
val scope = CoroutineScope(coroutineContext)
val scrollEdgeSize = 32.dp.toPx()
val scrollDelta = 128.dp.toPx()
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
val draggedItem = state.gridState.layoutInfo.visibleItemsInfo.find {
Rect(
it.offset.toOffset().let {off ->
if (isRtl) off.copy(x = state.gridState.layoutInfo.viewportSize.width - off.x - it.size.width)
else off
},
it.size.toSize()
).contains(offset)
}
if (draggedItem != null && state.startDrag(draggedItem)) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
},
onDrag = { change, dragAmount ->
val absPosition = state.draggedItemAbsolutePosition
val draggedItem = state.draggedItem
if (absPosition != null && draggedItem != null) {
state.draggedItemAbsolutePosition = absPosition + dragAmount.let {
if (isRtl) it.copy(x = -it.x)
else it
}
val draggedCenter = Rect(absPosition, draggedItem.size.toSize()).center
val dragOver = state.gridState.layoutInfo.visibleItemsInfo.find {
Rect(
it.offset.toOffset(),
it.size.toSize()
).contains(draggedCenter)
}
if (dragOver != null && dragOver.key != state.draggedItem?.key) {
scope.launch {
state.attemptMove(dragOver)
}
}
val toStart =
if (state.gridState.layoutInfo.orientation == Orientation.Horizontal) draggedCenter.x else draggedCenter.y
if (toStart - state.gridState.layoutInfo.viewportStartOffset < scrollEdgeSize) {
scope.launch {
state.enableScrolling(-scrollDelta)
}
} else if (state.gridState.layoutInfo.viewportEndOffset - toStart < scrollEdgeSize) {
scope.launch {
state.enableScrolling(scrollDelta)
}
} else {
state.endScrolling()
}
}
},
onDragCancel = {
scope.launch { state.cancelDrag() }
},
onDragEnd = {
scope.launch { state.endDrag() }
},
)
}
@Composable
fun LazyGridItemScope.DraggableItem(
modifier: Modifier = Modifier,
state: LazyDragAndDropGridState,
key: Any?,
content: @Composable BoxScope.(isDragged: Boolean) -> Unit
) {
val isDragged = state.draggedItem?.key == key || state.droppedItemKey == key
Box(
modifier = modifier
.then(if (isDragged) Modifier else Modifier.animateItemPlacement())
.zIndex(if (isDragged) 1f else 0f)
.offset {
if (state.draggedItem?.key == key) {
state.draggedItemOffset?.toIntOffset() ?: IntOffset.Zero
} else if (state.droppedItemKey == key) {
state.droppedItemOffset.value
} else {
IntOffset.Zero
}
},
content = { content(isDragged) },
)
}

View File

@ -0,0 +1,212 @@
package de.mm20.launcher2.ui.launcher.modals
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.helper.DraggableItem
import de.mm20.launcher2.ui.launcher.helper.LazyVerticalDragAndDropGrid
import de.mm20.launcher2.ui.launcher.helper.rememberLazyDragAndDropGridState
import kotlin.math.roundToInt
@Composable
fun EditFavoritesSheet(
onDismiss: () -> Unit,
) {
val viewModel: EditFavoritesSheetVM = viewModel()
LaunchedEffect(null) {
viewModel.reload()
}
val items by viewModel.gridItems.observeAsState(emptyList())
val loading by viewModel.loading.observeAsState(true)
val columns = 5
val state = rememberLazyDragAndDropGridState(
onDragStart = {
items.getOrNull(it.index) is FavoritesSheetGridItem.Favorite
}
) { from, to ->
viewModel.moveItem(from, to)
}
val iconSize = 48.dp.toPixels()
BottomSheetDialog(onDismissRequest = onDismiss, title = { /*TODO*/ }) {
if (loading) {
Box(modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp).align(Alignment.Center)
)
}
} else {
LazyVerticalDragAndDropGrid(
state = state,
columns = GridCells.Fixed(columns),
) {
items(
items.size,
key = { i ->
val it = items[i]
if (it is FavoritesSheetGridItem.Favorite) it.item.key else i
},
span = { i ->
val it = items[i]
when (it) {
is FavoritesSheetGridItem.Favorite -> GridItemSpan(1)
is FavoritesSheetGridItem.Divider -> GridItemSpan(columns)
is FavoritesSheetGridItem.EmptySection -> GridItemSpan(columns)
is FavoritesSheetGridItem.Spacer -> GridItemSpan(it.span)
is FavoritesSheetGridItem.Tags -> GridItemSpan(columns)
}
}
) { i ->
when (val it = items[i]) {
is FavoritesSheetGridItem.Favorite -> {
val icon by remember(it.item.key) {
viewModel.getIcon(
it.item,
iconSize.roundToInt()
)
}.collectAsState(null)
val badge by remember(it.item.key) {
viewModel.getBadge(
it.item,
)
}.collectAsState(null)
DraggableItem(state = state, key = it.item.key) { dragged ->
GridItem(
label = it.item.labelOverride ?: it.item.label,
icon = icon,
badge = badge
)
}
}
is FavoritesSheetGridItem.Divider -> {
Text(
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp),
text = stringResource(id = it.titleRes),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.secondary
)
}
is FavoritesSheetGridItem.EmptySection -> {
val shape = MaterialTheme.shapes.medium
val color = MaterialTheme.colorScheme.outline
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
.drawBehind {
drawOutline(
outline = shape.createOutline(
size,
layoutDirection,
Density(density, fontScale)
),
color = color,
style = Stroke(
2.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(
intervals = floatArrayOf(
4.dp.toPx(),
4.dp.toPx(),
)
)
)
)
}
) {
Text(
modifier = Modifier
.align(Alignment.Center)
.padding(
horizontal = 16.dp,
vertical = 24.dp,
),
text = "Drag items here",
style = MaterialTheme.typography.labelSmall,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.outline
)
}
}
is FavoritesSheetGridItem.Spacer -> {
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
)
}
is FavoritesSheetGridItem.Tags -> {}
}
}
}
}
}
}
@Composable
fun GridItem(
modifier: Modifier = Modifier,
label: String,
icon: LauncherIcon?,
badge: Badge?
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 4.dp, vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
ShapedLauncherIcon(
size = 48.dp,
icon = { icon },
badge = { badge })
Text(
label,
modifier = Modifier.padding(top = 8.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall
)
}
}
sealed interface FavoritesSheetGridItem {
class Favorite(val item: Searchable) : FavoritesSheetGridItem
class Divider(val titleRes: Int) : FavoritesSheetGridItem
class Spacer(val span: Int = 1) : FavoritesSheetGridItem
class EmptySection() : FavoritesSheetGridItem
class Tags() : FavoritesSheetGridItem
}

View File

@ -0,0 +1,145 @@
package de.mm20.launcher2.ui.launcher.modals
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class EditFavoritesSheetVM : ViewModel(), KoinComponent {
private val repository: FavoritesRepository by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
val gridItems = MutableLiveData<List<FavoritesSheetGridItem>>(emptyList())
val loading = MutableLiveData(false)
private var manuallySorted: MutableList<Searchable> = mutableListOf()
private var automaticallySorted: MutableList<Searchable> = mutableListOf()
private var frequentlyUsed: MutableList<Searchable> = mutableListOf()
suspend fun reload() {
loading.value = true
manuallySorted = mutableListOf()
manuallySorted = repository.getFavorites(
manuallySorted = true
).first().toMutableList()
automaticallySorted = repository.getFavorites(
automaticallySorted = true
).first().toMutableList()
frequentlyUsed = repository.getFavorites(
frequentlyUsed = true
).first().toMutableList()
buildItemList()
loading.value = false
}
private fun buildItemList() {
val items = mutableListOf<FavoritesSheetGridItem>()
items.add(FavoritesSheetGridItem.Tags())
items.add(FavoritesSheetGridItem.Divider(R.string.edit_favorites_dialog_pinned_sorted))
if (manuallySorted.isEmpty()) {
items.add(FavoritesSheetGridItem.EmptySection())
} else {
items.addAll(manuallySorted.map { FavoritesSheetGridItem.Favorite(it) })
items.add(FavoritesSheetGridItem.Spacer())
}
items.add(FavoritesSheetGridItem.Divider(R.string.edit_favorites_dialog_pinned_unsorted))
if (automaticallySorted.isEmpty()) {
items.add(FavoritesSheetGridItem.EmptySection())
} else {
items.addAll(automaticallySorted.map { FavoritesSheetGridItem.Favorite(it) })
items.add(FavoritesSheetGridItem.Spacer())
}
items.add(FavoritesSheetGridItem.Divider(R.string.edit_favorites_dialog_unpinned))
if (frequentlyUsed.isEmpty()) {
items.add(FavoritesSheetGridItem.EmptySection())
} else {
items.addAll(frequentlyUsed.map { FavoritesSheetGridItem.Favorite(it) })
items.add(FavoritesSheetGridItem.Spacer())
}
gridItems.value = items
}
fun moveItem(from: LazyGridItemInfo, to: LazyGridItemInfo) {
gridItems.value?.getOrNull(from.index)?.takeIf { it is FavoritesSheetGridItem.Favorite }
?: return
gridItems.value?.getOrNull(to.index)
?.takeIf {
it is FavoritesSheetGridItem.Favorite ||
it is FavoritesSheetGridItem.EmptySection ||
it is FavoritesSheetGridItem.Spacer
}
?: return
val manuallySortedSize = manuallySorted.size + 1
val automaticallySortedSize = automaticallySorted.size + 1
val item = when {
from.index < manuallySortedSize + 2 -> {
manuallySorted.removeAt(from.index - 2)
}
from.index < manuallySortedSize + automaticallySortedSize + 3 -> {
automaticallySorted.removeAt(from.index - 3 - manuallySortedSize)
}
else -> {
frequentlyUsed.removeAt(from.index - 4 - manuallySortedSize - automaticallySortedSize)
}
}
when {
to.index < manuallySortedSize + 2 -> {
manuallySorted.add((to.index - 2).coerceAtMost(manuallySorted.size), item)
}
to.index < manuallySortedSize + automaticallySortedSize + 3 -> {
automaticallySorted.add(
(to.index - 3 - manuallySortedSize).coerceAtMost(
automaticallySorted.size
), item
)
}
else -> {
frequentlyUsed.add(
(to.index - 4 - manuallySortedSize - automaticallySortedSize).coerceAtMost(
frequentlyUsed.size
),
item
)
}
}
repository.updateFavorites(
buildList {
addAll(manuallySorted)
},
buildList {
addAll(automaticallySorted)
},
)
buildItemList()
}
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon?> {
return iconRepository.getIcon(searchable, size)
}
fun getBadge(searchable: Searchable): Flow<Badge?> {
return badgeRepository.getBadge(searchable)
}
}

View File

@ -1,20 +0,0 @@
package de.mm20.launcher2.ui.launcher.modals
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.favorites.FavoritesItem
import de.mm20.launcher2.favorites.FavoritesRepository
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class EditFavoritesVM : ViewModel(), KoinComponent {
private val repository: FavoritesRepository by inject()
suspend fun getFavorites(): List<FavoritesItem> {
return repository.getAllFavoriteItems()
}
fun saveFavorites(favorites: List<FavoritesItem>) {
repository.saveFavorites(favorites)
}
}

View File

@ -1,128 +0,0 @@
package de.mm20.launcher2.ui.launcher.modals
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import de.mm20.launcher2.favorites.FavoritesItem
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.DialogEditFavoritesBinding
import de.mm20.launcher2.ui.databinding.EditFavoritesTitleBinding
import de.mm20.launcher2.ui.legacy.component.EditFavoritesRow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class EditFavoritesView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
val viewModel: EditFavoritesVM by (context as AppCompatActivity).viewModels()
private val binding = DialogEditFavoritesBinding.inflate(LayoutInflater.from(context), this)
init {
lifecycleScope.launch {
initView()
}
}
private lateinit var favorites: MutableList<FavoritesItem>
suspend fun initView() {
favorites = withContext(Dispatchers.IO) {
viewModel.getFavorites().toMutableList()
}
binding.progressBar.visibility = View.GONE
binding.itemList.addView(getLabel(R.string.edit_favorites_dialog_pinned_sorted))
binding.itemList.setContainerScrollView(binding.scrollView)
var stage = 0
for (favorite in favorites) {
if (favorite.pinPosition <= 1 && stage == 0) {
getLabel(R.string.edit_favorites_dialog_pinned_unsorted).let {
it.tag = "stage1"
binding.itemList.addDragView(it, it.getChildAt(1))
}
stage++
}
if (favorite.pinPosition == 0 && stage == 1) {
getLabel(R.string.edit_favorites_dialog_unpinned).let {
it.tag = "stage2"
binding.itemList.addDragView(it, it.getChildAt(1))
}
stage++
}
val view = EditFavoritesRow(context, favoritesItem = favorite)
binding.itemList.addDragView(view, view.getDragHandle())
}
if (stage == 0) {
getLabel(R.string.edit_favorites_dialog_pinned_unsorted).let {
it.tag = "stage1"
binding.itemList.addDragView(it, it.getChildAt(1))
}
stage++
}
if (stage == 1) {
getLabel(R.string.edit_favorites_dialog_unpinned).let {
it.tag = "stage2"
binding.itemList.addDragView(it, it.getChildAt(1))
}
}
binding.itemList.setOnViewSwapListener { firstView, firstPosition, secondView, secondPosition ->
if (firstView is EditFavoritesRow && secondView is EditFavoritesRow) {
val firstItem = firstView.favoritesItem
val secondItem = secondView.favoritesItem
val i = firstItem.pinPosition
firstItem.pinPosition = secondItem.pinPosition
secondItem.pinPosition = i
return@setOnViewSwapListener
}
val fw = if (firstPosition > secondPosition) secondView else firstView
val sw = if (firstPosition > secondPosition) firstView else secondView
if (fw.tag == "stage1" && sw is EditFavoritesRow) {
favorites.forEach {
if (it.pinPosition > 1) {
it.pinPosition++
}
}
sw.favoritesItem.pinPosition = 2
return@setOnViewSwapListener
}
if (sw.tag == "stage1" && fw is EditFavoritesRow) {
favorites.forEach {
if (it.pinPosition > 1) {
it.pinPosition--
}
}
return@setOnViewSwapListener
}
if (fw.tag == "stage2" && sw is EditFavoritesRow) {
sw.favoritesItem.pinPosition = 1
return@setOnViewSwapListener
}
if (sw.tag == "stage2" && fw is EditFavoritesRow) {
fw.favoritesItem.pinPosition = 0
return@setOnViewSwapListener
}
}
}
fun save() {
viewModel.saveFavorites(favorites)
}
private fun getLabel(@StringRes label: Int): FrameLayout {
return EditFavoritesTitleBinding.inflate(LayoutInflater.from(context)).also {
it.text.setText(label)
}.root
}
}

View File

@ -95,15 +95,6 @@ fun SearchBar(
style = style,
overflowMenu = { show, onDismissRequest ->
DropdownMenu(expanded = show, onDismissRequest = onDismissRequest) {
DropdownMenuItem(
onClick = {
activityViewModel.showEditFavorites()
onDismissRequest()
},
text = {
Text(stringResource(R.string.menu_item_edit_favs))
}
)
DropdownMenuItem(
onClick = {
context.startActivity(

View File

@ -1,18 +1,21 @@
package de.mm20.launcher2.ui.launcher.search
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.Work
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.res.stringResource
@ -23,6 +26,7 @@ import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.component.PartialLauncherCard
import de.mm20.launcher2.ui.launcher.modals.EditFavoritesSheet
import de.mm20.launcher2.ui.launcher.search.calculator.CalculatorItem
import de.mm20.launcher2.ui.launcher.search.common.grid.GridItem
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
@ -64,6 +68,8 @@ fun SearchColumn(
val wikipedia by viewModel.wikipediaResult.observeAsState(null)
val website by viewModel.websiteResult.observeAsState(null)
var showEditFavoritesDialog by remember { mutableStateOf(false) }
LazyColumn(
state = state,
@ -77,6 +83,40 @@ fun SearchColumn(
columns = columns,
showLabels = showLabels,
reverse = reverse,
after = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp, end = 8.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier
.weight(1f)
.horizontalScroll(rememberScrollState()),
) {
FilterChip(
modifier = Modifier.padding(start = 16.dp),
selected = true,
onClick = { /*TODO*/ },
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Star,
contentDescription = null
)
},
label = { Text("Favorites") }
)
}
SmallFloatingActionButton(
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
onClick = { showEditFavoritesDialog = true }
) {
Icon(imageVector = Icons.Rounded.Edit, contentDescription = null)
}
}
}
)
}
GridResults(
@ -90,7 +130,10 @@ fun SearchColumn(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.padding(top = if (reverse) 4.dp else 8.dp, bottom = if (reverse) 8.dp else 4.dp),
.padding(
top = if (reverse) 4.dp else 8.dp,
bottom = if (reverse) 8.dp else 4.dp
),
) {
FilterChip(
modifier = Modifier.padding(horizontal = 8.dp),
@ -100,7 +143,11 @@ fun SearchColumn(
Icon(imageVector = Icons.Rounded.Person, contentDescription = null)
},
label = {
Text(stringResource(R.string.apps_profile_main), maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(
stringResource(R.string.apps_profile_main),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
FilterChip(
@ -110,7 +157,11 @@ fun SearchColumn(
Icon(imageVector = Icons.Rounded.Work, contentDescription = null)
},
label = {
Text(stringResource(R.string.apps_profile_work), maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(
stringResource(R.string.apps_profile_work),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
}
@ -149,6 +200,12 @@ fun SearchColumn(
HiddenResults()
}
}
if (showEditFavoritesDialog) {
EditFavoritesSheet(
onDismiss = { showEditFavoritesDialog = false }
)
}
}
fun LazyListScope.GridResults(

View File

@ -1,16 +1,18 @@
package de.mm20.launcher2.ui.settings.debug
import android.content.Intent
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.content.FileProvider
import de.mm20.launcher2.crashreporter.CrashReporter
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.debug.DebugInformationDumper
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.locals.LocalNavController
import kotlinx.coroutines.launch
@ -20,39 +22,61 @@ import java.io.File
fun DebugSettingsScreen() {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val viewModel: DebugSettingsScreenVM = viewModel()
val navController = LocalNavController.current
PreferenceScreen(
stringResource(R.string.preference_screen_debug)
) {
item {
Preference(
title = stringResource(R.string.preference_crash_reporter),
summary = stringResource(R.string.preference_crash_reporter_summary),
onClick = {
navController?.navigate("settings/debug/crashreporter")
})
PreferenceCategory {
Preference(
title = stringResource(R.string.preference_crash_reporter),
summary = stringResource(R.string.preference_crash_reporter_summary),
onClick = {
navController?.navigate("settings/debug/crashreporter")
})
Preference(
title = stringResource(R.string.preference_export_log),
onClick = {
scope.launch {
val path = DebugInformationDumper().dump(context)
context.tryStartActivity(
Intent.createChooser(
Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(
Intent.EXTRA_STREAM, FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".fileprovider",
File(path)
Preference(
title = stringResource(R.string.preference_export_log),
onClick = {
scope.launch {
val path = DebugInformationDumper().dump(context)
context.tryStartActivity(
Intent.createChooser(
Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(
Intent.EXTRA_STREAM, FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".fileprovider",
File(path)
)
)
)
}, null
}, null
)
)
)
}
})
}
})
}
PreferenceCategory(stringResource(R.string.preference_category_debug_tools)) {
Preference(
title = stringResource(R.string.preference_debug_cleanup_database),
summary = stringResource(R.string.preference_debug_cleanup_database_summary),
onClick = {
scope.launch {
val removedCount = viewModel.cleanUpDatabase()
Toast.makeText(
context,
context.resources.getQuantityString(
R.plurals.debug_cleanup_database_result,
removedCount,
removedCount
),
Toast.LENGTH_SHORT
).show()
}
})
}
}
}
}

View File

@ -1,10 +1,16 @@
package de.mm20.launcher2.ui.settings.debug
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.favorites.FavoritesRepository
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class DebugSettingsScreenVM: ViewModel(), KoinComponent {
fun exportLog() {
val favoritesRepository: FavoritesRepository by inject()
suspend fun cleanUpDatabase(): Int {
return favoritesRepository.cleanupDatabase()
}
}