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.browser:browser:1.5.0")
|
||||||
implementation("androidx.biometric:biometric:1.1.0")
|
implementation("androidx.biometric:biometric:1.1.0")
|
||||||
implementation("androidx.palette:palette:1.0.0")
|
implementation("androidx.palette:palette:1.0.0")
|
||||||
implementation("androidx.core:core-ktx:1.9.0")
|
implementation("androidx.activity:activity-ktx:1.7.0-rc01")
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
|
implementation("androidx.core:core-ktx:1.10.0-rc01")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.0")
|
||||||
implementation("com.google.android.flexbox:flexbox:3.0.0")
|
implementation("com.google.android.flexbox:flexbox:3.0.0")
|
||||||
implementation("androidx.window:window:1.0.0")
|
implementation("androidx.window:window:1.0.0")
|
||||||
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
|
|
||||||
|
@ -181,7 +183,6 @@ dependencies {
|
||||||
implementation("io.reactivex:rxandroid:1.2.1")
|
implementation("io.reactivex:rxandroid:1.2.1")
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
|
||||||
|
|
||||||
// Coroutines
|
// Coroutines
|
||||||
implementation("com.fredporciuncula:flow-preferences:1.6.0")
|
implementation("com.fredporciuncula:flow-preferences:1.6.0")
|
||||||
|
@ -224,9 +225,6 @@ dependencies {
|
||||||
|
|
||||||
implementation("com.google.android.gms:play-services-gcm:17.0.0")
|
implementation("com.google.android.gms:play-services-gcm:17.0.0")
|
||||||
|
|
||||||
// Changelog
|
|
||||||
implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0")
|
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation("androidx.sqlite:sqlite-ktx:2.3.0")
|
implementation("androidx.sqlite:sqlite-ktx:2.3.0")
|
||||||
implementation("com.github.requery:sqlite-android:3.39.2")
|
implementation("com.github.requery:sqlite-android:3.39.2")
|
||||||
|
@ -261,7 +259,6 @@ dependencies {
|
||||||
implementation("com.mikepenz:fastadapter-extensions-binding:$fastAdapterVersion")
|
implementation("com.mikepenz:fastadapter-extensions-binding:$fastAdapterVersion")
|
||||||
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533")
|
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533")
|
||||||
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533")
|
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533")
|
||||||
implementation("com.nononsenseapps:filepicker:2.5.2")
|
|
||||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||||
implementation("com.github.mthli:Slice:v1.2")
|
implementation("com.github.mthli:Slice:v1.2")
|
||||||
implementation("io.noties.markwon:core:4.6.2")
|
implementation("io.noties.markwon:core:4.6.2")
|
||||||
|
|
|
@ -127,10 +127,6 @@
|
||||||
android:configChanges="uiMode|orientation|screenSize"/>
|
android:configChanges="uiMode|orientation|screenSize"/>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.security.BiometricActivity" />
|
android:name=".ui.security.BiometricActivity" />
|
||||||
<activity
|
|
||||||
android:name=".widget.CustomLayoutPickerActivity"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/FilePickerTheme" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||||
android:label="MyAnimeList"
|
android:label="MyAnimeList"
|
||||||
|
|
|
@ -39,8 +39,7 @@ class AppModule(val app: Application) : InjektModule {
|
||||||
|
|
||||||
addSingletonFactory { JavaScriptEngine(app) }
|
addSingletonFactory { JavaScriptEngine(app) }
|
||||||
|
|
||||||
addSingletonFactory { SourceManager(app).also { get<ExtensionManager>().init(it) } }
|
addSingletonFactory { SourceManager(app, get()) }
|
||||||
|
|
||||||
addSingletonFactory { ExtensionManager(app) }
|
addSingletonFactory { ExtensionManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { DownloadManager(app) }
|
addSingletonFactory { DownloadManager(app) }
|
||||||
|
|
|
@ -109,7 +109,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||||
setContentIntent(pendingIntent)
|
setContentIntent(pendingIntent)
|
||||||
setSmallIcon(R.drawable.ic_tachij2k_notification)
|
setSmallIcon(R.drawable.ic_tachij2k_notification)
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.nnf_ic_file_folder,
|
R.drawable.ic_file_open_24dp,
|
||||||
context.getString(R.string.open_log),
|
context.getString(R.string.open_log),
|
||||||
pendingIntent,
|
pendingIntent,
|
||||||
)
|
)
|
||||||
|
@ -144,7 +144,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||||
setContentIntent(NotificationHandler.openUrl(context, HELP_SKIPPED_URL))
|
setContentIntent(NotificationHandler.openUrl(context, HELP_SKIPPED_URL))
|
||||||
setSmallIcon(R.drawable.ic_tachij2k_notification)
|
setSmallIcon(R.drawable.ic_tachij2k_notification)
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.nnf_ic_file_folder,
|
R.drawable.ic_file_open_24dp,
|
||||||
context.getString(R.string.open_log),
|
context.getString(R.string.open_log),
|
||||||
NotificationReceiver.openErrorOrSkippedLogPendingActivity(context, uri),
|
NotificationReceiver.openErrorOrSkippedLogPendingActivity(context, uri),
|
||||||
)
|
)
|
||||||
|
|
|
@ -81,7 +81,7 @@ class ExtensionInstallService(
|
||||||
instance = this
|
instance = this
|
||||||
|
|
||||||
val list = intent.getParcelableArrayListExtra<ExtensionInfo>(KEY_EXTENSION)?.filter {
|
val list = intent.getParcelableArrayListExtra<ExtensionInfo>(KEY_EXTENSION)?.filter {
|
||||||
val installedExt = extensionManager.installedExtensions.find { installed ->
|
val installedExt = extensionManager.installedExtensionsFlow.value.find { installed ->
|
||||||
installed.pkgName == it.pkgName
|
installed.pkgName == it.pkgName
|
||||||
} ?: return@filter false
|
} ?: return@filter false
|
||||||
installedExt.versionCode < it.versionCode || installedExt.libVersion < it.libVersion
|
installedExt.versionCode < it.versionCode || installedExt.libVersion < it.libVersion
|
||||||
|
|
|
@ -5,7 +5,6 @@ import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
|
@ -20,14 +19,15 @@ import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
||||||
import eu.kanade.tachiyomi.util.system.launchNow
|
import eu.kanade.tachiyomi.util.system.launchNow
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import rx.Observable
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The manager of extensions installed as another apk which extend the available sources. It handles
|
* The manager of extensions installed as another apk which extend the available sources. It handles
|
||||||
|
@ -54,6 +54,8 @@ class ExtensionManager(
|
||||||
*/
|
*/
|
||||||
private val installer by lazy { ExtensionInstaller(context) }
|
private val installer by lazy { ExtensionInstaller(context) }
|
||||||
|
|
||||||
|
private val iconMap = mutableMapOf<String, Drawable>()
|
||||||
|
|
||||||
val downloadRelay
|
val downloadRelay
|
||||||
get() = installer.downloadsStateFlow
|
get() = installer.downloadsStateFlow
|
||||||
|
|
||||||
|
@ -66,27 +68,28 @@ class ExtensionManager(
|
||||||
/**
|
/**
|
||||||
* Relay used to notify the installed extensions.
|
* Relay used to notify the installed extensions.
|
||||||
*/
|
*/
|
||||||
private val installedExtensionsRelay = BehaviorRelay.create<List<Extension.Installed>>()
|
private val _installedExtensionsFlow = MutableStateFlow(emptyList<Extension.Installed>())
|
||||||
|
val installedExtensionsFlow = _installedExtensionsFlow.asStateFlow()
|
||||||
|
|
||||||
private val iconMap = mutableMapOf<String, Drawable>()
|
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of the currently installed extensions.
|
* List of the currently installed extensions.
|
||||||
*/
|
*/
|
||||||
var installedExtensions = emptyList<Extension.Installed>()
|
// private var installedExtensions = emptyList<Extension.Installed>()
|
||||||
private set(value) {
|
// set(value) {
|
||||||
field = value
|
// field = value
|
||||||
installedExtensionsRelay.call(value)
|
// installedExtensionsRelay.call(value)
|
||||||
downloadRelay.tryEmit("Finished/Installed/${value.size}" to (InstallStep.Done to null))
|
// downloadRelay.tryEmit("Finished/Installed/${value.size}" to (InstallStep.Done to null))
|
||||||
}
|
// }
|
||||||
|
|
||||||
fun getAppIconForSource(source: Source): Drawable? {
|
fun getAppIconForSource(source: Source): Drawable? {
|
||||||
return getAppIconForSource(source.id)
|
return getAppIconForSource(source.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAppIconForSource(sourceId: Long): Drawable? {
|
private fun getAppIconForSource(sourceId: Long): Drawable? {
|
||||||
val pkgName =
|
val pkgName = _installedExtensionsFlow.value
|
||||||
installedExtensions.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
|
.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
|
||||||
return if (pkgName != null) {
|
return if (pkgName != null) {
|
||||||
try {
|
try {
|
||||||
return iconMap[pkgName]
|
return iconMap[pkgName]
|
||||||
|
@ -102,47 +105,42 @@ class ExtensionManager(
|
||||||
/**
|
/**
|
||||||
* Relay used to notify the available extensions.
|
* Relay used to notify the available extensions.
|
||||||
*/
|
*/
|
||||||
private val availableExtensionsRelay = BehaviorRelay.create<List<Extension.Available>>()
|
private val _availableExtensionsFlow = MutableStateFlow(emptyList<Extension.Available>())
|
||||||
|
val availableExtensionsFlow = _availableExtensionsFlow.asStateFlow()
|
||||||
/**
|
|
||||||
* List of the currently available extensions.
|
|
||||||
*/
|
|
||||||
var availableExtensions = emptyList<Extension.Available>()
|
|
||||||
private set(value) {
|
|
||||||
field = value
|
|
||||||
availableExtensionsRelay.call(value)
|
|
||||||
updatedInstalledExtensionsStatuses(value)
|
|
||||||
downloadRelay.tryEmit("Finished/Available/${value.size}" to (InstallStep.Done to null))
|
|
||||||
setupAvailableSourcesMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var availableSources = hashMapOf<Long, Extension.AvailableSource>()
|
private var availableSources = hashMapOf<Long, Extension.AvailableSource>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relay used to notify the untrusted extensions.
|
* List of the currently available extensions.
|
||||||
*/
|
*/
|
||||||
private val untrustedExtensionsRelay = BehaviorRelay.create<List<Extension.Untrusted>>()
|
// var availableExtensions = emptyList<Extension.Available>()
|
||||||
|
// private set(value) {
|
||||||
|
// field = value
|
||||||
|
// availableExtensionsRelay.call(value)
|
||||||
|
// updatedInstalledExtensionsStatuses(value)
|
||||||
|
// downloadRelay.tryEmit("Finished/Available/${value.size}" to (InstallStep.Done to null))
|
||||||
|
// setupAvailableSourcesMap()
|
||||||
|
// }
|
||||||
|
|
||||||
|
private val _untrustedExtensionsFlow = MutableStateFlow(emptyList<Extension.Untrusted>())
|
||||||
|
val untrustedExtensionsFlow = _untrustedExtensionsFlow.asStateFlow()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of the currently untrusted extensions.
|
* List of the currently untrusted extensions.
|
||||||
*/
|
*/
|
||||||
var untrustedExtensions = emptyList<Extension.Untrusted>()
|
// var untrustedExtensions = emptyList<Extension.Untrusted>()
|
||||||
private set(value) {
|
// private set(value) {
|
||||||
field = value
|
// field = value
|
||||||
untrustedExtensionsRelay.call(value)
|
// untrustedExtensionsRelay.call(value)
|
||||||
downloadRelay.tryEmit("Finished/Untrusted/${value.size}" to (InstallStep.Done to null))
|
// downloadRelay.tryEmit("Finished/Untrusted/${value.size}" to (InstallStep.Done to null))
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The source manager where the sources of the extensions are added.
|
* The source manager where the sources of the extensions are added.
|
||||||
*/
|
*/
|
||||||
private lateinit var sourceManager: SourceManager
|
private lateinit var sourceManager: SourceManager
|
||||||
|
|
||||||
/**
|
init {
|
||||||
* Initializes this manager with the given source manager.
|
|
||||||
*/
|
|
||||||
fun init(sourceManager: SourceManager) {
|
|
||||||
this.sourceManager = sourceManager
|
|
||||||
initExtensions()
|
initExtensions()
|
||||||
ExtensionInstallReceiver(InstallationListener()).register(context)
|
ExtensionInstallReceiver(InstallationListener()).register(context)
|
||||||
}
|
}
|
||||||
|
@ -153,79 +151,77 @@ class ExtensionManager(
|
||||||
private fun initExtensions() {
|
private fun initExtensions() {
|
||||||
val extensions = ExtensionLoader.loadExtensions(context)
|
val extensions = ExtensionLoader.loadExtensions(context)
|
||||||
|
|
||||||
installedExtensions = extensions
|
_installedExtensionsFlow.value = extensions
|
||||||
.filterIsInstance<LoadResult.Success>()
|
.filterIsInstance<LoadResult.Success>()
|
||||||
.map { it.extension }
|
.map { it.extension }
|
||||||
installedExtensions
|
|
||||||
.flatMap { it.sources }
|
|
||||||
// overwrite is needed until the bundled sources are removed
|
|
||||||
.forEach { sourceManager.registerSource(it, true) }
|
|
||||||
|
|
||||||
untrustedExtensions = extensions
|
_untrustedExtensionsFlow.value = extensions
|
||||||
.filterIsInstance<LoadResult.Untrusted>()
|
.filterIsInstance<LoadResult.Untrusted>()
|
||||||
.map { it.extension }
|
.map { it.extension }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the relay of the installed extensions as an observable.
|
|
||||||
*/
|
|
||||||
fun getInstalledExtensionsObservable(): Observable<List<Extension.Installed>> {
|
|
||||||
return installedExtensionsRelay.asObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the relay of the available extensions as an observable.
|
|
||||||
*/
|
|
||||||
fun getAvailableExtensionsObservable(): Observable<List<Extension.Available>> {
|
|
||||||
return availableExtensionsRelay.asObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the relay of the untrusted extensions as an observable.
|
|
||||||
*/
|
|
||||||
fun getUntrustedExtensionsObservable(): Observable<List<Extension.Untrusted>> {
|
|
||||||
return untrustedExtensionsRelay.asObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isInstalledByApp(extension: Extension.Available): Boolean {
|
fun isInstalledByApp(extension: Extension.Available): Boolean {
|
||||||
return ExtensionLoader.isExtensionInstalledByApp(context, extension.pkgName)
|
return ExtensionLoader.isExtensionInstalledByApp(context, extension.pkgName)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the available extensions in the [api] and updates [availableExtensions].
|
* Finds the available extensions in the [api] and updates [availableExtensionsFlow].
|
||||||
*/
|
*/
|
||||||
fun findAvailableExtensions() {
|
suspend fun findAvailableExtensions() {
|
||||||
launchNow {
|
val extensions: List<Extension.Available> = try {
|
||||||
availableExtensions = try {
|
api.findExtensions()
|
||||||
api.findExtensions()
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
Timber.e(e)
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enableAdditionalSubLanguages(extensions)
|
||||||
|
|
||||||
|
_availableExtensionsFlow.value = extensions
|
||||||
|
updatedInstalledExtensionsStatuses(extensions)
|
||||||
|
setupAvailableSourcesMap()
|
||||||
|
downloadRelay.tryEmit("Finished/Available/${extensions.size}" to (InstallStep.Done to null))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables the additional sub-languages in the app first run. This addresses
|
||||||
|
* the issue where users still need to enable some specific languages even when
|
||||||
|
* the device language is inside that major group. As an example, if a user
|
||||||
|
* has a zh device language, the app will also enable zh-Hans and zh-Hant.
|
||||||
|
*
|
||||||
|
* If the user have already changed the enabledLanguages preference value once,
|
||||||
|
* the new languages will not be added to respect the user enabled choices.
|
||||||
|
*/
|
||||||
|
private fun enableAdditionalSubLanguages(extensions: List<Extension.Available>) {
|
||||||
|
if (subLanguagesEnabledOnFirstRun || extensions.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the source lang as some aren't present on the extension level.
|
||||||
|
val availableLanguages = extensions
|
||||||
|
.flatMap(Extension.Available::sources)
|
||||||
|
.distinctBy(Extension.AvailableSource::lang)
|
||||||
|
.map(Extension.AvailableSource::lang)
|
||||||
|
|
||||||
|
val deviceLanguage = Locale.getDefault().language
|
||||||
|
val defaultLanguages = preferences.enabledLanguages().defaultValue
|
||||||
|
val languagesToEnable = availableLanguages.filter {
|
||||||
|
it != deviceLanguage && it.startsWith(deviceLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences.enabledLanguages().set(defaultLanguages + languagesToEnable)
|
||||||
|
subLanguagesEnabledOnFirstRun = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupAvailableSourcesMap() {
|
private fun setupAvailableSourcesMap() {
|
||||||
availableSources = hashMapOf()
|
availableSources = hashMapOf()
|
||||||
availableExtensions.map { it.sources.orEmpty() }.flatten().forEach {
|
_availableExtensionsFlow.value.map { it.sources }.flatten().forEach {
|
||||||
availableSources[it.id] = it
|
availableSources[it.id] = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getStubSource(id: Long) = availableSources[id]
|
fun getStubSource(id: Long) = availableSources[id]
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the available extensions in the [api] and updates [availableExtensions].
|
|
||||||
*/
|
|
||||||
suspend fun findAvailableExtensionsAsync() {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
availableExtensions = try {
|
|
||||||
api.findExtensions()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the update field of the installed extensions with the given [availableExtensions].
|
* Sets the update field of the installed extensions with the given [availableExtensions].
|
||||||
*
|
*
|
||||||
|
@ -236,7 +232,7 @@ class ExtensionManager(
|
||||||
preferences.extensionUpdatesCount().set(0)
|
preferences.extensionUpdatesCount().set(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val mutInstalledExtensions = installedExtensions.toMutableList()
|
val mutInstalledExtensions = installedExtensionsFlow.value.toMutableList()
|
||||||
var changed = false
|
var changed = false
|
||||||
var hasUpdateCount = 0
|
var hasUpdateCount = 0
|
||||||
for ((index, installedExt) in mutInstalledExtensions.withIndex()) {
|
for ((index, installedExt) in mutInstalledExtensions.withIndex()) {
|
||||||
|
@ -257,9 +253,9 @@ class ExtensionManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
installedExtensions = mutInstalledExtensions
|
_installedExtensionsFlow.value = mutInstalledExtensions
|
||||||
}
|
}
|
||||||
preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate })
|
preferences.extensionUpdatesCount().set(installedExtensionsFlow.value.count { it.hasUpdate })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -358,15 +354,15 @@ class ExtensionManager(
|
||||||
* @param signature The signature to whitelist.
|
* @param signature The signature to whitelist.
|
||||||
*/
|
*/
|
||||||
fun trustSignature(signature: String) {
|
fun trustSignature(signature: String) {
|
||||||
val untrustedSignatures = untrustedExtensions.map { it.signatureHash }.toSet()
|
val untrustedSignatures = untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
|
||||||
if (signature !in untrustedSignatures) return
|
if (signature !in untrustedSignatures) return
|
||||||
|
|
||||||
ExtensionLoader.trustedSignatures += signature
|
ExtensionLoader.trustedSignatures += signature
|
||||||
val preference = preferences.trustedSignatures()
|
val preference = preferences.trustedSignatures()
|
||||||
preference.set(preference.get() + signature)
|
preference.set(preference.get() + signature)
|
||||||
|
|
||||||
val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature }
|
val nowTrustedExtensions = untrustedExtensionsFlow.value.filter { it.signatureHash == signature }
|
||||||
untrustedExtensions -= nowTrustedExtensions
|
_untrustedExtensionsFlow.value -= nowTrustedExtensions
|
||||||
|
|
||||||
val ctx = context
|
val ctx = context
|
||||||
launchNow {
|
launchNow {
|
||||||
|
@ -389,9 +385,8 @@ class ExtensionManager(
|
||||||
* @param extension The extension to be registered.
|
* @param extension The extension to be registered.
|
||||||
*/
|
*/
|
||||||
private fun registerNewExtension(extension: Extension.Installed) {
|
private fun registerNewExtension(extension: Extension.Installed) {
|
||||||
installedExtensions = installedExtensions + extension
|
_installedExtensionsFlow.value += extension
|
||||||
downloadRelay.tryEmit("Finished/${extension.pkgName}" to ExtensionIntallInfo(InstallStep.Installed, null))
|
downloadRelay.tryEmit("Finished/${extension.pkgName}" to ExtensionIntallInfo(InstallStep.Installed, null))
|
||||||
extension.sources.forEach { sourceManager.registerSource(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -401,16 +396,14 @@ class ExtensionManager(
|
||||||
* @param extension The extension to be registered.
|
* @param extension The extension to be registered.
|
||||||
*/
|
*/
|
||||||
private fun registerUpdatedExtension(extension: Extension.Installed) {
|
private fun registerUpdatedExtension(extension: Extension.Installed) {
|
||||||
val mutInstalledExtensions = installedExtensions.toMutableList()
|
val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList()
|
||||||
val oldExtension = mutInstalledExtensions.find { it.pkgName == extension.pkgName }
|
val oldExtension = mutInstalledExtensions.find { it.pkgName == extension.pkgName }
|
||||||
if (oldExtension != null) {
|
if (oldExtension != null) {
|
||||||
mutInstalledExtensions -= oldExtension
|
mutInstalledExtensions -= oldExtension
|
||||||
extension.sources.forEach { sourceManager.unregisterSource(it) }
|
|
||||||
}
|
}
|
||||||
mutInstalledExtensions += extension
|
mutInstalledExtensions += extension
|
||||||
installedExtensions = mutInstalledExtensions
|
_installedExtensionsFlow.value = mutInstalledExtensions
|
||||||
downloadRelay.tryEmit("Finished/${extension.pkgName}" to ExtensionIntallInfo(InstallStep.Installed, null))
|
downloadRelay.tryEmit("Finished/${extension.pkgName}" to ExtensionIntallInfo(InstallStep.Installed, null))
|
||||||
extension.sources.forEach { sourceManager.registerSource(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -420,14 +413,13 @@ class ExtensionManager(
|
||||||
* @param pkgName The package name of the uninstalled application.
|
* @param pkgName The package name of the uninstalled application.
|
||||||
*/
|
*/
|
||||||
private fun unregisterExtension(pkgName: String) {
|
private fun unregisterExtension(pkgName: String) {
|
||||||
val installedExtension = installedExtensions.find { it.pkgName == pkgName }
|
val installedExtension = installedExtensionsFlow.value.find { it.pkgName == pkgName }
|
||||||
if (installedExtension != null) {
|
if (installedExtension != null) {
|
||||||
installedExtensions = installedExtensions - installedExtension
|
_installedExtensionsFlow.value -= installedExtension
|
||||||
installedExtension.sources.forEach { sourceManager.unregisterSource(it) }
|
|
||||||
}
|
}
|
||||||
val untrustedExtension = untrustedExtensions.find { it.pkgName == pkgName }
|
val untrustedExtension = untrustedExtensionsFlow.value.find { it.pkgName == pkgName }
|
||||||
if (untrustedExtension != null) {
|
if (untrustedExtension != null) {
|
||||||
untrustedExtensions = untrustedExtensions - untrustedExtension
|
_untrustedExtensionsFlow.value -= untrustedExtension
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,21 +430,21 @@ class ExtensionManager(
|
||||||
|
|
||||||
override fun onExtensionInstalled(extension: Extension.Installed) {
|
override fun onExtensionInstalled(extension: Extension.Installed) {
|
||||||
registerNewExtension(extension.withUpdateCheck())
|
registerNewExtension(extension.withUpdateCheck())
|
||||||
preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate })
|
preferences.extensionUpdatesCount().set(installedExtensionsFlow.value.count { it.hasUpdate })
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExtensionUpdated(extension: Extension.Installed) {
|
override fun onExtensionUpdated(extension: Extension.Installed) {
|
||||||
registerUpdatedExtension(extension.withUpdateCheck())
|
registerUpdatedExtension(extension.withUpdateCheck())
|
||||||
preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate })
|
preferences.extensionUpdatesCount().set(installedExtensionsFlow.value.count { it.hasUpdate })
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExtensionUntrusted(extension: Extension.Untrusted) {
|
override fun onExtensionUntrusted(extension: Extension.Untrusted) {
|
||||||
untrustedExtensions += extension
|
_untrustedExtensionsFlow.value += extension
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPackageUninstalled(pkgName: String) {
|
override fun onPackageUninstalled(pkgName: String) {
|
||||||
unregisterExtension(pkgName)
|
unregisterExtension(pkgName)
|
||||||
preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate })
|
preferences.extensionUpdatesCount().set(installedExtensionsFlow.value.count { it.hasUpdate })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -464,7 +456,7 @@ class ExtensionManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
|
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
|
||||||
val availableExt = availableExtension ?: availableExtensionsRelay.value.find { it.pkgName == pkgName }
|
val availableExt = availableExtension ?: availableExtensionsFlow.value.find { it.pkgName == pkgName }
|
||||||
if (isUnofficial || availableExt == null) return false
|
if (isUnofficial || availableExt == null) return false
|
||||||
|
|
||||||
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
|
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
|
||||||
|
|
|
@ -97,7 +97,7 @@ internal class ExtensionGithubApi {
|
||||||
isNsfw = it.nsfw == 1,
|
isNsfw = it.nsfw == 1,
|
||||||
hasReadme = it.hasReadme == 1,
|
hasReadme = it.hasReadme == 1,
|
||||||
hasChangelog = it.hasChangelog == 1,
|
hasChangelog = it.hasChangelog == 1,
|
||||||
sources = it.sources,
|
sources = it.sources ?: emptyList(),
|
||||||
apkName = it.apk,
|
apkName = it.apk,
|
||||||
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
|
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
|
||||||
)
|
)
|
||||||
|
|
|
@ -46,7 +46,7 @@ sealed class Extension {
|
||||||
override val hasChangelog: Boolean,
|
override val hasChangelog: Boolean,
|
||||||
val apkName: String,
|
val apkName: String,
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val sources: List<AvailableSource>? = null,
|
val sources: List<AvailableSource>,
|
||||||
) : Extension()
|
) : Extension()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
@ -12,16 +12,31 @@ import eu.kanade.tachiyomi.source.online.all.Cubari
|
||||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||||
import eu.kanade.tachiyomi.source.online.english.KireiCake
|
import eu.kanade.tachiyomi.source.online.english.KireiCake
|
||||||
import eu.kanade.tachiyomi.source.online.english.MangaPlus
|
import eu.kanade.tachiyomi.source.online.english.MangaPlus
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
open class SourceManager(private val context: Context) {
|
class SourceManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val extensionManager: ExtensionManager,
|
||||||
|
) {
|
||||||
|
|
||||||
private val sourcesMap = mutableMapOf<Long, Source>()
|
private val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
|
|
||||||
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
|
private val sourcesMapFlow = MutableStateFlow(ConcurrentHashMap<Long, Source>())
|
||||||
|
|
||||||
protected val extensionManager: ExtensionManager by injectLazy()
|
private val stubSourcesMap = ConcurrentHashMap<Long, StubSource>()
|
||||||
|
|
||||||
|
val catalogueSources: Flow<List<CatalogueSource>> = sourcesMapFlow.map { it.values.filterIsInstance<CatalogueSource>() }
|
||||||
|
val onlineSources: Flow<List<HttpSource>> = catalogueSources.map { it.filterIsInstance<HttpSource>() }
|
||||||
|
|
||||||
private val delegatedSources = listOf(
|
private val delegatedSources = listOf(
|
||||||
DelegatedSource(
|
DelegatedSource(
|
||||||
|
@ -47,16 +62,39 @@ open class SourceManager(private val context: Context) {
|
||||||
).associateBy { it.sourceId }
|
).associateBy { it.sourceId }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
createInternalSources().forEach { registerSource(it) }
|
scope.launch {
|
||||||
|
extensionManager.installedExtensionsFlow
|
||||||
|
.collectLatest { extensions ->
|
||||||
|
val mutableMap = ConcurrentHashMap<Long, Source>(mapOf(LocalSource.ID to LocalSource(context)))
|
||||||
|
extensions.forEach { extension ->
|
||||||
|
extension.sources.forEach {
|
||||||
|
mutableMap[it.id] = it
|
||||||
|
delegatedSources[it.id]?.delegatedHttpSource?.delegate = it as? HttpSource
|
||||||
|
// registerStubSource(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourcesMapFlow.value = mutableMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scope.launch {
|
||||||
|
// sourceRepository.subscribeAll()
|
||||||
|
// .collectLatest { sources ->
|
||||||
|
// val mutableMap = stubSourcesMap.toMutableMap()
|
||||||
|
// sources.forEach {
|
||||||
|
// mutableMap[it.id] = StubSource(it)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun get(sourceKey: Long): Source? {
|
fun get(sourceKey: Long): Source? {
|
||||||
return sourcesMap[sourceKey]
|
return sourcesMapFlow.value[sourceKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOrStub(sourceKey: Long): Source {
|
fun getOrStub(sourceKey: Long): Source {
|
||||||
return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
|
return sourcesMapFlow.value[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
|
||||||
StubSource(sourceKey)
|
runBlocking { StubSource(sourceKey) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,24 +106,9 @@ open class SourceManager(private val context: Context) {
|
||||||
return delegatedSources.values.find { it.urlName == urlName }?.delegatedHttpSource
|
return delegatedSources.values.find { it.urlName == urlName }?.delegatedHttpSource
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
|
fun getOnlineSources() = sourcesMapFlow.value.values.filterIsInstance<HttpSource>()
|
||||||
|
|
||||||
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
|
fun getCatalogueSources() = sourcesMapFlow.value.values.filterIsInstance<CatalogueSource>()
|
||||||
|
|
||||||
internal fun registerSource(source: Source, overwrite: Boolean = false) {
|
|
||||||
if (overwrite || !sourcesMap.containsKey(source.id)) {
|
|
||||||
delegatedSources[source.id]?.delegatedHttpSource?.delegate = source as? HttpSource
|
|
||||||
sourcesMap[source.id] = source
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun unregisterSource(source: Source) {
|
|
||||||
sourcesMap.remove(source.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createInternalSources(): List<Source> = listOf(
|
|
||||||
LocalSource(context),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Suppress("OverridingDeprecatedMember")
|
@Suppress("OverridingDeprecatedMember")
|
||||||
inner class StubSource(override val id: Long) : Source {
|
inner class StubSource(override val id: Long) : Source {
|
||||||
|
@ -97,6 +120,7 @@ open class SourceManager(private val context: Context) {
|
||||||
throw getSourceNotInstalledException()
|
throw getSourceNotInstalledException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
return Observable.error(getSourceNotInstalledException())
|
return Observable.error(getSourceNotInstalledException())
|
||||||
}
|
}
|
||||||
|
@ -105,6 +129,7 @@ open class SourceManager(private val context: Context) {
|
||||||
throw getSourceNotInstalledException()
|
throw getSourceNotInstalledException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
return Observable.error(getSourceNotInstalledException())
|
return Observable.error(getSourceNotInstalledException())
|
||||||
}
|
}
|
||||||
|
@ -113,6 +138,7 @@ open class SourceManager(private val context: Context) {
|
||||||
throw getSourceNotInstalledException()
|
throw getSourceNotInstalledException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getPageList"))
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
return Observable.error(getSourceNotInstalledException())
|
return Observable.error(getSourceNotInstalledException())
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,7 +97,7 @@ abstract class HttpSource : CatalogueSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExtension(extensionManager: ExtensionManager? = null): Extension.Installed? =
|
fun getExtension(extensionManager: ExtensionManager? = null): Extension.Installed? =
|
||||||
(extensionManager ?: Injekt.get()).installedExtensions.find { it.sources.contains(this) }
|
(extensionManager ?: Injekt.get()).installedExtensionsFlow.value.find { it.sources.contains(this) }
|
||||||
|
|
||||||
fun extOnlyHasAllLanguage(extensionManager: ExtensionManager? = null) =
|
fun extOnlyHasAllLanguage(extensionManager: ExtensionManager? = null) =
|
||||||
getExtension(extensionManager)?.sources?.all { it.lang == "all" } ?: true
|
getExtension(extensionManager)?.sources?.all { it.lang == "all" } ?: true
|
||||||
|
|
|
@ -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")
|
@Suppress("UNCHECKED_CAST")
|
||||||
private fun <View> BaseCoroutinePresenter<View>.takeView(view: Any) = attachView(view as? View)
|
private fun <View> BaseCoroutinePresenter<View>.takeView(view: Any) = attachView(view as? View)
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroy() {
|
||||||
super.onDestroyView(view)
|
super.onDestroy()
|
||||||
presenter.onDestroy()
|
presenter.onDestroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -39,12 +40,12 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>(
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
presenterScope.launch {
|
presenterScope.launch {
|
||||||
val extensionJob = async {
|
val extensionJob = async {
|
||||||
extensionManager.findAvailableExtensionsAsync()
|
extensionManager.findAvailableExtensions()
|
||||||
extensions = toItems(
|
extensions = toItems(
|
||||||
Triple(
|
Triple(
|
||||||
extensionManager.installedExtensions,
|
extensionManager.installedExtensionsFlow.value,
|
||||||
extensionManager.untrustedExtensions,
|
extensionManager.untrustedExtensionsFlow.value,
|
||||||
extensionManager.availableExtensions,
|
extensionManager.availableExtensionsFlow.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
withContext(Dispatchers.Main) { controller?.setExtensions(extensions, false) }
|
withContext(Dispatchers.Main) { controller?.setExtensions(extensions, false) }
|
||||||
|
@ -53,16 +54,16 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>(
|
||||||
listOf(migrationJob, extensionJob).awaitAll()
|
listOf(migrationJob, extensionJob).awaitAll()
|
||||||
}
|
}
|
||||||
presenterScope.launch {
|
presenterScope.launch {
|
||||||
extensionManager.downloadRelay
|
extensionManager.downloadRelay.asSharedFlow()
|
||||||
.collect {
|
.collect {
|
||||||
if (it.first.startsWith("Finished")) {
|
if (it.first.startsWith("Finished")) {
|
||||||
firstLoad = true
|
firstLoad = true
|
||||||
currentDownloads.clear()
|
currentDownloads.clear()
|
||||||
extensions = toItems(
|
extensions = toItems(
|
||||||
Triple(
|
Triple(
|
||||||
extensionManager.installedExtensions,
|
extensionManager.installedExtensionsFlow.value,
|
||||||
extensionManager.untrustedExtensions,
|
extensionManager.untrustedExtensionsFlow.value,
|
||||||
extensionManager.availableExtensions,
|
extensionManager.availableExtensionsFlow.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
withUIContext { controller?.setExtensions(extensions) }
|
withUIContext { controller?.setExtensions(extensions) }
|
||||||
|
@ -91,9 +92,9 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>(
|
||||||
presenterScope.launch {
|
presenterScope.launch {
|
||||||
extensions = toItems(
|
extensions = toItems(
|
||||||
Triple(
|
Triple(
|
||||||
extensionManager.installedExtensions,
|
extensionManager.installedExtensionsFlow.value,
|
||||||
extensionManager.untrustedExtensions,
|
extensionManager.untrustedExtensionsFlow.value,
|
||||||
extensionManager.availableExtensions,
|
extensionManager.availableExtensionsFlow.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
withContext(Dispatchers.Main) { controller?.setExtensions(extensions, false) }
|
withContext(Dispatchers.Main) { controller?.setExtensions(extensions, false) }
|
||||||
|
@ -249,7 +250,7 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>(
|
||||||
|
|
||||||
fun updateExtension(extension: Extension.Installed) {
|
fun updateExtension(extension: Extension.Installed) {
|
||||||
val availableExt =
|
val availableExt =
|
||||||
extensionManager.availableExtensions.find { it.pkgName == extension.pkgName } ?: return
|
extensionManager.availableExtensionsFlow.value.find { it.pkgName == extension.pkgName } ?: return
|
||||||
installExtension(availableExt)
|
installExtension(availableExt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,7 +266,7 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>(
|
||||||
val intent = ExtensionInstallService.jobIntent(
|
val intent = ExtensionInstallService.jobIntent(
|
||||||
context,
|
context,
|
||||||
extensions.mapNotNull { extension ->
|
extensions.mapNotNull { extension ->
|
||||||
extensionManager.availableExtensions.find { it.pkgName == extension.pkgName }
|
extensionManager.availableExtensionsFlow.value.find { it.pkgName == extension.pkgName }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
@ -276,7 +277,9 @@ class ExtensionBottomPresenter() : BaseMigrationPresenter<ExtensionBottomSheet>(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findAvailableExtensions() {
|
fun findAvailableExtensions() {
|
||||||
extensionManager.findAvailableExtensions()
|
presenterScope.launch {
|
||||||
|
extensionManager.findAvailableExtensions()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trustSignature(signatureHash: String) {
|
fun trustSignature(signatureHash: String) {
|
||||||
|
|
|
@ -22,7 +22,7 @@ class ExtensionFilterController : SettingsController() {
|
||||||
|
|
||||||
val activeLangs = preferences.enabledLanguages().get()
|
val activeLangs = preferences.enabledLanguages().get()
|
||||||
|
|
||||||
val availableLangs = extensionManager.availableExtensions.groupBy { it.lang }.keys
|
val availableLangs = extensionManager.availableExtensionsFlow.value.groupBy { it.lang }.keys
|
||||||
.sortedWith(compareBy({ it !in activeLangs }, { LocaleHelper.getSourceDisplayName(it, context) }))
|
.sortedWith(compareBy({ it !in activeLangs }, { LocaleHelper.getSourceDisplayName(it, context) }))
|
||||||
|
|
||||||
availableLangs.forEach {
|
availableLangs.forEach {
|
||||||
|
|
|
@ -36,7 +36,7 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.getPreferenceKey
|
import eu.kanade.tachiyomi.source.getPreferenceKey
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController
|
||||||
import eu.kanade.tachiyomi.ui.setting.DSL
|
import eu.kanade.tachiyomi.ui.setting.DSL
|
||||||
import eu.kanade.tachiyomi.ui.setting.onChange
|
import eu.kanade.tachiyomi.ui.setting.onChange
|
||||||
import eu.kanade.tachiyomi.ui.setting.switchPreference
|
import eu.kanade.tachiyomi.ui.setting.switchPreference
|
||||||
|
@ -57,7 +57,7 @@ import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
class ExtensionDetailsController(bundle: Bundle? = null) :
|
class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||||
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
|
BaseCoroutineController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
|
||||||
PreferenceManager.OnDisplayPreferenceDialogListener,
|
PreferenceManager.OnDisplayPreferenceDialogListener,
|
||||||
DialogPreference.TargetFragment {
|
DialogPreference.TargetFragment {
|
||||||
|
|
||||||
|
@ -81,9 +81,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||||
override fun createBinding(inflater: LayoutInflater) =
|
override fun createBinding(inflater: LayoutInflater) =
|
||||||
ExtensionDetailControllerBinding.inflate(inflater.cloneInContext(getPreferenceThemeContext()))
|
ExtensionDetailControllerBinding.inflate(inflater.cloneInContext(getPreferenceThemeContext()))
|
||||||
|
|
||||||
override fun createPresenter(): ExtensionDetailsPresenter {
|
override val presenter = ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!)
|
||||||
return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTitle(): String? {
|
override fun getTitle(): String? {
|
||||||
return resources?.getString(R.string.extension_info)
|
return resources?.getString(R.string.extension_info)
|
||||||
|
|
|
@ -1,35 +1,34 @@
|
||||||
package eu.kanade.tachiyomi.ui.extension.details
|
package eu.kanade.tachiyomi.ui.extension.details
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import eu.kanade.tachiyomi.util.system.launchUI
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class ExtensionDetailsPresenter(
|
class ExtensionDetailsPresenter(
|
||||||
val pkgName: String,
|
val pkgName: String,
|
||||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||||
) : BasePresenter<ExtensionDetailsController>() {
|
) : BaseCoroutinePresenter<ExtensionDetailsController>() {
|
||||||
|
|
||||||
val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName }
|
val extension = extensionManager.installedExtensionsFlow.value.find { it.pkgName == pkgName }
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
bindToUninstalledExtension()
|
bindToUninstalledExtension()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindToUninstalledExtension() {
|
private fun bindToUninstalledExtension() {
|
||||||
extensionManager.getInstalledExtensionsObservable()
|
extensionManager.installedExtensionsFlow
|
||||||
.skip(1)
|
.drop(1)
|
||||||
.filter { extensions -> extensions.none { it.pkgName == pkgName } }
|
.onEach { extensions ->
|
||||||
.map { Unit }
|
extensions.filter { it.pkgName == pkgName }
|
||||||
.take(1)
|
presenterScope.launchUI { controller?.onExtensionUninstalled() }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
}
|
||||||
.subscribeFirst({ view, _ ->
|
.launchIn(presenterScope)
|
||||||
view.onExtensionUninstalled()
|
|
||||||
},)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun uninstallExtension() {
|
fun uninstallExtension() {
|
||||||
|
|
|
@ -821,15 +821,15 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExtensionUpdates(force: Boolean) {
|
fun getExtensionUpdates(force: Boolean) {
|
||||||
if ((force && extensionManager.availableExtensions.isEmpty()) ||
|
if ((force && extensionManager.availableExtensionsFlow.value.isEmpty()) ||
|
||||||
Date().time >= preferences.lastExtCheck().get() + TimeUnit.HOURS.toMillis(6)
|
Date().time >= preferences.lastExtCheck().get() + TimeUnit.HOURS.toMillis(6)
|
||||||
) {
|
) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
extensionManager.findAvailableExtensionsAsync()
|
extensionManager.findAvailableExtensions()
|
||||||
val pendingUpdates = ExtensionGithubApi().checkForUpdates(
|
val pendingUpdates = ExtensionGithubApi().checkForUpdates(
|
||||||
this@MainActivity,
|
this@MainActivity,
|
||||||
extensionManager.availableExtensions.takeIf { it.isNotEmpty() },
|
extensionManager.availableExtensionsFlow.value.takeIf { it.isNotEmpty() },
|
||||||
)
|
)
|
||||||
preferences.extensionUpdatesCount().set(pendingUpdates.size)
|
preferences.extensionUpdatesCount().set(pendingUpdates.size)
|
||||||
preferences.lastExtCheck().set(Date().time)
|
preferences.lastExtCheck().set(Date().time)
|
||||||
|
|
|
@ -110,7 +110,7 @@ class EditMangaDialog : DialogController {
|
||||||
|
|
||||||
languages.add("")
|
languages.add("")
|
||||||
languages.addAll(
|
languages.addAll(
|
||||||
extensionManager.availableExtensions.groupBy { it.lang }.keys
|
extensionManager.availableExtensionsFlow.value.groupBy { it.lang }.keys
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy(
|
compareBy(
|
||||||
{ it !in activeLangs },
|
{ it !in activeLangs },
|
||||||
|
|
|
@ -54,7 +54,7 @@ abstract class BaseMigrationPresenter<T : BaseMigrationInterface>(
|
||||||
val header = SelectionHeader()
|
val header = SelectionHeader()
|
||||||
val sourceGroup = library.groupBy { it.source }
|
val sourceGroup = library.groupBy { it.source }
|
||||||
val sortOrder = PreferenceValues.MigrationSourceOrder.fromPreference(preferences)
|
val sortOrder = PreferenceValues.MigrationSourceOrder.fromPreference(preferences)
|
||||||
val extensions = extensionManager.installedExtensions
|
val extensions = extensionManager.installedExtensionsFlow.value
|
||||||
val obsoleteSources =
|
val obsoleteSources =
|
||||||
extensions.filter { it.isObsolete }.map { it.sources }.flatten().map { it.id }
|
extensions.filter { it.isObsolete }.map { it.sources }.flatten().map { it.id }
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.databinding.MigrationControllerBinding
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController
|
import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController
|
||||||
import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController
|
import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController
|
||||||
import eu.kanade.tachiyomi.ui.source.BrowseController
|
import eu.kanade.tachiyomi.ui.source.BrowseController
|
||||||
import eu.kanade.tachiyomi.util.system.await
|
|
||||||
import eu.kanade.tachiyomi.util.system.launchUI
|
import eu.kanade.tachiyomi.util.system.launchUI
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import eu.kanade.tachiyomi.util.view.activityBinding
|
import eu.kanade.tachiyomi.util.view.activityBinding
|
||||||
|
@ -24,7 +23,6 @@ import eu.kanade.tachiyomi.util.view.scrollViewWith
|
||||||
import eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset
|
import eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
@ -94,9 +92,7 @@ class MigrationController :
|
||||||
val item = adapter?.getItem(position) as? SourceItem ?: return
|
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||||
|
|
||||||
launchUI {
|
launchUI {
|
||||||
val manga = Injekt.get<DatabaseHelper>().getFavoriteMangas().asRxSingle().await(
|
val manga = Injekt.get<DatabaseHelper>().getFavoriteMangas().executeAsBlocking()
|
||||||
Schedulers.io(),
|
|
||||||
)
|
|
||||||
val sourceMangas =
|
val sourceMangas =
|
||||||
manga.asSequence().filter { it.source == item.source.id }.map { it.id!! }.toList()
|
manga.asSequence().filter { it.source == item.source.id }.map { it.id!! }.toList()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|
|
@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.ui.main.BottomNavBarInterface
|
||||||
import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController
|
import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController
|
||||||
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchCardAdapter
|
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchCardAdapter
|
||||||
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.source.globalsearch.GlobalSearchPresenter
|
|
||||||
import eu.kanade.tachiyomi.util.view.activityBinding
|
import eu.kanade.tachiyomi.util.view.activityBinding
|
||||||
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -51,9 +50,7 @@ class SearchController(
|
||||||
bundle.getLongArray(SOURCES) ?: LongArray(0),
|
bundle.getLongArray(SOURCES) ?: LongArray(0),
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun createPresenter(): GlobalSearchPresenter {
|
override val presenter = SearchPresenter(initialQuery, manga!!, sources = sources)
|
||||||
return SearchPresenter(initialQuery, manga!!, sources = sources)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMangaClick(manga: Manga) {
|
override fun onMangaClick(manga: Manga) {
|
||||||
if (targetController is MigrationListController) {
|
if (targetController is MigrationListController) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.ui.setting
|
package eu.kanade.tachiyomi.ui.setting
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
@ -15,7 +14,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
|
import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
|
||||||
import eu.kanade.tachiyomi.util.system.getFilePicker
|
|
||||||
import eu.kanade.tachiyomi.util.system.withOriginalWidth
|
import eu.kanade.tachiyomi.util.system.withOriginalWidth
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
@ -167,13 +165,8 @@ class SettingsDownloadController : SettingsController() {
|
||||||
preferences.downloadsDirectory().set(path.toString())
|
preferences.downloadsDirectory().set(path.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun customDirectorySelected(currentDir: String) {
|
fun customDirectorySelected() {
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), DOWNLOAD_DIR)
|
||||||
try {
|
|
||||||
startActivityForResult(intent, DOWNLOAD_DIR)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadDirectoriesDialog(val controller: SettingsDownloadController) :
|
class DownloadDirectoriesDialog(val controller: SettingsDownloadController) :
|
||||||
|
@ -193,7 +186,7 @@ class SettingsDownloadController : SettingsController() {
|
||||||
setTitle(R.string.download_location)
|
setTitle(R.string.download_location)
|
||||||
setSingleChoiceItems(items.toTypedArray(), selectedIndex) { dialog, position ->
|
setSingleChoiceItems(items.toTypedArray(), selectedIndex) { dialog, position ->
|
||||||
if (position == externalDirs.lastIndex) {
|
if (position == externalDirs.lastIndex) {
|
||||||
controller.customDirectorySelected(currentDir)
|
controller.customDirectorySelected()
|
||||||
} else {
|
} else {
|
||||||
controller.predefinedDirectorySelected(items[position])
|
controller.predefinedDirectorySelected(items[position])
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,10 +93,7 @@ class SettingsMainController : SettingsController(), FloatingSearchInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionViewExpand(item: MenuItem?) {
|
override fun onActionViewExpand(item: MenuItem?) {
|
||||||
SettingsSearchController.lastSearch = "" // reset saved search query
|
router.pushController(RouterTransaction.with(SettingsSearchController()))
|
||||||
router.pushController(
|
|
||||||
RouterTransaction.with(SettingsSearchController()),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateTo(controller: Controller) {
|
private fun navigateTo(controller: Controller) {
|
||||||
|
|
|
@ -11,19 +11,20 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.databinding.SettingsSearchControllerBinding
|
import eu.kanade.tachiyomi.databinding.SettingsSearchControllerBinding
|
||||||
import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface
|
import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||||
import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface
|
import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsController
|
import eu.kanade.tachiyomi.ui.setting.SettingsController
|
||||||
import eu.kanade.tachiyomi.util.view.activityBinding
|
import eu.kanade.tachiyomi.util.view.activityBinding
|
||||||
import eu.kanade.tachiyomi.util.view.liftAppbarWith
|
import eu.kanade.tachiyomi.util.view.liftAppbarWith
|
||||||
import eu.kanade.tachiyomi.util.view.withFadeTransaction
|
import eu.kanade.tachiyomi.util.view.withFadeTransaction
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This controller shows and manages the different search result in settings search.
|
* This controller shows and manages the different search result in settings search.
|
||||||
* [SettingsSearchAdapter.OnTitleClickListener] called when preference is clicked in settings search
|
* [SettingsSearchAdapter.OnTitleClickListener] called when preference is clicked in settings search
|
||||||
*/
|
*/
|
||||||
class SettingsSearchController :
|
class SettingsSearchController :
|
||||||
NucleusController<SettingsSearchControllerBinding, SettingsSearchPresenter>(),
|
BaseController<SettingsSearchControllerBinding>(),
|
||||||
FloatingSearchInterface,
|
FloatingSearchInterface,
|
||||||
SmallToolbarInterface,
|
SmallToolbarInterface,
|
||||||
SettingsSearchAdapter.OnTitleClickListener {
|
SettingsSearchAdapter.OnTitleClickListener {
|
||||||
|
@ -33,6 +34,7 @@ class SettingsSearchController :
|
||||||
*/
|
*/
|
||||||
private var adapter: SettingsSearchAdapter? = null
|
private var adapter: SettingsSearchAdapter? = null
|
||||||
private var searchView: SearchView? = null
|
private var searchView: SearchView? = null
|
||||||
|
var query: String = ""
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
|
@ -40,18 +42,7 @@ class SettingsSearchController :
|
||||||
|
|
||||||
override fun createBinding(inflater: LayoutInflater) = SettingsSearchControllerBinding.inflate(inflater)
|
override fun createBinding(inflater: LayoutInflater) = SettingsSearchControllerBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun getTitle(): String {
|
override fun getTitle(): String = query
|
||||||
return presenter.query
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the [SettingsSearchPresenter] used in controller.
|
|
||||||
*
|
|
||||||
* @return instance of [SettingsSearchPresenter]
|
|
||||||
*/
|
|
||||||
override fun createPresenter(): SettingsSearchPresenter {
|
|
||||||
return SettingsSearchPresenter()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds items to the options menu.
|
* Adds items to the options menu.
|
||||||
|
@ -80,7 +71,7 @@ class SettingsSearchController :
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
if (!newText.isNullOrBlank()) {
|
if (!newText.isNullOrBlank()) {
|
||||||
lastSearch = newText
|
query = newText
|
||||||
}
|
}
|
||||||
setItems(getResultSet(newText))
|
setItems(getResultSet(newText))
|
||||||
return false
|
return false
|
||||||
|
@ -88,7 +79,7 @@ class SettingsSearchController :
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
searchView?.setQuery(lastSearch, true)
|
searchView?.setQuery(query, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionViewCollapse(item: MenuItem?) {
|
override fun onActionViewCollapse(item: MenuItem?) {
|
||||||
|
@ -151,13 +142,9 @@ class SettingsSearchController :
|
||||||
*/
|
*/
|
||||||
override fun onTitleClick(ctrl: SettingsController) {
|
override fun onTitleClick(ctrl: SettingsController) {
|
||||||
searchView?.query.let {
|
searchView?.query.let {
|
||||||
lastSearch = it.toString()
|
query = it.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
router.pushController(ctrl.withFadeTransaction())
|
router.pushController(ctrl.withFadeTransaction())
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
var lastSearch = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController
|
||||||
import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface
|
import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
||||||
|
@ -63,7 +63,7 @@ import kotlin.math.roundToInt
|
||||||
* Controller to manage the catalogues available in the app.
|
* Controller to manage the catalogues available in the app.
|
||||||
*/
|
*/
|
||||||
open class BrowseSourceController(bundle: Bundle) :
|
open class BrowseSourceController(bundle: Bundle) :
|
||||||
NucleusController<BrowseSourceControllerBinding, BrowseSourcePresenter>(bundle),
|
BaseCoroutineController<BrowseSourceControllerBinding, BrowseSourcePresenter>(bundle),
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
FloatingSearchInterface,
|
FloatingSearchInterface,
|
||||||
|
@ -146,13 +146,11 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||||
// return presenter.source.icon()
|
// return presenter.source.icon()
|
||||||
// }
|
// }
|
||||||
|
|
||||||
override fun createPresenter(): BrowseSourcePresenter {
|
override val presenter = BrowseSourcePresenter(
|
||||||
return BrowseSourcePresenter(
|
args.getLong(SOURCE_ID_KEY),
|
||||||
args.getLong(SOURCE_ID_KEY),
|
args.getString(SEARCH_QUERY_KEY),
|
||||||
args.getString(SEARCH_QUERY_KEY),
|
args.getBoolean(USE_LATEST_KEY),
|
||||||
args.getBoolean(USE_LATEST_KEY),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createBinding(inflater: LayoutInflater) = BrowseSourceControllerBinding.inflate(inflater)
|
override fun createBinding(inflater: LayoutInflater) = BrowseSourceControllerBinding.inflate(inflater)
|
||||||
|
|
||||||
|
@ -165,7 +163,6 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||||
|
|
||||||
binding.fab.isVisible = presenter.sourceFilters.isNotEmpty()
|
binding.fab.isVisible = presenter.sourceFilters.isNotEmpty()
|
||||||
binding.fab.setOnClickListener { showFilters() }
|
binding.fab.setOnClickListener { showFilters() }
|
||||||
binding.progress.isVisible = true
|
|
||||||
activityBinding?.appBar?.y = 0f
|
activityBinding?.appBar?.y = 0f
|
||||||
activityBinding?.appBar?.updateAppBarAfterY(recycler)
|
activityBinding?.appBar?.updateAppBarAfterY(recycler)
|
||||||
activityBinding?.appBar?.lockYPos = true
|
activityBinding?.appBar?.lockYPos = true
|
||||||
|
@ -178,6 +175,11 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (presenter.items.isNotEmpty()) {
|
||||||
|
onAddPage(1, presenter.items)
|
||||||
|
} else {
|
||||||
|
binding.progress.isVisible = true
|
||||||
|
}
|
||||||
requestFilePermissionsSafe(301, preferences, presenter.source is LocalSource)
|
requestFilePermissionsSafe(301, preferences, presenter.source is LocalSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,10 +280,9 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||||
val searchView = activityBinding?.searchToolbar?.searchView
|
val searchView = activityBinding?.searchToolbar?.searchView
|
||||||
|
|
||||||
activityBinding?.searchToolbar?.setQueryHint("", !isBehindGlobalSearch && presenter.query.isBlank())
|
activityBinding?.searchToolbar?.setQueryHint("", !isBehindGlobalSearch && presenter.query.isBlank())
|
||||||
val query = presenter.query
|
if (presenter.query.isNotBlank()) {
|
||||||
if (query.isNotBlank()) {
|
|
||||||
searchItem?.expandActionView()
|
searchItem?.expandActionView()
|
||||||
searchView?.setQuery(query, true)
|
searchView?.setQuery(presenter.query, true)
|
||||||
searchView?.clearFocus()
|
searchView?.clearFocus()
|
||||||
} else if (activityBinding?.searchToolbar?.isSearchExpanded == true) {
|
} else if (activityBinding?.searchToolbar?.isSearchExpanded == true) {
|
||||||
searchItem?.collapseActionView()
|
searchItem?.collapseActionView()
|
||||||
|
@ -516,7 +517,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||||
showProgressBar()
|
showProgressBar()
|
||||||
adapter?.clear()
|
adapter?.clear()
|
||||||
|
|
||||||
presenter.restartPager(newQuery, presenter.sourceFilters)
|
presenter.restartPager(newQuery)
|
||||||
updatePopLatestIcons()
|
updatePopLatestIcons()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.ui.source.browse
|
package eu.kanade.tachiyomi.ui.source.browse
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
@ -13,7 +12,7 @@ import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
|
||||||
import eu.kanade.tachiyomi.ui.source.filter.CheckboxItem
|
import eu.kanade.tachiyomi.ui.source.filter.CheckboxItem
|
||||||
import eu.kanade.tachiyomi.ui.source.filter.CheckboxSectionItem
|
import eu.kanade.tachiyomi.ui.source.filter.CheckboxSectionItem
|
||||||
import eu.kanade.tachiyomi.ui.source.filter.GroupItem
|
import eu.kanade.tachiyomi.ui.source.filter.GroupItem
|
||||||
|
@ -36,9 +35,6 @@ import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
@ -54,7 +50,7 @@ open class BrowseSourcePresenter(
|
||||||
val db: DatabaseHelper = Injekt.get(),
|
val db: DatabaseHelper = Injekt.get(),
|
||||||
val prefs: PreferencesHelper = Injekt.get(),
|
val prefs: PreferencesHelper = Injekt.get(),
|
||||||
private val coverCache: CoverCache = Injekt.get(),
|
private val coverCache: CoverCache = Injekt.get(),
|
||||||
) : BasePresenter<BrowseSourceController>() {
|
) : BaseCoroutinePresenter<BrowseSourceController>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selected source.
|
* Selected source.
|
||||||
|
@ -66,6 +62,10 @@ open class BrowseSourcePresenter(
|
||||||
|
|
||||||
var filtersChanged = false
|
var filtersChanged = false
|
||||||
|
|
||||||
|
var items = mutableListOf<BrowseSourceItem>()
|
||||||
|
val page: Int
|
||||||
|
get() = pager.currentPage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modifiable list of filters.
|
* Modifiable list of filters.
|
||||||
*/
|
*/
|
||||||
|
@ -87,39 +87,24 @@ open class BrowseSourcePresenter(
|
||||||
* Pager containing a list of manga results.
|
* Pager containing a list of manga results.
|
||||||
*/
|
*/
|
||||||
private lateinit var pager: Pager
|
private lateinit var pager: Pager
|
||||||
|
private var pagerJob: Job? = null
|
||||||
/**
|
|
||||||
* Subscription for the pager.
|
|
||||||
*/
|
|
||||||
private var pagerSubscription: Subscription? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription for one request from the pager.
|
* Subscription for one request from the pager.
|
||||||
*/
|
*/
|
||||||
private var nextPageJob: Job? = null
|
private var nextPageJob: Job? = null
|
||||||
|
|
||||||
init {
|
var query = searchQuery ?: ""
|
||||||
query = searchQuery ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate() {
|
||||||
super.onCreate(savedState)
|
super.onCreate()
|
||||||
|
if (!::pager.isInitialized) {
|
||||||
|
source = sourceManager.get(sourceId) as? CatalogueSource ?: return
|
||||||
|
|
||||||
source = sourceManager.get(sourceId) as? CatalogueSource ?: return
|
sourceFilters = source.getFilterList()
|
||||||
|
filtersChanged = false
|
||||||
sourceFilters = source.getFilterList()
|
restartPager()
|
||||||
filtersChanged = false
|
|
||||||
|
|
||||||
if (savedState != null) {
|
|
||||||
query = savedState.getString(::query.name, "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
restartPager()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSave(state: Bundle) {
|
|
||||||
state.putString(::query.name, query)
|
|
||||||
super.onSave(state)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,27 +125,27 @@ open class BrowseSourcePresenter(
|
||||||
val browseAsList = prefs.browseAsList()
|
val browseAsList = prefs.browseAsList()
|
||||||
val sourceListType = prefs.libraryLayout()
|
val sourceListType = prefs.libraryLayout()
|
||||||
val outlineCovers = prefs.outlineOnCovers()
|
val outlineCovers = prefs.outlineOnCovers()
|
||||||
|
items.clear()
|
||||||
|
|
||||||
// Prepare the pager.
|
// Prepare the pager.
|
||||||
pagerSubscription?.let { remove(it) }
|
pagerJob?.cancel()
|
||||||
pagerSubscription = pager.results()
|
pagerJob = presenterScope.launchIO {
|
||||||
.observeOn(Schedulers.io())
|
pager.results().onEach { (page, second) ->
|
||||||
.map { (first, second) ->
|
try {
|
||||||
first to second
|
val mangas = second
|
||||||
.map { networkToLocalManga(it, sourceId) }
|
.map { networkToLocalManga(it, sourceId) }
|
||||||
.filter { !prefs.hideInLibraryItems().get() || !it.favorite }
|
.filter { !prefs.hideInLibraryItems().get() || !it.favorite }
|
||||||
}
|
initializeMangas(mangas)
|
||||||
.doOnNext { initializeMangas(it.second) }
|
val items = mangas.map {
|
||||||
.map { (first, second) -> first to second.map { BrowseSourceItem(it, browseAsList, sourceListType, outlineCovers) } }
|
BrowseSourceItem(it, browseAsList, sourceListType, outlineCovers)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
}
|
||||||
.subscribeReplay(
|
this@BrowseSourcePresenter.items.addAll(items)
|
||||||
{ view, (page, mangas) ->
|
withUIContext { controller?.onAddPage(page, items) }
|
||||||
view.onAddPage(page, mangas)
|
} catch (error: Exception) {
|
||||||
},
|
|
||||||
{ _, error ->
|
|
||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
},
|
}
|
||||||
)
|
}.collect()
|
||||||
|
}
|
||||||
|
|
||||||
// Request first page.
|
// Request first page.
|
||||||
requestNext()
|
requestNext()
|
||||||
|
@ -173,14 +158,11 @@ open class BrowseSourcePresenter(
|
||||||
if (!hasNextPage()) return
|
if (!hasNextPage()) return
|
||||||
|
|
||||||
nextPageJob?.cancel()
|
nextPageJob?.cancel()
|
||||||
nextPageJob = launchIO {
|
nextPageJob = presenterScope.launchIO {
|
||||||
try {
|
try {
|
||||||
pager.requestNextPage()
|
pager.requestNextPage()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
withUIContext {
|
withUIContext { controller?.onAddPageError(e) }
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
view?.onAddPageError(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,10 +211,7 @@ open class BrowseSourcePresenter(
|
||||||
.filter { it.thumbnail_url == null && !it.initialized }
|
.filter { it.thumbnail_url == null && !it.initialized }
|
||||||
.map { getMangaDetails(it) }
|
.map { getMangaDetails(it) }
|
||||||
.onEach {
|
.onEach {
|
||||||
withUIContext {
|
withUIContext { controller?.onMangaInitialized(it) }
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
view?.onMangaInitialized(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.catch { e -> Timber.e(e) }
|
.catch { e -> Timber.e(e) }
|
||||||
.collect()
|
.collect()
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package eu.kanade.tachiyomi.ui.source.browse
|
package eu.kanade.tachiyomi.ui.source.browse
|
||||||
|
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import rx.Observable
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A general pager for source requests (latest updates, popular, search)
|
* A general pager for source requests (latest updates, popular, search)
|
||||||
|
@ -13,18 +14,18 @@ abstract class Pager(var currentPage: Int = 1) {
|
||||||
var hasNextPage = true
|
var hasNextPage = true
|
||||||
private set
|
private set
|
||||||
|
|
||||||
protected val results: PublishRelay<Pair<Int, List<SManga>>> = PublishRelay.create()
|
protected val results = MutableSharedFlow<Pair<Int, List<SManga>>>()
|
||||||
|
|
||||||
fun results(): Observable<Pair<Int, List<SManga>>> {
|
fun results(): SharedFlow<Pair<Int, List<SManga>>> {
|
||||||
return results.asObservable()
|
return results.asSharedFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract suspend fun requestNextPage()
|
abstract suspend fun requestNextPage()
|
||||||
|
|
||||||
fun onPageReceived(mangasPage: MangasPage) {
|
suspend fun onPageReceived(mangasPage: MangasPage) {
|
||||||
val page = currentPage
|
val page = currentPage
|
||||||
currentPage++
|
currentPage++
|
||||||
hasNextPage = mangasPage.hasNextPage && mangasPage.mangas.isNotEmpty()
|
hasNextPage = mangasPage.hasNextPage && mangasPage.mangas.isNotEmpty()
|
||||||
results.call(Pair(page, mangasPage.mangas))
|
results.emit(Pair(page, mangasPage.mangas))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.databinding.SourceGlobalSearchControllerBinding
|
import eu.kanade.tachiyomi.databinding.SourceGlobalSearchControllerBinding
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface
|
import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController
|
||||||
import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface
|
import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
||||||
|
@ -42,7 +42,7 @@ open class GlobalSearchController(
|
||||||
protected val initialQuery: String? = null,
|
protected val initialQuery: String? = null,
|
||||||
val extensionFilter: String? = null,
|
val extensionFilter: String? = null,
|
||||||
bundle: Bundle? = null,
|
bundle: Bundle? = null,
|
||||||
) : NucleusController<SourceGlobalSearchControllerBinding, GlobalSearchPresenter>(bundle),
|
) : BaseCoroutineController<SourceGlobalSearchControllerBinding, GlobalSearchPresenter>(bundle),
|
||||||
FloatingSearchInterface,
|
FloatingSearchInterface,
|
||||||
SmallToolbarInterface,
|
SmallToolbarInterface,
|
||||||
GlobalSearchAdapter.OnTitleClickListener,
|
GlobalSearchAdapter.OnTitleClickListener,
|
||||||
|
@ -78,14 +78,7 @@ open class GlobalSearchController(
|
||||||
return customTitle ?: presenter.query
|
return customTitle ?: presenter.query
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override val presenter = GlobalSearchPresenter(initialQuery, extensionFilter)
|
||||||
* Create the [GlobalSearchPresenter] used in controller.
|
|
||||||
*
|
|
||||||
* @return instance of [GlobalSearchPresenter]
|
|
||||||
*/
|
|
||||||
override fun createPresenter(): GlobalSearchPresenter {
|
|
||||||
return GlobalSearchPresenter(initialQuery, extensionFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTitleClick(source: CatalogueSource) {
|
override fun onTitleClick(source: CatalogueSource) {
|
||||||
preferences.lastUsedCatalogueSource().set(source.id)
|
preferences.lastUsedCatalogueSource().set(source.id)
|
||||||
|
@ -108,7 +101,7 @@ open class GlobalSearchController(
|
||||||
/**
|
/**
|
||||||
* Called when manga in global search is long clicked.
|
* Called when manga in global search is long clicked.
|
||||||
*
|
*
|
||||||
* @param manga clicked item containing manga information.
|
* @param position clicked item containing manga information.
|
||||||
*/
|
*/
|
||||||
override fun onMangaLongClick(position: Int, adapter: GlobalSearchCardAdapter) {
|
override fun onMangaLongClick(position: Int, adapter: GlobalSearchCardAdapter) {
|
||||||
val manga = adapter.getItem(position)?.manga ?: return
|
val manga = adapter.getItem(position)?.manga ?: return
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.ui.source.globalsearch
|
package eu.kanade.tachiyomi.ui.source.globalsearch
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
@ -12,15 +11,18 @@ import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
|
||||||
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourcePresenter
|
import eu.kanade.tachiyomi.util.system.awaitSingle
|
||||||
import eu.kanade.tachiyomi.util.system.runAsObservable
|
import eu.kanade.tachiyomi.util.system.launchIO
|
||||||
import rx.Observable
|
import eu.kanade.tachiyomi.util.system.launchUI
|
||||||
import rx.Subscription
|
import eu.kanade.tachiyomi.util.system.withUIContext
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.Job
|
||||||
import rx.schedulers.Schedulers
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import rx.subjects.PublishSubject
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import timber.log.Timber
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
@ -43,56 +45,43 @@ open class GlobalSearchPresenter(
|
||||||
val db: DatabaseHelper = Injekt.get(),
|
val db: DatabaseHelper = Injekt.get(),
|
||||||
private val preferences: PreferencesHelper = Injekt.get(),
|
private val preferences: PreferencesHelper = Injekt.get(),
|
||||||
private val coverCache: CoverCache = Injekt.get(),
|
private val coverCache: CoverCache = Injekt.get(),
|
||||||
) : BasePresenter<GlobalSearchController>() {
|
) : BaseCoroutinePresenter<GlobalSearchController>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enabled sources.
|
* Enabled sources.
|
||||||
*/
|
*/
|
||||||
val sources by lazy { getSourcesToQuery() }
|
val sources by lazy { getSourcesToQuery() }
|
||||||
|
|
||||||
/**
|
private var fetchSourcesJob: Job? = null
|
||||||
* Fetches the different sources by user settings.
|
|
||||||
*/
|
|
||||||
private var fetchSourcesSubscription: Subscription? = null
|
|
||||||
|
|
||||||
private var loadTime = hashMapOf<Long, Long>()
|
private var loadTime = hashMapOf<Long, Long>()
|
||||||
|
|
||||||
/**
|
var query = ""
|
||||||
* Subject which fetches image of given manga.
|
|
||||||
*/
|
|
||||||
private val fetchImageSubject = PublishSubject.create<Pair<List<Manga>, Source>>()
|
|
||||||
|
|
||||||
/**
|
private val fetchImageFlow = MutableSharedFlow<Pair<List<Manga>, Source>>()
|
||||||
* Subscription for fetching images of manga.
|
|
||||||
*/
|
private var fetchImageJob: Job? = null
|
||||||
private var fetchImageSubscription: Subscription? = null
|
|
||||||
|
|
||||||
private val extensionManager: ExtensionManager by injectLazy()
|
private val extensionManager: ExtensionManager by injectLazy()
|
||||||
|
|
||||||
private var extensionFilter: String? = null
|
private var extensionFilter: String? = null
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
var items: List<GlobalSearchItem> = emptyList()
|
||||||
super.onCreate(savedState)
|
|
||||||
|
|
||||||
extensionFilter = savedState?.getString(GlobalSearchPresenter::extensionFilter.name)
|
private val semaphore = Semaphore(5)
|
||||||
?: initialExtensionFilter
|
|
||||||
|
|
||||||
// Perform a search with previous or initial state
|
override fun onCreate() {
|
||||||
search(
|
super.onCreate()
|
||||||
savedState?.getString(BrowseSourcePresenter::query.name) ?: initialQuery.orEmpty(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
extensionFilter = initialExtensionFilter
|
||||||
fetchSourcesSubscription?.unsubscribe()
|
|
||||||
fetchImageSubscription?.unsubscribe()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSave(state: Bundle) {
|
if (items.isEmpty()) {
|
||||||
state.putString(BrowseSourcePresenter::query.name, query)
|
// Perform a search with previous or initial state
|
||||||
state.putString(GlobalSearchPresenter::extensionFilter.name, extensionFilter)
|
search(initialQuery.orEmpty())
|
||||||
super.onSave(state)
|
}
|
||||||
|
presenterScope.launchUI {
|
||||||
|
controller?.setItems(items)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,7 +115,7 @@ open class GlobalSearchPresenter(
|
||||||
}
|
}
|
||||||
|
|
||||||
val languages = preferences.enabledLanguages().get()
|
val languages = preferences.enabledLanguages().get()
|
||||||
val filterSources = extensionManager.installedExtensions
|
val filterSources = extensionManager.installedExtensionsFlow.value
|
||||||
.filter { it.pkgName == filter }
|
.filter { it.pkgName == filter }
|
||||||
.flatMap { it.sources }
|
.flatMap { it.sources }
|
||||||
.filter { it.lang in languages }
|
.filter { it.lang in languages }
|
||||||
|
@ -174,70 +163,49 @@ open class GlobalSearchPresenter(
|
||||||
|
|
||||||
// Create items with the initial state
|
// Create items with the initial state
|
||||||
val initialItems = sources.map { createCatalogueSearchItem(it, null) }
|
val initialItems = sources.map { createCatalogueSearchItem(it, null) }
|
||||||
var items = initialItems
|
items = initialItems
|
||||||
|
|
||||||
val pinnedSourceIds = preferences.pinnedCatalogues().get()
|
val pinnedSourceIds = preferences.pinnedCatalogues().get()
|
||||||
|
|
||||||
fetchSourcesSubscription?.unsubscribe()
|
fetchSourcesJob?.cancel()
|
||||||
fetchSourcesSubscription = Observable.from(sources).flatMap(
|
fetchSourcesJob = presenterScope.launch {
|
||||||
{ source ->
|
sources.map { source ->
|
||||||
Observable.defer { source.fetchSearchManga(1, query, source.getFilterList()) }
|
launch mainLaunch@{
|
||||||
.subscribeOn(Schedulers.io()).onErrorReturn {
|
semaphore.withPermit {
|
||||||
MangasPage(
|
if (this@GlobalSearchPresenter.items.find { it.source == source }?.results != null) {
|
||||||
emptyList(),
|
return@mainLaunch
|
||||||
false,
|
|
||||||
)
|
|
||||||
} // Ignore timeouts or other exceptions
|
|
||||||
.map { it.mangas.take(10) } // Get at most 10 manga from search result.
|
|
||||||
.map {
|
|
||||||
it.map {
|
|
||||||
networkToLocalManga(
|
|
||||||
it,
|
|
||||||
source.id,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} // Convert to local manga.
|
val mangas = try {
|
||||||
.doOnNext { fetchImage(it, source) } // Load manga covers.
|
source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle()
|
||||||
.map {
|
} catch (error: Exception) {
|
||||||
if (it.isNotEmpty() && !loadTime.containsKey(source.id)) {
|
MangasPage(emptyList(), false)
|
||||||
|
}
|
||||||
|
.mangas.take(10)
|
||||||
|
.map { networkToLocalManga(it, source.id) }
|
||||||
|
fetchImage(mangas, source)
|
||||||
|
if (mangas.isNotEmpty() && !loadTime.containsKey(source.id)) {
|
||||||
loadTime[source.id] = Date().time
|
loadTime[source.id] = Date().time
|
||||||
}
|
}
|
||||||
createCatalogueSearchItem(
|
val result = createCatalogueSearchItem(
|
||||||
source,
|
source,
|
||||||
it.map { GlobalSearchMangaItem(it) },
|
mangas.map { GlobalSearchMangaItem(it) },
|
||||||
)
|
)
|
||||||
|
items = items
|
||||||
|
.map { item -> if (item.source == result.source) result else item }
|
||||||
|
.sortedWith(
|
||||||
|
compareBy(
|
||||||
|
// Bubble up sources that actually have results
|
||||||
|
{ it.results.isNullOrEmpty() },
|
||||||
|
// Same as initial sort, i.e. pinned first then alphabetically
|
||||||
|
{ it.source.id.toString() !in pinnedSourceIds },
|
||||||
|
{ loadTime[it.source.id] ?: 0L },
|
||||||
|
{ "${it.source.name.lowercase(Locale.getDefault())} (${it.source.lang})" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
withUIContext { controller?.setItems(items) }
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
5,
|
|
||||||
)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
// Update matching source with the obtained results
|
|
||||||
.map { result ->
|
|
||||||
items
|
|
||||||
.map { item -> if (item.source == result.source) result else item }
|
|
||||||
.sortedWith(
|
|
||||||
compareBy(
|
|
||||||
// Bubble up sources that actually have results
|
|
||||||
{ it.results.isNullOrEmpty() },
|
|
||||||
// Same as initial sort, i.e. pinned first then alphabetically
|
|
||||||
{ it.source.id.toString() !in pinnedSourceIds },
|
|
||||||
{ loadTime[it.source.id] ?: 0L },
|
|
||||||
{ "${it.source.name.lowercase(Locale.getDefault())} (${it.source.lang})" },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// Update current state
|
}
|
||||||
.doOnNext { items = it }
|
|
||||||
// Deliver initial state
|
|
||||||
.startWith(initialItems)
|
|
||||||
.subscribeLatestCache(
|
|
||||||
{ view, manga ->
|
|
||||||
view.setItems(manga)
|
|
||||||
},
|
|
||||||
{ _, error ->
|
|
||||||
Timber.e(error)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -246,33 +214,26 @@ open class GlobalSearchPresenter(
|
||||||
* @param manga the list of manga to initialize.
|
* @param manga the list of manga to initialize.
|
||||||
*/
|
*/
|
||||||
private fun fetchImage(manga: List<Manga>, source: Source) {
|
private fun fetchImage(manga: List<Manga>, source: Source) {
|
||||||
fetchImageSubject.onNext(Pair(manga, source))
|
presenterScope.launch {
|
||||||
|
fetchImageFlow.emit(Pair(manga, source))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes to the initializer of manga details and updates the view if needed.
|
* Subscribes to the initializer of manga details and updates the view if needed.
|
||||||
*/
|
*/
|
||||||
private fun initializeFetchImageSubscription() {
|
private fun initializeFetchImageSubscription() {
|
||||||
fetchImageSubscription?.unsubscribe()
|
fetchImageJob?.cancel()
|
||||||
fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io())
|
fetchImageJob = fetchImageFlow.onEach { (mangaList, source) ->
|
||||||
.flatMap { (mangaList, source) ->
|
mangaList
|
||||||
Observable.from(mangaList)
|
.filter { it.thumbnail_url == null && !it.initialized }
|
||||||
.filter { it.thumbnail_url == null && !it.initialized }
|
.map {
|
||||||
.map { Pair(it, source) }
|
presenterScope.launchIO {
|
||||||
.concatMap { runAsObservable { getMangaDetails(it.first, it.second) } }
|
val manga = getMangaDetails(it, source)
|
||||||
.map { Pair(source as CatalogueSource, it) }
|
withUIContext { controller?.onMangaInitialized(source as CatalogueSource, manga) }
|
||||||
}
|
}
|
||||||
.onBackpressureBuffer()
|
}
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
}.launchIn(presenterScope)
|
||||||
.subscribe(
|
|
||||||
{ (source, manga) ->
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
view?.onMangaInitialized(source, manga)
|
|
||||||
},
|
|
||||||
{ error ->
|
|
||||||
Timber.e(error)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -36,12 +36,10 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.nononsenseapps.filepicker.FilePickerActivity
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -83,27 +81,6 @@ inline fun Context.notification(channelId: String, func: NotificationCompat.Buil
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to construct an Intent to use a custom file picker.
|
|
||||||
* @param currentDir the path the file picker will open with.
|
|
||||||
* @return an Intent to start the file picker activity.
|
|
||||||
*/
|
|
||||||
fun Context.getFilePicker(currentDir: String): Intent {
|
|
||||||
return Intent(this, CustomLayoutPickerActivity::class.java)
|
|
||||||
.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
|
||||||
.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
|
||||||
.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the give permission is granted.
|
|
||||||
*
|
|
||||||
* @param permission the permission to check.
|
|
||||||
* @return true if it has permissions.
|
|
||||||
*/
|
|
||||||
fun Context.hasPermission(permission: String) =
|
|
||||||
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the color for the given attribute.
|
* Returns the color for the given attribute.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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>
|
<item name="android:textSize">13sp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!--===-->
|
|
||||||
<!--OLD-->
|
|
||||||
<!--===-->
|
|
||||||
|
|
||||||
<style name="FilePickerTheme" parent="NNF_BaseTheme.Light">
|
|
||||||
<item name="colorPrimary">@color/primaryTachiyomi</item>
|
|
||||||
<item name="colorAccent">@color/secondaryTachiyomi</item>
|
|
||||||
<item name="colorButtonNormal">@color/primaryTachiyomi</item>
|
|
||||||
<item name="android:textSize">14sp</item>
|
|
||||||
|
|
||||||
<item name="alertDialogTheme">@style/FilePickerAlertDialogTheme</item>
|
|
||||||
|
|
||||||
<item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert"/>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue