HomeRootSection.kt (16797B) - raw
1 package me.rhunk.snapenhance.ui.manager.pages.home 2 3 import androidx.compose.foundation.clickable 4 import androidx.compose.foundation.layout.* 5 import androidx.compose.foundation.rememberScrollState 6 import androidx.compose.foundation.shape.RoundedCornerShape 7 import androidx.compose.foundation.verticalScroll 8 import androidx.compose.material.icons.Icons 9 import androidx.compose.material.icons.automirrored.filled.Help 10 import androidx.compose.material.icons.filled.BugReport 11 import androidx.compose.material.icons.filled.MoreVert 12 import androidx.compose.material.icons.filled.Settings 13 import androidx.compose.material3.* 14 import androidx.compose.runtime.* 15 import androidx.compose.ui.Alignment 16 import androidx.compose.ui.Modifier 17 import androidx.compose.ui.draw.clip 18 import androidx.compose.ui.graphics.vector.ImageVector 19 import androidx.compose.ui.platform.LocalDensity 20 import androidx.compose.ui.res.vectorResource 21 import androidx.compose.ui.text.LinkAnnotation 22 import androidx.compose.ui.text.SpanStyle 23 import androidx.compose.ui.text.buildAnnotatedString 24 import androidx.compose.ui.text.font.Font 25 import androidx.compose.ui.text.font.FontFamily 26 import androidx.compose.ui.text.font.FontWeight 27 import androidx.compose.ui.text.style.TextAlign 28 import androidx.compose.ui.text.style.TextOverflow 29 import androidx.compose.ui.text.withLink 30 import androidx.compose.ui.text.withStyle 31 import androidx.compose.ui.unit.Dp 32 import androidx.compose.ui.unit.dp 33 import androidx.compose.ui.unit.sp 34 import androidx.navigation.NavBackStackEntry 35 import kotlinx.coroutines.launch 36 import me.rhunk.snapenhance.R 37 import me.rhunk.snapenhance.action.EnumQuickActions 38 import me.rhunk.snapenhance.common.BuildConfig 39 import me.rhunk.snapenhance.common.action.EnumAction 40 import me.rhunk.snapenhance.common.ui.TopBarActionButton 41 import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState 42 import me.rhunk.snapenhance.common.ui.rememberAsyncMutableStateList 43 import me.rhunk.snapenhance.common.util.ktx.openLink 44 import me.rhunk.snapenhance.core.ui.Snapenhance 45 import me.rhunk.snapenhance.storage.getQuickTiles 46 import me.rhunk.snapenhance.storage.setQuickTiles 47 import me.rhunk.snapenhance.ui.manager.Routes 48 import me.rhunk.snapenhance.ui.manager.data.Updater 49 import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper 50 import java.text.DateFormat 51 52 class HomeRootSection : Routes.Route() { 53 companion object { 54 val cardMargin = 10.dp 55 } 56 57 private lateinit var activityLauncherHelper: ActivityLauncherHelper 58 59 private val cards by lazy { 60 EnumQuickActions.entries.map { 61 (context.translation["actions.${it.key}.name"] to it.icon) to it.action 62 }.associate { 63 it.first to it.second 64 }.toMutableMap().apply { 65 EnumAction.entries.forEach { action -> 66 this[context.translation["actions.${action.key}.name"] to action.icon] = { 67 context.launchActionIntent(action) 68 } 69 } 70 } 71 } 72 73 @Composable 74 private fun InfoCard( 75 content: @Composable ColumnScope.() -> Unit, 76 ) { 77 OutlinedCard( 78 modifier = Modifier 79 .padding(start = cardMargin, end = cardMargin) 80 .fillMaxWidth(), 81 colors = CardDefaults.cardColors( 82 containerColor = MaterialTheme.colorScheme.surfaceVariant, 83 contentColor = MaterialTheme.colorScheme.onSurfaceVariant 84 ) 85 ) { 86 Column( 87 modifier = Modifier 88 .fillMaxWidth() 89 .padding(all = 10.dp) 90 ) { 91 content() 92 } 93 } 94 } 95 96 @Composable 97 fun ExternalLinkIcon( 98 modifier: Modifier = Modifier, 99 size: Dp = 32.dp, 100 imageVector: ImageVector, 101 ) { 102 Icon( 103 imageVector = imageVector, 104 contentDescription = null, 105 tint = MaterialTheme.colorScheme.onSurfaceVariant, 106 modifier = Modifier 107 .size(size) 108 .clip(RoundedCornerShape(50)) 109 .then(modifier) 110 ) 111 } 112 113 override val title: @Composable (() -> Unit)? = {} 114 115 override val init: () -> Unit = { 116 activityLauncherHelper = ActivityLauncherHelper(context.activity!!) 117 } 118 119 override val topBarActions: @Composable (RowScope.() -> Unit) = { 120 TopBarActionButton( 121 onClick = { 122 routes.homeLogs.navigate() 123 }, 124 icon = Icons.Filled.BugReport, 125 text = context.translation["manager.routes.home_logs"] 126 ) 127 Spacer(modifier = Modifier.width(8.dp)) 128 TopBarActionButton( 129 onClick = { 130 routes.settings.navigate() 131 }, 132 icon = Icons.Filled.Settings, 133 text = context.translation["manager.routes.home_settings"] 134 ) 135 } 136 137 @OptIn(ExperimentalLayoutApi::class) 138 override val content: @Composable (NavBackStackEntry) -> Unit = { 139 val avenirNext = remember { 140 FontFamily( 141 Font(R.font.avenir_next_medium, FontWeight.Medium) 142 ) 143 } 144 145 Column( 146 modifier = Modifier 147 .fillMaxSize() 148 .verticalScroll(rememberScrollState()) 149 ) { 150 Icon( 151 imageVector = Snapenhance, contentDescription = null, 152 modifier = Modifier 153 .fillMaxWidth() 154 .padding(all = 8.dp) 155 .align(Alignment.CenterHorizontally), 156 tint = MaterialTheme.colorScheme.onSurfaceVariant, 157 ) 158 159 Text( 160 text = translation.format( 161 "version_title", 162 "versionName" to BuildConfig.VERSION_NAME 163 ), 164 fontSize = 14.sp, 165 fontFamily = avenirNext, 166 modifier = Modifier.align(Alignment.CenterHorizontally), 167 ) 168 169 Row( 170 horizontalArrangement = Arrangement.spacedBy( 171 15.dp, Alignment.CenterHorizontally 172 ), 173 verticalAlignment = Alignment.CenterVertically, 174 modifier = Modifier 175 .fillMaxWidth() 176 .padding(all = 5.dp) 177 ) { 178 ExternalLinkIcon( 179 modifier = Modifier.clickable { 180 context.androidContext.openLink("https://t.me/snapenhance") 181 }, 182 imageVector = ImageVector.vectorResource(id = R.drawable.ic_telegram), 183 ) 184 185 ExternalLinkIcon( 186 modifier = Modifier.clickable { 187 context.androidContext.openLink("https://github.com/rhunk/SnapEnhance") 188 }, 189 imageVector = ImageVector.vectorResource(id = R.drawable.ic_github), 190 ) 191 192 ExternalLinkIcon( 193 modifier = Modifier.offset(x = (-3).dp).clickable { 194 context.androidContext.openLink("https://github.com/rhunk/SnapEnhance/wiki") 195 }, 196 size = 40.dp, 197 imageVector = Icons.AutoMirrored.Default.Help, 198 ) 199 } 200 201 val selectedTiles = rememberAsyncMutableStateList(defaultValue = listOf()) { 202 context.database.getQuickTiles() 203 } 204 205 val latestUpdate by rememberAsyncMutableState(defaultValue = null) { Updater.latestRelease } 206 207 if (latestUpdate != null) { 208 Spacer(modifier = Modifier.height(10.dp)) 209 InfoCard { 210 Row( 211 modifier = Modifier.fillMaxWidth(), 212 horizontalArrangement = Arrangement.SpaceBetween, 213 verticalAlignment = Alignment.CenterVertically 214 ) { 215 Column { 216 Text( 217 text = translation["update_title"], 218 fontSize = 14.sp, 219 fontWeight = FontWeight.Bold, 220 ) 221 Text( 222 fontSize = 12.sp, 223 text = translation.format( 224 "update_content", 225 "version" to (latestUpdate?.versionName ?: "unknown") 226 ), 227 lineHeight = 20.sp, 228 overflow = TextOverflow.Ellipsis, 229 ) 230 } 231 Button( 232 modifier = Modifier.height(40.dp), 233 onClick = { 234 latestUpdate?.releaseUrl?.let { context.androidContext.openLink(it) } 235 } 236 ) { 237 Text(text = translation["update_button"]) 238 } 239 } 240 } 241 } 242 243 if (BuildConfig.DEBUG) { 244 Spacer(modifier = Modifier.height(10.dp)) 245 InfoCard { 246 Text( 247 text = translation["debug_build_summary_title"], 248 fontSize = 14.sp, 249 fontWeight = FontWeight.Bold, 250 ) 251 val buildSummary = buildAnnotatedString { 252 withStyle( 253 style = SpanStyle( 254 fontSize = 13.sp, 255 color = MaterialTheme.colorScheme.onSurfaceVariant, 256 fontWeight = FontWeight.Light 257 ) 258 ) { 259 append( 260 remember { 261 translation.format( 262 "debug_build_summary_content", 263 "versionName" to BuildConfig.VERSION_NAME, 264 "versionCode" to BuildConfig.VERSION_CODE.toString(), 265 ) 266 } 267 ) 268 append(" - ") 269 } 270 withLink( 271 LinkAnnotation.Clickable( 272 "git_hash", 273 linkInteractionListener = { 274 context.androidContext.openLink("https://github.com/rhunk/SnapEnhance/commit/${BuildConfig.GIT_HASH}") 275 } 276 ) 277 ) { 278 withStyle( 279 style = SpanStyle( 280 fontSize = 13.sp, fontWeight = FontWeight.Bold, 281 color = MaterialTheme.colorScheme.primary 282 ) 283 ) { 284 append(BuildConfig.GIT_HASH.substring(0, 7)) 285 } 286 } 287 } 288 Text( 289 text = buildSummary 290 ) 291 Text( 292 fontSize = 12.sp, 293 text = remember { 294 translation.format( 295 "debug_build_summary_date", 296 "date" to DateFormat.getDateTimeInstance() 297 .format(BuildConfig.BUILD_TIMESTAMP), 298 "days" to ((System.currentTimeMillis() - BuildConfig.BUILD_TIMESTAMP) / 86400000).toInt() 299 .toString() 300 ) 301 }, 302 lineHeight = 20.sp, 303 fontWeight = FontWeight.Light 304 ) 305 } 306 } 307 308 var showQuickActionsMenu by remember { mutableStateOf(false) } 309 310 Row( 311 modifier = Modifier 312 .fillMaxWidth() 313 .padding(start = 20.dp, end = 10.dp, top = 5.dp), 314 verticalAlignment = Alignment.CenterVertically 315 ) { 316 Text( 317 translation["quick_actions_title"], fontSize = 20.sp, 318 modifier = Modifier.weight(1f) 319 ) 320 Box { 321 IconButton( 322 onClick = { showQuickActionsMenu = !showQuickActionsMenu }, 323 ) { 324 Icon(Icons.Default.MoreVert, contentDescription = null) 325 } 326 DropdownMenu( 327 expanded = showQuickActionsMenu, 328 onDismissRequest = { showQuickActionsMenu = false } 329 ) { 330 cards.forEach { (card, _) -> 331 fun toggle(state: Boolean? = null) { 332 if (state?.let { !it } ?: selectedTiles.contains(card.first)) { 333 selectedTiles.remove(card.first) 334 } else { 335 selectedTiles.add(0, card.first) 336 } 337 context.coroutineScope.launch { 338 context.database.setQuickTiles(selectedTiles) 339 } 340 } 341 342 DropdownMenuItem(onClick = { toggle() }, text = { 343 Row( 344 verticalAlignment = Alignment.CenterVertically, 345 modifier = Modifier.padding(all = 5.dp) 346 ) { 347 Checkbox( 348 checked = selectedTiles.contains(card.first), 349 onCheckedChange = { 350 toggle(it) 351 } 352 ) 353 Text(text = card.first) 354 } 355 }) 356 } 357 } 358 } 359 } 360 361 FlowRow( 362 modifier = Modifier 363 .padding(all = cardMargin) 364 .fillMaxWidth(), 365 maxItemsInEachRow = 3, 366 horizontalArrangement = Arrangement.SpaceEvenly, 367 ) { 368 val tileHeight = LocalDensity.current.run { 369 remember { (context.androidContext.resources.displayMetrics.widthPixels / 3).toDp() - cardMargin / 2 } 370 } 371 372 remember(selectedTiles.size, context.translation.loadedLocale) { 373 selectedTiles.mapNotNull { 374 cards.entries.find { entry -> entry.key.first == it } 375 } 376 }.forEach { (card, action) -> 377 ElevatedCard( 378 modifier = Modifier 379 .height(tileHeight) 380 .weight(1f) 381 .padding(all = 6.dp), 382 onClick = { action(routes) } 383 ) { 384 Column( 385 modifier = Modifier 386 .fillMaxSize() 387 .padding(all = 5.dp), 388 horizontalAlignment = Alignment.CenterHorizontally, 389 verticalArrangement = Arrangement.SpaceEvenly, 390 ) { 391 Icon( 392 imageVector = card.second, contentDescription = null, 393 tint = MaterialTheme.colorScheme.onSurfaceVariant, 394 modifier = Modifier.size(50.dp) 395 ) 396 Text( 397 text = card.first, 398 lineHeight = 16.sp, 399 fontSize = 14.sp, 400 fontWeight = FontWeight.Bold, 401 textAlign = TextAlign.Center, 402 overflow = TextOverflow.Ellipsis, 403 ) 404 } 405 } 406 } 407 } 408 } 409 } 410 }