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 }