Add settings page to manage tags

This commit is contained in:
MM20 2022-12-21 19:24:32 +01:00
parent 85f7a740e0
commit ddc157741a
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
16 changed files with 586 additions and 38 deletions

View File

@ -128,6 +128,7 @@ dependencies {
implementation(project(":core:permissions"))
implementation(project(":core:preferences"))
implementation(project(":services:search"))
implementation(project(":services:tags"))
implementation(project(":data:unitconverter"))
implementation(project(":app:ui"))
implementation(project(":data:weather"))

View File

@ -28,6 +28,7 @@ import de.mm20.launcher2.notifications.notificationsModule
import de.mm20.launcher2.permissions.permissionsModule
import de.mm20.launcher2.preferences.preferencesModule
import de.mm20.launcher2.searchactions.searchActionsModule
import de.mm20.launcher2.services.tags.servicesTagsModule
import de.mm20.launcher2.weather.weatherModule
import kotlinx.coroutines.*
import org.koin.android.ext.koin.androidContext
@ -74,7 +75,8 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
weatherModule,
websitesModule,
widgetsModule,
wikipediaModule
wikipediaModule,
servicesTagsModule,
)
)
}

View File

@ -117,6 +117,7 @@ dependencies {
implementation(project(":core:ktx"))
implementation(project(":services:icons"))
implementation(project(":services:music"))
implementation(project(":services:tags"))
implementation(project(":data:weather"))
implementation(project(":data:calendar"))
implementation(project(":services:search"))

View File

@ -10,6 +10,7 @@ import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.SavableSearchable
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.coroutines.coroutineContext
@ -85,6 +86,6 @@ class CustomizeSearchableSheetVM(
}
suspend fun autocompleteTags(query: String): List<String> {
return customAttributesRepository.getAllTags(startsWith = query)
return customAttributesRepository.getAllTags(startsWith = query).first()
}
}

View File

@ -78,6 +78,7 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
availableTags.value =
customAttributesRepository
.getAllTags()
.first()
.filter {t -> pinnedTags.none { it.tag == t } }
.sortedBy { it.normalize() }
.map { Tag(it) }

View File

