Feat(ToolScreen): Finish tool store list

This commit is contained in:
2024-05-11 03:26:02 +08:00
parent c596767c37
commit 3d8bc944e3
15 changed files with 437 additions and 57 deletions

View File

@@ -168,4 +168,5 @@ dependencies {
implementation(libs.okhttp.logging) implementation(libs.okhttp.logging)
implementation(libs.paging.runtime) implementation(libs.paging.runtime)
implementation(libs.paging.compose) implementation(libs.paging.compose)
implementation(libs.androidsvg.aar)
} }

View File

@@ -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!!
}

View File

@@ -1,5 +1,7 @@
package top.fatweb.oxygen.toolbox.icon 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.Icons
import androidx.compose.material.icons.filled.AccessTime import androidx.compose.material.icons.filled.AccessTime
import androidx.compose.material.icons.filled.Build 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.KeyboardArrowDown
import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Star 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 { object OxygenIcons {
val ArrowDown = Icons.Rounded.KeyboardArrowDown val ArrowDown = Icons.Rounded.KeyboardArrowDown
@@ -33,4 +41,23 @@ object OxygenIcons {
val StarBorder = Icons.Outlined.StarBorder val StarBorder = Icons.Outlined.StarBorder
val Time = Icons.Default.AccessTime val Time = Icons.Default.AccessTime
val Tool = Icons.Default.Build val Tool = Icons.Default.Build
}
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)
}

View File

