Use Voyager for a few screens

This commit is contained in:
Jobobby04 2022-11-26 13:36:06 -05:00
parent bf9b2ca2ff
commit 7df12c68fd
13 changed files with 333 additions and 371 deletions

View File

@ -6,7 +6,7 @@ import androidx.core.os.bundleOf
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
import eu.kanade.tachiyomi.util.system.getParcelableCompat
import eu.kanade.tachiyomi.util.system.getSerializableCompat
class MangaController : BasicFullComposeController {
@ -33,9 +33,7 @@ class MangaController : BasicFullComposeController {
// SY -->
val smartSearchConfig: SourcesController.SmartSearchConfig?
get() = args.getParcelableCompat<SourcesController.SmartSearchConfig>(
SMART_SEARCH_CONFIG_EXTRA,
)
get() = args.getSerializableCompat(SMART_SEARCH_CONFIG_EXTRA)
// SY <--
@Composable

View File

@ -86,7 +86,7 @@ import exh.recs.RecommendsController
import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource
import exh.source.isMdBasedSource
import exh.ui.metadata.MetadataViewController
import exh.ui.metadata.MetadataViewScreen
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -166,7 +166,7 @@ class MangaScreen(
onEditCategoryClicked = screenModel::promptChangeCategories.takeIf { successState.manga.favorite },
onMigrateClicked = { migrateManga(router, screenModel.manga!!) }.takeIf { successState.manga.favorite },
// SY -->
onMetadataViewerClicked = { openMetadataViewer(router, successState.manga) },
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
onEditInfoClicked = screenModel::showEditMangaInfoDialog,
onRecommendClicked = { openRecommends(context, router, screenModel.source?.getMainSource(), successState.manga) },
onMergedSettingsClicked = screenModel::showEditMergedSettingsDialog,
@ -446,8 +446,8 @@ class MangaScreen(
}
// SY -->
private fun openMetadataViewer(router: Router, manga: Manga) {
router.pushController(MetadataViewController(manga))
private fun openMetadataViewer(navigator: Navigator, manga: Manga) {
navigator.push(MetadataViewScreen(manga.id, manga.source))
}
private fun openMergedMangaWebview(context: Context, mergedMangaData: MergedMangaData) {

View File

@ -2,13 +2,11 @@ package exh.pagepreview
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.core.os.bundleOf
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import exh.pagepreview.components.PagePreviewScreen
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
class PagePreviewController : FullComposeController<PagePreviewPresenter> {
class PagePreviewController : BasicFullComposeController {
@Suppress("unused")
constructor(bundle: Bundle? = null) : super(bundle)
@ -17,26 +15,9 @@ class PagePreviewController : FullComposeController<PagePreviewPresenter> {
bundleOf(MANGA_ID to mangaId),
)
override fun createPresenter() = PagePreviewPresenter(args.getLong(MANGA_ID, -1))
@Composable
override fun ComposeContent() {
PagePreviewScreen(
state = presenter.state.collectAsState().value,
pageDialogOpen = presenter.pageDialogOpen,
onPageSelected = presenter::moveToPage,
onOpenPage = this::openPage,
onOpenPageDialog = { presenter.pageDialogOpen = true },
onDismissPageDialog = { presenter.pageDialogOpen = false },
navigateUp = router::popCurrentController,
)
}
fun openPage(page: Int) {
val state = presenter.state.value as? PagePreviewState.Success ?: return
activity?.run {
startActivity(ReaderActivity.newIntent(this, state.manga.id, state.chapter.id, page))
}
Navigator(screen = PagePreviewScreen(args.getLong(MANGA_ID, -1)))
}
companion object {

View File

@ -0,0 +1,40 @@
package exh.pagepreview
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import exh.pagepreview.components.PagePreviewScreen
class PagePreviewScreen(private val mangaId: Long) : Screen {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { PagePreviewScreenModel(mangaId) }
val context = LocalContext.current
val state by screenModel.state.collectAsState()
val router = LocalRouter.currentOrThrow
PagePreviewScreen(
state = state,
pageDialogOpen = screenModel.pageDialogOpen,
onPageSelected = screenModel::moveToPage,
onOpenPage = { openPage(context, state, it) },
onOpenPageDialog = { screenModel.pageDialogOpen = true },
onDismissPageDialog = { screenModel.pageDialogOpen = false },
navigateUp = router::popCurrentController,
)
}
fun openPage(context: Context, state: PagePreviewState, page: Int) {
if (state !is PagePreviewState.Success) return
context.run {
startActivity(ReaderActivity.newIntent(this, state.manga.id, state.chapter.id, page))
}
}
}

View File

@ -1,9 +1,10 @@
package exh.pagepreview
import android.os.Bundle
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.history.interactor.GetNextChapters
import eu.kanade.domain.manga.interactor.GetManga
@ -12,10 +13,8 @@ import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.PagePreview
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
@ -23,29 +22,24 @@ import kotlinx.coroutines.flow.update
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class PagePreviewPresenter(
class PagePreviewScreenModel(
private val mangaId: Long,
private val getPagePreviews: GetPagePreviews = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val getNextChapters: GetNextChapters = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
) : BasePresenter<PagePreviewController>() {
private val _state = MutableStateFlow<PagePreviewState>(PagePreviewState.Loading)
val state = _state.asStateFlow()
) : StateScreenModel<PagePreviewState>(PagePreviewState.Loading) {
private val page = MutableStateFlow(1)
var pageDialogOpen by mutableStateOf(false)
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
presenterScope.launchIO {
init {
coroutineScope.launchIO {
val manga = getManga.await(mangaId)!!
val chapter = getNextChapters.await(mangaId, onlyUnread = false).lastOrNull()
if (chapter == null) {
_state.update {
mutableState.update {
PagePreviewState.Error(Exception("No chapters found"))
}
return@launchIO
@ -56,10 +50,10 @@ class PagePreviewPresenter(
when (
val previews = getPagePreviews.await(manga, source, page)
) {
is GetPagePreviews.Result.Error -> _state.update {
is GetPagePreviews.Result.Error -> mutableState.update {
PagePreviewState.Error(previews.error)
}
is GetPagePreviews.Result.Success -> _state.update {
is GetPagePreviews.Result.Success -> mutableState.update {
when (it) {
PagePreviewState.Loading, is PagePreviewState.Error -> {
PagePreviewState.Success(
@ -84,7 +78,7 @@ class PagePreviewPresenter(
}
}
.catch { e ->
_state.update {
mutableState.update {
PagePreviewState.Error(e)
}
}

View File

@ -1,68 +0,0 @@
package exh.ui.base
import android.os.Bundle
import androidx.annotation.CallSuper
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import nucleus.presenter.Presenter
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@Suppress("DEPRECATION", "unused")
open class CoroutinePresenter<V>(
private val scope: () -> CoroutineScope = ::MainScope,
) : Presenter<V>() {
var presenterScope: CoroutineScope = scope()
@CallSuper
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
if (!presenterScope.isActive) {
presenterScope = scope()
}
}
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("Use launchInView, Flow.inView, Flow.mapView")
override fun getView(): V? {
return super.getView()
}
fun launchInView(block: (CoroutineScope, V) -> Unit) = presenterScope.launchUI {
view?.let { block.invoke(this, it) }
}
inline fun <F> Flow<F>.inView(crossinline block: (V, F) -> Unit) = onEach {
withUIContext {
view?.let { view -> block(view, it) }
}
}
inline fun <F, P> Flow<F>.mapView(crossinline block: (V, F) -> P): Flow<P> {
return mapNotNull {
withUIContext {
view?.let { view -> block(view, it) }
}
}
}
fun Flow<*>.launchUnderContext(context: CoroutineContext = EmptyCoroutineContext) = flowOn(context)
.launch()
fun Flow<*>.launch() = launchIn(presenterScope)
@CallSuper
override fun onDestroy() {
super.onDestroy()
presenterScope.cancel()
}
}

View File

@ -1,128 +1,30 @@
package exh.ui.metadata
import android.os.Bundle
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import eu.kanade.domain.manga.interactor.GetManga
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.clickableNoIndication
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topSmallPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
class MetadataViewController : FullComposeController<MetadataViewPresenter> {
class MetadataViewController : BasicFullComposeController {
constructor(manga: Manga) : super(
bundleOf(
MangaController.MANGA_EXTRA to manga.id,
MANGA_EXTRA to manga.id,
SOURCE_EXTRA to manga.source,
),
) {
this.manga = manga
source = Injekt.get<SourceManager>().getOrStub(manga.source)
}
constructor(mangaId: Long) : this(
runBlocking { Injekt.get<GetManga>().await(mangaId)!! },
)
@Suppress("unused")
constructor(bundle: Bundle) : this(bundle.getLong(MangaController.MANGA_EXTRA))
var manga: Manga? = null
private set
var source: Source? = null
private set
override fun createPresenter(): MetadataViewPresenter {
return MetadataViewPresenter(manga!!, source!!)
}
constructor(bundle: Bundle) : super(bundle)
@Composable
override fun ComposeContent() {
val state by presenter.state.collectAsState()
Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = manga?.title,
navigateUp = router::popCurrentController,
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
when (val state = state) {
MetadataViewState.Loading -> LoadingScreen()
MetadataViewState.MetadataNotFound -> EmptyScreen(R.string.no_results_found)
MetadataViewState.SourceNotFound -> EmptyScreen(R.string.source_empty_screen)
is MetadataViewState.Success -> {
val context = LocalContext.current
val items = remember(state.meta) { state.meta.getExtraInfoPairs(context) }
ScrollbarLazyColumn(
contentPadding = paddingValues + WindowInsets.navigationBars.asPaddingValues() + topSmallPaddingValues,
) {
items(items) { (title, text) ->
Row(
Modifier
.fillMaxWidth()
.clickableNoIndication(
onLongClick = {
context.copyToClipboard(
title,
text,
)
},
onClick = {},
)
.padding(vertical = 8.dp),
) {
Text(
title,
modifier = Modifier
.width(140.dp)
.padding(start = 16.dp),
style = MaterialTheme.typography.bodyMedium,
)
Text(
text,
modifier = Modifier
.fillMaxWidth()
.padding(start = 8.dp, end = 8.dp),
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = 0.7F),
)
}
}
}
}
}
}
Navigator(screen = MetadataViewScreen(args.getLong(MANGA_EXTRA), args.getLong(SOURCE_EXTRA)))
}
companion object {
const val MANGA_EXTRA = "manga"
const val SOURCE_EXTRA = "source"
}
}

View File

@ -1,48 +0,0 @@
package exh.ui.metadata
import android.os.Bundle
import eu.kanade.domain.manga.interactor.GetFlatMetadataById
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.getMainSource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MetadataViewPresenter(
val manga: Manga,
val source: Source,
private val getFlatMetadataById: GetFlatMetadataById = Injekt.get(),
) : BasePresenter<MetadataViewController>() {
private val _state = MutableStateFlow<MetadataViewState>(MetadataViewState.Loading)
val state = _state.asStateFlow()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
presenterScope.launchNonCancellable {
val metadataSource = source.getMainSource<MetadataSource<*, *>>()
if (metadataSource == null) {
_state.value = MetadataViewState.SourceNotFound
return@launchNonCancellable
}
_state.value = when (val flatMetadata = getFlatMetadataById.await(manga.id)) {
null -> MetadataViewState.MetadataNotFound
else -> MetadataViewState.Success(flatMetadata.raise(metadataSource.metaClass))
}
}
}
}
sealed class MetadataViewState {
object Loading : MetadataViewState()
data class Success(val meta: RaisedSearchMetadata) : MetadataViewState()
object MetadataNotFound : MetadataViewState()
object SourceNotFound : MetadataViewState()
}

View File

@ -0,0 +1,100 @@
package exh.ui.metadata
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.clickableNoIndication
import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topSmallPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.copyToClipboard
class MetadataViewScreen(private val mangaId: Long, private val sourceId: Long) : Screen {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { MetadataViewScreenModel(mangaId, sourceId) }
val navigator = LocalNavigator.currentOrThrow
val state by screenModel.state.collectAsState()
Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = screenModel.manga.collectAsState().value?.title,
navigateUp = navigator::pop,
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
when (val state = state) {
MetadataViewState.Loading -> LoadingScreen()
MetadataViewState.MetadataNotFound -> EmptyScreen(R.string.no_results_found)
MetadataViewState.SourceNotFound -> EmptyScreen(R.string.source_empty_screen)
is MetadataViewState.Success -> {
val context = LocalContext.current
val items = remember(state.meta) { state.meta.getExtraInfoPairs(context) }
ScrollbarLazyColumn(
contentPadding = paddingValues + WindowInsets.navigationBars.asPaddingValues() + topSmallPaddingValues,
) {
items(items) { (title, text) ->
Row(
Modifier
.fillMaxWidth()
.clickableNoIndication(
onLongClick = {
context.copyToClipboard(
title,
text,
)
},
onClick = {},
)
.padding(vertical = 8.dp),
) {
Text(
title,
modifier = Modifier
.width(140.dp)
.padding(start = 16.dp),
style = MaterialTheme.typography.bodyMedium,
)
Text(
text,
modifier = Modifier
.fillMaxWidth()
.padding(start = 8.dp, end = 8.dp),
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = 0.7F),
)
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,55 @@
package exh.ui.metadata
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.domain.manga.interactor.GetFlatMetadataById
import eu.kanade.domain.manga.interactor.GetManga
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.util.lang.launchIO
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.getMainSource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MetadataViewScreenModel(
val mangaId: Long,
val sourceId: Long,
private val getFlatMetadataById: GetFlatMetadataById = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
) : StateScreenModel<MetadataViewState>(MetadataViewState.Loading) {
private val _manga = MutableStateFlow<Manga?>(null)
val manga = _manga.asStateFlow()
init {
coroutineScope.launchIO {
_manga.value = getManga.await(mangaId)
}
}
init {
coroutineScope.launchIO {
val metadataSource = sourceManager.get(sourceId)?.getMainSource<MetadataSource<*, *>>()
if (metadataSource == null) {
mutableState.value = MetadataViewState.SourceNotFound
return@launchIO
}
mutableState.value = when (val flatMetadata = getFlatMetadataById.await(mangaId)) {
null -> MetadataViewState.MetadataNotFound
else -> MetadataViewState.Success(flatMetadata.raise(metadataSource.metaClass))
}
}
}
}
sealed class MetadataViewState {
object Loading : MetadataViewState()
data class Success(val meta: RaisedSearchMetadata) : MetadataViewState()
object MetadataNotFound : MetadataViewState()
object SourceNotFound : MetadataViewState()
}

View File

@ -1,42 +1,16 @@
package exh.ui.smartsearch
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.getParcelableCompat
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.util.system.getSerializableCompat
class SmartSearchController(bundle: Bundle) : FullComposeController<SmartSearchPresenter>() {
private val sourceManager: SourceManager by injectLazy()
private val source = sourceManager.get(bundle.getLong(ARG_SOURCE_ID, -1)) as CatalogueSource
private val smartSearchConfig = bundle.getParcelableCompat<SourcesController.SmartSearchConfig>(ARG_SMART_SEARCH_CONFIG)!!
class SmartSearchController(bundle: Bundle) : BasicFullComposeController() {
private val sourceId = bundle.getLong(ARG_SOURCE_ID, -1)
private val smartSearchConfig = bundle.getSerializableCompat<SourcesController.SmartSearchConfig>(ARG_SMART_SEARCH_CONFIG)!!
constructor(sourceId: Long, smartSearchConfig: SourcesController.SmartSearchConfig) : this(
bundleOf(
@ -45,58 +19,9 @@ class SmartSearchController(bundle: Bundle) : FullComposeController<SmartSearchP
),
)
override fun createPresenter() = SmartSearchPresenter(source, smartSearchConfig)
@Composable
override fun ComposeContent() {
Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = source.name,
navigateUp = router::popCurrentController,
scrollBehavior = scrollBehavior,
)
},
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(it),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
) {
Text(
text = stringResource(R.string.searching_source),
style = MaterialTheme.typography.titleLarge,
)
CircularProgressIndicator(modifier = Modifier.size(56.dp))
}
}
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
presenter.smartSearchFlow
.onEach { results ->
if (results is SmartSearchPresenter.SearchResults.Found) {
val transaction = MangaController(results.manga.id, true, smartSearchConfig).withFadeTransaction()
router.replaceTopController(transaction)
} else {
if (results is SmartSearchPresenter.SearchResults.NotFound) {
applicationContext?.toast(R.string.could_not_find_manga)
} else {
applicationContext?.toast(R.string.automatic_search_error)
}
val transaction = BrowseSourceController(
source,
smartSearchConfig.origTitle,
smartSearchConfig,
).withFadeTransaction()
router.replaceTopController(transaction)
}
}
.launchIn(viewScope)
Navigator(screen = SmartSearchScreen(sourceId, smartSearchConfig))
}
companion object {

View File

@ -0,0 +1,88 @@
package exh.ui.smartsearch
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.toast
class SmartSearchScreen(private val sourceId: Long, private val smartSearchConfig: SourcesController.SmartSearchConfig) : Screen {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { SmartSearchScreenModel(sourceId, smartSearchConfig) }
val router = LocalRouter.currentOrThrow
val context = LocalContext.current
val state by screenModel.state.collectAsState()
LaunchedEffect(state) {
val results = state
if (results != null) {
if (results is SmartSearchScreenModel.SearchResults.Found) {
val transaction = MangaController(results.manga.id, true, smartSearchConfig).withFadeTransaction()
router.replaceTopController(transaction)
} else {
if (results is SmartSearchScreenModel.SearchResults.NotFound) {
context.toast(R.string.could_not_find_manga)
} else {
context.toast(R.string.automatic_search_error)
}
val transaction = BrowseSourceController(
screenModel.source,
smartSearchConfig.origTitle,
smartSearchConfig,
).withFadeTransaction()
router.replaceTopController(transaction)
}
}
}
Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = screenModel.source.name,
navigateUp = router::popCurrentController,
scrollBehavior = scrollBehavior,
)
},
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(it),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
) {
Text(
text = stringResource(R.string.searching_source),
style = MaterialTheme.typography.titleLarge,
)
CircularProgressIndicator(modifier = Modifier.size(56.dp))
}
}
}
}

View File

@ -1,35 +1,30 @@
package exh.ui.smartsearch
import android.os.Bundle
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.domain.manga.interactor.NetworkToLocalManga
import eu.kanade.domain.manga.model.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
import eu.kanade.tachiyomi.util.lang.launchIO
import exh.smartsearch.SmartSearchEngine
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SmartSearchPresenter(
private val source: CatalogueSource,
class SmartSearchScreenModel(
private val sourceId: Long,
private val config: SourcesController.SmartSearchConfig,
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
) :
BasePresenter<SmartSearchController>() {
private val _smartSearchFlow = MutableSharedFlow<SearchResults>()
val smartSearchFlow = _smartSearchFlow.asSharedFlow()
private val sourceManager: SourceManager = Injekt.get(),
) : StateScreenModel<SmartSearchScreenModel.SearchResults?>(null) {
private val smartSearchEngine = SmartSearchEngine()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
val source = sourceManager.get(sourceId) as CatalogueSource
presenterScope.launchIO {
init {
coroutineScope.launchIO {
val result = try {
val resultManga = smartSearchEngine.smartSearch(source, config.origTitle)
if (resultManga != null) {
@ -46,7 +41,7 @@ class SmartSearchPresenter(
}
}
_smartSearchFlow.emit(result)
mutableState.value = result
}
}