Feat(ToolScreen): Support uninstall tool

This commit is contained in:
2024-08-12 14:37:18 +08:00
parent cb6fe19033
commit 454108d871
8 changed files with 125 additions and 20 deletions

View File

@@ -8,8 +8,8 @@ import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Inbox import androidx.compose.material.icons.filled.Inbox
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Reorder import androidx.compose.material.icons.filled.Reorder
@@ -39,6 +39,7 @@ object OxygenIcons {
val Box = Icons.Default.Inbox val Box = Icons.Default.Inbox
val Close = Icons.Default.Close val Close = Icons.Default.Close
val Code = Icons.Default.Code val Code = Icons.Default.Code
val Delete = Icons.Default.Delete
val Download = Icons.Default.Download val Download = Icons.Default.Download
val Error = Icons.Default.Cancel val Error = Icons.Default.Cancel
val Home = Icons.Rounded.Home val Home = Icons.Rounded.Home

View File

@@ -9,7 +9,7 @@ import top.fatweb.oxygen.toolbox.ui.OxygenAppState
fun OxygenNavHost( fun OxygenNavHost(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
appState: OxygenAppState, appState: OxygenAppState,
onShowSnackbar: suspend (String, String?) -> Boolean, onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
startDestination: String startDestination: String
) { ) {
val navController = appState.navController val navController = appState.navController
@@ -32,6 +32,7 @@ fun OxygenNavHost(
onNavigateToToolView = navController::navigateToToolView onNavigateToToolView = navController::navigateToToolView
) )
toolsScreen( toolsScreen(
onShowSnackbar = onShowSnackbar,
onNavigateToToolView = navController::navigateToToolView, onNavigateToToolView = navController::navigateToToolView,
onNavigateToToolStore = { appState.navigateToTopLevelDestination(TopLevelDestination.TOOL_STORE) } onNavigateToToolStore = { appState.navigateToTopLevelDestination(TopLevelDestination.TOOL_STORE) }
) )

View File

@@ -11,6 +11,7 @@ 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 (message: String, action: String?) -> Boolean,
onNavigateToToolView: (username: String, toolId: String) -> Unit, onNavigateToToolView: (username: String, toolId: String) -> Unit,
onNavigateToToolStore: () -> Unit onNavigateToToolStore: () -> Unit
) { ) {
@@ -18,6 +19,7 @@ fun NavGraphBuilder.toolsScreen(
route = TOOLS_ROUTE route = TOOLS_ROUTE
) { ) {
ToolsRoute( ToolsRoute(
onShowSnackbar = onShowSnackbar,
onNavigateToToolView = onNavigateToToolView, onNavigateToToolView = onNavigateToToolView,
onNavigateToToolStore = onNavigateToToolStore onNavigateToToolStore = onNavigateToToolStore
) )

View File

@@ -29,31 +29,45 @@ 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.items
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
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
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
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.Loading
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.DialogClickerRow
import top.fatweb.oxygen.toolbox.ui.component.DialogSectionGroup
import top.fatweb.oxygen.toolbox.ui.component.DialogTitle
import top.fatweb.oxygen.toolbox.ui.component.ToolCard import top.fatweb.oxygen.toolbox.ui.component.ToolCard
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 ToolsRoute( internal fun ToolsRoute(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: ToolsScreenViewModel = hiltViewModel(), viewModel: ToolsScreenViewModel = hiltViewModel(),
onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
onNavigateToToolView: (username: String, toolId: String) -> Unit, onNavigateToToolView: (username: String, toolId: String) -> Unit,
onNavigateToToolStore: () -> Unit onNavigateToToolStore: () -> Unit
) { ) {
@@ -61,19 +75,29 @@ internal fun ToolsRoute(
ToolsScreen( ToolsScreen(
modifier = modifier, modifier = modifier,
onShowSnackbar = onShowSnackbar,
onNavigateToToolView = onNavigateToToolView, onNavigateToToolView = onNavigateToToolView,
onNavigateToToolStore = onNavigateToToolStore, onNavigateToToolStore = onNavigateToToolStore,
toolsScreenUiState = toolsScreenUiStateState toolsScreenUiState = toolsScreenUiStateState,
onUninstall = viewModel::uninstall,
onUndo = viewModel::undo
) )
} }
@Composable @Composable
internal fun ToolsScreen( internal fun ToolsScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
onNavigateToToolView: (username: String, toolId: String) -> Unit, onNavigateToToolView: (username: String, toolId: String) -> Unit,
onNavigateToToolStore: () -> Unit, onNavigateToToolStore: () -> Unit,
toolsScreenUiState: ToolsScreenUiState toolsScreenUiState: ToolsScreenUiState,
onUninstall: (ToolEntity) -> Unit,
onUndo: (ToolEntity) -> Unit
) { ) {
val localContext = LocalContext.current
val scope = rememberCoroutineScope()
ReportDrawnWhen { toolsScreenUiState !is ToolsScreenUiState.Loading } ReportDrawnWhen { toolsScreenUiState !is ToolsScreenUiState.Loading }
val itemsAvailable = howManyTools(toolsScreenUiState) val itemsAvailable = howManyTools(toolsScreenUiState)
@@ -83,6 +107,9 @@ internal fun ToolsScreen(
val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition") val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")
var selectedTool by remember { mutableStateOf<ToolEntity?>(null) }
var isShowMenu by remember { mutableStateOf(true) }
Box( Box(
modifier.fillMaxSize() modifier.fillMaxSize()
) { ) {
@@ -95,9 +122,7 @@ internal fun ToolsScreen(
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
val angle by infiniteTransition.animateFloat( val angle by infiniteTransition.animateFloat(
initialValue = 0F, initialValue = 0F, targetValue = 360F, animationSpec = infiniteRepeatable(
targetValue = 360F,
animationSpec = infiniteRepeatable(
animation = tween(800, easing = Ease), animation = tween(800, easing = Ease),
), label = "angle" ), label = "angle"
) )
@@ -110,6 +135,7 @@ internal fun ToolsScreen(
) )
} }
} }
ToolsScreenUiState.Nothing -> { ToolsScreenUiState.Nothing -> {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -122,6 +148,7 @@ internal fun ToolsScreen(
} }
} }
} }
is ToolsScreenUiState.Success -> { is ToolsScreenUiState.Success -> {
LazyVerticalStaggeredGrid( LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Adaptive(160.dp), columns = StaggeredGridCells.Adaptive(160.dp),
@@ -131,10 +158,12 @@ internal fun ToolsScreen(
state = state state = state
) { ) {
toolsPanel( toolsPanel(toolItems = toolsScreenUiState.tools,
toolItems = toolsScreenUiState.tools, onClick = onNavigateToToolView,
onClickToolCard = onNavigateToToolView onLongClick = {
) selectedTool = it
isShowMenu = true
})
item(span = StaggeredGridItemSpan.FullLine) { item(span = StaggeredGridItemSpan.FullLine) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@@ -150,27 +179,74 @@ internal fun ToolsScreen(
.windowInsetsPadding(WindowInsets.systemBars) .windowInsetsPadding(WindowInsets.systemBars)
.padding(horizontal = 2.dp) .padding(horizontal = 2.dp)
.align(Alignment.CenterEnd), .align(Alignment.CenterEnd),
state = scrollbarState, orientation = Orientation.Vertical, state = scrollbarState,
orientation = Orientation.Vertical,
onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable) onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable)
) )
} }
if (isShowMenu && selectedTool != null) {
ToolMenu(
onDismiss = { isShowMenu = false },
selectedTool = selectedTool!!,
onUninstall = {
isShowMenu = false
onUninstall(selectedTool!!)
scope.launch {
if (onShowSnackbar(
ResourcesUtils.getString(localContext, R.string.core_uninstall_success),
ResourcesUtils.getString(localContext, R.string.core_undo)
)
) {
onUndo(selectedTool!!)
}
}
}
)
}
} }
private fun LazyStaggeredGridScope.toolsPanel( private fun LazyStaggeredGridScope.toolsPanel(
toolItems: List<ToolEntity>, toolItems: List<ToolEntity>,
onClickToolCard: (username: String, toolId: String) -> Unit onClick: (username: String, toolId: String) -> Unit,
onLongClick: (ToolEntity) -> Unit
) { ) {
items( items(
items = toolItems, items = toolItems,
key = { it.id }, key = { it.id },
) { ) {
ToolCard( ToolCard(tool = it,
tool = it, onClick = { onClick(it.authorUsername, it.toolId) },
onClick = {onClickToolCard(it.authorUsername, it.toolId)}, onLongClick = { onLongClick(it) })
onLongClick = {onClickToolCard(it.authorUsername, it.toolId)} }
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ToolMenu(
modifier: Modifier = Modifier,
onDismiss: () -> Unit,
selectedTool: ToolEntity,
onUninstall: () -> Unit
) {
ModalBottomSheet(onDismissRequest = onDismiss, dragHandle = {}) {
Column(
modifier = modifier.padding(16.dp)
) {
DialogTitle(text = selectedTool.name)
HorizontalDivider()
Spacer(modifier = Modifier.height(4.dp))
DialogSectionGroup {
DialogClickerRow(
icon = OxygenIcons.Delete,
text = stringResource(R.string.core_uninstall),
onClick = onUninstall
) )
} }
} }
}
}
@Composable @Composable
private fun howManyTools(toolsScreenUiState: ToolsScreenUiState) = private fun howManyTools(toolsScreenUiState: ToolsScreenUiState) =

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
import top.fatweb.oxygen.toolbox.repository.tool.StoreRepository import top.fatweb.oxygen.toolbox.repository.tool.StoreRepository
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
@@ -17,7 +18,7 @@ import kotlin.time.Duration.Companion.seconds
@HiltViewModel @HiltViewModel
class ToolsScreenViewModel @Inject constructor( class ToolsScreenViewModel @Inject constructor(
private val storeRepository: StoreRepository, private val storeRepository: StoreRepository,
toolRepository: ToolRepository, private val toolRepository: ToolRepository,
savedStateHandle: SavedStateHandle savedStateHandle: SavedStateHandle
) : ViewModel() { ) : ViewModel() {
private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "") private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "")
@@ -37,6 +38,17 @@ class ToolsScreenViewModel @Inject constructor(
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds) started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
) )
fun uninstall(tool: ToolEntity) {
viewModelScope.launch {
toolRepository.removeTool(tool)
}
}
fun undo(tool: ToolEntity) {
viewModelScope.launch {
toolRepository.saveTool(tool)
}
}
} }
sealed interface ToolsScreenUiState { sealed interface ToolsScreenUiState {

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.os.Build import android.os.Build
import androidx.annotation.StringRes
import androidx.core.os.ConfigurationCompat import androidx.core.os.ConfigurationCompat
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import java.util.Locale import java.util.Locale
@@ -33,8 +34,14 @@ object ResourcesUtils {
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
context.packageManager.getPackageInfo(context.packageName, 0)?.longVersionCode ?: -1 context.packageManager.getPackageInfo(context.packageName, 0)?.longVersionCode ?: -1
else context.packageManager.getPackageInfo(context.packageName, 0)?.versionCode?.toLong() ?: -1 else context.packageManager.getPackageInfo(
context.packageName,
0
)?.versionCode?.toLong() ?: -1
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
-1 -1
} }
fun getString(context: Context, @StringRes resId: Int): String =
context.resources.getString(resId)
} }

