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:
parent
2bf263e301
commit
6d9a8a30e9
12 changed files with 144 additions and 10 deletions
|
@ -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" />
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?
|
||||||
|
}
|
Reference in a new issue