mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-20 19:42:35 +01:00
Global Search
This commit is contained in:
@@ -49,6 +49,7 @@
|
|||||||
<string name="location_library">Library</string>
|
<string name="location_library">Library</string>
|
||||||
<string name="location_updates">Updates</string>
|
<string name="location_updates">Updates</string>
|
||||||
<string name="location_sources">Sources</string>
|
<string name="location_sources">Sources</string>
|
||||||
|
<string name="location_global_search">Global search</string>
|
||||||
<string name="location_extensions">Extensions</string>
|
<string name="location_extensions">Extensions</string>
|
||||||
<string name="location_downloads">Downloads</string>
|
<string name="location_downloads">Downloads</string>
|
||||||
<string name="location_settings">Settings</string>
|
<string name="location_settings">Settings</string>
|
||||||
@@ -91,6 +92,7 @@
|
|||||||
<string name="reset_filters">Reset</string>
|
<string name="reset_filters">Reset</string>
|
||||||
<string name="filter_source">Filter</string>
|
<string name="filter_source">Filter</string>
|
||||||
<string name="in_library">In library</string>
|
<string name="in_library">In library</string>
|
||||||
|
<string name="no_results_found">No results found</string>
|
||||||
|
|
||||||
<!-- Reader Menu -->
|
<!-- Reader Menu -->
|
||||||
<string name="default_reader_mode">Default</string>
|
<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.SourcesScreenViewModel
|
||||||
import ca.gosyer.ui.sources.browse.SourceScreenViewModel
|
import ca.gosyer.ui.sources.browse.SourceScreenViewModel
|
||||||
import ca.gosyer.ui.sources.browse.filter.SourceFiltersViewModel
|
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.home.SourceHomeScreenViewModel
|
||||||
import ca.gosyer.ui.sources.settings.SourceSettingsScreenViewModel
|
import ca.gosyer.ui.sources.settings.SourceSettingsScreenViewModel
|
||||||
import ca.gosyer.ui.updates.UpdatesScreenViewModel
|
import ca.gosyer.ui.updates.UpdatesScreenViewModel
|
||||||
@@ -56,6 +57,7 @@ actual class ViewModelFactoryImpl(
|
|||||||
private val sourceFiltersFactory: (params: SourceFiltersViewModel.Params) -> SourceFiltersViewModel,
|
private val sourceFiltersFactory: (params: SourceFiltersViewModel.Params) -> SourceFiltersViewModel,
|
||||||
private val sourceSettingsFactory: (params: SourceSettingsScreenViewModel.Params) -> SourceSettingsScreenViewModel,
|
private val sourceSettingsFactory: (params: SourceSettingsScreenViewModel.Params) -> SourceSettingsScreenViewModel,
|
||||||
private val sourceHomeFactory: () -> SourceHomeScreenViewModel,
|
private val sourceHomeFactory: () -> SourceHomeScreenViewModel,
|
||||||
|
private val globalSearchFactory: (params: GlobalSearchViewModel.Params) -> GlobalSearchViewModel,
|
||||||
private val sourceFactory: (params: SourceScreenViewModel.Params) -> SourceScreenViewModel,
|
private val sourceFactory: (params: SourceScreenViewModel.Params) -> SourceScreenViewModel,
|
||||||
private val sourcesFactory: () -> SourcesScreenViewModel,
|
private val sourcesFactory: () -> SourcesScreenViewModel,
|
||||||
private val updatesFactory: () -> UpdatesScreenViewModel
|
private val updatesFactory: () -> UpdatesScreenViewModel
|
||||||
@@ -84,6 +86,7 @@ actual class ViewModelFactoryImpl(
|
|||||||
SourceFiltersViewModel::class -> sourceFiltersFactory(arg1 as SourceFiltersViewModel.Params)
|
SourceFiltersViewModel::class -> sourceFiltersFactory(arg1 as SourceFiltersViewModel.Params)
|
||||||
SourceSettingsScreenViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsScreenViewModel.Params)
|
SourceSettingsScreenViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsScreenViewModel.Params)
|
||||||
SourceHomeScreenViewModel::class -> sourceHomeFactory()
|
SourceHomeScreenViewModel::class -> sourceHomeFactory()
|
||||||
|
GlobalSearchViewModel::class -> globalSearchFactory(arg1 as GlobalSearchViewModel.Params)
|
||||||
SourceScreenViewModel::class -> sourceFactory(arg1 as SourceScreenViewModel.Params)
|
SourceScreenViewModel::class -> sourceFactory(arg1 as SourceScreenViewModel.Params)
|
||||||
SourcesScreenViewModel::class -> sourcesFactory()
|
SourcesScreenViewModel::class -> sourcesFactory()
|
||||||
UpdatesScreenViewModel::class -> updatesFactory()
|
UpdatesScreenViewModel::class -> updatesFactory()
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import ca.gosyer.ui.settings.ThemesViewModel
|
|||||||
import ca.gosyer.ui.sources.SourcesScreenViewModel
|
import ca.gosyer.ui.sources.SourcesScreenViewModel
|
||||||
import ca.gosyer.ui.sources.browse.SourceScreenViewModel
|
import ca.gosyer.ui.sources.browse.SourceScreenViewModel
|
||||||
import ca.gosyer.ui.sources.browse.filter.SourceFiltersViewModel
|
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.home.SourceHomeScreenViewModel
|
||||||
import ca.gosyer.ui.sources.settings.SourceSettingsScreenViewModel
|
import ca.gosyer.ui.sources.settings.SourceSettingsScreenViewModel
|
||||||
import ca.gosyer.ui.updates.UpdatesScreenViewModel
|
import ca.gosyer.ui.updates.UpdatesScreenViewModel
|
||||||
@@ -58,6 +59,7 @@ actual class ViewModelFactoryImpl(
|
|||||||
private val sourceFiltersFactory: (params: SourceFiltersViewModel.Params) -> SourceFiltersViewModel,
|
private val sourceFiltersFactory: (params: SourceFiltersViewModel.Params) -> SourceFiltersViewModel,
|
||||||
private val sourceSettingsFactory: (params: SourceSettingsScreenViewModel.Params) -> SourceSettingsScreenViewModel,
|
private val sourceSettingsFactory: (params: SourceSettingsScreenViewModel.Params) -> SourceSettingsScreenViewModel,
|
||||||
private val sourceHomeFactory: () -> SourceHomeScreenViewModel,
|
private val sourceHomeFactory: () -> SourceHomeScreenViewModel,
|
||||||
|
private val globalSearchFactory: (params: GlobalSearchViewModel.Params) -> GlobalSearchViewModel,
|
||||||
private val sourceFactory: (params: SourceScreenViewModel.Params) -> SourceScreenViewModel,
|
private val sourceFactory: (params: SourceScreenViewModel.Params) -> SourceScreenViewModel,
|
||||||
private val sourcesFactory: () -> SourcesScreenViewModel,
|
private val sourcesFactory: () -> SourcesScreenViewModel,
|
||||||
private val updatesFactory: () -> UpdatesScreenViewModel
|
private val updatesFactory: () -> UpdatesScreenViewModel
|
||||||
@@ -87,6 +89,7 @@ actual class ViewModelFactoryImpl(
|
|||||||
SourceFiltersViewModel::class -> sourceFiltersFactory(arg1 as SourceFiltersViewModel.Params)
|
SourceFiltersViewModel::class -> sourceFiltersFactory(arg1 as SourceFiltersViewModel.Params)
|
||||||
SourceSettingsScreenViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsScreenViewModel.Params)
|
SourceSettingsScreenViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsScreenViewModel.Params)
|
||||||
SourceHomeScreenViewModel::class -> sourceHomeFactory()
|
SourceHomeScreenViewModel::class -> sourceHomeFactory()
|
||||||
|
GlobalSearchViewModel::class -> globalSearchFactory(arg1 as GlobalSearchViewModel.Params)
|
||||||
SourceScreenViewModel::class -> sourceFactory(arg1 as SourceScreenViewModel.Params)
|
SourceScreenViewModel::class -> sourceFactory(arg1 as SourceScreenViewModel.Params)
|
||||||
SourcesScreenViewModel::class -> sourcesFactory()
|
SourcesScreenViewModel::class -> sourcesFactory()
|
||||||
UpdatesScreenViewModel::class -> updatesFactory()
|
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.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
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()
|
override val key: ScreenKey = source.id.toString()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val sourceVM = viewModel {
|
val sourceVM = viewModel {
|
||||||
instantiate<SourceScreenViewModel>(SourceScreenViewModel.Params(source))
|
instantiate<SourceScreenViewModel>(SourceScreenViewModel.Params(source, initialQuery))
|
||||||
}
|
}
|
||||||
val filterVM = viewModel {
|
val filterVM = viewModel {
|
||||||
instantiate<SourceFiltersViewModel>(SourceFiltersViewModel.Params(source.id))
|
instantiate<SourceFiltersViewModel>(SourceFiltersViewModel.Params(source.id))
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ class SourceScreenViewModel(
|
|||||||
private val sourceHandler: SourceInteractionHandler,
|
private val sourceHandler: SourceInteractionHandler,
|
||||||
private val catalogPreferences: CatalogPreferences,
|
private val catalogPreferences: CatalogPreferences,
|
||||||
private val libraryPreferences: LibraryPreferences,
|
private val libraryPreferences: LibraryPreferences,
|
||||||
contextWrapper: ContextWrapper
|
contextWrapper: ContextWrapper,
|
||||||
|
initialQuery: String?
|
||||||
) : ViewModel(contextWrapper) {
|
) : ViewModel(contextWrapper) {
|
||||||
|
|
||||||
@Inject constructor(
|
@Inject constructor(
|
||||||
@@ -43,7 +44,8 @@ class SourceScreenViewModel(
|
|||||||
sourceHandler,
|
sourceHandler,
|
||||||
catalogPreferences,
|
catalogPreferences,
|
||||||
libraryPreferences,
|
libraryPreferences,
|
||||||
contextWrapper
|
contextWrapper,
|
||||||
|
params.initialQuery
|
||||||
)
|
)
|
||||||
|
|
||||||
val displayMode = catalogPreferences.displayMode().stateIn(scope)
|
val displayMode = catalogPreferences.displayMode().stateIn(scope)
|
||||||
@@ -67,10 +69,10 @@ class SourceScreenViewModel(
|
|||||||
|
|
||||||
private val _usingFilters = MutableStateFlow(false)
|
private val _usingFilters = MutableStateFlow(false)
|
||||||
|
|
||||||
private val _sourceSearchQuery = MutableStateFlow<String?>(null)
|
private val _sourceSearchQuery = MutableStateFlow(initialQuery)
|
||||||
val sourceSearchQuery = _sourceSearchQuery.asStateFlow()
|
val sourceSearchQuery = _sourceSearchQuery.asStateFlow()
|
||||||
|
|
||||||
private val _query = MutableStateFlow<String?>(null)
|
private val _query = MutableStateFlow(sourceSearchQuery.value)
|
||||||
|
|
||||||
private val _pageNum = MutableStateFlow(1)
|
private val _pageNum = MutableStateFlow(1)
|
||||||
val pageNum = _pageNum.asStateFlow()
|
val pageNum = _pageNum.asStateFlow()
|
||||||
@@ -156,7 +158,7 @@ class SourceScreenViewModel(
|
|||||||
catalogPreferences.displayMode().set(displayMode)
|
catalogPreferences.displayMode().set(displayMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Params(val source: Source)
|
data class Params(val source: Source, val initialQuery: String?)
|
||||||
|
|
||||||
private companion object : CKLogger({})
|
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 androidx.compose.runtime.collectAsState
|
||||||
import ca.gosyer.ui.sources.browse.SourceScreen
|
import ca.gosyer.ui.sources.browse.SourceScreen
|
||||||
import ca.gosyer.ui.sources.components.LocalSourcesNavigator
|
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.ui.sources.home.components.SourceHomeScreenContent
|
||||||
import ca.gosyer.uicore.vm.viewModel
|
import ca.gosyer.uicore.vm.viewModel
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
@@ -37,7 +38,12 @@ class SourceHomeScreen : Screen {
|
|||||||
sources = vm.sources.collectAsState().value,
|
sources = vm.sources.collectAsState().value,
|
||||||
languages = vm.languages.collectAsState().value,
|
languages = vm.languages.collectAsState().value,
|
||||||
sourceLanguages = vm.sourceLanguages.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)
|
sources.map { it.lang }.toSet() - setOf(Source.LOCAL_SOURCE_LANG)
|
||||||
}.stateIn(scope, SharingStarted.Eagerly, emptySet())
|
}.stateIn(scope, SharingStarted.Eagerly, emptySet())
|
||||||
|
|
||||||
|
private val _query = MutableStateFlow("")
|
||||||
|
val query = _query.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
getSources()
|
getSources()
|
||||||
}
|
}
|
||||||
@@ -73,5 +76,9 @@ class SourceHomeScreenViewModel @Inject constructor(
|
|||||||
_languages.value = langs
|
_languages.value = langs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setQuery(query: String) {
|
||||||
|
_query.value = query
|
||||||
|
}
|
||||||
|
|
||||||
private companion object : CKLogger({})
|
private companion object : CKLogger({})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,13 +64,19 @@ fun SourceHomeScreenContent(
|
|||||||
sources: List<Source>,
|
sources: List<Source>,
|
||||||
languages: Set<String>,
|
languages: Set<String>,
|
||||||
sourceLanguages: Set<String>,
|
sourceLanguages: Set<String>,
|
||||||
setEnabledLanguages: (Set<String>) -> Unit
|
setEnabledLanguages: (Set<String>) -> Unit,
|
||||||
|
query: String,
|
||||||
|
setQuery: (String) -> Unit,
|
||||||
|
submitSearch: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val languageDialogState = rememberMaterialDialogState()
|
val languageDialogState = rememberMaterialDialogState()
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
SourceHomeScreenToolbar(
|
SourceHomeScreenToolbar(
|
||||||
languageDialogState::show
|
openEnabledLanguagesClick = languageDialogState::show,
|
||||||
|
query = query,
|
||||||
|
setQuery = setQuery,
|
||||||
|
submitSearch = submitSearch
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -106,7 +112,10 @@ fun SourceHomeScreenContent(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceHomeScreenToolbar(
|
fun SourceHomeScreenToolbar(
|
||||||
openEnabledLanguagesClick: () -> Unit
|
openEnabledLanguagesClick: () -> Unit,
|
||||||
|
query: String,
|
||||||
|
setQuery: (String) -> Unit,
|
||||||
|
submitSearch: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
Toolbar(
|
Toolbar(
|
||||||
stringResource(MR.strings.location_sources),
|
stringResource(MR.strings.location_sources),
|
||||||
@@ -114,6 +123,11 @@ fun SourceHomeScreenToolbar(
|
|||||||
getActionItems(
|
getActionItems(
|
||||||
openEnabledLanguagesClick = openEnabledLanguagesClick
|
openEnabledLanguagesClick = openEnabledLanguagesClick
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
searchText = query,
|
||||||
|
search = setQuery,
|
||||||
|
searchSubmit = {
|
||||||
|
submitSearch(query)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user