@ -1,11 +1,265 @@
package de.mm20.launcher2.ui.settings.tags
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.SmallMessage
import de.mm20.launcher2.ui.ktx.toPixels
@Composable
fun EditTagSheet(
tag: String?,
onDismiss: () -> Unit,
) {
val viewModel: EditTagSheetVM = viewModel()
val isCreatingNewTag = tag == null
LaunchedEffect(tag) {
viewModel.init(tag)
}
if (viewModel.loading) return
BottomSheetDialog(
title = {
Text(
stringResource(
if (viewModel.page == EditTagSheetPage.CustomizeTag || !isCreatingNewTag) R.string.edit_tag_title
else R.string.create_tag_title
)
)
},
confirmButton = {
if (viewModel.page == EditTagSheetPage.CustomizeTag) {
OutlinedButton(onClick = {
viewModel.save()
onDismiss()
}) {
Text(stringResource(R.string.close))
}
} else if (isCreatingNewTag) {
Button(
enabled = (viewModel.tagName.isNotBlank() && viewModel.page == EditTagSheetPage.CreateTag && !viewModel.tagNameExists)
|| (viewModel.page == EditTagSheetPage.PickItems && viewModel.taggedItems.isNotEmpty()),
onClick = { viewModel.onClickContinue() }) {
Text(stringResource(R.string.action_next))
}
} else {
OutlinedButton(onClick = { viewModel.closeItemPicker() }) {
Text(stringResource(id = R.string.ok))
}
}
},
onDismissRequest = {
if (viewModel.page == EditTagSheetPage.CustomizeTag) viewModel.save()
onDismiss()
},
swipeToDismiss = {
!(!isCreatingNewTag && viewModel.page == EditTagSheetPage.PickItems)
},
dismissOnBackPress = {
!(!isCreatingNewTag && viewModel.page == EditTagSheetPage.PickItems)
}
) {
when (viewModel.page) {
EditTagSheetPage.CreateTag -> CreateNewTagPage(viewModel)
EditTagSheetPage.PickItems -> PickItems(viewModel)
EditTagSheetPage.CustomizeTag -> CustomizeTag(viewModel)
}
}
}
@Composable
fun CreateNewTagPage(viewModel: EditTagSheetVM) {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardActions = KeyboardActions(
onDone = {
viewModel.onClickContinue()
}
),
isError = viewModel.tagNameExists,
supportingText = { if (viewModel.tagNameExists) Text(stringResource(id = R.string.tag_exists_error)) },
label = { Text(stringResource(R.string.tag_name)) },
value = viewModel.tagName,
onValueChange = { viewModel.tagName = it }
)
}
}
@Composable
fun PickItems(viewModel: EditTagSheetVM) {
LazyColumn(modifier = Modifier.fillMaxWidth()) {
item {
Text(stringResource(id = R.string.tag_select_items),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(bottom = 8.dp)
)
}
items(viewModel.taggableApps) {
val iconSize = 32.dp.toPixels()
val icon by remember(it.item.key) {
viewModel.getIcon(it.item, iconSize.toInt())
}.collectAsState(null)
ListItem(item = it, icon = icon, onTagChanged = { tagged ->
if (tagged) viewModel.tagItem(it.item)
else viewModel.untagItem(it.item)
})
}
item {
Box(
modifier = Modifier
.padding(vertical = 8.dp)
.background(MaterialTheme.colorScheme.outlineVariant)
.fillMaxWidth()
.height(1.dp)
)
}
items(viewModel.taggableOther) {
val iconSize = 32.dp.toPixels()
val icon by remember(it.item.key) {
viewModel.getIcon(it.item, iconSize.toInt())
}.collectAsState(null)
ListItem(item = it, icon = icon, onTagChanged = { tagged ->
if (tagged) viewModel.tagItem(it.item)
else viewModel.untagItem(it.item)
})
}
}
}
@Composable
fun ListItem(
item: TaggableItem,
icon: LauncherIcon?,
onTagChanged: (Boolean) -> Unit
) {
OutlinedCard(
modifier = Modifier
.padding(vertical = 4.dp)
.fillMaxWidth(),
onClick = {
onTagChanged(!item.isTagged)
}
) {
Row(
modifier = Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
ShapedLauncherIcon(icon = { icon }, size = 32.dp, modifier = Modifier.padding(4.dp))
Text(
modifier = Modifier
.padding(horizontal = 12.dp)
.weight(1f),
text = item.item.label,
style = MaterialTheme.typography.labelLarge
)
Checkbox(checked = item.isTagged, onCheckedChange = { checked ->
onTagChanged(checked)
})
}
}
}
@Composable
fun CustomizeTag(viewModel: EditTagSheetVM) {
val iconSize = 32.dp.toPixels()
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
label = { Text(stringResource(R.string.tag_name)) },
value = viewModel.tagName,
onValueChange = { viewModel.tagName = it }
)
val icon1 = remember(viewModel.taggedItems.getOrNull(0)?.key) {
viewModel.taggedItems.getOrNull(0)?.let {
viewModel.getIcon(it, iconSize.toInt())
}
}?.collectAsState(null)
val icon2 = remember(viewModel.taggedItems.getOrNull(1)?.key) {
viewModel.taggedItems.getOrNull(1)?.let {
viewModel.getIcon(it, iconSize.toInt())
}
}?.collectAsState(null)
val icon3 = remember(viewModel.taggedItems.getOrNull(2)?.key) {
viewModel.taggedItems.getOrNull(2)?.let {
viewModel.getIcon(it, iconSize.toInt())
}
}?.collectAsState(null)
TextButton(
modifier = Modifier
.padding(vertical = 16.dp).fillMaxWidth(),
onClick = { viewModel.openItemPicker() }) {
Text(
modifier = Modifier.weight(1f),
text = pluralStringResource(
R.plurals.tag_selected_items,
viewModel.taggedItems.size,
viewModel.taggedItems.size
)
)
Box(modifier = Modifier.padding(start = 8.dp).width(64.dp).height(32.dp), contentAlignment = Alignment.CenterEnd) {
ShapedLauncherIcon(size = 32.dp, icon = { icon1?.value }, modifier = Modifier.offset(x = -0.dp))
ShapedLauncherIcon(size = 32.dp, icon = { icon2?.value }, modifier = Modifier.offset(x = -16.dp))
ShapedLauncherIcon(size = 32.dp, icon = { icon3?.value }, modifier = Modifier.offset(x = -32.dp))
}
}
AnimatedVisibility(viewModel.tagNameExists || viewModel.taggedItems.isEmpty()) {
SmallMessage(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Rounded.Warning,
text = stringResource(
if (viewModel.taggedItems.isEmpty()) R.string.tag_no_items_message else R.string.tag_exists_message
)
)
}
}
}

