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 sourcePreferences: SourcePreferences,
|
||||||
private val extensionManager: ExtensionManager,
|
private val extensionManager: ExtensionManager,
|
||||||
) {
|
) {
|
||||||
fun await(sourceId: Long?): Boolean {
|
suspend fun await(sourceId: Long?): Boolean {
|
||||||
if (basePreferences.incognitoMode.get()) return true
|
if (basePreferences.incognitoMode.get()) return true
|
||||||
if (sourceId == null) return false
|
if (sourceId == null) return false
|
||||||
val extensionPackage = extensionManager.getExtensionPackage(sourceId) ?: 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.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import mihon.app.di.appGraph
|
import mihon.app.di.appGraph
|
||||||
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
val Source.icon: ImageBitmap?
|
suspend fun Source.icon(): ImageBitmap? = withIOContext {
|
||||||
get() {
|
Injekt.get<Context>().appGraph.extensionManager.getAppIconForSource(id)
|
||||||
return Injekt.get<Context>().appGraph.extensionManager.getAppIconForSource(id)
|
?.toBitmap()
|
||||||
?.toBitmap()
|
?.asImageBitmap()
|
||||||
?.asImageBitmap()
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ fun SourceIcon(
|
|||||||
source: Source,
|
source: Source,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val icon = source.icon
|
val icon = produceState<ImageBitmap?>(initialValue = null, source.id) { value = source.icon() }.value
|
||||||
|
|
||||||
when {
|
when {
|
||||||
source.isStub && icon == null -> {
|
source.isStub && icon == null -> {
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import androidx.compose.material3.OutlinedButton
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -170,12 +172,12 @@ fun DuplicateMangaDialog(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun DuplicateMangaListItem(
|
private fun DuplicateMangaListItem(
|
||||||
duplicate: MangaWithChapterCount,
|
duplicate: MangaWithChapterCount,
|
||||||
getSource: () -> Source,
|
getSource: suspend () -> Source,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onOpenManga: () -> Unit,
|
onOpenManga: () -> Unit,
|
||||||
onMigrate: () -> Unit,
|
onMigrate: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val source = getSource()
|
val source by produceState<Source?>(initialValue = null) { value = getSource() }
|
||||||
val manga = duplicate.manga
|
val manga = duplicate.manga
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -277,7 +279,7 @@ private fun DuplicateMangaListItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = source.name,
|
text = source?.name.orEmpty(),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|||||||
+3
-1
@@ -25,6 +25,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
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
|
val enhancedTrackers = trackerManager.trackers
|
||||||
.filter { it is EnhancedTracker }
|
.filter { it is EnhancedTracker }
|
||||||
.partition { service ->
|
.partition { service ->
|
||||||
val acceptedSources = (service as EnhancedTracker).getAcceptedSources()
|
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)
|
var enhancedTrackerInfo = stringResource(MR.strings.enhanced_tracking_info)
|
||||||
if (enhancedTrackers.second.isNotEmpty()) {
|
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.compose.ui.text.withStyle
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import dev.zacsweers.metro.AppScope
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||||
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||||
@@ -191,7 +193,9 @@ class RestoreBackupViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
validate(uri.toUri())
|
viewModelScope.launchIO {
|
||||||
|
validate(uri.toUri())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) {
|
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 {
|
val results = try {
|
||||||
backupFileValidator.validate(uri)
|
backupFileValidator.validate(uri)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class BackupFileValidator(
|
|||||||
*
|
*
|
||||||
* @return List of missing sources or missing trackers.
|
* @return List of missing sources or missing trackers.
|
||||||
*/
|
*/
|
||||||
fun validate(uri: Uri): Results {
|
suspend fun validate(uri: Uri): Results {
|
||||||
val backup = try {
|
val backup = try {
|
||||||
backupDecoder.decode(uri)
|
backupDecoder.decode(uri)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -26,7 +26,7 @@ class BackupFileValidator(
|
|||||||
|
|
||||||
val sources = backup.backupSources.associate { it.sourceId to it.name }
|
val sources = backup.backupSources.associate { it.sourceId to it.name }
|
||||||
val missingSources = sources
|
val missingSources = sources
|
||||||
.filter { sourceManager.get(it.key) == null }
|
.filterKeys { sourceManager.get(it) == null }
|
||||||
.values.map {
|
.values.map {
|
||||||
val id = it.toLongOrNull()
|
val id = it.toLongOrNull()
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ class BackupCreator(
|
|||||||
return mangaBackupCreator(mangas, options)
|
return mangaBackupCreator(mangas, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun backupSources(mangas: List<BackupManga>): List<BackupSource> {
|
private suspend fun backupSources(mangas: List<BackupManga>): List<BackupSource> {
|
||||||
return sourcesBackupCreator(mangas)
|
return sourcesBackupCreator(mangas)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ class BackupCreator(
|
|||||||
return extensionStoresBackupCreator()
|
return extensionStoresBackupCreator()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
private suspend fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||||
if (!options.sourceSettings) return emptyList()
|
if (!options.sourceSettings) return emptyList()
|
||||||
|
|
||||||
return preferenceBackupCreator.createSource(includePrivatePreferences = options.privateSettings)
|
return preferenceBackupCreator.createSource(includePrivatePreferences = options.privateSettings)
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ class PreferenceBackupCreator(
|
|||||||
.withPrivatePreferences(includePrivatePreferences)
|
.withPrivatePreferences(includePrivatePreferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createSource(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
|
suspend fun createSource(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
|
||||||
return sourceManager.getAll()
|
return sourceManager.getAll()
|
||||||
.filterIsInstance<ConfigurableSource>()
|
.filterIsInstance<ConfigurableSource>()
|
||||||
.map {
|
.map {
|
||||||
|
|||||||
+2
-5
@@ -11,14 +11,11 @@ class SourcesBackupCreator(
|
|||||||
private val sourceManager: SourceManager,
|
private val sourceManager: SourceManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
operator fun invoke(mangas: List<BackupManga>): List<BackupSource> {
|
suspend operator fun invoke(mangas: List<BackupManga>): List<BackupSource> {
|
||||||
return mangas
|
return mangas
|
||||||
.asSequence()
|
|
||||||
.map(BackupManga::source)
|
.map(BackupManga::source)
|
||||||
.distinct()
|
.distinct()
|
||||||
.map(sourceManager::getOrStub)
|
.map { sourceManager.getOrStub(it).toBackupSource() }
|
||||||
.map { it.toBackupSource() }
|
|
||||||
.toList()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class MangaCoverFetcher(
|
|||||||
private val coverFileLazy: Lazy<File?>,
|
private val coverFileLazy: Lazy<File?>,
|
||||||
private val customCoverFileLazy: Lazy<File>,
|
private val customCoverFileLazy: Lazy<File>,
|
||||||
private val diskCacheKeyLazy: Lazy<String>,
|
private val diskCacheKeyLazy: Lazy<String>,
|
||||||
private val sourceLazy: Lazy<HttpSource?>,
|
private val getSource: suspend () -> HttpSource?,
|
||||||
private val callFactoryLazy: Lazy<Call.Factory>,
|
private val callFactoryLazy: Lazy<Call.Factory>,
|
||||||
private val imageLoader: ImageLoader,
|
private val imageLoader: ImageLoader,
|
||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
@@ -169,8 +169,9 @@ class MangaCoverFetcher(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun executeNetworkRequest(): Response {
|
private suspend fun executeNetworkRequest(): Response {
|
||||||
val client = sourceLazy.value?.client ?: callFactoryLazy.value
|
val source = getSource()
|
||||||
val response = client.newCall(newRequest()).await()
|
val client = source?.client ?: callFactoryLazy.value
|
||||||
|
val response = client.newCall(newRequest(source)).await()
|
||||||
if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) {
|
if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) {
|
||||||
response.close()
|
response.close()
|
||||||
throw IOException(response.message)
|
throw IOException(response.message)
|
||||||
@@ -178,11 +179,11 @@ class MangaCoverFetcher(
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newRequest(): Request {
|
private fun newRequest(source: HttpSource?): Request {
|
||||||
val request = Request.Builder().apply {
|
val request = Request.Builder().apply {
|
||||||
url(url!!)
|
url(url!!)
|
||||||
|
|
||||||
val sourceHeaders = sourceLazy.value?.headers
|
val sourceHeaders = source?.headers
|
||||||
if (sourceHeaders != null) {
|
if (sourceHeaders != null) {
|
||||||
headers(sourceHeaders)
|
headers(sourceHeaders)
|
||||||
}
|
}
|
||||||
@@ -310,7 +311,7 @@ class MangaCoverFetcher(
|
|||||||
coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnailUrl) },
|
coverFileLazy = lazy { coverCache.getCoverFile(data.thumbnailUrl) },
|
||||||
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.id) },
|
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.id) },
|
||||||
diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
|
diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
|
||||||
sourceLazy = lazy { sourceManager.get(data.source) as? HttpSource },
|
getSource = { sourceManager.get(data.source) as? HttpSource },
|
||||||
callFactoryLazy = callFactoryLazy,
|
callFactoryLazy = callFactoryLazy,
|
||||||
imageLoader = imageLoader,
|
imageLoader = imageLoader,
|
||||||
)
|
)
|
||||||
@@ -331,7 +332,7 @@ class MangaCoverFetcher(
|
|||||||
coverFileLazy = lazy { coverCache.getCoverFile(data.url) },
|
coverFileLazy = lazy { coverCache.getCoverFile(data.url) },
|
||||||
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.mangaId) },
|
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.mangaId) },
|
||||||
diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
|
diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
|
||||||
sourceLazy = lazy { sourceManager.get(data.sourceId) as? HttpSource },
|
getSource = { sourceManager.get(data.sourceId) as? HttpSource },
|
||||||
callFactoryLazy = callFactoryLazy,
|
callFactoryLazy = callFactoryLazy,
|
||||||
imageLoader = imageLoader,
|
imageLoader = imageLoader,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ class DownloadCache(
|
|||||||
* @param chapterUrl the url of the chapter to query
|
* @param chapterUrl the url of the chapter to query
|
||||||
* @param mangaTitle the title of the manga to query.
|
* @param mangaTitle the title of the manga to query.
|
||||||
* @param sourceId the id of the source of the chapter.
|
* @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(
|
fun isChapterDownloaded(
|
||||||
chapterName: String,
|
chapterName: String,
|
||||||
@@ -141,13 +140,7 @@ class DownloadCache(
|
|||||||
chapterUrl: String,
|
chapterUrl: String,
|
||||||
mangaTitle: String,
|
mangaTitle: String,
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
skipCache: Boolean,
|
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (skipCache) {
|
|
||||||
val source = sourceManager.getOrStub(sourceId)
|
|
||||||
return provider.findChapterDir(chapterName, chapterScanlator, chapterUrl, mangaTitle, source) != null
|
|
||||||
}
|
|
||||||
|
|
||||||
renewCache()
|
renewCache()
|
||||||
|
|
||||||
val sourceDir = rootDownloadsDir.sourceDirs[sourceId]
|
val sourceDir = rootDownloadsDir.sourceDirs[sourceId]
|
||||||
@@ -354,8 +347,6 @@ class DownloadCache(
|
|||||||
// Try to wait until extensions and sources have loaded
|
// Try to wait until extensions and sources have loaded
|
||||||
var sources = emptyList<Source>()
|
var sources = emptyList<Source>()
|
||||||
withTimeoutOrNull(30.seconds) {
|
withTimeoutOrNull(30.seconds) {
|
||||||
sourceManager.isInitialized.first { it }
|
|
||||||
|
|
||||||
sources = getSources()
|
sources = getSources()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +409,7 @@ class DownloadCache(
|
|||||||
notifyChanges()
|
notifyChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSources(): List<Source> {
|
private suspend fun getSources(): List<Source> {
|
||||||
return sourceManager.getOnlineSources() + sourceManager.getStubSources()
|
return sourceManager.getOnlineSources() + sourceManager.getStubSources()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class DownloadManager(
|
|||||||
* @param chapters the list of chapters to enqueue.
|
* @param chapters the list of chapters to enqueue.
|
||||||
* @param autoStart whether to start the downloader after enqueing the chapters.
|
* @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)
|
downloader.queueChapters(manga, chapters, autoStart)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +188,6 @@ class DownloadManager(
|
|||||||
* @param chapterScanlator scanlator of the chapter to query
|
* @param chapterScanlator scanlator of the chapter to query
|
||||||
* @param mangaTitle the title of the manga to query.
|
* @param mangaTitle the title of the manga to query.
|
||||||
* @param sourceId the id of the source of the chapter.
|
* @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(
|
fun isChapterDownloaded(
|
||||||
chapterName: String,
|
chapterName: String,
|
||||||
@@ -196,9 +195,26 @@ class DownloadManager(
|
|||||||
chapterUrl: String,
|
chapterUrl: String,
|
||||||
mangaTitle: String,
|
mangaTitle: String,
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
skipCache: Boolean = false,
|
|
||||||
): Boolean {
|
): 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.
|
* Triggers the execution of the deletion of pending chapters.
|
||||||
*/
|
*/
|
||||||
fun deletePendingChapters() {
|
suspend fun deletePendingChapters() {
|
||||||
val pendingChapters = pendingDeleter.getPendingChapters()
|
val pendingChapters = pendingDeleter.getPendingChapters()
|
||||||
for ((manga, chapters) in pendingChapters) {
|
for ((manga, chapters) in pendingChapters) {
|
||||||
val source = sourceManager.get(manga.source) ?: continue
|
val source = sourceManager.get(manga.source) ?: continue
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import dev.zacsweers.metro.Inject
|
|||||||
import dev.zacsweers.metro.SingleIn
|
import dev.zacsweers.metro.SingleIn
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import tachiyomi.domain.chapter.interactor.GetChapter
|
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.
|
* 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
|
val objs = preferences.all
|
||||||
.mapNotNull { it.value as? String }
|
.mapNotNull { it.value as? String }
|
||||||
.mapNotNull { deserialize(it) }
|
.mapNotNull { deserialize(it) }
|
||||||
@@ -103,10 +102,10 @@ class DownloadStore(
|
|||||||
val cachedManga = mutableMapOf<Long, Manga?>()
|
val cachedManga = mutableMapOf<Long, Manga?>()
|
||||||
for ((mangaId, chapterId) in objs) {
|
for ((mangaId, chapterId) in objs) {
|
||||||
val manga = cachedManga.getOrPut(mangaId) {
|
val manga = cachedManga.getOrPut(mangaId) {
|
||||||
runBlocking { getManga.await(mangaId) }
|
getManga.await(mangaId)
|
||||||
} ?: continue
|
} ?: continue
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: 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))
|
downloads.add(Download(source, manga, chapter))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ class Downloader(
|
|||||||
* @param chapters the list of chapters to download.
|
* @param chapters the list of chapters to download.
|
||||||
* @param autoStart whether to start the downloader after enqueing the chapters.
|
* @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
|
if (chapters.isEmpty()) return
|
||||||
|
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: 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
|
// 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.
|
// may don't like it and they could ban the user.
|
||||||
downloadManager.downloadChapters(manga, chapters, false)
|
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.
|
* 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 {
|
try {
|
||||||
if (errors.isNotEmpty()) {
|
if (errors.isNotEmpty()) {
|
||||||
val file = context.createFileInCacheDir("mihon_update_errors.txt")
|
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) ->
|
mangas.groupBy { it.source }.forEach { (srcId, mangas) ->
|
||||||
val source = sourceManager.getOrStub(srcId)
|
val source = sourceManager.getOrStub(srcId)
|
||||||
out.write(" # $source\n")
|
out.write(" # $source\n")
|
||||||
mangas.forEach {
|
mangas.forEach { manga ->
|
||||||
out.write(" - ${it.title}\n")
|
out.write(" - ${manga.title}\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,10 +113,10 @@ class LibraryUpdateNotifier(
|
|||||||
/**
|
/**
|
||||||
* Warn when excessively checking any single source.
|
* Warn when excessively checking any single source.
|
||||||
*/
|
*/
|
||||||
fun showQueueSizeWarningNotificationIfNeeded(mangaToUpdate: List<LibraryManga>) {
|
suspend fun showQueueSizeWarningNotificationIfNeeded(mangaToUpdate: List<LibraryManga>) {
|
||||||
val maxUpdatesFromSource = mangaToUpdate
|
val maxUpdatesFromSource = mangaToUpdate
|
||||||
.groupBy { it.manga.source }
|
.groupBy { it.manga.source }
|
||||||
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
|
.filter { (sourceId, _) -> sourceManager.get(sourceId) !is UnmeteredSource }
|
||||||
.maxOfOrNull { it.value.size } ?: 0
|
.maxOfOrNull { it.value.size } ?: 0
|
||||||
|
|
||||||
if (maxUpdatesFromSource <= MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
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.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.sourcePreferences
|
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.i18n.MR
|
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) }
|
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }
|
||||||
.reduce(Long::or) and Long.MAX_VALUE
|
.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 prefApiUrl = preferences.getString("APIURL", "")
|
||||||
val prefApiKey = preferences.getString("APIKEY", "")
|
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.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.sourcePreferences
|
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.addAll
|
import kotlinx.serialization.json.addAll
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
@@ -29,8 +30,12 @@ class SuwayomiApi(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
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 client: OkHttpClient by lazy { source.client }
|
||||||
private val baseUrl: String by lazy { source.baseUrl.trimEnd('/') }
|
private val baseUrl: String by lazy { source.baseUrl.trimEnd('/') }
|
||||||
private val apiUrl: String by lazy { "$baseUrl/api/graphql" }
|
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.ExtensionInstaller
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -24,7 +25,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -56,18 +56,18 @@ class ExtensionManager(
|
|||||||
|
|
||||||
val scope = CoroutineScope(SupervisorJob())
|
val scope = CoroutineScope(SupervisorJob())
|
||||||
|
|
||||||
private val isInitialized = MutableStateFlow(false)
|
private val initialized = CompletableDeferred<Unit>()
|
||||||
|
|
||||||
private val iconMap = mutableMapOf<String, Drawable>()
|
private val iconMap = mutableMapOf<String, Drawable>()
|
||||||
|
|
||||||
private val installedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>())
|
private val installedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>())
|
||||||
val installedExtensionsFlow = installedExtensionMapFlow.mapExtensionsOnceInitialized()
|
val installedExtensionsFlow = installedExtensionMapFlow.mapExtensionsWhenInitialized()
|
||||||
|
|
||||||
private val availableExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
|
private val availableExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
|
||||||
val availableExtensionsFlow = availableExtensionMapFlow.mapExtensions(scope)
|
val availableExtensionsFlow = availableExtensionMapFlow.mapExtensions(scope)
|
||||||
|
|
||||||
private val untrustedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>())
|
private val untrustedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>())
|
||||||
val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensionsOnceInitialized()
|
val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensionsWhenInitialized()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
@@ -78,8 +78,13 @@ class ExtensionManager(
|
|||||||
|
|
||||||
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages.isSet()
|
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages.isSet()
|
||||||
|
|
||||||
fun getExtensionPackage(sourceId: Long): String? {
|
suspend fun getInstalledExtensions(): List<Extension.Installed> {
|
||||||
return installedExtensionMapFlow.value.values.find { extension ->
|
initialized.await()
|
||||||
|
return installedExtensionMapFlow.value.values.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getExtensionPackage(sourceId: Long): String? {
|
||||||
|
return getInstalledExtensions().find { extension ->
|
||||||
extension.sources.any { it.id == sourceId }
|
extension.sources.any { it.id == sourceId }
|
||||||
}
|
}
|
||||||
?.pkgName
|
?.pkgName
|
||||||
@@ -94,7 +99,7 @@ class ExtensionManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAppIconForSource(sourceId: Long): Drawable? {
|
suspend fun getAppIconForSource(sourceId: Long): Drawable? {
|
||||||
val pkgName = getExtensionPackage(sourceId) ?: return null
|
val pkgName = getExtensionPackage(sourceId) ?: return null
|
||||||
|
|
||||||
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
|
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
|
||||||
@@ -118,17 +123,23 @@ class ExtensionManager(
|
|||||||
* Loads and registers the installed extensions.
|
* Loads and registers the installed extensions.
|
||||||
*/
|
*/
|
||||||
private fun initExtensions() {
|
private fun initExtensions() {
|
||||||
val extensions = ExtensionLoader.loadExtensions(context)
|
try {
|
||||||
|
val extensions = ExtensionLoader.loadExtensions(context)
|
||||||
|
|
||||||
installedExtensionMapFlow.value = extensions
|
installedExtensionMapFlow.value = extensions
|
||||||
.filterIsInstance<LoadResult.Success>()
|
.filterIsInstance<LoadResult.Success>()
|
||||||
.associate { it.extension.pkgName to it.extension }
|
.associate { it.extension.pkgName to it.extension }
|
||||||
|
|
||||||
untrustedExtensionMapFlow.value = extensions
|
untrustedExtensionMapFlow.value = extensions
|
||||||
.filterIsInstance<LoadResult.Untrusted>()
|
.filterIsInstance<LoadResult.Untrusted>()
|
||||||
.associate { it.extension.pkgName to it.extension }
|
.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
|
* Extensions are loaded in the background, so this flow only starts emitting once that finished.
|
||||||
* seeded with at construction, so this only starts emitting once that finished.
|
|
||||||
*/
|
*/
|
||||||
private fun <T : Extension> StateFlow<Map<String, T>>.mapExtensionsOnceInitialized(): Flow<List<T>> {
|
private fun <T : Extension> StateFlow<Map<String, T>>.mapExtensionsWhenInitialized(): Flow<List<T>> {
|
||||||
return onStart { isInitialized.first { it } }.map { it.values.toList() }
|
return onStart { initialized.await() }.map { it.values.toList() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import tachiyomi.domain.source.model.StubSource
|
import tachiyomi.domain.source.model.StubSource
|
||||||
import tachiyomi.domain.source.repository.StubSourceRepository
|
import tachiyomi.domain.source.repository.StubSourceRepository
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
@@ -34,16 +33,18 @@ class AndroidSourceManager(
|
|||||||
private val downloadManager: Lazy<DownloadManager>,
|
private val downloadManager: Lazy<DownloadManager>,
|
||||||
) : SourceManager {
|
) : SourceManager {
|
||||||
|
|
||||||
private val _isInitialized = MutableStateFlow(false)
|
|
||||||
override val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.IO)
|
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>()
|
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 {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -59,7 +60,6 @@ class AndroidSourceManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sourcesMapFlow.value = mutableMap
|
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 {
|
override suspend fun getOrStub(sourceKey: Long): Source {
|
||||||
return sourcesMapFlow.value[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
|
return sourcesMap()[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
|
||||||
runBlocking { createStubSource(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 }
|
val onlineSourceIds = getOnlineSources().map { it.id }
|
||||||
return stubSourcesMap.values.filterNot { it.id in onlineSourceIds }
|
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.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -30,14 +31,15 @@ import androidx.preference.forEach
|
|||||||
import androidx.preference.getOnBindEditTextListener
|
import androidx.preference.getOnBindEditTextListener
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.core.util.ifSourcesLoaded
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.sourcePreferences
|
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||||
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
|
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import mihon.app.di.appGraph
|
import mihon.app.di.appGraph
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
@@ -46,18 +48,22 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
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()
|
LoadingScreen()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
AppBar(
|
AppBar(
|
||||||
title = context.appGraph.sourceManager.getOrStub(sourceId).toString(),
|
title = source.toString(),
|
||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
scrollBehavior = it,
|
scrollBehavior = it,
|
||||||
)
|
)
|
||||||
@@ -125,10 +131,13 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
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 sourceId = requireArguments().getLong(SOURCE_ID)
|
||||||
val appGraph = requireContext().appGraph
|
val appGraph = requireContext().appGraph
|
||||||
val source = appGraph.sourceManager.getOrStub(sourceId)
|
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 dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactoryKey
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -50,7 +51,7 @@ class MigrateMangaViewModel(
|
|||||||
private val _events: Channel<MigrationMangaEvent> = Channel()
|
private val _events: Channel<MigrationMangaEvent> = Channel()
|
||||||
val events: Flow<MigrationMangaEvent> = _events.receiveAsFlow()
|
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>())
|
private val selection = MutableStateFlow(emptySet<Long>())
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ class MigrateMangaViewModel(
|
|||||||
favorites,
|
favorites,
|
||||||
selection,
|
selection,
|
||||||
) { titleList, selection ->
|
) { titleList, selection ->
|
||||||
State(source = source, selection = selection, titleList = titleList)
|
State(source = source.await(), selection = selection, titleList = titleList)
|
||||||
}
|
}
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), State())
|
.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) }
|
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.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
|
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
|
||||||
import eu.kanade.core.util.ifSourcesLoaded
|
|
||||||
import eu.kanade.presentation.browse.BrowseSourceContent
|
import eu.kanade.presentation.browse.BrowseSourceContent
|
||||||
import eu.kanade.presentation.components.SearchToolbar
|
import eu.kanade.presentation.components.SearchToolbar
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
@@ -50,11 +49,6 @@ data class MigrateSourceSearchScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
if (!ifSourcesLoaded()) {
|
|
||||||
LoadingScreen()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -65,6 +59,12 @@ data class MigrateSourceSearchScreen(
|
|||||||
}
|
}
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
val source = state.source
|
||||||
|
if (source == null) {
|
||||||
|
LoadingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -103,19 +103,19 @@ data class MigrateSourceSearchScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
BrowseSourceContent(
|
BrowseSourceContent(
|
||||||
source = viewModel.source,
|
source = source,
|
||||||
mangaList = viewModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
|
mangaList = viewModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
|
||||||
columns = viewModel.getColumnsPreference(LocalConfiguration.current.orientation),
|
columns = viewModel.getColumnsPreference(LocalConfiguration.current.orientation),
|
||||||
displayMode = viewModel.displayMode,
|
displayMode = viewModel.displayMode,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
onWebViewClick = {
|
onWebViewClick = {
|
||||||
val source = viewModel.source as? HttpSource ?: return@BrowseSourceContent
|
val httpSource = source as? HttpSource ?: return@BrowseSourceContent
|
||||||
navigator.push(
|
navigator.push(
|
||||||
WebViewScreen(
|
WebViewScreen(
|
||||||
url = source.getHomeUrl(),
|
url = httpSource.getHomeUrl(),
|
||||||
initialTitle = source.name,
|
initialTitle = httpSource.name,
|
||||||
sourceId = source.id,
|
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.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
|
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
|
||||||
import eu.kanade.core.util.ifSourcesLoaded
|
|
||||||
import eu.kanade.presentation.browse.BrowseSourceContent
|
import eu.kanade.presentation.browse.BrowseSourceContent
|
||||||
import eu.kanade.presentation.browse.MissingSourceScreen
|
import eu.kanade.presentation.browse.MissingSourceScreen
|
||||||
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
|
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
|
||||||
@@ -76,11 +75,6 @@ data class BrowseSourceScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
if (!ifSourcesLoaded()) {
|
|
||||||
LoadingScreen()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val viewModel =
|
val viewModel =
|
||||||
assistedMetroViewModel<BrowseSourceViewModel, BrowseSourceViewModel.Factory> {
|
assistedMetroViewModel<BrowseSourceViewModel, BrowseSourceViewModel.Factory> {
|
||||||
create(sourceId = sourceId, listingQuery = listingQuery)
|
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(
|
MissingSourceScreen(
|
||||||
source = viewModel.source,
|
source = source,
|
||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -110,18 +110,18 @@ data class BrowseSourceScreen(
|
|||||||
|
|
||||||
val onHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) }
|
val onHelpClick = { uriHandler.openUri(LocalSource.HELP_URL) }
|
||||||
val onWebViewClick = f@{
|
val onWebViewClick = f@{
|
||||||
val source = viewModel.source as? HttpSource ?: return@f
|
val httpSource = source as? HttpSource ?: return@f
|
||||||
navigator.push(
|
navigator.push(
|
||||||
WebViewScreen(
|
WebViewScreen(
|
||||||
url = source.getHomeUrl(),
|
url = httpSource.getHomeUrl(),
|
||||||
initialTitle = source.name,
|
initialTitle = httpSource.name,
|
||||||
sourceId = source.id,
|
sourceId = httpSource.id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(viewModel.source) {
|
LaunchedEffect(source) {
|
||||||
assistUrl = (viewModel.source as? HttpSource)?.getHomeUrl()
|
assistUrl = (source as? HttpSource)?.getHomeUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -134,7 +134,7 @@ data class BrowseSourceScreen(
|
|||||||
BrowseSourceToolbar(
|
BrowseSourceToolbar(
|
||||||
searchQuery = state.toolbarQuery,
|
searchQuery = state.toolbarQuery,
|
||||||
onSearchQueryChange = viewModel::setToolbarQuery,
|
onSearchQueryChange = viewModel::setToolbarQuery,
|
||||||
source = viewModel.source,
|
source = source,
|
||||||
displayMode = viewModel.displayMode,
|
displayMode = viewModel.displayMode,
|
||||||
onDisplayModeChange = { viewModel.displayMode = it },
|
onDisplayModeChange = { viewModel.displayMode = it },
|
||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
@@ -168,7 +168,7 @@ data class BrowseSourceScreen(
|
|||||||
Text(text = stringResource(MR.strings.popular))
|
Text(text = stringResource(MR.strings.popular))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (viewModel.source.supportsLatest) {
|
if (source.supportsLatest) {
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = state.listing == Listing.Latest,
|
selected = state.listing == Listing.Latest,
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -213,7 +213,7 @@ data class BrowseSourceScreen(
|
|||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
BrowseSourceContent(
|
BrowseSourceContent(
|
||||||
source = viewModel.source,
|
source = source,
|
||||||
mangaList = viewModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
|
mangaList = viewModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
|
||||||
columns = viewModel.getColumnsPreference(LocalConfiguration.current.orientation),
|
columns = viewModel.getColumnsPreference(LocalConfiguration.current.orientation),
|
||||||
displayMode = viewModel.displayMode,
|
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.source.service.SourcePreferences
|
||||||
import eu.kanade.domain.track.interactor.AddTracks
|
import eu.kanade.domain.track.interactor.AddTracks
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.util.removeCovers
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -33,6 +34,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -60,7 +62,7 @@ import eu.kanade.tachiyomi.source.model.Filter as SourceModelFilter
|
|||||||
class BrowseSourceViewModel(
|
class BrowseSourceViewModel(
|
||||||
@Assisted private val sourceId: Long,
|
@Assisted private val sourceId: Long,
|
||||||
@Assisted listingQuery: String?,
|
@Assisted listingQuery: String?,
|
||||||
sourceManager: SourceManager,
|
private val sourceManager: SourceManager,
|
||||||
sourcePreferences: SourcePreferences,
|
sourcePreferences: SourcePreferences,
|
||||||
private val libraryPreferences: LibraryPreferences,
|
private val libraryPreferences: LibraryPreferences,
|
||||||
private val coverCache: CoverCache,
|
private val coverCache: CoverCache,
|
||||||
@@ -87,27 +89,32 @@ class BrowseSourceViewModel(
|
|||||||
|
|
||||||
var displayMode by sourcePreferences.sourceDisplayMode.asState(viewModelScope)
|
var displayMode by sourcePreferences.sourceDisplayMode.asState(viewModelScope)
|
||||||
|
|
||||||
val source = sourceManager.getOrStub(sourceId)
|
private val source: Source? get() = state.value.source
|
||||||
|
|
||||||
init {
|
init {
|
||||||
state.update {
|
viewModelScope.launchIO {
|
||||||
var query: String? = null
|
val source = sourceManager.getOrStub(sourceId)
|
||||||
var listing = it.listing
|
|
||||||
|
|
||||||
if (listing is Listing.Search) {
|
state.update {
|
||||||
query = listing.query
|
var query: String? = null
|
||||||
listing = Listing.Search(query, source.getFilterList())
|
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(
|
if (!getIncognitoState.await(source.id)) {
|
||||||
listing = listing,
|
sourcePreferences.lastUsedSource.set(source.id)
|
||||||
filters = source.getFilterList(),
|
}
|
||||||
toolbarQuery = query,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!getIncognitoState.await(source.id)) {
|
|
||||||
sourcePreferences.lastUsedSource.set(source.id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +122,9 @@ class BrowseSourceViewModel(
|
|||||||
* Flow of Pager flow tied to [State.listing]
|
* Flow of Pager flow tied to [State.listing]
|
||||||
*/
|
*/
|
||||||
private val hideInLibraryItems = sourcePreferences.hideInLibraryItems.get()
|
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()
|
.distinctUntilChanged()
|
||||||
.map { listing ->
|
.map { listing ->
|
||||||
Pager(PagingConfig(pageSize = 25)) {
|
Pager(PagingConfig(pageSize = 25)) {
|
||||||
@@ -143,6 +152,7 @@ class BrowseSourceViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun resetFilters() {
|
fun resetFilters() {
|
||||||
|
val source = source ?: return
|
||||||
state.update { it.copy(filters = source.getFilterList()) }
|
state.update { it.copy(filters = source.getFilterList()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +170,7 @@ class BrowseSourceViewModel(
|
|||||||
|
|
||||||
fun search(query: String? = null, filters: FilterList? = null) {
|
fun search(query: String? = null, filters: FilterList? = null) {
|
||||||
val input = state.value.listing as? Listing.Search
|
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 {
|
state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -174,7 +184,7 @@ class BrowseSourceViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun searchGenre(genreName: String) {
|
fun searchGenre(genreName: String) {
|
||||||
val defaultFilters = source.getFilterList()
|
val defaultFilters = source?.getFilterList() ?: return
|
||||||
var genreExists = false
|
var genreExists = false
|
||||||
|
|
||||||
filter@ for (sourceFilter in defaultFilters) {
|
filter@ for (sourceFilter in defaultFilters) {
|
||||||
@@ -235,7 +245,7 @@ class BrowseSourceViewModel(
|
|||||||
new = new.removeCovers(coverCache)
|
new = new.removeCovers(coverCache)
|
||||||
} else {
|
} else {
|
||||||
setMangaDefaultChapterFlags.await(manga)
|
setMangaDefaultChapterFlags.await(manga)
|
||||||
addTracks.bindEnhancedTrackers(manga, source)
|
addTracks.bindEnhancedTrackers(manga, sourceManager.getOrStub(manga.source))
|
||||||
}
|
}
|
||||||
|
|
||||||
updateManga.await(new.toMangaUpdate())
|
updateManga.await(new.toMangaUpdate())
|
||||||
@@ -351,6 +361,7 @@ class BrowseSourceViewModel(
|
|||||||
@Immutable
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val listing: Listing,
|
val listing: Listing,
|
||||||
|
val source: Source? = null,
|
||||||
val filters: FilterList = FilterList(),
|
val filters: FilterList = FilterList(),
|
||||||
val toolbarQuery: String? = null,
|
val toolbarQuery: String? = null,
|
||||||
val dialog: Dialog? = 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.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
|
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
|
||||||
import eu.kanade.core.util.ifSourcesLoaded
|
|
||||||
import eu.kanade.presentation.browse.GlobalSearchScreen
|
import eu.kanade.presentation.browse.GlobalSearchScreen
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
||||||
@@ -24,11 +23,6 @@ class GlobalSearchScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
if (!ifSourcesLoaded()) {
|
|
||||||
LoadingScreen()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
val viewModel =
|
val viewModel =
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@ class GlobalSearchViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getEnabledSources(): List<Source> {
|
override suspend fun getEnabledSources(): List<Source> {
|
||||||
return super.getEnabledSources()
|
return super.getEnabledSources()
|
||||||
.filter { state.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
|
.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.StateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
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()
|
return sourceManager.getAll()
|
||||||
.filter { it.lang in enabledLanguages && "${it.id}" !in disabledSources }
|
.filter { it.lang in enabledLanguages && "${it.id}" !in disabledSources }
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
@@ -107,7 +106,7 @@ abstract class SearchViewModel(
|
|||||||
return enabledSources
|
return enabledSources
|
||||||
}
|
}
|
||||||
|
|
||||||
return extensionManager.installedExtensionsFlow.first()
|
return extensionManager.getInstalledExtensions()
|
||||||
.filter { it.pkgName == filter }
|
.filter { it.pkgName == filter }
|
||||||
.flatMap { it.sources }
|
.flatMap { it.sources }
|
||||||
.filter { it in enabledSources }
|
.filter { it in enabledSources }
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ class MainActivity : BaseActivity() {
|
|||||||
setComposeContent {
|
setComposeContent {
|
||||||
val context = LocalContext.current
|
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 downloadOnly by preferences.downloadedOnly.collectAsState()
|
||||||
val indexing by downloadCache.isInitializing.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.Navigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import dev.zacsweers.metrox.viewmodel.assistedMetroViewModel
|
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.hasCustomCover
|
||||||
import eu.kanade.domain.manga.model.toSManga
|
import eu.kanade.domain.manga.model.toSManga
|
||||||
import eu.kanade.presentation.category.components.ChangeCategoryDialog
|
import eu.kanade.presentation.category.components.ChangeCategoryDialog
|
||||||
@@ -73,11 +72,6 @@ class MangaScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
if (!ifSourcesLoaded()) {
|
|
||||||
LoadingScreen()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
|||||||
@@ -798,7 +798,7 @@ class MangaViewModel(
|
|||||||
* Downloads the given list of chapters with the manager.
|
* Downloads the given list of chapters with the manager.
|
||||||
* @param chapters the list of chapters to download.
|
* @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
|
val manga = successState?.manga ?: return
|
||||||
downloadManager.downloadChapters(manga, chapters)
|
downloadManager.downloadChapters(manga, chapters)
|
||||||
toggleAllSelection(false)
|
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 loggedInTrackers = trackerManager.loggedInTrackers()
|
||||||
val source = sourceManager.getOrStub(sourceId)
|
val source = sourceManager.getOrStub(sourceId)
|
||||||
return loggedInTrackers
|
return loggedInTrackers
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
|||||||
import com.google.android.material.transition.platform.MaterialContainerTransform
|
import com.google.android.material.transition.platform.MaterialContainerTransform
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.core.util.ifSourcesLoaded
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.presentation.reader.DisplayRefreshHost
|
import eu.kanade.presentation.reader.DisplayRefreshHost
|
||||||
import eu.kanade.presentation.reader.OrientationSelectDialog
|
import eu.kanade.presentation.reader.OrientationSelectDialog
|
||||||
@@ -454,11 +453,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppBars(state: ReaderViewModel.State) {
|
fun AppBars(state: ReaderViewModel.State) {
|
||||||
if (!ifSourcesLoaded()) {
|
val isHttpSource = state.source is HttpSource
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val isHttpSource = viewModel.getSource() is HttpSource
|
|
||||||
|
|
||||||
val cropBorderPaged by readerPreferences.cropBorders.collectAsState()
|
val cropBorderPaged by readerPreferences.cropBorders.collectAsState()
|
||||||
val cropBorderWebtoon by readerPreferences.cropBordersWebtoon.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.Image
|
||||||
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
||||||
import eu.kanade.tachiyomi.data.saver.Location
|
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.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
||||||
@@ -155,6 +156,12 @@ class ReaderViewModel(
|
|||||||
val manga: Manga?
|
val manga: Manga?
|
||||||
get() = state.value.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.
|
* The chapter id of the currently loaded chapter. Used to restore from process kill.
|
||||||
*/
|
*/
|
||||||
@@ -265,7 +272,7 @@ class ReaderViewModel(
|
|||||||
.map(::ReaderChapter)
|
.map(::ReaderChapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val incognitoMode: Boolean by lazy { getIncognitoState.await(manga?.source) }
|
private var incognitoMode: Boolean = false
|
||||||
private val downloadAheadAmount = downloadPreferences.autoDownloadWhileReading.get()
|
private val downloadAheadAmount = downloadPreferences.autoDownloadWhileReading.get()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -316,11 +323,11 @@ class ReaderViewModel(
|
|||||||
withIOContext {
|
withIOContext {
|
||||||
try {
|
try {
|
||||||
val manga = getManga.await(mangaId) ?: error("Requested manga of id $mangaId not found")
|
val manga = getManga.await(mangaId) ?: error("Requested manga of id $mangaId not found")
|
||||||
sourceManager.isInitialized.first { it }
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
mutableState.update { it.copy(manga = manga) }
|
incognitoMode = getIncognitoState.await(manga.source)
|
||||||
|
mutableState.update { it.copy(manga = manga, source = source) }
|
||||||
if (chapterId == -1L) chapterId = initialChapterId
|
if (chapterId == -1L) chapterId = initialChapterId
|
||||||
|
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
|
||||||
loader = ChapterLoader(context, downloadManager, downloadProvider, chapterCache, manga, source)
|
loader = ChapterLoader(context, downloadManager, downloadProvider, chapterCache, manga, source)
|
||||||
|
|
||||||
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
|
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
|
||||||
@@ -425,13 +432,13 @@ class ReaderViewModel(
|
|||||||
if (chapter.pageLoader?.isLocal == false) {
|
if (chapter.pageLoader?.isLocal == false) {
|
||||||
val manga = manga ?: return
|
val manga = manga ?: return
|
||||||
val dbChapter = chapter.chapter
|
val dbChapter = chapter.chapter
|
||||||
val isDownloaded = downloadManager.isChapterDownloaded(
|
val source = state.value.source ?: return
|
||||||
|
val isDownloaded = downloadManager.isChapterDownloadedOnDisk(
|
||||||
dbChapter.name,
|
dbChapter.name,
|
||||||
dbChapter.scanlator,
|
dbChapter.scanlator,
|
||||||
dbChapter.url,
|
dbChapter.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
source,
|
||||||
skipCache = true,
|
|
||||||
)
|
)
|
||||||
if (isDownloaded) {
|
if (isDownloaded) {
|
||||||
chapter.state = ReaderChapter.State.Wait
|
chapter.state = ReaderChapter.State.Wait
|
||||||
@@ -654,7 +661,7 @@ class ReaderViewModel(
|
|||||||
return state.value.currentChapter
|
return state.value.currentChapter
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSource() = manga?.source?.let { sourceManager.getOrStub(it) } as? HttpSource
|
fun getSource() = state.value.source as? HttpSource
|
||||||
|
|
||||||
fun getChapterUrl(): String? {
|
fun getChapterUrl(): String? {
|
||||||
val sChapter = getCurrentChapter()?.chapter ?: return null
|
val sChapter = getCurrentChapter()?.chapter ?: return null
|
||||||
@@ -974,6 +981,7 @@ class ReaderViewModel(
|
|||||||
@Immutable
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val manga: Manga? = null,
|
val manga: Manga? = null,
|
||||||
|
val source: Source? = null,
|
||||||
val initError: Throwable? = null,
|
val initError: Throwable? = null,
|
||||||
val viewerChapters: ViewerChapters? = null,
|
val viewerChapters: ViewerChapters? = null,
|
||||||
val bookmarked: Boolean = false,
|
val bookmarked: Boolean = false,
|
||||||
|
|||||||
@@ -79,13 +79,12 @@ class ChapterLoader(
|
|||||||
*/
|
*/
|
||||||
private fun getPageLoader(chapter: ReaderChapter): PageLoader {
|
private fun getPageLoader(chapter: ReaderChapter): PageLoader {
|
||||||
val dbChapter = chapter.chapter
|
val dbChapter = chapter.chapter
|
||||||
val isDownloaded = downloadManager.isChapterDownloaded(
|
val isDownloaded = downloadManager.isChapterDownloadedOnDisk(
|
||||||
dbChapter.name,
|
dbChapter.name,
|
||||||
dbChapter.scanlator,
|
dbChapter.scanlator,
|
||||||
dbChapter.url,
|
dbChapter.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
source,
|
||||||
skipCache = true,
|
|
||||||
)
|
)
|
||||||
return when {
|
return when {
|
||||||
isDownloaded -> DownloadPageLoader(
|
isDownloaded -> DownloadPageLoader(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.platform.AbstractComposeView
|
|||||||
import eu.kanade.presentation.reader.ChapterTransition
|
import eu.kanade.presentation.reader.ChapterTransition
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.source.local.isLocal
|
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)
|
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?) {
|
fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?, source: Source?) {
|
||||||
data = if (manga != null) {
|
data = if (manga != null && source != null) {
|
||||||
Data(
|
Data(
|
||||||
transition = transition,
|
transition = transition,
|
||||||
currChapterDownloaded = transition.from.pageLoader?.isLocal == true,
|
currChapterDownloaded = transition.from.pageLoader?.isLocal == true,
|
||||||
goingToChapterDownloaded = manga.isLocal() ||
|
goingToChapterDownloaded = manga.isLocal() ||
|
||||||
transition.to?.chapter?.let { goingToChapter ->
|
transition.to?.chapter?.let { goingToChapter ->
|
||||||
downloadManager.isChapterDownloaded(
|
downloadManager.isChapterDownloadedOnDisk(
|
||||||
chapterName = goingToChapter.name,
|
chapterName = goingToChapter.name,
|
||||||
chapterScanlator = goingToChapter.scanlator,
|
chapterScanlator = goingToChapter.scanlator,
|
||||||
chapterUrl = goingToChapter.url,
|
chapterUrl = goingToChapter.url,
|
||||||
mangaTitle = manga.title,
|
mangaTitle = manga.title,
|
||||||
sourceId = manga.source,
|
source = source,
|
||||||
skipCache = true,
|
|
||||||
)
|
)
|
||||||
} ?: false,
|
} ?: false,
|
||||||
)
|
)
|
||||||
|
|||||||
+6
-1
@@ -62,7 +62,12 @@ class PagerTransitionHolder(
|
|||||||
addView(transitionView)
|
addView(transitionView)
|
||||||
addView(pagesContainer)
|
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)
|
transition.to?.let(::observeStatus)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -64,7 +64,12 @@ class WebtoonTransitionHolder(
|
|||||||
* Binds the given [transition] with this view holder, subscribing to its state.
|
* Binds the given [transition] with this view holder, subscribing to its state.
|
||||||
*/
|
*/
|
||||||
fun bind(transition: ChapterTransition) {
|
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) }
|
transition.to?.let { observeStatus(it, transition) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.presentation.webview.WebViewScreenContent
|
import eu.kanade.presentation.webview.WebViewScreenContent
|
||||||
@@ -24,6 +26,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
|||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
class WebViewActivity : BaseActivity() {
|
class WebViewActivity : BaseActivity() {
|
||||||
|
|
||||||
@@ -60,21 +63,28 @@ class WebViewActivity : BaseActivity() {
|
|||||||
val url = intent.extras?.getString(URL_KEY) ?: return
|
val url = intent.extras?.getString(URL_KEY) ?: return
|
||||||
assistUrl = url
|
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 {
|
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(
|
WebViewScreenContent(
|
||||||
onNavigateUp = { finish() },
|
onNavigateUp = { finish() },
|
||||||
initialTitle = intent.extras?.getString(TITLE_KEY),
|
initialTitle = intent.extras?.getString(TITLE_KEY),
|
||||||
url = url,
|
url = url,
|
||||||
headers = headers,
|
headers = headers.orEmpty(),
|
||||||
defaultUserAgentProvider = network::defaultUserAgentProvider,
|
defaultUserAgentProvider = network::defaultUserAgentProvider,
|
||||||
onUrlChange = { assistUrl = it },
|
onUrlChange = { assistUrl = it },
|
||||||
onShare = this::shareWebpage,
|
onShare = this::shareWebpage,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.ui.webview
|
package eu.kanade.tachiyomi.ui.webview
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
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.AssistContentScreen
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import eu.kanade.presentation.webview.WebViewScreenContent
|
import eu.kanade.presentation.webview.WebViewScreenContent
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
class WebViewScreen(
|
class WebViewScreen(
|
||||||
private val url: String,
|
private val url: String,
|
||||||
@@ -26,11 +29,17 @@ class WebViewScreen(
|
|||||||
val viewModel =
|
val viewModel =
|
||||||
assistedMetroViewModel<WebViewViewModel, WebViewViewModel.Factory> { create(sourceId = sourceId) }
|
assistedMetroViewModel<WebViewViewModel, WebViewViewModel.Factory> { create(sourceId = sourceId) }
|
||||||
|
|
||||||
|
val headers by viewModel.headers.collectAsState()
|
||||||
|
if (headers == null) {
|
||||||
|
LoadingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
WebViewScreenContent(
|
WebViewScreenContent(
|
||||||
onNavigateUp = { navigator.pop() },
|
onNavigateUp = { navigator.pop() },
|
||||||
initialTitle = initialTitle,
|
initialTitle = initialTitle,
|
||||||
url = url,
|
url = url,
|
||||||
headers = viewModel.headers,
|
headers = headers.orEmpty(),
|
||||||
defaultUserAgentProvider = viewModel::defaultUserAgentProvider,
|
defaultUserAgentProvider = viewModel::defaultUserAgentProvider,
|
||||||
onUrlChange = { assistUrl = it },
|
onUrlChange = { assistUrl = it },
|
||||||
onShare = { viewModel.shareWebpage(context, it) },
|
onShare = { viewModel.shareWebpage(context, it) },
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.webview
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
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.openInBrowser
|
||||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
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 logcat.LogPriority
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
@@ -34,14 +38,18 @@ class WebViewViewModel(
|
|||||||
fun create(sourceId: Long?): 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 {
|
init {
|
||||||
sourceId?.let { sourceManager.get(it) as? HttpSource }?.let { source ->
|
viewModelScope.launch {
|
||||||
try {
|
val source = sourceId?.let { sourceManager.get(it) as? HttpSource }
|
||||||
headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
headers.value = try {
|
||||||
|
source?.headers?.toMultimap()?.mapValues { it.value.getOrNull(0) ?: "" }.orEmpty()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to build headers" }
|
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.createFileInCacheDir
|
||||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.offsetAt
|
import kotlinx.datetime.offsetAt
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
@@ -66,7 +65,7 @@ class CrashLogUtil(
|
|||||||
private suspend fun getExtensionsInfo(): String? {
|
private suspend fun getExtensionsInfo(): String? {
|
||||||
val availableExtensions = extensionManager.availableExtensionsFlow.value.associateBy { it.pkgName }
|
val availableExtensions = extensionManager.availableExtensionsFlow.value.associateBy { it.pkgName }
|
||||||
|
|
||||||
val extensionInfoList = extensionManager.installedExtensionsFlow.first()
|
val extensionInfoList = extensionManager.getInstalledExtensions()
|
||||||
.sortedBy { it.name }
|
.sortedBy { it.name }
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
val availableExtension = availableExtensions[it.pkgName]
|
val availableExtension = availableExtensions[it.pkgName]
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ import tachiyomi.source.local.isLocal
|
|||||||
fun List<Chapter>.filterDownloaded(manga: Manga, downloadCache: DownloadCache): List<Chapter> {
|
fun List<Chapter>.filterDownloaded(manga: Manga, downloadCache: DownloadCache): List<Chapter> {
|
||||||
if (manga.isLocal()) return this
|
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()
|
saveSources()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initSources() {
|
private suspend fun initSources() {
|
||||||
val languages = sourcePreferences.enabledLanguages.get()
|
val languages = sourcePreferences.enabledLanguages.get()
|
||||||
val pinnedSources = sourcePreferences.pinnedSources.get().mapNotNull { it.toLongOrNull() }
|
val pinnedSources = sourcePreferences.pinnedSources.get().mapNotNull { it.toLongOrNull() }
|
||||||
val includedSources = sourcePreferences.migrationSources.get()
|
val includedSources = sourcePreferences.migrationSources.get()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.paging.PagingState
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import mihon.domain.manga.model.toDomainManga
|
import mihon.domain.manga.model.toDomainManga
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
@@ -11,49 +12,50 @@ import tachiyomi.domain.manga.model.Manga
|
|||||||
import tachiyomi.domain.source.repository.SourcePagingSource
|
import tachiyomi.domain.source.repository.SourcePagingSource
|
||||||
|
|
||||||
class SourceSearchPagingSource(
|
class SourceSearchPagingSource(
|
||||||
source: Source,
|
source: suspend () -> Source,
|
||||||
private val query: String,
|
private val query: String,
|
||||||
private val filters: FilterList,
|
private val filters: FilterList,
|
||||||
networkToLocalManga: NetworkToLocalManga,
|
networkToLocalManga: NetworkToLocalManga,
|
||||||
) : BaseSourcePagingSource(source, 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)
|
return source.getSearchManga(currentPage, query, filters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SourcePopularPagingSource(
|
class SourcePopularPagingSource(
|
||||||
source: Source,
|
source: suspend () -> Source,
|
||||||
networkToLocalManga: NetworkToLocalManga,
|
networkToLocalManga: NetworkToLocalManga,
|
||||||
) : BaseSourcePagingSource(source, networkToLocalManga) {
|
) : BaseSourcePagingSource(source, networkToLocalManga) {
|
||||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
override suspend fun requestNextPage(source: Source, currentPage: Int): MangasPage {
|
||||||
return source.getPopularManga(currentPage)
|
return source.getPopularManga(currentPage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SourceLatestPagingSource(
|
class SourceLatestPagingSource(
|
||||||
source: Source,
|
source: suspend () -> Source,
|
||||||
networkToLocalManga: NetworkToLocalManga,
|
networkToLocalManga: NetworkToLocalManga,
|
||||||
) : BaseSourcePagingSource(source, networkToLocalManga) {
|
) : BaseSourcePagingSource(source, networkToLocalManga) {
|
||||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
override suspend fun requestNextPage(source: Source, currentPage: Int): MangasPage {
|
||||||
return source.getLatestUpdates(currentPage)
|
return source.getLatestUpdates(currentPage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class BaseSourcePagingSource(
|
abstract class BaseSourcePagingSource(
|
||||||
protected val source: Source,
|
private val source: suspend () -> Source,
|
||||||
private val networkToLocalManga: NetworkToLocalManga,
|
private val networkToLocalManga: NetworkToLocalManga,
|
||||||
) : SourcePagingSource() {
|
) : SourcePagingSource() {
|
||||||
|
|
||||||
private val seenManga = hashSetOf<String>()
|
private val seenManga = hashSetOf<String>()
|
||||||
|
|
||||||
abstract suspend fun requestNextPage(currentPage: Int): MangasPage
|
abstract suspend fun requestNextPage(source: Source, currentPage: Int): MangasPage
|
||||||
|
|
||||||
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, Manga> {
|
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, Manga> {
|
||||||
val page = params.key ?: 1
|
val page = params.key ?: 1
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
val source = source()
|
||||||
val mangasPage = withIOContext {
|
val mangasPage = withIOContext {
|
||||||
requestNextPage(page.toInt())
|
requestNextPage(source, page.toInt())
|
||||||
.takeIf { it.mangas.isNotEmpty() }
|
.takeIf { it.mangas.isNotEmpty() }
|
||||||
?: throw NoResultsException()
|
?: throw NoResultsException()
|
||||||
}
|
}
|
||||||
@@ -68,6 +70,8 @@ abstract class BaseSourcePagingSource(
|
|||||||
prevKey = null,
|
prevKey = null,
|
||||||
nextKey = if (mangasPage.hasNextPage) page + 1 else null,
|
nextKey = if (mangasPage.hasNextPage) page + 1 else null,
|
||||||
)
|
)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
LoadResult.Error(e)
|
LoadResult.Error(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,15 +85,20 @@ class SourceRepositoryImpl(
|
|||||||
query: String,
|
query: String,
|
||||||
filterList: FilterList,
|
filterList: FilterList,
|
||||||
): SourcePagingSource {
|
): SourcePagingSource {
|
||||||
return SourceSearchPagingSource(sourceManager.getOrStub(sourceId), query, filterList, networkToLocalManga)
|
return SourceSearchPagingSource(
|
||||||
|
{ sourceManager.getOrStub(sourceId) },
|
||||||
|
query,
|
||||||
|
filterList,
|
||||||
|
networkToLocalManga,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPopular(sourceId: Long): SourcePagingSource {
|
override fun getPopular(sourceId: Long): SourcePagingSource {
|
||||||
return SourcePopularPagingSource(sourceManager.getOrStub(sourceId), networkToLocalManga)
|
return SourcePopularPagingSource({ sourceManager.getOrStub(sourceId) }, networkToLocalManga)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLatest(sourceId: Long): SourcePagingSource {
|
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(
|
private fun mapSourceToDomainSource(source: Source): DomainSource = DomainSource(
|
||||||
|
|||||||
@@ -3,22 +3,19 @@ package tachiyomi.domain.source.service
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import tachiyomi.domain.source.model.StubSource
|
import tachiyomi.domain.source.model.StubSource
|
||||||
|
|
||||||
interface SourceManager {
|
interface SourceManager {
|
||||||
|
|
||||||
val isInitialized: StateFlow<Boolean>
|
|
||||||
|
|
||||||
val sources: Flow<List<Source>>
|
val sources: Flow<List<Source>>
|
||||||
|
|
||||||
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<Source>
|
suspend fun getAll(): List<Source>
|
||||||
|
|
||||||
fun getOnlineSources(): List<HttpSource>
|
suspend fun getOnlineSources(): List<HttpSource>
|
||||||
|
|
||||||
fun getStubSources(): List<StubSource>
|
suspend fun getStubSources(): List<StubSource>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user