Browse Source

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

* open manga and chapter using URL

* removing unnnecessary logs

* Resolving comments

* Resolving comments
Joshua Owolabi 1 year ago
parent
commit
f84868a264

+ 4 - 0
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<ChapterRepository> { ChapterRepositoryImpl(get()) }
         addFactory { GetChapter(get()) }
         addFactory { GetChapterByMangaId(get()) }
+        addFactory { GetChapterByUrlAndMangaId(get()) }
         addFactory { UpdateChapter(get()) }
         addFactory { SetReadStatus(get(), get(), get(), get()) }
         addFactory { ShouldUpdateDbChapter() }

+ 19 - 6
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)
+                    }
                 }
             }
         }

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

+ 17 - 0
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
+        }
+    }
+}

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

+ 7 - 0
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<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
      * in the expected order; the index is ignored.

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