From 5fadea61e75433a9db9fbd21eb90eec1199b53da Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Fri, 28 Aug 2026 23:36:28 +0600 Subject: [PATCH] Make the source manager surface suspend (#3869) Assisted-by: Claude:claude-opus-5 --- .../java/eu/kanade/core/util/SourceUtil.kt | 13 ----- .../source/interactor/GetIncognitoState.kt | 2 +- .../eu/kanade/domain/source/model/Source.kt | 12 ++--- .../browse/components/BrowseIcons.kt | 2 +- .../manga/DuplicateMangaDialog.kt | 8 +-- .../settings/screen/SettingsTrackingScreen.kt | 4 +- .../screen/data/RestoreBackupScreen.kt | 8 ++- .../data/backup/BackupFileValidator.kt | 4 +- .../data/backup/create/BackupCreator.kt | 4 +- .../creators/PreferenceBackupCreator.kt | 2 +- .../create/creators/SourcesBackupCreator.kt | 7 +-- .../tachiyomi/data/coil/MangaCoverFetcher.kt | 15 +++--- .../tachiyomi/data/download/DownloadCache.kt | 11 +--- .../data/download/DownloadManager.kt | 26 +++++++-- .../tachiyomi/data/download/DownloadStore.kt | 7 ++- .../tachiyomi/data/download/Downloader.kt | 2 +- .../data/library/LibraryUpdateJob.kt | 8 +-- .../data/library/LibraryUpdateNotifier.kt | 4 +- .../tachiyomi/data/track/kavita/Kavita.kt | 4 +- .../data/track/suwayomi/SuwayomiApi.kt | 9 +++- .../tachiyomi/extension/ExtensionManager.kt | 48 ++++++++++------- .../tachiyomi/source/AndroidSourceManager.kt | 43 +++++++++------ .../details/SourcePreferencesScreen.kt | 25 ++++++--- .../migration/manga/MigrateMangaViewModel.kt | 5 +- .../search/MigrateSearchViewModel.kt | 2 +- .../search/MigrateSourceSearchScreen.kt | 22 ++++---- .../source/browse/BrowseSourceScreen.kt | 34 ++++++------ .../source/browse/BrowseSourceViewModel.kt | 53 +++++++++++-------- .../source/globalsearch/GlobalSearchScreen.kt | 6 --- .../globalsearch/GlobalSearchViewModel.kt | 2 +- .../source/globalsearch/SearchViewModel.kt | 5 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 2 +- .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 6 --- .../tachiyomi/ui/manga/MangaViewModel.kt | 2 +- .../ui/manga/track/TrackInfoDialog.kt | 2 +- .../tachiyomi/ui/reader/ReaderActivity.kt | 7 +-- .../tachiyomi/ui/reader/ReaderViewModel.kt | 24 ++++++--- .../ui/reader/loader/ChapterLoader.kt | 5 +- .../ui/reader/viewer/ReaderTransitionView.kt | 10 ++-- .../viewer/pager/PagerTransitionHolder.kt | 7 ++- .../viewer/webtoon/WebtoonTransitionHolder.kt | 7 ++- .../tachiyomi/ui/webview/WebViewActivity.kt | 30 +++++++---- .../tachiyomi/ui/webview/WebViewScreen.kt | 11 +++- .../tachiyomi/ui/webview/WebViewViewModel.kt | 16 ++++-- .../eu/kanade/tachiyomi/util/CrashLogUtil.kt | 3 +- .../util/chapter/ChapterFilterDownloaded.kt | 2 +- .../migration/config/MigrationConfigScreen.kt | 2 +- .../data/source/SourcePagingSource.kt | 22 ++++---- .../data/source/SourceRepositoryImpl.kt | 11 ++-- .../domain/source/service/SourceManager.kt | 13 ++--- 50 files changed, 328 insertions(+), 251 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/core/util/SourceUtil.kt diff --git a/app/src/main/java/eu/kanade/core/util/SourceUtil.kt b/app/src/main/java/eu/kanade/core/util/SourceUtil.kt deleted file mode 100644 index 9c7a0f273..000000000 --- a/app/src/main/java/eu/kanade/core/util/SourceUtil.kt +++ /dev/null @@ -1,13 +0,0 @@ -package eu.kanade.core.util - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import mihon.app.di.appGraph - -@Composable -fun ifSourcesLoaded(): Boolean { - val context = LocalContext.current - return remember { context.appGraph.sourceManager.isInitialized }.collectAsState().value -} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetIncognitoState.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetIncognitoState.kt index 2a9bcabbc..e27835c3c 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/GetIncognitoState.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetIncognitoState.kt @@ -14,7 +14,7 @@ class GetIncognitoState( private val sourcePreferences: SourcePreferences, private val extensionManager: ExtensionManager, ) { - fun await(sourceId: Long?): Boolean { + suspend fun await(sourceId: Long?): Boolean { if (basePreferences.incognitoMode.get()) return true if (sourceId == null) return false val extensionPackage = extensionManager.getExtensionPackage(sourceId) ?: return false diff --git a/app/src/main/java/eu/kanade/domain/source/model/Source.kt b/app/src/main/java/eu/kanade/domain/source/model/Source.kt index 636894a6d..7dcc9c4ac 100644 --- a/app/src/main/java/eu/kanade/domain/source/model/Source.kt +++ b/app/src/main/java/eu/kanade/domain/source/model/Source.kt @@ -5,13 +5,13 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.core.graphics.drawable.toBitmap import mihon.app.di.appGraph +import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.domain.source.model.Source import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -val Source.icon: ImageBitmap? - get() { - return Injekt.get().appGraph.extensionManager.getAppIconForSource(id) - ?.toBitmap() - ?.asImageBitmap() - } +suspend fun Source.icon(): ImageBitmap? = withIOContext { + Injekt.get().appGraph.extensionManager.getAppIconForSource(id) + ?.toBitmap() + ?.asImageBitmap() +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt index 4134656ad..7bd1bc790 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt @@ -44,7 +44,7 @@ fun SourceIcon( source: Source, modifier: Modifier = Modifier, ) { - val icon = source.icon + val icon = produceState(initialValue = null, source.id) { value = source.icon() }.value when { source.isStub && icon == null -> { diff --git a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt index 043f93720..6748d9bb9 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/DuplicateMangaDialog.kt @@ -38,6 +38,8 @@ import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.Typography import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -170,12 +172,12 @@ fun DuplicateMangaDialog( @Composable private fun DuplicateMangaListItem( duplicate: MangaWithChapterCount, - getSource: () -> Source, + getSource: suspend () -> Source, onDismissRequest: () -> Unit, onOpenManga: () -> Unit, onMigrate: () -> Unit, ) { - val source = getSource() + val source by produceState(initialValue = null) { value = getSource() } val manga = duplicate.manga Column( modifier = Modifier @@ -277,7 +279,7 @@ private fun DuplicateMangaListItem( ) } Text( - text = source.name, + text = source?.name.orEmpty(), style = MaterialTheme.typography.labelSmall, overflow = TextOverflow.Ellipsis, maxLines = 1, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt index 3819716ac..29aa399a5 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -105,11 +106,12 @@ object SettingsTrackingScreen : SearchableSettings { } } + val installedSources by produceState(initialValue = emptyList()) { value = sourceManager.getAll() } val enhancedTrackers = trackerManager.trackers .filter { it is EnhancedTracker } .partition { service -> val acceptedSources = (service as EnhancedTracker).getAcceptedSources() - sourceManager.getAll().any { it::class.qualifiedName in acceptedSources } + installedSources.any { it::class.qualifiedName in acceptedSources } } var enhancedTrackerInfo = stringResource(MR.strings.enhanced_tracking_info) if (enhancedTrackers.second.isNotEmpty()) { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt index 02d201874..4fa7d14d6 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.core.net.toUri import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.zacsweers.metro.AppScope @@ -41,6 +42,7 @@ import eu.kanade.tachiyomi.util.system.workManager import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import tachiyomi.core.common.util.lang.launchIO import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LazyColumnWithAction @@ -191,7 +193,9 @@ class RestoreBackupViewModel( } init { - validate(uri.toUri()) + viewModelScope.launchIO { + validate(uri.toUri()) + } } fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) { @@ -210,7 +214,7 @@ class RestoreBackupViewModel( ) } - private fun validate(uri: Uri) { + private suspend fun validate(uri: Uri) { val results = try { backupFileValidator.validate(uri) } catch (e: Exception) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt index a2d618041..5bae34de6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt @@ -17,7 +17,7 @@ class BackupFileValidator( * * @return List of missing sources or missing trackers. */ - fun validate(uri: Uri): Results { + suspend fun validate(uri: Uri): Results { val backup = try { backupDecoder.decode(uri) } catch (e: Exception) { @@ -26,7 +26,7 @@ class BackupFileValidator( val sources = backup.backupSources.associate { it.sourceId to it.name } val missingSources = sources - .filter { sourceManager.get(it.key) == null } + .filterKeys { sourceManager.get(it) == null } .values.map { val id = it.toLongOrNull() if (id == null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt index 8f085a54c..5a8725a58 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt @@ -136,7 +136,7 @@ class BackupCreator( return mangaBackupCreator(mangas, options) } - private fun backupSources(mangas: List): List { + private suspend fun backupSources(mangas: List): List { return sourcesBackupCreator(mangas) } @@ -152,7 +152,7 @@ class BackupCreator( return extensionStoresBackupCreator() } - private fun backupSourcePreferences(options: BackupOptions): List { + private suspend fun backupSourcePreferences(options: BackupOptions): List { if (!options.sourceSettings) return emptyList() return preferenceBackupCreator.createSource(includePrivatePreferences = options.privateSettings) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt index ba221f40f..d5d6a4d26 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt @@ -27,7 +27,7 @@ class PreferenceBackupCreator( .withPrivatePreferences(includePrivatePreferences) } - fun createSource(includePrivatePreferences: Boolean): List { + suspend fun createSource(includePrivatePreferences: Boolean): List { return sourceManager.getAll() .filterIsInstance() .map { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt index 6e7d668e7..2b8180ce9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt @@ -11,14 +11,11 @@ class SourcesBackupCreator( private val sourceManager: SourceManager, ) { - operator fun invoke(mangas: List): List { + suspend operator fun invoke(mangas: List): List { return mangas - .asSequence() .map(BackupManga::source) .distinct() - .map(sourceManager::getOrStub) - .map { it.toBackupSource() } - .toList() + .map { sourceManager.getOrStub(it).toBackupSource() } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt index 6d960a432..98d9d29f9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt @@ -51,7 +51,7 @@ class MangaCoverFetcher( private val coverFileLazy: Lazy, private val customCoverFileLazy: Lazy, private val diskCacheKeyLazy: Lazy, - private val sourceLazy: Lazy, + private val getSource: suspend () -> HttpSource?, private val callFactoryLazy: Lazy, private val imageLoader: ImageLoader, ) : Fetcher { @@ -169,8 +169,9 @@ class MangaCoverFetcher( } private suspend fun executeNetworkRequest(): Response { - val client = sourceLazy.value?.client ?: callFactoryLazy.value - val response = client.newCall(newRequest()).await() + val source = getSource() + val client = source?.client ?: callFactoryLazy.value + val response = client.newCall(newRequest(source)).await() if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) { response.close() throw IOException(response.message) @@ -178,11 +179,11 @@ class MangaCoverFetcher( return response } - private fun newRequest(): Request { + private fun newRequest(source: HttpSource?): Request { val request = Request.Builder().apply { url(url!!) - val sourceHeaders = sourceLazy.value?.headers + val sourceHeaders = source?.headers if (sourceHeaders != null) { headers(sourceHeaders) } @@ -310,7 +311,7 @@ class MangaCoverFetcher( coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnailUrl) }, customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.id) }, diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! }, - sourceLazy = lazy { sourceManager.get(data.source) as? HttpSource }, + getSource = { sourceManager.get(data.source) as? HttpSource }, callFactoryLazy = callFactoryLazy, imageLoader = imageLoader, ) @@ -331,7 +332,7 @@ class MangaCoverFetcher( coverFileLazy = lazy { coverCache.getCoverFile(data.url) }, customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.mangaId) }, diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! }, - sourceLazy = lazy { sourceManager.get(data.sourceId) as? HttpSource }, + getSource = { sourceManager.get(data.sourceId) as? HttpSource }, callFactoryLazy = callFactoryLazy, imageLoader = imageLoader, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index 61afab701..5816c0583 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -133,7 +133,6 @@ class DownloadCache( * @param chapterUrl the url of the chapter to query * @param mangaTitle the title of the manga to query. * @param sourceId the id of the source of the chapter. - * @param skipCache whether to skip the directory cache and check in the filesystem. */ fun isChapterDownloaded( chapterName: String, @@ -141,13 +140,7 @@ class DownloadCache( chapterUrl: String, mangaTitle: String, sourceId: Long, - skipCache: Boolean, ): Boolean { - if (skipCache) { - val source = sourceManager.getOrStub(sourceId) - return provider.findChapterDir(chapterName, chapterScanlator, chapterUrl, mangaTitle, source) != null - } - renewCache() val sourceDir = rootDownloadsDir.sourceDirs[sourceId] @@ -354,8 +347,6 @@ class DownloadCache( // Try to wait until extensions and sources have loaded var sources = emptyList() withTimeoutOrNull(30.seconds) { - sourceManager.isInitialized.first { it } - sources = getSources() } @@ -418,7 +409,7 @@ class DownloadCache( notifyChanges() } - private fun getSources(): List { + private suspend fun getSources(): List { return sourceManager.getOnlineSources() + sourceManager.getStubSources() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 3dfc6e471..3ba77c7d1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -140,7 +140,7 @@ class DownloadManager( * @param chapters the list of chapters to enqueue. * @param autoStart whether to start the downloader after enqueing the chapters. */ - fun downloadChapters(manga: Manga, chapters: List, autoStart: Boolean = true) { + suspend fun downloadChapters(manga: Manga, chapters: List, autoStart: Boolean = true) { downloader.queueChapters(manga, chapters, autoStart) } @@ -188,7 +188,6 @@ class DownloadManager( * @param chapterScanlator scanlator of the chapter to query * @param mangaTitle the title of the manga to query. * @param sourceId the id of the source of the chapter. - * @param skipCache whether to skip the directory cache and check in the filesystem. */ fun isChapterDownloaded( chapterName: String, @@ -196,9 +195,26 @@ class DownloadManager( chapterUrl: String, mangaTitle: String, sourceId: Long, - skipCache: Boolean = false, ): Boolean { - return cache.isChapterDownloaded(chapterName, chapterScanlator, chapterUrl, mangaTitle, sourceId, skipCache) + return cache.isChapterDownloaded(chapterName, chapterScanlator, chapterUrl, mangaTitle, sourceId) + } + + /** + * Returns true if the chapter is present on disk, bypassing the directory cache. + * + * @param chapterName the name of the chapter to query. + * @param chapterScanlator scanlator of the chapter to query + * @param mangaTitle the title of the manga to query. + * @param source the source of the chapter. + */ + fun isChapterDownloadedOnDisk( + chapterName: String, + chapterScanlator: String?, + chapterUrl: String, + mangaTitle: String, + source: Source, + ): Boolean { + return provider.findChapterDir(chapterName, chapterScanlator, chapterUrl, mangaTitle, source) != null } /** @@ -302,7 +318,7 @@ class DownloadManager( /** * Triggers the execution of the deletion of pending chapters. */ - fun deletePendingChapters() { + suspend fun deletePendingChapters() { val pendingChapters = pendingDeleter.getPendingChapters() for ((manga, chapters) in pendingChapters) { val source = sourceManager.get(manga.source) ?: continue diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt index e5a8e5bb0..3554c318c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt @@ -7,7 +7,6 @@ import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.source.online.HttpSource -import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import tachiyomi.domain.chapter.interactor.GetChapter @@ -92,7 +91,7 @@ class DownloadStore( /** * Returns the list of downloads to restore. It should be called in a background thread. */ - fun restore(): List { + suspend fun restore(): List { val objs = preferences.all .mapNotNull { it.value as? String } .mapNotNull { deserialize(it) } @@ -103,10 +102,10 @@ class DownloadStore( val cachedManga = mutableMapOf() for ((mangaId, chapterId) in objs) { val manga = cachedManga.getOrPut(mangaId) { - runBlocking { getManga.await(mangaId) } + getManga.await(mangaId) } ?: continue val source = sourceManager.get(manga.source) as? HttpSource ?: continue - val chapter = runBlocking { getChapter.await(chapterId) } ?: continue + val chapter = getChapter.await(chapterId) ?: continue downloads.add(Download(source, manga, chapter)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 410e764a7..4cbd55981 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -265,7 +265,7 @@ class Downloader( * @param chapters the list of chapters to download. * @param autoStart whether to start the downloader after enqueing the chapters. */ - fun queueChapters(manga: Manga, chapters: List, autoStart: Boolean) { + suspend fun queueChapters(manga: Manga, chapters: List, autoStart: Boolean) { if (chapters.isEmpty()) return val source = sourceManager.get(manga.source) as? HttpSource ?: return diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 7e94c0fa7..f0713ef54 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -329,7 +329,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet } } - private fun downloadChapters(manga: Manga, chapters: List) { + private suspend fun downloadChapters(manga: Manga, chapters: List) { // We don't want to start downloading while the library is updating, because websites // may don't like it and they could ban the user. downloadManager.downloadChapters(manga, chapters, false) @@ -387,7 +387,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet /** * Writes basic file of update errors to cache dir. */ - private fun writeErrorFile(errors: List>): File { + private suspend fun writeErrorFile(errors: List>): File { try { if (errors.isNotEmpty()) { val file = context.createFileInCacheDir("mihon_update_errors.txt") @@ -402,8 +402,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet mangas.groupBy { it.source }.forEach { (srcId, mangas) -> val source = sourceManager.getOrStub(srcId) out.write(" # $source\n") - mangas.forEach { - out.write(" - ${it.title}\n") + mangas.forEach { manga -> + out.write(" - ${manga.title}\n") } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index a9f01350c..29442a7d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -113,10 +113,10 @@ class LibraryUpdateNotifier( /** * Warn when excessively checking any single source. */ - fun showQueueSizeWarningNotificationIfNeeded(mangaToUpdate: List) { + suspend fun showQueueSizeWarningNotificationIfNeeded(mangaToUpdate: List) { val maxUpdatesFromSource = mangaToUpdate .groupBy { it.manga.source } - .filterKeys { sourceManager.get(it) !is UnmeteredSource } + .filter { (sourceId, _) -> sourceManager.get(sourceId) !is UnmeteredSource } .maxOfOrNull { it.value.size } ?: 0 if (maxUpdatesFromSource <= MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt index 5e91912a9..91c3ceed3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt @@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.sourcePreferences +import kotlinx.coroutines.runBlocking import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.source.service.SourceManager import tachiyomi.i18n.MR @@ -118,7 +119,8 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker { (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) } .reduce(Long::or) and Long.MAX_VALUE } - val preferences = (sourceManager.get(sourceId) as ConfigurableSource).sourcePreferences() + val preferences = runBlocking { sourceManager.get(sourceId) as ConfigurableSource } + .sourcePreferences() val prefApiUrl = preferences.getString("APIURL", "") val prefApiKey = preferences.getString("APIKEY", "") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt index f1b91f22d..059c70968 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt @@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.sourcePreferences +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import kotlinx.serialization.json.addAll import kotlinx.serialization.json.buildJsonObject @@ -29,8 +30,12 @@ class SuwayomiApi( ) { private val json: Json by injectLazy() - private val source: HttpSource by lazy { (sourceManager.get(sourceId) as HttpSource) } - private val configurableSource: ConfigurableSource by lazy { (sourceManager.get(sourceId) as ConfigurableSource) } + + // Blocking is fine here: these are only touched from OkHttp and tracker threads. + private val source: HttpSource by lazy { runBlocking { sourceManager.get(sourceId) as HttpSource } } + private val configurableSource: ConfigurableSource by lazy { + runBlocking { sourceManager.get(sourceId) as ConfigurableSource } + } private val client: OkHttpClient by lazy { source.client } private val baseUrl: String by lazy { source.baseUrl.trimEnd('/') } private val apiUrl: String by lazy { "$baseUrl/api/graphql" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index 949de947d..61a90abcb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver import eu.kanade.tachiyomi.extension.util.ExtensionInstaller import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -24,7 +25,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn @@ -56,18 +56,18 @@ class ExtensionManager( val scope = CoroutineScope(SupervisorJob()) - private val isInitialized = MutableStateFlow(false) + private val initialized = CompletableDeferred() private val iconMap = mutableMapOf() private val installedExtensionMapFlow = MutableStateFlow(emptyMap()) - val installedExtensionsFlow = installedExtensionMapFlow.mapExtensionsOnceInitialized() + val installedExtensionsFlow = installedExtensionMapFlow.mapExtensionsWhenInitialized() private val availableExtensionMapFlow = MutableStateFlow(emptyMap()) val availableExtensionsFlow = availableExtensionMapFlow.mapExtensions(scope) private val untrustedExtensionMapFlow = MutableStateFlow(emptyMap()) - val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensionsOnceInitialized() + val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensionsWhenInitialized() init { scope.launch(Dispatchers.IO) { @@ -78,8 +78,13 @@ class ExtensionManager( private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages.isSet() - fun getExtensionPackage(sourceId: Long): String? { - return installedExtensionMapFlow.value.values.find { extension -> + suspend fun getInstalledExtensions(): List { + initialized.await() + return installedExtensionMapFlow.value.values.toList() + } + + suspend fun getExtensionPackage(sourceId: Long): String? { + return getInstalledExtensions().find { extension -> extension.sources.any { it.id == sourceId } } ?.pkgName @@ -94,7 +99,7 @@ class ExtensionManager( } } - fun getAppIconForSource(sourceId: Long): Drawable? { + suspend fun getAppIconForSource(sourceId: Long): Drawable? { val pkgName = getExtensionPackage(sourceId) ?: return null return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { @@ -118,17 +123,23 @@ class ExtensionManager( * Loads and registers the installed extensions. */ private fun initExtensions() { - val extensions = ExtensionLoader.loadExtensions(context) + try { + val extensions = ExtensionLoader.loadExtensions(context) - installedExtensionMapFlow.value = extensions - .filterIsInstance() - .associate { it.extension.pkgName to it.extension } + installedExtensionMapFlow.value = extensions + .filterIsInstance() + .associate { it.extension.pkgName to it.extension } - untrustedExtensionMapFlow.value = extensions - .filterIsInstance() - .associate { it.extension.pkgName to it.extension } + untrustedExtensionMapFlow.value = extensions + .filterIsInstance() + .associate { it.extension.pkgName to it.extension } - isInitialized.value = true + initialized.complete(Unit) + } catch (e: Throwable) { + // Release anything waiting on the extensions before the failure propagates + initialized.complete(Unit) + throw e + } } /** @@ -380,10 +391,9 @@ class ExtensionManager( } /** - * Extensions are loaded in the background, and [stateIn] would replay the empty list it was - * seeded with at construction, so this only starts emitting once that finished. + * Extensions are loaded in the background, so this flow only starts emitting once that finished. */ - private fun StateFlow>.mapExtensionsOnceInitialized(): Flow> { - return onStart { isInitialized.first { it } }.map { it.values.toList() } + private fun StateFlow>.mapExtensionsWhenInitialized(): Flow> { + return onStart { initialized.await() }.map { it.values.toList() } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt index 6a753c53a..06e5ad010 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt @@ -12,12 +12,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.repository.StubSourceRepository import tachiyomi.domain.source.service.SourceManager @@ -34,16 +33,18 @@ class AndroidSourceManager( private val downloadManager: Lazy, ) : SourceManager { - private val _isInitialized = MutableStateFlow(false) - override val isInitialized: StateFlow = _isInitialized.asStateFlow() - private val scope = CoroutineScope(Job() + Dispatchers.IO) - private val sourcesMapFlow = MutableStateFlow(ConcurrentHashMap()) + /** + * Null until the extensions have loaded, so that nothing observes the empty seed value. + */ + private val sourcesMapFlow = MutableStateFlow?>(null) private val stubSourcesMap = ConcurrentHashMap() - override val sources: Flow> = sourcesMapFlow.map { it.values.toList() } + override val sources: Flow> = sourcesMapFlow + .filterNotNull() + .map { it.values.toList() } init { scope.launch { @@ -59,7 +60,6 @@ class AndroidSourceManager( } } sourcesMapFlow.value = mutableMap - _isInitialized.value = true } } @@ -74,21 +74,30 @@ class AndroidSourceManager( } } - override fun get(sourceKey: Long): Source? { - return sourcesMapFlow.value[sourceKey] + /** + * Awaits the extensions to have loaded before returning the sources. + */ + private suspend fun sourcesMap(): Map = sourcesMapFlow.filterNotNull().first() + + override suspend fun get(sourceKey: Long): Source? { + return sourcesMap()[sourceKey] } - override fun getOrStub(sourceKey: Long): Source { - return sourcesMapFlow.value[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) { - runBlocking { createStubSource(sourceKey) } + override suspend fun getOrStub(sourceKey: Long): Source { + return sourcesMap()[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) { + createStubSource(sourceKey) } } - override fun getAll() = sourcesMapFlow.value.values.toList() + override suspend fun getAll(): List { + return sourcesMap().values.toList() + } - override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance() + override suspend fun getOnlineSources(): List { + return sourcesMap().values.filterIsInstance() + } - override fun getStubSources(): List { + override suspend fun getStubSources(): List { val onlineSourceIds = getOnlineSources().map { it.id } return stubSourcesMap.values.filterNot { it.id in onlineSourceIds } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt index 19b3103df..23fd44b94 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/SourcePreferencesScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -30,14 +31,15 @@ import androidx.preference.forEach import androidx.preference.getOnBindEditTextListener import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import eu.kanade.core.util.ifSourcesLoaded import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.sourcePreferences import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito +import kotlinx.coroutines.launch import mihon.app.di.appGraph import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.screens.LoadingScreen @@ -46,18 +48,22 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() { @Composable override fun Content() { - if (!ifSourcesLoaded()) { + val context = LocalContext.current + val navigator = LocalNavigator.currentOrThrow + + val source by produceState(initialValue = null) { + value = context.appGraph.sourceManager.getOrStub(sourceId) + } + + if (source == null) { LoadingScreen() return } - val context = LocalContext.current - val navigator = LocalNavigator.currentOrThrow - Scaffold( topBar = { AppBar( - title = context.appGraph.sourceManager.getOrStub(sourceId).toString(), + title = source.toString(), navigateUp = navigator::pop, scrollBehavior = it, ) @@ -125,10 +131,13 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() { } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceScreen = populateScreen() + preferenceScreen = preferenceManager.createPreferenceScreen(requireContext()) + lifecycleScope.launch { + preferenceScreen = populateScreen() + } } - private fun populateScreen(): PreferenceScreen { + private suspend fun populateScreen(): PreferenceScreen { val sourceId = requireArguments().getLong(SOURCE_ID) val appGraph = requireContext().appGraph val source = appGraph.sourceManager.getOrStub(sourceId) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaViewModel.kt index 84f8c0674..119fffac1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrateMangaViewModel.kt @@ -12,6 +12,7 @@ import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey import eu.kanade.tachiyomi.source.Source import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -50,7 +51,7 @@ class MigrateMangaViewModel( private val _events: Channel = Channel() val events: Flow = _events.receiveAsFlow() - private val source by lazy { sourceManager.getOrStub(sourceId) } + private val source = viewModelScope.async { sourceManager.getOrStub(sourceId) } private val selection = MutableStateFlow(emptySet()) @@ -68,7 +69,7 @@ class MigrateMangaViewModel( favorites, selection, ) { titleList, selection -> - State(source = source, selection = selection, titleList = titleList) + State(source = source.await(), selection = selection, titleList = titleList) } .flowOn(Dispatchers.IO) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), State()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchViewModel.kt index 749ec2d9c..76d437691 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchViewModel.kt @@ -64,7 +64,7 @@ class MigrateSearchViewModel( } } - override fun getEnabledSources(): List { + override suspend fun getEnabledSources(): List { return migrationSources.mapNotNull { sourceManager.get(it) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt index a451b8f8c..25d647f75 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSourceSearchScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.platform.LocalUriHandler import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel -import eu.kanade.core.util.ifSourcesLoaded import eu.kanade.presentation.browse.BrowseSourceContent import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.util.Screen @@ -50,11 +49,6 @@ data class MigrateSourceSearchScreen( @Composable override fun Content() { - if (!ifSourcesLoaded()) { - LoadingScreen() - return - } - val uriHandler = LocalUriHandler.current val navigator = LocalNavigator.currentOrThrow val scope = rememberCoroutineScope() @@ -65,6 +59,12 @@ data class MigrateSourceSearchScreen( } val state by viewModel.state.collectAsState() + val source = state.source + if (source == null) { + LoadingScreen() + return + } + val snackbarHostState = remember { SnackbarHostState() } Scaffold( @@ -103,19 +103,19 @@ data class MigrateSourceSearchScreen( } } BrowseSourceContent( - source = viewModel.source, + source = source, mangaList = viewModel.mangaPagerFlowFlow.collectAsLazyPagingItems(), columns = viewModel.getColumnsPreference(LocalConfiguration.current.orientation), displayMode = viewModel.displayMode, snackbarHostState = snackbarHostState, contentPadding = paddingValues, onWebViewClick = { - val source = viewModel.source as? HttpSource ?: return@BrowseSourceContent + val httpSource = source as? HttpSource ?: return@BrowseSourceContent navigator.push( WebViewScreen( - url = source.getHomeUrl(), - initialTitle = source.name, - sourceId = source.id, + url = httpSource.getHomeUrl(), + initialTitle = httpSource.name, + sourceId = httpSource.id, ), ) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index 0770e60a3..49b145ee6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -35,7 +35,6 @@ import androidx.compose.ui.platform.LocalUriHandler import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel -import eu.kanade.core.util.ifSourcesLoaded import eu.kanade.presentation.browse.BrowseSourceContent import eu.kanade.presentation.browse.MissingSourceScreen import eu.kanade.presentation.browse.components.BrowseSourceToolbar @@ -76,11 +75,6 @@ data class BrowseSourceScreen( @Composable override fun Content() { - if (!ifSourcesLoaded()) { - LoadingScreen() - return - } - val viewModel = assistedMetroViewModel { create(sourceId = sourceId, listingQuery = listingQuery) @@ -95,9 +89,15 @@ data class BrowseSourceScreen( } } - if (viewModel.source is StubSource) { + val source = state.source + if (source == null) { + LoadingScreen() + return + } + + if (source is StubSource) { MissingSourceScreen( - source = viewModel.source, + source = source, navigateUp = navigateUp, ) return @@ -110,18 +110,18 @@ data class BrowseSourceScreen( val onHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) } val onWebViewClick = f@{ - val source = viewModel.source as? HttpSource ?: return@f + val httpSource = source as? HttpSource ?: return@f navigator.push( WebViewScreen( - url = source.getHomeUrl(), - initialTitle = source.name, - sourceId = source.id, + url = httpSource.getHomeUrl(), + initialTitle = httpSource.name, + sourceId = httpSource.id, ), ) } - LaunchedEffect(viewModel.source) { - assistUrl = (viewModel.source as? HttpSource)?.getHomeUrl() + LaunchedEffect(source) { + assistUrl = (source as? HttpSource)?.getHomeUrl() } Scaffold( @@ -134,7 +134,7 @@ data class BrowseSourceScreen( BrowseSourceToolbar( searchQuery = state.toolbarQuery, onSearchQueryChange = viewModel::setToolbarQuery, - source = viewModel.source, + source = source, displayMode = viewModel.displayMode, onDisplayModeChange = { viewModel.displayMode = it }, navigateUp = navigateUp, @@ -168,7 +168,7 @@ data class BrowseSourceScreen( Text(text = stringResource(MR.strings.popular)) }, ) - if (viewModel.source.supportsLatest) { + if (source.supportsLatest) { FilterChip( selected = state.listing == Listing.Latest, onClick = { @@ -213,7 +213,7 @@ data class BrowseSourceScreen( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { paddingValues -> BrowseSourceContent( - source = viewModel.source, + source = source, mangaList = viewModel.mangaPagerFlowFlow.collectAsLazyPagingItems(), columns = viewModel.getColumnsPreference(LocalConfiguration.current.orientation), displayMode = viewModel.displayMode, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceViewModel.kt index 7b46f1a94..ea39231f3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceViewModel.kt @@ -26,6 +26,7 @@ import eu.kanade.domain.source.interactor.GetIncognitoState import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.util.removeCovers import kotlinx.coroutines.flow.MutableStateFlow @@ -33,6 +34,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -60,7 +62,7 @@ import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter class BrowseSourceViewModel( @Assisted private val sourceId: Long, @Assisted listingQuery: String?, - sourceManager: SourceManager, + private val sourceManager: SourceManager, sourcePreferences: SourcePreferences, private val libraryPreferences: LibraryPreferences, private val coverCache: CoverCache, @@ -87,27 +89,32 @@ class BrowseSourceViewModel( var displayMode by sourcePreferences.sourceDisplayMode.asState(viewModelScope) - val source = sourceManager.getOrStub(sourceId) + private val source: Source? get() = state.value.source init { - state.update { - var query: String? = null - var listing = it.listing + viewModelScope.launchIO { + val source = sourceManager.getOrStub(sourceId) - if (listing is Listing.Search) { - query = listing.query - listing = Listing.Search(query, source.getFilterList()) + state.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( + source = source, + listing = listing, + filters = source.getFilterList(), + toolbarQuery = query, + ) } - it.copy( - listing = listing, - filters = source.getFilterList(), - toolbarQuery = query, - ) - } - - if (!getIncognitoState.await(source.id)) { - sourcePreferences.lastUsedSource.set(source.id) + if (!getIncognitoState.await(source.id)) { + sourcePreferences.lastUsedSource.set(source.id) + } } } @@ -115,7 +122,9 @@ class BrowseSourceViewModel( * Flow of Pager flow tied to [State.listing] */ private val hideInLibraryItems = sourcePreferences.hideInLibraryItems.get() - val mangaPagerFlowFlow = state.map { it.listing } + val mangaPagerFlowFlow = state.map { it.source to it.listing } + .filter { (source, _) -> source != null } + .map { (_, listing) -> listing } .distinctUntilChanged() .map { listing -> Pager(PagingConfig(pageSize = 25)) { @@ -143,6 +152,7 @@ class BrowseSourceViewModel( } fun resetFilters() { + val source = source ?: return state.update { it.copy(filters = source.getFilterList()) } } @@ -160,7 +170,7 @@ class BrowseSourceViewModel( fun search(query: String? = null, filters: FilterList? = null) { val input = state.value.listing as? Listing.Search - ?: Listing.Search(query = null, filters = source.getFilterList()) + ?: Listing.Search(query = null, filters = source?.getFilterList() ?: FilterList()) state.update { it.copy( @@ -174,7 +184,7 @@ class BrowseSourceViewModel( } fun searchGenre(genreName: String) { - val defaultFilters = source.getFilterList() + val defaultFilters = source?.getFilterList() ?: return var genreExists = false filter@ for (sourceFilter in defaultFilters) { @@ -235,7 +245,7 @@ class BrowseSourceViewModel( new = new.removeCovers(coverCache) } else { setMangaDefaultChapterFlags.await(manga) - addTracks.bindEnhancedTrackers(manga, source) + addTracks.bindEnhancedTrackers(manga, sourceManager.getOrStub(manga.source)) } updateManga.await(new.toMangaUpdate()) @@ -351,6 +361,7 @@ class BrowseSourceViewModel( @Immutable data class State( val listing: Listing, + val source: Source? = null, val filters: FilterList = FilterList(), val toolbarQuery: String? = null, val dialog: Dialog? = null, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt index 250a985c1..491211df1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.setValue import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel -import eu.kanade.core.util.ifSourcesLoaded import eu.kanade.presentation.browse.GlobalSearchScreen import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen @@ -24,11 +23,6 @@ class GlobalSearchScreen( @Composable override fun Content() { - if (!ifSourcesLoaded()) { - LoadingScreen() - return - } - val navigator = LocalNavigator.currentOrThrow val viewModel = diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchViewModel.kt index 3b3480291..3b0de4ebd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchViewModel.kt @@ -52,7 +52,7 @@ class GlobalSearchViewModel( } } - override fun getEnabledSources(): List { + override suspend fun getEnabledSources(): List { return super.getEnabledSources() .filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchViewModel.kt index 71d603479..2896f9c5b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchViewModel.kt @@ -16,7 +16,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -88,7 +87,7 @@ abstract class SearchViewModel( } } - open fun getEnabledSources(): List { + open suspend fun getEnabledSources(): List { return sourceManager.getAll() .filter { it.lang in enabledLanguages && "${it.id}" !in disabledSources } .sortedWith( @@ -107,7 +106,7 @@ abstract class SearchViewModel( return enabledSources } - return extensionManager.installedExtensionsFlow.first() + return extensionManager.getInstalledExtensions() .filter { it.pkgName == filter } .flatMap { it.sources } .filter { it in enabledSources } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 2a9bea744..83e2576a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -171,7 +171,7 @@ class MainActivity : BaseActivity() { setComposeContent { val context = LocalContext.current - var incognito by remember { mutableStateOf(getIncognitoState.await(null)) } + var incognito by remember { mutableStateOf(false) } val downloadOnly by preferences.downloadedOnly.collectAsState() val indexing by downloadCache.isInitializing.collectAsState() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index d1efaf058..c2a9e31b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -21,7 +21,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel -import eu.kanade.core.util.ifSourcesLoaded import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.domain.manga.model.toSManga import eu.kanade.presentation.category.components.ChangeCategoryDialog @@ -73,11 +72,6 @@ class MangaScreen( @Composable override fun Content() { - if (!ifSourcesLoaded()) { - LoadingScreen() - return - } - val navigator = LocalNavigator.currentOrThrow val context = LocalContext.current val haptic = LocalHapticFeedback.current diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaViewModel.kt index 08fa62c12..505740172 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaViewModel.kt @@ -798,7 +798,7 @@ class MangaViewModel( * Downloads the given list of chapters with the manager. * @param chapters the list of chapters to download. */ - private fun downloadChapters(chapters: List) { + private suspend fun downloadChapters(chapters: List) { val manga = successState?.manga ?: return downloadManager.downloadChapters(manga, chapters) toggleAllSelection(false) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt index 80dd19103..b32e89c3c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt @@ -273,7 +273,7 @@ data class TrackInfoDialogHomeScreen( } } - private fun List.mapToTrackItem(): List { + private suspend fun List.mapToTrackItem(): List { val loggedInTrackers = trackerManager.loggedInTrackers() val source = sourceManager.getOrStub(sourceId) return loggedInTrackers diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 4bdb503ee..da42f14a3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -48,7 +48,6 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.google.android.material.transition.platform.MaterialContainerTransform import com.hippo.unifile.UniFile import dev.zacsweers.metro.Inject -import eu.kanade.core.util.ifSourcesLoaded import eu.kanade.domain.base.BasePreferences import eu.kanade.presentation.reader.DisplayRefreshHost import eu.kanade.presentation.reader.OrientationSelectDialog @@ -454,11 +453,7 @@ class ReaderActivity : BaseActivity() { @Composable fun AppBars(state: ReaderViewModel.State) { - if (!ifSourcesLoaded()) { - return - } - - val isHttpSource = viewModel.getSource() is HttpSource + val isHttpSource = state.source is HttpSource val cropBorderPaged by readerPreferences.cropBorders.collectAsState() val cropBorderWebtoon by readerPreferences.cropBordersWebtoon.collectAsState() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 39a235f6e..c52cdc31f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -35,6 +35,7 @@ import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.saver.Image import eu.kanade.tachiyomi.data.saver.ImageSaver import eu.kanade.tachiyomi.data.saver.Location +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader @@ -155,6 +156,12 @@ class ReaderViewModel( val manga: Manga? get() = state.value.manga + /** + * The source of the manga loaded in the reader. Null until it has been resolved. + */ + val source: Source? + get() = state.value.source + /** * The chapter id of the currently loaded chapter. Used to restore from process kill. */ @@ -265,7 +272,7 @@ class ReaderViewModel( .map(::ReaderChapter) } - private val incognitoMode: Boolean by lazy { getIncognitoState.await(manga?.source) } + private var incognitoMode: Boolean = false private val downloadAheadAmount = downloadPreferences.autoDownloadWhileReading.get() init { @@ -316,11 +323,11 @@ class ReaderViewModel( withIOContext { try { val manga = getManga.await(mangaId) ?: error("Requested manga of id $mangaId not found") - sourceManager.isInitialized.first { it } - mutableState.update { it.copy(manga = manga) } + val source = sourceManager.getOrStub(manga.source) + incognitoMode = getIncognitoState.await(manga.source) + mutableState.update { it.copy(manga = manga, source = source) } if (chapterId == -1L) chapterId = initialChapterId - val source = sourceManager.getOrStub(manga.source) loader = ChapterLoader(context, downloadManager, downloadProvider, chapterCache, manga, source) loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id }) @@ -425,13 +432,13 @@ class ReaderViewModel( if (chapter.pageLoader?.isLocal == false) { val manga = manga ?: return val dbChapter = chapter.chapter - val isDownloaded = downloadManager.isChapterDownloaded( + val source = state.value.source ?: return + val isDownloaded = downloadManager.isChapterDownloadedOnDisk( dbChapter.name, dbChapter.scanlator, dbChapter.url, manga.title, - manga.source, - skipCache = true, + source, ) if (isDownloaded) { chapter.state = ReaderChapter.State.Wait @@ -654,7 +661,7 @@ class ReaderViewModel( return state.value.currentChapter } - fun getSource() = manga?.source?.let { sourceManager.getOrStub(it) } as? HttpSource + fun getSource() = state.value.source as? HttpSource fun getChapterUrl(): String? { val sChapter = getCurrentChapter()?.chapter ?: return null @@ -974,6 +981,7 @@ class ReaderViewModel( @Immutable data class State( val manga: Manga? = null, + val source: Source? = null, val initError: Throwable? = null, val viewerChapters: ViewerChapters? = null, val bookmarked: Boolean = false, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index 3cbc9416d..0aa597ec7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -79,13 +79,12 @@ class ChapterLoader( */ private fun getPageLoader(chapter: ReaderChapter): PageLoader { val dbChapter = chapter.chapter - val isDownloaded = downloadManager.isChapterDownloaded( + val isDownloaded = downloadManager.isChapterDownloadedOnDisk( dbChapter.name, dbChapter.scanlator, dbChapter.url, manga.title, - manga.source, - skipCache = true, + source, ) return when { isDownloaded -> DownloadPageLoader( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt index e935157d4..6ca0149f2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.platform.AbstractComposeView import eu.kanade.presentation.reader.ChapterTransition import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import tachiyomi.domain.manga.model.Manga import tachiyomi.source.local.isLocal @@ -27,20 +28,19 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) } - fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?) { - data = if (manga != null) { + fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?, source: Source?) { + data = if (manga != null && source != null) { Data( transition = transition, currChapterDownloaded = transition.from.pageLoader?.isLocal == true, goingToChapterDownloaded = manga.isLocal() || transition.to?.chapter?.let { goingToChapter -> - downloadManager.isChapterDownloaded( + downloadManager.isChapterDownloadedOnDisk( chapterName = goingToChapter.name, chapterScanlator = goingToChapter.scanlator, chapterUrl = goingToChapter.url, mangaTitle = manga.title, - sourceId = manga.source, - skipCache = true, + source = source, ) } ?: false, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt index 4569f4370..0ec4cd803 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt @@ -62,7 +62,12 @@ class PagerTransitionHolder( addView(transitionView) addView(pagesContainer) - transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.manga) + transitionView.bind( + transition, + viewer.downloadManager, + viewer.activity.viewModel.manga, + viewer.activity.viewModel.source, + ) transition.to?.let(::observeStatus) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt index 57036d070..a13632032 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt @@ -64,7 +64,12 @@ class WebtoonTransitionHolder( * Binds the given [transition] with this view holder, subscribing to its state. */ fun bind(transition: ChapterTransition) { - transitionView.bind(transition, viewer.downloadManager, viewer.activity.viewModel.manga) + transitionView.bind( + transition, + viewer.downloadManager, + viewer.activity.viewModel.manga, + viewer.activity.viewModel.source, + ) transition.to?.let { observeStatus(it, transition) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt index b15d1adff..a5374c4a6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt @@ -6,6 +6,8 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.widget.Toast +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.core.net.toUri import dev.zacsweers.metro.Inject import eu.kanade.presentation.webview.WebViewScreenContent @@ -24,6 +26,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.source.service.SourceManager import tachiyomi.i18n.MR +import tachiyomi.presentation.core.screens.LoadingScreen class WebViewActivity : BaseActivity() { @@ -60,21 +63,28 @@ class WebViewActivity : BaseActivity() { val url = intent.extras?.getString(URL_KEY) ?: return assistUrl = url - var headers = emptyMap() - (sourceManager.get(intent.extras!!.getLong(SOURCE_KEY)) as? HttpSource)?.let { source -> - try { - headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Failed to build headers" } - } - } - setComposeContent { + // Null until the source it belongs to has been resolved + val headers by produceState?>(initialValue = null) { + val source = sourceManager.get(intent.extras!!.getLong(SOURCE_KEY)) as? HttpSource + value = try { + source?.headers?.toMultimap()?.mapValues { it.value.getOrNull(0) ?: "" }.orEmpty() + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { "Failed to build headers" } + emptyMap() + } + } + + if (headers == null) { + LoadingScreen() + return@setComposeContent + } + WebViewScreenContent( onNavigateUp = { finish() }, initialTitle = intent.extras?.getString(TITLE_KEY), url = url, - headers = headers, + headers = headers.orEmpty(), defaultUserAgentProvider = network::defaultUserAgentProvider, onUrlChange = { assistUrl = it }, onShare = this::shareWebpage, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt index ec40661f9..51e6a6f20 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewScreen.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.ui.webview import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -8,6 +10,7 @@ import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.Screen import eu.kanade.presentation.webview.WebViewScreenContent +import tachiyomi.presentation.core.screens.LoadingScreen class WebViewScreen( private val url: String, @@ -26,11 +29,17 @@ class WebViewScreen( val viewModel = assistedMetroViewModel { create(sourceId = sourceId) } + val headers by viewModel.headers.collectAsState() + if (headers == null) { + LoadingScreen() + return + } + WebViewScreenContent( onNavigateUp = { navigator.pop() }, initialTitle = initialTitle, url = url, - headers = viewModel.headers, + headers = headers.orEmpty(), defaultUserAgentProvider = viewModel::defaultUserAgentProvider, onUrlChange = { assistUrl = it }, onShare = { viewModel.shareWebpage(context, it) }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewViewModel.kt index 9ba4b21a4..27ee37aba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewViewModel.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.webview import android.content.Context import androidx.core.net.toUri import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory @@ -15,6 +16,9 @@ import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import logcat.LogPriority import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import tachiyomi.core.common.util.system.logcat @@ -34,14 +38,18 @@ class WebViewViewModel( fun create(sourceId: Long?): WebViewViewModel } - var headers = emptyMap() + /** Null until the source it belongs to has been resolved. */ + val headers: StateFlow?> + field = MutableStateFlow?>(null) init { - sourceId?.let { sourceManager.get(it) as? HttpSource }?.let { source -> - try { - headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } + viewModelScope.launch { + val source = sourceId?.let { sourceManager.get(it) as? HttpSource } + headers.value = try { + source?.headers?.toMultimap()?.mapValues { it.value.getOrNull(0) ?: "" }.orEmpty() } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Failed to build headers" } + emptyMap() } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt index fe1d33bd2..3d3858e43 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt @@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast -import kotlinx.coroutines.flow.first import kotlinx.datetime.TimeZone import kotlinx.datetime.offsetAt import kotlinx.datetime.toLocalDateTime @@ -66,7 +65,7 @@ class CrashLogUtil( private suspend fun getExtensionsInfo(): String? { val availableExtensions = extensionManager.availableExtensionsFlow.value.associateBy { it.pkgName } - val extensionInfoList = extensionManager.installedExtensionsFlow.first() + val extensionInfoList = extensionManager.getInstalledExtensions() .sortedBy { it.name } .mapNotNull { val availableExtension = availableExtensions[it.pkgName] diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterFilterDownloaded.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterFilterDownloaded.kt index 2f7cba6b4..2591cdf6a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterFilterDownloaded.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterFilterDownloaded.kt @@ -11,5 +11,5 @@ import tachiyomi.source.local.isLocal fun List.filterDownloaded(manga: Manga, downloadCache: DownloadCache): List { if (manga.isLocal()) return this - return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, it.url, manga.title, manga.source, false) } + return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, it.url, manga.title, manga.source) } } diff --git a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt index 9d3562ff5..9db91f9b6 100644 --- a/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt +++ b/app/src/main/java/mihon/feature/migration/config/MigrationConfigScreen.kt @@ -346,7 +346,7 @@ class MigrationConfigScreen(private val mangaIds: Collection) : Screen() { saveSources() } - private fun initSources() { + private suspend fun initSources() { val languages = sourcePreferences.enabledLanguages.get() val pinnedSources = sourcePreferences.pinnedSources.get().mapNotNull { it.toLongOrNull() } val includedSources = sourcePreferences.migrationSources.get() diff --git a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt index bc9ea3fd4..772c96fb3 100644 --- a/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt +++ b/data/src/main/java/tachiyomi/data/source/SourcePagingSource.kt @@ -4,6 +4,7 @@ import androidx.paging.PagingState import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage +import kotlinx.coroutines.CancellationException import mihon.domain.manga.model.toDomainManga import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.domain.manga.interactor.NetworkToLocalManga @@ -11,49 +12,50 @@ import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.source.repository.SourcePagingSource class SourceSearchPagingSource( - source: Source, + source: suspend () -> Source, private val query: String, private val filters: FilterList, networkToLocalManga: NetworkToLocalManga, ) : BaseSourcePagingSource(source, networkToLocalManga) { - override suspend fun requestNextPage(currentPage: Int): MangasPage { + override suspend fun requestNextPage(source: Source, currentPage: Int): MangasPage { return source.getSearchManga(currentPage, query, filters) } } class SourcePopularPagingSource( - source: Source, + source: suspend () -> Source, networkToLocalManga: NetworkToLocalManga, ) : BaseSourcePagingSource(source, networkToLocalManga) { - override suspend fun requestNextPage(currentPage: Int): MangasPage { + override suspend fun requestNextPage(source: Source, currentPage: Int): MangasPage { return source.getPopularManga(currentPage) } } class SourceLatestPagingSource( - source: Source, + source: suspend () -> Source, networkToLocalManga: NetworkToLocalManga, ) : BaseSourcePagingSource(source, networkToLocalManga) { - override suspend fun requestNextPage(currentPage: Int): MangasPage { + override suspend fun requestNextPage(source: Source, currentPage: Int): MangasPage { return source.getLatestUpdates(currentPage) } } abstract class BaseSourcePagingSource( - protected val source: Source, + private val source: suspend () -> Source, private val networkToLocalManga: NetworkToLocalManga, ) : SourcePagingSource() { private val seenManga = hashSetOf() - abstract suspend fun requestNextPage(currentPage: Int): MangasPage + abstract suspend fun requestNextPage(source: Source, currentPage: Int): MangasPage override suspend fun load(params: LoadParams): LoadResult { val page = params.key ?: 1 return try { + val source = source() val mangasPage = withIOContext { - requestNextPage(page.toInt()) + requestNextPage(source, page.toInt()) .takeIf { it.mangas.isNotEmpty() } ?: throw NoResultsException() } @@ -68,6 +70,8 @@ abstract class BaseSourcePagingSource( prevKey = null, nextKey = if (mangasPage.hasNextPage) page + 1 else null, ) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { LoadResult.Error(e) } diff --git a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt index f6c88b2f0..43337d01b 100644 --- a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt @@ -85,15 +85,20 @@ class SourceRepositoryImpl( query: String, filterList: FilterList, ): SourcePagingSource { - return SourceSearchPagingSource(sourceManager.getOrStub(sourceId), query, filterList, networkToLocalManga) + return SourceSearchPagingSource( + { sourceManager.getOrStub(sourceId) }, + query, + filterList, + networkToLocalManga, + ) } override fun getPopular(sourceId: Long): SourcePagingSource { - return SourcePopularPagingSource(sourceManager.getOrStub(sourceId), networkToLocalManga) + return SourcePopularPagingSource({ sourceManager.getOrStub(sourceId) }, networkToLocalManga) } override fun getLatest(sourceId: Long): SourcePagingSource { - return SourceLatestPagingSource(sourceManager.getOrStub(sourceId), networkToLocalManga) + return SourceLatestPagingSource({ sourceManager.getOrStub(sourceId) }, networkToLocalManga) } private fun mapSourceToDomainSource(source: Source): DomainSource = DomainSource( diff --git a/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt b/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt index 554ccf0fd..26408a4c8 100644 --- a/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt +++ b/domain/src/main/java/tachiyomi/domain/source/service/SourceManager.kt @@ -3,22 +3,19 @@ package tachiyomi.domain.source.service import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow import tachiyomi.domain.source.model.StubSource interface SourceManager { - val isInitialized: StateFlow - val sources: Flow> - fun get(sourceKey: Long): Source? + suspend fun get(sourceKey: Long): Source? - fun getOrStub(sourceKey: Long): Source + suspend fun getOrStub(sourceKey: Long): Source - fun getAll(): List + suspend fun getAll(): List - fun getOnlineSources(): List + suspend fun getOnlineSources(): List - fun getStubSources(): List + suspend fun getStubSources(): List }