EditThemeSection.kt (17082B) - raw


      1 package me.rhunk.snapenhance.ui.manager.pages.theming
      2 
      3 import androidx.compose.foundation.ExperimentalFoundationApi
      4 import androidx.compose.foundation.layout.*
      5 import androidx.compose.foundation.lazy.LazyColumn
      6 import androidx.compose.foundation.lazy.items
      7 import androidx.compose.foundation.lazy.rememberLazyListState
      8 import androidx.compose.material.icons.Icons
      9 import androidx.compose.material.icons.filled.*
     10 import androidx.compose.material3.*
     11 import androidx.compose.runtime.*
     12 import androidx.compose.ui.Alignment
     13 import androidx.compose.ui.Modifier
     14 import androidx.compose.ui.focus.FocusRequester
     15 import androidx.compose.ui.focus.focusRequester
     16 import androidx.compose.ui.graphics.Color
     17 import androidx.compose.ui.graphics.toArgb
     18 import androidx.compose.ui.text.font.FontWeight
     19 import androidx.compose.ui.text.style.TextAlign
     20 import androidx.compose.ui.text.style.TextOverflow
     21 import androidx.compose.ui.unit.dp
     22 import androidx.compose.ui.unit.sp
     23 import androidx.navigation.NavBackStackEntry
     24 import kotlinx.coroutines.Dispatchers
     25 import kotlinx.coroutines.delay
     26 import kotlinx.coroutines.launch
     27 import kotlinx.coroutines.withContext
     28 import me.rhunk.snapenhance.common.data.*
     29 import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState
     30 import me.rhunk.snapenhance.common.ui.rememberAsyncMutableStateList
     31 import me.rhunk.snapenhance.common.ui.transparentTextFieldColors
     32 import me.rhunk.snapenhance.storage.*
     33 import me.rhunk.snapenhance.ui.manager.Routes
     34 import me.rhunk.snapenhance.ui.util.AlertDialogs
     35 import me.rhunk.snapenhance.ui.util.CircularAlphaTile
     36 import me.rhunk.snapenhance.ui.util.Dialog
     37 
     38 class EditThemeSection: Routes.Route() {
     39     private var saveCallback by mutableStateOf<(() -> Unit)?>(null)
     40     private var addEntryCallback by mutableStateOf<(key: String, initialColor: Int) -> Unit>({ _, _ -> })
     41     private var deleteCallback by mutableStateOf<(() -> Unit)?>(null)
     42     private var themeColors = mutableStateListOf<ThemeColorEntry>()
     43 
     44     private val alertDialogs by lazy {
     45         AlertDialogs(context.translation)
     46     }
     47 
     48     override val topBarActions: @Composable (RowScope.() -> Unit) = {
     49         var deleteConfirmationDialog by remember { mutableStateOf(false) }
     50 
     51         if (deleteConfirmationDialog) {
     52             Dialog(onDismissRequest = {
     53                 deleteConfirmationDialog = false
     54             }) {
     55                 alertDialogs.ConfirmDialog(
     56                     title = "Delete Theme",
     57                     message = "Are you sure you want to delete this theme?",
     58                     onConfirm = {
     59                         deleteCallback?.invoke()
     60                         deleteConfirmationDialog = false
     61                     },
     62                     onDismiss = {
     63                         deleteConfirmationDialog = false
     64                     }
     65                 )
     66             }
     67         }
     68 
     69         deleteCallback?.let {
     70             IconButton(onClick = {
     71                 deleteConfirmationDialog = true
     72             }) {
     73                 Icon(Icons.Default.Delete, contentDescription = null)
     74             }
     75         }
     76     }
     77 
     78     @OptIn(ExperimentalFoundationApi::class)
     79     override val floatingActionButton: @Composable () -> Unit = {
     80         Column(
     81             horizontalAlignment = Alignment.End,
     82             verticalArrangement = Arrangement.spacedBy(5.dp),
     83         ) {
     84             var addAttributeDialog by remember { mutableStateOf(false) }
     85             val attributesTranslation = remember { context.translation.getCategory("theming_attributes") }
     86 
     87             if (addAttributeDialog) {
     88                 AlertDialog(
     89                     title = { Text("Select an attribute to add") },
     90                     onDismissRequest = {
     91                         addAttributeDialog = false
     92                     },
     93                     confirmButton = {},
     94                     text = {
     95                         var filter by remember { mutableStateOf("") }
     96                         val attributes = rememberAsyncMutableStateList(defaultValue = listOf(), keys = arrayOf(filter)) {
     97                             AvailableThemingAttributes[ThemingAttributeType.COLOR]?.filter { key ->
     98                                 themeColors.none { it.key == key } && (key.contains(filter, ignoreCase = true) || attributesTranslation.getOrNull(key)?.contains(filter, ignoreCase = true) == true)
     99                             } ?: emptyList()
    100                         }
    101 
    102                         LazyColumn(
    103                             modifier = Modifier
    104                                 .fillMaxHeight(0.7f)
    105                                 .fillMaxWidth(),
    106                         ) {
    107                             stickyHeader {
    108                                 TextField(
    109                                     modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp),
    110                                     value = filter,
    111                                     onValueChange = { filter = it },
    112                                     label = { Text("Search") },
    113                                     colors = transparentTextFieldColors().copy(
    114                                         focusedContainerColor = MaterialTheme.colorScheme.surfaceBright,
    115                                         unfocusedContainerColor = MaterialTheme.colorScheme.surfaceBright
    116                                     )
    117                                 )
    118                             }
    119                             item {
    120                                 if (attributes.isEmpty()) {
    121                                     Text("No attributes")
    122                                 }
    123                             }
    124                             items(attributes) { attribute ->
    125                                 Card(
    126                                     modifier = Modifier.padding(5.dp).fillMaxWidth(),
    127                                     onClick = {
    128                                         addEntryCallback(attribute, Color.White.toArgb())
    129                                         addAttributeDialog = false
    130                                     }
    131                                 ) {
    132                                     val attributeTranslation = remember(attribute) {
    133                                         attributesTranslation.getOrNull(attribute)
    134                                     }
    135 
    136                                     Column(
    137                                         modifier = Modifier.padding(8.dp)
    138                                     ) {
    139                                         Text(attributeTranslation ?: attribute, lineHeight = 15.sp)
    140                                         attributeTranslation?.let {
    141                                             Text(attribute, fontWeight = FontWeight.Light, fontSize = 10.sp, lineHeight = 15.sp)
    142                                         }
    143                                     }
    144                                 }
    145                             }
    146                         }
    147                     }
    148                 )
    149             }
    150 
    151             FloatingActionButton(onClick = {
    152                 addAttributeDialog = true
    153             }) {
    154                 Icon(Icons.Default.Add, contentDescription = null)
    155             }
    156 
    157             saveCallback?.let {
    158                 FloatingActionButton(onClick = {
    159                     it()
    160                 }) {
    161                     Icon(Icons.Default.Save, contentDescription = null)
    162                 }
    163             }
    164         }
    165     }
    166 
    167     override val content: @Composable (NavBackStackEntry) -> Unit = {
    168         val coroutineScope = rememberCoroutineScope()
    169         val currentThemeId = remember { it.arguments?.getString("theme_id")?.toIntOrNull() }
    170 
    171         LaunchedEffect(Unit) {
    172             themeColors.clear()
    173         }
    174 
    175         var themeName by remember { mutableStateOf("") }
    176         var themeDescription by remember { mutableStateOf("") }
    177         var themeVersion by remember { mutableStateOf("1.0.0") }
    178         var themeAuthor by remember { mutableStateOf("") }
    179         var themeUpdateUrl by remember { mutableStateOf("") }
    180 
    181         val themeInfo by rememberAsyncMutableState(defaultValue = null) {
    182             currentThemeId?.let { themeId ->
    183                 context.database.getThemeInfo(themeId)?.also { theme ->
    184                     themeName = theme.name
    185                     themeDescription = theme.description ?: ""
    186                     theme.version?.let { themeVersion = it }
    187                     themeAuthor = theme.author ?: ""
    188                     themeUpdateUrl = theme.updateUrl ?: ""
    189                 }
    190             }
    191         }
    192 
    193         val lazyListState = rememberLazyListState()
    194 
    195         rememberAsyncMutableState(defaultValue = DatabaseThemeContent(), keys = arrayOf(themeInfo)) {
    196             currentThemeId?.let {
    197                 context.database.getThemeContent(it)?.also { content ->
    198                     themeColors.clear()
    199                     themeColors.addAll(content.colors)
    200                     withContext(Dispatchers.Main) {
    201                         lazyListState.scrollToItem(themeColors.size)
    202                     }
    203                 }
    204             } ?: DatabaseThemeContent()
    205         }
    206 
    207         if (themeName.isNotBlank()) {
    208             saveCallback = {
    209                 coroutineScope.launch(Dispatchers.IO) {
    210                     val theme = DatabaseTheme(
    211                         id = currentThemeId ?: -1,
    212                         enabled = themeInfo?.enabled ?: false,
    213                         name = themeName,
    214                         description = themeDescription,
    215                         version = themeVersion,
    216                         author = themeAuthor,
    217                         updateUrl = themeUpdateUrl
    218                     )
    219                     val themeId = context.database.addOrUpdateTheme(theme, currentThemeId)
    220                     context.database.setThemeContent(themeId, DatabaseThemeContent(
    221                         colors = themeColors
    222                     ))
    223                     withContext(Dispatchers.Main) {
    224                         routes.theming.navigateReload()
    225                     }
    226                 }
    227             }
    228         } else {
    229             saveCallback = null
    230         }
    231 
    232         LaunchedEffect(Unit) {
    233             deleteCallback = null
    234             if (currentThemeId != null) {
    235                 deleteCallback = {
    236                     coroutineScope.launch(Dispatchers.IO) {
    237                         context.database.deleteTheme(currentThemeId)
    238                         withContext(Dispatchers.Main) {
    239                             routes.theming.navigateReload()
    240                         }
    241                     }
    242                 }
    243             }
    244             addEntryCallback = { key, initialColor ->
    245                 coroutineScope.launch(Dispatchers.Main) {
    246                     themeColors.add(ThemeColorEntry(key, initialColor))
    247                     delay(100)
    248                     lazyListState.scrollToItem(themeColors.size)
    249                 }
    250             }
    251         }
    252 
    253         var moreOptionsExpanded by remember { mutableStateOf(false) }
    254 
    255         Column(
    256             modifier = Modifier.fillMaxSize(),
    257             verticalArrangement = Arrangement.spacedBy(8.dp)
    258         ) {
    259             Row(
    260                 verticalAlignment = Alignment.CenterVertically,
    261             ) {
    262                 val focusRequester = remember { FocusRequester() }
    263 
    264                 TextField(
    265                     modifier = Modifier.weight(1f).focusRequester(focusRequester),
    266                     value = themeName,
    267                     onValueChange = { themeName = it },
    268                     label = { Text("Theme Name") },
    269                     colors = transparentTextFieldColors(),
    270                     singleLine = true,
    271                 )
    272                 LaunchedEffect(Unit) {
    273                     if (currentThemeId == null) {
    274                         delay(200)
    275                         focusRequester.requestFocus()
    276                     }
    277                 }
    278                 IconButton(
    279                     modifier = Modifier.padding(4.dp),
    280                     onClick = {
    281                         moreOptionsExpanded = !moreOptionsExpanded
    282                     }
    283                 ) {
    284                     Icon(if (moreOptionsExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, contentDescription = null)
    285                 }
    286             }
    287 
    288             if (moreOptionsExpanded) {
    289                 TextField(
    290                     modifier = Modifier.fillMaxWidth(),
    291                     maxLines = 3,
    292                     value = themeDescription,
    293                     onValueChange = { themeDescription = it },
    294                     label = { Text("Description") },
    295                     colors = transparentTextFieldColors()
    296                 )
    297                 TextField(
    298                     modifier = Modifier.fillMaxWidth(),
    299                     singleLine = true,
    300                     value = themeVersion,
    301                     onValueChange = { themeVersion = it },
    302                     label = { Text("Version") },
    303                     colors = transparentTextFieldColors()
    304                 )
    305                 TextField(
    306                     modifier = Modifier.fillMaxWidth(),
    307                     singleLine = true,
    308                     value = themeAuthor,
    309                     onValueChange = { themeAuthor = it },
    310                     label = { Text("Author") },
    311                     colors = transparentTextFieldColors()
    312                 )
    313                 TextField(
    314                     modifier = Modifier.fillMaxWidth(),
    315                     singleLine = true,
    316                     value = themeUpdateUrl,
    317                     onValueChange = { themeUpdateUrl = it },
    318                     label = { Text("Update URL") },
    319                     colors = transparentTextFieldColors()
    320                 )
    321             }
    322 
    323             LazyColumn(
    324                 modifier = Modifier.fillMaxWidth(),
    325                 state = lazyListState,
    326                 contentPadding = PaddingValues(10.dp),
    327                 verticalArrangement = Arrangement.spacedBy(4.dp),
    328                 reverseLayout = true,
    329             ) {
    330                 item {
    331                     Spacer(modifier = Modifier.height(150.dp))
    332                 }
    333                 items(themeColors) { colorEntry ->
    334                     var showEditColorDialog by remember { mutableStateOf(false) }
    335                     var currentColor by remember { mutableIntStateOf(colorEntry.value) }
    336 
    337                     ElevatedCard(
    338                         modifier = Modifier
    339                             .fillMaxWidth(),
    340                         onClick = {
    341                             showEditColorDialog = true
    342                         }
    343                     ) {
    344                         Row(
    345                             modifier = Modifier
    346                                 .padding(4.dp)
    347                                 .fillMaxWidth(),
    348                             verticalAlignment = Alignment.CenterVertically
    349                         ) {
    350                             Icon(Icons.Default.Colorize, contentDescription = null, modifier = Modifier.padding(8.dp))
    351                             Column(
    352                                 modifier = Modifier.weight(1f)
    353                             ) {
    354                                 val translation = remember(colorEntry.key) { context.translation.getOrNull("theming_attributes.${colorEntry.key}") }
    355                                 Text(text = translation ?: colorEntry.key, overflow = TextOverflow.Ellipsis, maxLines = 1, lineHeight = 15.sp)
    356                                 translation?.let {
    357                                     Text(text = colorEntry.key, fontSize = 10.sp, fontWeight = FontWeight.Light, overflow = TextOverflow.Ellipsis, maxLines = 1, lineHeight = 15.sp)
    358                                 }
    359                             }
    360                             CircularAlphaTile(selectedColor = Color(currentColor))
    361                         }
    362                     }
    363 
    364                     if (showEditColorDialog) {
    365                         Dialog(onDismissRequest = { showEditColorDialog = false }) {
    366                             alertDialogs.ColorPickerDialog(
    367                                 initialColor = Color(currentColor),
    368                                 setProperty = {
    369                                     if (it == null) {
    370                                         themeColors.remove(colorEntry)
    371                                         return@ColorPickerDialog
    372                                     }
    373                                     currentColor = it.toArgb()
    374                                     colorEntry.value = currentColor
    375                                 },
    376                                 dismiss = {
    377                                     showEditColorDialog = false
    378                                 }
    379                             )
    380                         }
    381                     }
    382                 }
    383                 item {
    384                     if (themeColors.isEmpty()) {
    385                         Text("No colors added yet", modifier = Modifier
    386                             .fillMaxWidth()
    387                             .padding(8.dp), fontWeight = FontWeight.Light, textAlign = TextAlign.Center)
    388                     }
    389                 }
    390             }
    391         }
    392     }
    393 }