Feat(ToolStore and Tools): Support query by tool name or keywords

This commit is contained in:
2024-08-19 17:48:42 +08:00
parent 47647217f1
commit 167df010a9
16 changed files with 111 additions and 87 deletions

View File

@@ -20,12 +20,19 @@ interface ToolDao {
@Delete @Delete
suspend fun deleteTool(tool: ToolEntity) suspend fun deleteTool(tool: ToolEntity)
@Query("SELECT * FROM tools WHERE id = :id") @Query("SELECT * FROM tools " +
"WHERE id = :id")
fun selectToolById(id: Long): Flow<ToolEntity?> fun selectToolById(id: Long): Flow<ToolEntity?>
@Query("SELECT * FROM tools ORDER BY updateTime DESC") @Query("SELECT * FROM tools " +
fun selectAllTools(): Flow<List<ToolEntity>> "WHERE :searchValue = '' " +
"OR name LIKE '%' || :searchValue || '%' COLLATE NOCASE " +
"OR keywords LIKE '%\"%' || :searchValue || '%\"%' COLLATE NOCASE " +
"ORDER BY updateTime DESC")
fun selectAllTools(searchValue: String): Flow<List<ToolEntity>>
@Query("SELECT * FROM tools WHERE authorUsername = :username and toolId = :toolId LIMIT 1") @Query("SELECT * FROM tools " +
"WHERE authorUsername = :username " +
"and toolId = :toolId LIMIT 1")
fun selectToolByUsernameAndToolId(username: String, toolId: String): Flow<ToolEntity?> fun selectToolByUsernameAndToolId(username: String, toolId: String): Flow<ToolEntity?>
} }

View File

