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.
This commit is contained in:
arkon 2023-08-25 22:25:00 -04:00
parent 2bf263e301
commit 6d9a8a30e9
12 changed files with 144 additions and 10 deletions

View File

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

View File

@ -76,7 +76,7 @@ fun ExtensionScreen(
enabled = !state.isLoading, enabled = !state.isLoading,
) { ) {
when { when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> { state.isEmpty -> {
val msg = if (!searchQuery.isNullOrEmpty()) { val msg = if (!searchQuery.isNullOrEmpty()) {
R.string.no_results_found R.string.no_results_found

View File

@ -51,7 +51,7 @@ fun MigrateSourceScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
when { when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen( state.isEmpty -> EmptyScreen(
textResource = R.string.information_empty_library, textResource = R.string.information_empty_library,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),

View File

@ -47,7 +47,7 @@ fun SourcesScreen(
onLongClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit,
) { ) {
when { when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.isEmpty -> EmptyScreen( state.isEmpty -> EmptyScreen(
textResource = R.string.source_empty_screen, textResource = R.string.source_empty_screen,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),

View File

@ -65,7 +65,7 @@ fun HistoryScreen(
) { contentPadding -> ) { contentPadding ->
state.list.let { state.list.let {
if (it == null) { if (it == null) {
LoadingScreen(modifier = Modifier.padding(contentPadding)) LoadingScreen(Modifier.padding(contentPadding))
} else if (it.isEmpty()) { } else if (it.isEmpty()) {
val msg = if (!state.searchQuery.isNullOrEmpty()) { val msg = if (!state.searchQuery.isNullOrEmpty()) {
R.string.no_results_found R.string.no_results_found

View File

@ -81,7 +81,7 @@ fun UpdateScreen(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { contentPadding -> ) { contentPadding ->
when { when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.items.isEmpty() -> EmptyScreen( state.items.isEmpty() -> EmptyScreen(
textResource = R.string.information_no_recent, textResource = R.string.information_no_recent,
modifier = Modifier.padding(contentPadding), modifier = Modifier.padding(contentPadding),

View File

@ -1,8 +1,9 @@
package eu.kanade.tachiyomi.ui.main package eu.kanade.tachiyomi.ui.deeplink
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import eu.kanade.tachiyomi.ui.main.MainActivity
class DeepLinkActivity : Activity() { class DeepLinkActivity : Activity() {

View File

@ -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,
),
)
}
}
}
}
}

View File

@ -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
}
}

View File

@ -148,7 +148,7 @@ object LibraryTab : Tab {
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { contentPadding -> ) { contentPadding ->
when { when {
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding)) state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
state.searchQuery.isNullOrEmpty() && !state.hasActiveFilters && state.isLibraryEmpty -> { state.searchQuery.isNullOrEmpty() && !state.hasActiveFilters && state.isLibraryEmpty -> {
val handler = LocalUriHandler.current val handler = LocalUriHandler.current
EmptyScreen( EmptyScreen(

View File

@ -71,6 +71,7 @@ import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen 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.home.HomeScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.more.NewUpdateScreen 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) val query = intent.getStringExtra(SearchManager.QUERY) ?: intent.getStringExtra(Intent.EXTRA_TEXT)
if (!query.isNullOrEmpty()) { if (!query.isNullOrEmpty()) {
navigator.popUntilRoot() navigator.popUntilRoot()
navigator.push(GlobalSearchScreen(query)) navigator.push(DeepLinkScreen(query))
} }
null null
} }

View File

@ -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?
}