Allow extensions to open manga or chapter by URL (#9996)

* open manga and chapter using URL

* removing unnnecessary logs

* Resolving comments

* Resolving comments
This commit is contained in:
Joshua Owolabi 2023-10-22 02:44:43 +01:00 committed by GitHub
parent 15423bfc84
commit f84868a264
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 125 additions and 13 deletions

View File

@ -41,6 +41,7 @@ import tachiyomi.domain.category.interactor.UpdateCategory
import tachiyomi.domain.category.repository.CategoryRepository import tachiyomi.domain.category.repository.CategoryRepository
import tachiyomi.domain.chapter.interactor.GetChapter import tachiyomi.domain.chapter.interactor.GetChapter
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.interactor.UpdateChapter
@ -56,6 +57,7 @@ import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.manga.interactor.GetFavorites import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.interactor.GetLibraryManga import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.manga.interactor.GetMangaWithChapters import tachiyomi.domain.manga.interactor.GetMangaWithChapters
import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags import tachiyomi.domain.manga.interactor.ResetViewerFlags
@ -99,6 +101,7 @@ class DomainModule : InjektModule {
addFactory { GetFavorites(get()) } addFactory { GetFavorites(get()) }
addFactory { GetLibraryManga(get()) } addFactory { GetLibraryManga(get()) }
addFactory { GetMangaWithChapters(get(), get()) } addFactory { GetMangaWithChapters(get(), get()) }
addFactory { GetMangaByUrlAndSourceId(get()) }
addFactory { GetManga(get()) } addFactory { GetManga(get()) }
addFactory { GetNextChapters(get(), get(), get()) } addFactory { GetNextChapters(get(), get(), get()) }
addFactory { ResetViewerFlags(get()) } addFactory { ResetViewerFlags(get()) }
@ -126,6 +129,7 @@ class DomainModule : InjektModule {
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) } addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetChapter(get()) } addFactory { GetChapter(get()) }
addFactory { GetChapterByMangaId(get()) } addFactory { GetChapterByMangaId(get()) }
addFactory { GetChapterByUrlAndMangaId(get()) }
addFactory { UpdateChapter(get()) } addFactory { UpdateChapter(get()) }
addFactory { SetReadStatus(get(), get(), get(), get()) } addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() } addFactory { ShouldUpdateDbChapter() }

View File

@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@ -14,6 +15,7 @@ import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.LoadingScreen import tachiyomi.presentation.core.screens.LoadingScreen
@ -23,6 +25,7 @@ class DeepLinkScreen(
@Composable @Composable
override fun Content() { override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { val screenModel = rememberScreenModel {
@ -46,12 +49,22 @@ class DeepLinkScreen(
navigator.replace(GlobalSearchScreen(query)) navigator.replace(GlobalSearchScreen(query))
} }
is DeepLinkScreenModel.State.Result -> { is DeepLinkScreenModel.State.Result -> {
navigator.replace( val resultState = state as DeepLinkScreenModel.State.Result
MangaScreen( if (resultState.chapterId == null) {
(state as DeepLinkScreenModel.State.Result).manga.id, navigator.replace(
true, MangaScreen(
), resultState.manga.id,
) true,
),
)
} else {
navigator.pop()
ReaderActivity.newIntent(
context,
resultState.manga.id,
resultState.chapterId,
).also(context::startActivity)
}
} }
} }
} }

View File

@ -3,10 +3,20 @@ package eu.kanade.tachiyomi.ui.deeplink
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.model.toDomainManga import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ResolvableSource import eu.kanade.tachiyomi.source.online.ResolvableSource
import eu.kanade.tachiyomi.source.online.UriType
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchIO
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -15,25 +25,59 @@ import uy.kohesive.injekt.api.get
class DeepLinkScreenModel( class DeepLinkScreenModel(
query: String = "", query: String = "",
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(),
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) { ) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) {
init { init {
coroutineScope.launchIO { coroutineScope.launchIO {
val manga = sourceManager.getCatalogueSources() val source = sourceManager.getCatalogueSources()
.filterIsInstance<ResolvableSource>() .filterIsInstance<ResolvableSource>()
.filter { it.canResolveUri(query) } .firstOrNull { it.getUriType(query) != UriType.Unknown }
.firstNotNullOfOrNull { it.getManga(query)?.toDomainManga(it.id) }
val manga = source?.getManga(query)?.let {
getMangaFromSManga(it, source.id)
}
val chapter = if (source?.getUriType(query) == UriType.Chapter && manga != null) {
source.getChapter(query)?.let { getChapterFromSChapter(it, manga, source) }
} else {
null
}
mutableState.update { mutableState.update {
if (manga == null) { if (manga == null) {
State.NoResults State.NoResults
} else { } else {
State.Result(manga) if (chapter == null) {
State.Result(manga)
} else {
State.Result(manga, chapter.id)
}
} }
} }
} }
} }
private suspend fun getChapterFromSChapter(sChapter: SChapter, manga: Manga, source: Source): Chapter? {
val localChapter = getChapterByUrlAndMangaId.await(sChapter.url, manga.id)
return if (localChapter == null) {
val sourceChapters = source.getChapterList(manga.toSManga())
val newChapters = syncChaptersWithSource.await(sourceChapters, manga, source, false)
newChapters.find { it.url == sChapter.url }
} else {
localChapter
}
}
private suspend fun getMangaFromSManga(sManga: SManga, sourceId: Long): Manga {
return getMangaByUrlAndSourceId.awaitManga(sManga.url, sourceId)
?: networkToLocalManga.await(sManga.toDomainManga(sourceId))
}
sealed interface State { sealed interface State {
@Immutable @Immutable
data object Loading : State data object Loading : State
@ -42,6 +86,6 @@ class DeepLinkScreenModel(
data object NoResults : State data object NoResults : State
@Immutable @Immutable
data class Result(val manga: Manga) : State data class Result(val manga: Manga, val chapterId: Long? = null) : State
} }
} }

View File

@ -0,0 +1,17 @@
package tachiyomi.domain.chapter.interactor
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.repository.ChapterRepository
class GetChapterByUrlAndMangaId(
private val chapterRepository: ChapterRepository,
) {
suspend fun await(url: String, sourceId: Long): Chapter? {
return try {
chapterRepository.getChapterByUrlAndMangaId(url, sourceId)
} catch (e: Exception) {
null
}
}
}

View File

@ -0,0 +1,12 @@
package tachiyomi.domain.manga.interactor
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.repository.MangaRepository
class GetMangaByUrlAndSourceId(
private val mangaRepository: MangaRepository,
) {
suspend fun awaitManga(url: String, sourceId: Long): Manga? {
return mangaRepository.getMangaByUrlAndSourceId(url, sourceId)
}
}

View File

@ -289,6 +289,13 @@ abstract class HttpSource : CatalogueSource {
*/ */
protected abstract fun chapterListParse(response: Response): List<SChapter> protected abstract fun chapterListParse(response: Response): List<SChapter>
/**
* Parses the response from the site and returns a SChapter Object.
*
* @param response the response from the site.
*/
protected abstract fun chapterPageParse(response: Response): SChapter
/** /**
* Get the list of pages a chapter has. Pages should be returned * Get the list of pages a chapter has. Pages should be returned
* in the expected order; the index is ignored. * in the expected order; the index is ignored.

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.source.online package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
/** /**
@ -11,11 +12,12 @@ import eu.kanade.tachiyomi.source.model.SManga
interface ResolvableSource : Source { interface ResolvableSource : Source {
/** /**
* Whether this source may potentially handle the given URI. * Returns the UriType of the uri input.
* Returns Unknown if unable to resolve the URI
* *
* @since extensions-lib 1.5 * @since extensions-lib 1.5
*/ */
fun canResolveUri(uri: String): Boolean fun getUriType(uri: String): UriType
/** /**
* Called if canHandleUri is true. Returns the corresponding SManga, if possible. * Called if canHandleUri is true. Returns the corresponding SManga, if possible.
@ -23,4 +25,17 @@ interface ResolvableSource : Source {
* @since extensions-lib 1.5 * @since extensions-lib 1.5
*/ */
suspend fun getManga(uri: String): SManga? suspend fun getManga(uri: String): SManga?
/**
* Called if canHandleUri is true. Returns the corresponding SChapter, if possible.
*
* @since extensions-lib 1.5
*/
suspend fun getChapter(uri: String): SChapter?
}
sealed interface UriType {
object Manga : UriType
object Chapter : UriType
object Unknown : UriType
} }