Tags editor
This commit is contained in:
parent
cc4df325ad
commit
5073cb0297
@ -1,6 +1,5 @@
|
|||||||
package de.mm20.launcher2.customattrs
|
package de.mm20.launcher2.customattrs
|
||||||
|
|
||||||
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.CustomAttributeEntity
|
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.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -23,6 +23,9 @@ interface CustomAttributesRepository {
|
|||||||
fun setCustomLabel(searchable: Searchable, label: String)
|
fun setCustomLabel(searchable: Searchable, label: String)
|
||||||
fun clearCustomLabel(searchable: Searchable)
|
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 search(query: String): Flow<List<Searchable>>
|
||||||
|
|
||||||
suspend fun export(toDir: File)
|
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>> {
|
override suspend fun search(query: String): Flow<List<Searchable>> {
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
return flow {
|
return flow {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package de.mm20.launcher2.database
|
|||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
import de.mm20.launcher2.database.entities.CustomAttributeEntity
|
import de.mm20.launcher2.database.entities.CustomAttributeEntity
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@ -17,9 +18,18 @@ interface CustomAttrsDao {
|
|||||||
@Insert
|
@Insert
|
||||||
fun setCustomAttribute(entity: CustomAttributeEntity)
|
fun setCustomAttribute(entity: CustomAttributeEntity)
|
||||||
|
|
||||||
|
@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>>
|
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>>
|
fun search(query: String): Flow<List<String>>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun setTags(key: String, tags: List<CustomAttributeEntity>) {
|
||||||
|
clearCustomAttribute(key, "tag")
|
||||||
|
insertCustomAttributes(tags)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -654,4 +654,6 @@
|
|||||||
<string name="create_app_shortcut">Create shortcut</string>
|
<string name="create_app_shortcut">Create shortcut</string>
|
||||||
<string name="frequently_used_show_in_favorites">Show in favorites</string>
|
<string name="frequently_used_show_in_favorites">Show in favorites</string>
|
||||||
<string name="frequently_used_rows">Number of rows</string>
|
<string name="frequently_used_rows">Number of rows</string>
|
||||||
|
|
||||||
|
<string name="customize_tags_placeholder">Tags…</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,10 +1,7 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.search.common.customattrs
|
package de.mm20.launcher2.ui.launcher.search.common.customattrs
|
||||||
|
|
||||||
import android.graphics.drawable.InsetDrawable
|
import android.graphics.drawable.InsetDrawable
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
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.compose.ui.text.style.TextAlign
|
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.R
|
||||||
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
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.ktx.toPixels
|
||||||
import de.mm20.launcher2.ui.locals.LocalGridColumns
|
import de.mm20.launcher2.ui.locals.LocalGridColumns
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@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) {
|
DisposableEffect(searchable.key) {
|
||||||
onDispose {
|
onDispose {
|
||||||
viewModel.setCustomLabel(customLabelValue)
|
viewModel.setCustomLabel(customLabelValue)
|
||||||
|
viewModel.setTags(tags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,4 +76,12 @@ class CustomizeSearchableSheetVM(
|
|||||||
customAttributesRepository.setCustomLabel(searchable, label)
|
customAttributesRepository.setCustomLabel(searchable, label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setTags(tags: List<String>) {
|
||||||
|
customAttributesRepository.setTags(searchable, tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTags(): Flow<List<String>> {
|
||||||
|
return customAttributesRepository.getTags(searchable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user