Make the source manager surface suspend (#3869)
Assisted-by: Claude:claude-opus-5
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<Context>().appGraph.extensionManager.getAppIconForSource(id)
|
||||
?.toBitmap()
|
||||
?.asImageBitmap()
|
||||
}
|
||||
suspend fun Source.icon(): ImageBitmap? = withIOContext {
|
||||
Injekt.get<Context>().appGraph.extensionManager.getAppIconForSource(id)
|
||||
?.toBitmap()
|
||||
?.asImageBitmap()
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ fun SourceIcon(
|
||||
source: Source,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val icon = source.icon
|
||||
val icon = produceState<ImageBitmap?>(initialValue = null, source.id) { value = source.icon() }.value
|
||||
|
||||
when {
|
||||
source.isStub && icon == null -> {
|
||||
|
||||
@@ -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<Source?>(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,
|
||||
|
||||
+3
-1
@@ -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()) {
|
||||
|
||||
+6
-2
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -136,7 +136,7 @@ class BackupCreator(
|
||||
return mangaBackupCreator(mangas, options)
|
||||
}
|
||||
|
||||
private fun backupSources(mangas: List<BackupManga>): List<BackupSource> {
|
||||
private suspend fun backupSources(mangas: List<BackupManga>): List<BackupSource> {
|
||||
return sourcesBackupCreator(mangas)
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ class BackupCreator(
|
||||
return extensionStoresBackupCreator()
|
||||
}
|
||||
|
||||
private fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||
private suspend fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||
if (!options.sourceSettings) return emptyList()
|
||||
|
||||
return preferenceBackupCreator.createSource(includePrivatePreferences = options.privateSettings)
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ class PreferenceBackupCreator(
|
||||
.withPrivatePreferences(includePrivatePreferences)
|
||||
}
|
||||
|
||||
fun createSource(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
|
||||
suspend fun createSource(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
|
||||
return sourceManager.getAll()
|
||||
.filterIsInstance<ConfigurableSource>()
|
||||
.map {
|
||||
|
||||
+2
-5
@@ -11,14 +11,11 @@ class SourcesBackupCreator(
|
||||
private val sourceManager: SourceManager,
|
||||
) {
|
||||
|
||||
operator fun invoke(mangas: List<BackupManga>): List<BackupSource> {
|
||||
suspend operator fun invoke(mangas: List<BackupManga>): List<BackupSource> {
|
||||
return mangas
|
||||
.asSequence()
|
||||
.map(BackupManga::source)
|
||||
.distinct()
|
||||
.map(sourceManager::getOrStub)
|
||||
.map { it.toBackupSource() }
|
||||
.toList()
|
||||
.map { sourceManager.getOrStub(it).toBackupSource() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class MangaCoverFetcher(
|
||||
private val coverFileLazy: Lazy<File?>,
|
||||
private val customCoverFileLazy: Lazy<File>,
|
||||
private val diskCacheKeyLazy: Lazy<String>,
|
||||
private val sourceLazy: Lazy<HttpSource?>,
|
||||
private val getSource: suspend () -> HttpSource?,
|
||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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<Source>()
|
||||
withTimeoutOrNull(30.seconds) {
|
||||
sourceManager.isInitialized.first { it }
|
||||
|
||||
sources = getSources()
|
||||
}
|
||||
|
||||
@@ -418,7 +409,7 @@ class DownloadCache(
|
||||
notifyChanges()
|
||||
}
|
||||
|
||||
private fun getSources(): List<Source> {
|
||||
private suspend fun getSources(): List<Source> {
|
||||
return sourceManager.getOnlineSources() + sourceManager.getStubSources()
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Chapter>, autoStart: Boolean = true) {
|
||||
suspend fun downloadChapters(manga: Manga, chapters: List<Chapter>, 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
|
||||
|
||||
@@ -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<Download> {
|
||||
suspend fun restore(): List<Download> {
|
||||
val objs = preferences.all
|
||||
.mapNotNull { it.value as? String }
|
||||
.mapNotNull { deserialize(it) }
|
||||
@@ -103,10 +102,10 @@ class DownloadStore(
|
||||
val cachedManga = mutableMapOf<Long, Manga?>()
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Chapter>, autoStart: Boolean) {
|
||||
suspend fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) {
|
||||
if (chapters.isEmpty()) return
|
||||
|
||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return
|
||||
|
||||
@@ -329,7 +329,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
private suspend fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
// 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<Pair<Manga, String?>>): File {
|
||||
private suspend fun writeErrorFile(errors: List<Pair<Manga, String?>>): 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +113,10 @@ class LibraryUpdateNotifier(
|
||||
/**
|
||||
* Warn when excessively checking any single source.
|
||||
*/
|
||||
fun showQueueSizeWarningNotificationIfNeeded(mangaToUpdate: List<LibraryManga>) {
|
||||
suspend fun showQueueSizeWarningNotificationIfNeeded(mangaToUpdate: List<LibraryManga>) {
|
||||
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) {
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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<Unit>()
|
||||
|
||||
private val iconMap = mutableMapOf<String, Drawable>()
|
||||
|
||||
private val installedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>())
|
||||
val installedExtensionsFlow = installedExtensionMapFlow.mapExtensionsOnceInitialized()
|
||||
val installedExtensionsFlow = installedExtensionMapFlow.mapExtensionsWhenInitialized()
|
||||
|
||||
private val availableExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
|
||||
val availableExtensionsFlow = availableExtensionMapFlow.mapExtensions(scope)
|
||||
|
||||
private val untrustedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>())
|
||||
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<Extension.Installed> {
|
||||
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<LoadResult.Success>()
|
||||
.associate { it.extension.pkgName to it.extension }
|
||||
installedExtensionMapFlow.value = extensions
|
||||
.filterIsInstance<LoadResult.Success>()
|
||||
.associate { it.extension.pkgName to it.extension }
|
||||
|
||||
untrustedExtensionMapFlow.value = extensions
|
||||
.filterIsInstance<LoadResult.Untrusted>()
|
||||
.associate { it.extension.pkgName to it.extension }
|
||||
untrustedExtensionMapFlow.value = extensions
|
||||
.filterIsInstance<LoadResult.Untrusted>()
|
||||
.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 <T : Extension> StateFlow<Map<String, T>>.mapExtensionsOnceInitialized(): Flow<List<T>> {
|
||||
return onStart { isInitialized.first { it } }.map { it.values.toList() }
|
||||
private fun <T : Extension> StateFlow<Map<String, T>>.mapExtensionsWhenInitialized(): Flow<List<T>> {
|
||||
return onStart { initialized.await() }.map { it.values.toList() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DownloadManager>,
|
||||
) : SourceManager {
|
||||
|
||||
private val _isInitialized = MutableStateFlow(false)
|
||||
override val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
|
||||
private val sourcesMapFlow = MutableStateFlow(ConcurrentHashMap<Long, Source>())
|
||||
/**
|
||||
* Null until the extensions have loaded, so that nothing observes the empty seed value.
|
||||
*/
|
||||
private val sourcesMapFlow = MutableStateFlow<Map<Long, Source>?>(null)
|
||||
|
||||
private val stubSourcesMap = ConcurrentHashMap<Long, StubSource>()
|
||||
|
||||
override val sources: Flow<List<Source>> = sourcesMapFlow.map { it.values.toList() }
|
||||
override val sources: Flow<List<Source>> = 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<Long, Source> = 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<Source> {
|
||||
return sourcesMap().values.toList()
|
||||
}
|
||||
|
||||
override fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance<HttpSource>()
|
||||
override suspend fun getOnlineSources(): List<HttpSource> {
|
||||
return sourcesMap().values.filterIsInstance<HttpSource>()
|
||||
}
|
||||
|
||||
override fun getStubSources(): List<StubSource> {
|
||||
override suspend fun getStubSources(): List<StubSource> {
|
||||
val onlineSourceIds = getOnlineSources().map { it.id }
|
||||
return stubSourcesMap.values.filterNot { it.id in onlineSourceIds }
|
||||
}
|
||||
|
||||
+17
-8
@@ -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<Source?>(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)
|
||||
|
||||
+3
-2
@@ -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<MigrationMangaEvent> = Channel()
|
||||
val events: Flow<MigrationMangaEvent> = _events.receiveAsFlow()
|
||||
|
||||
private val source by lazy { sourceManager.getOrStub(sourceId) }
|
||||
private val source = viewModelScope.async { sourceManager.getOrStub(sourceId) }
|
||||
|
||||
private val selection = MutableStateFlow(emptySet<Long>())
|
||||
|
||||
@@ -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())
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ class MigrateSearchViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEnabledSources(): List<Source> {
|
||||
override suspend fun getEnabledSources(): List<Source> {
|
||||
return migrationSources.mapNotNull { sourceManager.get(it) }
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -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,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
||||
+17
-17
@@ -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<BrowseSourceViewModel, BrowseSourceViewModel.Factory> {
|
||||
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,
|
||||
|
||||
+32
-21
@@ -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,
|
||||
|
||||
-6
@@ -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 =
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ class GlobalSearchViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEnabledSources(): List<Source> {
|
||||
override suspend fun getEnabledSources(): List<Source> {
|
||||
return super.getEnabledSources()
|
||||
.filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
|
||||
}
|
||||
|
||||
+2
-3
@@ -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<Source> {
|
||||
open suspend fun getEnabledSources(): List<Source> {
|
||||
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 }
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Chapter>) {
|
||||
private suspend fun downloadChapters(chapters: List<Chapter>) {
|
||||
val manga = successState?.manga ?: return
|
||||
downloadManager.downloadChapters(manga, chapters)
|
||||
toggleAllSelection(false)
|
||||
|
||||
@@ -273,7 +273,7 @@ data class TrackInfoDialogHomeScreen(
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Track>.mapToTrackItem(): List<TrackItem> {
|
||||
private suspend fun List<Track>.mapToTrackItem(): List<TrackItem> {
|
||||
val loggedInTrackers = trackerManager.loggedInTrackers()
|
||||
val source = sourceManager.getOrStub(sourceId)
|
||||
return loggedInTrackers
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
+6
-1
@@ -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)
|
||||
}
|
||||
|
||||
+6
-1
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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<String, String>()
|
||||
(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<Map<String, String>?>(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,
|
||||
|
||||
@@ -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<WebViewViewModel, WebViewViewModel.Factory> { 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) },
|
||||
|
||||
@@ -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<String, String>()
|
||||
/** Null until the source it belongs to has been resolved. */
|
||||
val headers: StateFlow<Map<String, String>?>
|
||||
field = MutableStateFlow<Map<String, String>?>(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -11,5 +11,5 @@ import tachiyomi.source.local.isLocal
|
||||
fun List<Chapter>.filterDownloaded(manga: Manga, downloadCache: DownloadCache): List<Chapter> {
|
||||
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) }
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ class MigrationConfigScreen(private val mangaIds: Collection<Long>) : 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()
|
||||
|
||||
Reference in New Issue
Block a user