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:
parent
15423bfc84
commit
f84868a264
7 changed files with 125 additions and 13 deletions
|
@ -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() }
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue