Feat(ToolStarScreen): Support star tool

This commit is contained in:
2024-08-20 15:31:24 +08:00
parent 086b588445
commit da4c58b631
12 changed files with 361 additions and 35 deletions

View File

@@ -31,6 +31,15 @@ interface ToolDao {
"ORDER BY updateTime DESC")
fun selectAllTools(searchValue: String): Flow<List<ToolEntity>>
@Query("SELECT * FROM tools " +
"WHERE isStar = 1 " +
"AND (:searchValue = '' " +
"OR name LIKE '%' || :searchValue || '%' COLLATE NOCASE " +
"OR keywords LIKE '%\"%' || :searchValue || '%\"%' COLLATE NOCASE" +
") " +
"ORDER BY updateTime DESC")
fun selectStarTools(searchValue: String): Flow<List<ToolEntity>>
@Query("SELECT * FROM tools " +
"WHERE authorUsername = :username " +
"and toolId = :toolId LIMIT 1")

View File

@@ -45,7 +45,9 @@ fun OxygenNavHost(
onBackClick = navController::popBackStack
)
starScreen(
isVertical = isVertical
isVertical = isVertical,
searchValue = searchValue,
onNavigateToToolView = navController::navigateToToolView
)
}
}

View File

@@ -15,7 +15,9 @@ const val STAR_ROUTE = "star_route"
fun NavController.navigateToStar(navOptions: NavOptions) = navigate(STAR_ROUTE, navOptions)
fun NavGraphBuilder.starScreen(
isVertical: Boolean
isVertical: Boolean,
searchValue: String,
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit
) {
composable(
route = STAR_ROUTE,
@@ -38,6 +40,9 @@ fun NavGraphBuilder.starScreen(
}
}
) {
StarRoute()
StarRoute(
searchValue = searchValue,
onNavigateToToolView = onNavigateToToolView
)
}
}

View File

