diff --git a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt index ed823883..acd9df21 100644 --- a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt +++ b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt @@ -1,6 +1,5 @@ package de.mm20.launcher2.customattrs -import android.util.Log import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.entities.CustomAttributeEntity @@ -11,6 +10,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import org.json.JSONArray import org.json.JSONException import java.io.File @@ -23,6 +23,9 @@ interface CustomAttributesRepository { fun setCustomLabel(searchable: Searchable, label: String) fun clearCustomLabel(searchable: Searchable) + fun setTags(searchable: Searchable, tags: List) + fun getTags(searchable: Searchable): Flow> + suspend fun search(query: String): Flow> suspend fun export(toDir: File) @@ -84,6 +87,22 @@ internal class CustomAttributesRepositoryImpl( } } + override fun setTags(searchable: Searchable, tags: List) { + val dao = appDatabase.customAttrsDao() + scope.launch { + dao.setTags(searchable.key, tags.map { + CustomTag(it).toDatabaseEntity(searchable.key) + }) + } + } + + override fun getTags(searchable: Searchable): Flow> { + val dao = appDatabase.customAttrsDao() + return dao.getCustomAttributes(listOf(searchable.key), CustomAttributeType.Tag.value).map { + it.map { it.value } + } + } + override suspend fun search(query: String): Flow> { if (query.isBlank()) { return flow { diff --git a/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt b/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt index 59d4497e..5ee3f8a3 100644 --- a/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt +++ b/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt @@ -3,6 +3,7 @@ package de.mm20.launcher2.database import androidx.room.Dao import androidx.room.Insert import androidx.room.Query +import androidx.room.Transaction import de.mm20.launcher2.database.entities.CustomAttributeEntity import kotlinx.coroutines.flow.Flow @@ -17,9 +18,18 @@ interface CustomAttrsDao { @Insert fun setCustomAttribute(entity: CustomAttributeEntity) + @Insert + suspend fun insertCustomAttributes(entities: List) + @Query("SELECT * FROM CustomAttributes WHERE type = :type AND key IN (:keys)") fun getCustomAttributes(keys: List, type: String) : Flow> @Query("SELECT DISTINCT key FROM CustomAttributes WHERE (type = 'label' OR type = 'tag') AND value LIKE :query") fun search(query: String): Flow> + + @Transaction + suspend fun setTags(key: String, tags: List) { + clearCustomAttribute(key, "tag") + insertCustomAttributes(tags) + } } \ No newline at end of file diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 9e8097a4..5f54d63a 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -654,4 +654,6 @@ Create shortcut Show in favorites Number of rows + + Tags… \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/component/OutlinedTagsInputField.kt b/ui/src/main/java/de/mm20/launcher2/ui/component/OutlinedTagsInputField.kt new file mode 100644 index 00000000..d3c25d9d --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/component/OutlinedTagsInputField.kt @@ -0,0 +1,186 @@ +package de.mm20.launcher2.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Clear +import androidx.compose.material.icons.rounded.Tag +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.launch + +@Composable +fun OutlinedTagsInputField( + modifier: Modifier = Modifier, + tags: List, + onTagsChange: (tags: List) -> Unit, + placeholder: @Composable (() -> Unit)? = null, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + textStyle: TextStyle = MaterialTheme.typography.bodyLarge, + textColor: Color = LocalContentColor.current, +) { + var value by remember { mutableStateOf("") } + var lastTagFocused by remember { mutableStateOf(false) } + + val scrollState = rememberScrollState() + val scope = rememberCoroutineScope() + + BasicTextField( + modifier = modifier + .onKeyEvent { + if (it.key == Key.Backspace && value.isEmpty() && tags.isNotEmpty()) { + if (!lastTagFocused) { + lastTagFocused = true + } else { + onTagsChange(tags.dropLast(1)) + lastTagFocused = false + } + return@onKeyEvent true + } + lastTagFocused = false + false + } + .onFocusChanged { + if (!it.hasFocus && value.isNotBlank()) { + onTagsChange((tags + value).toImmutableList()) + value = "" + } else if (it.hasFocus) { + scope.launch { + scrollState.animateScrollTo(scrollState.maxValue) + } + } + }, + value = value, onValueChange = { + val newTags = it.split(",") + if (newTags.size > 1) { + onTagsChange(tags + newTags.dropLast(1).filter { it.isNotBlank() }) + } + value = newTags.last() + lastTagFocused = false + }, + textStyle = textStyle.copy( + color = textColor + ), + interactionSource = interactionSource, + singleLine = true, + keyboardActions = KeyboardActions(onDone = { + if (value.isNotBlank()) { + onTagsChange(tags + value) + value = "" + } + }), + decorationBox = { innerTextField -> + TextFieldDefaults.OutlinedTextFieldDecorationBox( + contentPadding = PaddingValues(0.dp), + value = value, + innerTextField = { Box { + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()).padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + for ((i, tag) in tags.withIndex()) { + InputChip( + selected = i == tags.lastIndex && lastTagFocused, + modifier = Modifier.padding(end = 12.dp), + onClick = { }, + leadingIcon = { + Icon(imageVector = Icons.Rounded.Tag, contentDescription = null) + }, + label = { Text(tag) }, + trailingIcon = { + Icon( + modifier = Modifier.clickable { + onTagsChange(tags.filter { it != tag }) + }, imageVector = Icons.Rounded.Clear, contentDescription = null + ) + }, + ) + } + Box( + modifier = Modifier + .height(56.dp), + contentAlignment = Alignment.CenterStart + ) { + if (value.isEmpty()) { + CompositionLocalProvider( + LocalTextStyle provides textStyle, + LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant, + ) { + placeholder?.invoke() + } + } + innerTextField() + } + } + + } }, + enabled = true, + singleLine = true, + visualTransformation = VisualTransformation.None, + interactionSource = interactionSource, + ) + /*Box { + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + verticalAlignment = Alignment.CenterVertically + ) { + for ((i, tag) in tags.withIndex()) { + InputChip( + selected = i == tags.lastIndex && lastTagFocused, + modifier = Modifier.padding(end = 12.dp), + onClick = { }, + leadingIcon = { + Icon(imageVector = Icons.Rounded.Tag, contentDescription = null) + }, + label = { Text(tag) }, + trailingIcon = { + Icon( + modifier = Modifier.clickable { + onTagsChange(tags.filter { it != tag }) + }, imageVector = Icons.Rounded.Clear, contentDescription = null + ) + }, + ) + } + Box( + modifier = Modifier + .padding(vertical = 12.dp) + .padding( + start = if (tags.isEmpty()) 16.dp else 0.dp, + end = 16.dp + ) + ) { + if (value.isEmpty()) { + CompositionLocalProvider( + LocalTextStyle provides textStyle, + LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant, + ) { + placeholder?.invoke() + } + } + innerTextField() + } + } + + }*/ + }, + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary) + ) +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt index ce5f2532..349e00cd 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt @@ -1,10 +1,7 @@ package de.mm20.launcher2.ui.launcher.search.common.customattrs import android.graphics.drawable.InsetDrawable -import android.widget.Toast -import androidx.activity.compose.BackHandler import androidx.appcompat.content.res.AppCompatResources -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan @@ -19,7 +16,6 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -31,8 +27,10 @@ import de.mm20.launcher2.search.data.Searchable 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.OutlinedTagsInputField import de.mm20.launcher2.ui.ktx.toPixels import de.mm20.launcher2.ui.locals.LocalGridColumns +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @Composable @@ -116,9 +114,27 @@ fun CustomizeSearchableSheet( }, ) + var tags by remember { mutableStateOf(emptyList()) } + + LaunchedEffect(searchable.key) { + tags = viewModel.getTags().first() + } + + OutlinedTagsInputField( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + tags = tags, onTagsChange = { tags = it }, + placeholder = { + Text(stringResource(R.string.customize_tags_placeholder)) + }, + textStyle = MaterialTheme.typography.bodyMedium + ) + DisposableEffect(searchable.key) { onDispose { viewModel.setCustomLabel(customLabelValue) + viewModel.setTags(tags) } } } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt index 2a5d966e..cccb7029 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt @@ -76,4 +76,12 @@ class CustomizeSearchableSheetVM( customAttributesRepository.setCustomLabel(searchable, label) } } + + fun setTags(tags: List) { + customAttributesRepository.setTags(searchable, tags) + } + + fun getTags(): Flow> { + return customAttributesRepository.getTags(searchable) + } } \ No newline at end of file