Refactor(ToolStore): Optimize user experience

Added prompts for errors when loading more and errors when reloading. Add autoload next page.
This commit is contained in:
2024-08-28 15:14:42 +08:00
parent cead8fe91e
commit 253d186fdf
6 changed files with 148 additions and 52 deletions

View File

@@ -48,7 +48,7 @@ internal class ToolStorePagingSource(
} }
} ?: toolEntity } ?: toolEntity
}, },
prevKey = if (currentPage == 0) null else currentPage - 1, prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (currentPage < pages) currentPage + 1 else null nextKey = if (currentPage < pages) currentPage + 1 else null
) )
} catch (e: Throwable) { } catch (e: Throwable) {

View File

@@ -0,0 +1,42 @@
package top.fatweb.oxygen.toolbox.ui.component
import androidx.annotation.StringRes
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import top.fatweb.oxygen.toolbox.ui.util.ResourcesUtils
@Composable
fun ClickableText(
@StringRes text: Int,
@StringRes replaceText: Int,
onClick: (Int) -> Unit
) {
val context = LocalContext.current
val primaryColor = MaterialTheme.colorScheme.primary
val annotatedString = buildAnnotatedString {
val clickablePart = ResourcesUtils.getString(
context = context,
resId = replaceText
)
val mainText = ResourcesUtils.getString(
context = context,
resId = text,
clickablePart
)
append(mainText.substringBefore(clickablePart))
pushStringAnnotation(tag = "Click", annotation = clickablePart)
withStyle(style = SpanStyle(color = primaryColor)) {
append(clickablePart)
}
pop()
append(mainText.substringAfter(clickablePart))
}
ClickableText(text = annotatedString, onClick = onClick)
}

View File

@@ -1,5 +1,6 @@
package top.fatweb.oxygen.toolbox.ui.store package top.fatweb.oxygen.toolbox.ui.store
import android.widget.Toast
import androidx.activity.compose.ReportDrawnWhen import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -23,7 +24,6 @@ import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid 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.items
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@@ -46,6 +46,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@@ -55,10 +56,14 @@ import androidx.paging.compose.collectAsLazyPagingItems
import top.fatweb.oxygen.toolbox.R import top.fatweb.oxygen.toolbox.R
import top.fatweb.oxygen.toolbox.icon.OxygenIcons import top.fatweb.oxygen.toolbox.icon.OxygenIcons
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
import top.fatweb.oxygen.toolbox.ui.component.ClickableText
import top.fatweb.oxygen.toolbox.ui.component.DEFAULT_TOOL_CARD_SKELETON_COUNT
import top.fatweb.oxygen.toolbox.ui.component.ToolCard import top.fatweb.oxygen.toolbox.ui.component.ToolCard
import top.fatweb.oxygen.toolbox.ui.component.ToolCardSkeleton
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
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
import top.fatweb.oxygen.toolbox.ui.util.ResourcesUtils
@Composable @Composable
internal fun ToolStoreRoute( internal fun ToolStoreRoute(
@@ -101,9 +106,11 @@ internal fun ToolStoreScreen(
onChangeInstallType: (type: ToolStoreUiState.InstallInfo.Type) -> Unit, onChangeInstallType: (type: ToolStoreUiState.InstallInfo.Type) -> Unit,
onInstallTool: (installTool: ToolEntity) -> Unit onInstallTool: (installTool: ToolEntity) -> Unit
) { ) {
val context = LocalContext.current
val isToolLoading = val isToolLoading =
toolStorePagingItems.loadState.refresh is LoadState.Loading toolStorePagingItems.loadState.refresh == LoadState.Loading
|| toolStorePagingItems.loadState.append is LoadState.Loading || toolStorePagingItems.loadState.append == LoadState.Loading
ReportDrawnWhen { !isToolLoading } ReportDrawnWhen { !isToolLoading }
@@ -114,11 +121,19 @@ internal fun ToolStoreScreen(
} }
} }
LaunchedEffect(toolStorePagingItems.loadState.refresh) { LaunchedEffect(toolStorePagingItems.loadState.refresh) {
if (toolStorePagingItems.loadState.refresh is LoadState.Loading) { if (toolStorePagingItems.loadState.refresh != LoadState.Loading) {
pullToRefreshState.startRefresh()
} else {
pullToRefreshState.endRefresh() pullToRefreshState.endRefresh()
} }
if (toolStorePagingItems.loadState.refresh is LoadState.Error) {
Toast.makeText(
context,
ResourcesUtils.getString(
context = context,
resId = R.string.feature_store_reload_error
),
Toast.LENGTH_LONG
).show()
}
} }
val itemsAvailable = toolStorePagingItems.itemCount val itemsAvailable = toolStorePagingItems.itemCount
@@ -142,39 +157,55 @@ internal fun ToolStoreScreen(
.clipToBounds() .clipToBounds()
.nestedScroll(pullToRefreshState.nestedScrollConnection) .nestedScroll(pullToRefreshState.nestedScrollConnection)
) { ) {
LazyVerticalStaggeredGrid( if (itemsAvailable > 0 || (toolStorePagingItems.loadState.refresh == LoadState.Loading && itemsAvailable == 0)) {
modifier = Modifier LazyVerticalStaggeredGrid(
.fillMaxSize(), modifier = Modifier
columns = StaggeredGridCells.Adaptive(160.dp), .fillMaxSize(),
contentPadding = PaddingValues(16.dp), columns = StaggeredGridCells.Adaptive(160.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(16.dp),
verticalItemSpacing = 24.dp, horizontalArrangement = Arrangement.spacedBy(16.dp),
state = state verticalItemSpacing = 24.dp,
) { state = state
toolsPanel( ) {
toolStorePagingItems = toolStorePagingItems, if (itemsAvailable > 0) {
onAction = { tool, installType -> toolsPanel(
installTool = tool toolStorePagingItems = toolStorePagingItems,
onChangeInstallStatus(ToolStoreUiState.InstallInfo.Status.Pending) onAction = { tool, installType ->
onChangeInstallType(installType) installTool = tool
}, onChangeInstallStatus(ToolStoreUiState.InstallInfo.Status.Pending)
onClick = { onChangeInstallType(installType)
onNavigateToToolView(it.authorUsername, it.toolId, it.upgrade != null) },
onClick = {
onNavigateToToolView(it.authorUsername, it.toolId, it.upgrade != null)
}
)
}
if (itemsAvailable == 0 || toolStorePagingItems.loadState.append == LoadState.Loading) {
items(count = DEFAULT_TOOL_CARD_SKELETON_COUNT) {
ToolCardSkeleton()
}
}
if (toolStorePagingItems.loadState.append is LoadState.Error) {
item(span = StaggeredGridItemSpan.FullLine) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
ClickableText(
text = R.string.feature_store_load_more_error,
replaceText = R.string.feature_store_retry
) {
toolStorePagingItems.retry()
}
}
}
}
item(span = StaggeredGridItemSpan.FullLine) {
Spacer(Modifier.height(8.dp))
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
} }
)
item(span = StaggeredGridItemSpan.FullLine) {
Spacer(Modifier.height(8.dp))
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
} }
} }
PullToRefreshContainer(
modifier = Modifier
.align(Alignment.TopCenter),
state = pullToRefreshState,
)
if (itemsAvailable == 0 && !isToolLoading) { if (itemsAvailable == 0 && !isToolLoading) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -183,15 +214,30 @@ internal fun ToolStoreScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Text( if (toolStorePagingItems.loadState.refresh is LoadState.Error) {
text = stringResource( ClickableText(
if (searchValue.isEmpty()) R.string.core_nothing text = R.string.feature_store_load_error,
else R.string.core_nothing_found replaceText = R.string.feature_store_retry
) {
toolStorePagingItems.refresh()
}
} else {
Text(
text = stringResource(
if (searchValue.isEmpty()) R.string.core_nothing
else R.string.core_nothing_found
)
) )
) }
} }
} }
PullToRefreshContainer(
modifier = Modifier
.align(Alignment.TopCenter),
state = pullToRefreshState,
)
state.DraggableScrollbar( state.DraggableScrollbar(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
@@ -218,21 +264,21 @@ private fun LazyStaggeredGridScope.toolsPanel(
onClick: (ToolEntity) -> Unit onClick: (ToolEntity) -> Unit
) { ) {
items( items(
items = toolStorePagingItems.itemSnapshotList, count = toolStorePagingItems.itemCount
key = { it!!.id },
) { ) {
val item = toolStorePagingItems[it]!!
ToolCard( ToolCard(
tool = it!!, tool = item,
specifyVer = it.upgrade, specifyVer = item.upgrade,
actionIcon = if (it.upgrade != null) OxygenIcons.Upgrade else if (!it.isInstalled) OxygenIcons.Download else null, actionIcon = if (item.upgrade != null) OxygenIcons.Upgrade else if (!item.isInstalled) OxygenIcons.Download else null,
actionIconContentDescription = stringResource(R.string.core_install), actionIconContentDescription = stringResource(R.string.core_install),
onAction = { onAction = {
onAction( onAction(
it, item,
if (it.upgrade != null) ToolStoreUiState.InstallInfo.Type.Upgrade else ToolStoreUiState.InstallInfo.Type.Install if (item.upgrade != null) ToolStoreUiState.InstallInfo.Type.Upgrade else ToolStoreUiState.InstallInfo.Type.Install
) )
}, },
onClick = { onClick(it) } onClick = { onClick(item) }
) )
} }
} }

View File

@@ -42,6 +42,6 @@ object ResourcesUtils {
-1 -1
} }
fun getString(context: Context, @StringRes resId: Int): String = fun getString(context: Context, @StringRes resId: Int, vararg formatArgs: Any): String =
context.resources.getString(resId) context.resources.getString(resId, *formatArgs)
} }

