Convert Favorites sync to a StateFlow

This commit is contained in:
Jobobby04 2020-11-30 20:22:29 -05:00
parent 9a2ed755b7
commit ee18f94788
3 changed files with 61 additions and 70 deletions

View File

@ -51,18 +51,20 @@ import exh.favorites.FavoritesSyncStatus
import exh.mangaDexSourceIds import exh.mangaDexSourceIds
import exh.nHentaiSourceIds import exh.nHentaiSourceIds
import exh.ui.LoaderManager import exh.ui.LoaderManager
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.clicks
import reactivecircus.flowbinding.appcompat.queryTextChanges import reactivecircus.flowbinding.appcompat.queryTextChanges
import reactivecircus.flowbinding.viewpager.pageSelections import reactivecircus.flowbinding.viewpager.pageSelections
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
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.concurrent.TimeUnit import kotlin.time.ExperimentalTime
import kotlin.time.milliseconds
class LibraryController( class LibraryController(
bundle: Bundle? = null, bundle: Bundle? = null,
@ -145,7 +147,7 @@ class LibraryController(
// Old sync status // Old sync status
private var oldSyncStatus: FavoritesSyncStatus? = null private var oldSyncStatus: FavoritesSyncStatus? = null
// Favorites // Favorites
private var favoritesSyncSubscription: Subscription? = null private var favoritesSyncJob: Job? = null
val loaderManager = LoaderManager() val loaderManager = LoaderManager()
// <-- EH // <-- EH
@ -677,18 +679,19 @@ class LibraryController(
} }
// SY --> // SY -->
@OptIn(ExperimentalTime::class)
override fun onAttach(view: View) { override fun onAttach(view: View) {
super.onAttach(view) super.onAttach(view)
// --> EXH // --> EXH
cleanupSyncState() cleanupSyncState()
favoritesSyncSubscription = favoritesSyncJob =
presenter.favoritesSync.status presenter.favoritesSync.status
.sample(100, TimeUnit.MILLISECONDS) .sample(100.milliseconds)
.observeOn(AndroidSchedulers.mainThread()) .onEach {
.subscribe {
updateSyncStatus(it) updateSyncStatus(it)
} }
.launchIn(scope)
// <-- EXH // <-- EXH
} }
@ -714,8 +717,8 @@ class LibraryController(
// --> EXH // --> EXH
private fun cleanupSyncState() { private fun cleanupSyncState() {
favoritesSyncSubscription?.unsubscribe() favoritesSyncJob?.cancel()
favoritesSyncSubscription = null favoritesSyncJob = null
// Close sync status // Close sync status
favSyncDialog?.dismiss() favSyncDialog?.dismiss()
favSyncDialog = null favSyncDialog = null
@ -733,7 +736,6 @@ class LibraryController(
favSyncDialog = buildDialog() favSyncDialog = buildDialog()
?.title(R.string.favorites_syncing) ?.title(R.string.favorites_syncing)
?.cancelable(false) ?.cancelable(false)
// ?.progress(true, 0)
favSyncDialog?.show() favSyncDialog?.show()
} }
@ -763,10 +765,10 @@ class LibraryController(
?.cancelable(false) ?.cancelable(false)
?.positiveButton(R.string.show_gallery) { ?.positiveButton(R.string.show_gallery) {
openManga(status.manga) openManga(status.manga)
presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle(activity!!)) presenter.favoritesSync.status.value = FavoritesSyncStatus.Idle(activity!!)
} }
?.negativeButton(android.R.string.ok) { ?.negativeButton(android.R.string.ok) {
presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle(activity!!)) presenter.favoritesSync.status.value = FavoritesSyncStatus.Idle(activity!!)
} }
favSyncDialog?.show() favSyncDialog?.show()
} }
@ -779,7 +781,7 @@ class LibraryController(
?.message(text = activity!!.getString(R.string.favorites_sync_error_string, status.message)) ?.message(text = activity!!.getString(R.string.favorites_sync_error_string, status.message))
?.cancelable(false) ?.cancelable(false)
?.positiveButton(android.R.string.ok) { ?.positiveButton(android.R.string.ok) {
presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle(activity!!)) presenter.favoritesSync.status.value = FavoritesSyncStatus.Idle(activity!!)
} }
favSyncDialog?.show() favSyncDialog?.show()
} }
@ -792,7 +794,7 @@ class LibraryController(
?.message(text = activity!!.getString(R.string.favorites_sync_done_errors_message, status.message)) ?.message(text = activity!!.getString(R.string.favorites_sync_done_errors_message, status.message))
?.cancelable(false) ?.cancelable(false)
?.positiveButton(android.R.string.ok) { ?.positiveButton(android.R.string.ok) {
presenter.favoritesSync.status.onNext(FavoritesSyncStatus.Idle(activity!!)) presenter.favoritesSync.status.value = FavoritesSyncStatus.Idle(activity!!)
} }
favSyncDialog?.show() favSyncDialog?.show()
} }