View File

@@ -14,7 +14,10 @@
<string name="core_no_connect">⚠️ 无法连接至互联网</string> <string name="core_no_connect">⚠️ 无法连接至互联网</string>
<string name="core_install">安装</string> <string name="core_install">安装</string>
<string name="core_installing">安装中……</string> <string name="core_installing">安装中……</string>
<string name="core_uninstall">卸载</string>
<string name="core_uninstall_success">卸载成功</string>
<string name="core_cancel">取消</string> <string name="core_cancel">取消</string>
<string name="core_undo">撤消</string>
<string name="feature_store_title">商店</string> <string name="feature_store_title">商店</string>
<string name="feature_store_install_tool">安装工具</string> <string name="feature_store_install_tool">安装工具</string>

View File

@@ -13,7 +13,10 @@
<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="core_install">Install</string> <string name="core_install">Install</string>
<string name="core_installing">Installing…</string> <string name="core_installing">Installing…</string>
<string name="core_uninstall">Uninstall</string>
<string name="core_uninstall_success">Uninstalled successfully</string>
<string name="core_cancel">Cancel</string> <string name="core_cancel">Cancel</string>
<string name="core_undo">Undo</string>
<string name="feature_store_title">Store</string> <string name="feature_store_title">Store</string>
<string name="feature_store_install_tool">Install Tool</string> <string name="feature_store_install_tool">Install Tool</string>