Add settings page to manage tags
This commit is contained in:
parent
85f7a740e0
commit
ddc157741a
@ -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"))
|
||||
|
||||
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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) }
|
||||
|
||||
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@ -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'")
|
||||
|
||||
@ -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>
|
||||
@ -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 {
|
||||
|
||||
@ -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"))
|
||||
|
||||
}
|
||||
@ -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()) }
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user