@@ -10,7 +10,8 @@ fun OxygenNavHost(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
appState: OxygenAppState, appState: OxygenAppState,
onShowSnackbar: suspend (String, String?) -> Boolean, onShowSnackbar: suspend (String, String?) -> Boolean,
startDestination: String startDestination: String,
handleOnCanScrollChange: (Boolean) -> Unit
) { ) {
val navController = appState.navController val navController = appState.navController
NavHost( NavHost(
@@ -29,7 +30,8 @@ fun OxygenNavHost(
onBackClick = navController::popBackStack onBackClick = navController::popBackStack
) )
toolsScreen( toolsScreen(
onShowSnackbar = onShowSnackbar,
handleOnCanScrollChange = handleOnCanScrollChange
) )
starScreen( starScreen(

View File

@@ -10,10 +10,16 @@ const val TOOLS_ROUTE = "tools_route"
fun NavController.navigateToTools(navOptions: NavOptions) = navigate(TOOLS_ROUTE, navOptions) fun NavController.navigateToTools(navOptions: NavOptions) = navigate(TOOLS_ROUTE, navOptions)
fun NavGraphBuilder.toolsScreen() { fun NavGraphBuilder.toolsScreen(
onShowSnackbar: suspend (String, String?) -> Boolean,
handleOnCanScrollChange: (Boolean) -> Unit
) {
composable( composable(
route = TOOLS_ROUTE route = TOOLS_ROUTE
) { ) {
ToolsRoute() ToolsRoute(
onShowSnackbar = onShowSnackbar,
handleOnCanScrollChange = handleOnCanScrollChange
)
} }
} }

View File

@@ -78,7 +78,12 @@ fun OxygenApp(appState: OxygenAppState) {
val noConnectMessage = stringResource(R.string.core_no_connect) 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() val bottomAppBarScrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
LaunchedEffect(isOffline) { LaunchedEffect(isOffline) {
@@ -175,7 +180,8 @@ fun OxygenApp(appState: OxygenAppState) {
startDestination = when (appState.launchPageConfig) { startDestination = when (appState.launchPageConfig) {
LaunchPageConfig.TOOLS -> TOOLS_ROUTE LaunchPageConfig.TOOLS -> TOOLS_ROUTE
LaunchPageConfig.STAR -> STAR_ROUTE LaunchPageConfig.STAR -> STAR_ROUTE
} },
handleOnCanScrollChange = handleOnCanScrollChange
) )
} }
} }

View File

@@ -3,6 +3,11 @@ package top.fatweb.oxygen.toolbox.ui.about
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.activity.compose.ReportDrawnWhen 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.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsBottomHeight
@@ -33,6 +39,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -46,6 +53,7 @@ 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
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -54,6 +62,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import top.fatweb.oxygen.toolbox.R 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.icon.OxygenIcons
import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.DraggableScrollbar import top.fatweb.oxygen.toolbox.ui.component.scrollbar.DraggableScrollbar
@@ -105,6 +114,8 @@ internal fun LibrariesScreen(
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(canScroll = { state.canScrollForward }) val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(canScroll = { state.canScrollForward })
val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")
var activeSearch by remember { mutableStateOf(false) } var activeSearch by remember { mutableStateOf(false) }
var searchValue by remember { mutableStateOf("") } var searchValue by remember { mutableStateOf("") }
@@ -156,8 +167,26 @@ internal fun LibrariesScreen(
) )
) { ) {
when (librariesScreenUiState) { when (librariesScreenUiState) {
LibrariesScreenUiState.Loading -> { LibrariesScreenUiState.Loading -> {Column(
Text(text = stringResource(R.string.feature_settings_loading)) 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 -> { LibrariesScreenUiState.Nothing -> {

View File

@@ -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
)
}
}

View File

@@ -1,14 +1,22 @@
package top.fatweb.oxygen.toolbox.ui.settings package top.fatweb.oxygen.toolbox.ui.settings
import androidx.compose.animation.AnimatedVisibility 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.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@@ -26,6 +34,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -34,6 +43,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import top.fatweb.oxygen.toolbox.R 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.icon.OxygenIcons
import top.fatweb.oxygen.toolbox.model.userdata.DarkThemeConfig import top.fatweb.oxygen.toolbox.model.userdata.DarkThemeConfig
import top.fatweb.oxygen.toolbox.model.userdata.LanguageConfig import top.fatweb.oxygen.toolbox.model.userdata.LanguageConfig
@@ -82,6 +92,7 @@ fun SettingsDialog(
onNavigateToAbout: () -> Unit onNavigateToAbout: () -> Unit
) { ) {
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")
AlertDialog( AlertDialog(
modifier = modifier modifier = modifier
@@ -101,10 +112,26 @@ fun SettingsDialog(
) { ) {
when (settingsUiState) { when (settingsUiState) {
SettingsUiState.Loading -> { SettingsUiState.Loading -> {
Text( Column(
modifier = Modifier.padding(vertical = 16.dp), modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.feature_settings_loading) 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 -> { is SettingsUiState.Success -> {

View File

@@ -2,17 +2,21 @@ package top.fatweb.oxygen.toolbox.ui.tool
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope
import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.material3.Text
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import top.fatweb.oxygen.toolbox.model.tool.Tool import top.fatweb.oxygen.toolbox.model.tool.Tool
import top.fatweb.oxygen.toolbox.ui.component.ToolCard
fun LazyStaggeredGridScope.toolsPanel( fun LazyStaggeredGridScope.toolsPanel(
toolStorePagingItems: LazyPagingItems<Tool> toolStorePagingItems: LazyPagingItems<Tool>,
onClickToolCard: (username: String, toolId: String) -> Unit
) { ) {
items( items(
items = toolStorePagingItems.itemSnapshotList, items = toolStorePagingItems.itemSnapshotList,
key = { it!!.id }, key = { it!!.id },
) { ) {
Text(text = it!!.name) ToolCard(
tool = it!!,
onClickToolCard = {onClickToolCard(it.author.username, it.toolId)}
)
} }
} }

View File

@@ -1,18 +1,26 @@
package top.fatweb.oxygen.toolbox.ui.tool package top.fatweb.oxygen.toolbox.ui.tool
import android.util.Log import android.widget.Toast
import androidx.activity.compose.ReportDrawnWhen 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.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
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
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsPadding 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.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems 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.model.tool.Tool
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.DraggableScrollbar import top.fatweb.oxygen.toolbox.ui.component.scrollbar.DraggableScrollbar
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.rememberDraggableScroller import top.fatweb.oxygen.toolbox.ui.component.scrollbar.rememberDraggableScroller
@@ -36,12 +51,16 @@ import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
@Composable @Composable
internal fun ToolsRoute( internal fun ToolsRoute(
modifier: Modifier = Modifier, 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( ToolsScreen(
modifier = modifier, modifier = modifier,
onShowSnackbar = onShowSnackbar,
handleOnCanScrollChange = handleOnCanScrollChange,
toolStorePagingItems = toolStorePagingItems toolStorePagingItems = toolStorePagingItems
) )
} }
@@ -49,12 +68,15 @@ internal fun ToolsRoute(
@Composable @Composable
internal fun ToolsScreen( internal fun ToolsScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onShowSnackbar: suspend (String, String?) -> Boolean,
handleOnCanScrollChange: (Boolean) -> Unit,
toolStorePagingItems: LazyPagingItems<Tool> toolStorePagingItems: LazyPagingItems<Tool>
) { ) {
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 } ReportDrawnWhen { !isToolLoading }
val itemsAvailable = toolStorePagingItems.itemCount val itemsAvailable = toolStorePagingItems.itemCount
@@ -62,18 +84,30 @@ internal fun ToolsScreen(
val state = rememberLazyStaggeredGridState() val state = rememberLazyStaggeredGridState()
val scrollbarState = state.scrollbarState(itemsAvailable = itemsAvailable) 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( Box(
modifier.fillMaxSize() modifier.fillMaxSize()
) { ) {
LazyVerticalStaggeredGrid( LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Adaptive(300.dp), columns = StaggeredGridCells.Adaptive(160.dp),
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalItemSpacing = 24.dp, verticalItemSpacing = 24.dp,
state = state state = state
) { ) {
toolsPanel(toolStorePagingItems = toolStorePagingItems) toolsPanel(
toolStorePagingItems = toolStorePagingItems,
onClickToolCard = handleOnClickToolCard
)
item(span = StaggeredGridItemSpan.FullLine) { item(span = StaggeredGridItemSpan.FullLine) {
Spacer(modifier = Modifier.height(8.dp)) 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( state.DraggableScrollbar(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
@@ -91,26 +148,4 @@ internal fun ToolsScreen(
onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable) 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()
})
)
}
}*/

View File

@@ -24,14 +24,12 @@ class ToolsScreenViewModel @Inject constructor(
private val currentPage = savedStateHandle.getStateFlow(CURRENT_PAGE, 1) private val currentPage = savedStateHandle.getStateFlow(CURRENT_PAGE, 1)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun getStoreData(): Flow<PagingData<Tool>> { val storeData: Flow<PagingData<Tool>> = combine(
return combine( searchValue,
searchValue, currentPage,
currentPage, ::Pair
::Pair ).flatMapLatest { (searchValue, currentPage) ->
).flatMapLatest { (searchValue, currentPage) -> toolRepository.getStore(searchValue, currentPage).cachedIn(viewModelScope)
toolRepository.getStore(searchValue, currentPage).cachedIn(viewModelScope)
}
} }
} }

View File

@@ -9,11 +9,11 @@
<string name="core_unknown">未知</string> <string name="core_unknown">未知</string>
<string name="core_website">网站</string> <string name="core_website">网站</string>
<string name="core_search">搜索</string> <string name="core_search">搜索</string>
<string name="core_loading">加载中…</string>
<string name="core_no_connect">⚠️ 无法连接至互联网</string> <string name="core_no_connect">⚠️ 无法连接至互联网</string>
<string name="feature_tools_title">工具</string> <string name="feature_tools_title">工具</string>
<string name="feature_star_title">收藏</string> <string name="feature_star_title">收藏</string>
<string name="feature_settings_title">设置</string> <string name="feature_settings_title">设置</string>
<string name="feature_settings_loading">加载中…</string>
<string name="feature_settings_language">语言</string> <string name="feature_settings_language">语言</string>
<string name="feature_settings_language_system_default">系统默认</string> <string name="feature_settings_language_system_default">系统默认</string>
<string name="feature_settings_launch_page">启动页</string> <string name="feature_settings_launch_page">启动页</string>
@@ -34,4 +34,5 @@
<string name="feature_settings_more_about">关于</string> <string name="feature_settings_more_about">关于</string>
<string name="feature_settings_top_app_bar_action_icon_description">更多</string> <string name="feature_settings_top_app_bar_action_icon_description">更多</string>
<string name="feature_settings_top_app_bar_navigation_icon_description">搜索</string> <string name="feature_settings_top_app_bar_navigation_icon_description">搜索</string>
<string name="feature_tools_description">简介</string>
</resources> </resources>

View File

@@ -8,11 +8,11 @@
<string name="core_unknown">Unknown</string> <string name="core_unknown">Unknown</string>
<string name="core_website">Website</string> <string name="core_website">Website</string>
<string name="core_search">Search</string> <string name="core_search">Search</string>
<string name="core_loading">Loading…</string>
<string name="core_no_connect">⚠️ Unable to connect to the internet</string> <string name="core_no_connect">⚠️ Unable to connect to the internet</string>
<string name="feature_tools_title">Tools</string> <string name="feature_tools_title">Tools</string>
<string name="feature_star_title">Star</string> <string name="feature_star_title">Star</string>
<string name="feature_settings_title">Settings</string> <string name="feature_settings_title">Settings</string>
<string name="feature_settings_loading">Loading…</string>
<string name="feature_settings_language">Language</string> <string name="feature_settings_language">Language</string>
<string name="feature_settings_language_system_default">System Default</string> <string name="feature_settings_language_system_default">System Default</string>
<string name="feature_settings_language_chinese" translatable="false">中文</string> <string name="feature_settings_language_chinese" translatable="false">中文</string>
@@ -35,4 +35,5 @@
<string name="feature_settings_more_about">About</string> <string name="feature_settings_more_about">About</string>
<string name="feature_settings_top_app_bar_action_icon_description">More</string> <string name="feature_settings_top_app_bar_action_icon_description">More</string>
<string name="feature_settings_top_app_bar_navigation_icon_description">Search</string> <string name="feature_settings_top_app_bar_navigation_icon_description">Search</string>
<string name="feature_tools_description">Desc</string>
</resources> </resources>

View File

@@ -29,6 +29,7 @@ kotlinxSerializationJson = "1.6.3"
retrofit = "2.9.0" retrofit = "2.9.0"
retrofitKotlinxSerializationJson = "1.0.0" retrofitKotlinxSerializationJson = "1.0.0"
okhttp = "4.12.0" okhttp = "4.12.0"
androidsvg = "1.4"
[plugins] [plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" } 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" } okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" } paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" }
paging-compose = { group = "androidx.paging", name = "paging-compose", 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" }