View File

@ -0,0 +1,120 @@
package de.mm20.launcher2.ui.settings.tags
import android.util.Log
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.applications.AppRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchService
import de.mm20.launcher2.services.tags.TagsService
import kotlinx.coroutines.Dispatchers
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 EditTagSheetVM : ViewModel(), KoinComponent {
private val tagService: TagsService by inject()
private val searchService: SearchService by inject()
private val iconRepository: IconRepository by inject()
private val appRepository: AppRepository by inject()
private var oldTagName by mutableStateOf<String?>(null)
private var allTags by mutableStateOf(emptySet<String>())
var tagName by mutableStateOf("")
var loading by mutableStateOf(true)
var page by mutableStateOf(EditTagSheetPage.CreateTag)
var taggedItems by mutableStateOf(emptyList<SavableSearchable>())
var taggableApps by mutableStateOf(emptyList<TaggableItem>())
var taggableOther by mutableStateOf(emptyList<TaggableItem>())
val tagNameExists by derivedStateOf {
tagName != oldTagName && allTags.contains(tagName)
}
fun init(tag: String?) {
Log.d("MM20", "Init with tag: $tag")
loading = true
this.oldTagName = tag
this.tagName = tag ?: ""
this.page = if (tag == null) EditTagSheetPage.CreateTag else EditTagSheetPage.CustomizeTag
this.taggedItems = emptyList()
viewModelScope.launch(Dispatchers.Default) {
allTags = tagService.getAllTags().first().toSet()
val items = if (tag != null) tagService.getTaggedItems(tag).first() else emptyList()
val apps = appRepository.getAllInstalledApps().first().sorted()
taggedItems = items
taggableApps = apps.map { app -> TaggableItem(app, items.any { app.key == it.key }) }
taggableOther = items.mapNotNull { item ->
if (apps.any { item.key == it.key }) null
else TaggableItem(item, true)
}.sortedBy { it.item }
loading = false
}
}
fun save() {
val oldName = oldTagName
val newName = tagName
if (taggedItems.isEmpty() && oldName != null) tagService.deleteTag(oldName)
else if (oldName != null) tagService.updateTag(oldName, newName = newName, items = taggedItems)
else tagService.createTag(tagName, taggedItems)
loading = true
}
fun onClickContinue() {
if (page == EditTagSheetPage.CreateTag && tagNameExists) return
page = if (page == EditTagSheetPage.CreateTag) EditTagSheetPage.PickItems else EditTagSheetPage.CustomizeTag
oldTagName = tagName
}
fun getIcon(item: SavableSearchable, size: Int): Flow<LauncherIcon> {
return iconRepository.getIcon(item, size)
}
fun openItemPicker() {
page = EditTagSheetPage.PickItems
}
fun closeItemPicker() {
page = EditTagSheetPage.CustomizeTag
}
fun tagItem(item: SavableSearchable) {
taggedItems = taggedItems + item
taggableApps =
taggableApps.map { app -> app.copy(isTagged = taggedItems.any { it.key == app.item.key }) }
taggableOther =
taggableOther.map { oth -> oth.copy(isTagged = taggedItems.any { it.key == oth.item.key }) }
}
fun untagItem(item: SavableSearchable) {
taggedItems = taggedItems.filter { it.key != item.key }
taggableApps =
taggableApps.map { app -> app.copy(isTagged = taggedItems.any { it.key == app.item.key }) }
taggableOther =
taggableOther.map { oth -> oth.copy(isTagged = taggedItems.any { it.key == oth.item.key }) }
}
}
enum class EditTagSheetPage {
CreateTag,
PickItems,
CustomizeTag,
}
@Stable
data class TaggableItem(val item: SavableSearchable, val isTagged: Boolean)

View File

@ -2,12 +2,26 @@ package de.mm20.launcher2.ui.settings.tags
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.Tag
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
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
@ -16,27 +30,71 @@ import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
fun TagsSettingsScreen() {
val viewModel: TagsSettingsScreenVM = viewModel()
LaunchedEffect(null) {
viewModel.update()
}
val tags by remember { viewModel.tags }.collectAsState(emptyList())
PreferenceScreen(
title = "Tags",
floatingActionButton = {
FloatingActionButton(onClick = { /*TODO*/ }) {
FloatingActionButton(onClick = { viewModel.createTag.value = true }) {
Icon(Icons.Rounded.Add, null)
}
}
) {
item {
PreferenceCategory {
for (tag in viewModel.tags.value) {
for (tag in tags) {
var showMenu by remember { mutableStateOf(false) }
Preference(
icon = Icons.Rounded.Tag,
title = tag,
onClick = {
viewModel.editTag.value = tag
},
controls = {
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Rounded.MoreVert, null)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }) {
DropdownMenuItem(
text = { Text(stringResource(R.string.duplicate)) },
leadingIcon = { Icon(Icons.Rounded.ContentCopy, null) },
onClick = {
viewModel.duplicateTag(tag)
showMenu = false
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.menu_delete)) },
leadingIcon = { Icon(Icons.Rounded.Delete, null) },
onClick = {
viewModel.deleteTag(tag)
showMenu = false
}
)
}
}
)
}
}
}
}
if (viewModel.editTag.value != null) {
EditTagSheet(
tag = viewModel.editTag.value,
onDismiss = {
viewModel.editTag.value = null
viewModel.createTag.value = false
}
)
} else if(viewModel.createTag.value) {
EditTagSheet(
tag = null,
onDismiss = {
viewModel.createTag.value = false
viewModel.editTag.value = null
}
)
}
}

View File

