Adapt source implementation for TachiyomiX 1.6 (#3243)

This commit is contained in:
AntsyLich
2026-06-14 02:34:31 +06:00
committed by GitHub
parent 430b13bb81
commit 4c37f4c764
38 changed files with 597 additions and 524 deletions
@@ -35,6 +35,7 @@ import mihon.domain.extension.interactor.RemoveExtensionStore
import mihon.domain.extension.interactor.UpdateExtensionStores
import mihon.domain.extension.repository.ExtensionStoreRepository
import mihon.domain.migration.usecases.MigrateMangaUseCase
import mihon.domain.source.interactor.UpdateMangaFromRemote
import mihon.domain.upcoming.interactor.GetUpcomingManga
import tachiyomi.data.category.CategoryRepositoryImpl
import tachiyomi.data.chapter.ChapterRepositoryImpl
@@ -204,5 +205,7 @@ class DomainModule : InjektModule {
addFactory { ToggleIncognito(get()) }
addFactory { GetIncognitoState(get(), get(), get()) }
addFactory { UpdateMangaFromRemote(get(), get(), get(), get(), get(), get(), get()) }
}
}
@@ -1,17 +1,9 @@
package eu.kanade.domain.manga.interactor
import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.repository.MangaRepository
import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.ZonedDateTime
@@ -28,67 +20,6 @@ class UpdateManga(
return mangaRepository.updateAll(mangaUpdates)
}
suspend fun awaitUpdateFromSource(
localManga: Manga,
remoteManga: SManga,
manualFetch: Boolean,
coverCache: CoverCache = Injekt.get(),
libraryPreferences: LibraryPreferences = Injekt.get(),
downloadManager: DownloadManager = Injekt.get(),
): Boolean {
val remoteTitle = try {
remoteManga.title
} catch (_: UninitializedPropertyAccessException) {
""
}
// if the manga isn't a favorite (or 'update titles' preference is enabled), set its title from source and update in db
val title =
if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles.get())) {
remoteTitle
} else {
null
}
val coverLastModified =
when {
// Never refresh covers if the url is empty to avoid "losing" existing covers
remoteManga.thumbnail_url.isNullOrEmpty() -> null
!manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null
localManga.isLocal() -> Instant.now().toEpochMilli()
localManga.hasCustomCover(coverCache) -> {
coverCache.deleteFromCache(localManga, false)
null
}
else -> {
coverCache.deleteFromCache(localManga, false)
Instant.now().toEpochMilli()
}
}
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
val success = mangaRepository.update(
MangaUpdate(
id = localManga.id,
title = title,
coverLastModified = coverLastModified,
author = remoteManga.author,
artist = remoteManga.artist,
description = remoteManga.description,
genre = remoteManga.getGenres(),
thumbnailUrl = thumbnailUrl,
status = remoteManga.status.toLong(),
updateStrategy = remoteManga.update_strategy,
initialized = true,
),
)
if (success && title != null) {
downloadManager.renameManga(localManga, title)
}
return success
}
suspend fun awaitUpdateFetchInterval(
manga: Manga,
dateTime: ZonedDateTime = ZonedDateTime.now(),
@@ -10,7 +10,7 @@ import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
@@ -27,7 +27,7 @@ fun GlobalSearchScreen(
onChangeSearchFilter: (SourceFilter) -> Unit,
onToggleResults: () -> Unit,
getManga: @Composable (Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit,
onClickSource: (Source) -> Unit,
onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit,
) {
@@ -62,10 +62,10 @@ fun GlobalSearchScreen(
@Composable
internal fun GlobalSearchContent(
items: Map<CatalogueSource, SearchItemResult>,
items: Map<Source, SearchItemResult>,
contentPadding: PaddingValues,
getManga: @Composable (Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit,
onClickSource: (Source) -> Unit,
onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit,
fromSourceId: Long? = null,
@@ -3,7 +3,7 @@ package eu.kanade.presentation.browse
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import tachiyomi.domain.manga.model.Manga
@@ -19,7 +19,7 @@ fun MigrateSearchScreen(
onChangeSearchFilter: (SourceFilter) -> Unit,
onToggleResults: () -> Unit,
getManga: @Composable (Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit,
onClickSource: (Source) -> Unit,
onClickItem: (Manga) -> Unit,
onLongClickItem: (Manga) -> Unit,
) {
@@ -111,7 +111,7 @@ object SettingsTrackingScreen : SearchableSettings {
.filter { it is EnhancedTracker }
.partition { service ->
val acceptedSources = (service as EnhancedTracker).getAcceptedSources()
sourceManager.getCatalogueSources().any { it::class.qualifiedName in acceptedSources }
sourceManager.getAll().any { it::class.qualifiedName in acceptedSources }
}
var enhancedTrackerInfo = stringResource(MR.strings.enhanced_tracking_info)
if (enhancedTrackers.second.isNotEmpty()) {
@@ -28,7 +28,7 @@ class PreferenceBackupCreator(
}
fun createSource(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
return sourceManager.getCatalogueSources()
return sourceManager.getAll()
.filterIsInstance<ConfigurableSource>()
.map {
BackupSourcePreferences(
@@ -18,10 +18,6 @@ import androidx.work.WorkInfo
import androidx.work.WorkQuery
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.model.SManga
@@ -41,6 +37,7 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority
import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.core.common.util.lang.withIOContext
@@ -83,13 +80,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private val sourceManager: SourceManager = Injekt.get()
private val libraryPreferences: LibraryPreferences = Injekt.get()
private val downloadManager: DownloadManager = Injekt.get()
private val coverCache: CoverCache = Injekt.get()
private val getLibraryManga: GetLibraryManga = Injekt.get()
private val getManga: GetManga = Injekt.get()
private val updateManga: UpdateManga = Injekt.get()
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
private val fetchInterval: FetchInterval = Injekt.get()
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get()
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get()
private val notifier = LibraryUpdateNotifier(context)
@@ -331,19 +326,16 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private suspend fun updateManga(manga: Manga, fetchWindow: Pair<Long, Long>): List<Chapter> {
val source = sourceManager.getOrStub(manga.source)
// Update manga metadata if needed
if (libraryPreferences.autoUpdateMetadata.get()) {
val networkManga = source.getMangaDetails(manga.toSManga())
updateManga.awaitUpdateFromSource(manga, networkManga, manualFetch = false, coverCache)
}
val update = updateMangaFromRemote(
source = source,
manga = manga,
fetchDetails = libraryPreferences.autoUpdateMetadata.get(),
fetchChapters = true,
fetchWindow = fetchWindow,
)
.getOrThrow()
val chapters = source.getChapterList(manga.toSManga())
// Get manga from database to account for if it was removed during the update and
// to get latest data so it doesn't get overwritten later on
val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
return syncChaptersWithSource.await(chapters, dbManga, source, false, fetchWindow)
return if (update.manga.favorite) update.newChapters else emptyList()
}
private suspend fun withUpdateNotification(
@@ -10,12 +10,7 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkQuery
import androidx.work.WorkerParameters
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.copyFrom
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.workManager
@@ -27,12 +22,12 @@ import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority
import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -46,9 +41,8 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
CoroutineWorker(context, workerParams) {
private val sourceManager: SourceManager = Injekt.get()
private val coverCache: CoverCache = Injekt.get()
private val getLibraryManga: GetLibraryManga = Injekt.get()
private val updateManga: UpdateManga = Injekt.get()
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get()
private val notifier = LibraryUpdateNotifier(context)
@@ -120,14 +114,11 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
) {
val source = sourceManager.get(manga.source) ?: return@withUpdateNotification
try {
val networkManga = source.getMangaDetails(manga.toSManga())
val updatedManga = manga.prepUpdateCover(coverCache, networkManga, true)
.copyFrom(networkManga)
try {
updateManga.await(updatedManga.toMangaUpdate())
} catch (e: Exception) {
logcat(LogPriority.ERROR) { "Manga doesn't exist anymore" }
}
updateMangaFromRemote(
source = source,
manga = manga,
fetchDetails = true,
).getOrThrow()
} catch (e: Throwable) {
// Ignore errors and continue
logcat(LogPriority.ERROR, e)
@@ -10,7 +10,6 @@ import eu.kanade.domain.extension.interactor.TrustExtension
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.util.lang.Hash
@@ -301,9 +300,7 @@ internal object ExtensionLoader {
}
}
val langs = sources.filterIsInstance<CatalogueSource>()
.map { it.lang }
.toSet()
val langs = sources.map { it.lang }.toSet()
val lang = when (langs.size) {
0 -> ""
1 -> langs.first()
@@ -38,9 +38,7 @@ class AndroidSourceManager(
private val stubSourcesMap = ConcurrentHashMap<Long, StubSource>()
override val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map {
it.values.filterIsInstance<CatalogueSource>()
}
override val sources: Flow<List<Source>> = sourcesMapFlow.map { it.values.toList() }
init {
scope.launchIO {
@@ -87,9 +85,9 @@ class AndroidSourceManager(
}
}
override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance<HttpSource>()
override fun getAll() = sourcesMapFlow.value.values.toList()
override fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance<CatalogueSource>()
override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance<HttpSource>()
override fun getStubSources(): List<StubSource> {
val onlineSourceIds = getOnlineSources().map { it.id }
@@ -120,7 +120,7 @@ class ExtensionsScreenModel(
when (extension) {
is Extension.Installed -> extension.sources.any { source ->
source.name.contains(subquery, ignoreCase = true) ||
(source as? HttpSource)?.baseUrl?.contains(subquery, ignoreCase = true) == true ||
(source as? HttpSource)?.getHomeUrl()?.contains(subquery, ignoreCase = true) == true ||
source.id == subquery.toLongOrNull()
}
@@ -99,7 +99,8 @@ class ExtensionDetailsScreenModel(
val urls = extension.sources
.filterIsInstance<HttpSource>()
.mapNotNull { it.baseUrl.takeUnless { url -> url.isEmpty() } }
.flatMap { listOf(it.baseUrl, it.getHomeUrl()) }
.filter { it.isNotEmpty() }
.distinct()
val cleared = urls.sumOf {
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import kotlinx.coroutines.flow.update
@@ -21,8 +21,8 @@ class MigrateSearchScreenModel(
private val migrationSources by lazy { sourcePreferences.migrationSources.get() }
override val sortComparator = { map: Map<CatalogueSource, SearchItemResult> ->
compareBy<CatalogueSource>(
override val sortComparator = { map: Map<Source, SearchItemResult> ->
compareBy<Source>(
{ (map[it] as? SearchItemResult.Success)?.isEmpty ?: true },
{ migrationSources.indexOf(it.id) },
)
@@ -41,7 +41,7 @@ class MigrateSearchScreenModel(
}
}
override fun getEnabledSources(): List<CatalogueSource> {
return migrationSources.mapNotNull { sourceManager.get(it) as? CatalogueSource }
override fun getEnabledSources(): List<Source> {
return migrationSources.mapNotNull { sourceManager.get(it) }
}
}
@@ -110,7 +110,7 @@ data class MigrateSourceSearchScreen(
val source = screenModel.source as? HttpSource ?: return@BrowseSourceContent
navigator.push(
WebViewScreen(
url = source.baseUrl,
url = source.getHomeUrl(),
initialTitle = source.name,
sourceId = source.id,
),
@@ -44,7 +44,6 @@ import eu.kanade.presentation.category.components.ChangeCategoryDialog
import eu.kanade.presentation.manga.DuplicateMangaDialog
import eu.kanade.presentation.util.AssistContentScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
@@ -111,7 +110,7 @@ data class BrowseSourceScreen(
val source = screenModel.source as? HttpSource ?: return@f
navigator.push(
WebViewScreen(
url = source.baseUrl,
url = source.getHomeUrl(),
initialTitle = source.name,
sourceId = source.id,
),
@@ -119,7 +118,7 @@ data class BrowseSourceScreen(
}
LaunchedEffect(screenModel.source) {
assistUrl = (screenModel.source as? HttpSource)?.baseUrl
assistUrl = (screenModel.source as? HttpSource)?.getHomeUrl()
}
Scaffold(
@@ -166,7 +165,7 @@ data class BrowseSourceScreen(
Text(text = stringResource(MR.strings.popular))
},
)
if ((screenModel.source as CatalogueSource).supportsLatest) {
if (screenModel.source.supportsLatest) {
FilterChip(
selected = state.listing == Listing.Latest,
onClick = {
@@ -20,7 +20,6 @@ import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.util.removeCovers
import kotlinx.coroutines.flow.SharingStarted
@@ -74,22 +73,20 @@ class BrowseSourceScreenModel(
val source = sourceManager.getOrStub(sourceId)
init {
if (source is CatalogueSource) {
mutableState.update {
var query: String? = null
var listing = it.listing
mutableState.update {
var query: String? = null
var listing = it.listing
if (listing is Listing.Search) {
query = listing.query
listing = Listing.Search(query, source.getFilterList())
}
it.copy(
listing = listing,
filters = source.getFilterList(),
toolbarQuery = query,
)
if (listing is Listing.Search) {
query = listing.query
listing = Listing.Search(query, source.getFilterList())
}
it.copy(
listing = listing,
filters = source.getFilterList(),
toolbarQuery = query,
)
}
if (!getIncognitoState.await(source.id)) {
@@ -129,8 +126,6 @@ class BrowseSourceScreenModel(
}
fun resetFilters() {
if (source !is CatalogueSource) return
mutableState.update { it.copy(filters = source.getFilterList()) }
}
@@ -139,8 +134,6 @@ class BrowseSourceScreenModel(
}
fun setFilters(filters: FilterList) {
if (source !is CatalogueSource) return
mutableState.update {
it.copy(
filters = filters,
@@ -149,8 +142,6 @@ class BrowseSourceScreenModel(
}
fun search(query: String? = null, filters: FilterList? = null) {
if (source !is CatalogueSource) return
val input = state.value.listing as? Listing.Search
?: Listing.Search(query = null, filters = source.getFilterList())
@@ -166,8 +157,6 @@ class BrowseSourceScreenModel(
}
fun searchGenre(genreName: String) {
if (source !is CatalogueSource) return
val defaultFilters = source.getFilterList()
var genreExists = false
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
class GlobalSearchScreenModel(
initialQuery: String = "",
@@ -18,7 +18,7 @@ class GlobalSearchScreenModel(
}
}
override fun getEnabledSources(): List<CatalogueSource> {
override fun getEnabledSources(): List<Source> {
return super.getEnabledSources()
.filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
}
@@ -8,7 +8,7 @@ import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async
@@ -52,8 +52,8 @@ abstract class SearchScreenModel(
protected var extensionFilter: String? = null
open val sortComparator = { map: Map<CatalogueSource, SearchItemResult> ->
compareBy<CatalogueSource>(
open val sortComparator = { map: Map<Source, SearchItemResult> ->
compareBy<Source>(
{ (map[it] as? SearchItemResult.Success)?.isEmpty ?: true },
{ "${it.id}" !in pinnedSources },
{ "${it.name.lowercase()} (${it.lang})" },
@@ -79,8 +79,8 @@ abstract class SearchScreenModel(
}
}
open fun getEnabledSources(): List<CatalogueSource> {
return sourceManager.getCatalogueSources()
open fun getEnabledSources(): List<Source> {
return sourceManager.getAll()
.filter { it.lang in enabledLanguages && "${it.id}" !in disabledSources }
.sortedWith(
compareBy(
@@ -90,7 +90,7 @@ abstract class SearchScreenModel(
)
}
private fun getSelectedSources(): List<CatalogueSource> {
private fun getSelectedSources(): List<Source> {
val enabledSources = getEnabledSources()
val filter = extensionFilter
@@ -101,7 +101,6 @@ abstract class SearchScreenModel(
return extensionManager.installedExtensionsFlow.value
.filter { it.pkgName == filter }
.flatMap { it.sources }
.filterIsInstance<CatalogueSource>()
.filter { it in enabledSources }
}
@@ -179,7 +178,7 @@ abstract class SearchScreenModel(
}
}
private fun updateItems(items: Map<CatalogueSource, SearchItemResult>) {
private fun updateItems(items: Map<Source, SearchItemResult>) {
mutableState.update {
it.copy(
items = items
@@ -188,7 +187,7 @@ abstract class SearchScreenModel(
}
}
private fun updateItem(source: CatalogueSource, result: SearchItemResult) {
private fun updateItem(source: Source, result: SearchItemResult) {
updateItems(state.value.items + (source to result))
}
@@ -209,7 +208,7 @@ abstract class SearchScreenModel(
val searchQuery: String? = null,
val sourceFilter: SourceFilter = SourceFilter.PinnedOnly,
val onlyShowHasResults: Boolean = false,
val items: Map<CatalogueSource, SearchItemResult> = mapOf(),
val items: Map<Source, SearchItemResult> = mapOf(),
val dialog: Dialog? = null,
) {
val progress: Int = items.count { it.value !is SearchItemResult.Loading }
@@ -3,14 +3,13 @@ package eu.kanade.tachiyomi.ui.deeplink
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
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.online.ResolvableSource
import eu.kanade.tachiyomi.source.online.UriType
import kotlinx.coroutines.flow.update
import mihon.domain.manga.model.toDomainManga
import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
import tachiyomi.domain.chapter.model.Chapter
@@ -25,12 +24,12 @@ class DeepLinkScreenModel(
private val sourceManager: SourceManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getChapterByUrlAndMangaId: GetChapterByUrlAndMangaId = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
) : StateScreenModel<DeepLinkScreenModel.State>(State.Loading) {
init {
screenModelScope.launchIO {
val source = sourceManager.getCatalogueSources()
val source = sourceManager.getAll()
.filterIsInstance<ResolvableSource>()
.firstOrNull { it.getUriType(query) != UriType.Unknown }
@@ -61,13 +60,11 @@ class DeepLinkScreenModel(
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
}
return localChapter
?: updateMangaFromRemote(manga, fetchChapters = true)
.getOrElse { return null }
.newChapters
.find { it.url == sChapter.url }
}
sealed interface State {
@@ -16,13 +16,11 @@ import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.chapter.interactor.GetAvailableScanlators
import eu.kanade.domain.chapter.interactor.SetReadStatus
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetExcludedScanlators
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.chaptersFiltered
import eu.kanade.domain.manga.model.downloadedFilter
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.domain.track.interactor.TrackChapter
@@ -36,14 +34,12 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.network.HttpException
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.chapter.getNextUnread
import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
@@ -54,13 +50,13 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import logcat.LogPriority
import mihon.domain.chapter.interactor.FilterChaptersForDownload
import mihon.domain.source.interactor.UpdateMangaFromRemote
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.CheckboxState
import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.preference.mapAsCheckboxState
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.category.interactor.GetCategories
@@ -111,13 +107,13 @@ class MangaScreenModel(
private val setReadStatus: SetReadStatus = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(),
private val addTracks: AddTracks = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val mangaRepository: MangaRepository = Injekt.get(),
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get(),
private val updateMangaFromRemote: UpdateMangaFromRemote = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
) : StateScreenModel<MangaScreenModel.State>(State.Loading) {
@@ -237,11 +233,11 @@ class MangaScreenModel(
// Fetch info-chapters when needed
if (screenModelScope.isActive) {
val fetchFromSourceTasks = listOf(
async { if (needRefreshInfo) fetchMangaFromSource() },
async { if (needRefreshChapter) fetchChaptersFromSource() },
fetchAllFromSource(
manualFetch = false,
fetchDetails = needRefreshInfo,
fetchChapters = needRefreshChapter,
)
fetchFromSourceTasks.awaitAll()
}
// Initial loading finished
@@ -252,38 +248,54 @@ class MangaScreenModel(
fun fetchAllFromSource(manualFetch: Boolean = true) {
screenModelScope.launch {
updateSuccessState { it.copy(isRefreshingData = true) }
val fetchFromSourceTasks = listOf(
async { fetchMangaFromSource(manualFetch) },
async { fetchChaptersFromSource(manualFetch) },
fetchAllFromSource(
manualFetch = manualFetch,
fetchDetails = true,
fetchChapters = true,
)
fetchFromSourceTasks.awaitAll()
updateSuccessState { it.copy(isRefreshingData = false) }
}
}
// Manga info - start
/**
* Fetch manga information from source.
*/
private suspend fun fetchMangaFromSource(manualFetch: Boolean = false) {
private suspend fun fetchAllFromSource(
manualFetch: Boolean,
fetchDetails: Boolean,
fetchChapters: Boolean,
) {
val state = successState ?: return
try {
withIOContext {
val networkManga = state.source.getMangaDetails(state.manga.toSManga())
updateManga.awaitUpdateFromSource(state.manga, networkManga, manualFetch)
}
} catch (e: Throwable) {
// Ignore early hints "errors" that aren't handled by OkHttp
if (e is HttpException && e.code == 103) return
withUIContext {
val update = updateMangaFromRemote(
source = state.source,
manga = state.manga,
fetchDetails = fetchDetails,
fetchChapters = fetchChapters,
manualFetch = manualFetch,
)
.getOrThrow()
if (manualFetch) {
downloadNewChapters(update.newChapters)
}
}
} catch (_: CancellationException) {
// ignore
} catch (e: Exception) {
val message = if (e is NoChaptersException) {
context.stringResource(MR.strings.no_chapters_error)
} else {
logcat(LogPriority.ERROR, e)
with(context) { e.formattedMessage }
}
logcat(LogPriority.ERROR, e)
screenModelScope.launch {
snackbarHostState.showSnackbar(message = with(context) { e.formattedMessage })
snackbarHostState.showSnackbar(message = message)
}
}
}
// Manga info - start
fun toggleFavorite() {
toggleFavorite(
onRemoved = {
@@ -548,42 +560,6 @@ class MangaScreenModel(
}
}
/**
* Requests an updated list of chapters from the source.
*/
private suspend fun fetchChaptersFromSource(manualFetch: Boolean = false) {
val state = successState ?: return
try {
withIOContext {
val chapters = state.source.getChapterList(state.manga.toSManga())
val newChapters = syncChaptersWithSource.await(
chapters,
state.manga,
state.source,
manualFetch,
)
if (manualFetch) {
downloadNewChapters(newChapters)
}
}
} catch (e: Throwable) {
val message = if (e is NoChaptersException) {
context.stringResource(MR.strings.no_chapters_error)
} else {
logcat(LogPriority.ERROR, e)
with(context) { e.formattedMessage }
}
screenModelScope.launch {
snackbarHostState.showSnackbar(message = message)
}
val newManga = mangaRepository.getMangaById(mangaId)
updateSuccessState { it.copy(manga = newManga, isRefreshingData = false) }
}
}
/**
* @throws IllegalStateException if the swipe action is [LibraryPreferences.ChapterSwipeAction.Disabled]
*/
@@ -13,33 +13,6 @@ import uy.kohesive.injekt.api.get
import java.io.InputStream
import java.time.Instant
/**
* Call before updating [Manga.thumbnail_url] to ensure old cover can be cleared from cache
*/
fun Manga.prepUpdateCover(coverCache: CoverCache, remoteManga: SManga, refreshSameUrl: Boolean): Manga {
// Never refresh covers if the new url is null, as the current url has possibly become invalid
val newUrl = remoteManga.thumbnail_url ?: return this
// Never refresh covers if the url is empty to avoid "losing" existing covers
if (newUrl.isEmpty()) return this
if (!refreshSameUrl && thumbnailUrl == newUrl) return this
return when {
isLocal() -> {
this.copy(coverLastModified = Instant.now().toEpochMilli())
}
hasCustomCover(coverCache) -> {
coverCache.deleteFromCache(this, false)
this
}
else -> {
coverCache.deleteFromCache(this, false)
this.copy(coverLastModified = Instant.now().toEpochMilli())
}
}
}
fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Manga {
if (isLocal()) return this
return if (coverCache.deleteFromCache(this, true) > 0) {