From f84868a2641578b7e07719d8f580e4668804fd50 Mon Sep 17 00:00:00 2001 From: Joshua Owolabi Date: Sun, 22 Oct 2023 02:44:43 +0100 Subject: [PATCH] Allow extensions to open manga or chapter by URL (#9996) * open manga and chapter using URL * removing unnnecessary logs * Resolving comments * Resolving comments --- .../java/eu/kanade/domain/DomainModule.kt | 4 ++ .../tachiyomi/ui/deeplink/DeepLinkScreen.kt | 25 ++++++--- .../ui/deeplink/DeepLinkScreenModel.kt | 54 +++++++++++++++++-- .../interactor/GetChapterByUrlAndMangaId.kt | 17 ++++++ .../interactor/GetMangaByUrlAndSourceId.kt | 12 +++++ .../tachiyomi/source/online/HttpSource.kt | 7 +++ .../source/online/ResolvableSource.kt | 19 ++++++- 7 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapterByUrlAndMangaId.kt create mode 100644 domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 4da261de6..8559cd923 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -41,6 +41,7 @@ import tachiyomi.domain.category.interactor.UpdateCategory import tachiyomi.domain.category.repository.CategoryRepository import tachiyomi.domain.chapter.interactor.GetChapter import tachiyomi.domain.chapter.interactor.GetChapterByMangaId +import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter 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.GetLibraryManga import tachiyomi.domain.manga.interactor.GetManga +import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId import tachiyomi.domain.manga.interactor.GetMangaWithChapters import tachiyomi.domain.manga.interactor.NetworkToLocalManga import tachiyomi.domain.manga.interactor.ResetViewerFlags @@ -99,6 +101,7 @@ class DomainModule : InjektModule { addFactory { GetFavorites(get()) } addFactory { GetLibraryManga(get()) } addFactory { GetMangaWithChapters(get(), get()) } + addFactory { GetMangaByUrlAndSourceId(get()) } addFactory { GetManga(get()) } addFactory { GetNextChapters(get(), get(), get()) } addFactory { ResetViewerFlags(get()) } @@ -126,6 +129,7 @@ class DomainModule : InjektModule { addSingletonFactory { ChapterRepositoryImpl(get()) } addFactory { GetChapter(get()) } addFactory { GetChapterByMangaId(get()) } + addFactory { GetChapterByUrlAndMangaId(get()) } addFactory { UpdateChapter(get()) } addFactory { SetReadStatus(get(), get(), get(), get()) } addFactory { ShouldUpdateDbChapter() } 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 index 4b7c989dc..87e75f46b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import cafe.adriel.voyager.core.model.rememberScreenModel 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.ui.browse.source.globalsearch.GlobalSearchScreen 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.screens.LoadingScreen @@ -23,6 +25,7 @@ class DeepLinkScreen( @Composable override fun Content() { + val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow val screenModel = rememberScreenModel { @@ -46,12 +49,22 @@ class DeepLinkScreen( navigator.replace(GlobalSearchScreen(query)) } is DeepLinkScreenModel.State.Result -> { - navigator.replace( - MangaScreen( - (state as DeepLinkScreenModel.State.Result).manga.id, - true, - ), - ) + val resultState = state as DeepLinkScreenModel.State.Result + if (resultState.chapterId == null) { + navigator.replace( + MangaScreen( + resultState.manga.id, + true, + ), + ) + } else { + navigator.pop() + ReaderActivity.newIntent( + context, + resultState.manga.id, + resultState.chapterId, + ).also(context::startActivity) + } } } } 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 index 4446c28a4..bd10f8a6c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt @@ -3,10 +3,20 @@ 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.chapter.interactor.SyncChaptersWithSource 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.UriType import kotlinx.coroutines.flow.update 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.source.service.SourceManager import uy.kohesive.injekt.Injekt @@ -15,25 +25,59 @@ import uy.kohesive.injekt.api.get class DeepLinkScreenModel( query: String = "", 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(State.Loading) { init { coroutineScope.launchIO { - val manga = sourceManager.getCatalogueSources() + val source = sourceManager.getCatalogueSources() .filterIsInstance() - .filter { it.canResolveUri(query) } - .firstNotNullOfOrNull { it.getManga(query)?.toDomainManga(it.id) } + .firstOrNull { it.getUriType(query) != UriType.Unknown } + + 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 { if (manga == null) { State.NoResults } 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 { @Immutable data object Loading : State @@ -42,6 +86,6 @@ class DeepLinkScreenModel( data object NoResults : State @Immutable - data class Result(val manga: Manga) : State + data class Result(val manga: Manga, val chapterId: Long? = null) : State } } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapterByUrlAndMangaId.kt b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapterByUrlAndMangaId.kt new file mode 100644 index 000000000..f0399d17f --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/chapter/interactor/GetChapterByUrlAndMangaId.kt @@ -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 + } + } +} diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt new file mode 100644 index 000000000..507000d82 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt @@ -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) + } +} diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 957febceb..7450a60bf 100644 --- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -289,6 +289,13 @@ abstract class HttpSource : CatalogueSource { */ protected abstract fun chapterListParse(response: Response): List + /** + * 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 * in the expected order; the index is ignored. 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 index 6a00c2e55..5885844c8 100644 --- 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 @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.source.online import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga /** @@ -11,11 +12,12 @@ import eu.kanade.tachiyomi.source.model.SManga 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 */ - fun canResolveUri(uri: String): Boolean + fun getUriType(uri: String): UriType /** * Called if canHandleUri is true. Returns the corresponding SManga, if possible. @@ -23,4 +25,17 @@ interface ResolvableSource : Source { * @since extensions-lib 1.5 */ 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 }