Rework edit favorites panel
This commit is contained in:
parent
c2ccc15093
commit
86bdbecda3
@ -239,11 +239,4 @@ val OpenSourceLicenses = arrayOf(
|
|||||||
licenseText = R.raw.license_apache_2,
|
licenseText = R.raw.license_apache_2,
|
||||||
url = "https://bigbadaboom.github.io/androidsvg/"
|
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"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@ -17,6 +17,9 @@ interface SearchDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
fun insertSkipExisting(items: FavoritesItemEntity)
|
fun insertSkipExisting(items: FavoritesItemEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insertAllReplaceExisting(items: List<FavoritesItemEntity>)
|
||||||
|
|
||||||
|
|
||||||
@Query("SELECT * FROM Searchable " +
|
@Query("SELECT * FROM Searchable " +
|
||||||
"WHERE ((:manuallySorted AND pinned > 1) OR " +
|
"WHERE ((:manuallySorted AND pinned > 1) OR " +
|
||||||
@ -155,4 +158,6 @@ interface SearchDao {
|
|||||||
@Query("DELETE FROM Searchable WHERE hidden = 0")
|
@Query("DELETE FROM Searchable WHERE hidden = 0")
|
||||||
fun deleteAllFavorites()
|
fun deleteAllFavorites()
|
||||||
|
|
||||||
|
@Query("UPDATE Searchable SET `pinned` = 0")
|
||||||
|
fun unpinAll()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package de.mm20.launcher2.favorites
|
package de.mm20.launcher2.favorites
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
import de.mm20.launcher2.database.AppDatabase
|
import de.mm20.launcher2.database.AppDatabase
|
||||||
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
||||||
@ -17,12 +18,32 @@ import org.koin.core.component.KoinComponent
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
interface FavoritesRepository {
|
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(
|
fun getFavorites(
|
||||||
columns: Int,
|
columns: Int,
|
||||||
maxRows: Int? = null,
|
maxRows: Int? = null,
|
||||||
excludeCalendarEvents: Boolean = false
|
excludeCalendarEvents: Boolean = false
|
||||||
): Flow<List<Searchable>>
|
): 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 getPinnedCalendarEvents(): Flow<List<Searchable>>
|
||||||
fun getHiddenCalendarEventKeys(): Flow<List<String>>
|
fun getHiddenCalendarEventKeys(): Flow<List<String>>
|
||||||
fun isPinned(searchable: Searchable): Flow<Boolean>
|
fun isPinned(searchable: Searchable): Flow<Boolean>
|
||||||
@ -32,7 +53,11 @@ interface FavoritesRepository {
|
|||||||
fun hideItem(searchable: Searchable)
|
fun hideItem(searchable: Searchable)
|
||||||
fun unhideItem(searchable: Searchable)
|
fun unhideItem(searchable: Searchable)
|
||||||
fun incrementLaunchCounter(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 getHiddenItems(): Flow<List<Searchable>>
|
||||||
fun getHiddenItemKeys(): Flow<List<String>>
|
fun getHiddenItemKeys(): Flow<List<String>>
|
||||||
fun remove(searchable: Searchable)
|
fun remove(searchable: Searchable)
|
||||||
@ -51,6 +76,13 @@ interface FavoritesRepository {
|
|||||||
|
|
||||||
suspend fun export(toDir: File)
|
suspend fun export(toDir: File)
|
||||||
suspend fun import(fromDir: 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(
|
internal class FavoritesRepositoryImpl(
|
||||||
@ -82,13 +114,11 @@ internal class FavoritesRepositoryImpl(
|
|||||||
manuallySorted = true,
|
manuallySorted = true,
|
||||||
automaticallySorted = true,
|
automaticallySorted = true,
|
||||||
frequentlyUsed = false,
|
frequentlyUsed = false,
|
||||||
limit = columns * (maxRows ?: 20))
|
limit = columns * (maxRows ?: 20)
|
||||||
|
)
|
||||||
}.map {
|
}.map {
|
||||||
it.mapNotNull {
|
it.mapNotNull {
|
||||||
val item = fromDatabaseEntity(it).searchable
|
val item = fromDatabaseEntity(it).searchable
|
||||||
if (item == null) {
|
|
||||||
dao.deleteByKey(it.key)
|
|
||||||
}
|
|
||||||
return@mapNotNull item
|
return@mapNotNull item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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>> {
|
override fun getPinnedCalendarEvents(): Flow<List<CalendarEvent>> {
|
||||||
return database.searchDao().getFavoritesWithTypes(
|
return database.searchDao().getFavoritesWithTypes(
|
||||||
includeTypes = listOf("calendar"),
|
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>> {
|
override fun getHiddenItems(): Flow<List<Searchable>> {
|
||||||
return database.searchDao().getHiddenItems().map {
|
return database.searchDao().getHiddenItems().map {
|
||||||
it.mapNotNull { fromDatabaseEntity(it).searchable }
|
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 {
|
private fun fromDatabaseEntity(entity: FavoritesItemEntity): FavoritesItem {
|
||||||
val deserializer: SearchableDeserializer =
|
val deserializer: SearchableDeserializer =
|
||||||
getDeserializer(context, entity.serializedSearchable)
|
getDeserializer(context, entity.serializedSearchable)
|
||||||
|
val searchable = deserializer.deserialize(entity.serializedSearchable.substringAfter("#"))
|
||||||
|
if (searchable == null) removeInvalidItem(entity.key)
|
||||||
return FavoritesItem(
|
return FavoritesItem(
|
||||||
key = entity.key,
|
key = entity.key,
|
||||||
searchable = deserializer.deserialize(entity.serializedSearchable.substringAfter("#")),
|
searchable = searchable,
|
||||||
launchCount = entity.launchCount,
|
launchCount = entity.launchCount,
|
||||||
pinPosition = entity.pinPosition,
|
pinPosition = entity.pinPosition,
|
||||||
hidden = entity.hidden
|
hidden = entity.hidden
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removeInvalidItem(key: String) {
|
||||||
|
scope.launch {
|
||||||
|
database.searchDao().deleteByKey(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getFromKeys(keys: List<String>): List<Searchable> {
|
override fun getFromKeys(keys: List<String>): List<Searchable> {
|
||||||
val dao = database.searchDao()
|
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) {
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -434,6 +434,13 @@
|
|||||||
<string name="preference_imperial_units_summary">Use degrees Fahrenheit and miles per hour</string>
|
<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_imperial_units">Imperial units</string>
|
||||||
<string name="preference_category_debug">Debug</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_category_icons">Icons</string>
|
||||||
<string name="preference_cards">Cards</string>
|
<string name="preference_cards">Cards</string>
|
||||||
<string name="preference_cards_summary">Customize card appearance</string>
|
<string name="preference_cards_summary">Customize card appearance</string>
|
||||||
|
|||||||
@ -382,10 +382,6 @@ dependencyResolutionManagement {
|
|||||||
alias("lottie")
|
alias("lottie")
|
||||||
.to("com.airbnb.android", "lottie-compose")
|
.to("com.airbnb.android", "lottie-compose")
|
||||||
.version("5.2.0")
|
.version("5.2.0")
|
||||||
|
|
||||||
alias("composereorderable")
|
|
||||||
.to("org.burnoutcrew.composereorderable", "reorderable")
|
|
||||||
.version("0.9.2")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,7 +112,6 @@ dependencies {
|
|||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
||||||
implementation(libs.lottie)
|
implementation(libs.lottie)
|
||||||
implementation(libs.composereorderable)
|
|
||||||
|
|
||||||
implementation(project(":material-color-utilities"))
|
implementation(project(":material-color-utilities"))
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,9 @@ import androidx.compose.animation.core.Animatable
|
|||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
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.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@ -93,12 +95,16 @@ fun ShapedLauncherIcon(
|
|||||||
clip = currentIcon?.backgroundLayer !is TransparentLayer
|
clip = currentIcon?.backgroundLayer !is TransparentLayer
|
||||||
this.shape = shape
|
this.shape = shape
|
||||||
}
|
}
|
||||||
.pointerInput(onClick, onLongClick) {
|
.then(
|
||||||
|
if (onClick != null || onLongClick != null) {
|
||||||
|
Modifier.pointerInput(onClick, onLongClick) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onLongPress = { onLongClick?.invoke() },
|
onLongPress = { onLongClick?.invoke() },
|
||||||
onTap = { onClick?.invoke() },
|
onTap = { onClick?.invoke() },
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
|
} else Modifier
|
||||||
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
currentIcon?.let {
|
currentIcon?.let {
|
||||||
@ -124,12 +130,16 @@ fun ShapedLauncherIcon(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(size * 0.33f)
|
.size(size * 0.33f)
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.pointerInput(onClick, onLongClick) {
|
.then(
|
||||||
|
if (onClick != null || onLongClick != null) {
|
||||||
|
Modifier.pointerInput(onClick, onLongClick) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onLongPress = { onLongClick?.invoke() },
|
onLongPress = { onLongClick?.invoke() },
|
||||||
onTap = { onClick?.invoke() },
|
onTap = { onClick?.invoke() },
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
|
} else Modifier
|
||||||
|
),
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
shape = CircleShape
|
shape = CircleShape
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -13,8 +13,6 @@ import org.koin.core.component.inject
|
|||||||
class LauncherActivityVM : ViewModel(), KoinComponent {
|
class LauncherActivityVM : ViewModel(), KoinComponent {
|
||||||
private val dataStore: LauncherDataStore by inject()
|
private val dataStore: LauncherDataStore by inject()
|
||||||
|
|
||||||
val isEditFavoritesShown = MutableLiveData(false)
|
|
||||||
|
|
||||||
private var isDarkInMode = MutableStateFlow(false)
|
private var isDarkInMode = MutableStateFlow(false)
|
||||||
|
|
||||||
private val dimBackgroundState = combine(
|
private val dimBackgroundState = combine(
|
||||||
@ -46,13 +44,5 @@ class LauncherActivityVM : ViewModel(), KoinComponent {
|
|||||||
isDarkInMode.value = darkMode
|
isDarkInMode.value = darkMode
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showEditFavorites() {
|
|
||||||
isEditFavoritesShown.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideEditFavorites() {
|
|
||||||
isEditFavoritesShown.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val layout = dataStore.data.map { it.appearance.layout }.asLiveData()
|
val layout = dataStore.data.map { it.appearance.layout }.asLiveData()
|
||||||
}
|
}
|
||||||
@ -42,7 +42,6 @@ import de.mm20.launcher2.ui.base.ProvideCurrentTime
|
|||||||
import de.mm20.launcher2.ui.base.ProvideSettings
|
import de.mm20.launcher2.ui.base.ProvideSettings
|
||||||
import de.mm20.launcher2.ui.component.NavBarEffects
|
import de.mm20.launcher2.ui.component.NavBarEffects
|
||||||
import de.mm20.launcher2.ui.ktx.animateTo
|
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.HomeTransitionManager
|
||||||
import de.mm20.launcher2.ui.launcher.transitions.LocalHomeTransitionManager
|
import de.mm20.launcher2.ui.launcher.transitions.LocalHomeTransitionManager
|
||||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
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() {
|
override fun onAttachedToWindow() {
|
||||||
|
|||||||
@ -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) },
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -95,15 +95,6 @@ fun SearchBar(
|
|||||||
style = style,
|
style = style,
|
||||||
overflowMenu = { show, onDismissRequest ->
|
overflowMenu = { show, onDismissRequest ->
|
||||||
DropdownMenu(expanded = show, onDismissRequest = onDismissRequest) {
|
DropdownMenu(expanded = show, onDismissRequest = onDismissRequest) {
|
||||||
DropdownMenuItem(
|
|
||||||
onClick = {
|
|
||||||
activityViewModel.showEditFavorites()
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(stringResource(R.string.menu_item_edit_favs))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
context.startActivity(
|
context.startActivity(
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.search
|
package de.mm20.launcher2.ui.launcher.search
|
||||||
|
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material.icons.Icons
|
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.Person
|
||||||
|
import androidx.compose.material.icons.rounded.Star
|
||||||
import androidx.compose.material.icons.rounded.Work
|
import androidx.compose.material.icons.rounded.Work
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.res.stringResource
|
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.R
|
||||||
import de.mm20.launcher2.ui.component.LauncherCard
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
import de.mm20.launcher2.ui.component.PartialLauncherCard
|
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.calculator.CalculatorItem
|
||||||
import de.mm20.launcher2.ui.launcher.search.common.grid.GridItem
|
import de.mm20.launcher2.ui.launcher.search.common.grid.GridItem
|
||||||
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
|
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
|
||||||
@ -64,6 +68,8 @@ fun SearchColumn(
|
|||||||
val wikipedia by viewModel.wikipediaResult.observeAsState(null)
|
val wikipedia by viewModel.wikipediaResult.observeAsState(null)
|
||||||
val website by viewModel.websiteResult.observeAsState(null)
|
val website by viewModel.websiteResult.observeAsState(null)
|
||||||
|
|
||||||
|
var showEditFavoritesDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = state,
|
state = state,
|
||||||
@ -77,6 +83,40 @@ fun SearchColumn(
|
|||||||
columns = columns,
|
columns = columns,
|
||||||
showLabels = showLabels,
|
showLabels = showLabels,
|
||||||
reverse = reverse,
|
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(
|
GridResults(
|
||||||
@ -90,7 +130,10 @@ fun SearchColumn(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp)
|
.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(
|
FilterChip(
|
||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
@ -100,7 +143,11 @@ fun SearchColumn(
|
|||||||
Icon(imageVector = Icons.Rounded.Person, contentDescription = null)
|
Icon(imageVector = Icons.Rounded.Person, contentDescription = null)
|
||||||
},
|
},
|
||||||
label = {
|
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(
|
FilterChip(
|
||||||
@ -110,7 +157,11 @@ fun SearchColumn(
|
|||||||
Icon(imageVector = Icons.Rounded.Work, contentDescription = null)
|
Icon(imageVector = Icons.Rounded.Work, contentDescription = null)
|
||||||
},
|
},
|
||||||
label = {
|
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()
|
HiddenResults()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showEditFavoritesDialog) {
|
||||||
|
EditFavoritesSheet(
|
||||||
|
onDismiss = { showEditFavoritesDialog = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LazyListScope.GridResults(
|
fun LazyListScope.GridResults(
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
package de.mm20.launcher2.ui.settings.debug
|
package de.mm20.launcher2.ui.settings.debug
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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.core.content.FileProvider
|
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.debug.DebugInformationDumper
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.component.preferences.Preference
|
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.component.preferences.PreferenceScreen
|
||||||
import de.mm20.launcher2.ui.locals.LocalNavController
|
import de.mm20.launcher2.ui.locals.LocalNavController
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -20,11 +22,13 @@ import java.io.File
|
|||||||
fun DebugSettingsScreen() {
|
fun DebugSettingsScreen() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val viewModel: DebugSettingsScreenVM = viewModel()
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
PreferenceScreen(
|
PreferenceScreen(
|
||||||
stringResource(R.string.preference_screen_debug)
|
stringResource(R.string.preference_screen_debug)
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
|
PreferenceCategory {
|
||||||
Preference(
|
Preference(
|
||||||
title = stringResource(R.string.preference_crash_reporter),
|
title = stringResource(R.string.preference_crash_reporter),
|
||||||
summary = stringResource(R.string.preference_crash_reporter_summary),
|
summary = stringResource(R.string.preference_crash_reporter_summary),
|
||||||
@ -54,5 +58,25 @@ fun DebugSettingsScreen() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,10 +1,16 @@
|
|||||||
package de.mm20.launcher2.ui.settings.debug
|
package de.mm20.launcher2.ui.settings.debug
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
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.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
class DebugSettingsScreenVM: ViewModel(), KoinComponent {
|
class DebugSettingsScreenVM: ViewModel(), KoinComponent {
|
||||||
fun exportLog() {
|
|
||||||
|
|
||||||
|
val favoritesRepository: FavoritesRepository by inject()
|
||||||
|
suspend fun cleanUpDatabase(): Int {
|
||||||
|
return favoritesRepository.cleanupDatabase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user