mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
Remove some old libraries
And converting 4 screens into coroutine MVPs
This commit is contained in:
parent
28dc7bd738
commit
6a0d161793
39 changed files with 372 additions and 817 deletions
|
@ -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")
|
||||
|
|
|
@ -127,10 +127,6 @@
|
|||
android:configChanges="uiMode|orientation|screenSize"/>
|
||||
<activity
|
||||
android:name=".ui.security.BiometricActivity" />
|
||||
<activity
|
||||
android:name=".widget.CustomLayoutPickerActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/FilePickerTheme" />
|
||||
<activity
|
||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||
android:label="MyAnimeList"
|
||||
|
|
|
@ -39,8 +39,7 @@ class AppModule(val app: Application) : InjektModule {
|
|||
|
||||
addSingletonFactory { JavaScriptEngine(app) }
|
||||
|
||||
addSingletonFactory { SourceManager(app).also { get<ExtensionManager>().init(it) } }
|
||||
|
||||
addSingletonFactory { SourceManager(app, get()) }
|
||||
addSingletonFactory { ExtensionManager(app) }
|
||||
|
||||
addSingletonFactory { DownloadManager(app) }
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -81,7 +81,7 @@ class ExtensionInstallService(
|
|||
instance = this
|
||||
|
||||
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
|
||||
} ?: return@filter false
|
||||
installedExt.versionCode < it.versionCode || installedExt.libVersion < it.libVersion
|
||||
|
|
|
@ -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<String, Drawable>()
|
||||
|
||||
val downloadRelay
|
||||
get() = installer.downloadsStateFlow
|
||||
|
||||
|
@ -66,27 +68,28 @@ class ExtensionManager(
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
var installedExtensions = emptyList<Extension.Installed>()
|
||||
private set(value) {
|
||||
field = value
|
||||
installedExtensionsRelay.call(value)
|
||||
downloadRelay.tryEmit("Finished/Installed/${value.size}" to (InstallStep.Done to null))
|
||||
}
|
||||
// private var installedExtensions = emptyList<Extension.Installed>()
|
||||
// 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<Extension.Available>>()
|
||||
|
||||
/**
|
||||
* 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 val _availableExtensionsFlow = MutableStateFlow(emptyList<Extension.Available>())
|
||||
val availableExtensionsFlow = _availableExtensionsFlow.asStateFlow()
|
||||
|
||||
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.
|
||||
*/
|
||||
var untrustedExtensions = emptyList<Extension.Untrusted>()
|
||||
private set(value) {
|
||||
field = value
|
||||
untrustedExtensionsRelay.call(value)
|
||||
downloadRelay.tryEmit("Finished/Untrusted/${value.size}" to (InstallStep.Done to null))
|
||||
}
|
||||
// var untrustedExtensions = emptyList<Extension.Untrusted>()
|
||||
// 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<LoadResult.Success>()
|
||||
.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>()
|
||||
.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 {
|
||||
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() {
|
||||
launchNow {
|
||||
availableExtensions = try {
|
||||
api.findExtensions()
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
suspend fun findAvailableExtensions() {
|
||||
val extensions: List<Extension.Available> = try {
|
||||
api.findExtensions()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
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() {
|
||||
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)
|
||||
|
|
|
@ -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")}",
|
||||
)
|
||||
|
|
|
@ -46,7 +46,7 @@ sealed class Extension {
|
|||
override val hasChangelog: Boolean,
|
||||
val apkName: String,
|
||||
val iconUrl: String,
|
||||
val sources: List<AvailableSource>? = null,
|
||||
val sources: List<AvailableSource>,
|
||||
) : Extension()
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -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<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(
|
||||
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<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? {
|
||||
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<HttpSource>()
|
||||
fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance<HttpSource>()
|
||||
|
||||
fun getCatalogueSources() = sourcesMap.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),
|
||||
)
|
||||
fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance<CatalogueSource>()
|
||||
|
||||
@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<SManga> {
|
||||
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<List<SChapter>> {
|
||||
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<List<Page>> {
|
||||
return Observable.error(getSourceNotInstalledException())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -18,8 +18,8 @@ abstract class BaseCoroutineController<VB : ViewBinding, PS : BaseCoroutinePrese
|
|||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <View> BaseCoroutinePresenter<View>.takeView(view: Any) = attachView(view as? View)
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
presenter.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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<ExtensionBottomSheet>(
|
|||
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<ExtensionBottomSheet>(
|
|||
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<ExtensionBottomSheet>(
|
|||
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<ExtensionBottomSheet>(
|
|||
|
||||
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<ExtensionBottomSheet>(
|
|||
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<ExtensionBottomSheet>(
|
|||
}
|
||||
|
||||
fun findAvailableExtensions() {
|
||||
extensionManager.findAvailableExtensions()
|
||||
presenterScope.launch {
|
||||
extensionManager.findAvailableExtensions()
|
||||
}
|
||||
}
|
||||
|
||||
fun trustSignature(signatureHash: String) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
|
||||
BaseCoroutineController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(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)
|
||||
|
|
|
@ -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<ExtensionDetailsController>() {
|
||||
) : BaseCoroutinePresenter<ExtensionDetailsController>() {
|
||||
|
||||
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() {
|
||||
|
|
|
@ -821,15 +821,15 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), 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)
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -54,7 +54,7 @@ abstract class BaseMigrationPresenter<T : BaseMigrationInterface>(
|
|||
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 }
|
||||
|
||||
|
|
|
@ -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<DatabaseHelper>().getFavoriteMangas().asRxSingle().await(
|
||||
Schedulers.io(),
|
||||
)
|
||||
val manga = Injekt.get<DatabaseHelper>().getFavoriteMangas().executeAsBlocking()
|
||||
val sourceMangas =
|
||||
manga.asSequence().filter { it.source == item.source.id }.map { it.id!! }.toList()
|
||||
withContext(Dispatchers.Main) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<SettingsSearchControllerBinding, SettingsSearchPresenter>(),
|
||||
BaseController<SettingsSearchControllerBinding>(),
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<BrowseSourceControllerBinding, BrowseSourcePresenter>(bundle),
|
||||
BaseCoroutineController<BrowseSourceControllerBinding, BrowseSourcePresenter>(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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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<BrowseSourceController>() {
|
||||
) : BaseCoroutinePresenter<BrowseSourceController>() {
|
||||
|
||||
/**
|
||||
* Selected source.
|
||||
|
@ -66,6 +62,10 @@ open class BrowseSourcePresenter(
|
|||
|
||||
var filtersChanged = false
|
||||
|
||||
var items = mutableListOf<BrowseSourceItem>()
|
||||
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 ?: ""
|
||||
}
|
||||
var query = searchQuery ?: ""
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (!::pager.isInitialized) {
|
||||
source = sourceManager.get(sourceId) as? CatalogueSource ?: return
|
||||
|
||||
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()
|
||||
|
|
|
@ -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<Pair<Int, List<SManga>>> = PublishRelay.create()
|
||||
protected val results = MutableSharedFlow<Pair<Int, List<SManga>>>()
|
||||
|
||||
fun results(): Observable<Pair<Int, List<SManga>>> {
|
||||
return results.asObservable()
|
||||
fun results(): SharedFlow<Pair<Int, List<SManga>>> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<SourceGlobalSearchControllerBinding, GlobalSearchPresenter>(bundle),
|
||||
) : BaseCoroutineController<SourceGlobalSearchControllerBinding, GlobalSearchPresenter>(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
|
||||
|
|
|
@ -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<GlobalSearchController>() {
|
||||
) : BaseCoroutinePresenter<GlobalSearchController>() {
|
||||
|
||||
/**
|
||||
* 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<Long, Long>()
|
||||
|
||||
/**
|
||||
* Subject which fetches image of given manga.
|
||||
*/
|
||||
private val fetchImageSubject = PublishSubject.create<Pair<List<Manga>, Source>>()
|
||||
var query = ""
|
||||
|
||||
/**
|
||||
* Subscription for fetching images of manga.
|
||||
*/
|
||||
private var fetchImageSubscription: Subscription? = null
|
||||
private val fetchImageFlow = MutableSharedFlow<Pair<List<Manga>, 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<GlobalSearchItem> = 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
|
||||
}
|
||||
} // Convert to local manga.
|
||||
.doOnNext { fetchImage(it, source) } // Load manga covers.
|
||||
.map {
|
||||
if (it.isNotEmpty() && !loadTime.containsKey(source.id)) {
|
||||
val mangas = try {
|
||||
source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle()
|
||||
} catch (error: Exception) {
|
||||
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
|
||||
}
|
||||
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<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.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
5
app/src/main/res/drawable/ic_file_open_24dp.xml
Normal file
5
app/src/main/res/drawable/ic_file_open_24dp.xml
Normal 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>
|
|
@ -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>
|
|
@ -384,21 +384,4 @@
|
|||
<item name="android:textSize">13sp</item>
|
||||
</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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue