From 3e785a2555e4ce1cef9becab01305e93b12dc009 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 10 May 2022 17:45:11 +0300 Subject: [PATCH] Refactor and optimization --- .idea/inspectionProfiles/Project_Default.xml | 3 + .../koitharu/kotatsu/utils/ext/DebugExt.kt | 3 + .../kotatsu/base/domain/MangaUtils.kt | 14 +- .../kotatsu/base/ui/BaseFullscreenActivity.kt | 8 +- .../koitharu/kotatsu/base/ui/BaseViewModel.kt | 6 +- .../kotatsu/core/github/GithubModule.kt | 4 +- .../koitharu/kotatsu/core/github/VersionId.kt | 40 ++- .../kotatsu/core/prefs/AppSettings.kt | 1 - .../kotatsu/core/prefs/AppSettingsObserver.kt | 35 +++ .../koitharu/kotatsu/core/prefs/ReaderMode.kt | 2 +- .../kotatsu/core/ui/AppCrashHandler.kt | 3 +- .../kotatsu/details/ui/DetailsActivity.kt | 2 +- .../kotatsu/details/ui/DetailsViewModel.kt | 265 ++++-------------- .../details/ui/MangaDetailsDelegate.kt | 184 ++++++++++++ .../download/domain/DownloadManager.kt | 6 +- .../kotatsu/download/ui/DownloadsActivity.kt | 13 +- .../FavouritesCategoriesViewModel.kt | 11 +- .../ui/categories/adapter/CategoryAD.kt | 2 +- .../history/ui/HistoryListViewModel.kt | 15 +- .../kotatsu/list/ui/MangaListViewModel.kt | 25 +- .../list/ui/adapter/CurrentFilterAD.kt | 2 +- .../list/ui/filter/FilterCoordinator.kt | 10 +- .../kotatsu/local/ui/LocalListFragment.kt | 6 +- .../kotatsu/local/ui/LocalListViewModel.kt | 8 +- .../koitharu/kotatsu/main/ui/MainViewModel.kt | 11 +- .../kotatsu/reader/data/ModelMapping.kt | 27 ++ .../kotatsu/reader/ui/ReaderActivity.kt | 100 +++---- .../reader/ui/ReaderControlDelegate.kt | 28 +- .../kotatsu/reader/ui/ReaderManager.kt | 45 +++ .../kotatsu/reader/ui/ReaderToastView.kt | 26 +- .../kotatsu/reader/ui/ReaderViewModel.kt | 94 +++---- .../remotelist/ui/RemoteListViewModel.kt | 8 +- .../kotatsu/settings/AppUpdateChecker.kt | 33 +-- .../settings/SourceSettingsFragment.kt | 6 +- .../settings/backup/BackupSettingsFragment.kt | 9 +- .../utils/LifecycleAwareServiceConnection.kt | 26 +- .../koitharu/kotatsu/utils/ext/AndroidExt.kt | 11 + .../kotatsu/utils/ext/CoroutineExt.kt | 9 - .../koitharu/kotatsu/utils/ext/LiveDataExt.kt | 1 + app/src/main/res/menu/opt_reader_bottom.xml | 4 + .../koitharu/kotatsu/utils/ext/DebugExt.kt | 5 + 41 files changed, 593 insertions(+), 518 deletions(-) create mode 100644 app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt create mode 100644 app/src/release/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index a6fb1fbe4..2bcd23609 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -4,6 +4,9 @@ + + diff --git a/app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt b/app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt new file mode 100644 index 000000000..e00bb6a83 --- /dev/null +++ b/app/src/debug/java/org/koitharu/kotatsu/utils/ext/DebugExt.kt @@ -0,0 +1,3 @@ +package org.koitharu.kotatsu.utils.ext + +fun Throwable.printStackTraceDebug() = printStackTrace() \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt index 03b0dd53b..6c481e467 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt @@ -3,21 +3,21 @@ package org.koitharu.kotatsu.base.domain import android.graphics.BitmapFactory import android.net.Uri import android.util.Size +import java.io.File +import java.io.InputStream +import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import okhttp3.OkHttpClient import okhttp3.Request import org.koin.core.component.KoinComponent import org.koin.core.component.get -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.medianOrNull -import java.io.File -import java.io.InputStream -import java.util.zip.ZipFile +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug object MangaUtils : KoinComponent { @@ -53,9 +53,7 @@ object MangaUtils : KoinComponent { } return size.width * 2 < size.height } catch (e: Exception) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } + e.printStackTraceDebug() return null } } @@ -78,4 +76,4 @@ object MangaUtils : KoinComponent { check(imageHeight > 0 && imageWidth > 0) return Size(imageWidth, imageHeight) } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt index 64317e4a7..e43ca8877 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseFullscreenActivity.kt @@ -7,10 +7,12 @@ import android.view.View import android.view.WindowManager import androidx.viewbinding.ViewBinding +@Suppress("DEPRECATION") private const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN +@Suppress("DEPRECATION") private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or @@ -18,7 +20,8 @@ private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY -abstract class BaseFullscreenActivity : BaseActivity(), +abstract class BaseFullscreenActivity : + BaseActivity(), View.OnSystemUiVisibilityChangeListener { override fun onCreate(savedInstanceState: Bundle?) { @@ -35,16 +38,19 @@ abstract class BaseFullscreenActivity : BaseActivity(), showSystemUI() } + @Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith") @Deprecated("Deprecated in Java") final override fun onSystemUiVisibilityChange(visibility: Int) { onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) } // TODO WindowInsetsControllerCompat works incorrect + @Suppress("DEPRECATION") protected fun hideSystemUI() { window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN } + @Suppress("DEPRECATION") protected fun showSystemUI() { window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN } diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt index 39233e28f..f4904f8ed 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/BaseViewModel.kt @@ -5,9 +5,9 @@ import androidx.lifecycle.viewModelScope import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.* -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.base.ui.util.CountedBooleanLiveData import org.koitharu.kotatsu.utils.SingleLiveEvent +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug abstract class BaseViewModel : ViewModel() { @@ -34,9 +34,7 @@ abstract class BaseViewModel : ViewModel() { } private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable -> - if (BuildConfig.DEBUG) { - throwable.printStackTrace() - } + throwable.printStackTraceDebug() if (throwable !is CancellationException) { onError.postCall(throwable) } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt index 7da9e309f..58d8d22c6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubModule.kt @@ -4,7 +4,5 @@ import org.koin.dsl.module val githubModule get() = module { - factory { - GithubRepository(get()) - } + factory { GithubRepository(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt index 09557cb47..88304755b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt @@ -54,27 +54,23 @@ class VersionId( return result } - companion object { - - private fun variantWeight(variantType: String) = - when (variantType.lowercase(Locale.ROOT)) { - "a", "alpha" -> 1 - "b", "beta" -> 2 - "rc" -> 4 - "" -> 8 - else -> 0 - } - - fun parse(versionName: String): VersionId { - val parts = versionName.substringBeforeLast('-').split('.') - val variant = versionName.substringAfterLast('-', "") - return VersionId( - major = parts.getOrNull(0)?.toIntOrNull() ?: 0, - minor = parts.getOrNull(1)?.toIntOrNull() ?: 0, - build = parts.getOrNull(2)?.toIntOrNull() ?: 0, - variantType = variant.filter(Char::isLetter), - variantNumber = variant.filter(Char::isDigit).toIntOrNull() ?: 0 - ) - } + private fun variantWeight(variantType: String) = when (variantType.lowercase(Locale.ROOT)) { + "a", "alpha" -> 1 + "b", "beta" -> 2 + "rc" -> 4 + "" -> 8 + else -> 0 } +} + +fun VersionId(versionName: String): VersionId { + val parts = versionName.substringBeforeLast('-').split('.') + val variant = versionName.substringAfterLast('-', "") + return VersionId( + major = parts.getOrNull(0)?.toIntOrNull() ?: 0, + minor = parts.getOrNull(1)?.toIntOrNull() ?: 0, + build = parts.getOrNull(2)?.toIntOrNull() ?: 0, + variantType = variant.filter(Char::isLetter), + variantNumber = variant.filter(Char::isDigit).toIntOrNull() ?: 0, + ) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 185336542..dd5f57cd5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.core.prefs -import android.annotation.TargetApi import android.content.Context import android.content.SharedPreferences import android.net.ConnectivityManager diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt new file mode 100644 index 000000000..88c62514c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettingsObserver.kt @@ -0,0 +1,35 @@ +package org.koitharu.kotatsu.core.prefs + +import androidx.lifecycle.liveData +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.flow.flow + +fun AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow { + var lastValue: T = valueProducer() + emit(lastValue) + observe().collect { + if (it == key) { + val value = valueProducer() + if (value != lastValue) { + emit(value) + } + lastValue = value + } + } +} + +fun AppSettings.observeAsLiveData( + context: CoroutineContext, + key: String, + valueProducer: AppSettings.() -> T +) = liveData(context) { + emit(valueProducer()) + observe().collect { + if (it == key) { + val value = valueProducer() + if (value != latestValue) { + emit(value) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt index bfc8b7b83..9ec51d479 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/ReaderMode.kt @@ -10,4 +10,4 @@ enum class ReaderMode(val id: Int) { fun valueOf(id: Int) = values().firstOrNull { it.id == id } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt index 20a7bf0c3..fb3216cb2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/AppCrashHandler.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.util.Log import kotlin.system.exitProcess +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug class AppCrashHandler(private val applicationContext: Context) : Thread.UncaughtExceptionHandler { @@ -13,7 +14,7 @@ class AppCrashHandler(private val applicationContext: Context) : Thread.Uncaught try { applicationContext.startActivity(intent) } catch (t: Throwable) { - t.printStackTrace() + t.printStackTraceDebug() } Log.e("CRASH", e.message, e) exitProcess(1) diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index f6ecc6f0d..7762f512c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -191,7 +191,7 @@ class DetailsActivity : R.id.action_save -> { viewModel.manga.value?.let { val chaptersCount = it.chapters?.size ?: 0 - val branches = viewModel.branches.value.orEmpty() + val branches = viewModel.branches.value?.toList().orEmpty() if (chaptersCount > 5 || branches.size > 1) { showSaveConfirmation(it, chaptersCount, branches) } else { diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index c1624a6b8..31007f673 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -1,131 +1,106 @@ package org.koitharu.kotatsu.details.ui -import androidx.core.os.LocaleListCompat import androidx.lifecycle.asFlow import androidx.lifecycle.asLiveData +import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job +import java.io.IOException +import java.util.* +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.plus -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository -import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException -import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.details.ui.model.ChapterListItem -import org.koitharu.kotatsu.details.ui.model.toListItem import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.util.mapToSet -import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct -import org.koitharu.kotatsu.utils.ext.iterator -import java.io.IOException +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug class DetailsViewModel( - private val intent: MangaIntent, + intent: MangaIntent, private val historyRepository: HistoryRepository, - private val favouritesRepository: FavouritesRepository, + favouritesRepository: FavouritesRepository, private val localMangaRepository: LocalMangaRepository, private val trackingRepository: TrackingRepository, - private val mangaDataRepository: MangaDataRepository, + mangaDataRepository: MangaDataRepository, private val bookmarksRepository: BookmarksRepository, private val settings: AppSettings, ) : BaseViewModel() { + private val delegate = MangaDetailsDelegate( + intent = intent, + settings = settings, + mangaDataRepository = mangaDataRepository, + historyRepository = historyRepository, + localMangaRepository = localMangaRepository, + ) + private var loadingJob: Job - private val mangaData = MutableStateFlow(intent.manga) - private val selectedBranch = MutableStateFlow(null) val onShowToast = SingleLiveEvent() - private val history = mangaData.mapNotNull { it?.id } - .distinctUntilChanged() - .flatMapLatest { mangaId -> - historyRepository.observeOne(mangaId) - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) + private val history = historyRepository.observeOne(delegate.mangaId) + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) - private val favourite = mangaData.mapNotNull { it?.id } - .distinctUntilChanged() - .flatMapLatest { mangaId -> - favouritesRepository.observeCategoriesIds(mangaId).map { it.isNotEmpty() } - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) - - private val newChapters = mangaData.mapNotNull { it?.id } - .distinctUntilChanged() - .mapLatest { mangaId -> - trackingRepository.getNewChaptersCount(mangaId) - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0) - - // Remote manga for saved and saved for remote - private val relatedManga = MutableStateFlow(null) - private val chaptersQuery = MutableStateFlow("") - - private val chaptersReversed = settings.observe() - .filter { it == AppSettings.KEY_REVERSE_CHAPTERS } - .map { settings.chaptersReverse } - .onStart { emit(settings.chaptersReverse) } + private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() } .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) - val manga = mangaData.filterNotNull() - .asLiveData(viewModelScope.coroutineContext) - val favouriteCategories = favourite - .asLiveData(viewModelScope.coroutineContext) - val newChaptersCount = newChapters - .asLiveData(viewModelScope.coroutineContext) - val readingHistory = history - .asLiveData(viewModelScope.coroutineContext) - val isChaptersReversed = chaptersReversed - .asLiveData(viewModelScope.coroutineContext) + private val newChapters = viewModelScope.async(Dispatchers.Default) { + trackingRepository.getNewChaptersCount(delegate.mangaId) + } - val bookmarks = mangaData.flatMapLatest { + private val chaptersQuery = MutableStateFlow("") + + private val chaptersReversed = settings.observeAsFlow(AppSettings.KEY_REVERSE_CHAPTERS) { chaptersReverse } + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) + + val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext) + val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext) + val newChaptersCount = liveData(viewModelScope.coroutineContext) { emit(newChapters.await()) } + val readingHistory = history.asLiveData(viewModelScope.coroutineContext) + val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext) + + val bookmarks = delegate.manga.flatMapLatest { if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList()) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) val onMangaRemoved = SingleLiveEvent() - val branches = mangaData.map { - it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty() + val branches = delegate.manga.map { + val chapters = it?.chapters ?: return@map emptySet() + chapters.mapTo(TreeSet()) { x -> x.branch } }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) val selectedBranchIndex = combine( branches.asFlow(), - selectedBranch + delegate.selectedBranch ) { branches, selected -> branches.indexOf(selected) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) - val isChaptersEmpty = mangaData.mapNotNull { m -> - m?.run { chapters.isNullOrEmpty() } + val isChaptersEmpty = delegate.manga.map { m -> + m?.chapters?.isEmpty() == true }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false) val chapters = combine( combine( - mangaData.map { it?.chapters.orEmpty() }, - relatedManga, - history.map { it?.chapterId }, - newChapters, - selectedBranch - ) { chapters, related, currentId, newCount, branch -> - val relatedChapters = related?.chapters - if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) { - mapChaptersWithSource(chapters, relatedChapters, currentId, newCount, branch) - } else { - mapChapters(chapters, relatedChapters, currentId, newCount, branch) - } + delegate.manga, + delegate.relatedManga, + history, + delegate.selectedBranch, + ) { manga, related, history, branch -> + delegate.mapChapters(manga, related, history, newChapters.await(), branch) }, chaptersReversed, chaptersQuery, @@ -134,7 +109,7 @@ class DetailsViewModel( }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) val selectedBranchValue: String? - get() = selectedBranch.value + get() = delegate.selectedBranch.value init { loadingJob = doLoad() @@ -146,7 +121,11 @@ class DetailsViewModel( } fun deleteLocal() { - val m = mangaData.value ?: return + val m = delegate.manga.value + if (m == null) { + onShowToast.call(R.string.file_not_found) + return + } launchLoadingJob(Dispatchers.Default) { val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m) checkNotNull(manga) { "Cannot find saved manga for ${m.title}" } @@ -171,11 +150,11 @@ class DetailsViewModel( } fun setSelectedBranch(branch: String?) { - selectedBranch.value = branch + delegate.selectedBranch.value = branch } fun getRemoteManga(): Manga? { - return relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL } + return delegate.relatedManga.value?.takeUnless { it.source == MangaSource.LOCAL } } fun performChapterSearch(query: String?) { @@ -183,7 +162,7 @@ class DetailsViewModel( } fun onDownloadComplete(downloadedManga: Manga) { - val currentManga = mangaData.value ?: return + val currentManga = delegate.manga.value ?: return if (currentManga.id != downloadedManga.id) { return } @@ -194,142 +173,16 @@ class DetailsViewModel( runCatching { localMangaRepository.getDetails(downloadedManga) }.onSuccess { - relatedManga.value = it + delegate.relatedManga.value = it }.onFailure { - if (BuildConfig.DEBUG) { - it.printStackTrace() - } + it.printStackTraceDebug() } } } } private fun doLoad() = launchLoadingJob(Dispatchers.Default) { - var manga = mangaDataRepository.resolveIntent(intent) - ?: throw MangaNotFoundException("Cannot find manga") - mangaData.value = manga - manga = MangaRepository(manga.source).getDetails(manga) - // find default branch - val hist = historyRepository.getOne(manga) - selectedBranch.value = if (hist != null) { - val currentChapter = manga.chapters?.find { it.id == hist.chapterId } - if (currentChapter != null) currentChapter.branch else predictBranch(manga.chapters) - } else { - predictBranch(manga.chapters) - } - mangaData.value = manga - relatedManga.value = runCatching { - if (manga.source == MangaSource.LOCAL) { - val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null - MangaRepository(m.source).getDetails(m) - } else { - localMangaRepository.findSavedManga(manga) - } - }.onFailure { error -> - if (BuildConfig.DEBUG) error.printStackTrace() - }.getOrNull() - } - - private fun mapChapters( - chapters: List, - downloadedChapters: List?, - currentId: Long?, - newCount: Int, - branch: String?, - ): List { - val result = ArrayList(chapters.size) - val dateFormat = settings.getDateFormat() - val currentIndex = chapters.indexOfFirst { it.id == currentId } - val firstNewIndex = chapters.size - newCount - val downloadedIds = downloadedChapters?.mapToSet { it.id } - for (i in chapters.indices) { - val chapter = chapters[i] - if (chapter.branch != branch) { - continue - } - result += chapter.toListItem( - isCurrent = i == currentIndex, - isUnread = i > currentIndex, - isNew = i >= firstNewIndex, - isMissing = false, - isDownloaded = downloadedIds?.contains(chapter.id) == true, - dateFormat = dateFormat, - ) - } - return result - } - - private fun mapChaptersWithSource( - chapters: List, - sourceChapters: List, - currentId: Long?, - newCount: Int, - branch: String?, - ): List { - val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id } - val result = ArrayList(sourceChapters.size) - val currentIndex = sourceChapters.indexOfFirst { it.id == currentId } - val firstNewIndex = sourceChapters.size - newCount - val dateFormat = settings.getDateFormat() - for (i in sourceChapters.indices) { - val chapter = sourceChapters[i] - val localChapter = chaptersMap.remove(chapter.id) - if (chapter.branch != branch) { - continue - } - result += localChapter?.toListItem( - isCurrent = i == currentIndex, - isUnread = i > currentIndex, - isNew = i >= firstNewIndex, - isMissing = false, - isDownloaded = false, - dateFormat = dateFormat, - ) ?: chapter.toListItem( - isCurrent = i == currentIndex, - isUnread = i > currentIndex, - isNew = i >= firstNewIndex, - isMissing = true, - isDownloaded = false, - dateFormat = dateFormat, - ) - } - if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source - result.ensureCapacity(result.size + chaptersMap.size) - chaptersMap.values.mapNotNullTo(result) { - if (it.branch == branch) { - it.toListItem( - isCurrent = false, - isUnread = true, - isNew = false, - isMissing = false, - isDownloaded = false, - dateFormat = dateFormat, - ) - } else { - null - } - } - result.sortBy { it.chapter.number } - } - return result - } - - private fun predictBranch(chapters: List?): String? { - if (chapters.isNullOrEmpty()) { - return null - } - val groups = chapters.groupBy { it.branch } - for (locale in LocaleListCompat.getAdjustedDefault()) { - var language = locale.getDisplayLanguage(locale).toTitleCase(locale) - if (groups.containsKey(language)) { - return language - } - language = locale.getDisplayName(locale).toTitleCase(locale) - if (groups.containsKey(language)) { - return language - } - } - return groups.maxByOrNull { it.value.size }?.key + delegate.doLoad() } private fun List.filterSearch(query: String): List { diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt new file mode 100644 index 000000000..07f03dbda --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt @@ -0,0 +1,184 @@ +package org.koitharu.kotatsu.details.ui + +import androidx.core.os.LocaleListCompat +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.koitharu.kotatsu.base.domain.MangaDataRepository +import org.koitharu.kotatsu.base.domain.MangaIntent +import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException +import org.koitharu.kotatsu.core.model.MangaHistory +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.details.ui.model.ChapterListItem +import org.koitharu.kotatsu.details.ui.model.toListItem +import org.koitharu.kotatsu.history.domain.HistoryRepository +import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.mapToSet +import org.koitharu.kotatsu.parsers.util.toTitleCase +import org.koitharu.kotatsu.utils.ext.iterator +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug + +class MangaDetailsDelegate( + private val intent: MangaIntent, + private val settings: AppSettings, + private val mangaDataRepository: MangaDataRepository, + private val historyRepository: HistoryRepository, + private val localMangaRepository: LocalMangaRepository, +) { + + private val mangaData = MutableStateFlow(intent.manga) + + val selectedBranch = MutableStateFlow(null) + // Remote manga for saved and saved for remote + val relatedManga = MutableStateFlow(null) + val manga: StateFlow + get() = mangaData + val mangaId = intent.manga?.id ?: intent.mangaId + + suspend fun doLoad() { + var manga = mangaDataRepository.resolveIntent(intent) + ?: throw MangaNotFoundException("Cannot find manga") + mangaData.value = manga + manga = MangaRepository(manga.source).getDetails(manga) + // find default branch + val hist = historyRepository.getOne(manga) + selectedBranch.value = if (hist != null) { + val currentChapter = manga.chapters?.find { it.id == hist.chapterId } + if (currentChapter != null) currentChapter.branch else predictBranch(manga.chapters) + } else { + predictBranch(manga.chapters) + } + mangaData.value = manga + relatedManga.value = runCatching { + if (manga.source == MangaSource.LOCAL) { + val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null + MangaRepository(m.source).getDetails(m) + } else { + localMangaRepository.findSavedManga(manga) + } + }.onFailure { error -> + error.printStackTraceDebug() + }.getOrNull() + } + + fun mapChapters( + manga: Manga?, + related: Manga?, + history: MangaHistory?, + newCount: Int, + branch: String?, + ): List { + val chapters = manga?.chapters ?: return emptyList() + val relatedChapters = related?.chapters + return if (related?.source != MangaSource.LOCAL && !relatedChapters.isNullOrEmpty()) { + mapChaptersWithSource(chapters, relatedChapters, history?.chapterId, newCount, branch) + } else { + mapChapters(chapters, relatedChapters, history?.chapterId, newCount, branch) + } + } + + private fun mapChapters( + chapters: List, + downloadedChapters: List?, + currentId: Long?, + newCount: Int, + branch: String?, + ): List { + val result = ArrayList(chapters.size) + val dateFormat = settings.getDateFormat() + val currentIndex = chapters.indexOfFirst { it.id == currentId } + val firstNewIndex = chapters.size - newCount + val downloadedIds = downloadedChapters?.mapToSet { it.id } + for (i in chapters.indices) { + val chapter = chapters[i] + if (chapter.branch != branch) { + continue + } + result += chapter.toListItem( + isCurrent = i == currentIndex, + isUnread = i > currentIndex, + isNew = i >= firstNewIndex, + isMissing = false, + isDownloaded = downloadedIds?.contains(chapter.id) == true, + dateFormat = dateFormat, + ) + } + return result + } + + private fun mapChaptersWithSource( + chapters: List, + sourceChapters: List, + currentId: Long?, + newCount: Int, + branch: String?, + ): List { + val chaptersMap = chapters.associateByTo(HashMap(chapters.size)) { it.id } + val result = ArrayList(sourceChapters.size) + val currentIndex = sourceChapters.indexOfFirst { it.id == currentId } + val firstNewIndex = sourceChapters.size - newCount + val dateFormat = settings.getDateFormat() + for (i in sourceChapters.indices) { + val chapter = sourceChapters[i] + val localChapter = chaptersMap.remove(chapter.id) + if (chapter.branch != branch) { + continue + } + result += localChapter?.toListItem( + isCurrent = i == currentIndex, + isUnread = i > currentIndex, + isNew = i >= firstNewIndex, + isMissing = false, + isDownloaded = false, + dateFormat = dateFormat, + ) ?: chapter.toListItem( + isCurrent = i == currentIndex, + isUnread = i > currentIndex, + isNew = i >= firstNewIndex, + isMissing = true, + isDownloaded = false, + dateFormat = dateFormat, + ) + } + if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source + result.ensureCapacity(result.size + chaptersMap.size) + chaptersMap.values.mapNotNullTo(result) { + if (it.branch == branch) { + it.toListItem( + isCurrent = false, + isUnread = true, + isNew = false, + isMissing = false, + isDownloaded = false, + dateFormat = dateFormat, + ) + } else { + null + } + } + result.sortBy { it.chapter.number } + } + return result + } + + private fun predictBranch(chapters: List?): String? { + if (chapters.isNullOrEmpty()) { + return null + } + val groups = chapters.groupBy { it.branch } + for (locale in LocaleListCompat.getAdjustedDefault()) { + var language = locale.getDisplayLanguage(locale).toTitleCase(locale) + if (groups.containsKey(language)) { + return language + } + language = locale.getDisplayName(locale).toTitleCase(locale) + if (groups.containsKey(language)) { + return language + } + } + return groups.maxByOrNull { it.value.size }?.key + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt index 58335ed31..d079eb51f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt @@ -12,7 +12,6 @@ import kotlinx.coroutines.sync.Semaphore import okhttp3.OkHttpClient import okhttp3.Request import okio.IOException -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.parser.MangaRepository @@ -24,6 +23,7 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.utils.ext.deleteAwait +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.waitForNetwork import org.koitharu.kotatsu.utils.progress.ProgressJob @@ -156,9 +156,7 @@ class DownloadManager( outState.value = DownloadState.Cancelled(startId, manga, cover) throw e } catch (e: Throwable) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } + e.printStackTraceDebug() outState.value = DownloadState.Error(startId, manga, cover, e) } finally { withContext(NonCancellable) { diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt index e249e4dc5..a0c6c63dd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadsActivity.kt @@ -3,10 +3,8 @@ package org.koitharu.kotatsu.download.ui import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.ViewGroup import androidx.core.graphics.Insets import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.flatMapLatest @@ -17,7 +15,7 @@ import org.koin.android.ext.android.get import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding import org.koitharu.kotatsu.download.ui.service.DownloadService -import org.koitharu.kotatsu.utils.LifecycleAwareServiceConnection +import org.koitharu.kotatsu.utils.bindServiceWithLifecycle class DownloadsActivity : BaseActivity() { @@ -28,11 +26,10 @@ class DownloadsActivity : BaseActivity() { val adapter = DownloadsAdapter(lifecycleScope, get()) binding.recyclerView.setHasFixedSize(true) binding.recyclerView.adapter = adapter - LifecycleAwareServiceConnection.bindService( - this, - this, - Intent(this, DownloadService::class.java), - 0 + bindServiceWithLifecycle( + owner = this, + service = Intent(this, DownloadService::class.java), + flags = 0, ).service.flatMapLatest { binder -> (binder as? DownloadService.DownloadBinder)?.downloads ?: flowOf(null) }.onEach { diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt index 46dc79586..1e24d033f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt @@ -3,10 +3,11 @@ package org.koitharu.kotatsu.favourites.ui.categories import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct @@ -70,9 +71,7 @@ class FavouritesCategoriesViewModel( return result } - private fun observeAllCategoriesVisible() = settings.observe() - .filter { it == AppSettings.KEY_ALL_FAVOURITES_VISIBLE } - .map { settings.isAllFavouritesVisible } - .onStart { emit(settings.isAllFavouritesVisible) } - .distinctUntilChanged() + private fun observeAllCategoriesVisible() = settings.observeAsFlow(AppSettings.KEY_ALL_FAVOURITES_VISIBLE) { + isAllFavouritesVisible + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt index d840b783f..e64e36e5a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt @@ -16,7 +16,7 @@ fun categoryAD( clickListener.onItemClick(item.category, it) } @Suppress("ClickableViewAccessibility") - binding.imageViewHandle.setOnTouchListener { v, event -> + binding.imageViewHandle.setOnTouchListener { _, event -> if (event.actionMasked == MotionEvent.ACTION_DOWN) { clickListener.onItemLongClick(item.category, itemView) } else { diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index 42dd81e95..1e14aeeca 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -2,14 +2,15 @@ package org.koitharu.kotatsu.history.ui import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import java.util.* -import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.MangaWithHistory @@ -19,6 +20,8 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.onFirst +import java.util.* +import java.util.concurrent.TimeUnit class HistoryListViewModel( private val repository: HistoryRepository, @@ -29,11 +32,7 @@ class HistoryListViewModel( val isGroupingEnabled = MutableLiveData() - private val historyGrouping = settings.observe() - .filter { it == AppSettings.KEY_HISTORY_GROUPING } - .map { settings.historyGrouping } - .onStart { emit(settings.historyGrouping) } - .distinctUntilChanged() + private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { historyGrouping } .onEach { isGroupingEnabled.postValue(it) } override val content = combine( diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index 20f768c2f..6adc8c0d2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -4,16 +4,14 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.onEach import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.core.prefs.observeAsFlow +import org.koitharu.kotatsu.core.prefs.observeAsLiveData import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.list.ui.model.MangaGridModel -import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel -import org.koitharu.kotatsu.list.ui.model.MangaListModel import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct abstract class MangaListViewModel( private val settings: AppSettings, @@ -21,20 +19,15 @@ abstract class MangaListViewModel( abstract val content: LiveData> val listMode = MutableLiveData() - val gridScale = settings.observe() - .filter { it == AppSettings.KEY_GRID_SIZE } - .map { settings.gridSize / 100f } - .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO) { - settings.gridSize / 100f - } + val gridScale = settings.observeAsLiveData( + context = viewModelScope.coroutineContext + Dispatchers.Default, + key = AppSettings.KEY_GRID_SIZE, + valueProducer = { gridSize / 100f }, + ) open fun onRemoveFilterTag(tag: MangaTag) = Unit - protected fun createListModeFlow() = settings.observe() - .filter { it == AppSettings.KEY_LIST_MODE } - .map { settings.listMode } - .onStart { emit(settings.listMode) } - .distinctUntilChanged() + protected fun createListModeFlow() = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode } .onEach { if (listMode.value != it) { listMode.postValue(it) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt index 86b72c738..c13fd3cfa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/CurrentFilterAD.kt @@ -13,7 +13,7 @@ fun currentFilterAD( val chipGroup = itemView as ChipsView - chipGroup.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { chip, data -> + chipGroup.onChipCloseClickListener = ChipsView.OnChipCloseClickListener { _, data -> listener.onTagRemoveClick(data as? MangaTag ?: return@OnChipCloseClickListener) } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt index acba2466c..0cbb4fad7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt @@ -1,18 +1,18 @@ package org.koitharu.kotatsu.list.ui.filter import androidx.annotation.WorkerThread +import java.util.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.* -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct -import java.util.* +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug class FilterCoordinator( private val repository: RemoteMangaRepository, @@ -113,7 +113,7 @@ class FilterCoordinator( FilterItem.Sort(it, isSelected = it == state.sortOrder) } } - if(allTags.isLoading || allTags.isError || tags.isNotEmpty()) { + if (allTags.isLoading || allTags.isError || tags.isNotEmpty()) { list.add(FilterItem.Header(R.string.genres, state.tags.size)) tags.mapTo(list) { FilterItem.Tag(it, isChecked = it in state.tags) @@ -153,9 +153,7 @@ class FilterCoordinator( runCatching { repository.getTags() }.onFailure { error -> - if (BuildConfig.DEBUG) { - error.printStackTrace() - } + error.printStackTraceDebug() }.getOrNull() } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt index 4e5115ac8..fc2dbad03 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListFragment.kt @@ -15,11 +15,11 @@ import androidx.core.net.toUri import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.utils.ShareHelper +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.progress.Progress class LocalListFragment : MangaListFragment(), ActivityResultCallback> { @@ -68,9 +68,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback - if (BuildConfig.DEBUG) { - error.printStackTrace() - } + error.printStackTraceDebug() } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt index f2b98d7e0..17a2c25b2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.onStart import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.observeAsLiveData import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.SingleLiveEvent @@ -21,11 +22,11 @@ class MainViewModel( val onOpenReader = SingleLiveEvent() var defaultSection by settings::defaultSection - val isSuggestionsEnabled = settings.observe() - .filter { it == AppSettings.KEY_SUGGESTIONS } - .onStart { emit("") } - .map { settings.isSuggestionsEnabled } - .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + val isSuggestionsEnabled = settings.observeAsLiveData( + context = viewModelScope.coroutineContext + Dispatchers.Default, + key = AppSettings.KEY_SUGGESTIONS, + valueProducer = { isSuggestionsEnabled } + ) val isResumeEnabled = historyRepository .observeHasItems() diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt b/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt new file mode 100644 index 000000000..289f44386 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/data/ModelMapping.kt @@ -0,0 +1,27 @@ +package org.koitharu.kotatsu.reader.data + +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter + +fun Manga.filterChapters(branch: String?): Manga { + if (chapters.isNullOrEmpty()) return this + return copy(chapters = chapters?.filter { it.branch == branch }) +} + +private fun Manga.copy(chapters: List?) = Manga( + id = id, + title = title, + altTitle = altTitle, + url = url, + publicUrl = publicUrl, + rating = rating, + isNsfw = isNsfw, + coverUrl = coverUrl, + tags = tags, + state = state, + author = author, + largeCoverUrl = largeCoverUrl, + description = description, + chapters = chapters, + source = source, +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index c54ed94d6..e4a29b948 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -6,11 +6,13 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.* -import android.widget.Toast import androidx.activity.result.ActivityResultCallback import androidx.core.graphics.Insets -import androidx.core.view.* -import androidx.fragment.app.commit +import androidx.core.view.OnApplyWindowInsetsListener +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isVisible +import androidx.core.view.updatePadding +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.transition.Slide import androidx.transition.TransitionManager @@ -37,11 +39,7 @@ import org.koitharu.kotatsu.databinding.ActivityReaderBinding import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaPage -import org.koitharu.kotatsu.reader.ui.pager.BaseReader import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState -import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment -import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment -import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet import org.koitharu.kotatsu.settings.SettingsActivity @@ -51,6 +49,8 @@ import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.hasGlobalPoint import org.koitharu.kotatsu.utils.ext.observeWithPrevious +import org.koitharu.kotatsu.utils.ext.postDelayed +import java.util.concurrent.TimeUnit class ReaderActivity : BaseFullscreenActivity(), @@ -75,13 +75,13 @@ class ReaderActivity : private lateinit var controlDelegate: ReaderControlDelegate private val savePageRequest = registerForActivityResult(PageSaveContract(), this) private var gestureInsets: Insets = Insets.NONE - - private val reader - get() = supportFragmentManager.findFragmentById(R.id.container) as? BaseReader<*> + private lateinit var readerManager: ReaderManager + private val hideUiRunnable = Runnable { setUiIsVisible(false) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityReaderBinding.inflate(layoutInflater)) + readerManager = ReaderManager(supportFragmentManager, R.id.container) supportActionBar?.setDisplayHomeAsUpEnabled(true) touchHelper = GridTouchHelper(this, this) orientationHelper = ScreenOrientationHelper(this) @@ -91,6 +91,7 @@ class ReaderActivity : insetsDelegate.interceptingWindowInsetsListener = this orientationHelper.observeAutoOrientation() + .flowWithLifecycle(lifecycle) .onEach { binding.toolbarBottom.menu.findItem(R.id.action_screen_rotate).isVisible = !it }.launchIn(lifecycleScope) @@ -113,33 +114,20 @@ class ReaderActivity : } private fun onInitReader(mode: ReaderMode) { - val currentReader = reader - when (mode) { - ReaderMode.WEBTOON -> if (currentReader !is WebtoonReaderFragment) { - supportFragmentManager.commit { - replace(R.id.container, WebtoonReaderFragment()) - } - } - ReaderMode.REVERSED -> if (currentReader !is ReversedReaderFragment) { - supportFragmentManager.commit { - replace(R.id.container, ReversedReaderFragment()) - } - } - ReaderMode.STANDARD -> if (currentReader !is PagerReaderFragment) { - supportFragmentManager.commit { - replace(R.id.container, PagerReaderFragment()) - } - } + if (readerManager.currentMode != mode) { + readerManager.replace(mode) } - binding.toolbarBottom.menu.findItem(R.id.action_reader_mode).setIcon( - when (mode) { - ReaderMode.WEBTOON -> R.drawable.ic_script - ReaderMode.REVERSED -> R.drawable.ic_read_reversed - ReaderMode.STANDARD -> R.drawable.ic_book_page - } - ) - binding.appbarTop.postDelayed(1000) { - setUiIsVisible(false) + val iconRes = when (mode) { + ReaderMode.WEBTOON -> R.drawable.ic_script + ReaderMode.REVERSED -> R.drawable.ic_read_reversed + ReaderMode.STANDARD -> R.drawable.ic_book_page + } + binding.toolbarBottom.menu.findItem(R.id.action_reader_mode).run { + setIcon(iconRes) + setVisible(true) + } + if (binding.appbarTop.isVisible) { + lifecycle.postDelayed(hideUiRunnable, TimeUnit.SECONDS.toMillis(1)) } } @@ -151,18 +139,8 @@ class ReaderActivity : override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_reader_mode -> { - ReaderConfigDialog.show( - supportFragmentManager, - when (reader) { - is PagerReaderFragment -> ReaderMode.STANDARD - is WebtoonReaderFragment -> ReaderMode.WEBTOON - is ReversedReaderFragment -> ReaderMode.REVERSED - else -> { - showWaitWhileLoading() - return false - } - } - ) + val currentMode = readerManager.currentMode ?: return false + ReaderConfigDialog.show(supportFragmentManager, currentMode) } R.id.action_settings -> { startActivity(SettingsActivity.newReaderSettingsIntent(this)) @@ -184,17 +162,17 @@ class ReaderActivity : supportFragmentManager, pages, title?.toString().orEmpty(), - reader?.getCurrentState()?.page ?: -1 + readerManager.currentReader?.getCurrentState()?.page ?: -1, ) } else { - showWaitWhileLoading() + return false } } R.id.action_save_page -> { viewModel.getCurrentPage()?.also { page -> - viewModel.saveCurrentState(reader?.getCurrentState()) + viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState()) viewModel.saveCurrentPage(page, savePageRequest) - } ?: showWaitWhileLoading() + } ?: return false } R.id.action_bookmark -> { if (viewModel.isBookmarkAdded.value == true) { @@ -216,10 +194,14 @@ class ReaderActivity : val hasPages = !viewModel.content.value?.pages.isNullOrEmpty() binding.layoutLoading.isVisible = isLoading && !hasPages if (isLoading && hasPages) { - binding.toastView.show(R.string.loading_, true) + binding.toastView.show(R.string.loading_) } else { binding.toastView.hide() } + val menu = binding.toolbarBottom.menu + menu.findItem(R.id.action_bookmark).isVisible = hasPages + menu.findItem(R.id.action_pages_thumbs).isVisible = hasPages + menu.findItem(R.id.action_save_page).isVisible = hasPages } private fun onError(e: Throwable) { @@ -279,14 +261,14 @@ class ReaderActivity : val index = pages.indexOfFirst { it.id == page.id } if (index != -1) { withContext(Dispatchers.Main) { - reader?.switchPageTo(index, true) + readerManager.currentReader?.switchPageTo(index, true) } } } } override fun onReaderModeChanged(mode: ReaderMode) { - viewModel.saveCurrentState(reader?.getCurrentState()) + viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState()) viewModel.switchMode(mode) } @@ -304,12 +286,6 @@ class ReaderActivity : } } - private fun showWaitWhileLoading() { - Toast.makeText(this, R.string.wait_for_loading_finish, Toast.LENGTH_SHORT).apply { - setGravity(Gravity.CENTER, 0, 0) - }.show() - } - private fun setWindowSecure(isSecure: Boolean) { if (isSecure) { window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) @@ -358,7 +334,7 @@ class ReaderActivity : override fun onWindowInsetsChanged(insets: Insets) = Unit override fun switchPageBy(delta: Int) { - reader?.switchPageBy(delta) + readerManager.currentReader?.switchPageBy(delta) } override fun toggleUiVisibility() { diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt index f8c5d73c0..dbe853894 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt @@ -5,14 +5,16 @@ import android.view.SoundEffectConstants import android.view.View import androidx.lifecycle.LifecycleCoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.utils.GridTouchHelper -@Suppress("UNUSED_PARAMETER") class ReaderControlDelegate( - private val scope: LifecycleCoroutineScope, - private val settings: AppSettings, + scope: LifecycleCoroutineScope, + settings: AppSettings, private val listener: OnInteractionListener ) { @@ -20,12 +22,8 @@ class ReaderControlDelegate( private var isVolumeKeysSwitchEnabled: Boolean = false init { - settings.observe() - .filter { it == AppSettings.KEY_READER_SWITCHERS } - .map { settings.readerPageSwitch } - .onStart { emit(settings.readerPageSwitch) } - .distinctUntilChanged() - .flowOn(Dispatchers.IO) + settings.observeAsFlow(AppSettings.KEY_READER_SWITCHERS) { readerPageSwitch } + .flowOn(Dispatchers.Default) .onEach { isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in it isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS in it @@ -57,7 +55,7 @@ class ReaderControlDelegate( } } - fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean = when (keyCode) { + fun onKeyDown(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean = when (keyCode) { KeyEvent.KEYCODE_VOLUME_UP -> if (isVolumeKeysSwitchEnabled) { listener.switchPageBy(-1) true @@ -92,9 +90,11 @@ class ReaderControlDelegate( else -> false } - fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { - return (isVolumeKeysSwitchEnabled && - (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP)) + fun onKeyUp(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean { + return ( + isVolumeKeysSwitchEnabled && + (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) + ) } interface OnInteractionListener { diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt new file mode 100644 index 000000000..c5497fe8a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderManager.kt @@ -0,0 +1,45 @@ +package org.koitharu.kotatsu.reader.ui + +import androidx.annotation.IdRes +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.commit +import org.koitharu.kotatsu.core.prefs.ReaderMode +import org.koitharu.kotatsu.reader.ui.pager.BaseReader +import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment +import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment +import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment +import java.util.* + +class ReaderManager( + private val fragmentManager: FragmentManager, + @IdRes private val containerResId: Int, +) { + + private val modeMap = EnumMap>>(ReaderMode::class.java) + + init { + modeMap[ReaderMode.STANDARD] = PagerReaderFragment::class.java + modeMap[ReaderMode.REVERSED] = ReversedReaderFragment::class.java + modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java + } + + val currentReader: BaseReader<*>? + get() = fragmentManager.findFragmentById(containerResId) as? BaseReader<*> + + val currentMode: ReaderMode? + get() { + val readerClass = currentReader?.javaClass ?: return null + return modeMap.entries.find { it.value == readerClass }?.key + } + + fun replace(newMode: ReaderMode) { + val readerClass = requireNotNull(modeMap[newMode]) + fragmentManager.commit { + replace(containerResId, readerClass, null, null) + } + } + + fun replace(reader: BaseReader<*>) { + fragmentManager.commit { replace(containerResId, reader) } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt index f9852f4c6..a2acc8df7 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderToastView.kt @@ -1,13 +1,11 @@ package org.koitharu.kotatsu.reader.ui import android.content.Context -import android.graphics.Color import android.util.AttributeSet import android.view.Gravity import android.view.ViewGroup import androidx.annotation.StringRes import androidx.core.view.isVisible -import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.transition.Fade import androidx.transition.Slide import androidx.transition.TransitionManager @@ -15,26 +13,28 @@ import androidx.transition.TransitionSet import com.google.android.material.textview.MaterialTextView class ReaderToastView @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, ) : MaterialTextView(context, attrs, defStyleAttr) { private var hideRunnable = Runnable { hide() } - fun show(message: CharSequence, isLoading: Boolean) { + fun show(message: CharSequence) { removeCallbacks(hideRunnable) text = message setupTransition() isVisible = true } - fun show(@StringRes messageId: Int, isLoading: Boolean) { - show(context.getString(messageId), isLoading) + fun show(@StringRes messageId: Int) { + show(context.getString(messageId)) } fun showTemporary(message: CharSequence, duration: Long) { - show(message, false) + show(message) postDelayed(hideRunnable, duration) } @@ -49,7 +49,7 @@ class ReaderToastView @JvmOverloads constructor( super.onDetachedFromWindow() } - private fun setupTransition () { + private fun setupTransition() { val parentView = parent as? ViewGroup ?: return val transition = TransitionSet() .setOrdering(TransitionSet.ORDERING_TOGETHER) @@ -58,14 +58,4 @@ class ReaderToastView @JvmOverloads constructor( .addTransition(Fade()) TransitionManager.beginDelayedTransition(parentView, transition) } - - // FIXME use it as compound drawable - private fun createProgressDrawable(): CircularProgressDrawable { - val drawable = CircularProgressDrawable(context) - drawable.setStyle(CircularProgressDrawable.DEFAULT) - drawable.arrowEnabled = false - drawable.setColorSchemeColors(Color.WHITE) - drawable.centerRadius = lineHeight / 3f - return drawable - } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index f9ba0655f..784c844ff 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -8,9 +8,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import org.koin.core.component.KoinComponent -import org.koin.core.component.get -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaIntent @@ -21,22 +18,25 @@ import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.prefs.ReaderMode -import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy +import org.koitharu.kotatsu.core.prefs.* import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.reader.data.filterChapters import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.ext.IgnoreErrors import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.processLifecycleScope import java.util.* +private const val BOUNDS_PAGE_OFFSET = 2 +private const val PAGES_TRIM_THRESHOLD = 120 +private const val PREFETCH_LIMIT = 10 + class ReaderViewModel( private val intent: MangaIntent, initialState: ReaderState?, @@ -78,22 +78,19 @@ class ReaderViewModel( val manga: Manga? get() = mangaData.value - val readerAnimation = settings.observe() - .filter { it == AppSettings.KEY_READER_ANIMATION } - .map { settings.readerAnimation } - .onStart { emit(settings.readerAnimation) } - .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO) + val readerAnimation = settings.observeAsLiveData( + context = viewModelScope.coroutineContext + Dispatchers.Default, + key = AppSettings.KEY_READER_ANIMATION, + valueProducer = { readerAnimation } + ) val isScreenshotsBlockEnabled = combine( mangaData, - settings.observe() - .filter { it == AppSettings.KEY_SCREENSHOTS_POLICY } - .onStart { emit("") } - .map { settings.screenshotsPolicy }, + settings.observeAsFlow(AppSettings.KEY_SCREENSHOTS_POLICY) { screenshotsPolicy }, ) { manga, policy -> policy == ScreenshotsPolicy.BLOCK_ALL || (policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw) - }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO) + }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) val onZoomChanged = SingleLiveEvent() @@ -142,7 +139,7 @@ class ReaderViewModel( if (state != null) { currentState.value = state } - saveState( + historyRepository.saveStateAsync( mangaData.value ?: return, state ?: currentState.value ?: return ) @@ -169,9 +166,7 @@ class ReaderViewModel( } catch (e: CancellationException) { throw e } catch (e: Exception) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } + e.printStackTraceDebug() onPageSaved.postCall(null) } } @@ -286,7 +281,7 @@ class ReaderViewModel( } val branch = chapters[currentState.value?.chapterId ?: 0L].branch - mangaData.value = manga.copy(chapters = manga.chapters?.filter { it.branch == branch }) + mangaData.value = manga.filterChapters(branch) readerMode.postValue(mode) val pages = loadChapter(requireNotNull(currentState.value).chapterId) @@ -349,9 +344,9 @@ class ReaderViewModel( private fun subscribeToSettings() { settings.observe() - .filter { it == AppSettings.KEY_ZOOM_MODE } - .onEach { onZoomChanged.postCall(Unit) } - .launchIn(viewModelScope + Dispatchers.IO) + .onEach { key -> + if (key == AppSettings.KEY_ZOOM_MODE) onZoomChanged.postCall(Unit) + }.launchIn(viewModelScope + Dispatchers.Default) } private fun List.trySublist(fromIndex: Int, toIndex: Int): List { @@ -363,40 +358,23 @@ class ReaderViewModel( subList(fromIndexBounded, toIndexBounded) } } +} - private fun Manga.copy(chapters: List?) = Manga( - id = id, - title = title, - altTitle = altTitle, - url = url, - publicUrl = publicUrl, - rating = rating, - isNsfw = isNsfw, - coverUrl = coverUrl, - tags = tags, - state = state, - author = author, - largeCoverUrl = largeCoverUrl, - description = description, - chapters = chapters, - source = source, - ) - - private companion object : KoinComponent { - - const val BOUNDS_PAGE_OFFSET = 2 - const val PAGES_TRIM_THRESHOLD = 120 - const val PREFETCH_LIMIT = 10 - - fun saveState(manga: Manga, state: ReaderState) { - processLifecycleScope.launch(Dispatchers.Default + IgnoreErrors) { - get().addOrUpdate( - manga = manga, - chapterId = state.chapterId, - page = state.page, - scroll = state.scroll - ) - } +/** + * This function is not a member of the ReaderViewModel + * because it should work independently of the ViewModel's lifecycle. + */ +private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState): Job { + return processLifecycleScope.launch(Dispatchers.Default) { + runCatching { + addOrUpdate( + manga = manga, + chapterId = state.chapterId, + page = state.page, + scroll = state.scroll + ) + }.onFailure { + it.printStackTraceDebug() } } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index b2a540baa..9c9922598 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -6,7 +6,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.* -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.ui.widgets.ChipsView @@ -21,6 +20,7 @@ import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug private const val FILTER_MIN_INTERVAL = 750L @@ -133,9 +133,7 @@ class RemoteListViewModel( } hasNextPage.value = list.isNotEmpty() } catch (e: Throwable) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } + e.printStackTraceDebug() listError.value = e if (!mangaList.value.isNullOrEmpty()) { onError.postCall(e) @@ -158,4 +156,4 @@ class RemoteListViewModel( textSecondary = 0, actionStringRes = if (filterState.tags.isEmpty()) 0 else R.string.reset_filter, ) -} +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt index 5b7bc661e..a9e2ab345 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt @@ -8,15 +8,6 @@ import android.net.Uri import androidx.activity.ComponentActivity import androidx.annotation.MainThread import com.google.android.material.dialog.MaterialAlertDialogBuilder -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import java.security.cert.CertificateEncodingException -import java.security.cert.CertificateException -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koin.android.ext.android.get @@ -28,6 +19,16 @@ import org.koitharu.kotatsu.core.github.VersionId import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.parsers.util.byte2HexFormatted import org.koitharu.kotatsu.utils.FileSize +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.security.cert.CertificateEncodingException +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit class AppUpdateChecker(private val activity: ComponentActivity) { @@ -45,8 +46,8 @@ class AppUpdateChecker(private val activity: ComponentActivity) { suspend fun checkNow() = runCatching { val version = repo.getLatestVersion() - val newVersionId = VersionId.parse(version.name) - val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME) + val newVersionId = VersionId(version.name) + val currentVersionId = VersionId(BuildConfig.VERSION_NAME) val result = newVersionId > currentVersionId if (result) { withContext(Dispatchers.Main) { @@ -56,7 +57,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) { settings.lastUpdateCheckTimestamp = System.currentTimeMillis() result }.onFailure { - it.printStackTrace() + it.printStackTraceDebug() }.getOrNull() @MainThread @@ -99,7 +100,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) { PackageManager.GET_SIGNATURES ) } catch (e: PackageManager.NameNotFoundException) { - e.printStackTrace() + e.printStackTraceDebug() return null } val signatures = packageInfo?.signatures @@ -109,7 +110,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) { val cf = CertificateFactory.getInstance("X509") cf.generateCertificate(input) as X509Certificate } catch (e: CertificateException) { - e.printStackTrace() + e.printStackTraceDebug() return null } return try { @@ -117,10 +118,10 @@ class AppUpdateChecker(private val activity: ComponentActivity) { val publicKey: ByteArray = md.digest(c.encoded) publicKey.byte2HexFormatted() } catch (e: NoSuchAlgorithmException) { - e.printStackTrace() + e.printStackTraceDebug() null } catch (e: CertificateEncodingException) { - e.printStackTrace() + e.printStackTraceDebug() null } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt index 384905df2..4ffa13c5b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt @@ -6,7 +6,6 @@ import androidx.preference.Preference import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.parser.MangaRepository @@ -14,6 +13,7 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.serializableArgument import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import org.koitharu.kotatsu.utils.ext.withArgs @@ -70,9 +70,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { preference.title = getString(R.string.logged_in_as, username) }.onFailure { error -> preference.isEnabled = error is AuthRequiredException - if (BuildConfig.DEBUG) { - error.printStackTrace() - } + error.printStackTraceDebug() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt index b41ebb205..53ad51cbc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupSettingsFragment.kt @@ -7,12 +7,13 @@ import androidx.activity.result.ActivityResultCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.preference.Preference import com.google.android.material.snackbar.Snackbar -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore), +class BackupSettingsFragment : + BasePreferenceFragment(R.string.backup_restore), ActivityResultCallback { private val backupSelectCall = registerForActivityResult( @@ -34,9 +35,7 @@ class BackupSettingsFragment : BasePreferenceFragment(R.string.backup_restore), try { backupSelectCall.launch(arrayOf("*/*")) } catch (e: ActivityNotFoundException) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } + e.printStackTraceDebug() Snackbar.make( listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT ).show() diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt b/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt index 03dd423ea..cedc875fa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/LifecycleAwareServiceConnection.kt @@ -10,7 +10,7 @@ import androidx.lifecycle.LifecycleOwner import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class LifecycleAwareServiceConnection private constructor( +class LifecycleAwareServiceConnection( private val host: Activity, ) : ServiceConnection, DefaultLifecycleObserver { @@ -31,19 +31,15 @@ class LifecycleAwareServiceConnection private constructor( super.onDestroy(owner) host.unbindService(this) } +} - companion object { - - fun bindService( - host: Activity, - lifecycleOwner: LifecycleOwner, - service: Intent, - flags: Int, - ): LifecycleAwareServiceConnection { - val connection = LifecycleAwareServiceConnection(host) - host.bindService(service, connection, flags) - lifecycleOwner.lifecycle.addObserver(connection) - return connection - } - } +fun Activity.bindServiceWithLifecycle( + owner: LifecycleOwner, + service: Intent, + flags: Int +): LifecycleAwareServiceConnection { + val connection = LifecycleAwareServiceConnection(this) + bindService(service, connection, flags) + owner.lifecycle.addObserver(connection) + return connection } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt index 6f15f7cd3..bd6ad731b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt @@ -9,7 +9,11 @@ import android.net.Uri import android.os.Build import androidx.activity.result.ActivityResultLauncher import androidx.core.app.ActivityOptionsCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.coroutineScope import androidx.work.CoroutineWorker +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume @@ -55,4 +59,11 @@ fun ActivityResultLauncher.tryLaunch(input: I, options: ActivityOptionsCo return runCatching { launch(input, options) }.isSuccess +} + +fun Lifecycle.postDelayed(runnable: Runnable, delay: Long) { + coroutineScope.launch { + delay(delay) + runnable.run() + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt index 412ead77c..dd4907134 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CoroutineExt.kt @@ -3,15 +3,6 @@ package org.koitharu.kotatsu.utils.ext import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.CoroutineExceptionHandler -import org.koitharu.kotatsu.BuildConfig - -val IgnoreErrors - get() = CoroutineExceptionHandler { _, e -> - if (BuildConfig.DEBUG) { - e.printStackTrace() - } - } val processLifecycleScope: LifecycleCoroutineScope inline get() = ProcessLifecycleOwner.get().lifecycleScope \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt index ad1fe8b39..e3cd66b43 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/LiveDataExt.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.liveData +import kotlinx.coroutines.Deferred import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/res/menu/opt_reader_bottom.xml b/app/src/main/res/menu/opt_reader_bottom.xml index 978a29d74..569bffc11 100644 --- a/app/src/main/res/menu/opt_reader_bottom.xml +++ b/app/src/main/res/menu/opt_reader_bottom.xml @@ -9,12 +9,14 @@ android:id="@+id/action_bookmark" android:icon="@drawable/ic_bookmark" android:title="@string/bookmark_add" + android:visible="false" app:showAsAction="always" />