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 }