Browse Source

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.
arkon 1 year ago
parent
commit
6d9a8a30e9

+ 2 - 2
app/src/main/AndroidManifest.xml

@@ -65,10 +65,10 @@
             android:exported="false" />
 
         <activity
-            android:name=".ui.main.DeepLinkActivity"
+            android:name=".ui.deeplink.DeepLinkActivity"
             android:launchMode="singleTask"
             android:theme="@android:style/Theme.NoDisplay"
-            android:label="@string/action_global_search"
+            android:label="@string/action_search"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.SEARCH" />

+ 1 - 1
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

+ 1 - 1
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),

+ 1 - 1
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),

+ 1 - 1
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

+ 1 - 1
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),

+ 2 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/main/DeepLinkActivity.kt → 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() {
 

+ 59 - 0
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,
+                        ),
+                    )
+                }
+            }
+        }
+    }
+}

+ 47 - 0
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<DeepLinkScreenModel.State>(State.Loading) {
+
+    init {
+        coroutineScope.launchIO {
+            val manga = sourceManager.getCatalogueSources()
+                .filterIsInstance<ResolvableSource>()
+                .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
+    }
+}

+ 1 - 1
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(

+ 2 - 1
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
             }

+ 26 - 0
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?
+}