diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b19a10d94b58..54d63bd37dfd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -158,10 +158,12 @@ dependencies { implementation("androidx.browser:browser:1.5.0") implementation("androidx.biometric:biometric:1.1.0") implementation("androidx.palette:palette:1.0.0") - implementation("androidx.core:core-ktx:1.9.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") + implementation("androidx.activity:activity-ktx:1.7.0-rc01") + implementation("androidx.core:core-ktx:1.10.0-rc01") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.0") implementation("com.google.android.flexbox:flexbox:3.0.0") implementation("androidx.window:window:1.0.0") + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") @@ -181,7 +183,6 @@ dependencies { implementation("io.reactivex:rxandroid:1.2.1") implementation("io.reactivex:rxjava:1.3.8") implementation("com.jakewharton.rxrelay:rxrelay:1.2.0") - implementation("com.github.pwittchen:reactivenetwork:0.13.0") // Coroutines implementation("com.fredporciuncula:flow-preferences:1.6.0") @@ -224,9 +225,6 @@ dependencies { implementation("com.google.android.gms:play-services-gcm:17.0.0") - // Changelog - implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0") - // Database implementation("androidx.sqlite:sqlite-ktx:2.3.0") implementation("com.github.requery:sqlite-android:3.39.2") @@ -261,7 +259,6 @@ dependencies { implementation("com.mikepenz:fastadapter-extensions-binding:$fastAdapterVersion") implementation("com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533") implementation("com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533") - implementation("com.nononsenseapps:filepicker:2.5.2") implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0") implementation("com.github.mthli:Slice:v1.2") implementation("io.noties.markwon:core:4.6.2") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 30bebe8036d4..232663c397b2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -127,10 +127,6 @@ android:configChanges="uiMode|orientation|screenSize"/> - ().init(it) } } - + addSingletonFactory { SourceManager(app, get()) } addSingletonFactory { ExtensionManager(app) } addSingletonFactory { DownloadManager(app) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index 07a9d14022f3..9d51e5a3cc67 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -109,7 +109,7 @@ class LibraryUpdateNotifier(private val context: Context) { setContentIntent(pendingIntent) setSmallIcon(R.drawable.ic_tachij2k_notification) addAction( - R.drawable.nnf_ic_file_folder, + R.drawable.ic_file_open_24dp, context.getString(R.string.open_log), pendingIntent, ) @@ -144,7 +144,7 @@ class LibraryUpdateNotifier(private val context: Context) { setContentIntent(NotificationHandler.openUrl(context, HELP_SKIPPED_URL)) setSmallIcon(R.drawable.ic_tachij2k_notification) addAction( - R.drawable.nnf_ic_file_folder, + R.drawable.ic_file_open_24dp, context.getString(R.string.open_log), NotificationReceiver.openErrorOrSkippedLogPendingActivity(context, uri), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt index ccdbbc955712..00f0a4561f58 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt @@ -81,7 +81,7 @@ class ExtensionInstallService( instance = this val list = intent.getParcelableArrayListExtra(KEY_EXTENSION)?.filter { - val installedExt = extensionManager.installedExtensions.find { installed -> + val installedExt = extensionManager.installedExtensionsFlow.value.find { installed -> installed.pkgName == it.pkgName } ?: return@filter false installedExt.versionCode < it.versionCode || installedExt.libVersion < it.libVersion diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index bfb9c362a258..765d5d41d7da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -5,7 +5,6 @@ import android.graphics.drawable.Drawable import android.os.Build import android.os.Parcelable import androidx.preference.PreferenceManager -import com.jakewharton.rxrelay.BehaviorRelay import eu.kanade.tachiyomi.data.preference.PreferenceKeys import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi @@ -20,14 +19,15 @@ import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo import eu.kanade.tachiyomi.util.system.launchNow import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.parcelize.Parcelize -import rx.Observable +import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.Locale /** * The manager of extensions installed as another apk which extend the available sources. It handles @@ -54,6 +54,8 @@ class ExtensionManager( */ private val installer by lazy { ExtensionInstaller(context) } + private val iconMap = mutableMapOf() + val downloadRelay get() = installer.downloadsStateFlow @@ -66,27 +68,28 @@ class ExtensionManager( /** * Relay used to notify the installed extensions. */ - private val installedExtensionsRelay = BehaviorRelay.create>() + private val _installedExtensionsFlow = MutableStateFlow(emptyList()) + val installedExtensionsFlow = _installedExtensionsFlow.asStateFlow() - private val iconMap = mutableMapOf() + private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet() /** * List of the currently installed extensions. */ - var installedExtensions = emptyList() - private set(value) { - field = value - installedExtensionsRelay.call(value) - downloadRelay.tryEmit("Finished/Installed/${value.size}" to (InstallStep.Done to null)) - } +// private var installedExtensions = emptyList() +// set(value) { +// field = value +// installedExtensionsRelay.call(value) +// downloadRelay.tryEmit("Finished/Installed/${value.size}" to (InstallStep.Done to null)) +// } fun getAppIconForSource(source: Source): Drawable? { return getAppIconForSource(source.id) } private fun getAppIconForSource(sourceId: Long): Drawable? { - val pkgName = - installedExtensions.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName + val pkgName = _installedExtensionsFlow.value + .find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName return if (pkgName != null) { try { return iconMap[pkgName] @@ -102,47 +105,42 @@ class ExtensionManager( /** * Relay used to notify the available extensions. */ - private val availableExtensionsRelay = BehaviorRelay.create>() - - /** - * List of the currently available extensions. - */ - var availableExtensions = emptyList() - private set(value) { - field = value - availableExtensionsRelay.call(value) - updatedInstalledExtensionsStatuses(value) - downloadRelay.tryEmit("Finished/Available/${value.size}" to (InstallStep.Done to null)) - setupAvailableSourcesMap() - } + private val _availableExtensionsFlow = MutableStateFlow(emptyList()) + val availableExtensionsFlow = _availableExtensionsFlow.asStateFlow() private var availableSources = hashMapOf() /** - * Relay used to notify the untrusted extensions. + * List of the currently available extensions. */ - private val untrustedExtensionsRelay = BehaviorRelay.create>() +// var availableExtensions = emptyList() +// private set(value) { +// field = value +// availableExtensionsRelay.call(value) +// updatedInstalledExtensionsStatuses(value) +// downloadRelay.tryEmit("Finished/Available/${value.size}" to (InstallStep.Done to null)) +// setupAvailableSourcesMap() +// } + + private val _untrustedExtensionsFlow = MutableStateFlow(emptyList()) + val untrustedExtensionsFlow = _untrustedExtensionsFlow.asStateFlow() /** * List of the currently untrusted extensions. */ - var untrustedExtensions = emptyList() - private set(value) { - field = value - untrustedExtensionsRelay.call(value) - downloadRelay.tryEmit("Finished/Untrusted/${value.size}" to (InstallStep.Done to null)) - } +// var untrustedExtensions = emptyList() +// private set(value) { +// field = value +// untrustedExtensionsRelay.call(value) +// downloadRelay.tryEmit("Finished/Untrusted/${value.size}" to (InstallStep.Done to null)) +// } /** * The source manager where the sources of the extensions are added. */ private lateinit var sourceManager: SourceManager - /** - * Initializes this manager with the given source manager. - */ - fun init(sourceManager: SourceManager) { - this.sourceManager = sourceManager + init { initExtensions() ExtensionInstallReceiver(InstallationListener()).register(context) } @@ -153,79 +151,77 @@ class ExtensionManager( private fun initExtensions() { val extensions = ExtensionLoader.loadExtensions(context) - installedExtensions = extensions + _installedExtensionsFlow.value = extensions .filterIsInstance() .map { it.extension } - installedExtensions - .flatMap { it.sources } - // overwrite is needed until the bundled sources are removed - .forEach { sourceManager.registerSource(it, true) } - untrustedExtensions = extensions + _untrustedExtensionsFlow.value = extensions .filterIsInstance() .map { it.extension } } - /** - * Returns the relay of the installed extensions as an observable. - */ - fun getInstalledExtensionsObservable(): Observable> { - return installedExtensionsRelay.asObservable() + fun isInstalledByApp(extension: Extension.Available): Boolean { + return ExtensionLoader.isExtensionInstalledByApp(context, extension.pkgName) } /** - * Returns the relay of the available extensions as an observable. + * Finds the available extensions in the [api] and updates [availableExtensionsFlow]. */ - fun getAvailableExtensionsObservable(): Observable> { - return availableExtensionsRelay.asObservable() - } + suspend fun findAvailableExtensions() { + val extensions: List = try { + api.findExtensions() + } catch (e: Exception) { + Timber.e(e) + emptyList() + } - /** - * Returns the relay of the untrusted extensions as an observable. - */ - fun getUntrustedExtensionsObservable(): Observable> { - return untrustedExtensionsRelay.asObservable() - } + enableAdditionalSubLanguages(extensions) - fun isInstalledByApp(extension: Extension.Available): Boolean { - return ExtensionLoader.isExtensionInstalledByApp(context, extension.pkgName) + _availableExtensionsFlow.value = extensions + updatedInstalledExtensionsStatuses(extensions) + setupAvailableSourcesMap() + downloadRelay.tryEmit("Finished/Available/${extensions.size}" to (InstallStep.Done to null)) } /** - * Finds the available extensions in the [api] and updates [availableExtensions]. + * Enables the additional sub-languages in the app first run. This addresses + * the issue where users still need to enable some specific languages even when + * the device language is inside that major group. As an example, if a user + * has a zh device language, the app will also enable zh-Hans and zh-Hant. + * + * If the user have already changed the enabledLanguages preference value once, + * the new languages will not be added to respect the user enabled choices. */ - fun findAvailableExtensions() { - launchNow { - availableExtensions = try { - api.findExtensions() - } catch (e: Exception) { - emptyList() - } + private fun enableAdditionalSubLanguages(extensions: List) { + if (subLanguagesEnabledOnFirstRun || extensions.isEmpty()) { + return + } + + // Use the source lang as some aren't present on the extension level. + val availableLanguages = extensions + .flatMap(Extension.Available::sources) + .distinctBy(Extension.AvailableSource::lang) + .map(Extension.AvailableSource::lang) + + val deviceLanguage = Locale.getDefault().language + val defaultLanguages = preferences.enabledLanguages().defaultValue + val languagesToEnable = availableLanguages.filter { + it != deviceLanguage && it.startsWith(deviceLanguage) } + + preferences.enabledLanguages().set(defaultLanguages + languagesToEnable) + subLanguagesEnabledOnFirstRun = true } private fun setupAvailableSourcesMap() { availableSources = hashMapOf() - availableExtensions.map { it.sources.orEmpty() }.flatten().forEach { + _availableExtensionsFlow.value.map { it.sources }.flatten().forEach { availableSources[it.id] = it } } fun getStubSource(id: Long) = availableSources[id] - /** - * Finds the available extensions in the [api] and updates [availableExtensions]. - */ - suspend fun findAvailableExtensionsAsync() { - withContext(Dispatchers.IO) { - availableExtensions = try { - api.findExtensions() - } catch (e: Exception) { - emptyList() - } - } - } - /** * Sets the update field of the installed extensions with the given [availableExtensions]. * @@ -236,7 +232,7 @@ class ExtensionManager( preferences.extensionUpdatesCount().set(0) return } - val mutInstalledExtensions = installedExtensions.toMutableList() + val mutInstalledExtensions = installedExtensionsFlow.value.toMutableList() var changed = false var hasUpdateCount = 0 for ((index, installedExt) in mutInstalledExtensions.withIndex()) { @@ -257,9 +253,9 @@ class ExtensionManager( } } if (changed) { - installedExtensions = mutInstalledExtensions + _installedExtensionsFlow.value = mutInstalledExtensions } - preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate }) + preferences.extensionUpdatesCount().set(installedExtensionsFlow.value.count { it.hasUpdate }) } /** @@ -358,15 +354,15 @@ class ExtensionManager( * @param signature The signature to whitelist. */ fun trustSignature(signature: String) { - val untrustedSignatures = untrustedExtensions.map { it.signatureHash }.toSet() + val untrustedSignatures = untrustedExtensionsFlow.value.map { it.signatureHash }.toSet() if (signature !in untrustedSignatures) return ExtensionLoader.trustedSignatures += signature val preference = preferences.trustedSignatures() preference.set(preference.get() + signature) - val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature } - untrustedExtensions -= nowTrustedExtensions + val nowTrustedExtensions = untrustedExtensionsFlow.value.filter { it.signatureHash == signature } + _untrustedExtensionsFlow.value -= nowTrustedExtensions val ctx = context launchNow { @@ -389,9 +385,8 @@ class ExtensionManager( * @param extension The extension to be registered. */ private fun registerNewExtension(extension: Extension.Installed) { - installedExtensions = installedExtensions + extension + _installedExtensionsFlow.value += extension downloadRelay.tryEmit("Finished/${extension.pkgName}" to ExtensionIntallInfo(InstallStep.Installed, null)) - extension.sources.forEach { sourceManager.registerSource(it) } } /** @@ -401,16 +396,14 @@ class ExtensionManager( * @param extension The extension to be registered. */ private fun registerUpdatedExtension(extension: Extension.Installed) { - val mutInstalledExtensions = installedExtensions.toMutableList() + val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList() val oldExtension = mutInstalledExtensions.find { it.pkgName == extension.pkgName } if (oldExtension != null) { mutInstalledExtensions -= oldExtension - extension.sources.forEach { sourceManager.unregisterSource(it) } } mutInstalledExtensions += extension - installedExtensions = mutInstalledExtensions + _installedExtensionsFlow.value = mutInstalledExtensions downloadRelay.tryEmit("Finished/${extension.pkgName}" to ExtensionIntallInfo(InstallStep.Installed, null)) - extension.sources.forEach { sourceManager.registerSource(it) } } /** @@ -420,14 +413,13 @@ class ExtensionManager( * @param pkgName The package name of the uninstalled application. */ private fun unregisterExtension(pkgName: String) { - val installedExtension = installedExtensions.find { it.pkgName == pkgName } + val installedExtension = installedExtensionsFlow.value.find { it.pkgName == pkgName } if (installedExtension != null) { - installedExtensions = installedExtensions - installedExtension - installedExtension.sources.forEach { sourceManager.unregisterSource(it) } + _installedExtensionsFlow.value -= installedExtension } - val untrustedExtension = untrustedExtensions.find { it.pkgName == pkgName } + val untrustedExtension = untrustedExtensionsFlow.value.find { it.pkgName == pkgName } if (untrustedExtension != null) { - untrustedExtensions = untrustedExtensions - untrustedExtension + _untrustedExtensionsFlow.value -= untrustedExtension } } @@ -438,21 +430,21 @@ class ExtensionManager( override fun onExtensionInstalled(extension: Extension.Installed) { registerNewExtension(extension.withUpdateCheck()) - preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate }) + preferences.extensionUpdatesCount().set(installedExtensionsFlow.value.count { it.hasUpdate }) } override fun onExtensionUpdated(extension: Extension.Installed) { registerUpdatedExtension(extension.withUpdateCheck()) - preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate }) + preferences.extensionUpdatesCount().set(installedExtensionsFlow.value.count { it.hasUpdate }) } override fun onExtensionUntrusted(extension: Extension.Untrusted) { - untrustedExtensions += extension + _untrustedExtensionsFlow.value += extension } override fun onPackageUninstalled(pkgName: String) { unregisterExtension(pkgName) - preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate }) + preferences.extensionUpdatesCount().set(installedExtensionsFlow.value.count { it.hasUpdate }) } } @@ -464,7 +456,7 @@ class ExtensionManager( } private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean { - val availableExt = availableExtension ?: availableExtensionsRelay.value.find { it.pkgName == pkgName } + val availableExt = availableExtension ?: availableExtensionsFlow.value.find { it.pkgName == pkgName } if (isUnofficial || availableExt == null) return false return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt index 3f0f040c199f..206b1626ef55 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt @@ -97,7 +97,7 @@ internal class ExtensionGithubApi { isNsfw = it.nsfw == 1, hasReadme = it.hasReadme == 1, hasChangelog = it.hasChangelog == 1, - sources = it.sources, + sources = it.sources ?: emptyList(), apkName = it.apk, iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}", ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt index 246d6c65663c..45d40e450955 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -46,7 +46,7 @@ sealed class Extension { override val hasChangelog: Boolean, val apkName: String, val iconUrl: String, - val sources: List? = null, + val sources: List, ) : Extension() @Serializable diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index 366e7e3f1a24..045ea680e6ac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -12,16 +12,31 @@ import eu.kanade.tachiyomi.source.online.all.Cubari import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.english.KireiCake import eu.kanade.tachiyomi.source.online.english.MangaPlus +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import rx.Observable -import uy.kohesive.injekt.injectLazy +import java.util.concurrent.ConcurrentHashMap -open class SourceManager(private val context: Context) { +class SourceManager( + private val context: Context, + private val extensionManager: ExtensionManager, +) { - private val sourcesMap = mutableMapOf() + private val scope = CoroutineScope(Job() + Dispatchers.IO) - private val stubSourcesMap = mutableMapOf() + private val sourcesMapFlow = MutableStateFlow(ConcurrentHashMap()) - protected val extensionManager: ExtensionManager by injectLazy() + private val stubSourcesMap = ConcurrentHashMap() + + val catalogueSources: Flow> = sourcesMapFlow.map { it.values.filterIsInstance() } + val onlineSources: Flow> = catalogueSources.map { it.filterIsInstance() } private val delegatedSources = listOf( DelegatedSource( @@ -47,16 +62,39 @@ open class SourceManager(private val context: Context) { ).associateBy { it.sourceId } init { - createInternalSources().forEach { registerSource(it) } + scope.launch { + extensionManager.installedExtensionsFlow + .collectLatest { extensions -> + val mutableMap = ConcurrentHashMap(mapOf(LocalSource.ID to LocalSource(context))) + extensions.forEach { extension -> + extension.sources.forEach { + mutableMap[it.id] = it + delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource +// registerStubSource(it) + } + } + sourcesMapFlow.value = mutableMap + } + } + +// scope.launch { +// sourceRepository.subscribeAll() +// .collectLatest { sources -> +// val mutableMap = stubSourcesMap.toMutableMap() +// sources.forEach { +// mutableMap[it.id] = StubSource(it) +// } +// } +// } } - open fun get(sourceKey: Long): Source? { - return sourcesMap[sourceKey] + fun get(sourceKey: Long): Source? { + return sourcesMapFlow.value[sourceKey] } fun getOrStub(sourceKey: Long): Source { - return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) { - StubSource(sourceKey) + return sourcesMapFlow.value[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) { + runBlocking { StubSource(sourceKey) } } } @@ -68,24 +106,9 @@ open class SourceManager(private val context: Context) { return delegatedSources.values.find { it.urlName == urlName }?.delegatedHttpSource } - fun getOnlineSources() = sourcesMap.values.filterIsInstance() - - fun getCatalogueSources() = sourcesMap.values.filterIsInstance() - - internal fun registerSource(source: Source, overwrite: Boolean = false) { - if (overwrite || !sourcesMap.containsKey(source.id)) { - delegatedSources[source.id]?.delegatedHttpSource?.delegate = source as? HttpSource - sourcesMap[source.id] = source - } - } - - internal fun unregisterSource(source: Source) { - sourcesMap.remove(source.id) - } + fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance() - private fun createInternalSources(): List = listOf( - LocalSource(context), - ) + fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance() @Suppress("OverridingDeprecatedMember") inner class StubSource(override val id: Long) : Source { @@ -97,6 +120,7 @@ open class SourceManager(private val context: Context) { throw getSourceNotInstalledException() } + @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails")) override fun fetchMangaDetails(manga: SManga): Observable { return Observable.error(getSourceNotInstalledException()) } @@ -105,6 +129,7 @@ open class SourceManager(private val context: Context) { throw getSourceNotInstalledException() } + @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList")) override fun fetchChapterList(manga: SManga): Observable> { return Observable.error(getSourceNotInstalledException()) } @@ -113,6 +138,7 @@ open class SourceManager(private val context: Context) { throw getSourceNotInstalledException() } + @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getPageList")) override fun fetchPageList(chapter: SChapter): Observable> { return Observable.error(getSourceNotInstalledException()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt index 60cf09466e08..683aadd28800 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -97,7 +97,7 @@ abstract class HttpSource : CatalogueSource { } fun getExtension(extensionManager: ExtensionManager? = null): Extension.Installed? = - (extensionManager ?: Injekt.get()).installedExtensions.find { it.sources.contains(this) } + (extensionManager ?: Injekt.get()).installedExtensionsFlow.value.find { it.sources.contains(this) } fun extOnlyHasAllLanguage(extensionManager: ExtensionManager? = null) = getExtension(extensionManager)?.sources?.all { it.lang == "all" } ?: true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.kt deleted file mode 100644 index 8150c2e8dd56..000000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseRxActivity.kt +++ /dev/null @@ -1,39 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.activity - -import android.content.res.Resources -import android.os.Bundle -import androidx.lifecycle.lifecycleScope -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate -import eu.kanade.tachiyomi.util.system.getThemeWithExtras -import eu.kanade.tachiyomi.util.system.setLocaleByAppCompat -import eu.kanade.tachiyomi.util.system.setThemeByPref -import nucleus.view.NucleusAppCompatActivity -import uy.kohesive.injekt.injectLazy - -abstract class BaseRxActivity

> : NucleusAppCompatActivity

() { - - val scope = lifecycleScope - private val preferences by injectLazy() - private var updatedTheme: Resources.Theme? = null - - override fun onCreate(savedInstanceState: Bundle?) { - setLocaleByAppCompat() - updatedTheme = null - setThemeByPref(preferences) - super.onCreate(savedInstanceState) - SecureActivityDelegate.setSecure(this) - } - - override fun onResume() { - super.onResume() - SecureActivityDelegate.promptLockIfNeeded(this) - } - - override fun getTheme(): Resources.Theme { - val newTheme = getThemeWithExtras(super.getTheme(), preferences, updatedTheme) - updatedTheme = newTheme - return newTheme - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseCoroutineController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseCoroutineController.kt index 3069d89f4b46..587e4e60365a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseCoroutineController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseCoroutineController.kt @@ -18,8 +18,8 @@ abstract class BaseCoroutineController BaseCoroutinePresenter.takeView(view: Any) = attachView(view as? View) - override fun onDestroyView(view: View) { - super.onDestroyView(view) + override fun onDestroy() { + super.onDestroy() presenter.onDestroy() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt deleted file mode 100644 index 0786d5e137c2..000000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt +++ /dev/null @@ -1,23 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.controller - -import android.os.Bundle -import androidx.viewbinding.ViewBinding -import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorDelegate -import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorLifecycleListener -import nucleus.factory.PresenterFactory -import nucleus.presenter.Presenter - -@Suppress("LeakingThis") -abstract class NucleusController>(val bundle: Bundle? = null) : - RxController(bundle), - PresenterFactory

{ - - private val delegate = NucleusConductorDelegate(this) - - val presenter: P - get() = delegate.presenter!! - - init { - addLifecycleListener(NucleusConductorLifecycleListener(delegate)) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt deleted file mode 100644 index 981794f9345e..000000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt +++ /dev/null @@ -1,90 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.presenter - -import android.os.Bundle -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.cancel -import nucleus.presenter.RxPresenter -import nucleus.presenter.delivery.Delivery -import rx.Observable - -open class BasePresenter : RxPresenter() { - - lateinit var presenterScope: CoroutineScope - - /** - * Query from the view where applicable - */ - var query: String = "" - protected set - - override fun onCreate(savedState: Bundle?) { - try { - super.onCreate(savedState) - presenterScope = MainScope() - } catch (e: NullPointerException) { - // Swallow this error. This should be fixed in the library but since it's not critical - // (only used by restartables) it should be enough. It saves me a fork. - } - } - - override fun onDestroy() { - super.onDestroy() - presenterScope.cancel() - } - - /** - * Subscribes an observable with [deliverFirst] and adds it to the presenter's lifecycle - * subscription list. - * - * @param onNext function to execute when the observable emits an item. - * @param onError function to execute when the observable throws an error. - */ - fun Observable.subscribeFirst(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) = - compose(deliverFirst()).subscribe(split(onNext, onError)).apply { add(this) } - - /** - * Subscribes an observable with [deliverLatestCache] and adds it to the presenter's lifecycle - * subscription list. - * - * @param onNext function to execute when the observable emits an item. - * @param onError function to execute when the observable throws an error. - */ - fun Observable.subscribeLatestCache(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) = - compose(deliverLatestCache()).subscribe(split(onNext, onError)).apply { add(this) } - - /** - * Subscribes an observable with [deliverReplay] and adds it to the presenter's lifecycle - * subscription list. - * - * @param onNext function to execute when the observable emits an item. - * @param onError function to execute when the observable throws an error. - */ - fun Observable.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) = - compose(deliverReplay()).subscribe(split(onNext, onError)).apply { add(this) } - - /** - * Subscribes an observable with [DeliverWithView] and adds it to the presenter's lifecycle - * subscription list. - * - * @param onNext function to execute when the observable emits an item. - * @param onError function to execute when the observable throws an error. - */ - fun Observable.subscribeWithView(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) = - compose(DeliverWithView(view())).subscribe(split(onNext, onError)).apply { add(this) } - - /** - * A deliverable that only emits to the view if attached, otherwise the event is ignored. - */ - class DeliverWithView(private val view: Observable) : Observable.Transformer> { - - override fun call(observable: Observable): Observable> { - return observable - .materialize() - .filter { notification -> !notification.isOnCompleted } - .flatMap { notification -> - view.take(1).filter { it != null }.map { Delivery(it, notification) } - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.kt deleted file mode 100644 index cd07ed478ae4..000000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorDelegate.kt +++ /dev/null @@ -1,46 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.presenter - -import android.os.Bundle -import nucleus.factory.PresenterFactory -import nucleus.presenter.Presenter - -class NucleusConductorDelegate

>(private val factory: PresenterFactory

) { - - var presenter: P? = null - get() { - if (field == null) { - field = factory.createPresenter() - field!!.create(bundle) - bundle = null - } - return field - } - - private var bundle: Bundle? = null - - fun onSaveInstanceState(): Bundle { - val bundle = Bundle() - // getPresenter(); // Workaround a crash related to saving instance state with child routers - presenter?.save(bundle) - return bundle - } - - fun onRestoreInstanceState(presenterState: Bundle?) { - bundle = presenterState - } - - @Suppress("UNCHECKED_CAST") - private fun Presenter.takeView(view: Any) = takeView(view as View) - - fun onTakeView(view: Any) { - presenter?.takeView(view) - } - - fun onDropView() { - presenter?.dropView() - } - - fun onDestroy() { - presenter?.destroy() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.kt deleted file mode 100644 index f59febccfaaa..000000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/NucleusConductorLifecycleListener.kt +++ /dev/null @@ -1,32 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.presenter - -import android.os.Bundle -import android.view.View -import com.bluelinelabs.conductor.Controller - -class NucleusConductorLifecycleListener(private val delegate: NucleusConductorDelegate<*>) : Controller.LifecycleListener() { - - override fun postCreateView(controller: Controller, view: View) { - delegate.onTakeView(controller) - } - - override fun preDestroyView(controller: Controller, view: View) { - delegate.onDropView() - } - - override fun preDestroy(controller: Controller) { - delegate.onDestroy() - } - - override fun onSaveInstanceState(controller: Controller, outState: Bundle) { - outState.putBundle(PRESENTER_STATE_KEY, delegate.onSaveInstanceState()) - } - - override fun onRestoreInstanceState(controller: Controller, savedInstanceState: Bundle) { - delegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY)) - } - - companion object { - private const val PRESENTER_STATE_KEY = "presenter_state" - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt index 43d6bb3afb55..42f72b31d268 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt @@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.util.system.withUIContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch @@ -39,12 +40,12 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter( super.onCreate() presenterScope.launch { val extensionJob = async { - extensionManager.findAvailableExtensionsAsync() + extensionManager.findAvailableExtensions() extensions = toItems( Triple( - extensionManager.installedExtensions, - extensionManager.untrustedExtensions, - extensionManager.availableExtensions, + extensionManager.installedExtensionsFlow.value, + extensionManager.untrustedExtensionsFlow.value, + extensionManager.availableExtensionsFlow.value, ), ) withContext(Dispatchers.Main) { controller?.setExtensions(extensions, false) } @@ -53,16 +54,16 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter( listOf(migrationJob, extensionJob).awaitAll() } presenterScope.launch { - extensionManager.downloadRelay + extensionManager.downloadRelay.asSharedFlow() .collect { if (it.first.startsWith("Finished")) { firstLoad = true currentDownloads.clear() extensions = toItems( Triple( - extensionManager.installedExtensions, - extensionManager.untrustedExtensions, - extensionManager.availableExtensions, + extensionManager.installedExtensionsFlow.value, + extensionManager.untrustedExtensionsFlow.value, + extensionManager.availableExtensionsFlow.value, ), ) withUIContext { controller?.setExtensions(extensions) } @@ -91,9 +92,9 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter( presenterScope.launch { extensions = toItems( Triple( - extensionManager.installedExtensions, - extensionManager.untrustedExtensions, - extensionManager.availableExtensions, + extensionManager.installedExtensionsFlow.value, + extensionManager.untrustedExtensionsFlow.value, + extensionManager.availableExtensionsFlow.value, ), ) withContext(Dispatchers.Main) { controller?.setExtensions(extensions, false) } @@ -249,7 +250,7 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter( fun updateExtension(extension: Extension.Installed) { val availableExt = - extensionManager.availableExtensions.find { it.pkgName == extension.pkgName } ?: return + extensionManager.availableExtensionsFlow.value.find { it.pkgName == extension.pkgName } ?: return installExtension(availableExt) } @@ -265,7 +266,7 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter( val intent = ExtensionInstallService.jobIntent( context, extensions.mapNotNull { extension -> - extensionManager.availableExtensions.find { it.pkgName == extension.pkgName } + extensionManager.availableExtensionsFlow.value.find { it.pkgName == extension.pkgName } }, ) ContextCompat.startForegroundService(context, intent) @@ -276,7 +277,9 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter( } fun findAvailableExtensions() { - extensionManager.findAvailableExtensions() + presenterScope.launch { + extensionManager.findAvailableExtensions() + } } fun trustSignature(signatureHash: String) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionFilterController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionFilterController.kt index 070dd9a22faa..96e81dab4df2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionFilterController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionFilterController.kt @@ -22,7 +22,7 @@ class ExtensionFilterController : SettingsController() { val activeLangs = preferences.enabledLanguages().get() - val availableLangs = extensionManager.availableExtensions.groupBy { it.lang }.keys + val availableLangs = extensionManager.availableExtensionsFlow.value.groupBy { it.lang }.keys .sortedWith(compareBy({ it !in activeLangs }, { LocaleHelper.getSourceDisplayName(it, context) })) availableLangs.forEach { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsController.kt index 1eea2ecc2983..d84a966945a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsController.kt @@ -36,7 +36,7 @@ import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.getPreferenceKey import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController import eu.kanade.tachiyomi.ui.setting.DSL import eu.kanade.tachiyomi.ui.setting.onChange import eu.kanade.tachiyomi.ui.setting.switchPreference @@ -57,7 +57,7 @@ import uy.kohesive.injekt.injectLazy @SuppressLint("RestrictedApi") class ExtensionDetailsController(bundle: Bundle? = null) : - NucleusController(bundle), + BaseCoroutineController(bundle), PreferenceManager.OnDisplayPreferenceDialogListener, DialogPreference.TargetFragment { @@ -81,9 +81,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) : override fun createBinding(inflater: LayoutInflater) = ExtensionDetailControllerBinding.inflate(inflater.cloneInContext(getPreferenceThemeContext())) - override fun createPresenter(): ExtensionDetailsPresenter { - return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!) - } + override val presenter = ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!) override fun getTitle(): String? { return resources?.getString(R.string.extension_info) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsPresenter.kt index 3f17753219f6..42d42d2540e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsPresenter.kt @@ -1,35 +1,34 @@ package eu.kanade.tachiyomi.ui.extension.details -import android.os.Bundle import eu.kanade.tachiyomi.extension.ExtensionManager -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import rx.android.schedulers.AndroidSchedulers +import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter +import eu.kanade.tachiyomi.util.system.launchUI +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class ExtensionDetailsPresenter( val pkgName: String, private val extensionManager: ExtensionManager = Injekt.get(), -) : BasePresenter() { +) : BaseCoroutinePresenter() { - val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName } - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) + val extension = extensionManager.installedExtensionsFlow.value.find { it.pkgName == pkgName } + override fun onCreate() { + super.onCreate() bindToUninstalledExtension() } private fun bindToUninstalledExtension() { - extensionManager.getInstalledExtensionsObservable() - .skip(1) - .filter { extensions -> extensions.none { it.pkgName == pkgName } } - .map { Unit } - .take(1) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst({ view, _ -> - view.onExtensionUninstalled() - },) + extensionManager.installedExtensionsFlow + .drop(1) + .onEach { extensions -> + extensions.filter { it.pkgName == pkgName } + presenterScope.launchUI { controller?.onExtensionUninstalled() } + } + .launchIn(presenterScope) } fun uninstallExtension() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 06d252a50703..a5a3f81db394 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -821,15 +821,15 @@ open class MainActivity : BaseActivity(), DownloadServiceLi } fun getExtensionUpdates(force: Boolean) { - if ((force && extensionManager.availableExtensions.isEmpty()) || + if ((force && extensionManager.availableExtensionsFlow.value.isEmpty()) || Date().time >= preferences.lastExtCheck().get() + TimeUnit.HOURS.toMillis(6) ) { lifecycleScope.launch(Dispatchers.IO) { try { - extensionManager.findAvailableExtensionsAsync() + extensionManager.findAvailableExtensions() val pendingUpdates = ExtensionGithubApi().checkForUpdates( this@MainActivity, - extensionManager.availableExtensions.takeIf { it.isNotEmpty() }, + extensionManager.availableExtensionsFlow.value.takeIf { it.isNotEmpty() }, ) preferences.extensionUpdatesCount().set(pendingUpdates.size) preferences.lastExtCheck().set(Date().time) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt index 1d6da7551c42..7e458931becd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt @@ -110,7 +110,7 @@ class EditMangaDialog : DialogController { languages.add("") languages.addAll( - extensionManager.availableExtensions.groupBy { it.lang }.keys + extensionManager.availableExtensionsFlow.value.groupBy { it.lang }.keys .sortedWith( compareBy( { it !in activeLangs }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/BaseMigrationPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/BaseMigrationPresenter.kt index d185471094c1..1aa14dfb493b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/BaseMigrationPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/BaseMigrationPresenter.kt @@ -54,7 +54,7 @@ abstract class BaseMigrationPresenter( val header = SelectionHeader() val sourceGroup = library.groupBy { it.source } val sortOrder = PreferenceValues.MigrationSourceOrder.fromPreference(preferences) - val extensions = extensionManager.installedExtensions + val extensions = extensionManager.installedExtensionsFlow.value val obsoleteSources = extensions.filter { it.isObsolete }.map { it.sources }.flatten().map { it.id } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt index c2180a86f86a..2390bcf85d42 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt @@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.databinding.MigrationControllerBinding import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController import eu.kanade.tachiyomi.ui.source.BrowseController -import eu.kanade.tachiyomi.util.system.await import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.view.activityBinding @@ -24,7 +23,6 @@ import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import rx.schedulers.Schedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -94,9 +92,7 @@ class MigrationController : val item = adapter?.getItem(position) as? SourceItem ?: return launchUI { - val manga = Injekt.get().getFavoriteMangas().asRxSingle().await( - Schedulers.io(), - ) + val manga = Injekt.get().getFavoriteMangas().executeAsBlocking() val sourceMangas = manga.asSequence().filter { it.source == item.source.id }.map { it.id!! }.toList() withContext(Dispatchers.Main) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt index 2a2f03629f64..27dbe21972b5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt @@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.ui.main.BottomNavBarInterface import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchCardAdapter import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchController -import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchPresenter import eu.kanade.tachiyomi.util.view.activityBinding import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener import uy.kohesive.injekt.Injekt @@ -51,9 +50,7 @@ class SearchController( bundle.getLongArray(SOURCES) ?: LongArray(0), ) - override fun createPresenter(): GlobalSearchPresenter { - return SearchPresenter(initialQuery, manga!!, sources = sources) - } + override val presenter = SearchPresenter(initialQuery, manga!!, sources = sources) override fun onMangaClick(manga: Manga) { if (targetController is MigrationListController) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt index 7736fecbc0a7..4d048299e3e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.setting import android.app.Activity -import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri import android.os.Environment @@ -15,7 +14,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn -import eu.kanade.tachiyomi.util.system.getFilePicker import eu.kanade.tachiyomi.util.system.withOriginalWidth import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -167,13 +165,8 @@ class SettingsDownloadController : SettingsController() { preferences.downloadsDirectory().set(path.toString()) } - fun customDirectorySelected(currentDir: String) { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - try { - startActivityForResult(intent, DOWNLOAD_DIR) - } catch (e: ActivityNotFoundException) { - startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR) - } + fun customDirectorySelected() { + startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), DOWNLOAD_DIR) } class DownloadDirectoriesDialog(val controller: SettingsDownloadController) : @@ -193,7 +186,7 @@ class SettingsDownloadController : SettingsController() { setTitle(R.string.download_location) setSingleChoiceItems(items.toTypedArray(), selectedIndex) { dialog, position -> if (position == externalDirs.lastIndex) { - controller.customDirectorySelected(currentDir) + controller.customDirectorySelected() } else { controller.predefinedDirectorySelected(items[position]) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt index 5aa03ab05222..e1e1d1888de2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt @@ -93,10 +93,7 @@ class SettingsMainController : SettingsController(), FloatingSearchInterface { } override fun onActionViewExpand(item: MenuItem?) { - SettingsSearchController.lastSearch = "" // reset saved search query - router.pushController( - RouterTransaction.with(SettingsSearchController()), - ) + router.pushController(RouterTransaction.with(SettingsSearchController())) } private fun navigateTo(controller: Controller) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchController.kt index 6485379a8182..35212cd50f64 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchController.kt @@ -11,19 +11,20 @@ import androidx.recyclerview.widget.LinearLayoutManager import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.databinding.SettingsSearchControllerBinding import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface -import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface import eu.kanade.tachiyomi.ui.setting.SettingsController import eu.kanade.tachiyomi.util.view.activityBinding import eu.kanade.tachiyomi.util.view.liftAppbarWith import eu.kanade.tachiyomi.util.view.withFadeTransaction +import uy.kohesive.injekt.api.get /** * This controller shows and manages the different search result in settings search. * [SettingsSearchAdapter.OnTitleClickListener] called when preference is clicked in settings search */ class SettingsSearchController : - NucleusController(), + BaseController(), FloatingSearchInterface, SmallToolbarInterface, SettingsSearchAdapter.OnTitleClickListener { @@ -33,6 +34,7 @@ class SettingsSearchController : */ private var adapter: SettingsSearchAdapter? = null private var searchView: SearchView? = null + var query: String = "" init { setHasOptionsMenu(true) @@ -40,18 +42,7 @@ class SettingsSearchController : override fun createBinding(inflater: LayoutInflater) = SettingsSearchControllerBinding.inflate(inflater) - override fun getTitle(): String { - return presenter.query - } - - /** - * Create the [SettingsSearchPresenter] used in controller. - * - * @return instance of [SettingsSearchPresenter] - */ - override fun createPresenter(): SettingsSearchPresenter { - return SettingsSearchPresenter() - } + override fun getTitle(): String = query /** * Adds items to the options menu. @@ -80,7 +71,7 @@ class SettingsSearchController : override fun onQueryTextChange(newText: String?): Boolean { if (!newText.isNullOrBlank()) { - lastSearch = newText + query = newText } setItems(getResultSet(newText)) return false @@ -88,7 +79,7 @@ class SettingsSearchController : }, ) - searchView?.setQuery(lastSearch, true) + searchView?.setQuery(query, true) } override fun onActionViewCollapse(item: MenuItem?) { @@ -151,13 +142,9 @@ class SettingsSearchController : */ override fun onTitleClick(ctrl: SettingsController) { searchView?.query.let { - lastSearch = it.toString() + query = it.toString() } router.pushController(ctrl.withFadeTransaction()) } - - companion object { - var lastSearch = "" - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchPresenter.kt deleted file mode 100644 index 0d03d7561b32..000000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchPresenter.kt +++ /dev/null @@ -1,26 +0,0 @@ -package eu.kanade.tachiyomi.ui.setting.search - -import android.os.Bundle -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -/** - * Presenter of [SettingsSearchController] - * Function calls should be done from here. UI calls should be done from the controller. - */ -open class SettingsSearchPresenter : BasePresenter() { - - val preferences: PreferencesHelper = Injekt.get() - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - query = savedState?.getString(SettingsSearchPresenter::query.name) ?: "" // TODO - Some way to restore previous query? - } - - override fun onSave(state: Bundle) { - state.putString(SettingsSearchPresenter::query.name, query) - super.onSave(state) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index d99b70142aab..df3816bc1939 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -29,7 +29,7 @@ import eu.kanade.tachiyomi.source.icon import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.SearchActivity @@ -63,7 +63,7 @@ import kotlin.math.roundToInt * Controller to manage the catalogues available in the app. */ open class BrowseSourceController(bundle: Bundle) : - NucleusController(bundle), + BaseCoroutineController(bundle), FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, FloatingSearchInterface, @@ -146,13 +146,11 @@ open class BrowseSourceController(bundle: Bundle) : // return presenter.source.icon() // } - override fun createPresenter(): BrowseSourcePresenter { - return BrowseSourcePresenter( - args.getLong(SOURCE_ID_KEY), - args.getString(SEARCH_QUERY_KEY), - args.getBoolean(USE_LATEST_KEY), - ) - } + override val presenter = BrowseSourcePresenter( + args.getLong(SOURCE_ID_KEY), + args.getString(SEARCH_QUERY_KEY), + args.getBoolean(USE_LATEST_KEY), + ) override fun createBinding(inflater: LayoutInflater) = BrowseSourceControllerBinding.inflate(inflater) @@ -165,7 +163,6 @@ open class BrowseSourceController(bundle: Bundle) : binding.fab.isVisible = presenter.sourceFilters.isNotEmpty() binding.fab.setOnClickListener { showFilters() } - binding.progress.isVisible = true activityBinding?.appBar?.y = 0f activityBinding?.appBar?.updateAppBarAfterY(recycler) activityBinding?.appBar?.lockYPos = true @@ -178,6 +175,11 @@ open class BrowseSourceController(bundle: Bundle) : } return } + if (presenter.items.isNotEmpty()) { + onAddPage(1, presenter.items) + } else { + binding.progress.isVisible = true + } requestFilePermissionsSafe(301, preferences, presenter.source is LocalSource) } @@ -278,10 +280,9 @@ open class BrowseSourceController(bundle: Bundle) : val searchView = activityBinding?.searchToolbar?.searchView activityBinding?.searchToolbar?.setQueryHint("", !isBehindGlobalSearch && presenter.query.isBlank()) - val query = presenter.query - if (query.isNotBlank()) { + if (presenter.query.isNotBlank()) { searchItem?.expandActionView() - searchView?.setQuery(query, true) + searchView?.setQuery(presenter.query, true) searchView?.clearFocus() } else if (activityBinding?.searchToolbar?.isSearchExpanded == true) { searchItem?.collapseActionView() @@ -516,7 +517,7 @@ open class BrowseSourceController(bundle: Bundle) : showProgressBar() adapter?.clear() - presenter.restartPager(newQuery, presenter.sourceFilters) + presenter.restartPager(newQuery) updatePopLatestIcons() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt index 20e619850dde..65d126f168ec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.ui.source.browse -import android.os.Bundle import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper @@ -13,7 +12,7 @@ import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter import eu.kanade.tachiyomi.ui.source.filter.CheckboxItem import eu.kanade.tachiyomi.ui.source.filter.CheckboxSectionItem import eu.kanade.tachiyomi.ui.source.filter.GroupItem @@ -36,9 +35,6 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -54,7 +50,7 @@ open class BrowseSourcePresenter( val db: DatabaseHelper = Injekt.get(), val prefs: PreferencesHelper = Injekt.get(), private val coverCache: CoverCache = Injekt.get(), -) : BasePresenter() { +) : BaseCoroutinePresenter() { /** * Selected source. @@ -66,6 +62,10 @@ open class BrowseSourcePresenter( var filtersChanged = false + var items = mutableListOf() + val page: Int + get() = pager.currentPage + /** * Modifiable list of filters. */ @@ -87,39 +87,24 @@ open class BrowseSourcePresenter( * Pager containing a list of manga results. */ private lateinit var pager: Pager - - /** - * Subscription for the pager. - */ - private var pagerSubscription: Subscription? = null + private var pagerJob: Job? = null /** * Subscription for one request from the pager. */ private var nextPageJob: Job? = null - init { - query = searchQuery ?: "" - } - - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) + var query = searchQuery ?: "" - source = sourceManager.get(sourceId) as? CatalogueSource ?: return + override fun onCreate() { + super.onCreate() + if (!::pager.isInitialized) { + source = sourceManager.get(sourceId) as? CatalogueSource ?: return - sourceFilters = source.getFilterList() - filtersChanged = false - - if (savedState != null) { - query = savedState.getString(::query.name, "") + sourceFilters = source.getFilterList() + filtersChanged = false + restartPager() } - - restartPager() - } - - override fun onSave(state: Bundle) { - state.putString(::query.name, query) - super.onSave(state) } /** @@ -140,27 +125,27 @@ open class BrowseSourcePresenter( val browseAsList = prefs.browseAsList() val sourceListType = prefs.libraryLayout() val outlineCovers = prefs.outlineOnCovers() + items.clear() // Prepare the pager. - pagerSubscription?.let { remove(it) } - pagerSubscription = pager.results() - .observeOn(Schedulers.io()) - .map { (first, second) -> - first to second - .map { networkToLocalManga(it, sourceId) } - .filter { !prefs.hideInLibraryItems().get() || !it.favorite } - } - .doOnNext { initializeMangas(it.second) } - .map { (first, second) -> first to second.map { BrowseSourceItem(it, browseAsList, sourceListType, outlineCovers) } } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeReplay( - { view, (page, mangas) -> - view.onAddPage(page, mangas) - }, - { _, error -> + pagerJob?.cancel() + pagerJob = presenterScope.launchIO { + pager.results().onEach { (page, second) -> + try { + val mangas = second + .map { networkToLocalManga(it, sourceId) } + .filter { !prefs.hideInLibraryItems().get() || !it.favorite } + initializeMangas(mangas) + val items = mangas.map { + BrowseSourceItem(it, browseAsList, sourceListType, outlineCovers) + } + this@BrowseSourcePresenter.items.addAll(items) + withUIContext { controller?.onAddPage(page, items) } + } catch (error: Exception) { Timber.e(error) - }, - ) + } + }.collect() + } // Request first page. requestNext() @@ -173,14 +158,11 @@ open class BrowseSourcePresenter( if (!hasNextPage()) return nextPageJob?.cancel() - nextPageJob = launchIO { + nextPageJob = presenterScope.launchIO { try { pager.requestNextPage() } catch (e: Throwable) { - withUIContext { - @Suppress("DEPRECATION") - view?.onAddPageError(e) - } + withUIContext { controller?.onAddPageError(e) } } } } @@ -229,10 +211,7 @@ open class BrowseSourcePresenter( .filter { it.thumbnail_url == null && !it.initialized } .map { getMangaDetails(it) } .onEach { - withUIContext { - @Suppress("DEPRECATION") - view?.onMangaInitialized(it) - } + withUIContext { controller?.onMangaInitialized(it) } } .catch { e -> Timber.e(e) } .collect() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/Pager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/Pager.kt index 4c714a76192e..5f8004cc7d76 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/Pager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/Pager.kt @@ -1,9 +1,10 @@ package eu.kanade.tachiyomi.ui.source.browse -import com.jakewharton.rxrelay.PublishRelay import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SManga -import rx.Observable +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow /** * A general pager for source requests (latest updates, popular, search) @@ -13,18 +14,18 @@ abstract class Pager(var currentPage: Int = 1) { var hasNextPage = true private set - protected val results: PublishRelay>> = PublishRelay.create() + protected val results = MutableSharedFlow>>() - fun results(): Observable>> { - return results.asObservable() + fun results(): SharedFlow>> { + return results.asSharedFlow() } abstract suspend fun requestNextPage() - fun onPageReceived(mangasPage: MangasPage) { + suspend fun onPageReceived(mangasPage: MangasPage) { val page = currentPage currentPage++ hasNextPage = mangasPage.hasNextPage && mangasPage.mangas.isNotEmpty() - results.call(Pair(page, mangasPage.mangas)) + results.emit(Pair(page, mangasPage.mangas)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchController.kt index 830bf274760e..3a29ec5170f7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchController.kt @@ -17,7 +17,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.databinding.SourceGlobalSearchControllerBinding import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface -import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.SearchActivity @@ -42,7 +42,7 @@ open class GlobalSearchController( protected val initialQuery: String? = null, val extensionFilter: String? = null, bundle: Bundle? = null, -) : NucleusController(bundle), +) : BaseCoroutineController(bundle), FloatingSearchInterface, SmallToolbarInterface, GlobalSearchAdapter.OnTitleClickListener, @@ -78,14 +78,7 @@ open class GlobalSearchController( return customTitle ?: presenter.query } - /** - * Create the [GlobalSearchPresenter] used in controller. - * - * @return instance of [GlobalSearchPresenter] - */ - override fun createPresenter(): GlobalSearchPresenter { - return GlobalSearchPresenter(initialQuery, extensionFilter) - } + override val presenter = GlobalSearchPresenter(initialQuery, extensionFilter) override fun onTitleClick(source: CatalogueSource) { preferences.lastUsedCatalogueSource().set(source.id) @@ -108,7 +101,7 @@ open class GlobalSearchController( /** * Called when manga in global search is long clicked. * - * @param manga clicked item containing manga information. + * @param position clicked item containing manga information. */ override fun onMangaLongClick(position: Int, adapter: GlobalSearchCardAdapter) { val manga = adapter.getItem(position)?.manga ?: return diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt index 0018a65fafbc..ace9ec97a05b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchPresenter.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.ui.source.globalsearch -import android.os.Bundle import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga @@ -12,15 +11,18 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.source.browse.BrowseSourcePresenter -import eu.kanade.tachiyomi.util.system.runAsObservable -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers -import rx.subjects.PublishSubject -import timber.log.Timber +import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter +import eu.kanade.tachiyomi.util.system.awaitSingle +import eu.kanade.tachiyomi.util.system.launchIO +import eu.kanade.tachiyomi.util.system.launchUI +import eu.kanade.tachiyomi.util.system.withUIContext +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -43,56 +45,43 @@ open class GlobalSearchPresenter( val db: DatabaseHelper = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(), private val coverCache: CoverCache = Injekt.get(), -) : BasePresenter() { +) : BaseCoroutinePresenter() { /** * Enabled sources. */ val sources by lazy { getSourcesToQuery() } - /** - * Fetches the different sources by user settings. - */ - private var fetchSourcesSubscription: Subscription? = null + private var fetchSourcesJob: Job? = null private var loadTime = hashMapOf() - /** - * Subject which fetches image of given manga. - */ - private val fetchImageSubject = PublishSubject.create, Source>>() + var query = "" - /** - * Subscription for fetching images of manga. - */ - private var fetchImageSubscription: Subscription? = null + private val fetchImageFlow = MutableSharedFlow, Source>>() + + private var fetchImageJob: Job? = null private val extensionManager: ExtensionManager by injectLazy() private var extensionFilter: String? = null - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) + var items: List = emptyList() - extensionFilter = savedState?.getString(GlobalSearchPresenter::extensionFilter.name) - ?: initialExtensionFilter + private val semaphore = Semaphore(5) - // Perform a search with previous or initial state - search( - savedState?.getString(BrowseSourcePresenter::query.name) ?: initialQuery.orEmpty(), - ) - } + override fun onCreate() { + super.onCreate() - override fun onDestroy() { - fetchSourcesSubscription?.unsubscribe() - fetchImageSubscription?.unsubscribe() - super.onDestroy() - } + extensionFilter = initialExtensionFilter - override fun onSave(state: Bundle) { - state.putString(BrowseSourcePresenter::query.name, query) - state.putString(GlobalSearchPresenter::extensionFilter.name, extensionFilter) - super.onSave(state) + if (items.isEmpty()) { + // Perform a search with previous or initial state + search(initialQuery.orEmpty()) + } + presenterScope.launchUI { + controller?.setItems(items) + } } /** @@ -126,7 +115,7 @@ open class GlobalSearchPresenter( } val languages = preferences.enabledLanguages().get() - val filterSources = extensionManager.installedExtensions + val filterSources = extensionManager.installedExtensionsFlow.value .filter { it.pkgName == filter } .flatMap { it.sources } .filter { it.lang in languages } @@ -174,70 +163,49 @@ open class GlobalSearchPresenter( // Create items with the initial state val initialItems = sources.map { createCatalogueSearchItem(it, null) } - var items = initialItems - + items = initialItems val pinnedSourceIds = preferences.pinnedCatalogues().get() - fetchSourcesSubscription?.unsubscribe() - fetchSourcesSubscription = Observable.from(sources).flatMap( - { source -> - Observable.defer { source.fetchSearchManga(1, query, source.getFilterList()) } - .subscribeOn(Schedulers.io()).onErrorReturn { - MangasPage( - emptyList(), - false, - ) - } // Ignore timeouts or other exceptions - .map { it.mangas.take(10) } // Get at most 10 manga from search result. - .map { - it.map { - networkToLocalManga( - it, - source.id, - ) + fetchSourcesJob?.cancel() + fetchSourcesJob = presenterScope.launch { + sources.map { source -> + launch mainLaunch@{ + semaphore.withPermit { + if (this@GlobalSearchPresenter.items.find { it.source == source }?.results != null) { + return@mainLaunch + } + val mangas = try { + source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle() + } catch (error: Exception) { + MangasPage(emptyList(), false) } - } // Convert to local manga. - .doOnNext { fetchImage(it, source) } // Load manga covers. - .map { - if (it.isNotEmpty() && !loadTime.containsKey(source.id)) { + .mangas.take(10) + .map { networkToLocalManga(it, source.id) } + fetchImage(mangas, source) + if (mangas.isNotEmpty() && !loadTime.containsKey(source.id)) { loadTime[source.id] = Date().time } - createCatalogueSearchItem( + val result = createCatalogueSearchItem( source, - it.map { GlobalSearchMangaItem(it) }, + mangas.map { GlobalSearchMangaItem(it) }, ) + items = items + .map { item -> if (item.source == result.source) result else item } + .sortedWith( + compareBy( + // Bubble up sources that actually have results + { it.results.isNullOrEmpty() }, + // Same as initial sort, i.e. pinned first then alphabetically + { it.source.id.toString() !in pinnedSourceIds }, + { loadTime[it.source.id] ?: 0L }, + { "${it.source.name.lowercase(Locale.getDefault())} (${it.source.lang})" }, + ), + ) + withUIContext { controller?.setItems(items) } } - }, - 5, - ) - .observeOn(AndroidSchedulers.mainThread()) - // Update matching source with the obtained results - .map { result -> - items - .map { item -> if (item.source == result.source) result else item } - .sortedWith( - compareBy( - // Bubble up sources that actually have results - { it.results.isNullOrEmpty() }, - // Same as initial sort, i.e. pinned first then alphabetically - { it.source.id.toString() !in pinnedSourceIds }, - { loadTime[it.source.id] ?: 0L }, - { "${it.source.name.lowercase(Locale.getDefault())} (${it.source.lang})" }, - ), - ) + } } - // Update current state - .doOnNext { items = it } - // Deliver initial state - .startWith(initialItems) - .subscribeLatestCache( - { view, manga -> - view.setItems(manga) - }, - { _, error -> - Timber.e(error) - }, - ) + } } /** @@ -246,33 +214,26 @@ open class GlobalSearchPresenter( * @param manga the list of manga to initialize. */ private fun fetchImage(manga: List, source: Source) { - fetchImageSubject.onNext(Pair(manga, source)) + presenterScope.launch { + fetchImageFlow.emit(Pair(manga, source)) + } } /** * Subscribes to the initializer of manga details and updates the view if needed. */ private fun initializeFetchImageSubscription() { - fetchImageSubscription?.unsubscribe() - fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io()) - .flatMap { (mangaList, source) -> - Observable.from(mangaList) - .filter { it.thumbnail_url == null && !it.initialized } - .map { Pair(it, source) } - .concatMap { runAsObservable { getMangaDetails(it.first, it.second) } } - .map { Pair(source as CatalogueSource, it) } - } - .onBackpressureBuffer() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { (source, manga) -> - @Suppress("DEPRECATION") - view?.onMangaInitialized(source, manga) - }, - { error -> - Timber.e(error) - }, - ) + fetchImageJob?.cancel() + fetchImageJob = fetchImageFlow.onEach { (mangaList, source) -> + mangaList + .filter { it.thumbnail_url == null && !it.initialized } + .map { + presenterScope.launchIO { + val manga = getMangaDetails(it, source) + withUIContext { controller?.onMangaInitialized(source as CatalogueSource, manga) } + } + } + }.launchIn(presenterScope) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 88beb6ff1a0b..51212001e7b3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -36,12 +36,10 @@ import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.net.toUri import com.hippo.unifile.UniFile -import com.nononsenseapps.filepicker.FilePickerActivity import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File @@ -83,27 +81,6 @@ inline fun Context.notification(channelId: String, func: NotificationCompat.Buil return builder.build() } -/** - * Helper method to construct an Intent to use a custom file picker. - * @param currentDir the path the file picker will open with. - * @return an Intent to start the file picker activity. - */ -fun Context.getFilePicker(currentDir: String): Intent { - return Intent(this, CustomLayoutPickerActivity::class.java) - .putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir) -} - -/** - * Checks if the give permission is granted. - * - * @param permission the permission to check. - * @return true if it has permissions. - */ -fun Context.hasPermission(permission: String) = - ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED - /** * Returns the color for the given attribute. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/CustomLayoutPicker.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/CustomLayoutPicker.kt deleted file mode 100644 index 8713973433ea..000000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/CustomLayoutPicker.kt +++ /dev/null @@ -1,32 +0,0 @@ -package eu.kanade.tachiyomi.widget - -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.nononsenseapps.filepicker.AbstractFilePickerFragment -import com.nononsenseapps.filepicker.FilePickerActivity -import com.nononsenseapps.filepicker.FilePickerFragment -import com.nononsenseapps.filepicker.LogicHandler -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.view.inflate -import java.io.File - -class CustomLayoutPickerActivity : FilePickerActivity() { - - override fun getFragment(startPath: String?, mode: Int, allowMultiple: Boolean, allowCreateDir: Boolean): AbstractFilePickerFragment { - val fragment = CustomLayoutFilePickerFragment() - fragment.setArgs(startPath, mode, allowMultiple, allowCreateDir) - return fragment - } -} - -class CustomLayoutFilePickerFragment : FilePickerFragment() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - LogicHandler.VIEWTYPE_DIR -> { - val view = parent.inflate(R.layout.common_listitem_dir) - DirViewHolder(view) - } - else -> super.onCreateViewHolder(parent, viewType) - } - } -} diff --git a/app/src/main/res/drawable/ic_file_open_24dp.xml b/app/src/main/res/drawable/ic_file_open_24dp.xml new file mode 100644 index 000000000000..e7828748a788 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_open_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/common_listitem_dir.xml b/app/src/main/res/layout/common_listitem_dir.xml deleted file mode 100644 index 1e1271e31882..000000000000 --- a/app/src/main/res/layout/common_listitem_dir.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index df6f7c7def54..0f23b7279076 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -384,21 +384,4 @@ 13sp - - - - - - -