View File

@ -30,10 +30,10 @@ import exh.util.wifiManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Request import okhttp3.Request
import rx.subjects.BehaviorSubject
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
@ -61,7 +61,7 @@ class FavoritesSyncHelper(val context: Context) {
private val logger = XLog.tag("EHFavSync").build() private val logger = XLog.tag("EHFavSync").build()
val status: BehaviorSubject<FavoritesSyncStatus> = BehaviorSubject.create(FavoritesSyncStatus.Idle(context)) val status: MutableStateFlow<FavoritesSyncStatus> = MutableStateFlow(FavoritesSyncStatus.Idle(context))
@Synchronized @Synchronized
fun runSync() { fun runSync() {
@ -69,7 +69,7 @@ class FavoritesSyncHelper(val context: Context) {
return return
} }
status.onNext(FavoritesSyncStatus.Initializing(context)) status.value = FavoritesSyncStatus.Initializing(context)
scope.launch(Dispatchers.IO) { beginSync() } scope.launch(Dispatchers.IO) { beginSync() }
} }
@ -77,12 +77,12 @@ class FavoritesSyncHelper(val context: Context) {
private suspend fun beginSync() { private suspend fun beginSync() {
// Check if logged in // Check if logged in
if (!prefs.enableExhentai().get()) { if (!prefs.enableExhentai().get()) {
status.onNext(FavoritesSyncStatus.Error(context.getString(R.string.please_login))) status.value = FavoritesSyncStatus.Error(context.getString(R.string.please_login))
return return
} }
// Validate library state // Validate library state
status.onNext(FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_verifying_library), context = context)) status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_verifying_library), context = context)
val libraryManga = db.getLibraryMangas().await() val libraryManga = db.getLibraryMangas().await()
val seenManga = HashSet<Long>(libraryManga.size) val seenManga = HashSet<Long>(libraryManga.size)
libraryManga.forEach { libraryManga.forEach {
@ -90,10 +90,8 @@ class FavoritesSyncHelper(val context: Context) {
if (it.id in seenManga) { if (it.id in seenManga) {
val inCategories = db.getCategoriesForManga(it).await() val inCategories = db.getCategoriesForManga(it).await()
status.onNext( status.value = FavoritesSyncStatus.BadLibraryState.MangaInMultipleCategories(it, inCategories, context)
FavoritesSyncStatus.BadLibraryState
.MangaInMultipleCategories(it, inCategories, context)
)
logger.w(context.getString(R.string.favorites_sync_manga_multiple_categories_error, it.id)) logger.w(context.getString(R.string.favorites_sync_manga_multiple_categories_error, it.id))
return return
} else { } else {
@ -103,10 +101,10 @@ class FavoritesSyncHelper(val context: Context) {
// Download remote favorites // Download remote favorites
val favorites = try { val favorites = try {
status.onNext(FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_downloading), context = context)) status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_downloading), context = context)
exh.fetchFavorites() exh.fetchFavorites()
} catch (e: Exception) { } catch (e: Exception) {
status.onNext(FavoritesSyncStatus.Error(context.getString(R.string.favorites_sync_failed_to_featch))) status.value = FavoritesSyncStatus.Error(context.getString(R.string.favorites_sync_failed_to_featch))
logger.e(context.getString(R.string.favorites_sync_could_not_fetch), e) logger.e(context.getString(R.string.favorites_sync_could_not_fetch), e)
return return
} }
@ -136,17 +134,17 @@ class FavoritesSyncHelper(val context: Context) {
storage.getRealm().use { realm -> storage.getRealm().use { realm ->
realm.trans { realm.trans {
db.inTransaction { db.inTransaction {
status.onNext(FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_calculating_remote_changes), context = context)) status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_calculating_remote_changes), context = context)
val remoteChanges = storage.getChangedRemoteEntries(realm, favorites.first) val remoteChanges = storage.getChangedRemoteEntries(realm, favorites.first)
val localChanges = if (prefs.exhReadOnlySync().get()) { val localChanges = if (prefs.exhReadOnlySync().get()) {
null // Do not build local changes if they are not going to be applied null // Do not build local changes if they are not going to be applied
} else { } else {
status.onNext(FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_calculating_local_changes), context = context)) status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_calculating_local_changes), context = context)
storage.getChangedDbEntries(realm) storage.getChangedDbEntries(realm)
} }
// Apply remote categories // Apply remote categories
status.onNext(FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_syncing_category_names), context = context)) status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_syncing_category_names), context = context)
applyRemoteCategories(favorites.second) applyRemoteCategories(favorites.second)
// Apply change sets // Apply change sets
@ -155,7 +153,7 @@ class FavoritesSyncHelper(val context: Context) {
applyChangeSetToRemote(errorList, localChanges) applyChangeSetToRemote(errorList, localChanges)
} }
status.onNext(FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_cleaning_up), context = context)) status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_cleaning_up), context = context)
storage.snapshotEntries(realm) storage.snapshotEntries(realm)
} }
} }
@ -169,7 +167,7 @@ class FavoritesSyncHelper(val context: Context) {
logger.w(context.getString(R.string.favorites_sync_ignoring_exception), e) logger.w(context.getString(R.string.favorites_sync_ignoring_exception), e)
return return
} catch (e: Exception) { } catch (e: Exception) {
status.onNext(FavoritesSyncStatus.Error(context.getString(R.string.favorites_sync_unknown_error, e.message))) status.value = FavoritesSyncStatus.Error(context.getString(R.string.favorites_sync_unknown_error, e.message))
logger.e(context.getString(R.string.favorites_sync_sync_error), e) logger.e(context.getString(R.string.favorites_sync_sync_error), e)
return return
} finally { } finally {
@ -188,9 +186,9 @@ class FavoritesSyncHelper(val context: Context) {
} }
if (errorList.isEmpty()) { if (errorList.isEmpty()) {
status.onNext(FavoritesSyncStatus.Idle(context)) status.value = FavoritesSyncStatus.Idle(context)
} else { } else {
status.onNext(FavoritesSyncStatus.CompleteWithErrors(errorList)) status.value = FavoritesSyncStatus.CompleteWithErrors(errorList)
} }
} }
@ -258,7 +256,7 @@ class FavoritesSyncHelper(val context: Context) {
if (prefs.exhLenientSync().get()) { if (prefs.exhLenientSync().get()) {
errorList += errorString errorList += errorString
} else { } else {
status.onNext(FavoritesSyncStatus.Error(errorString)) status.value = FavoritesSyncStatus.Error(errorString)
throw IgnoredException() throw IgnoredException()
} }
} }
@ -286,7 +284,7 @@ class FavoritesSyncHelper(val context: Context) {
private suspend fun applyChangeSetToRemote(errorList: MutableList<String>, changeSet: ChangeSet) { private suspend fun applyChangeSetToRemote(errorList: MutableList<String>, changeSet: ChangeSet) {
// Apply removals // Apply removals
if (changeSet.removed.isNotEmpty()) { if (changeSet.removed.isNotEmpty()) {
status.onNext(FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_removing_galleries, changeSet.removed.size), context = context)) status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_removing_galleries, changeSet.removed.size), context = context)
val formBody = FormBody.Builder() val formBody = FormBody.Builder()
.add("ddact", "delete") .add("ddact", "delete")
@ -308,7 +306,7 @@ class FavoritesSyncHelper(val context: Context) {
if (prefs.exhLenientSync().get()) { if (prefs.exhLenientSync().get()) {
errorList += errorString errorList += errorString
} else { } else {
status.onNext(FavoritesSyncStatus.Error(errorString)) status.value = FavoritesSyncStatus.Error(errorString)
throw IgnoredException() throw IgnoredException()
} }
} }
@ -317,12 +315,10 @@ class FavoritesSyncHelper(val context: Context) {
// Apply additions // Apply additions
throttleManager.resetThrottle() throttleManager.resetThrottle()
changeSet.added.forEachIndexed { index, it -> changeSet.added.forEachIndexed { index, it ->
status.onNext( status.value = FavoritesSyncStatus.Processing(
FavoritesSyncStatus.Processing( context.getString(R.string.favorites_sync_adding_to_remote, index + 1, changeSet.added.size),
context.getString(R.string.favorites_sync_adding_to_remote, index + 1, changeSet.added.size), needWarnThrottle(),
needWarnThrottle(), context
context
)
) )
throttleManager.throttle() throttleManager.throttle()
@ -336,7 +332,7 @@ class FavoritesSyncHelper(val context: Context) {
// Apply removals // Apply removals
changeSet.removed.forEachIndexed { index, it -> changeSet.removed.forEachIndexed { index, it ->
status.onNext(FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_remove_from_local, index + 1, changeSet.removed.size), context = context)) status.value = FavoritesSyncStatus.Processing(context.getString(R.string.favorites_sync_remove_from_local, index + 1, changeSet.removed.size), context = context)
val url = it.getUrl() val url = it.getUrl()
// Consider both EX and EH sources // Consider both EX and EH sources
@ -366,12 +362,10 @@ class FavoritesSyncHelper(val context: Context) {
// Apply additions // Apply additions
throttleManager.resetThrottle() throttleManager.resetThrottle()
changeSet.added.forEachIndexed { index, it -> changeSet.added.forEachIndexed { index, it ->
status.onNext( status.value = FavoritesSyncStatus.Processing(
FavoritesSyncStatus.Processing( context.getString(R.string.favorites_sync_add_to_local, index + 1, changeSet.added.size),
context.getString(R.string.favorites_sync_add_to_local, index + 1, changeSet.added.size), needWarnThrottle(),
needWarnThrottle(), context
context
)
) )
throttleManager.throttle() throttleManager.throttle()
@ -387,7 +381,7 @@ class FavoritesSyncHelper(val context: Context) {
if (result is GalleryAddEvent.Fail) { if (result is GalleryAddEvent.Fail) {
if (result is GalleryAddEvent.Fail.NotFound) { if (result is GalleryAddEvent.Fail.NotFound) {
XLog.e(context.getString(R.string.favorites_sync_remote_not_exist, it.getUrl())) XLog.tag("EHFavSync").enableStackTrace(2).e(context.getString(R.string.favorites_sync_remote_not_exist, it.getUrl()))
// Skip this gallery, it no longer exists // Skip this gallery, it no longer exists
return@forEachIndexed return@forEachIndexed
} }
@ -400,7 +394,7 @@ class FavoritesSyncHelper(val context: Context) {
if (prefs.exhLenientSync().get()) { if (prefs.exhLenientSync().get()) {
errorList += errorString errorList += errorString
} else { } else {
status.onNext(FavoritesSyncStatus.Error(errorString)) status.value = FavoritesSyncStatus.Error(errorString)
throw IgnoredException() throw IgnoredException()
} }
} else if (result is GalleryAddEvent.Success) { } else if (result is GalleryAddEvent.Success) {

View File

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.source.online.all.EHentai
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import exh.metadata.metadata.EHentaiSearchMetadata import exh.metadata.metadata.EHentaiSearchMetadata
import exh.util.executeOnIO
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -20,13 +21,13 @@ class LocalFavoritesStorage {
fun getRealm(): Realm = Realm.getInstance(realmConfig) fun getRealm(): Realm = Realm.getInstance(realmConfig)
fun getChangedDbEntries(realm: Realm) = suspend fun getChangedDbEntries(realm: Realm) =
getChangedEntries( getChangedEntries(
realm, realm,
parseToFavoriteEntries( parseToFavoriteEntries(
loadDbCategories( loadDbCategories(
db.getFavoriteMangas() db.getFavoriteMangas()
.executeAsBlocking() .executeOnIO()
.asSequence() .asSequence()
) )
) )
@ -37,22 +38,19 @@ class LocalFavoritesStorage {
realm, realm,
parseToFavoriteEntries( parseToFavoriteEntries(
entries.asSequence().map { entries.asSequence().map {
Pair( it.fav to it.manga.apply {
it.fav, favorite = true
it.manga.apply { date_added = System.currentTimeMillis()
favorite = true }
date_added = System.currentTimeMillis()
}
)
} }
) )
) )
fun snapshotEntries(realm: Realm) { suspend fun snapshotEntries(realm: Realm) {
val dbMangas = parseToFavoriteEntries( val dbMangas = parseToFavoriteEntries(
loadDbCategories( loadDbCategories(
db.getFavoriteMangas() db.getFavoriteMangas()
.executeAsBlocking() .executeOnIO()
.asSequence() .asSequence()
) )
) )
@ -100,19 +98,16 @@ class LocalFavoritesStorage {
it.category == entry.category it.category == entry.category
} }
private fun loadDbCategories(manga: Sequence<Manga>): Sequence<Pair<Int, Manga>> { private suspend fun loadDbCategories(manga: Sequence<Manga>): Sequence<Pair<Int, Manga>> {
val dbCategories = db.getCategories().executeAsBlocking() val dbCategories = db.getCategories().executeOnIO()
return manga.filter(this::validateDbManga).mapNotNull { return manga.filter(this::validateDbManga).mapNotNull {
val category = db.getCategoriesForManga(it).executeAsBlocking() val category = db.getCategoriesForManga(it).executeAsBlocking()
Pair( dbCategories.indexOf(
dbCategories.indexOf( category.firstOrNull()
category.firstOrNull() ?: return@mapNotNull null
?: return@mapNotNull null ) to it
),
it
)
} }
} }