Tags editor

This commit is contained in:
MM20 2022-09-21 21:41:30 +02:00
parent cc4df325ad
commit 5073cb0297
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
6 changed files with 246 additions and 5 deletions

View File

@ -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<String>)
fun getTags(searchable: Searchable): Flow<List<String>>
suspend fun search(query: String): Flow<List<Searchable>>
suspend fun export(toDir: File)
@ -84,6 +87,22 @@ internal class CustomAttributesRepositoryImpl(
}
}
override fun setTags(searchable: Searchable, tags: List<String>) {
val dao = appDatabase.customAttrsDao()
scope.launch {
dao.setTags(searchable.key, tags.map {
CustomTag(it).toDatabaseEntity(searchable.key)
})
}
}
override fun getTags(searchable: Searchable): Flow<List<String>> {
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<List<Searchable>> {
if (query.isBlank()) {
return flow {

View File

@ -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<CustomAttributeEntity>)
@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")
fun search(query: String): Flow<List<String>>
@Transaction
suspend fun setTags(key: String, tags: List<CustomAttributeEntity>) {
clearCustomAttribute(key, "tag")
insertCustomAttributes(tags)
}
}

View File

@ -654,4 +654,6 @@
<string name="create_app_shortcut">Create shortcut</string>
<string name="frequently_used_show_in_favorites">Show in favorites</string>
<string name="frequently_used_rows">Number of rows</string>
<string name="customize_tags_placeholder">Tags…</string>
</resources>

View File

@ -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<String>,
onTagsChange: (tags: List<String>) -> 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)
)
}

View File

@ -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<String>()) }
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)
}
}
}

View File

@ -76,4 +76,12 @@ class CustomizeSearchableSheetVM(
customAttributesRepository.setCustomLabel(searchable, label)
}
}
fun setTags(tags: List<String>) {
customAttributesRepository.setTags(searchable, tags)
}
fun getTags(): Flow<List<String>> {
return customAttributesRepository.getTags(searchable)
}
}