Remove some old libraries

And converting 4 screens into coroutine MVPs
This commit is contained in:
Jays2Kings 2023-03-21 16:06:33 -04:00
parent 28dc7bd738
commit 6a0d161793
39 changed files with 372 additions and 817 deletions

View file

@ -158,10 +158,12 @@ dependencies {
implementation("androidx.browser:browser:1.5.0") implementation("androidx.browser:browser:1.5.0")
implementation("androidx.biometric:biometric:1.1.0") implementation("androidx.biometric:biometric:1.1.0")
implementation("androidx.palette:palette:1.0.0") implementation("androidx.palette:palette:1.0.0")
implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.activity:activity-ktx:1.7.0-rc01")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") 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("com.google.android.flexbox:flexbox:3.0.0")
implementation("androidx.window:window:1.0.0") implementation("androidx.window:window:1.0.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.constraintlayout:constraintlayout:2.1.4")
@ -181,7 +183,6 @@ dependencies {
implementation("io.reactivex:rxandroid:1.2.1") implementation("io.reactivex:rxandroid:1.2.1")
implementation("io.reactivex:rxjava:1.3.8") implementation("io.reactivex:rxjava:1.3.8")
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0") implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
// Coroutines // Coroutines
implementation("com.fredporciuncula:flow-preferences:1.6.0") implementation("com.fredporciuncula:flow-preferences:1.6.0")
@ -224,9 +225,6 @@ dependencies {
implementation("com.google.android.gms:play-services-gcm:17.0.0") implementation("com.google.android.gms:play-services-gcm:17.0.0")
// Changelog
implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0")
// Database // Database
implementation("androidx.sqlite:sqlite-ktx:2.3.0") implementation("androidx.sqlite:sqlite-ktx:2.3.0")
implementation("com.github.requery:sqlite-android:3.39.2") implementation("com.github.requery:sqlite-android:3.39.2")
@ -261,7 +259,6 @@ dependencies {
implementation("com.mikepenz:fastadapter-extensions-binding:$fastAdapterVersion") implementation("com.mikepenz:fastadapter-extensions-binding:$fastAdapterVersion")
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533") implementation("com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533")
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter-ui: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.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
implementation("com.github.mthli:Slice:v1.2") implementation("com.github.mthli:Slice:v1.2")
implementation("io.noties.markwon:core:4.6.2") implementation("io.noties.markwon:core:4.6.2")

View file

@ -127,10 +127,6 @@
android:configChanges="uiMode|orientation|screenSize"/> android:configChanges="uiMode|orientation|screenSize"/>
<activity <activity
android:name=".ui.security.BiometricActivity" /> android:name=".ui.security.BiometricActivity" />
<activity
android:name=".widget.CustomLayoutPickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme" />
<activity <activity
android:name=".ui.setting.track.MyAnimeListLoginActivity" android:name=".ui.setting.track.MyAnimeListLoginActivity"
android:label="MyAnimeList" android:label="MyAnimeList"

View file

@ -39,8 +39,7 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { JavaScriptEngine(app) } addSingletonFactory { JavaScriptEngine(app) }
addSingletonFactory { SourceManager(app).also { get<ExtensionManager>().init(it) } } addSingletonFactory { SourceManager(app, get()) }
addSingletonFactory { ExtensionManager(app) } addSingletonFactory { ExtensionManager(app) }
addSingletonFactory { DownloadManager(app) } addSingletonFactory { DownloadManager(app) }

View file

@ -109,7 +109,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(pendingIntent) setContentIntent(pendingIntent)
setSmallIcon(R.drawable.ic_tachij2k_notification) setSmallIcon(R.drawable.ic_tachij2k_notification)
addAction( addAction(
R.drawable.nnf_ic_file_folder, R.drawable.ic_file_open_24dp,
context.getString(R.string.open_log), context.getString(R.string.open_log),
pendingIntent, pendingIntent,
) )
@ -144,7 +144,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(NotificationHandler.openUrl(context, HELP_SKIPPED_URL)) setContentIntent(NotificationHandler.openUrl(context, HELP_SKIPPED_URL))
setSmallIcon(R.drawable.ic_tachij2k_notification) setSmallIcon(R.drawable.ic_tachij2k_notification)
addAction( addAction(
R.drawable.nnf_ic_file_folder, R.drawable.ic_file_open_24dp,
context.getString(R.string.open_log), context.getString(R.string.open_log),
NotificationReceiver.openErrorOrSkippedLogPendingActivity(context, uri), NotificationReceiver.openErrorOrSkippedLogPendingActivity(context, uri),
) )

View file

@ -81,7 +81,7 @@ class ExtensionInstallService(
instance = this instance = this
val list = intent.getParcelableArrayListExtra<ExtensionInfo>(KEY_EXTENSION)?.filter { val list = intent.getParcelableArrayListExtra<ExtensionInfo>(KEY_EXTENSION)?.filter {
val installedExt = extensionManager.installedExtensions.find { installed -> val installedExt = extensionManager.installedExtensionsFlow.value.find { installed ->
installed.pkgName == it.pkgName installed.pkgName == it.pkgName
} ?: return@filter false } ?: return@filter false
installedExt.versionCode < it.versionCode || installedExt.libVersion < it.libVersion installedExt.versionCode < it.versionCode || installedExt.libVersion < it.libVersion

View file

@ -5,7 +5,6 @@ import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.preference.PreferenceKeys import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi 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.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.system.launchNow import eu.kanade.tachiyomi.util.system.launchNow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import rx.Observable import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get 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 * 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 installer by lazy { ExtensionInstaller(context) }
private val iconMap = mutableMapOf<String, Drawable>()
val downloadRelay val downloadRelay
get() = installer.downloadsStateFlow get() = installer.downloadsStateFlow
@ -66,27 +68,28 @@ class ExtensionManager(
/** /**
* Relay used to notify the installed extensions. * Relay used to notify the installed extensions.
*/ */
private val installedExtensionsRelay = BehaviorRelay.create<List<Extension.Installed>>() private val _installedExtensionsFlow = MutableStateFlow(emptyList<Extension.Installed>())
val installedExtensionsFlow = _installedExtensionsFlow.asStateFlow()
private val iconMap = mutableMapOf<String, Drawable>() private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
/** /**
* List of the currently installed extensions. * List of the currently installed extensions.
*/ */
var installedExtensions = emptyList<Extension.Installed>() // private var installedExtensions = emptyList<Extension.Installed>()
private set(value) { // set(value) {
field = value // field = value
installedExtensionsRelay.call(value) // installedExtensionsRelay.call(value)
downloadRelay.tryEmit("Finished/Installed/${value.size}" to (InstallStep.Done to null)) // downloadRelay.tryEmit("Finished/Installed/${value.size}" to (InstallStep.Done to null))
} // }
fun getAppIconForSource(source: Source): Drawable? { fun getAppIconForSource(source: Source): Drawable? {
return getAppIconForSource(source.id) return getAppIconForSource(source.id)
} }
private fun getAppIconForSource(sourceId: Long): Drawable? { private fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName = val pkgName = _installedExtensionsFlow.value
installedExtensions.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName .find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
return if (pkgName != null) { return if (pkgName != null) {
try { try {
return iconMap[pkgName] return iconMap[pkgName]
@ -102,47 +105,42 @@ class ExtensionManager(
/** /**
* Relay used to notify the available extensions. * Relay used to notify the available extensions.
*/ */
private val availableExtensionsRelay = BehaviorRelay.create<List<Extension.Available>>() private val _availableExtensionsFlow = MutableStateFlow(emptyList<Extension.Available>())
val availableExtensionsFlow = _availableExtensionsFlow.asStateFlow()
/**
* List of the currently available extensions.
*/
var availableExtensions = emptyList<Extension.Available>()
private set(value) {
field = value
availableExtensionsRelay.call(value)
updatedInstalledExtensionsStatuses(value)
downloadRelay.tryEmit("Finished/Available/${value.size}" to (InstallStep.Done to null))
setupAvailableSourcesMap()
}
private var availableSources = hashMapOf<Long, Extension.AvailableSource>() private var availableSources = hashMapOf<Long, Extension.AvailableSource>()
/** /**
* Relay used to notify the untrusted extensions. * List of the currently available extensions.
*/ */
private val untrustedExtensionsRelay = BehaviorRelay.create<List<Extension.Untrusted>>() // var availableExtensions = emptyList<Extension.Available>()
// 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<Extension.Untrusted>())
val untrustedExtensionsFlow = _untrustedExtensionsFlow.asStateFlow()
/** /**
* List of the currently untrusted extensions. * List of the currently untrusted extensions.
*/ */
var untrustedExtensions = emptyList<Extension.Untrusted>() // var untrustedExtensions = emptyList<Extension.Untrusted>()
private set(value) { // private set(value) {
field = value // field = value
untrustedExtensionsRelay.call(value) // untrustedExtensionsRelay.call(value)
downloadRelay.tryEmit("Finished/Untrusted/${value.size}" to (InstallStep.Done to null)) // downloadRelay.tryEmit("Finished/Untrusted/${value.size}" to (InstallStep.Done to null))
} // }
/** /**
* The source manager where the sources of the extensions are added. * The source manager where the sources of the extensions are added.
*/ */
private lateinit var sourceManager: SourceManager private lateinit var sourceManager: SourceManager
/** init {
* Initializes this manager with the given source manager.
*/
fun init(sourceManager: SourceManager) {
this.sourceManager = sourceManager
initExtensions() initExtensions()
ExtensionInstallReceiver(InstallationListener()).register(context) ExtensionInstallReceiver(InstallationListener()).register(context)
} }
@ -153,79 +151,77 @@ class ExtensionManager(
private fun initExtensions() { private fun initExtensions() {
val extensions = ExtensionLoader.loadExtensions(context) val extensions = ExtensionLoader.loadExtensions(context)
installedExtensions = extensions _installedExtensionsFlow.value = extensions
.filterIsInstance<LoadResult.Success>() .filterIsInstance<LoadResult.Success>()
.map { it.extension } .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<LoadResult.Untrusted>() .filterIsInstance<LoadResult.Untrusted>()
.map { it.extension } .map { it.extension }
} }
/**
* Returns the relay of the installed extensions as an observable.
*/
fun getInstalledExtensionsObservable(): Observable<List<Extension.Installed>> {
return installedExtensionsRelay.asObservable()
}
/**
* Returns the relay of the available extensions as an observable.
*/
fun getAvailableExtensionsObservable(): Observable<List<Extension.Available>> {
return availableExtensionsRelay.asObservable()
}
/**
* Returns the relay of the untrusted extensions as an observable.
*/
fun getUntrustedExtensionsObservable(): Observable<List<Extension.Untrusted>> {
return untrustedExtensionsRelay.asObservable()
}
fun isInstalledByApp(extension: Extension.Available): Boolean { fun isInstalledByApp(extension: Extension.Available): Boolean {
return ExtensionLoader.isExtensionInstalledByApp(context, extension.pkgName) return ExtensionLoader.isExtensionInstalledByApp(context, extension.pkgName)
} }
/** /**
* Finds the available extensions in the [api] and updates [availableExtensions]. * Finds the available extensions in the [api] and updates [availableExtensionsFlow].
*/ */
fun findAvailableExtensions() { suspend fun findAvailableExtensions() {
launchNow { val extensions: List<Extension.Available> = try {
availableExtensions = try { api.findExtensions()
api.findExtensions() } catch (e: Exception) {
} catch (e: Exception) { Timber.e(e)
emptyList() emptyList()
}
} }
enableAdditionalSubLanguages(extensions)
_availableExtensionsFlow.value = extensions
updatedInstalledExtensionsStatuses(extensions)
setupAvailableSourcesMap()
downloadRelay.tryEmit("Finished/Available/${extensions.size}" to (InstallStep.Done to null))
}
/**
* 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.
*/
private fun enableAdditionalSubLanguages(extensions: List<Extension.Available>) {
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() { private fun setupAvailableSourcesMap() {
availableSources = hashMapOf() availableSources = hashMapOf()
availableExtensions.map { it.sources.orEmpty() }.flatten().forEach { _availableExtensionsFlow.value.map { it.sources }.flatten().forEach {
availableSources[it.id] = it availableSources[it.id] = it
} }
} }
fun getStubSource(id: Long) = availableSources[id] 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]. * Sets the update field of the installed extensions with the given [availableExtensions].
* *
@ -236,7 +232,7 @@ class ExtensionManager(
preferences.extensionUpdatesCount().set(0) preferences.extensionUpdatesCount().set(0)
return return
} }
val mutInstalledExtensions = installedExtensions.toMutableList() val mutInstalledExtensions = installedExtensionsFlow.value.toMutableList()
var changed = false var changed = false
var hasUpdateCount = 0 var hasUpdateCount = 0
for ((index, installedExt) in mutInstalledExtensions.withIndex()) { for ((index, installedExt) in mutInstalledExtensions.withIndex()) {
@ -257,9 +253,9 @@ class ExtensionManager(
} }
} }
if (changed) { 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. * @param signature The signature to whitelist.
*/ */
fun trustSignature(signature: String) { fun trustSignature(signature: String) {
val untrustedSignatures = untrustedExtensions.map { it.signatureHash }.toSet() val untrustedSignatures = untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
if (signature !in untrustedSignatures) return if (signature !in untrustedSignatures) return
ExtensionLoader.trustedSignatures += signature ExtensionLoader.trustedSignatures += signature
val preference = preferences.trustedSignatures() val preference = preferences.trustedSignatures()
preference.set(preference.get() + signature) preference.set(preference.get() + signature)
val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature } val nowTrustedExtensions = untrustedExtensionsFlow.value.filter { it.signatureHash == signature }
untrustedExtensions -= nowTrustedExtensions _untrustedExtensionsFlow.value -= nowTrustedExtensions
val ctx = context val ctx = context
launchNow { launchNow {
@ -389,9 +385,8 @@ class ExtensionManager(
* @param extension The extension to be registered. * @param extension The extension to be registered.
*/ */
private fun registerNewExtension(extension: Extension.Installed) { private fun registerNewExtension(extension: Extension.Installed) {
installedExtensions = installedExtensions + extension _installedExtensionsFlow.value += extension
downloadRelay.tryEmit("Finished/${extension.pkgName}" to ExtensionIntallInfo(InstallStep.Installed, null)) 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. * @param extension The extension to be registered.
*/ */
private fun registerUpdatedExtension(extension: Extension.Installed) { private fun registerUpdatedExtension(extension: Extension.Installed) {
val mutInstalledExtensions = installedExtensions.toMutableList() val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList()
val oldExtension = mutInstalledExtensions.find { it.pkgName == extension.pkgName } val oldExtension = mutInstalledExtensions.find { it.pkgName == extension.pkgName }
if (oldExtension != null) { if (oldExtension != null) {
mutInstalledExtensions -= oldExtension mutInstalledExtensions -= oldExtension
extension.sources.forEach { sourceManager.unregisterSource(it) }
} }
mutInstalledExtensions += extension mutInstalledExtensions += extension
installedExtensions = mutInstalledExtensions _installedExtensionsFlow.value = mutInstalledExtensions
downloadRelay.tryEmit("Finished/${extension.pkgName}" to ExtensionIntallInfo(InstallStep.Installed, null)) 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. * @param pkgName The package name of the uninstalled application.
*/ */
private fun unregisterExtension(pkgName: String) { private fun unregisterExtension(pkgName: String) {
val installedExtension = installedExtensions.find { it.pkgName == pkgName } val installedExtension = installedExtensionsFlow.value.find { it.pkgName == pkgName }
if (installedExtension != null) { if (installedExtension != null) {
installedExtensions = installedExtensions - installedExtension _installedExtensionsFlow.value -= installedExtension
installedExtension.sources.forEach { sourceManager.unregisterSource(it) }
} }
val untrustedExtension = untrustedExtensions.find { it.pkgName == pkgName } val untrustedExtension = untrustedExtensionsFlow.value.find { it.pkgName == pkgName }
if (untrustedExtension != null) { if (untrustedExtension != null) {
untrustedExtensions = untrustedExtensions - untrustedExtension _untrustedExtensionsFlow.value -= untrustedExtension
} }
} }
@ -438,21 +430,21 @@ class ExtensionManager(
override fun onExtensionInstalled(extension: Extension.Installed) { override fun onExtensionInstalled(extension: Extension.Installed) {
registerNewExtension(extension.withUpdateCheck()) registerNewExtension(extension.withUpdateCheck())
preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate }) preferences.extensionUpdatesCount().set(installedExtensionsFlow.value.count { it.hasUpdate })
} }
override fun onExtensionUpdated(extension: Extension.Installed) { override fun onExtensionUpdated(extension: Extension.Installed) {
registerUpdatedExtension(extension.withUpdateCheck()) registerUpdatedExtension(extension.withUpdateCheck())
preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate }) preferences.extensionUpdatesCount().set(installedExtensionsFlow.value.count { it.hasUpdate })
} }
override fun onExtensionUntrusted(extension: Extension.Untrusted) { override fun onExtensionUntrusted(extension: Extension.Untrusted) {
untrustedExtensions += extension _untrustedExtensionsFlow.value += extension
} }
override fun onPackageUninstalled(pkgName: String) { override fun onPackageUninstalled(pkgName: String) {
unregisterExtension(pkgName) 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 { 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 if (isUnofficial || availableExt == null) return false
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion) return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)

View file

@ -97,7 +97,7 @@ internal class ExtensionGithubApi {
isNsfw = it.nsfw == 1, isNsfw = it.nsfw == 1,
hasReadme = it.hasReadme == 1, hasReadme = it.hasReadme == 1,
hasChangelog = it.hasChangelog == 1, hasChangelog = it.hasChangelog == 1,
sources = it.sources, sources = it.sources ?: emptyList(),
apkName = it.apk, apkName = it.apk,
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}", iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
) )

View file

@ -46,7 +46,7 @@ sealed class Extension {
override val hasChangelog: Boolean, override val hasChangelog: Boolean,
val apkName: String, val apkName: String,
val iconUrl: String, val iconUrl: String,
val sources: List<AvailableSource>? = null, val sources: List<AvailableSource>,
) : Extension() ) : Extension()
@Serializable @Serializable

View file

@ -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.all.MangaDex
import eu.kanade.tachiyomi.source.online.english.KireiCake import eu.kanade.tachiyomi.source.online.english.KireiCake
import eu.kanade.tachiyomi.source.online.english.MangaPlus 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 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<Long, Source>() private val scope = CoroutineScope(Job() + Dispatchers.IO)
private val stubSourcesMap = mutableMapOf<Long, StubSource>() private val sourcesMapFlow = MutableStateFlow(ConcurrentHashMap<Long, Source>())
protected val extensionManager: ExtensionManager by injectLazy() private val stubSourcesMap = ConcurrentHashMap<Long, StubSource>()
val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<CatalogueSource>() }
val onlineSources: Flow<List<HttpSource>> = catalogueSources.map { it.filterIsInstance<HttpSource>() }
private val delegatedSources = listOf( private val delegatedSources = listOf(
DelegatedSource( DelegatedSource(
@ -47,16 +62,39 @@ open class SourceManager(private val context: Context) {
).associateBy { it.sourceId } ).associateBy { it.sourceId }
init { init {
createInternalSources().forEach { registerSource(it) } scope.launch {
extensionManager.installedExtensionsFlow
.collectLatest { extensions ->
val mutableMap = ConcurrentHashMap<Long, Source>(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? { fun get(sourceKey: Long): Source? {
return sourcesMap[sourceKey] return sourcesMapFlow.value[sourceKey]
} }
fun getOrStub(sourceKey: Long): Source { fun getOrStub(sourceKey: Long): Source {
return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) { return sourcesMapFlow.value[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
StubSource(sourceKey) runBlocking { StubSource(sourceKey) }
} }
} }
@ -68,24 +106,9 @@ open class SourceManager(private val context: Context) {
return delegatedSources.values.find { it.urlName == urlName }?.delegatedHttpSource return delegatedSources.values.find { it.urlName == urlName }?.delegatedHttpSource
} }
fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>() fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance<HttpSource>()
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>() fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance<CatalogueSource>()
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)
}
private fun createInternalSources(): List<Source> = listOf(
LocalSource(context),
)
@Suppress("OverridingDeprecatedMember") @Suppress("OverridingDeprecatedMember")
inner class StubSource(override val id: Long) : Source { inner class StubSource(override val id: Long) : Source {
@ -97,6 +120,7 @@ open class SourceManager(private val context: Context) {
throw getSourceNotInstalledException() throw getSourceNotInstalledException()
} }
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails"))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return Observable.error(getSourceNotInstalledException()) return Observable.error(getSourceNotInstalledException())
} }
@ -105,6 +129,7 @@ open class SourceManager(private val context: Context) {
throw getSourceNotInstalledException() throw getSourceNotInstalledException()
} }
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList"))
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.error(getSourceNotInstalledException()) return Observable.error(getSourceNotInstalledException())
} }
@ -113,6 +138,7 @@ open class SourceManager(private val context: Context) {
throw getSourceNotInstalledException() throw getSourceNotInstalledException()
} }
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getPageList"))
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return Observable.error(getSourceNotInstalledException()) return Observable.error(getSourceNotInstalledException())
} }

View file

@ -97,7 +97,7 @@ abstract class HttpSource : CatalogueSource {
} }
fun getExtension(extensionManager: ExtensionManager? = null): Extension.Installed? = 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) = fun extOnlyHasAllLanguage(extensionManager: ExtensionManager? = null) =
getExtension(extensionManager)?.sources?.all { it.lang == "all" } ?: true getExtension(extensionManager)?.sources?.all { it.lang == "all" } ?: true

View file

@ -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<P : BasePresenter<*>> : NucleusAppCompatActivity<P>() {
val scope = lifecycleScope
private val preferences by injectLazy<PreferencesHelper>()
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
}
}

View file

@ -18,8 +18,8 @@ abstract class BaseCoroutineController<VB : ViewBinding, PS : BaseCoroutinePrese
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private fun <View> BaseCoroutinePresenter<View>.takeView(view: Any) = attachView(view as? View) private fun <View> BaseCoroutinePresenter<View>.takeView(view: Any) = attachView(view as? View)
override fun onDestroyView(view: View) { override fun onDestroy() {
super.onDestroyView(view) super.onDestroy()
presenter.onDestroy() presenter.onDestroy()
} }
} }

View file

@ -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<VB : ViewBinding, P : Presenter<*>>(val bundle: Bundle? = null) :
RxController<VB>(bundle),
PresenterFactory<P> {
private val delegate = NucleusConductorDelegate(this)
val presenter: P
get() = delegate.presenter!!
init {
addLifecycleListener(NucleusConductorLifecycleListener(delegate))
}
}

View file

@ -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<V> : RxPresenter<V>() {
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 <T> Observable<T>.subscribeFirst(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) =
compose(deliverFirst<T>()).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 <T> Observable<T>.subscribeLatestCache(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) =
compose(deliverLatestCache<T>()).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 <T> Observable<T>.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) =
compose(deliverReplay<T>()).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 <T> Observable<T>.subscribeWithView(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) =
compose(DeliverWithView<V, T>(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<View, T>(private val view: Observable<View>) : Observable.Transformer<T, Delivery<View, T>> {
override fun call(observable: Observable<T>): Observable<Delivery<View, T>> {
return observable
.materialize()
.filter { notification -> !notification.isOnCompleted }
.flatMap { notification ->
view.take(1).filter { it != null }.map { Delivery(it, notification) }
}
}
}
}

View file

@ -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<P : Presenter<*>>(private val factory: PresenterFactory<P>) {
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 <View> Presenter<View>.takeView(view: Any) = takeView(view as View)
fun onTakeView(view: Any) {
presenter?.takeView(view)
}
fun onDropView() {
presenter?.dropView()
}
fun onDestroy() {
presenter?.destroy()
}
}

View file

@ -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"
}
}

View file

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -39,12 +40,12 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>(
super.onCreate() super.onCreate()
presenterScope.launch { presenterScope.launch {
val extensionJob = async { val extensionJob = async {
extensionManager.findAvailableExtensionsAsync() extensionManager.findAvailableExtensions()
extensions = toItems( extensions = toItems(
Triple( Triple(
extensionManager.installedExtensions, extensionManager.installedExtensionsFlow.value,
extensionManager.untrustedExtensions, extensionManager.untrustedExtensionsFlow.value,
extensionManager.availableExtensions, extensionManager.availableExtensionsFlow.value,
), ),
) )
withContext(Dispatchers.Main) { controller?.setExtensions(extensions, false) } withContext(Dispatchers.Main) { controller?.setExtensions(extensions, false) }
@ -53,16 +54,16 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>(
listOf(migrationJob, extensionJob).awaitAll() listOf(migrationJob, extensionJob).awaitAll()
} }
presenterScope.launch { presenterScope.launch {
extensionManager.downloadRelay extensionManager.downloadRelay.asSharedFlow()
.collect { .collect {
if (it.first.startsWith("Finished")) { if (it.first.startsWith("Finished")) {
firstLoad = true firstLoad = true
currentDownloads.clear() currentDownloads.clear()
extensions = toItems( extensions = toItems(
Triple( Triple(
extensionManager.installedExtensions, extensionManager.installedExtensionsFlow.value,
extensionManager.untrustedExtensions, extensionManager.untrustedExtensionsFlow.value,
extensionManager.availableExtensions, extensionManager.availableExtensionsFlow.value,
), ),
) )
withUIContext { controller?.setExtensions(extensions) } withUIContext { controller?.setExtensions(extensions) }
@ -91,9 +92,9 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>(
presenterScope.launch { presenterScope.launch {
extensions = toItems( extensions = toItems(
Triple( Triple(
extensionManager.installedExtensions, extensionManager.installedExtensionsFlow.value,
extensionManager.untrustedExtensions, extensionManager.untrustedExtensionsFlow.value,
extensionManager.availableExtensions, extensionManager.availableExtensionsFlow.value,
), ),
) )
withContext(Dispatchers.Main) { controller?.setExtensions(extensions, false) } withContext(Dispatchers.Main) { controller?.setExtensions(extensions, false) }
@ -249,7 +250,7 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>(
fun updateExtension(extension: Extension.Installed) { fun updateExtension(extension: Extension.Installed) {
val availableExt = val availableExt =
extensionManager.availableExtensions.find { it.pkgName == extension.pkgName } ?: return extensionManager.availableExtensionsFlow.value.find { it.pkgName == extension.pkgName } ?: return
installExtension(availableExt) installExtension(availableExt)
} }
@ -265,7 +266,7 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>(
val intent = ExtensionInstallService.jobIntent( val intent = ExtensionInstallService.jobIntent(
context, context,
extensions.mapNotNull { extension -> extensions.mapNotNull { extension ->
extensionManager.availableExtensions.find { it.pkgName == extension.pkgName } extensionManager.availableExtensionsFlow.value.find { it.pkgName == extension.pkgName }
}, },
) )
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
@ -276,7 +277,9 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>(
} }
fun findAvailableExtensions() { fun findAvailableExtensions() {
extensionManager.findAvailableExtensions() presenterScope.launch {
extensionManager.findAvailableExtensions()
}
} }
fun trustSignature(signatureHash: String) { fun trustSignature(signatureHash: String) {

View file

@ -22,7 +22,7 @@ class ExtensionFilterController : SettingsController() {
val activeLangs = preferences.enabledLanguages().get() 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) })) .sortedWith(compareBy({ it !in activeLangs }, { LocaleHelper.getSourceDisplayName(it, context) }))
availableLangs.forEach { availableLangs.forEach {

View file

@ -36,7 +36,7 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getPreferenceKey import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.source.online.HttpSource 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.DSL
import eu.kanade.tachiyomi.ui.setting.onChange import eu.kanade.tachiyomi.ui.setting.onChange
import eu.kanade.tachiyomi.ui.setting.switchPreference import eu.kanade.tachiyomi.ui.setting.switchPreference
@ -57,7 +57,7 @@ import uy.kohesive.injekt.injectLazy
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
class ExtensionDetailsController(bundle: Bundle? = null) : class ExtensionDetailsController(bundle: Bundle? = null) :
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle), BaseCoroutineController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
PreferenceManager.OnDisplayPreferenceDialogListener, PreferenceManager.OnDisplayPreferenceDialogListener,
DialogPreference.TargetFragment { DialogPreference.TargetFragment {
@ -81,9 +81,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
override fun createBinding(inflater: LayoutInflater) = override fun createBinding(inflater: LayoutInflater) =
ExtensionDetailControllerBinding.inflate(inflater.cloneInContext(getPreferenceThemeContext())) ExtensionDetailControllerBinding.inflate(inflater.cloneInContext(getPreferenceThemeContext()))
override fun createPresenter(): ExtensionDetailsPresenter { override val presenter = ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!)
return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!)
}
override fun getTitle(): String? { override fun getTitle(): String? {
return resources?.getString(R.string.extension_info) return resources?.getString(R.string.extension_info)

View file

@ -1,35 +1,34 @@
package eu.kanade.tachiyomi.ui.extension.details package eu.kanade.tachiyomi.ui.extension.details
import android.os.Bundle
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
import rx.android.schedulers.AndroidSchedulers 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class ExtensionDetailsPresenter( class ExtensionDetailsPresenter(
val pkgName: String, val pkgName: String,
private val extensionManager: ExtensionManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(),
) : BasePresenter<ExtensionDetailsController>() { ) : BaseCoroutinePresenter<ExtensionDetailsController>() {
val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName } val extension = extensionManager.installedExtensionsFlow.value.find { it.pkgName == pkgName }
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
override fun onCreate() {
super.onCreate()
bindToUninstalledExtension() bindToUninstalledExtension()
} }
private fun bindToUninstalledExtension() { private fun bindToUninstalledExtension() {
extensionManager.getInstalledExtensionsObservable() extensionManager.installedExtensionsFlow
.skip(1) .drop(1)
.filter { extensions -> extensions.none { it.pkgName == pkgName } } .onEach { extensions ->
.map { Unit } extensions.filter { it.pkgName == pkgName }
.take(1) presenterScope.launchUI { controller?.onExtensionUninstalled() }
.observeOn(AndroidSchedulers.mainThread()) }
.subscribeFirst({ view, _ -> .launchIn(presenterScope)
view.onExtensionUninstalled()
},)
} }
fun uninstallExtension() { fun uninstallExtension() {

View file

@ -821,15 +821,15 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
} }
fun getExtensionUpdates(force: Boolean) { fun getExtensionUpdates(force: Boolean) {
if ((force && extensionManager.availableExtensions.isEmpty()) || if ((force && extensionManager.availableExtensionsFlow.value.isEmpty()) ||
Date().time >= preferences.lastExtCheck().get() + TimeUnit.HOURS.toMillis(6) Date().time >= preferences.lastExtCheck().get() + TimeUnit.HOURS.toMillis(6)
) { ) {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
extensionManager.findAvailableExtensionsAsync() extensionManager.findAvailableExtensions()
val pendingUpdates = ExtensionGithubApi().checkForUpdates( val pendingUpdates = ExtensionGithubApi().checkForUpdates(
this@MainActivity, this@MainActivity,
extensionManager.availableExtensions.takeIf { it.isNotEmpty() }, extensionManager.availableExtensionsFlow.value.takeIf { it.isNotEmpty() },
) )
preferences.extensionUpdatesCount().set(pendingUpdates.size) preferences.extensionUpdatesCount().set(pendingUpdates.size)
preferences.lastExtCheck().set(Date().time) preferences.lastExtCheck().set(Date().time)

View file

@ -110,7 +110,7 @@ class EditMangaDialog : DialogController {
languages.add("") languages.add("")
languages.addAll( languages.addAll(
extensionManager.availableExtensions.groupBy { it.lang }.keys extensionManager.availableExtensionsFlow.value.groupBy { it.lang }.keys
.sortedWith( .sortedWith(
compareBy( compareBy(
{ it !in activeLangs }, { it !in activeLangs },

View file

@ -54,7 +54,7 @@ abstract class BaseMigrationPresenter<T : BaseMigrationInterface>(
val header = SelectionHeader() val header = SelectionHeader()
val sourceGroup = library.groupBy { it.source } val sourceGroup = library.groupBy { it.source }
val sortOrder = PreferenceValues.MigrationSourceOrder.fromPreference(preferences) val sortOrder = PreferenceValues.MigrationSourceOrder.fromPreference(preferences)
val extensions = extensionManager.installedExtensions val extensions = extensionManager.installedExtensionsFlow.value
val obsoleteSources = val obsoleteSources =
extensions.filter { it.isObsolete }.map { it.sources }.flatten().map { it.id } extensions.filter { it.isObsolete }.map { it.sources }.flatten().map { it.id }

View file

@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.databinding.MigrationControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController
import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController
import eu.kanade.tachiyomi.ui.source.BrowseController 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.launchUI
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.view.activityBinding 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 eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -94,9 +92,7 @@ class MigrationController :
val item = adapter?.getItem(position) as? SourceItem ?: return val item = adapter?.getItem(position) as? SourceItem ?: return
launchUI { launchUI {
val manga = Injekt.get<DatabaseHelper>().getFavoriteMangas().asRxSingle().await( val manga = Injekt.get<DatabaseHelper>().getFavoriteMangas().executeAsBlocking()
Schedulers.io(),
)
val sourceMangas = val sourceMangas =
manga.asSequence().filter { it.source == item.source.id }.map { it.id!! }.toList() manga.asSequence().filter { it.source == item.source.id }.map { it.id!! }.toList()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

View file

@ -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.migration.manga.process.MigrationListController
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchCardAdapter import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchCardAdapter
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchController 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.activityBinding
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -51,9 +50,7 @@ class SearchController(
bundle.getLongArray(SOURCES) ?: LongArray(0), bundle.getLongArray(SOURCES) ?: LongArray(0),
) )
override fun createPresenter(): GlobalSearchPresenter { override val presenter = SearchPresenter(initialQuery, manga!!, sources = sources)
return SearchPresenter(initialQuery, manga!!, sources = sources)
}
override fun onMangaClick(manga: Manga) { override fun onMangaClick(manga: Manga) {
if (targetController is MigrationListController) { if (targetController is MigrationListController) {

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Environment 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.database.models.Category
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
import eu.kanade.tachiyomi.util.system.getFilePicker
import eu.kanade.tachiyomi.util.system.withOriginalWidth import eu.kanade.tachiyomi.util.system.withOriginalWidth
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -167,13 +165,8 @@ class SettingsDownloadController : SettingsController() {
preferences.downloadsDirectory().set(path.toString()) preferences.downloadsDirectory().set(path.toString())
} }
fun customDirectorySelected(currentDir: String) { fun customDirectorySelected() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), DOWNLOAD_DIR)
try {
startActivityForResult(intent, DOWNLOAD_DIR)
} catch (e: ActivityNotFoundException) {
startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR)
}
} }
class DownloadDirectoriesDialog(val controller: SettingsDownloadController) : class DownloadDirectoriesDialog(val controller: SettingsDownloadController) :
@ -193,7 +186,7 @@ class SettingsDownloadController : SettingsController() {
setTitle(R.string.download_location) setTitle(R.string.download_location)
setSingleChoiceItems(items.toTypedArray(), selectedIndex) { dialog, position -> setSingleChoiceItems(items.toTypedArray(), selectedIndex) { dialog, position ->
if (position == externalDirs.lastIndex) { if (position == externalDirs.lastIndex) {
controller.customDirectorySelected(currentDir) controller.customDirectorySelected()
} else { } else {
controller.predefinedDirectorySelected(items[position]) controller.predefinedDirectorySelected(items[position])
} }

View file

@ -93,10 +93,7 @@ class SettingsMainController : SettingsController(), FloatingSearchInterface {
} }
override fun onActionViewExpand(item: MenuItem?) { 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) { private fun navigateTo(controller: Controller) {

View file

@ -11,19 +11,20 @@ import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.SettingsSearchControllerBinding import eu.kanade.tachiyomi.databinding.SettingsSearchControllerBinding
import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface 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.main.FloatingSearchInterface
import eu.kanade.tachiyomi.ui.setting.SettingsController import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.util.view.activityBinding import eu.kanade.tachiyomi.util.view.activityBinding
import eu.kanade.tachiyomi.util.view.liftAppbarWith import eu.kanade.tachiyomi.util.view.liftAppbarWith
import eu.kanade.tachiyomi.util.view.withFadeTransaction 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. * This controller shows and manages the different search result in settings search.
* [SettingsSearchAdapter.OnTitleClickListener] called when preference is clicked in settings search * [SettingsSearchAdapter.OnTitleClickListener] called when preference is clicked in settings search
*/ */
class SettingsSearchController : class SettingsSearchController :
NucleusController<SettingsSearchControllerBinding, SettingsSearchPresenter>(), BaseController<SettingsSearchControllerBinding>(),
FloatingSearchInterface, FloatingSearchInterface,
SmallToolbarInterface, SmallToolbarInterface,
SettingsSearchAdapter.OnTitleClickListener { SettingsSearchAdapter.OnTitleClickListener {
@ -33,6 +34,7 @@ class SettingsSearchController :
*/ */
private var adapter: SettingsSearchAdapter? = null private var adapter: SettingsSearchAdapter? = null
private var searchView: SearchView? = null private var searchView: SearchView? = null
var query: String = ""
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
@ -40,18 +42,7 @@ class SettingsSearchController :
override fun createBinding(inflater: LayoutInflater) = SettingsSearchControllerBinding.inflate(inflater) override fun createBinding(inflater: LayoutInflater) = SettingsSearchControllerBinding.inflate(inflater)
override fun getTitle(): String { override fun getTitle(): String = query
return presenter.query
}
/**
* Create the [SettingsSearchPresenter] used in controller.
*
* @return instance of [SettingsSearchPresenter]
*/
override fun createPresenter(): SettingsSearchPresenter {
return SettingsSearchPresenter()
}
/** /**
* Adds items to the options menu. * Adds items to the options menu.
@ -80,7 +71,7 @@ class SettingsSearchController :
override fun onQueryTextChange(newText: String?): Boolean { override fun onQueryTextChange(newText: String?): Boolean {
if (!newText.isNullOrBlank()) { if (!newText.isNullOrBlank()) {
lastSearch = newText query = newText
} }
setItems(getResultSet(newText)) setItems(getResultSet(newText))
return false return false
@ -88,7 +79,7 @@ class SettingsSearchController :
}, },
) )
searchView?.setQuery(lastSearch, true) searchView?.setQuery(query, true)
} }
override fun onActionViewCollapse(item: MenuItem?) { override fun onActionViewCollapse(item: MenuItem?) {
@ -151,13 +142,9 @@ class SettingsSearchController :
*/ */
override fun onTitleClick(ctrl: SettingsController) { override fun onTitleClick(ctrl: SettingsController) {
searchView?.query.let { searchView?.query.let {
lastSearch = it.toString() query = it.toString()
} }
router.pushController(ctrl.withFadeTransaction()) router.pushController(ctrl.withFadeTransaction())
} }
companion object {
var lastSearch = ""
}
} }

View file

@ -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<SettingsSearchController>() {
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)
}
}

View file

@ -29,7 +29,7 @@ import eu.kanade.tachiyomi.source.icon
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource 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.FloatingSearchInterface
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.main.SearchActivity import eu.kanade.tachiyomi.ui.main.SearchActivity
@ -63,7 +63,7 @@ import kotlin.math.roundToInt
* Controller to manage the catalogues available in the app. * Controller to manage the catalogues available in the app.
*/ */
open class BrowseSourceController(bundle: Bundle) : open class BrowseSourceController(bundle: Bundle) :
NucleusController<BrowseSourceControllerBinding, BrowseSourcePresenter>(bundle), BaseCoroutineController<BrowseSourceControllerBinding, BrowseSourcePresenter>(bundle),
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
FloatingSearchInterface, FloatingSearchInterface,
@ -146,13 +146,11 @@ open class BrowseSourceController(bundle: Bundle) :
// return presenter.source.icon() // return presenter.source.icon()
// } // }
override fun createPresenter(): BrowseSourcePresenter { override val presenter = BrowseSourcePresenter(
return BrowseSourcePresenter( args.getLong(SOURCE_ID_KEY),
args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY),
args.getString(SEARCH_QUERY_KEY), args.getBoolean(USE_LATEST_KEY),
args.getBoolean(USE_LATEST_KEY), )
)
}
override fun createBinding(inflater: LayoutInflater) = BrowseSourceControllerBinding.inflate(inflater) 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.isVisible = presenter.sourceFilters.isNotEmpty()
binding.fab.setOnClickListener { showFilters() } binding.fab.setOnClickListener { showFilters() }
binding.progress.isVisible = true
activityBinding?.appBar?.y = 0f activityBinding?.appBar?.y = 0f
activityBinding?.appBar?.updateAppBarAfterY(recycler) activityBinding?.appBar?.updateAppBarAfterY(recycler)
activityBinding?.appBar?.lockYPos = true activityBinding?.appBar?.lockYPos = true
@ -178,6 +175,11 @@ open class BrowseSourceController(bundle: Bundle) :
} }
return return
} }
if (presenter.items.isNotEmpty()) {
onAddPage(1, presenter.items)
} else {
binding.progress.isVisible = true
}
requestFilePermissionsSafe(301, preferences, presenter.source is LocalSource) requestFilePermissionsSafe(301, preferences, presenter.source is LocalSource)
} }
@ -278,10 +280,9 @@ open class BrowseSourceController(bundle: Bundle) :
val searchView = activityBinding?.searchToolbar?.searchView val searchView = activityBinding?.searchToolbar?.searchView
activityBinding?.searchToolbar?.setQueryHint("", !isBehindGlobalSearch && presenter.query.isBlank()) activityBinding?.searchToolbar?.setQueryHint("", !isBehindGlobalSearch && presenter.query.isBlank())
val query = presenter.query if (presenter.query.isNotBlank()) {
if (query.isNotBlank()) {
searchItem?.expandActionView() searchItem?.expandActionView()
searchView?.setQuery(query, true) searchView?.setQuery(presenter.query, true)
searchView?.clearFocus() searchView?.clearFocus()
} else if (activityBinding?.searchToolbar?.isSearchExpanded == true) { } else if (activityBinding?.searchToolbar?.isSearchExpanded == true) {
searchItem?.collapseActionView() searchItem?.collapseActionView()
@ -516,7 +517,7 @@ open class BrowseSourceController(bundle: Bundle) :
showProgressBar() showProgressBar()
adapter?.clear() adapter?.clear()
presenter.restartPager(newQuery, presenter.sourceFilters) presenter.restartPager(newQuery)
updatePopLatestIcons() updatePopLatestIcons()
} }

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.source.browse package eu.kanade.tachiyomi.ui.source.browse
import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper 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.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga 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.CheckboxItem
import eu.kanade.tachiyomi.ui.source.filter.CheckboxSectionItem import eu.kanade.tachiyomi.ui.source.filter.CheckboxSectionItem
import eu.kanade.tachiyomi.ui.source.filter.GroupItem 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.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -54,7 +50,7 @@ open class BrowseSourcePresenter(
val db: DatabaseHelper = Injekt.get(), val db: DatabaseHelper = Injekt.get(),
val prefs: PreferencesHelper = Injekt.get(), val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
) : BasePresenter<BrowseSourceController>() { ) : BaseCoroutinePresenter<BrowseSourceController>() {
/** /**
* Selected source. * Selected source.
@ -66,6 +62,10 @@ open class BrowseSourcePresenter(
var filtersChanged = false var filtersChanged = false
var items = mutableListOf<BrowseSourceItem>()
val page: Int
get() = pager.currentPage
/** /**
* Modifiable list of filters. * Modifiable list of filters.
*/ */
@ -87,39 +87,24 @@ open class BrowseSourcePresenter(
* Pager containing a list of manga results. * Pager containing a list of manga results.
*/ */
private lateinit var pager: Pager private lateinit var pager: Pager
private var pagerJob: Job? = null
/**
* Subscription for the pager.
*/
private var pagerSubscription: Subscription? = null
/** /**
* Subscription for one request from the pager. * Subscription for one request from the pager.
*/ */
private var nextPageJob: Job? = null private var nextPageJob: Job? = null
init { var query = searchQuery ?: ""
query = searchQuery ?: ""
}
override fun onCreate(savedState: Bundle?) { override fun onCreate() {
super.onCreate(savedState) super.onCreate()
if (!::pager.isInitialized) {
source = sourceManager.get(sourceId) as? CatalogueSource ?: return
source = sourceManager.get(sourceId) as? CatalogueSource ?: return sourceFilters = source.getFilterList()
filtersChanged = false
sourceFilters = source.getFilterList() restartPager()
filtersChanged = false
if (savedState != null) {
query = savedState.getString(::query.name, "")
} }
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 browseAsList = prefs.browseAsList()
val sourceListType = prefs.libraryLayout() val sourceListType = prefs.libraryLayout()
val outlineCovers = prefs.outlineOnCovers() val outlineCovers = prefs.outlineOnCovers()
items.clear()
// Prepare the pager. // Prepare the pager.
pagerSubscription?.let { remove(it) } pagerJob?.cancel()
pagerSubscription = pager.results() pagerJob = presenterScope.launchIO {
.observeOn(Schedulers.io()) pager.results().onEach { (page, second) ->
.map { (first, second) -> try {
first to second val mangas = second
.map { networkToLocalManga(it, sourceId) } .map { networkToLocalManga(it, sourceId) }
.filter { !prefs.hideInLibraryItems().get() || !it.favorite } .filter { !prefs.hideInLibraryItems().get() || !it.favorite }
} initializeMangas(mangas)
.doOnNext { initializeMangas(it.second) } val items = mangas.map {
.map { (first, second) -> first to second.map { BrowseSourceItem(it, browseAsList, sourceListType, outlineCovers) } } BrowseSourceItem(it, browseAsList, sourceListType, outlineCovers)
.observeOn(AndroidSchedulers.mainThread()) }
.subscribeReplay( this@BrowseSourcePresenter.items.addAll(items)
{ view, (page, mangas) -> withUIContext { controller?.onAddPage(page, items) }
view.onAddPage(page, mangas) } catch (error: Exception) {
},
{ _, error ->
Timber.e(error) Timber.e(error)
}, }
) }.collect()
}
// Request first page. // Request first page.
requestNext() requestNext()
@ -173,14 +158,11 @@ open class BrowseSourcePresenter(
if (!hasNextPage()) return if (!hasNextPage()) return
nextPageJob?.cancel() nextPageJob?.cancel()
nextPageJob = launchIO { nextPageJob = presenterScope.launchIO {
try { try {
pager.requestNextPage() pager.requestNextPage()
} catch (e: Throwable) { } catch (e: Throwable) {
withUIContext { withUIContext { controller?.onAddPageError(e) }
@Suppress("DEPRECATION")
view?.onAddPageError(e)
}
} }
} }
} }
@ -229,10 +211,7 @@ open class BrowseSourcePresenter(
.filter { it.thumbnail_url == null && !it.initialized } .filter { it.thumbnail_url == null && !it.initialized }
.map { getMangaDetails(it) } .map { getMangaDetails(it) }
.onEach { .onEach {
withUIContext { withUIContext { controller?.onMangaInitialized(it) }
@Suppress("DEPRECATION")
view?.onMangaInitialized(it)
}
} }
.catch { e -> Timber.e(e) } .catch { e -> Timber.e(e) }
.collect() .collect()

View file

@ -1,9 +1,10 @@
package eu.kanade.tachiyomi.ui.source.browse 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.MangasPage
import eu.kanade.tachiyomi.source.model.SManga 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) * A general pager for source requests (latest updates, popular, search)
@ -13,18 +14,18 @@ abstract class Pager(var currentPage: Int = 1) {
var hasNextPage = true var hasNextPage = true
private set private set
protected val results: PublishRelay<Pair<Int, List<SManga>>> = PublishRelay.create() protected val results = MutableSharedFlow<Pair<Int, List<SManga>>>()
fun results(): Observable<Pair<Int, List<SManga>>> { fun results(): SharedFlow<Pair<Int, List<SManga>>> {
return results.asObservable() return results.asSharedFlow()
} }
abstract suspend fun requestNextPage() abstract suspend fun requestNextPage()
fun onPageReceived(mangasPage: MangasPage) { suspend fun onPageReceived(mangasPage: MangasPage) {
val page = currentPage val page = currentPage
currentPage++ currentPage++
hasNextPage = mangasPage.hasNextPage && mangasPage.mangas.isNotEmpty() hasNextPage = mangasPage.hasNextPage && mangasPage.mangas.isNotEmpty()
results.call(Pair(page, mangasPage.mangas)) results.emit(Pair(page, mangasPage.mangas))
} }
} }

View file

@ -17,7 +17,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.SourceGlobalSearchControllerBinding import eu.kanade.tachiyomi.databinding.SourceGlobalSearchControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface 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.FloatingSearchInterface
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.main.SearchActivity import eu.kanade.tachiyomi.ui.main.SearchActivity
@ -42,7 +42,7 @@ open class GlobalSearchController(
protected val initialQuery: String? = null, protected val initialQuery: String? = null,
val extensionFilter: String? = null, val extensionFilter: String? = null,
bundle: Bundle? = null, bundle: Bundle? = null,
) : NucleusController<SourceGlobalSearchControllerBinding, GlobalSearchPresenter>(bundle), ) : BaseCoroutineController<SourceGlobalSearchControllerBinding, GlobalSearchPresenter>(bundle),
FloatingSearchInterface, FloatingSearchInterface,
SmallToolbarInterface, SmallToolbarInterface,
GlobalSearchAdapter.OnTitleClickListener, GlobalSearchAdapter.OnTitleClickListener,
@ -78,14 +78,7 @@ open class GlobalSearchController(
return customTitle ?: presenter.query return customTitle ?: presenter.query
} }
/** override val presenter = GlobalSearchPresenter(initialQuery, extensionFilter)
* Create the [GlobalSearchPresenter] used in controller.
*
* @return instance of [GlobalSearchPresenter]
*/
override fun createPresenter(): GlobalSearchPresenter {
return GlobalSearchPresenter(initialQuery, extensionFilter)
}
override fun onTitleClick(source: CatalogueSource) { override fun onTitleClick(source: CatalogueSource) {
preferences.lastUsedCatalogueSource().set(source.id) preferences.lastUsedCatalogueSource().set(source.id)
@ -108,7 +101,7 @@ open class GlobalSearchController(
/** /**
* Called when manga in global search is long clicked. * 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) { override fun onMangaLongClick(position: Int, adapter: GlobalSearchCardAdapter) {
val manga = adapter.getItem(position)?.manga ?: return val manga = adapter.getItem(position)?.manga ?: return

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.source.globalsearch package eu.kanade.tachiyomi.ui.source.globalsearch
import android.os.Bundle
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga 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.SourceManager
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga 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.browse.BrowseSourcePresenter import eu.kanade.tachiyomi.util.system.awaitSingle
import eu.kanade.tachiyomi.util.system.runAsObservable import eu.kanade.tachiyomi.util.system.launchIO
import rx.Observable import eu.kanade.tachiyomi.util.system.launchUI
import rx.Subscription import eu.kanade.tachiyomi.util.system.withUIContext
import rx.android.schedulers.AndroidSchedulers import kotlinx.coroutines.Job
import rx.schedulers.Schedulers import kotlinx.coroutines.flow.MutableSharedFlow
import rx.subjects.PublishSubject import kotlinx.coroutines.flow.launchIn
import timber.log.Timber 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -43,56 +45,43 @@ open class GlobalSearchPresenter(
val db: DatabaseHelper = Injekt.get(), val db: DatabaseHelper = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
) : BasePresenter<GlobalSearchController>() { ) : BaseCoroutinePresenter<GlobalSearchController>() {
/** /**
* Enabled sources. * Enabled sources.
*/ */
val sources by lazy { getSourcesToQuery() } val sources by lazy { getSourcesToQuery() }
/** private var fetchSourcesJob: Job? = null
* Fetches the different sources by user settings.
*/
private var fetchSourcesSubscription: Subscription? = null
private var loadTime = hashMapOf<Long, Long>() private var loadTime = hashMapOf<Long, Long>()
/** var query = ""
* Subject which fetches image of given manga.
*/
private val fetchImageSubject = PublishSubject.create<Pair<List<Manga>, Source>>()
/** private val fetchImageFlow = MutableSharedFlow<Pair<List<Manga>, Source>>()
* Subscription for fetching images of manga.
*/ private var fetchImageJob: Job? = null
private var fetchImageSubscription: Subscription? = null
private val extensionManager: ExtensionManager by injectLazy() private val extensionManager: ExtensionManager by injectLazy()
private var extensionFilter: String? = null private var extensionFilter: String? = null
override fun onCreate(savedState: Bundle?) { var items: List<GlobalSearchItem> = emptyList()
super.onCreate(savedState)
extensionFilter = savedState?.getString(GlobalSearchPresenter::extensionFilter.name) private val semaphore = Semaphore(5)
?: initialExtensionFilter
// Perform a search with previous or initial state override fun onCreate() {
search( super.onCreate()
savedState?.getString(BrowseSourcePresenter::query.name) ?: initialQuery.orEmpty(),
)
}
override fun onDestroy() { extensionFilter = initialExtensionFilter
fetchSourcesSubscription?.unsubscribe()
fetchImageSubscription?.unsubscribe()
super.onDestroy()
}
override fun onSave(state: Bundle) { if (items.isEmpty()) {
state.putString(BrowseSourcePresenter::query.name, query) // Perform a search with previous or initial state
state.putString(GlobalSearchPresenter::extensionFilter.name, extensionFilter) search(initialQuery.orEmpty())
super.onSave(state) }
presenterScope.launchUI {
controller?.setItems(items)
}
} }
/** /**
@ -126,7 +115,7 @@ open class GlobalSearchPresenter(
} }
val languages = preferences.enabledLanguages().get() val languages = preferences.enabledLanguages().get()
val filterSources = extensionManager.installedExtensions val filterSources = extensionManager.installedExtensionsFlow.value
.filter { it.pkgName == filter } .filter { it.pkgName == filter }
.flatMap { it.sources } .flatMap { it.sources }
.filter { it.lang in languages } .filter { it.lang in languages }
@ -174,70 +163,49 @@ open class GlobalSearchPresenter(
// Create items with the initial state // Create items with the initial state
val initialItems = sources.map { createCatalogueSearchItem(it, null) } val initialItems = sources.map { createCatalogueSearchItem(it, null) }
var items = initialItems items = initialItems
val pinnedSourceIds = preferences.pinnedCatalogues().get() val pinnedSourceIds = preferences.pinnedCatalogues().get()
fetchSourcesSubscription?.unsubscribe() fetchSourcesJob?.cancel()
fetchSourcesSubscription = Observable.from(sources).flatMap( fetchSourcesJob = presenterScope.launch {
{ source -> sources.map { source ->
Observable.defer { source.fetchSearchManga(1, query, source.getFilterList()) } launch mainLaunch@{
.subscribeOn(Schedulers.io()).onErrorReturn { semaphore.withPermit {
MangasPage( if (this@GlobalSearchPresenter.items.find { it.source == source }?.results != null) {
emptyList(), return@mainLaunch
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,
)
} }
} // Convert to local manga. val mangas = try {
.doOnNext { fetchImage(it, source) } // Load manga covers. source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle()
.map { } catch (error: Exception) {
if (it.isNotEmpty() && !loadTime.containsKey(source.id)) { MangasPage(emptyList(), false)
}
.mangas.take(10)
.map { networkToLocalManga(it, source.id) }
fetchImage(mangas, source)
if (mangas.isNotEmpty() && !loadTime.containsKey(source.id)) {
loadTime[source.id] = Date().time loadTime[source.id] = Date().time
} }
createCatalogueSearchItem( val result = createCatalogueSearchItem(
source, 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. * @param manga the list of manga to initialize.
*/ */
private fun fetchImage(manga: List<Manga>, source: Source) { private fun fetchImage(manga: List<Manga>, 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. * Subscribes to the initializer of manga details and updates the view if needed.
*/ */
private fun initializeFetchImageSubscription() { private fun initializeFetchImageSubscription() {
fetchImageSubscription?.unsubscribe() fetchImageJob?.cancel()
fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io()) fetchImageJob = fetchImageFlow.onEach { (mangaList, source) ->
.flatMap { (mangaList, source) -> mangaList
Observable.from(mangaList) .filter { it.thumbnail_url == null && !it.initialized }
.filter { it.thumbnail_url == null && !it.initialized } .map {
.map { Pair(it, source) } presenterScope.launchIO {
.concatMap { runAsObservable { getMangaDetails(it.first, it.second) } } val manga = getMangaDetails(it, source)
.map { Pair(source as CatalogueSource, it) } withUIContext { controller?.onMangaInitialized(source as CatalogueSource, manga) }
} }
.onBackpressureBuffer() }
.observeOn(AndroidSchedulers.mainThread()) }.launchIn(presenterScope)
.subscribe(
{ (source, manga) ->
@Suppress("DEPRECATION")
view?.onMangaInitialized(source, manga)
},
{ error ->
Timber.e(error)
},
)
} }
/** /**

View file

@ -36,12 +36,10 @@ import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.net.toUri import androidx.core.net.toUri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.nononsenseapps.filepicker.FilePickerActivity
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@ -83,27 +81,6 @@ inline fun Context.notification(channelId: String, func: NotificationCompat.Buil
return builder.build() 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. * Returns the color for the given attribute.
* *

View file

@ -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<File> {
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)
}
}
}

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.89,2 1.99,2H15v-8h5V8L14,2zM13,9V3.5L18.5,9H13zM17,21.66V16h5.66v2h-2.24l2.95,2.95l-1.41,1.41L19,19.41l0,2.24H17z"/>
</vector>

View file

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/nnf_item_container"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeight"
android:background="?selectableItemBackground"
android:focusable="true"
android:minHeight="?android:listPreferredItemHeight"
android:nextFocusLeft="@+id/nnf_button_cancel"
android:nextFocusRight="@+id/nnf_button_ok"
android:orientation="horizontal">
<ImageView
android:id="@+id/item_icon"
android:layout_width="?android:listPreferredItemHeight"
android:layout_height="?android:listPreferredItemHeight"
android:adjustViewBounds="true"
android:scaleType="center"
android:src="@drawable/nnf_ic_file_folder"
android:tint="?attr/colorSecondary"
android:visibility="visible"
tools:ignore="ContentDescription"/>
<TextView
android:id="@android:id/text1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:padding="8dp"
android:singleLine="true"
android:text="@string/nnf_name"/>
</LinearLayout>

View file

@ -384,21 +384,4 @@
<item name="android:textSize">13sp</item> <item name="android:textSize">13sp</item>
</style> </style>
<!--===-->
<!--OLD-->
<!--===-->
<style name="FilePickerTheme" parent="NNF_BaseTheme.Light">
<item name="colorPrimary">@color/primaryTachiyomi</item>
<item name="colorAccent">@color/secondaryTachiyomi</item>
<item name="colorButtonNormal">@color/primaryTachiyomi</item>
<item name="android:textSize">14sp</item>
<item name="alertDialogTheme">@style/FilePickerAlertDialogTheme</item>
<item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
</style>
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert"/>
</resources> </resources>