manager: add basic module repo support 2

This commit is contained in:
YuKongA
2025-11-27 13:02:42 +08:00
committed by shirkneko
parent 5e64eee624
commit 3853928305
4 changed files with 357 additions and 202 deletions

View File

@@ -1,5 +1,6 @@
package com.sukisu.ultra.ui package com.sukisu.ultra.ui
import android.annotation.SuppressLint
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -23,9 +24,12 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -38,6 +42,7 @@ import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationSty
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.NavGraphs import com.ramcosta.composedestinations.generated.NavGraphs
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
@@ -166,8 +171,8 @@ fun MainScreen(navController: DestinationsNavigator) {
val pagerState = rememberPagerState(initialPage = 0, pageCount = { 4 }) val pagerState = rememberPagerState(initialPage = 0, pageCount = { 4 })
val hazeState = remember { HazeState() } val hazeState = remember { HazeState() }
val hazeStyle = HazeStyle( val hazeStyle = HazeStyle(
backgroundColor = MiuixTheme.colorScheme.background, backgroundColor = MiuixTheme.colorScheme.surface,
tint = HazeTint(MiuixTheme.colorScheme.background.copy(0.8f)) tint = HazeTint(MiuixTheme.colorScheme.surface.copy(0.8f))
) )
val handlePageChange: (Int) -> Unit = remember(pagerState, coroutineScope) { val handlePageChange: (Int) -> Unit = remember(pagerState, coroutineScope) {
{ page -> { page ->

View File

@@ -37,7 +37,7 @@ fun BottomBar(
if (!fullFeatured) return if (!fullFeatured) return
val item = BottomBarDestination.entries.mapIndexed { index, destination -> val item = BottomBarDestination.entries.map { destination ->
NavigationItem( NavigationItem(
label = stringResource(destination.label), label = stringResource(destination.label),
icon = destination.icon, icon = destination.icon,

View File

@@ -1,6 +1,7 @@
package com.sukisu.ultra.ui.screen package com.sukisu.ultra.ui.screen
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
@@ -16,6 +17,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -23,15 +25,15 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Code
import androidx.compose.material.icons.rounded.Download
import androidx.compose.material.icons.rounded.Link import androidx.compose.material.icons.rounded.Link
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -58,13 +60,17 @@ import dev.chrisbanes.haze.HazeTint
import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.component.MarkdownContent import com.sukisu.ultra.ui.component.MarkdownContent
import com.sukisu.ultra.ui.component.SearchBox
import com.sukisu.ultra.ui.component.SearchPager
import com.sukisu.ultra.ui.component.rememberConfirmDialog import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.theme.isInDarkTheme
import com.sukisu.ultra.ui.util.DownloadListener import com.sukisu.ultra.ui.util.DownloadListener
import com.sukisu.ultra.ui.util.download import com.sukisu.ultra.ui.util.download
import com.sukisu.ultra.ui.viewmodel.ModuleRepoViewModel import com.sukisu.ultra.ui.viewmodel.ModuleRepoViewModel
@@ -76,12 +82,16 @@ import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.PullToRefresh
import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.SmallTitle import top.yukonga.miuix.kmp.basic.SmallTitle
import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.basic.rememberPullToRefreshState
import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.icons.useful.Back import top.yukonga.miuix.kmp.icon.icons.useful.Back
import top.yukonga.miuix.kmp.icon.icons.useful.Redo
import top.yukonga.miuix.kmp.icon.icons.useful.Save
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.utils.PressFeedbackType import top.yukonga.miuix.kmp.utils.PressFeedbackType
import top.yukonga.miuix.kmp.utils.getWindowSize import top.yukonga.miuix.kmp.utils.getWindowSize
@@ -129,8 +139,11 @@ fun ModuleRepoScreen(
navigator: DestinationsNavigator navigator: DestinationsNavigator
) { ) {
val viewModel = viewModel<ModuleRepoViewModel>() val viewModel = viewModel<ModuleRepoViewModel>()
val searchStatus by viewModel.searchStatus
val context = LocalContext.current val context = LocalContext.current
val uriHandler = LocalUriHandler.current val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val isDark = isInDarkTheme(prefs.getInt("color_mode", 0))
val actionIconTint = colorScheme.onSurface.copy(alpha = if (isDark) 0.7f else 0.9f)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -140,6 +153,9 @@ fun ModuleRepoScreen(
} }
val scrollBehavior = MiuixScrollBehavior() val scrollBehavior = MiuixScrollBehavior()
val dynamicTopPadding by remember {
derivedStateOf { 12.dp * (1f - scrollBehavior.state.collapsedFraction) }
}
val hazeState = remember { HazeState() } val hazeState = remember { HazeState() }
val hazeStyle = HazeStyle( val hazeStyle = HazeStyle(
backgroundColor = colorScheme.surface, backgroundColor = colorScheme.surface,
@@ -158,21 +174,15 @@ fun ModuleRepoScreen(
Scaffold( Scaffold(
topBar = { topBar = {
searchStatus.TopAppBarAnim(hazeState = hazeState, hazeStyle = hazeStyle) {
TopAppBar( TopAppBar(
modifier = Modifier.hazeEffect(hazeState) {
style = hazeStyle
blurRadius = 30.dp
noiseFactor = 0f
},
color = Color.Transparent, color = Color.Transparent,
title = stringResource(R.string.module_repo), title = stringResource(R.string.module_repo),
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
navigationIcon = { navigationIcon = {
IconButton( IconButton(
modifier = Modifier.padding(start = 16.dp), modifier = Modifier.padding(start = 16.dp),
onClick = { onClick = { navigator.popBackStack() }
navigator.popBackStack()
}
) { ) {
Icon( Icon(
imageVector = MiuixIcons.Useful.Back, imageVector = MiuixIcons.Useful.Back,
@@ -183,6 +193,84 @@ fun ModuleRepoScreen(
} }
) )
} }
},
popupHost = {
searchStatus.SearchPager(defaultResult = {}) {
item {
Spacer(Modifier.height(6.dp))
}
items(viewModel.searchResults.value, key = { it.moduleId }) { module ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
.padding(bottom = 12.dp),
insideMargin = PaddingValues(16.dp),
showIndication = true,
pressFeedbackType = PressFeedbackType.Sink,
onClick = {
val args = RepoModuleArg(
moduleId = module.moduleId,
moduleName = module.moduleName,
authors = module.authors,
authorsList = module.authorList.map { AuthorArg(it.name, it.link) },
homepageUrl = module.homepageUrl,
sourceUrl = module.sourceUrl,
latestRelease = module.latestRelease,
latestReleaseTime = module.latestReleaseTime,
releases = module.releases.map { r ->
ReleaseArg(
tagName = r.tagName,
name = r.name,
publishedAt = r.publishedAt,
assets = r.assets.map { a ->
ReleaseAssetArg(name = a.name, downloadUrl = a.downloadUrl, size = a.size)
}
)
}
)
navigator.navigate(ModuleRepoDetailScreenDestination(args)) { launchSingleTop = true }
}
) {
Column {
if (module.moduleName.isNotBlank()) {
Text(
text = module.moduleName,
fontSize = 17.sp,
fontWeight = FontWeight(550),
color = colorScheme.onSurface
)
}
if (module.moduleId.isNotBlank()) {
Text(
text = "ID: ${module.moduleId}",
fontSize = 12.sp,
fontWeight = FontWeight(550),
color = colorScheme.onSurfaceVariantSummary,
)
}
Text(
text = "${stringResource(id = R.string.module_author)}: ${module.authors}",
fontSize = 12.sp,
modifier = Modifier.padding(bottom = 1.dp),
fontWeight = FontWeight(550),
color = colorScheme.onSurfaceVariantSummary,
)
if (module.summary.isNotBlank()) {
Text(
text = module.summary,
fontSize = 14.sp,
color = colorScheme.onSurfaceVariantSummary,
modifier = Modifier.padding(top = 2.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 4,
)
}
}
}
}
}
},
) { innerPadding -> ) { innerPadding ->
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val isLoading = viewModel.modules.value.isEmpty() val isLoading = viewModel.modules.value.isEmpty()
@@ -190,13 +278,49 @@ fun ModuleRepoScreen(
if (isLoading) { if (isLoading) {
Box( Box(
modifier = Modifier modifier = Modifier
.height(getWindowSize().height.dp) .fillMaxSize(),
.hazeSource(state = hazeState),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
InfiniteProgressIndicator() InfiniteProgressIndicator()
} }
} else { } else {
LaunchedEffect(searchStatus.searchText) { viewModel.updateSearchText(searchStatus.searchText) }
searchStatus.SearchBox(
searchBarTopPadding = dynamicTopPadding,
contentPadding = PaddingValues(
top = innerPadding.calculateTopPadding(),
start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection)
),
hazeState = hazeState,
hazeStyle = hazeStyle
) { boxHeight ->
var isRefreshing by rememberSaveable { mutableStateOf(false) }
val pullToRefreshState = rememberPullToRefreshState()
LaunchedEffect(isRefreshing) {
if (isRefreshing) {
delay(350)
viewModel.refresh()
isRefreshing = false
}
}
val refreshTexts = listOf(
stringResource(R.string.refresh_pulling),
stringResource(R.string.refresh_release),
stringResource(R.string.refresh_refresh),
stringResource(R.string.refresh_complete),
)
PullToRefresh(
isRefreshing = isRefreshing,
pullToRefreshState = pullToRefreshState,
onRefresh = { if (!isRefreshing) isRefreshing = true },
refreshTexts = refreshTexts,
contentPadding = PaddingValues(
top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp,
start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection)
),
) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.height(getWindowSize().height.dp) .height(getWindowSize().height.dp)
@@ -205,36 +329,30 @@ fun ModuleRepoScreen(
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection)
.hazeSource(state = hazeState), .hazeSource(state = hazeState),
contentPadding = PaddingValues( contentPadding = PaddingValues(
top = innerPadding.calculateTopPadding(), top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp,
start = innerPadding.calculateStartPadding(layoutDirection), start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection) end = innerPadding.calculateEndPadding(layoutDirection)
), ),
overscrollEffect = null,
) { ) {
items(viewModel.modules.value, key = { it.moduleId }) { module -> items(
val moduleName = remember(module.moduleName) { module.moduleName } items = viewModel.modules.value,
val moduleId = remember(module.moduleId) { module.moduleId } key = { it.moduleId },
val authors = remember(module.authors) { module.authors } contentType = { "module" }
val summary = remember(module.summary) { module.summary } ) { module ->
val latestReleaseTime = remember(module.latestReleaseTime) { module.latestReleaseTime } val latestTag = module.latestRelease
val latestTag = remember(module.latestRelease) { module.latestRelease }
val latestRel = remember(module.releases, latestTag) { val latestRel = remember(module.releases, latestTag) {
module.releases.find { it.tagName == latestTag } ?: module.releases.firstOrNull() module.releases.find { it.tagName == latestTag } ?: module.releases.firstOrNull()
} }
val latestAsset = remember(latestRel) { latestRel?.assets?.firstOrNull() } val latestAsset = remember(latestRel) { latestRel?.assets?.firstOrNull() }
val moduleVersion = stringResource(id = R.string.module_version)
val moduleAuthor = stringResource(id = R.string.module_author) val moduleAuthor = stringResource(id = R.string.module_author)
val secondaryContainer = colorScheme.secondaryContainer.copy(alpha = 0.8f)
val actionIconTint = colorScheme.onSurface.copy(alpha = 0.9f)
val installBg = colorScheme.tertiaryContainer.copy(alpha = 0.6f)
val installTint = colorScheme.onTertiaryContainer.copy(alpha = 0.8f)
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 12.dp) .padding(horizontal = 12.dp)
.padding(vertical = 8.dp), .padding(bottom = 12.dp),
insideMargin = PaddingValues(16.dp), insideMargin = PaddingValues(16.dp),
showIndication = true, showIndication = true,
pressFeedbackType = PressFeedbackType.Sink, pressFeedbackType = PressFeedbackType.Sink,
@@ -242,7 +360,7 @@ fun ModuleRepoScreen(
val args = RepoModuleArg( val args = RepoModuleArg(
moduleId = module.moduleId, moduleId = module.moduleId,
moduleName = module.moduleName, moduleName = module.moduleName,
authors = authors, authors = module.authors,
authorsList = module.authorList.map { AuthorArg(it.name, it.link) }, authorsList = module.authorList.map { AuthorArg(it.name, it.link) },
homepageUrl = module.homepageUrl, homepageUrl = module.homepageUrl,
sourceUrl = module.sourceUrl, sourceUrl = module.sourceUrl,
@@ -265,39 +383,32 @@ fun ModuleRepoScreen(
} }
) { ) {
Column { Column {
if (moduleName.isNotBlank()) { if (module.moduleName.isNotBlank()) {
Text( Text(
text = moduleName, text = module.moduleName,
fontSize = 17.sp, fontSize = 17.sp,
fontWeight = FontWeight(550), fontWeight = FontWeight(550),
color = colorScheme.onSurface color = colorScheme.onSurface
) )
} }
if (moduleId.isNotBlank()) { if (module.moduleId.isNotBlank()) {
Text( Text(
text = moduleId, text = "ID: ${module.moduleId}",
fontSize = 14.sp, fontSize = 12.sp,
fontWeight = FontWeight(550), fontWeight = FontWeight(550),
color = colorScheme.onSurfaceVariantSummary, color = colorScheme.onSurfaceVariantSummary,
) )
} }
Text( Text(
text = "$moduleVersion: $latestTag", text = "$moduleAuthor: ${module.authors}",
fontSize = 12.sp,
modifier = Modifier.padding(top = 2.dp),
fontWeight = FontWeight(550),
color = colorScheme.onSurfaceVariantSummary,
)
Text(
text = "$moduleAuthor: $authors",
fontSize = 12.sp, fontSize = 12.sp,
modifier = Modifier.padding(bottom = 1.dp), modifier = Modifier.padding(bottom = 1.dp),
fontWeight = FontWeight(550), fontWeight = FontWeight(550),
color = colorScheme.onSurfaceVariantSummary, color = colorScheme.onSurfaceVariantSummary,
) )
if (summary.isNotBlank()) { if (module.summary.isNotBlank()) {
Text( Text(
text = summary, text = module.summary,
fontSize = 14.sp, fontSize = 14.sp,
color = colorScheme.onSurfaceVariantSummary, color = colorScheme.onSurfaceVariantSummary,
modifier = Modifier.padding(top = 2.dp), modifier = Modifier.padding(top = 2.dp),
@@ -305,52 +416,29 @@ fun ModuleRepoScreen(
maxLines = 4, maxLines = 4,
) )
} }
if (latestReleaseTime.isNotBlank()) {
Text(
modifier = Modifier.fillMaxWidth(),
text = latestReleaseTime,
fontSize = 12.sp,
color = colorScheme.onSurfaceVariantSummary,
textAlign = TextAlign.End
)
}
HorizontalDivider( HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp), modifier = Modifier.padding(vertical = 8.dp),
thickness = 0.5.dp, thickness = 0.5.dp,
color = colorScheme.outline.copy(alpha = 0.5f) color = colorScheme.outline.copy(alpha = 0.5f)
) )
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
if (module.homepageUrl.isNotBlank()) { Column {
IconButton( Text(
backgroundColor = secondaryContainer, text = latestTag,
minHeight = 35.dp, fontSize = 12.sp,
minWidth = 35.dp, modifier = Modifier.padding(top = 2.dp),
onClick = { uriHandler.openUri(module.homepageUrl) }, fontWeight = FontWeight(550),
) { color = colorScheme.onSurfaceVariantSummary,
Icon(
modifier = Modifier.size(20.dp),
imageVector = Icons.Rounded.Link,
tint = actionIconTint,
contentDescription = null
) )
} if (module.latestReleaseTime.isNotBlank()) {
} Text(
if (module.sourceUrl.isNotBlank()) { text = module.latestReleaseTime,
IconButton( fontSize = 12.sp,
backgroundColor = secondaryContainer, color = colorScheme.onSurfaceVariantSummary,
minHeight = 35.dp, textAlign = TextAlign.End
minWidth = 35.dp,
onClick = { uriHandler.openUri(module.sourceUrl) },
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = Icons.Rounded.Code,
tint = actionIconTint,
contentDescription = null
) )
} }
} }
@@ -359,7 +447,7 @@ fun ModuleRepoScreen(
val fileName = latestAsset.name val fileName = latestAsset.name
val downloadingText = stringResource(R.string.module_downloading) val downloadingText = stringResource(R.string.module_downloading)
IconButton( IconButton(
backgroundColor = installBg, backgroundColor = colorScheme.secondaryContainer.copy(alpha = 0.8f),
minHeight = 35.dp, minHeight = 35.dp,
minWidth = 35.dp, minWidth = 35.dp,
onClick = { onClick = {
@@ -373,7 +461,8 @@ fun ModuleRepoScreen(
) )
} }
} }
val confirmContent = context.getString(R.string.module_install_prompt_with_name, fileName) val confirmContent =
context.getString(R.string.module_install_prompt_with_name, fileName)
confirmDialog.showConfirm( confirmDialog.showConfirm(
title = confirmTitle, title = confirmTitle,
content = confirmContent content = confirmContent
@@ -386,14 +475,14 @@ fun ModuleRepoScreen(
) { ) {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
imageVector = Icons.Rounded.Download, imageVector = MiuixIcons.Useful.Save,
tint = installTint, tint = actionIconTint,
contentDescription = stringResource(R.string.install) contentDescription = stringResource(R.string.install)
) )
Text( Text(
modifier = Modifier.padding(start = 4.dp, end = 3.dp), modifier = Modifier.padding(start = 4.dp, end = 2.dp),
text = stringResource(R.string.install), text = stringResource(R.string.install),
color = installTint, color = actionIconTint,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 15.sp fontSize = 15.sp
) )
@@ -407,9 +496,11 @@ fun ModuleRepoScreen(
item { Spacer(Modifier.height(12.dp)) } item { Spacer(Modifier.height(12.dp)) }
} }
} }
}
DownloadListener(context, onInstallModule) DownloadListener(context, onInstallModule)
} }
} }
}
@SuppressLint("StringFormatInvalid", "DefaultLocale") @SuppressLint("StringFormatInvalid", "DefaultLocale")
@Composable @Composable
@@ -419,6 +510,11 @@ fun ModuleRepoDetailScreen(
module: RepoModuleArg module: RepoModuleArg
) { ) {
val context = LocalContext.current val context = LocalContext.current
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val isDark = isInDarkTheme(prefs.getInt("color_mode", 0))
val actionIconTint = colorScheme.onSurface.copy(alpha = if (isDark) 0.7f else 0.9f)
val secondaryContainer = colorScheme.secondaryContainer.copy(alpha = 0.8f)
val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val confirmTitle = stringResource(R.string.module) val confirmTitle = stringResource(R.string.module)
var pendingDownload by remember { mutableStateOf<(() -> Unit)?>(null) } var pendingDownload by remember { mutableStateOf<(() -> Unit)?>(null) }
@@ -459,6 +555,20 @@ fun ModuleRepoDetailScreen(
tint = colorScheme.onSurface tint = colorScheme.onSurface
) )
} }
},
actions = {
if (module.homepageUrl.isNotBlank()) {
IconButton(
modifier = Modifier.padding(end = 16.dp),
onClick = { uriHandler.openUri(module.homepageUrl) }
) {
Icon(
imageVector = MiuixIcons.Useful.Redo,
contentDescription = null,
tint = colorScheme.onBackground
)
}
}
} }
) )
} }
@@ -497,8 +607,13 @@ fun ModuleRepoDetailScreen(
bottom = innerPadding.calculateBottomPadding(), bottom = innerPadding.calculateBottomPadding(),
), ),
) { ) {
if (readmeLoaded && readmeText != null) {
item { item {
AnimatedVisibility(
visible = readmeLoaded && readmeText != null,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
) {
Column {
SmallTitle(text = "README") SmallTitle(text = "README")
Card( Card(
modifier = Modifier modifier = Modifier
@@ -511,6 +626,7 @@ fun ModuleRepoDetailScreen(
} }
} }
} }
}
if (module.authorsList.isNotEmpty()) { if (module.authorsList.isNotEmpty()) {
item { item {
SmallTitle( SmallTitle(
@@ -519,9 +635,6 @@ fun ModuleRepoDetailScreen(
) )
} }
item { item {
val secondaryContainer = colorScheme.secondaryContainer.copy(alpha = 0.8f)
val actionIconTint = colorScheme.onSurface.copy(alpha = 0.9f)
val uriHandler = LocalUriHandler.current
Card( Card(
modifier = Modifier modifier = Modifier
.padding(horizontal = 12.dp), .padding(horizontal = 12.dp),
@@ -580,10 +693,12 @@ fun ModuleRepoDetailScreen(
modifier = Modifier.padding(top = 10.dp) modifier = Modifier.padding(top = 10.dp)
) )
} }
items(module.releases, key = { it.tagName }) { rel -> items(
items = module.releases,
key = { it.tagName },
contentType = { "release" }
) { rel ->
val title = remember(rel.name, rel.tagName) { rel.name.ifBlank { rel.tagName } } val title = remember(rel.name, rel.tagName) { rel.name.ifBlank { rel.tagName } }
val installBg = colorScheme.tertiaryContainer.copy(alpha = 0.6f)
val installTint = colorScheme.onTertiaryContainer.copy(alpha = 0.8f)
Card( Card(
modifier = Modifier modifier = Modifier
@@ -633,7 +748,6 @@ fun ModuleRepoDetailScreen(
rel.assets.forEachIndexed { index, asset -> rel.assets.forEachIndexed { index, asset ->
val fileName = asset.name val fileName = asset.name
stringResource(R.string.module_start_downloading)
val downloadingText = stringResource(R.string.module_downloading) val downloadingText = stringResource(R.string.module_downloading)
val sizeText = remember(asset.size) { val sizeText = remember(asset.size) {
val s = asset.size val s = asset.size
@@ -683,7 +797,7 @@ fun ModuleRepoDetailScreen(
) )
} }
IconButton( IconButton(
backgroundColor = installBg, backgroundColor = secondaryContainer,
minHeight = 35.dp, minHeight = 35.dp,
minWidth = 35.dp, minWidth = 35.dp,
onClick = onClickDownload, onClick = onClickDownload,
@@ -694,14 +808,14 @@ fun ModuleRepoDetailScreen(
) { ) {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
imageVector = Icons.Rounded.Download, imageVector = MiuixIcons.Useful.Save,
tint = installTint, tint = actionIconTint,
contentDescription = stringResource(R.string.install) contentDescription = stringResource(R.string.install)
) )
Text( Text(
modifier = Modifier.padding(start = 4.dp, end = 3.dp), modifier = Modifier.padding(start = 4.dp, end = 2.dp),
text = stringResource(R.string.install), text = stringResource(R.string.install),
color = installTint, color = actionIconTint,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 15.sp fontSize = 15.sp
) )

View File

@@ -12,6 +12,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.component.SearchStatus
import com.sukisu.ultra.ui.util.HanziToPinyin
import okhttp3.Request import okhttp3.Request
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
@@ -64,6 +66,12 @@ class ModuleRepoViewModel : ViewModel() {
var isRefreshing by mutableStateOf(false) var isRefreshing by mutableStateOf(false)
private set private set
private val _searchStatus = mutableStateOf(SearchStatus(""))
val searchStatus: State<SearchStatus> = _searchStatus
private val _searchResults = mutableStateOf<List<RepoModule>>(emptyList())
val searchResults: State<List<RepoModule>> = _searchResults
fun refresh() { fun refresh() {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.Main) { isRefreshing = true } withContext(Dispatchers.Main) { isRefreshing = true }
@@ -75,6 +83,34 @@ class ModuleRepoViewModel : ViewModel() {
} }
} }
suspend fun updateSearchText(text: String) {
_searchStatus.value.searchText = text
if (text.isEmpty()) {
_searchStatus.value.resultStatus = SearchStatus.ResultStatus.DEFAULT
_searchResults.value = emptyList()
return
}
val result = withContext(Dispatchers.IO) {
_searchStatus.value.resultStatus = SearchStatus.ResultStatus.LOAD
_modules.value.filter {
it.moduleId.contains(text, true)
|| it.moduleName.contains(text, true)
|| it.authors.contains(text, true)
|| it.summary.contains(text, true)
|| HanziToPinyin.getInstance().toPinyinString(it.moduleName).contains(text, true)
}
}
_searchResults.value = result
_searchStatus.value.resultStatus = if (result.isEmpty()) {
SearchStatus.ResultStatus.EMPTY
} else {
SearchStatus.ResultStatus.SHOW
}
}
private fun fetchModulesInternal(): List<RepoModule> { private fun fetchModulesInternal(): List<RepoModule> {
return runCatching { return runCatching {
val request = Request.Builder().url(MODULES_URL).build() val request = Request.Builder().url(MODULES_URL).build()