From 347f98b3920a26332760e00bc6b771032e6677e7 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. (cherry picked from commit 6d9a8a30e974574b8d92ba478333e6f62b0de2e2) --- 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 d2ef65e3b..d3f59cc36 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -66,10 +66,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 f0444f93e..057aa99a8 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -79,7 +79,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 c59fb832f..2fa3e9214 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -56,7 +56,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 e3c4d76eb..d5b218d51 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt @@ -54,7 +54,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 1a80c8f01..3bac670d0 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 8c1a990bb..655577f39 100644 --- a/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/updates/UpdatesScreen.kt @@ -84,7 +84,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 1e381c09a..0635b03bb 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 000000000..4b7c989dc --- /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 000000000..4446c28a4 --- /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 adbea7fa7..e1b2e9250 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 @@ -194,7 +194,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 90038b379..bd648e537 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -69,6 +69,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 @@ -473,7 +474,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 000000000..6a00c2e55 --- /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? +}