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.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

View File

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

View File

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

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.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<ToolEntity?>(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,27 +179,74 @@ 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<ToolEntity>,
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) =

View File

@@ -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 {

View File

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

View File

@@ -14,7 +14,10 @@
<string name="core_no_connect">⚠️ 无法连接至互联网</string>
<string name="core_install">安装</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_undo">撤消</string>
<string name="feature_store_title">商店</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_install">Install</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_undo">Undo</string>
<string name="feature_store_title">Store</string>
<string name="feature_store_install_tool">Install Tool</string>