@ -4,17 +4,35 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.services.tags.TagsService
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class TagsSettingsScreenVM: ViewModel(), KoinComponent {
private val customAttributesRepository: CustomAttributesRepository by inject()
private val tagsService: TagsService by inject()
val tags = mutableStateOf(emptyList<String>())
val tags = tagsService.getAllTags()
suspend fun update() {
tags.value = customAttributesRepository.getAllTags()
var editTag = mutableStateOf<String?>(null)
var createTag = mutableStateOf(false)
fun duplicateTag(tag: String) {
viewModelScope.launch {
val allTags = tags.first()
var i = 2
var newName = "$tag ($i)"
while(allTags.contains(newName)) {
i++
newName = "$tag ($i)"
}
tagsService.cloneTag(tag, newName)
}
}
fun deleteTag(tag: String) {
tagsService.deleteTag(tag)
}
}

View File

@ -9,10 +9,10 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface CustomAttrsDao {
@Query("SELECT * FROM CustomAttributes WHERE type = :type AND key = :key LIMIT 1")
@Query("SELECT * FROM CustomAttributes WHERE type = :type AND `key` = :key LIMIT 1")
fun getCustomAttribute(key: String, type: String) : Flow<CustomAttributeEntity?>
@Query("DELETE FROM CustomAttributes WHERE type = :type AND key = :key")
@Query("DELETE FROM CustomAttributes WHERE type = :type AND `key` = :key")
fun clearCustomAttribute(key: String, type: String)
@Insert
@ -21,10 +21,10 @@ interface CustomAttrsDao {
@Insert
suspend fun insertCustomAttributes(entities: List<CustomAttributeEntity>)
@Query("SELECT * FROM CustomAttributes WHERE type = :type AND key IN (:keys)")
@Query("SELECT * FROM CustomAttributes WHERE type = :type AND `key` IN (:keys)")
fun getCustomAttributes(keys: List<String>, type: String) : Flow<List<CustomAttributeEntity>>
@Query("SELECT DISTINCT key FROM CustomAttributes WHERE (type = 'label' OR type = 'tag') AND value LIKE :query")
@Query("SELECT DISTINCT `key` FROM CustomAttributes WHERE (type = 'label' OR type = 'tag') AND value LIKE :query")
fun search(query: String): Flow<List<String>>
@Transaction
@ -34,24 +34,30 @@ interface CustomAttrsDao {
}
@Query("SELECT DISTINCT value FROM CustomAttributes WHERE type = 'tag' AND value LIKE :like ORDER BY value")
suspend fun getAllTagsLike(like: String): List<String>
fun getAllTagsLike(like: String): Flow<List<String>>
@Query("SELECT DISTINCT value FROM CustomAttributes WHERE type = 'tag' ORDER BY value")
suspend fun getAllTags(): List<String>
fun getAllTags(): Flow<List<String>>
@Query("SELECT key FROM CustomAttributes WHERE type = 'tag' AND value = :tag")
@Query("SELECT `key` FROM CustomAttributes WHERE type = 'tag' AND value = :tag")
fun getItemsWithTag(tag: String): Flow<List<String>>
@Transaction
suspend fun setItemsWithTag(tag: String, items: List<String>) {
deleteTag(tag)
insertCustomAttributes(items.map { CustomAttributeEntity(it, "tag", tag) })
}
@Transaction
suspend fun addTag(key: String, tag: String) {
removeTag(key, tag)
insertTag(key, tag)
}
@Query("DELETE FROM CustomAttributes WHERE type = 'tag' AND key = :key AND value = :tag")
@Query("DELETE FROM CustomAttributes WHERE type = 'tag' AND `key` = :key AND value = :tag")
suspend fun removeTag(key: String, tag: String)
@Query("INSERT INTO CustomAttributes (key, value, type) VALUES (:key, :tag, 'tag')")
@Query("INSERT INTO CustomAttributes (`key`, value, type) VALUES (:key, :tag, 'tag')")
suspend fun insertTag(key: String, tag: String)
@Query("UPDATE CustomAttributes SET value = :newName WHERE value = :oldName AND type = 'tag'")

View File

@ -710,4 +710,16 @@
<string name="search_action_websearch_url_hint">The URL template that is used to construct the web search URL. Use ${1} as a placeholder for the actual search term, e.g. https://google.com/search?q=${1}.</string>
<string name="more_information">More information</string>
<string name="experimental_feature">Experimental</string>
<string name="create_tag_title">New tag</string>
<string name="edit_tag_title">Edit tag</string>
<string name="tag_exists_error">A tag with this name already exists.</string>
<string name="tag_exists_message">A tag with this name already exists. If you continue, the two tags will be merged.</string>
<string name="tag_no_items_message">No items are assigned to this tag. If you continue, the tag will be deleted.</string>
<string name="tag_empty_message">Tag name must not be blank.</string>
<string name="tag_select_items">Select items:</string>
<string name="tag_name">Tag name</string>
<plurals name="tag_selected_items">
<item quantity="one">%1$d item selected</item>
<item quantity="other">%1$d items selected</item>
</plurals>
</resources>

View File

@ -34,11 +34,13 @@ interface CustomAttributesRepository {
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
suspend fun getAllTags(startsWith: String? = null): List<String>
fun getAllTags(startsWith: String? = null): Flow<List<String>>
fun getItemsForTag(tag: String): Flow<List<SavableSearchable>>
fun setItemsForTag(tag: String, items: List<SavableSearchable>): Job
fun addTag(item: SavableSearchable, tag: String)
fun renameTag(oldName: String, newName: String)
fun renameTag(oldName: String, newName: String): Job
fun deleteTag(tag: String): Job
suspend fun cleanupDatabase(): Int
}
@ -114,7 +116,7 @@ internal class CustomAttributesRepositoryImpl(
}
}
override suspend fun getAllTags(startsWith: String?): List<String> {
override fun getAllTags(startsWith: String?): Flow<List<String>> {
val dao = appDatabase.customAttrsDao()
return if (startsWith != null) {
dao.getAllTagsLike("$startsWith%")
@ -130,6 +132,17 @@ internal class CustomAttributesRepositoryImpl(
}
}
override fun setItemsForTag(tag: String, items: List<SavableSearchable>): Job {
val dao = appDatabase.customAttrsDao()
return scope.launch {
dao.setItemsWithTag(tag, items.map { it.key })
for (item in items) {
favoritesRepository.save(item)
}
}
}
override fun addTag(item: SavableSearchable, tag: String) {
val dao = appDatabase.customAttrsDao()
scope.launch {
@ -137,13 +150,20 @@ internal class CustomAttributesRepositoryImpl(
}
}
override fun renameTag(oldName: String, newName: String) {
override fun renameTag(oldName: String, newName: String): Job {
val dao = appDatabase.customAttrsDao()
scope.launch {
return scope.launch {
dao.renameTag(oldName, newName)
}
}
override fun deleteTag(tag: String): Job {
val dao = appDatabase.customAttrsDao()
return scope.launch {
dao.deleteTag(tag)
}
}
override fun search(query: String): Flow<ImmutableList<SavableSearchable>> {
if (query.isBlank()) {
return flow {

View File

@ -47,5 +47,7 @@ dependencies {
implementation(project(":core:base"))
implementation(project(":core:ktx"))
implementation(project(":core:crashreporter"))
implementation(project(":data:customattrs"))
implementation(project(":data:favorites"))
}

View File

@ -4,5 +4,5 @@ import de.mm20.launcher2.services.tags.impl.TagsServiceImpl
import org.koin.dsl.module
val servicesTagsModule = module {
single<TagsService> { TagsServiceImpl() }
single<TagsService> { TagsServiceImpl(get(), get()) }
}

View File

@ -1,8 +1,15 @@
package de.mm20.launcher2.services.tags
import de.mm20.launcher2.search.SavableSearchable
import kotlinx.coroutines.flow.Flow
interface TagsService {
fun getTags(startsWith: String? = null): List<String>
fun renameTag(oldName: String, newName: String)
fun getAllTags(startsWith: String? = null): Flow<List<String>>
fun deleteTag(tag: String)
fun cloneTag(tag: String, newTag: String)
fun getTaggedItems(tag: String): Flow<List<SavableSearchable>>
fun createTag(tag: String, items: List<SavableSearchable>)
fun updateTag(tag: String, newName: String? = null, items: List<SavableSearchable>? = null)
}

View File

@ -1,22 +1,67 @@
package de.mm20.launcher2.services.tags.impl
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.services.tags.TagsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
internal class TagsServiceImpl(
): TagsService {
override fun getTags(startsWith: String?): List<String> {
TODO("Not yet implemented")
}
override fun renameTag(oldName: String, newName: String) {
TODO("Not yet implemented")
private val customAttributesRepository: CustomAttributesRepository,
private val favoritesRepository: FavoritesRepository,
) : TagsService {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
override fun getAllTags(startsWith: String?): Flow<List<String>> {
return customAttributesRepository.getAllTags(startsWith)
}
override fun deleteTag(tag: String) {
TODO("Not yet implemented")
favoritesRepository.remove(Tag(tag))
customAttributesRepository.deleteTag(tag)
}
override fun cloneTag(tag: String, newTag: String) {
TODO("Not yet implemented")
scope.launch {
val items = getTaggedItems(tag).first()
createTag(newTag, items)
}
}
override fun getTaggedItems(tag: String): Flow<List<SavableSearchable>> {
return customAttributesRepository.getItemsForTag(tag)
}
override fun updateTag(tag: String, newName: String?, items: List<SavableSearchable>?) {
scope.launch {
if (items != null) {
customAttributesRepository.setItemsForTag(tag, items).join()
}
if (newName != null && newName != tag) {
customAttributesRepository.renameTag(tag, newName).join()
val pinnedTags = favoritesRepository.getFavorites(
includeTypes = listOf(Tag.Domain),
manuallySorted = true,
automaticallySorted = true
).first()
val oldTag = Tag(tag)
if (pinnedTags.any { it.key == oldTag.key }) {
favoritesRepository.unpinItem(oldTag)
favoritesRepository.pinItem(Tag(newName))
}
}
}
}
override fun createTag(tag: String, items: List<SavableSearchable>) {
scope.launch {
customAttributesRepository.setItemsForTag(tag, items)
}
}
}