@@ -8,6 +8,8 @@ interface ToolRepository {
fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>>
fun getStarToolsStream(searchValue: String): Flow<List<ToolEntity>>
fun getToolById(id: Long): Flow<ToolEntity?>
fun getToolByUsernameAndToolId(username: String, toolId: String): Flow<ToolEntity?>

View File

@@ -17,6 +17,9 @@ class OfflineToolRepository @Inject constructor(
override fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>> =
toolDao.selectAllTools(searchValue)
override fun getStarToolsStream(searchValue: String): Flow<List<ToolEntity>> =
toolDao.selectStarTools(searchValue)
override fun getToolById(id: Long): Flow<ToolEntity?> =
toolDao.selectToolById(id)

View File

@@ -1,20 +0,0 @@
package top.fatweb.oxygen.toolbox.ui.star
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
internal fun StarRoute(
modifier: Modifier = Modifier
) {
StarScreen(
modifier = modifier
)
}
@Composable
internal fun StarScreen(
modifier: Modifier = Modifier
) {
}

View File

@@ -0,0 +1,237 @@
package top.fatweb.oxygen.toolbox.ui.star
import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.animation.core.Ease
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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
@Composable
internal fun StarRoute(
modifier: Modifier = Modifier,
viewModel: StarScreenViewModel = hiltViewModel(),
searchValue: String,
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit
) {
val starScreenUiState by viewModel.starScreenUiState.collectAsStateWithLifecycle()
LaunchedEffect(searchValue) {
viewModel.onSearchValueChange(searchValue)
}
StarScreen(
modifier = modifier,
starScreenUiState = starScreenUiState,
onNavigateToToolView = onNavigateToToolView,
onUnstar = viewModel::unstar
)
}
@Composable
internal fun StarScreen(
modifier: Modifier = Modifier,
starScreenUiState: StarScreenUiState,
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit,
onUnstar: (ToolEntity) -> Unit
) {
ReportDrawnWhen { starScreenUiState !is StarScreenUiState.Loading }
val itemsAvailable = howManyTools(starScreenUiState)
val state = rememberLazyStaggeredGridState()
val scrollbarState = state.scrollbarState(itemsAvailable = itemsAvailable)
val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")
var selectedTool by remember { mutableStateOf<ToolEntity?>(null) }
var isShowMenu by remember { mutableStateOf(true) }
Box(
modifier.fillMaxSize()
) {
when (starScreenUiState) {
StarScreenUiState.Loading -> {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val angle by infiniteTransition.animateFloat(
initialValue = 0F, targetValue = 360F, animationSpec = infiniteRepeatable(
animation = tween(800, easing = Ease)
), label = "angle"
)
Icon(
modifier = Modifier
.size(32.dp)
.graphicsLayer { rotationZ = angle },
imageVector = OxygenIcons.Loading,
contentDescription = ""
)
}
}
StarScreenUiState.Nothing -> {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = stringResource(R.string.feature_star_no_tools_starred))
}
}
is StarScreenUiState.Success -> {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Adaptive(160.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalItemSpacing = 24.dp,
state = state
) {
toolsPanel(
toolItems = starScreenUiState.tools,
onClick = onNavigateToToolView,
onLongClick = {
selectedTool = it
isShowMenu = true
}
)
item(span = StaggeredGridItemSpan.FullLine) {
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
}
}
}
}
state.DraggableScrollbar(
modifier = Modifier
.fillMaxHeight()
.windowInsetsPadding(WindowInsets.systemBars)
.padding(horizontal = 2.dp)
.align(Alignment.CenterEnd),
state = scrollbarState,
orientation = Orientation.Vertical,
onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable)
)
}
if (isShowMenu && selectedTool != null) {
ToolMenu(
onDismiss = { isShowMenu = false },
selectedTool = selectedTool!!,
onUnstar = {
isShowMenu = false
onUnstar(selectedTool!!)
}
)
}
}
private fun LazyStaggeredGridScope.toolsPanel(
toolItems: List<ToolEntity>,
onClick: (username: String, toolId: String, preview: Boolean) -> Unit,
onLongClick: (ToolEntity) -> Unit
) {
items(
items = toolItems,
key = { it.id }
) {
ToolCard(
tool = it,
actionIcon = if (it.isStar) OxygenIcons.Star else null,
actionIconContentDescription = stringResource(R.string.core_star),
onClick = { onClick(it.authorUsername, it.toolId, false) },
onLongClick = { onLongClick(it) },
onAction = { onClick(it.authorUsername, it.toolId, false) }
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ToolMenu(
modifier: Modifier = Modifier,
onDismiss: () -> Unit,
selectedTool: ToolEntity,
onUnstar: () -> 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.StarBorder,
text = stringResource(R.string.core_unstar),
onClick = onUnstar
)
}
}
}
}
@Composable
private fun howManyTools(starScreenUiState: StarScreenUiState) =
when (starScreenUiState) {
StarScreenUiState.Loading, StarScreenUiState.Nothing -> 0
is StarScreenUiState.Success -> starScreenUiState.tools.size
}

View File

@@ -0,0 +1,59 @@
package top.fatweb.oxygen.toolbox.ui.star
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
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.ToolRepository
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@HiltViewModel
class StarScreenViewModel @Inject constructor(
private val toolRepository: ToolRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "")
@OptIn(ExperimentalCoroutinesApi::class)
val starScreenUiState: StateFlow<StarScreenUiState> =
searchValue.flatMapLatest { searchValue ->
toolRepository.getStarToolsStream(searchValue).map {
if (it.isEmpty()) {
StarScreenUiState.Nothing
} else {
StarScreenUiState.Success(it)
}
}
}.stateIn(
scope = viewModelScope,
initialValue = StarScreenUiState.Loading,
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
)
fun onSearchValueChange(value: String) {
savedStateHandle[SEARCH_VALUE] = value
}
fun unstar(tool: ToolEntity) {
viewModelScope.launch {
toolRepository.updateTool(tool.copy(isStar = false))
}
}
}
sealed interface StarScreenUiState {
data object Loading : StarScreenUiState
data object Nothing : StarScreenUiState
data class Success(val tools: List<ToolEntity>) : StarScreenUiState
}
private const val SEARCH_VALUE = "searchValue"

