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.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")

View file

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

View file

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

View file

@ -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),
)

View file

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

View file

@ -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)

View file

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

View file

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

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.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())
}

View file

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

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")
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()
}
}

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.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) {

View file

@ -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 {

View file

@ -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)

View file

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

View file

@ -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)

View file

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

View file

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

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.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) {

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.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) {

View file

@ -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])
}

View file

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

View file

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

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.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()
}

View file

@ -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()

View file

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

View file

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

View file

@ -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)
}
/**

View file

@ -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.
*

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>
</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>