mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Global Search
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
<string name="location_library">Library</string>
|
||||
<string name="location_updates">Updates</string>
|
||||
<string name="location_sources">Sources</string>
|
||||
<string name="location_global_search">Global search</string>
|
||||
<string name="location_extensions">Extensions</string>
|
||||
<string name="location_downloads">Downloads</string>
|
||||
<string name="location_settings">Settings</string>
|
||||
@@ -91,6 +92,7 @@
|
||||
<string name="reset_filters">Reset</string>
|
||||
<string name="filter_source">Filter</string>
|
||||
<string name="in_library">In library</string>
|
||||
<string name="no_results_found">No results found</string>
|
||||
|
||||
<!-- Reader Menu -->
|
||||
<string name="default_reader_mode">Default</string>
|
||||
|
||||
@@ -26,6 +26,7 @@ import ca.gosyer.ui.settings.ThemesViewModel
|
||||
import ca.gosyer.ui.sources.SourcesScreenViewModel
|
||||
import ca.gosyer.ui.sources.browse.SourceScreenViewModel
|
||||
import ca.gosyer.ui.sources.browse.filter.SourceFiltersViewModel
|
||||
import ca.gosyer.ui.sources.globalsearch.GlobalSearchViewModel
|
||||
import ca.gosyer.ui.sources.home.SourceHomeScreenViewModel
|
||||
import ca.gosyer.ui.sources.settings.SourceSettingsScreenViewModel
|
||||
import ca.gosyer.ui.updates.UpdatesScreenViewModel
|
||||
@@ -56,6 +57,7 @@ actual class ViewModelFactoryImpl(
|
||||
private val sourceFiltersFactory: (params: SourceFiltersViewModel.Params) -> SourceFiltersViewModel,
|
||||
private val sourceSettingsFactory: (params: SourceSettingsScreenViewModel.Params) -> SourceSettingsScreenViewModel,
|
||||
private val sourceHomeFactory: () -> SourceHomeScreenViewModel,
|
||||
private val globalSearchFactory: (params: GlobalSearchViewModel.Params) -> GlobalSearchViewModel,
|
||||
private val sourceFactory: (params: SourceScreenViewModel.Params) -> SourceScreenViewModel,
|
||||
private val sourcesFactory: () -> SourcesScreenViewModel,
|
||||
private val updatesFactory: () -> UpdatesScreenViewModel
|
||||
@@ -84,6 +86,7 @@ actual class ViewModelFactoryImpl(
|
||||
SourceFiltersViewModel::class -> sourceFiltersFactory(arg1 as SourceFiltersViewModel.Params)
|
||||
SourceSettingsScreenViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsScreenViewModel.Params)
|
||||
SourceHomeScreenViewModel::class -> sourceHomeFactory()
|
||||
GlobalSearchViewModel::class -> globalSearchFactory(arg1 as GlobalSearchViewModel.Params)
|
||||
SourceScreenViewModel::class -> sourceFactory(arg1 as SourceScreenViewModel.Params)
|
||||
SourcesScreenViewModel::class -> sourcesFactory()
|
||||
UpdatesScreenViewModel::class -> updatesFactory()
|
||||
|
||||
@@ -27,6 +27,7 @@ import ca.gosyer.ui.settings.ThemesViewModel
|
||||
import ca.gosyer.ui.sources.SourcesScreenViewModel
|
||||
import ca.gosyer.ui.sources.browse.SourceScreenViewModel
|
||||
import ca.gosyer.ui.sources.browse.filter.SourceFiltersViewModel
|
||||
import ca.gosyer.ui.sources.globalsearch.GlobalSearchViewModel
|
||||
import ca.gosyer.ui.sources.home.SourceHomeScreenViewModel
|
||||
import ca.gosyer.ui.sources.settings.SourceSettingsScreenViewModel
|
||||
import ca.gosyer.ui.updates.UpdatesScreenViewModel
|
||||
@@ -58,6 +59,7 @@ actual class ViewModelFactoryImpl(
|
||||
private val sourceFiltersFactory: (params: SourceFiltersViewModel.Params) -> SourceFiltersViewModel,
|
||||
private val sourceSettingsFactory: (params: SourceSettingsScreenViewModel.Params) -> SourceSettingsScreenViewModel,
|
||||
private val sourceHomeFactory: () -> SourceHomeScreenViewModel,
|
||||
private val globalSearchFactory: (params: GlobalSearchViewModel.Params) -> GlobalSearchViewModel,
|
||||
private val sourceFactory: (params: SourceScreenViewModel.Params) -> SourceScreenViewModel,
|
||||
private val sourcesFactory: () -> SourcesScreenViewModel,
|
||||
private val updatesFactory: () -> UpdatesScreenViewModel
|
||||
@@ -87,6 +89,7 @@ actual class ViewModelFactoryImpl(
|
||||
SourceFiltersViewModel::class -> sourceFiltersFactory(arg1 as SourceFiltersViewModel.Params)
|
||||
SourceSettingsScreenViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsScreenViewModel.Params)
|
||||
SourceHomeScreenViewModel::class -> sourceHomeFactory()
|
||||
GlobalSearchViewModel::class -> globalSearchFactory(arg1 as GlobalSearchViewModel.Params)
|
||||
SourceScreenViewModel::class -> sourceFactory(arg1 as SourceScreenViewModel.Params)
|
||||
SourcesScreenViewModel::class -> sourcesFactory()
|
||||
UpdatesScreenViewModel::class -> updatesFactory()
|
||||
|
||||
@@ -20,14 +20,14 @@ import cafe.adriel.voyager.core.screen.ScreenKey
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
|
||||
class SourceScreen(val source: Source) : Screen {
|
||||
class SourceScreen(val source: Source, val initialQuery: String? = null) : Screen {
|
||||
|
||||
override val key: ScreenKey = source.id.toString()
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val sourceVM = viewModel {
|
||||
instantiate<SourceScreenViewModel>(SourceScreenViewModel.Params(source))
|
||||
instantiate<SourceScreenViewModel>(SourceScreenViewModel.Params(source, initialQuery))
|
||||
}
|
||||
val filterVM = viewModel {
|
||||
instantiate<SourceFiltersViewModel>(SourceFiltersViewModel.Params(source.id))
|
||||
|
||||
@@ -29,7 +29,8 @@ class SourceScreenViewModel(
|
||||
private val sourceHandler: SourceInteractionHandler,
|
||||
private val catalogPreferences: CatalogPreferences,
|
||||
private val libraryPreferences: LibraryPreferences,
|
||||
contextWrapper: ContextWrapper
|
||||
contextWrapper: ContextWrapper,
|
||||
initialQuery: String?
|
||||
) : ViewModel(contextWrapper) {
|
||||
|
||||
@Inject constructor(
|
||||
@@ -43,7 +44,8 @@ class SourceScreenViewModel(
|
||||
sourceHandler,
|
||||
catalogPreferences,
|
||||
libraryPreferences,
|
||||
contextWrapper
|
||||
contextWrapper,
|
||||
params.initialQuery
|
||||
)
|
||||
|
||||
val displayMode = catalogPreferences.displayMode().stateIn(scope)
|
||||
@@ -67,10 +69,10 @@ class SourceScreenViewModel(
|
||||
|
||||
private val _usingFilters = MutableStateFlow(false)
|
||||
|
||||
private val _sourceSearchQuery = MutableStateFlow<String?>(null)
|
||||
private val _sourceSearchQuery = MutableStateFlow(initialQuery)
|
||||
val sourceSearchQuery = _sourceSearchQuery.asStateFlow()
|
||||
|
||||
private val _query = MutableStateFlow<String?>(null)
|
||||
private val _query = MutableStateFlow(sourceSearchQuery.value)
|
||||
|
||||
private val _pageNum = MutableStateFlow(1)
|
||||
val pageNum = _pageNum.asStateFlow()
|
||||
@@ -156,7 +158,7 @@ class SourceScreenViewModel(
|
||||
catalogPreferences.displayMode().set(displayMode)
|
||||
}
|
||||
|
||||
data class Params(val source: Source)
|
||||
data class Params(val source: Source, val initialQuery: String?)
|
||||
|
||||
private companion object : CKLogger({})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.sources.globalsearch
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import ca.gosyer.ui.manga.MangaScreen
|
||||
import ca.gosyer.ui.sources.browse.SourceScreen
|
||||
import ca.gosyer.ui.sources.components.LocalSourcesNavigator
|
||||
import ca.gosyer.ui.sources.globalsearch.components.GlobalSearchScreenContent
|
||||
import ca.gosyer.uicore.vm.viewModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.core.screen.ScreenKey
|
||||
import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
|
||||
class GlobalSearchScreen(private val initialQuery: String) : Screen {
|
||||
|
||||
override val key: ScreenKey = uniqueScreenKey
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val vm = viewModel {
|
||||
instantiate<GlobalSearchViewModel>(GlobalSearchViewModel.Params(initialQuery))
|
||||
}
|
||||
val sourcesNavigator = LocalSourcesNavigator.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
GlobalSearchScreenContent(
|
||||
sources = vm.sources.collectAsState().value,
|
||||
results = vm.results,
|
||||
displayMode = vm.displayMode.collectAsState().value,
|
||||
query = vm.query.collectAsState().value,
|
||||
setQuery = vm::setQuery,
|
||||
submitSearch = vm::startSearch,
|
||||
onSourceClick = {
|
||||
if (sourcesNavigator != null) {
|
||||
sourcesNavigator.select(it, vm.query.value)
|
||||
} else {
|
||||
navigator push SourceScreen(it, vm.query.value)
|
||||
}
|
||||
},
|
||||
onMangaClick = {
|
||||
navigator push MangaScreen(it.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.sources.globalsearch
|
||||
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import ca.gosyer.core.logging.CKLogger
|
||||
import ca.gosyer.data.catalog.CatalogPreferences
|
||||
import ca.gosyer.data.models.MangaPage
|
||||
import ca.gosyer.data.models.Source
|
||||
import ca.gosyer.data.server.interactions.SourceInteractionHandler
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.uicore.vm.ViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class GlobalSearchViewModel @Inject constructor(
|
||||
private val sourceHandler: SourceInteractionHandler,
|
||||
catalogPreferences: CatalogPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
params: Params
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _query = MutableStateFlow(params.initialQuery)
|
||||
val query = _query.asStateFlow()
|
||||
|
||||
private val installedSources = MutableStateFlow(emptyList<Source>())
|
||||
|
||||
private val languages = catalogPreferences.languages().stateIn(scope)
|
||||
val displayMode = catalogPreferences.displayMode().stateIn(scope)
|
||||
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
val sources = combine(installedSources, languages) { installedSources, languages ->
|
||||
installedSources.filter {
|
||||
it.lang in languages || it.id == Source.LOCAL_SOURCE_ID
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private val search = MutableStateFlow(params.initialQuery)
|
||||
|
||||
val results = SnapshotStateMap<Long, Search>()
|
||||
|
||||
init {
|
||||
getSources()
|
||||
readySearch()
|
||||
}
|
||||
|
||||
private fun getSources() {
|
||||
sourceHandler.getSourceList()
|
||||
.onEach { sources ->
|
||||
installedSources.value = sources.sortedWith(
|
||||
compareBy<Source, String>(String.CASE_INSENSITIVE_ORDER) { it.lang }
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER) {
|
||||
it.name
|
||||
}
|
||||
)
|
||||
_isLoading.value = false
|
||||
}
|
||||
.catch {
|
||||
info(it) { "Error getting sources" }
|
||||
_isLoading.value = false
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private val semaphore = Semaphore(5)
|
||||
|
||||
private fun readySearch() {
|
||||
search
|
||||
.combine(sources) { query, sources ->
|
||||
query to sources
|
||||
}
|
||||
.mapLatest { (query, sources) ->
|
||||
results.clear()
|
||||
supervisorScope {
|
||||
sources.map { source ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
sourceHandler
|
||||
.getSearchResults(source, query, 1)
|
||||
.map {
|
||||
if (it.mangaList.isEmpty()) {
|
||||
Search.Failure(MR.strings.no_results_found.toPlatformString())
|
||||
} else {
|
||||
Search.Success(it)
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
info(it) { "Error getting search from ${source.displayName}" }
|
||||
emit(Search.Failure(it))
|
||||
}
|
||||
.onEach {
|
||||
results[source.id] = it
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
info(it) { "Error getting sources" }
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun setQuery(query: String) {
|
||||
_query.value = query
|
||||
}
|
||||
|
||||
fun startSearch(query: String) {
|
||||
search.value = query
|
||||
}
|
||||
|
||||
data class Params(val initialQuery: String)
|
||||
|
||||
sealed class Search {
|
||||
object Searching : Search()
|
||||
data class Success(val mangaPage: MangaPage) : Search()
|
||||
data class Failure(val e: String?) : Search() {
|
||||
constructor(e: Throwable) : this(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object : CKLogger({})
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.sources.globalsearch.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.times
|
||||
import ca.gosyer.data.models.Manga
|
||||
import ca.gosyer.ui.sources.browse.components.SourceMangaBadges
|
||||
import ca.gosyer.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.uicore.image.KamelImage
|
||||
import io.kamel.image.lazyPainterResource
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchMangaComfortableGridItem(
|
||||
modifier: Modifier,
|
||||
manga: Manga,
|
||||
inLibrary: Boolean
|
||||
) {
|
||||
val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium)
|
||||
val fontStyle = LocalTextStyle.current.merge(
|
||||
TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.width((mangaAspectRatio * 200.dp))
|
||||
.clip(MaterialTheme.shapes.medium) then modifier
|
||||
) {
|
||||
Column {
|
||||
KamelImage(
|
||||
cover,
|
||||
contentDescription = manga.title,
|
||||
modifier = Modifier
|
||||
.height(200.dp)
|
||||
.aspectRatio(mangaAspectRatio, true)
|
||||
.clip(MaterialTheme.shapes.medium),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Text(
|
||||
text = manga.title,
|
||||
style = fontStyle,
|
||||
maxLines = 3,
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
SourceMangaBadges(
|
||||
inLibrary = inLibrary,
|
||||
modifier = Modifier.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.sources.globalsearch.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ca.gosyer.data.models.Manga
|
||||
import ca.gosyer.ui.sources.browse.components.SourceMangaBadges
|
||||
import ca.gosyer.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.uicore.image.KamelImage
|
||||
import io.kamel.image.lazyPainterResource
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchMangaCompactGridItem(
|
||||
modifier: Modifier,
|
||||
manga: Manga,
|
||||
inLibrary: Boolean
|
||||
) {
|
||||
val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium)
|
||||
val fontStyle = LocalTextStyle.current.merge(
|
||||
TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.padding(4.dp)
|
||||
.height(200.dp)
|
||||
.aspectRatio(mangaAspectRatio, true)
|
||||
.clip(MaterialTheme.shapes.medium) then modifier
|
||||
) {
|
||||
KamelImage(
|
||||
cover,
|
||||
manga.title,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxSize().then(shadowGradient))
|
||||
Text(
|
||||
text = manga.title,
|
||||
color = Color.White,
|
||||
style = fontStyle,
|
||||
maxLines = 2,
|
||||
modifier = Modifier.align(Alignment.BottomStart).padding(8.dp)
|
||||
)
|
||||
SourceMangaBadges(
|
||||
inLibrary = inLibrary,
|
||||
modifier = Modifier.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val shadowGradient = Modifier.drawWithCache {
|
||||
val gradient = Brush.linearGradient(
|
||||
0.75f to Color.Transparent,
|
||||
1.0f to Color(0xAA000000),
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(0f, size.height)
|
||||
)
|
||||
onDrawBehind {
|
||||
drawRect(gradient)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.sources.globalsearch.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ArrowForward
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ca.gosyer.data.library.model.DisplayMode
|
||||
import ca.gosyer.data.models.Manga
|
||||
import ca.gosyer.data.models.Source
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.components.HorizontalScrollbar
|
||||
import ca.gosyer.ui.base.components.VerticalScrollbar
|
||||
import ca.gosyer.ui.base.components.localeToString
|
||||
import ca.gosyer.ui.base.components.rememberScrollbarAdapter
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.ui.sources.globalsearch.GlobalSearchViewModel.Search
|
||||
import ca.gosyer.uicore.components.ErrorScreen
|
||||
import ca.gosyer.uicore.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchScreenContent(
|
||||
sources: List<Source>,
|
||||
results: SnapshotStateMap<Long, Search>,
|
||||
displayMode: DisplayMode,
|
||||
query: String,
|
||||
setQuery: (String) -> Unit,
|
||||
submitSearch: (String) -> Unit,
|
||||
onSourceClick: (Source) -> Unit,
|
||||
onMangaClick: (Manga) -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Toolbar(
|
||||
name = stringResource(MR.strings.location_global_search),
|
||||
searchText = query,
|
||||
search = setQuery,
|
||||
searchSubmit = { submitSearch(query) }
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Box(Modifier.padding(padding)) {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(state = state) {
|
||||
val sourcesSuccess = sources.filter { results[it.id] is Search.Success }
|
||||
val loadingSources = sources.filter { results[it.id] == null }
|
||||
val failedSources = sources.filter { results[it.id] is Search.Failure }
|
||||
items(sourcesSuccess) {
|
||||
GlobalSearchItem(
|
||||
source = it,
|
||||
search = results[it.id] ?: Search.Searching,
|
||||
displayMode = displayMode,
|
||||
onSourceClick = onSourceClick,
|
||||
onMangaClick = onMangaClick
|
||||
)
|
||||
}
|
||||
items(loadingSources) {
|
||||
GlobalSearchItem(
|
||||
source = it,
|
||||
search = results[it.id] ?: Search.Searching,
|
||||
displayMode = displayMode,
|
||||
onSourceClick = onSourceClick,
|
||||
onMangaClick = onMangaClick
|
||||
)
|
||||
}
|
||||
items(failedSources) {
|
||||
GlobalSearchItem(
|
||||
source = it,
|
||||
search = results[it.id] ?: Search.Searching,
|
||||
displayMode = displayMode,
|
||||
onSourceClick = onSourceClick,
|
||||
onMangaClick = onMangaClick
|
||||
)
|
||||
}
|
||||
}
|
||||
VerticalScrollbar(
|
||||
modifier = Modifier.align(Alignment.CenterEnd)
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = 4.dp, vertical = 8.dp),
|
||||
adapter = rememberScrollbarAdapter(state)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchItem(
|
||||
source: Source,
|
||||
search: Search,
|
||||
displayMode: DisplayMode,
|
||||
onSourceClick: (Source) -> Unit,
|
||||
onMangaClick: (Manga) -> Unit
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable { onSourceClick(source) }
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
source.name,
|
||||
maxLines = 1,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
Text(
|
||||
localeToString(source.displayLang),
|
||||
maxLines = 1,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
Icon(Icons.Rounded.ArrowForward, stringResource(MR.strings.action_search))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
when (search) {
|
||||
is Search.Failure -> Box(Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = 16.dp), contentAlignment = Alignment.Center) {
|
||||
ErrorScreen(search.e)
|
||||
}
|
||||
Search.Searching -> Box(Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = 16.dp), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
is Search.Success -> Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val state = rememberLazyListState()
|
||||
LazyRow(Modifier.fillMaxSize(), state) {
|
||||
items(search.mangaPage.mangaList) {
|
||||
if (displayMode == DisplayMode.ComfortableGrid) {
|
||||
GlobalSearchMangaComfortableGridItem(
|
||||
Modifier.clickable { onMangaClick(it) },
|
||||
it,
|
||||
it.inLibrary
|
||||
)
|
||||
} else {
|
||||
GlobalSearchMangaCompactGridItem(
|
||||
Modifier.clickable { onMangaClick(it) },
|
||||
it,
|
||||
it.inLibrary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalScrollbar(
|
||||
rememberScrollbarAdapter(state),
|
||||
Modifier.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import ca.gosyer.ui.sources.browse.SourceScreen
|
||||
import ca.gosyer.ui.sources.components.LocalSourcesNavigator
|
||||
import ca.gosyer.ui.sources.globalsearch.GlobalSearchScreen
|
||||
import ca.gosyer.ui.sources.home.components.SourceHomeScreenContent
|
||||
import ca.gosyer.uicore.vm.viewModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
@@ -37,7 +38,12 @@ class SourceHomeScreen : Screen {
|
||||
sources = vm.sources.collectAsState().value,
|
||||
languages = vm.languages.collectAsState().value,
|
||||
sourceLanguages = vm.sourceLanguages.collectAsState().value,
|
||||
setEnabledLanguages = vm::setEnabledLanguages
|
||||
setEnabledLanguages = vm::setEnabledLanguages,
|
||||
query = vm.query.collectAsState().value,
|
||||
setQuery = vm::setQuery,
|
||||
submitSearch = {
|
||||
navigator push GlobalSearchScreen(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ class SourceHomeScreenViewModel @Inject constructor(
|
||||
sources.map { it.lang }.toSet() - setOf(Source.LOCAL_SOURCE_LANG)
|
||||
}.stateIn(scope, SharingStarted.Eagerly, emptySet())
|
||||
|
||||
private val _query = MutableStateFlow("")
|
||||
val query = _query.asStateFlow()
|
||||
|
||||
init {
|
||||
getSources()
|
||||
}
|
||||
@@ -73,5 +76,9 @@ class SourceHomeScreenViewModel @Inject constructor(
|
||||
_languages.value = langs
|
||||
}
|
||||
|
||||
fun setQuery(query: String) {
|
||||
_query.value = query
|
||||
}
|
||||
|
||||
private companion object : CKLogger({})
|
||||
}
|
||||
|
||||
@@ -64,13 +64,19 @@ fun SourceHomeScreenContent(
|
||||
sources: List<Source>,
|
||||
languages: Set<String>,
|
||||
sourceLanguages: Set<String>,
|
||||
setEnabledLanguages: (Set<String>) -> Unit
|
||||
setEnabledLanguages: (Set<String>) -> Unit,
|
||||
query: String,
|
||||
setQuery: (String) -> Unit,
|
||||
submitSearch: (String) -> Unit
|
||||
) {
|
||||
val languageDialogState = rememberMaterialDialogState()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SourceHomeScreenToolbar(
|
||||
languageDialogState::show
|
||||
openEnabledLanguagesClick = languageDialogState::show,
|
||||
query = query,
|
||||
setQuery = setQuery,
|
||||
submitSearch = submitSearch
|
||||
)
|
||||
}
|
||||
) {
|
||||
@@ -106,7 +112,10 @@ fun SourceHomeScreenContent(
|
||||
|
||||
@Composable
|
||||
fun SourceHomeScreenToolbar(
|
||||
openEnabledLanguagesClick: () -> Unit
|
||||
openEnabledLanguagesClick: () -> Unit,
|
||||
query: String,
|
||||
setQuery: (String) -> Unit,
|
||||
submitSearch: (String) -> Unit
|
||||
) {
|
||||
Toolbar(
|
||||
stringResource(MR.strings.location_sources),
|
||||
@@ -114,6 +123,11 @@ fun SourceHomeScreenToolbar(
|
||||
getActionItems(
|
||||
openEnabledLanguagesClick = openEnabledLanguagesClick
|
||||
)
|
||||
},
|
||||
searchText = query,
|
||||
search = setQuery,
|
||||
searchSubmit = {
|
||||
submitSearch(query)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user