@@ -11,7 +11,9 @@ fun OxygenNavHost(
appState: OxygenAppState, appState: OxygenAppState,
onShowSnackbar: suspend (message: String, action: String?) -> Boolean, onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
startDestination: String, startDestination: String,
isVertical: Boolean isVertical: Boolean,
searchValue: String,
searchCount: Int
) { ) {
val navController = appState.navController val navController = appState.navController
NavHost( NavHost(
@@ -19,9 +21,6 @@ fun OxygenNavHost(
navController = navController, navController = navController,
startDestination = startDestination startDestination = startDestination
) { ) {
searchScreen(
onBackClick = navController::popBackStack
)
aboutScreen( aboutScreen(
onBackClick = navController::popBackStack, onBackClick = navController::popBackStack,
onNavigateToLibraries = navController::navigateToLibraries onNavigateToLibraries = navController::navigateToLibraries
@@ -31,10 +30,13 @@ fun OxygenNavHost(
) )
toolStoreScreen( toolStoreScreen(
isVertical = isVertical, isVertical = isVertical,
searchValue = searchValue,
searchCount = searchCount,
onNavigateToToolView = navController::navigateToToolView onNavigateToToolView = navController::navigateToToolView
) )
toolsScreen( toolsScreen(
isVertical = isVertical, isVertical = isVertical,
searchValue = searchValue,
onShowSnackbar = onShowSnackbar, onShowSnackbar = onShowSnackbar,
onNavigateToToolView = navController::navigateToToolView, onNavigateToToolView = navController::navigateToToolView,
onNavigateToToolStore = { appState.navigateToTopLevelDestination(TopLevelDestination.ToolStore) } onNavigateToToolStore = { appState.navigateToTopLevelDestination(TopLevelDestination.ToolStore) }

View File

@@ -1,22 +0,0 @@
package top.fatweb.oxygen.toolbox.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import top.fatweb.oxygen.toolbox.ui.search.SearchRoute
const val SEARCH_ROUTE = "search_route"
fun NavController.navigateToSearch(navOptions: NavOptions? = null) =
navigate(SEARCH_ROUTE, navOptions)
fun NavGraphBuilder.searchScreen(
onBackClick: () -> Unit
) {
composable(
route = SEARCH_ROUTE
) {
SearchRoute(onBackClick = onBackClick)
}
}

View File

@@ -17,6 +17,8 @@ fun NavController.navigateToToolStore(navOptions: NavOptions? = null) =
fun NavGraphBuilder.toolStoreScreen( fun NavGraphBuilder.toolStoreScreen(
isVertical: Boolean, isVertical: Boolean,
searchValue: String,
searchCount: Int,
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit
) { ) {
composable( composable(
@@ -41,6 +43,8 @@ fun NavGraphBuilder.toolStoreScreen(
} }
) { ) {
ToolStoreRoute( ToolStoreRoute(
searchValue = searchValue,
searchCount = searchCount,
onNavigateToToolView = onNavigateToToolView onNavigateToToolView = onNavigateToToolView
) )
} }

View File

@@ -16,6 +16,7 @@ fun NavController.navigateToTools(navOptions: NavOptions) = navigate(TOOLS_ROUTE
fun NavGraphBuilder.toolsScreen( fun NavGraphBuilder.toolsScreen(
isVertical: Boolean, isVertical: Boolean,
searchValue: String,
onShowSnackbar: suspend (message: String, action: String?) -> Boolean, onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit, onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit,
onNavigateToToolStore: () -> Unit onNavigateToToolStore: () -> Unit
@@ -50,6 +51,7 @@ fun NavGraphBuilder.toolsScreen(
} }
) { ) {
ToolsRoute( ToolsRoute(
searchValue = searchValue,
onShowSnackbar = onShowSnackbar, onShowSnackbar = onShowSnackbar,
onNavigateToToolView = onNavigateToToolView, onNavigateToToolView = onNavigateToToolView,
onNavigateToToolStore = onNavigateToToolStore onNavigateToToolStore = onNavigateToToolStore

View File

@@ -6,7 +6,7 @@ import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
interface ToolRepository { interface ToolRepository {
val toolViewTemplate: Flow<String> val toolViewTemplate: Flow<String>
fun getAllToolsStream(): Flow<List<ToolEntity>> fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>>
fun getToolById(id: Long): Flow<ToolEntity?> fun getToolById(id: Long): Flow<ToolEntity?>

View File

@@ -14,8 +14,8 @@ class OfflineToolRepository @Inject constructor(
override val toolViewTemplate: Flow<String> override val toolViewTemplate: Flow<String>
get() = toolDataSource.toolViewTemplate get() = toolDataSource.toolViewTemplate
override fun getAllToolsStream(): Flow<List<ToolEntity>> = override fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>> =
toolDao.selectAllTools() toolDao.selectAllTools(searchValue)
override fun getToolById(id: Long): Flow<ToolEntity?> = override fun getToolById(id: Long): Flow<ToolEntity?> =
toolDao.selectToolById(id) toolDao.selectToolById(id)

View File

@@ -27,6 +27,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@@ -52,6 +53,7 @@ import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationBarItem
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRail import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRail
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRailItem import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRailItem
import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar
import top.fatweb.oxygen.toolbox.ui.component.SearchButtonPosition
import top.fatweb.oxygen.toolbox.ui.settings.SettingsDialog import top.fatweb.oxygen.toolbox.ui.settings.SettingsDialog
import top.fatweb.oxygen.toolbox.ui.theme.GradientColors import top.fatweb.oxygen.toolbox.ui.theme.GradientColors
import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors
@@ -79,6 +81,20 @@ fun OxygenApp(appState: OxygenAppState) {
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
var activeSearch by remember { mutableStateOf(false) }
var searchValue by remember { mutableStateOf("") }
var searchCount by remember { mutableIntStateOf(0) }
LaunchedEffect(destination) {
activeSearch = false
searchValue = ""
if (searchCount == 0) {
searchCount++
} else {
searchCount = 0
}
}
LaunchedEffect(isOffline) { LaunchedEffect(isOffline) {
if (isOffline) { if (isOffline) {
snackbarHostState.showSnackbar( snackbarHostState.showSnackbar(
@@ -152,12 +168,26 @@ fun OxygenApp(appState: OxygenAppState) {
navigationIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_navigation_icon_description), navigationIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_navigation_icon_description),
actionIcon = OxygenIcons.MoreVert, actionIcon = OxygenIcons.MoreVert,
actionIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_action_icon_description), actionIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_action_icon_description),
activeSearch = activeSearch,
searchButtonPosition = SearchButtonPosition.Navigation,
query = searchValue,
colors = TopAppBarDefaults.centerAlignedTopAppBarColors( colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent, containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent scrolledContainerColor = Color.Transparent
), ),
onNavigationClick = { appState.navigateToSearch() }, onNavigationClick = { activeSearch = true },
onActionClick = { showSettingsDialog = true } onActionClick = { showSettingsDialog = true },
onQueryChange = {
searchValue = it
},
onSearch = {
searchCount++
},
onCancelSearch = {
searchValue = ""
activeSearch = false
searchCount = 0
}
) )
} }
@@ -174,7 +204,9 @@ fun OxygenApp(appState: OxygenAppState) {
LaunchPageConfig.Tools -> TOOLS_ROUTE LaunchPageConfig.Tools -> TOOLS_ROUTE
LaunchPageConfig.Star -> STAR_ROUTE LaunchPageConfig.Star -> STAR_ROUTE
}, },
isVertical = appState.shouldShowBottomBar isVertical = appState.shouldShowBottomBar,
searchValue = searchValue,
searchCount = searchCount
) )
} }
} }

View File

@@ -26,7 +26,6 @@ import top.fatweb.oxygen.toolbox.navigation.TOOL_STORE_ROUTE
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
import top.fatweb.oxygen.toolbox.navigation.navigateToAbout import top.fatweb.oxygen.toolbox.navigation.navigateToAbout
import top.fatweb.oxygen.toolbox.navigation.navigateToLibraries import top.fatweb.oxygen.toolbox.navigation.navigateToLibraries
import top.fatweb.oxygen.toolbox.navigation.navigateToSearch
import top.fatweb.oxygen.toolbox.navigation.navigateToStar import top.fatweb.oxygen.toolbox.navigation.navigateToStar
import top.fatweb.oxygen.toolbox.navigation.navigateToToolStore import top.fatweb.oxygen.toolbox.navigation.navigateToToolStore
import top.fatweb.oxygen.toolbox.navigation.navigateToTools import top.fatweb.oxygen.toolbox.navigation.navigateToTools
@@ -119,8 +118,6 @@ class OxygenAppState(
} }
} }
fun navigateToSearch() = navController.navigateToSearch()
fun navigateToLibraries() = navController.navigateToLibraries() fun navigateToLibraries() = navController.navigateToLibraries()
fun navigateToAbout() = navController.navigateToAbout() fun navigateToAbout() = navController.navigateToAbout()

View File

@@ -51,6 +51,7 @@ fun OxygenTopAppBar(
actionIcon: ImageVector? = null, actionIcon: ImageVector? = null,
actionIconContentDescription: String? = null, actionIconContentDescription: String? = null,
activeSearch: Boolean = false, activeSearch: Boolean = false,
searchButtonPosition: SearchButtonPosition = SearchButtonPosition.Action,
query: String = "", query: String = "",
colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(), colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(),
onNavigationClick: () -> Unit = {}, onNavigationClick: () -> Unit = {},
@@ -122,7 +123,15 @@ fun OxygenTopAppBar(
else title() else title()
}, },
navigationIcon = { navigationIcon = {
navigationIcon?.let { if (activeSearch && searchButtonPosition == SearchButtonPosition.Navigation) IconButton(
onClick = onCancelSearch
) {
Icon(
imageVector = OxygenIcons.Close,
contentDescription = stringResource(R.string.core_close)
)
}
else navigationIcon?.let {
IconButton(onClick = onNavigationClick) { IconButton(onClick = onNavigationClick) {
Icon( Icon(
imageVector = navigationIcon, imageVector = navigationIcon,
@@ -133,7 +142,9 @@ fun OxygenTopAppBar(
} }
}, },
actions = { actions = {
if (activeSearch) IconButton(onClick = onCancelSearch) { if (activeSearch && searchButtonPosition == SearchButtonPosition.Action) IconButton(
onClick = onCancelSearch
) {
Icon( Icon(
imageVector = OxygenIcons.Close, imageVector = OxygenIcons.Close,
contentDescription = stringResource(R.string.core_close) contentDescription = stringResource(R.string.core_close)
@@ -154,6 +165,10 @@ fun OxygenTopAppBar(
) )
} }
enum class SearchButtonPosition {
Navigation, Action
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@OxygenPreviews @OxygenPreviews
@Composable @Composable

View File

@@ -1,29 +0,0 @@
package top.fatweb.oxygen.toolbox.ui.search
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
@Composable
internal fun SearchRoute(
modifier: Modifier = Modifier,
onBackClick: () -> Unit,
// searchViewmodel: SearchViewModel = hiltViewModel()
) {
Row(
modifier = modifier
.fillMaxSize()
.safeDrawingPadding()
) {
IconButton(onClick = onBackClick) {
Icon(imageVector = OxygenIcons.Back, contentDescription = null)
}
Text("Search")
}
}

View File

@@ -1,9 +0,0 @@
package top.fatweb.oxygen.toolbox.ui.search
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class SearchViewModel @Inject constructor(
) : ViewModel()

View File

@@ -37,6 +37,7 @@ import androidx.compose.material3.Icon
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.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -64,11 +65,17 @@ import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
internal fun ToolStoreRoute( internal fun ToolStoreRoute(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: ToolStoreViewModel = hiltViewModel(), viewModel: ToolStoreViewModel = hiltViewModel(),
searchValue: String,
searchCount: Int,
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit, onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit,
) { ) {
val toolStorePagingItems = viewModel.storeData.collectAsLazyPagingItems() val toolStorePagingItems = viewModel.storeData.collectAsLazyPagingItems()
val installInfo by viewModel.installInfo.collectAsState() val installInfo by viewModel.installInfo.collectAsState()
LaunchedEffect(searchCount) {
viewModel.onSearchValueChange(searchValue)
}
ToolStoreScreen( ToolStoreScreen(
modifier = modifier, modifier = modifier,
onNavigateToToolView = onNavigateToToolView, onNavigateToToolView = onNavigateToToolView,

View File

@@ -38,6 +38,10 @@ class ToolStoreViewModel @Inject constructor(
) )
} }
fun onSearchValueChange(value: String) {
savedStateHandle[SEARCH_VALUE] = value
}
fun changeInstallInfo( fun changeInstallInfo(
status: ToolStoreUiState.InstallInfo.Status = installInfo.value.status, status: ToolStoreUiState.InstallInfo.Status = installInfo.value.status,
type: ToolStoreUiState.InstallInfo.Type = installInfo.value.type type: ToolStoreUiState.InstallInfo.Type = installInfo.value.type

View File

@@ -36,6 +36,7 @@ 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.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -67,12 +68,17 @@ import top.fatweb.oxygen.toolbox.ui.util.ResourcesUtils
internal fun ToolsRoute( internal fun ToolsRoute(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: ToolsScreenViewModel = hiltViewModel(), viewModel: ToolsScreenViewModel = hiltViewModel(),
searchValue: String,
onShowSnackbar: suspend (message: String, action: String?) -> Boolean, onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit, onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit,
onNavigateToToolStore: () -> Unit onNavigateToToolStore: () -> Unit
) { ) {
val toolsScreenUiStateState by viewModel.toolsScreenUiState.collectAsStateWithLifecycle() val toolsScreenUiStateState by viewModel.toolsScreenUiState.collectAsStateWithLifecycle()
LaunchedEffect(searchValue) {
viewModel.onSearchValueChange(searchValue)
}
ToolsScreen( ToolsScreen(
modifier = modifier, modifier = modifier,
onShowSnackbar = onShowSnackbar, onShowSnackbar = onShowSnackbar,

View File

@@ -4,8 +4,10 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
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 kotlinx.coroutines.launch
@@ -19,25 +21,31 @@ import kotlin.time.Duration.Companion.seconds
class ToolsScreenViewModel @Inject constructor( class ToolsScreenViewModel @Inject constructor(
private val storeRepository: StoreRepository, private val storeRepository: StoreRepository,
private val toolRepository: ToolRepository, private val toolRepository: ToolRepository,
savedStateHandle: SavedStateHandle private val savedStateHandle: SavedStateHandle
) : ViewModel() { ) : ViewModel() {
private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "") private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "")
@OptIn(ExperimentalCoroutinesApi::class)
val toolsScreenUiState: StateFlow<ToolsScreenUiState> = val toolsScreenUiState: StateFlow<ToolsScreenUiState> =
toolRepository.getAllToolsStream() searchValue.flatMapLatest { searchValue ->
.map { toolRepository.getAllToolsStream(searchValue).map {
if (it.isEmpty()) { if (it.isEmpty()) {
ToolsScreenUiState.Nothing ToolsScreenUiState.Nothing
} else { } else {
ToolsScreenUiState.Success(it) ToolsScreenUiState.Success(it)
} }
} }
.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
initialValue = ToolsScreenUiState.Loading, initialValue = ToolsScreenUiState.Loading,
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds) started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
) )
fun onSearchValueChange(value: String) {
savedStateHandle[SEARCH_VALUE] = value
}
fun uninstall(tool: ToolEntity) { fun uninstall(tool: ToolEntity) {
viewModelScope.launch { viewModelScope.launch {
toolRepository.removeTool(tool) toolRepository.removeTool(tool)