From 6d9a8a30e974574b8d92ba478333e6f62b0de2e2 Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 25 Aug 2023 22:25:00 -0400 Subject: [PATCH] Add ResolvableSource interface for potentially opening entries directly based on some URI via a share intent Implemented as an intermediate step in the existing Global Search share intent workflow. If any source manages to resolve the URI (e.g., a URL, a slug, etc.), the resolved SManga entry is directly opened. If nothing gets resolved, continue to a Global Search. --- app/src/main/AndroidManifest.xml | 4 +- .../presentation/browse/ExtensionsScreen.kt | 2 +- .../browse/MigrateSourceScreen.kt | 2 +- .../presentation/browse/SourcesScreen.kt | 2 +- .../presentation/history/HistoryScreen.kt | 2 +- .../presentation/updates/UpdatesScreen.kt | 2 +- .../ui/{main => deeplink}/DeepLinkActivity.kt | 3 +- .../tachiyomi/ui/deeplink/DeepLinkScreen.kt | 59 +++++++++++++++++++ .../ui/deeplink/DeepLinkScreenModel.kt | 47 +++++++++++++++ .../kanade/tachiyomi/ui/library/LibraryTab.kt | 2 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 3 +- .../source/online/ResolvableSource.kt | 26 ++++++++ 12 files changed, 144 insertions(+), 10 deletions(-) rename app/src/main/java/eu/kanade/tachiyomi/ui/{main => deeplink}/DeepLinkActivity.kt (84%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreen.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt create mode 100644 source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ResolvableSource.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 07e296b895..f8034f7110 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -65,10 +65,10 @@ android:exported="false" /> diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt index 433add984d..fab831da7c 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -76,7 +76,7 @@ fun ExtensionScreen( enabled = !state.isLoading, ) { when { - state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) + state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) state.isEmpty -> { val msg = if (!searchQuery.isNullOrEmpty()) { R.string.no_results_found diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt index aaf23646a3..2c4f4fc46c 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -51,7 +51,7 @@ fun MigrateSourceScreen( ) { val context = LocalContext.current when { - state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) + state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) state.isEmpty -> EmptyScreen( textResource = R.string.information_empty_library, modifier = Modifier.padding(contentPadding), diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt index 12175e4bdd..de54345955 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt @@ -47,7 +47,7 @@ fun SourcesScreen( onLongClickItem: (Source) -> Unit, ) { when { - state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) + state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) state.isEmpty -> EmptyScreen( textResource = R.string.source_empty_screen, modifier = Modifier.padding(contentPadding), diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index 1a80c8f016..3bac670d0e 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -65,7 +65,7 @@ fun HistoryScreen( ) { contentPadding -> state.list.let { if (it == null) { - LoadingScreen(modifier = Modifier.padding(contentPadding)) + LoadingScreen(Modifier.padding(contentPadding)) } else if (it.isEmpty()) { val msg = if (!state.searchQuery.isNullOrEmpty()) { R.string.no_results_found diff --git a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt index dbb5e3305d..1f4a56d5f1 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -81,7 +81,7 @@ fun UpdateScreen( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { contentPadding -> when { - state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) + state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) state.items.isEmpty() -> EmptyScreen( textResource = R.string.information_no_recent, modifier = Modifier.padding(contentPadding), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/DeepLinkActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkActivity.kt similarity index 84% rename from app/src/main/java/eu/kanade/tachiyomi/ui/main/DeepLinkActivity.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkActivity.kt index 1e381c09a6..0635b03bb9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/DeepLinkActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkActivity.kt @@ -1,8 +1,9 @@ -package eu.kanade.tachiyomi.ui.main +package eu.kanade.tachiyomi.ui.deeplink import android.app.Activity import android.content.Intent import android.os.Bundle +import eu.kanade.tachiyomi.ui.main.MainActivity class DeepLinkActivity : Activity() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreen.kt new file mode 100644 index 0000000000..4b7c989dca --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreen.kt @@ -0,0 +1,59 @@ +package eu.kanade.tachiyomi.ui.deeplink + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.util.Screen +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen +import eu.kanade.tachiyomi.ui.manga.MangaScreen +import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.screens.LoadingScreen + +class DeepLinkScreen( + val query: String = "", +) : Screen() { + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + + val screenModel = rememberScreenModel { + DeepLinkScreenModel(query = query) + } + val state by screenModel.state.collectAsState() + Scaffold( + topBar = { scrollBehavior -> + AppBar( + title = stringResource(R.string.action_search_hint), + navigateUp = navigator::pop, + scrollBehavior = scrollBehavior, + ) + }, + ) { contentPadding -> + when (state) { + is DeepLinkScreenModel.State.Loading -> { + LoadingScreen(Modifier.padding(contentPadding)) + } + is DeepLinkScreenModel.State.NoResults -> { + navigator.replace(GlobalSearchScreen(query)) + } + is DeepLinkScreenModel.State.Result -> { + navigator.replace( + MangaScreen( + (state as DeepLinkScreenModel.State.Result).manga.id, + true, + ), + ) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt new file mode 100644 index 0000000000..4446c28a48 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt @@ -0,0 +1,47 @@ +package eu.kanade.tachiyomi.ui.deeplink + +import androidx.compose.runtime.Immutable +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import eu.kanade.domain.manga.model.toDomainManga +import eu.kanade.tachiyomi.source.online.ResolvableSource +import kotlinx.coroutines.flow.update +import tachiyomi.core.util.lang.launchIO +import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.source.service.SourceManager +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class DeepLinkScreenModel( + query: String = "", + private val sourceManager: SourceManager = Injekt.get(), +) : StateScreenModel(State.Loading) { + + init { + coroutineScope.launchIO { + val manga = sourceManager.getCatalogueSources() + .filterIsInstance() + .filter { it.canResolveUri(query) } + .firstNotNullOfOrNull { it.getManga(query)?.toDomainManga(it.id) } + + mutableState.update { + if (manga == null) { + State.NoResults + } else { + State.Result(manga) + } + } + } + } + + sealed interface State { + @Immutable + data object Loading : State + + @Immutable + data object NoResults : State + + @Immutable + data class Result(val manga: Manga) : State + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index c3a11d0349..cde9762d57 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt @@ -148,7 +148,7 @@ object LibraryTab : Tab { snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { contentPadding -> when { - state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) + state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) state.searchQuery.isNullOrEmpty() && !state.hasActiveFilters && state.isLibraryEmpty -> { val handler = LocalUriHandler.current EmptyScreen( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index e2516ea7cb..868a8cee7e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -71,6 +71,7 @@ import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen +import eu.kanade.tachiyomi.ui.deeplink.DeepLinkScreen import eu.kanade.tachiyomi.ui.home.HomeScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.more.NewUpdateScreen @@ -409,7 +410,7 @@ class MainActivity : BaseActivity() { val query = intent.getStringExtra(SearchManager.QUERY) ?: intent.getStringExtra(Intent.EXTRA_TEXT) if (!query.isNullOrEmpty()) { navigator.popUntilRoot() - navigator.push(GlobalSearchScreen(query)) + navigator.push(DeepLinkScreen(query)) } null } diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ResolvableSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ResolvableSource.kt new file mode 100644 index 0000000000..6a00c2e557 --- /dev/null +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ResolvableSource.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.source.online + +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.model.SManga + +/** + * A source that may handle opening an SManga for a given URI. + * + * @since extensions-lib 1.5 + */ +interface ResolvableSource : Source { + + /** + * Whether this source may potentially handle the given URI. + * + * @since extensions-lib 1.5 + */ + fun canResolveUri(uri: String): Boolean + + /** + * Called if canHandleUri is true. Returns the corresponding SManga, if possible. + * + * @since extensions-lib 1.5 + */ + suspend fun getManga(uri: String): SManga? +}