View File

@@ -86,19 +86,21 @@ internal fun ToolsRoute(
onNavigateToToolStore = onNavigateToToolStore,
toolsScreenUiState = toolsScreenUiStateState,
onUninstall = viewModel::uninstall,
onUndo = viewModel::undo
onUndo = viewModel::undo,
onChangeStar = viewModel::changeStar
)
}
@Composable
internal fun ToolsScreen(
modifier: Modifier = Modifier,
toolsScreenUiState: ToolsScreenUiState,
onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit,
onNavigateToToolStore: () -> Unit,
toolsScreenUiState: ToolsScreenUiState,
onUninstall: (ToolEntity) -> Unit,
onUndo: (ToolEntity) -> Unit
onUndo: (ToolEntity) -> Unit,
onChangeStar: (ToolEntity, Boolean) -> Unit
) {
val localContext = LocalContext.current
@@ -164,16 +166,18 @@ internal fun ToolsScreen(
state = state
) {
toolsPanel(toolItems = toolsScreenUiState.tools,
toolsPanel(
toolItems = toolsScreenUiState.tools,
onClick = onNavigateToToolView,
onLongClick = {
selectedTool = it
isShowMenu = true
})
}
)
item(span = StaggeredGridItemSpan.FullLine) {
Spacer(modifier = Modifier.height(8.dp))
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
}
}
}
@@ -207,6 +211,10 @@ internal fun ToolsScreen(
onUndo(selectedTool!!)
}
}
},
onChangeStar = {
isShowMenu = false
onChangeStar(selectedTool!!, it)
}
)
}
@@ -219,11 +227,16 @@ private fun LazyStaggeredGridScope.toolsPanel(
) {
items(
items = toolItems,
key = { it.id },
key = { it.id }
) {
ToolCard(tool = it,
ToolCard(
tool = it,
actionIcon = if (it.isStar) OxygenIcons.Star else null,
actionIconContentDescription = stringResource(R.string.core_star),
onClick = { onClick(it.authorUsername, it.toolId, false) },
onLongClick = { onLongClick(it) })
onLongClick = { onLongClick(it) },
onAction = { onClick(it.authorUsername, it.toolId, false) }
)
}
}
@@ -233,7 +246,8 @@ private fun ToolMenu(
modifier: Modifier = Modifier,
onDismiss: () -> Unit,
selectedTool: ToolEntity,
onUninstall: () -> Unit
onUninstall: () -> Unit,
onChangeStar: (Boolean) -> Unit
) {
ModalBottomSheet(onDismissRequest = onDismiss, dragHandle = {}) {
Column(
@@ -248,6 +262,11 @@ private fun ToolMenu(
text = stringResource(R.string.core_uninstall),
onClick = onUninstall
)
DialogClickerRow(
icon = if (selectedTool.isStar) OxygenIcons.StarBorder else OxygenIcons.Star,
text = stringResource(if (selectedTool.isStar) R.string.core_unstar else R.string.core_star),
onClick = { onChangeStar(!selectedTool.isStar) }
)
}
}
}

View File

@@ -12,14 +12,12 @@ 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
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@HiltViewModel
class ToolsScreenViewModel @Inject constructor(
private val storeRepository: StoreRepository,
private val toolRepository: ToolRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
@@ -57,6 +55,12 @@ class ToolsScreenViewModel @Inject constructor(
toolRepository.saveTool(tool)
}
}
fun changeStar(tool: ToolEntity, star: Boolean) {
viewModelScope.launch {
toolRepository.updateTool(tool.copy(isStar = star))
}
}
}
sealed interface ToolsScreenUiState {