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:
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user