View File

@@ -26,6 +26,10 @@
<string name="core_nothing_found">未找到相关内容</string> <string name="core_nothing_found">未找到相关内容</string>
<string name="feature_store_title">商店</string> <string name="feature_store_title">商店</string>
<string name="feature_store_retry">重试</string>
<string name="feature_store_load_error">⚠️ 无法加载商店内容,请稍后%1$s……</string>
<string name="feature_store_reload_error">⚠️ 重载失败,请稍后重试……</string>
<string name="feature_store_load_more_error">⚠️ 无法载入更多内容,请稍后%1$s……</string>
<string name="feature_store_install_tool">安装工具</string> <string name="feature_store_install_tool">安装工具</string>
<string name="feature_store_ask_install">确定安装由用户 %1$s 提供的工具 %2$s 吗?</string> <string name="feature_store_ask_install">确定安装由用户 %1$s 提供的工具 %2$s 吗?</string>
<string name="feature_store_install_success">安装成功</string> <string name="feature_store_install_success">安装成功</string>

View File

@@ -25,6 +25,10 @@
<string name="core_nothing_found">Nothing found</string> <string name="core_nothing_found">Nothing found</string>
<string name="feature_store_title">Store</string> <string name="feature_store_title">Store</string>
<string name="feature_store_retry">try again</string>
<string name="feature_store_load_error">⚠️ Unable to load store content, please %1$s later…</string>
<string name="feature_store_reload_error">⚠️ Reload failed, please try again later…</string>
<string name="feature_store_load_more_error">⚠️ Unable to load more content, please %1$s later…</string>
<string name="feature_store_install_tool">Install Tool</string> <string name="feature_store_install_tool">Install Tool</string>
<string name="feature_store_ask_install">Are you sure you want to install tool %1$s provided by user %2$s?</string> <string name="feature_store_ask_install">Are you sure you want to install tool %1$s provided by user %2$s?</string>
<string name="feature_store_install_success">Install Success</string> <string name="feature_store_install_success">Install Success</string>
@@ -40,7 +44,7 @@
<string name="feature_tools_title">Tools</string> <string name="feature_tools_title">Tools</string>
<string name="feature_tools_description">Desc</string> <string name="feature_tools_description">Desc</string>
<string name="feature_tools_can_not_open">⚠️ Can not open the tool</string> <string name="feature_tools_can_not_open">⚠️ Unable to open the tool</string>
<string name="feature_tools_no_tools_installed">No tools installed yet</string> <string name="feature_tools_no_tools_installed">No tools installed yet</string>
<string name="feature_tools_go_to_store">Go to store…</string> <string name="feature_tools_go_to_store">Go to store…</string>