11package com.w2sv.filenavigator.ui.screen.navigatorsettings.components
22
3+ import android.annotation.SuppressLint
4+ import androidx.compose.foundation.clickable
35import androidx.compose.foundation.gestures.detectTapGestures
46import androidx.compose.foundation.layout.Arrangement
57import androidx.compose.foundation.layout.Column
@@ -10,15 +12,22 @@ import androidx.compose.foundation.layout.height
1012import androidx.compose.foundation.layout.padding
1113import androidx.compose.foundation.layout.width
1214import androidx.compose.material.icons.Icons
13- import androidx.compose.material.icons.filled.Check
15+ import androidx.compose.material.icons.filled.Add
16+ import androidx.compose.material.icons.filled.Delete
1417import androidx.compose.material.icons.outlined.Warning
1518import androidx.compose.material3.AlertDialog
1619import androidx.compose.material3.Badge
20+ import androidx.compose.material3.FilledTonalIconButton
1721import androidx.compose.material3.Icon
1822import androidx.compose.material3.IconButton
1923import androidx.compose.material3.MaterialTheme
2024import androidx.compose.material3.OutlinedTextField
25+ import androidx.compose.material3.PlainTooltip
2126import androidx.compose.material3.Text
27+ import androidx.compose.material3.TooltipBox
28+ import androidx.compose.material3.TooltipDefaults
29+ import androidx.compose.material3.TooltipScope
30+ import androidx.compose.material3.TooltipState
2231import androidx.compose.runtime.Composable
2332import androidx.compose.runtime.Stable
2433import androidx.compose.runtime.derivedStateOf
@@ -40,6 +49,7 @@ import com.w2sv.kotlinutils.coroutines.flow.emit
4049import kotlinx.coroutines.CoroutineScope
4150import kotlinx.coroutines.flow.MutableSharedFlow
4251import kotlinx.coroutines.flow.asSharedFlow
52+ import kotlinx.coroutines.launch
4353
4454private enum class FileExtensionInvalidityReason (val errorMessage : String ) {
4555 ContainsSpecialCharacter (" Extension must not contain special characters" ),
@@ -55,7 +65,11 @@ private class CustomFileType(private val scope: CoroutineScope) {
5565 name = value.trim().replaceFirstChar(Char ::titlecase)
5666 }
5767
58- val fileExtensions = mutableStateListOf<String >()
68+ val extensions = mutableStateListOf<String >()
69+
70+ fun deleteExtension (index : Int ) {
71+ extensions.removeAt(index)
72+ }
5973
6074 var newFileExtension by mutableStateOf(" " )
6175 private set
@@ -67,18 +81,18 @@ private class CustomFileType(private val scope: CoroutineScope) {
6781 val newFileExtensionInvalidityReason by derivedStateOf {
6882 when {
6983 newFileExtension.any { ! it.isLetterOrDigit() } -> FileExtensionInvalidityReason .ContainsSpecialCharacter
70- fileExtensions .contains(newFileExtension) -> FileExtensionInvalidityReason .AlreadyAmongstFileExtensions
84+ extensions .contains(newFileExtension) -> FileExtensionInvalidityReason .AlreadyAmongstFileExtensions
7185 else -> null
7286 }
7387 }
7488 val newFileExtensionCanBeAdded by derivedStateOf { newFileExtensionInvalidityReason == null && newFileExtension.isNotBlank() }
7589
7690 fun addNewFileExtension () {
77- fileExtensions .add(newFileExtension)
91+ extensions .add(newFileExtension)
7892 newFileExtension = " "
7993 }
8094
81- val canBeCreated by derivedStateOf { name.isNotEmpty() && fileExtensions .isNotEmpty() }
95+ val canBeCreated by derivedStateOf { name.isNotEmpty() && extensions .isNotEmpty() }
8296
8397 val clearFocus get() = _clearFocus .asSharedFlow()
8498 private val _clearFocus = MutableSharedFlow <Unit >()
@@ -114,35 +128,15 @@ private fun StatelessFileTypeCreationDialog(customFileType: CustomFileType, onDi
114128 placeholder = { Text (" Enter name" ) },
115129 singleLine = true
116130 )
117- // Text(
118- // text = "File Extensions",
119- // modifier = Modifier.padding(top = 12.dp, bottom = 8.dp),
120- // style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium)
121- // )
122- // LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 32.dp), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
123- // items(customFileType.fileExtensions) {
124- // if (it.isNotEmpty()) {
125- // Badge {
126- // Text(it)
127- // }
128- // }
129- // }
130- // }
131131 FileExtensionTextField (
132132 customFileType = customFileType,
133133 modifier = Modifier
134134 .width(192 .dp)
135135 .padding(vertical = 16 .dp)
136136 )
137137 FlowRow (horizontalArrangement = Arrangement .spacedBy(8 .dp), verticalArrangement = Arrangement .spacedBy(12 .dp)) {
138- customFileType.fileExtensions.forEachIndexed { i, el ->
139- Badge (containerColor = MaterialTheme .colorScheme.secondaryContainer) {
140- Text (
141- text = el,
142- modifier = Modifier .padding(horizontal = 10 .dp, vertical = 6 .dp),
143- style = MaterialTheme .typography.labelLarge
144- )
145- }
138+ customFileType.extensions.forEachIndexed { i, extension ->
139+ FileExtensionBadgeWithTooltip (extension = extension, deleteExtension = { customFileType.deleteExtension(i) })
146140 }
147141 }
148142 }
@@ -151,6 +145,59 @@ private fun StatelessFileTypeCreationDialog(customFileType: CustomFileType, onDi
151145 )
152146}
153147
148+ @Composable
149+ private fun FileExtensionBadgeWithTooltip (
150+ extension : String ,
151+ deleteExtension : () -> Unit ,
152+ modifier : Modifier = Modifier ,
153+ scope : CoroutineScope = rememberCoroutineScope()
154+ ) {
155+ val tooltipState = remember { TooltipState () }
156+
157+ TooltipBox (
158+ positionProvider = TooltipDefaults .rememberPlainTooltipPositionProvider(),
159+ tooltip = {
160+ FileExtensionDeletionTooltip (
161+ onClick = {
162+ deleteExtension()
163+ tooltipState.dismiss()
164+ }
165+ )
166+ },
167+ state = tooltipState,
168+ modifier = modifier
169+ ) {
170+ FileExtensionBadge (extension, modifier = Modifier .clickable { scope.launch { tooltipState.show() } })
171+ }
172+ }
173+
174+ @SuppressLint(" ComposeUnstableReceiver" )
175+ @Composable
176+ private fun TooltipScope.FileExtensionDeletionTooltip (onClick : () -> Unit , modifier : Modifier = Modifier ) {
177+ PlainTooltip (caretSize = TooltipDefaults .caretSize, tonalElevation = 4 .dp, shadowElevation = 4 .dp, modifier = modifier) {
178+ IconButton (onClick = onClick) {
179+ Icon (
180+ imageVector = Icons .Default .Delete ,
181+ contentDescription = " Delete file extension" ,
182+ )
183+ }
184+ }
185+ }
186+
187+ @Composable
188+ private fun FileExtensionBadge (extension : String , modifier : Modifier = Modifier ) {
189+ Badge (
190+ containerColor = MaterialTheme .colorScheme.secondaryContainer,
191+ modifier = modifier
192+ ) {
193+ Text (
194+ text = extension,
195+ modifier = Modifier .padding(horizontal = 10 .dp, vertical = 6 .dp),
196+ style = MaterialTheme .typography.bodyLarge
197+ )
198+ }
199+ }
200+
154201@Composable
155202private fun FileExtensionTextField (customFileType : CustomFileType , modifier : Modifier = Modifier ) {
156203 OutlinedTextField (
@@ -163,8 +210,8 @@ private fun FileExtensionTextField(customFileType: CustomFileType, modifier: Mod
163210 trailingIcon = when {
164211 customFileType.newFileExtensionCanBeAdded -> {
165212 {
166- IconButton (onClick = customFileType::addNewFileExtension) {
167- Icon (Icons .Default .Check , contentDescription = null , tint = AppColor .success)
213+ FilledTonalIconButton (onClick = customFileType::addNewFileExtension) {
214+ Icon (Icons .Default .Add , contentDescription = null , tint = AppColor .success)
168215 }
169216 }
170217 }
@@ -196,8 +243,12 @@ private fun FileExtensionTextField(customFileType: CustomFileType, modifier: Mod
196243private fun StatelessFileTypeCreationDialogPrev () {
197244 AppTheme {
198245 StatelessFileTypeCreationDialog (
199- CustomFileType (rememberCoroutineScope()).apply { fileExtensions.addAll(listOf (" jpg" , " png" , " jpg" , " jpg" , " jpgasdf" )) },
200- {}
246+ customFileType = CustomFileType (rememberCoroutineScope())
247+ .apply {
248+ extensions.addAll(listOf (" jpg" , " png" , " jpgasdf" ))
249+ updateNewFileExtension(" dot" )
250+ },
251+ onDismissRequest = {}
201252 )
202253 }
203254}
0 commit comments