From 3d8bc944e341605aa8a8fccae31a59f80ceb0a7c Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Sat, 11 May 2024 03:26:02 +0800 Subject: [PATCH] Feat(ToolScreen): Finish tool store list --- app/build.gradle.kts | 1 + .../top/fatweb/oxygen/toolbox/icon/Loading.kt | 80 +++++++++ .../fatweb/oxygen/toolbox/icon/OxygenIcons.kt | 29 +++- .../toolbox/navigation/OxygenNavHost.kt | 6 +- .../toolbox/navigation/ToolsNavigation.kt | 10 +- .../top/fatweb/oxygen/toolbox/ui/OxygenApp.kt | 10 +- .../toolbox/ui/about/LibrariesScreen.kt | 33 +++- .../oxygen/toolbox/ui/component/ToolCard.kt | 161 ++++++++++++++++++ .../toolbox/ui/settings/SettingsDialog.kt | 35 +++- .../oxygen/toolbox/ui/tool/ToolsPanel.kt | 10 +- .../oxygen/toolbox/ui/tool/ToolsScreen.kt | 97 +++++++---- .../toolbox/ui/tool/ToolsScreenViewModel.kt | 14 +- app/src/main/res/values-zh/strings.xml | 3 +- app/src/main/res/values/strings.xml | 3 +- gradle/libs.versions.toml | 2 + 15 files changed, 437 insertions(+), 57 deletions(-) create mode 100644 app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/Loading.kt create mode 100644 app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/ToolCard.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 96cd765..9433104 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -168,4 +168,5 @@ dependencies { implementation(libs.okhttp.logging) implementation(libs.paging.runtime) implementation(libs.paging.compose) + implementation(libs.androidsvg.aar) } \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/Loading.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/Loading.kt new file mode 100644 index 0000000..266ca7e --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/Loading.kt @@ -0,0 +1,80 @@ +package top.fatweb.oxygen.toolbox.icon + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + + +@Preview +@Composable +private fun VectorPreview() { + Image(OxygenIcons.Loading, null) +} + +private var loading: ImageVector? = null + +val OxygenIcons.Loading: ImageVector + get() { + if (loading != null) { + return loading!! + } + loading = ImageVector.Builder( + name = "Loading", + defaultWidth = 1024.dp, + defaultHeight = 1024.dp, + viewportWidth = 1024f, + viewportHeight = 1024f + ).apply { + path( + fill = SolidColor(Color.Black), + fillAlpha = 1.0f, + stroke = null, + strokeAlpha = 1.0f, + strokeLineWidth = 1.0f, + strokeLineCap = StrokeCap.Butt, + strokeLineJoin = StrokeJoin.Miter, + strokeLineMiter = 1.0f, + pathFillType = PathFillType.NonZero + ) { + moveTo(988f, 548f) + curveToRelative(-19.9f, 0f, -36f, -16.1f, -36f, -36f) + curveToRelative(0f, -59.4f, -11.6f, -117f, -34.6f, -171.3f) + arcToRelative( + 440.45f, + 440.45f, + 0f, + isMoreThanHalf = false, + isPositiveArc = false, + -94.3f, + -139.9f + ) + arcToRelative( + 437.71f, + 437.71f, + 0f, + isMoreThanHalf = false, + isPositiveArc = false, + -139.9f, + -94.3f + ) + curveTo(629f, 83.6f, 571.4f, 72f, 512f, 72f) + curveToRelative(-19.9f, 0f, -36f, -16.1f, -36f, -36f) + reflectiveCurveToRelative(16.1f, -36f, 36f, -36f) + curveToRelative(69.1f, 0f, 136.2f, 13.5f, 199.3f, 40.3f) + curveTo(772.3f, 66f, 827f, 103f, 874f, 150f) + curveToRelative(47f, 47f, 83.9f, 101.8f, 109.7f, 162.7f) + curveToRelative(26.7f, 63.1f, 40.2f, 130.2f, 40.2f, 199.3f) + curveToRelative(0.1f, 19.9f, -16f, 36f, -35.9f, 36f) + close() + } + }.build() + return loading!! + } diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt index a317bd8..6dd08ad 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/icon/OxygenIcons.kt @@ -1,5 +1,7 @@ package top.fatweb.oxygen.toolbox.icon +import android.graphics.BitmapFactory +import android.graphics.drawable.PictureDrawable import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccessTime import androidx.compose.material.icons.filled.Build @@ -16,6 +18,12 @@ import androidx.compose.material.icons.rounded.Home import androidx.compose.material.icons.rounded.KeyboardArrowDown import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Star +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.core.graphics.drawable.toBitmap +import com.caverock.androidsvg.SVG +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi object OxygenIcons { val ArrowDown = Icons.Rounded.KeyboardArrowDown @@ -33,4 +41,23 @@ object OxygenIcons { val StarBorder = Icons.Outlined.StarBorder val Time = Icons.Default.AccessTime val Tool = Icons.Default.Build -} \ No newline at end of file + + fun fromSvgBase64(base64String: String): ImageBitmap { + val svg = SVG.getFromString(base64DecodeToString(base64String)) + val drawable = PictureDrawable(svg.renderToPicture()) + return drawable.toBitmap().asImageBitmap() + } + + fun fromPngBase64(base64String: String): ImageBitmap { + val byteArray = base64DecodeToByteArray(base64String) + return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size).asImageBitmap() + } + + @OptIn(ExperimentalEncodingApi::class) + private fun base64DecodeToString(base64String: String): String = + Base64.decode(base64String).decodeToString() + + @OptIn(ExperimentalEncodingApi::class) + private fun base64DecodeToByteArray(base64String: String): ByteArray = + Base64.decode(base64String) +} diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt index 70e39f4..a845ce9 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/OxygenNavHost.kt @@ -10,7 +10,8 @@ fun OxygenNavHost( modifier: Modifier = Modifier, appState: OxygenAppState, onShowSnackbar: suspend (String, String?) -> Boolean, - startDestination: String + startDestination: String, + handleOnCanScrollChange: (Boolean) -> Unit ) { val navController = appState.navController NavHost( @@ -29,7 +30,8 @@ fun OxygenNavHost( onBackClick = navController::popBackStack ) toolsScreen( - + onShowSnackbar = onShowSnackbar, + handleOnCanScrollChange = handleOnCanScrollChange ) starScreen( diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt index 85e664a..c3cae51 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/navigation/ToolsNavigation.kt @@ -10,10 +10,16 @@ const val TOOLS_ROUTE = "tools_route" fun NavController.navigateToTools(navOptions: NavOptions) = navigate(TOOLS_ROUTE, navOptions) -fun NavGraphBuilder.toolsScreen() { +fun NavGraphBuilder.toolsScreen( + onShowSnackbar: suspend (String, String?) -> Boolean, + handleOnCanScrollChange: (Boolean) -> Unit +) { composable( route = TOOLS_ROUTE ) { - ToolsRoute() + ToolsRoute( + onShowSnackbar = onShowSnackbar, + handleOnCanScrollChange = handleOnCanScrollChange + ) } } \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt index c210add..2bb6756 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/OxygenApp.kt @@ -78,7 +78,12 @@ fun OxygenApp(appState: OxygenAppState) { val noConnectMessage = stringResource(R.string.core_no_connect) - val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + var canScroll by remember { mutableStateOf(false) } + val handleOnCanScrollChange = { value: Boolean -> + canScroll = value + } + val topAppBarScrollBehavior = + TopAppBarDefaults.enterAlwaysScrollBehavior(canScroll = { canScroll }) val bottomAppBarScrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior() LaunchedEffect(isOffline) { @@ -175,7 +180,8 @@ fun OxygenApp(appState: OxygenAppState) { startDestination = when (appState.launchPageConfig) { LaunchPageConfig.TOOLS -> TOOLS_ROUTE LaunchPageConfig.STAR -> STAR_ROUTE - } + }, + handleOnCanScrollChange = handleOnCanScrollChange ) } } diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/about/LibrariesScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/about/LibrariesScreen.kt index 5b8f64c..7db70dc 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/about/LibrariesScreen.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/about/LibrariesScreen.kt @@ -3,6 +3,11 @@ package top.fatweb.oxygen.toolbox.ui.about import android.content.Intent import android.net.Uri import androidx.activity.compose.ReportDrawnWhen +import androidx.compose.animation.core.Ease +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -21,6 +26,7 @@ import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsBottomHeight @@ -33,6 +39,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -46,6 +53,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext @@ -54,6 +62,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import top.fatweb.oxygen.toolbox.R +import top.fatweb.oxygen.toolbox.icon.Loading import top.fatweb.oxygen.toolbox.icon.OxygenIcons import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar import top.fatweb.oxygen.toolbox.ui.component.scrollbar.DraggableScrollbar @@ -105,6 +114,8 @@ internal fun LibrariesScreen( val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(canScroll = { state.canScrollForward }) + val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition") + var activeSearch by remember { mutableStateOf(false) } var searchValue by remember { mutableStateOf("") } @@ -156,8 +167,26 @@ internal fun LibrariesScreen( ) ) { when (librariesScreenUiState) { - LibrariesScreenUiState.Loading -> { - Text(text = stringResource(R.string.feature_settings_loading)) + LibrariesScreenUiState.Loading -> {Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + val angle by infiniteTransition.animateFloat( + initialValue = 0F, + targetValue = 360F, + animationSpec = infiniteRepeatable( + animation = tween(800, easing = Ease), + ), label = "angle" + ) + Icon( + modifier = Modifier + .size(32.dp) + .graphicsLayer { rotationZ = angle }, + imageVector = OxygenIcons.Loading, + contentDescription = "" + ) + } } LibrariesScreenUiState.Nothing -> { diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/ToolCard.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/ToolCard.kt new file mode 100644 index 0000000..f11693a --- /dev/null +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/component/ToolCard.kt @@ -0,0 +1,161 @@ +package top.fatweb.oxygen.toolbox.ui.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import top.fatweb.oxygen.toolbox.R +import top.fatweb.oxygen.toolbox.icon.OxygenIcons +import top.fatweb.oxygen.toolbox.model.tool.Tool + +@Composable +fun ToolCard( + modifier: Modifier = Modifier, + tool: Tool, + onClickToolCard: () -> Unit +) { + Card( + modifier = modifier, + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + onClick = onClickToolCard + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + ToolVer(ver = tool.ver) + Spacer(modifier = Modifier.height(16.dp)) + ToolIcon(icon = tool.icon) + Spacer(modifier = Modifier.height(16.dp)) + ToolInfo( + toolName = tool.name, + toolId = tool.toolId, + toolDesc = tool.description + ) + Spacer(modifier = Modifier.height(16.dp)) + AuthorInfo( + avatar = tool.author.avatar, + nickname = tool.author.nickname + ) + } + } +} + +@Composable +fun ToolVer( + modifier: Modifier = Modifier, + ver: String +) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors(contentColor = MaterialTheme.colorScheme.onSecondaryContainer) + ) { + Column( + modifier = Modifier + .background(color = MaterialTheme.colorScheme.surfaceContainer) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + style = MaterialTheme.typography.bodyMedium, + text = ver + ) + } + } +} + +@Composable +fun ToolIcon( + modifier: Modifier = Modifier, + icon: String +) { + Box( + modifier = modifier + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier.size(80.dp), + bitmap = OxygenIcons.fromSvgBase64(icon), + contentDescription = "" + ) + } +} + +@Composable +fun ToolInfo( + modifier: Modifier = Modifier, + toolName: String, + toolId: String, + toolDesc: String +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.ExtraBold, + text = toolName + ) + Text( + style = MaterialTheme.typography.bodyMedium, + text = "ID: $toolId" + ) + Text( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline, + text = "${stringResource(R.string.feature_tools_description)}: $toolDesc" + ) + } +} + +@Composable +fun AuthorInfo( + modifier: Modifier = Modifier, + avatar: String, + nickname: String +) { + Row( + modifier = modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) + ) { + Surface( + modifier = Modifier + .size(24.dp), + shape = RoundedCornerShape(12.dp) + ) { + Image( + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceContainer), + bitmap = OxygenIcons.fromPngBase64(avatar), contentDescription = "Avatar" + ) + } + Text( + style = MaterialTheme.typography.bodyMedium, + text = nickname + ) + } +} diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/settings/SettingsDialog.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/settings/SettingsDialog.kt index 3d38f1b..49229f9 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/settings/SettingsDialog.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/settings/SettingsDialog.kt @@ -1,14 +1,22 @@ package top.fatweb.oxygen.toolbox.ui.settings import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Ease +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState @@ -26,6 +34,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource @@ -34,6 +43,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import top.fatweb.oxygen.toolbox.R +import top.fatweb.oxygen.toolbox.icon.Loading import top.fatweb.oxygen.toolbox.icon.OxygenIcons import top.fatweb.oxygen.toolbox.model.userdata.DarkThemeConfig import top.fatweb.oxygen.toolbox.model.userdata.LanguageConfig @@ -82,6 +92,7 @@ fun SettingsDialog( onNavigateToAbout: () -> Unit ) { val configuration = LocalConfiguration.current + val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition") AlertDialog( modifier = modifier @@ -101,10 +112,26 @@ fun SettingsDialog( ) { when (settingsUiState) { SettingsUiState.Loading -> { - Text( - modifier = Modifier.padding(vertical = 16.dp), - text = stringResource(R.string.feature_settings_loading) - ) + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + val angle by infiniteTransition.animateFloat( + initialValue = 0F, + targetValue = 360F, + animationSpec = infiniteRepeatable( + animation = tween(800, easing = Ease), + ), label = "angle" + ) + Icon( + modifier = Modifier + .size(32.dp) + .graphicsLayer { rotationZ = angle }, + imageVector = OxygenIcons.Loading, + contentDescription = "" + ) + } } is SettingsUiState.Success -> { diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsPanel.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsPanel.kt index 6b57aa9..7a2ceee 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsPanel.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsPanel.kt @@ -2,17 +2,21 @@ package top.fatweb.oxygen.toolbox.ui.tool import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope import androidx.compose.foundation.lazy.staggeredgrid.items -import androidx.compose.material3.Text import androidx.paging.compose.LazyPagingItems import top.fatweb.oxygen.toolbox.model.tool.Tool +import top.fatweb.oxygen.toolbox.ui.component.ToolCard fun LazyStaggeredGridScope.toolsPanel( - toolStorePagingItems: LazyPagingItems + toolStorePagingItems: LazyPagingItems, + onClickToolCard: (username: String, toolId: String) -> Unit ) { items( items = toolStorePagingItems.itemSnapshotList, key = { it!!.id }, ) { - Text(text = it!!.name) + ToolCard( + tool = it!!, + onClickToolCard = {onClickToolCard(it.author.username, it.toolId)} + ) } } \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreen.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreen.kt index 86ea983..02eedc3 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreen.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreen.kt @@ -1,18 +1,26 @@ package top.fatweb.oxygen.toolbox.ui.tool -import android.util.Log +import android.widget.Toast import androidx.activity.compose.ReportDrawnWhen +import androidx.compose.animation.core.Ease +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsPadding @@ -20,14 +28,21 @@ import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems +import top.fatweb.oxygen.toolbox.icon.Loading +import top.fatweb.oxygen.toolbox.icon.OxygenIcons import top.fatweb.oxygen.toolbox.model.tool.Tool import top.fatweb.oxygen.toolbox.ui.component.scrollbar.DraggableScrollbar import top.fatweb.oxygen.toolbox.ui.component.scrollbar.rememberDraggableScroller @@ -36,12 +51,16 @@ import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState @Composable internal fun ToolsRoute( modifier: Modifier = Modifier, - viewModel: ToolsScreenViewModel = hiltViewModel() + viewModel: ToolsScreenViewModel = hiltViewModel(), + onShowSnackbar: suspend (String, String?) -> Boolean, + handleOnCanScrollChange: (Boolean) -> Unit ) { - val toolStorePagingItems = viewModel.getStoreData().collectAsLazyPagingItems() + val toolStorePagingItems = viewModel.storeData.collectAsLazyPagingItems() ToolsScreen( modifier = modifier, + onShowSnackbar = onShowSnackbar, + handleOnCanScrollChange = handleOnCanScrollChange, toolStorePagingItems = toolStorePagingItems ) } @@ -49,12 +68,15 @@ internal fun ToolsRoute( @Composable internal fun ToolsScreen( modifier: Modifier = Modifier, + onShowSnackbar: suspend (String, String?) -> Boolean, + handleOnCanScrollChange: (Boolean) -> Unit, toolStorePagingItems: LazyPagingItems ) { - val isToolLoading = toolStorePagingItems.loadState.refresh is LoadState.Loading + val context = LocalContext.current + val isToolLoading = + toolStorePagingItems.loadState.refresh is LoadState.Loading + || toolStorePagingItems.loadState.append is LoadState.Loading - Log.d("TAG", "ToolsScreen: ${toolStorePagingItems.loadState}") - ReportDrawnWhen { !isToolLoading } val itemsAvailable = toolStorePagingItems.itemCount @@ -62,18 +84,30 @@ internal fun ToolsScreen( val state = rememberLazyStaggeredGridState() val scrollbarState = state.scrollbarState(itemsAvailable = itemsAvailable) + val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition") + + val handleOnClickToolCard = { username: String, toolId: String -> + Toast.makeText(context, "$username:$toolId", Toast.LENGTH_LONG).show() + } + + LaunchedEffect(state.canScrollForward) { + handleOnCanScrollChange(state.canScrollForward) + } Box( modifier.fillMaxSize() ) { LazyVerticalStaggeredGrid( - columns = StaggeredGridCells.Adaptive(300.dp), + columns = StaggeredGridCells.Adaptive(160.dp), contentPadding = PaddingValues(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalItemSpacing = 24.dp, state = state ) { - toolsPanel(toolStorePagingItems = toolStorePagingItems) + toolsPanel( + toolStorePagingItems = toolStorePagingItems, + onClickToolCard = handleOnClickToolCard + ) item(span = StaggeredGridItemSpan.FullLine) { Spacer(modifier = Modifier.height(8.dp)) @@ -81,6 +115,29 @@ internal fun ToolsScreen( } } + if (toolStorePagingItems.loadState.refresh is LoadState.Loading || toolStorePagingItems.loadState.append is LoadState.Loading) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + val angle by infiniteTransition.animateFloat( + initialValue = 0F, + targetValue = 360F, + animationSpec = infiniteRepeatable( + animation = tween(800, easing = Ease), + ), label = "angle" + ) + Icon( + modifier = Modifier + .size(32.dp) + .graphicsLayer { rotationZ = angle }, + imageVector = OxygenIcons.Loading, + contentDescription = "" + ) + } + } + state.DraggableScrollbar( modifier = Modifier .fillMaxHeight() @@ -91,26 +148,4 @@ internal fun ToolsScreen( onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable) ) } -} - -/* -@OxygenPreviews -@Composable -fun ToolsScreenLoadingPreview() { - OxygenTheme { - ToolsScreen(toolsScreenUiState = ToolsScreenUiState.Loading) - } -} - -@OxygenPreviews -@Composable -fun ToolsScreenPreview() { - OxygenTheme { - ToolsScreen( - toolsScreenUiState = ToolsScreenUiState.Success( - runBlocking { - ToolDataSource().tool.first() - }) - ) - } -}*/ +} \ No newline at end of file diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreenViewModel.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreenViewModel.kt index 1faf51a..2fac814 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreenViewModel.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/tool/ToolsScreenViewModel.kt @@ -24,14 +24,12 @@ class ToolsScreenViewModel @Inject constructor( private val currentPage = savedStateHandle.getStateFlow(CURRENT_PAGE, 1) @OptIn(ExperimentalCoroutinesApi::class) - fun getStoreData(): Flow> { - return combine( - searchValue, - currentPage, - ::Pair - ).flatMapLatest { (searchValue, currentPage) -> - toolRepository.getStore(searchValue, currentPage).cachedIn(viewModelScope) - } + val storeData: Flow> = combine( + searchValue, + currentPage, + ::Pair + ).flatMapLatest { (searchValue, currentPage) -> + toolRepository.getStore(searchValue, currentPage).cachedIn(viewModelScope) } } diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index b173c10..b9133c2 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -9,11 +9,11 @@ 未知 网站 搜索 + 加载中… ⚠️ 无法连接至互联网 工具 收藏 设置 - 加载中… 语言 系统默认 启动页 @@ -34,4 +34,5 @@ 关于 更多 搜索 + 简介 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e2257a..6b64397 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,11 +8,11 @@ Unknown Website Search + Loading… ⚠️ Unable to connect to the internet Tools Star Settings - Loading… Language System Default 中文 @@ -35,4 +35,5 @@ About More Search + Desc \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 00d6322..a4247cc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,6 +29,7 @@ kotlinxSerializationJson = "1.6.3" retrofit = "2.9.0" retrofitKotlinxSerializationJson = "1.0.0" okhttp = "4.12.0" +androidsvg = "1.4" [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } @@ -89,3 +90,4 @@ retrofit-kotlin-serialization = { group = "com.jakewharton.retrofit", name = "re okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" } paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" } +androidsvg-aar = { group = "com.caverock", name = "androidsvg-aar", version.ref = "androidsvg" }