Feat(ToolStore and Tools): Support query by tool name or keywords
This commit is contained in:
@@ -20,12 +20,19 @@ interface ToolDao {
|
||||
@Delete
|
||||
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?>
|
||||
|
||||
@Query("SELECT * FROM tools ORDER BY updateTime DESC")
|
||||
fun selectAllTools(): Flow<List<ToolEntity>>
|
||||
@Query("SELECT * FROM tools " +
|
||||
"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?>
|
||||
}
|
||||
@@ -11,7 +11,9 @@ fun OxygenNavHost(
|
||||
appState: OxygenAppState,
|
||||
onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
|
||||
startDestination: String,
|
||||
isVertical: Boolean
|
||||
isVertical: Boolean,
|
||||
searchValue: String,
|
||||
searchCount: Int
|
||||
) {
|
||||
val navController = appState.navController
|
||||
NavHost(
|
||||
@@ -19,9 +21,6 @@ fun OxygenNavHost(
|
||||
navController = navController,
|
||||
startDestination = startDestination
|
||||
) {
|
||||
searchScreen(
|
||||
onBackClick = navController::popBackStack
|
||||
)
|
||||
aboutScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onNavigateToLibraries = navController::navigateToLibraries
|
||||
@@ -31,10 +30,13 @@ fun OxygenNavHost(
|
||||
)
|
||||
toolStoreScreen(
|
||||
isVertical = isVertical,
|
||||
searchValue = searchValue,
|
||||
searchCount = searchCount,
|
||||
onNavigateToToolView = navController::navigateToToolView
|
||||
)
|
||||
toolsScreen(
|
||||
isVertical = isVertical,
|
||||
searchValue = searchValue,
|
||||
onShowSnackbar = onShowSnackbar,
|
||||
onNavigateToToolView = navController::navigateToToolView,
|
||||
onNavigateToToolStore = { appState.navigateToTopLevelDestination(TopLevelDestination.ToolStore) }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ fun NavController.navigateToToolStore(navOptions: NavOptions? = null) =
|
||||
|
||||
fun NavGraphBuilder.toolStoreScreen(
|
||||
isVertical: Boolean,
|
||||
searchValue: String,
|
||||
searchCount: Int,
|
||||
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit
|
||||
) {
|
||||
composable(
|
||||
@@ -41,6 +43,8 @@ fun NavGraphBuilder.toolStoreScreen(
|
||||
}
|
||||
) {
|
||||
ToolStoreRoute(
|
||||
searchValue = searchValue,
|
||||
searchCount = searchCount,
|
||||
onNavigateToToolView = onNavigateToToolView
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ fun NavController.navigateToTools(navOptions: NavOptions) = navigate(TOOLS_ROUTE
|
||||
|
||||
fun NavGraphBuilder.toolsScreen(
|
||||
isVertical: Boolean,
|
||||
searchValue: String,
|
||||
onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
|
||||
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit,
|
||||
onNavigateToToolStore: () -> Unit
|
||||
@@ -50,6 +51,7 @@ fun NavGraphBuilder.toolsScreen(
|
||||
}
|
||||
) {
|
||||
ToolsRoute(
|
||||
searchValue = searchValue,
|
||||
onShowSnackbar = onShowSnackbar,
|
||||
onNavigateToToolView = onNavigateToToolView,
|
||||
onNavigateToToolStore = onNavigateToToolStore
|
||||
|
||||
@@ -6,7 +6,7 @@ import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||
interface ToolRepository {
|
||||
val toolViewTemplate: Flow<String>
|
||||
|
||||
fun getAllToolsStream(): Flow<List<ToolEntity>>
|
||||
fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>>
|
||||
|
||||
fun getToolById(id: Long): Flow<ToolEntity?>
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ class OfflineToolRepository @Inject constructor(
|
||||
override val toolViewTemplate: Flow<String>
|
||||
get() = toolDataSource.toolViewTemplate
|
||||
|
||||
override fun getAllToolsStream(): Flow<List<ToolEntity>> =
|
||||
toolDao.selectAllTools()
|
||||
override fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>> =
|
||||
toolDao.selectAllTools(searchValue)
|
||||
|
||||
override fun getToolById(id: Long): Flow<ToolEntity?> =
|
||||
toolDao.selectToolById(id)
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.OxygenNavigationRailItem
|
||||
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.theme.GradientColors
|
||||
import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors
|
||||
@@ -79,6 +81,20 @@ fun OxygenApp(appState: OxygenAppState) {
|
||||
|
||||
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) {
|
||||
if (isOffline) {
|
||||
snackbarHostState.showSnackbar(
|
||||
@@ -152,12 +168,26 @@ fun OxygenApp(appState: OxygenAppState) {
|
||||
navigationIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_navigation_icon_description),
|
||||
actionIcon = OxygenIcons.MoreVert,
|
||||
actionIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_action_icon_description),
|
||||
activeSearch = activeSearch,
|
||||
searchButtonPosition = SearchButtonPosition.Navigation,
|
||||
query = searchValue,
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Transparent
|
||||
),
|
||||
onNavigationClick = { appState.navigateToSearch() },
|
||||
onActionClick = { showSettingsDialog = true }
|
||||
onNavigationClick = { activeSearch = 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.Star -> STAR_ROUTE
|
||||
},
|
||||
isVertical = appState.shouldShowBottomBar
|
||||
isVertical = appState.shouldShowBottomBar,
|
||||
searchValue = searchValue,
|
||||
searchCount = searchCount
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.navigateToAbout
|
||||
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.navigateToToolStore
|
||||
import top.fatweb.oxygen.toolbox.navigation.navigateToTools
|
||||
@@ -119,8 +118,6 @@ class OxygenAppState(
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToSearch() = navController.navigateToSearch()
|
||||
|
||||
fun navigateToLibraries() = navController.navigateToLibraries()
|
||||
|
||||
fun navigateToAbout() = navController.navigateToAbout()
|
||||
|
||||
@@ -51,6 +51,7 @@ fun OxygenTopAppBar(
|
||||
actionIcon: ImageVector? = null,
|
||||
actionIconContentDescription: String? = null,
|
||||
activeSearch: Boolean = false,
|
||||
searchButtonPosition: SearchButtonPosition = SearchButtonPosition.Action,
|
||||
query: String = "",
|
||||
colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(),
|
||||
onNavigationClick: () -> Unit = {},
|
||||
@@ -122,7 +123,15 @@ fun OxygenTopAppBar(
|
||||
else title()
|
||||
},
|
||||
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) {
|
||||
Icon(
|
||||
imageVector = navigationIcon,
|
||||
@@ -133,7 +142,9 @@ fun OxygenTopAppBar(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (activeSearch) IconButton(onClick = onCancelSearch) {
|
||||
if (activeSearch && searchButtonPosition == SearchButtonPosition.Action) IconButton(
|
||||
onClick = onCancelSearch
|
||||
) {
|
||||
Icon(
|
||||
imageVector = OxygenIcons.Close,
|
||||
contentDescription = stringResource(R.string.core_close)
|
||||
@@ -154,6 +165,10 @@ fun OxygenTopAppBar(
|
||||
)
|
||||
}
|
||||
|
||||
enum class SearchButtonPosition {
|
||||
Navigation, Action
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OxygenPreviews
|
||||
@Composable
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -37,6 +37,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -64,11 +65,17 @@ import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
|
||||
internal fun ToolStoreRoute(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ToolStoreViewModel = hiltViewModel(),
|
||||
searchValue: String,
|
||||
searchCount: Int,
|
||||
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit,
|
||||
) {
|
||||
val toolStorePagingItems = viewModel.storeData.collectAsLazyPagingItems()
|
||||
val installInfo by viewModel.installInfo.collectAsState()
|
||||
|
||||
LaunchedEffect(searchCount) {
|
||||
viewModel.onSearchValueChange(searchValue)
|
||||
}
|
||||
|
||||
ToolStoreScreen(
|
||||
modifier = modifier,
|
||||
onNavigateToToolView = onNavigateToToolView,
|
||||
|
||||
@@ -38,6 +38,10 @@ class ToolStoreViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun onSearchValueChange(value: String) {
|
||||
savedStateHandle[SEARCH_VALUE] = value
|
||||
}
|
||||
|
||||
fun changeInstallInfo(
|
||||
status: ToolStoreUiState.InstallInfo.Status = installInfo.value.status,
|
||||
type: ToolStoreUiState.InstallInfo.Type = installInfo.value.type
|
||||
|
||||
@@ -36,6 +36,7 @@ import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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
|
||||
@@ -67,12 +68,17 @@ import top.fatweb.oxygen.toolbox.ui.util.ResourcesUtils
|
||||
internal fun ToolsRoute(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ToolsScreenViewModel = hiltViewModel(),
|
||||
searchValue: String,
|
||||
onShowSnackbar: suspend (message: String, action: String?) -> Boolean,
|
||||
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit,
|
||||
onNavigateToToolStore: () -> Unit
|
||||
) {
|
||||
val toolsScreenUiStateState by viewModel.toolsScreenUiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(searchValue) {
|
||||
viewModel.onSearchValueChange(searchValue)
|
||||
}
|
||||
|
||||
ToolsScreen(
|
||||
modifier = modifier,
|
||||
onShowSnackbar = onShowSnackbar,
|
||||
|
||||
@@ -4,8 +4,10 @@ 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
|
||||
@@ -19,24 +21,30 @@ import kotlin.time.Duration.Companion.seconds
|
||||
class ToolsScreenViewModel @Inject constructor(
|
||||
private val storeRepository: StoreRepository,
|
||||
private val toolRepository: ToolRepository,
|
||||
savedStateHandle: SavedStateHandle
|
||||
private val savedStateHandle: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "")
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val toolsScreenUiState: StateFlow<ToolsScreenUiState> =
|
||||
toolRepository.getAllToolsStream()
|
||||
.map {
|
||||
searchValue.flatMapLatest { searchValue ->
|
||||
toolRepository.getAllToolsStream(searchValue).map {
|
||||
if (it.isEmpty()) {
|
||||
ToolsScreenUiState.Nothing
|
||||
} else {
|
||||
ToolsScreenUiState.Success(it)
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = ToolsScreenUiState.Loading,
|
||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = ToolsScreenUiState.Loading,
|
||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
||||
)
|
||||
|
||||
|
||||
fun onSearchValueChange(value: String) {
|
||||
savedStateHandle[SEARCH_VALUE] = value
|
||||
}
|
||||
|
||||
fun uninstall(tool: ToolEntity) {
|
||||
viewModelScope.launch {
|
||||
|
||||
Reference in New Issue
Block a user