From 454108d87139b1c8986cba12dfedeef943f66f2a Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Mon, 12 Aug 2024 14:37:18 +0800 Subject: [PATCH] Feat(ToolScreen): Support uninstall tool --- .../fatweb/oxygen/toolbox/icon/OxygenIcons.kt | 3 +- .../toolbox/navigation/OxygenNavHost.kt | 3 +- .../toolbox/navigation/ToolsNavigation.kt | 2 + .../oxygen/toolbox/ui/tool/ToolsScreen.kt | 108 +++++++++++++++--- .../toolbox/ui/tool/ToolsScreenViewModel.kt | 14 ++- .../oxygen/toolbox/ui/util/ResourcesUtils.kt | 9 +- app/src/main/res/values-zh/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 8 files changed, 125 insertions(+), 20 deletions(-) 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 9f39cd2..8744fe8 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 @@ -8,8 +8,8 @@ import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Close 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.Error import androidx.compose.material.icons.filled.Inbox import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Reorder @@ -39,6 +39,7 @@ object OxygenIcons { val Box = Icons.Default.Inbox val Close = Icons.Default.Close val Code = Icons.Default.Code + val Delete = Icons.Default.Delete val Download = Icons.Default.Download val Error = Icons.Default.Cancel val Home = Icons.Rounded.Home 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 71cd8f7..2e0b1de 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 @@ -9,7 +9,7 @@ import top.fatweb.oxygen.toolbox.ui.OxygenAppState fun OxygenNavHost( modifier: Modifier = Modifier, appState: OxygenAppState, - onShowSnackbar: suspend (String, String?) -> Boolean, + onShowSnackbar: suspend (message: String, action: String?) -> Boolean, startDestination: String ) { val navController = appState.navController @@ -32,6 +32,7 @@ fun OxygenNavHost( onNavigateToToolView = navController::navigateToToolView ) toolsScreen( + onShowSnackbar = onShowSnackbar, onNavigateToToolView = navController::navigateToToolView, onNavigateToToolStore = { appState.navigateToTopLevelDestination(TopLevelDestination.TOOL_STORE) } ) 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 fd127f7..646b240 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 @@ -11,6 +11,7 @@ const val TOOLS_ROUTE = "tools_route" fun NavController.navigateToTools(navOptions: NavOptions) = navigate(TOOLS_ROUTE, navOptions) fun NavGraphBuilder.toolsScreen( + onShowSnackbar: suspend (message: String, action: String?) -> Boolean, onNavigateToToolView: (username: String, toolId: String) -> Unit, onNavigateToToolStore: () -> Unit ) { @@ -18,6 +19,7 @@ fun NavGraphBuilder.toolsScreen( route = TOOLS_ROUTE ) { ToolsRoute( + onShowSnackbar = onShowSnackbar, onNavigateToToolView = onNavigateToToolView, onNavigateToToolStore = onNavigateToToolStore ) 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 98fea97..18207d8 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 @@ -29,31 +29,45 @@ import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan import androidx.compose.foundation.lazy.staggeredgrid.items 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.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable 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.Modifier import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch 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.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.scrollbar.DraggableScrollbar import top.fatweb.oxygen.toolbox.ui.component.scrollbar.rememberDraggableScroller import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState +import top.fatweb.oxygen.toolbox.ui.util.ResourcesUtils @Composable internal fun ToolsRoute( modifier: Modifier = Modifier, viewModel: ToolsScreenViewModel = hiltViewModel(), + onShowSnackbar: suspend (message: String, action: String?) -> Boolean, onNavigateToToolView: (username: String, toolId: String) -> Unit, onNavigateToToolStore: () -> Unit ) { @@ -61,19 +75,29 @@ internal fun ToolsRoute( ToolsScreen( modifier = modifier, + onShowSnackbar = onShowSnackbar, onNavigateToToolView = onNavigateToToolView, onNavigateToToolStore = onNavigateToToolStore, - toolsScreenUiState = toolsScreenUiStateState + toolsScreenUiState = toolsScreenUiStateState, + onUninstall = viewModel::uninstall, + onUndo = viewModel::undo ) } @Composable internal fun ToolsScreen( modifier: Modifier = Modifier, + onShowSnackbar: suspend (message: String, action: String?) -> Boolean, onNavigateToToolView: (username: String, toolId: String) -> 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 } val itemsAvailable = howManyTools(toolsScreenUiState) @@ -83,6 +107,9 @@ internal fun ToolsScreen( val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition") + var selectedTool by remember { mutableStateOf(null) } + var isShowMenu by remember { mutableStateOf(true) } + Box( modifier.fillMaxSize() ) { @@ -95,9 +122,7 @@ internal fun ToolsScreen( verticalArrangement = Arrangement.Center ) { val angle by infiniteTransition.animateFloat( - initialValue = 0F, - targetValue = 360F, - animationSpec = infiniteRepeatable( + initialValue = 0F, targetValue = 360F, animationSpec = infiniteRepeatable( animation = tween(800, easing = Ease), ), label = "angle" ) @@ -110,6 +135,7 @@ internal fun ToolsScreen( ) } } + ToolsScreenUiState.Nothing -> { Column( modifier = Modifier.fillMaxSize(), @@ -122,6 +148,7 @@ internal fun ToolsScreen( } } } + is ToolsScreenUiState.Success -> { LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Adaptive(160.dp), @@ -131,10 +158,12 @@ internal fun ToolsScreen( state = state ) { - toolsPanel( - toolItems = toolsScreenUiState.tools, - onClickToolCard = onNavigateToToolView - ) + toolsPanel(toolItems = toolsScreenUiState.tools, + onClick = onNavigateToToolView, + onLongClick = { + selectedTool = it + isShowMenu = true + }) item(span = StaggeredGridItemSpan.FullLine) { Spacer(modifier = Modifier.height(8.dp)) @@ -150,28 +179,75 @@ internal fun ToolsScreen( .windowInsetsPadding(WindowInsets.systemBars) .padding(horizontal = 2.dp) .align(Alignment.CenterEnd), - state = scrollbarState, orientation = Orientation.Vertical, + state = scrollbarState, + orientation = Orientation.Vertical, 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( toolItems: List, - onClickToolCard: (username: String, toolId: String) -> Unit + onClick: (username: String, toolId: String) -> Unit, + onLongClick: (ToolEntity) -> Unit ) { items( items = toolItems, key = { it.id }, ) { - ToolCard( - tool = it, - onClick = {onClickToolCard(it.authorUsername, it.toolId)}, - onLongClick = {onClickToolCard(it.authorUsername, it.toolId)} - ) + ToolCard(tool = it, + onClick = { onClick(it.authorUsername, it.toolId) }, + onLongClick = { onLongClick(it) }) } } +@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 private fun howManyTools(toolsScreenUiState: ToolsScreenUiState) = when (toolsScreenUiState) { 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 aadd859..dfbe071 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 @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import top.fatweb.oxygen.toolbox.model.tool.ToolEntity import top.fatweb.oxygen.toolbox.repository.tool.StoreRepository import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository @@ -17,7 +18,7 @@ import kotlin.time.Duration.Companion.seconds @HiltViewModel class ToolsScreenViewModel @Inject constructor( private val storeRepository: StoreRepository, - toolRepository: ToolRepository, + private val toolRepository: ToolRepository, savedStateHandle: SavedStateHandle ) : ViewModel() { private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "") @@ -37,6 +38,17 @@ class ToolsScreenViewModel @Inject constructor( 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 { diff --git a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/ResourcesUtils.kt b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/ResourcesUtils.kt index a1da58d..0e97b8c 100644 --- a/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/ResourcesUtils.kt +++ b/app/src/main/kotlin/top/fatweb/oxygen/toolbox/ui/util/ResourcesUtils.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.pm.PackageManager import android.content.res.Resources import android.os.Build +import androidx.annotation.StringRes import androidx.core.os.ConfigurationCompat import androidx.core.os.LocaleListCompat import java.util.Locale @@ -33,8 +34,14 @@ object ResourcesUtils { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) 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) { -1 } + + fun getString(context: Context, @StringRes resId: Int): String = + context.resources.getString(resId) } \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 5482fb2..ffac717 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -14,7 +14,10 @@ ⚠️ 无法连接至互联网 安装 安装中…… + 卸载 + 卸载成功 取消 + 撤消 商店 安装工具 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dbafa95..956f21b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,7 +13,10 @@ ⚠️ Unable to connect to the internet Install Installing… + Uninstall + Uninstalled successfully Cancel + Undo Store Install Tool