From de7012cabf3975b85c74abbd5cb09c5db73d7c6c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 15 Sep 2022 08:36:20 +0300 Subject: [PATCH 01/53] Change acra sender to http --- app/build.gradle | 4 ++- .../java/org/koitharu/kotatsu/KotatsuApp.kt | 29 +++++++++++-------- app/src/main/res/values/constants.xml | 2 +- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bb67f50e5..43243690e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,6 +28,8 @@ android { // define this values in your local.properties file buildConfigField 'String', 'SHIKIMORI_CLIENT_ID', "\"${localProperty('shikimori.clientId')}\"" buildConfigField 'String', 'SHIKIMORI_CLIENT_SECRET', "\"${localProperty('shikimori.clientSecret')}\"" + resValue "string", "acra_login", "${localProperty('acra.login')}" + resValue "string", "acra_password", "${localProperty('acra.password')}" } buildTypes { debug { @@ -119,7 +121,7 @@ dependencies { implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.github.solkin:disk-lru-cache:1.4' - implementation 'ch.acra:acra-mail:5.9.6' + implementation 'ch.acra:acra-http:5.9.6' implementation 'ch.acra:acra-dialog:5.9.6' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index 23b3df2c6..3a207e19f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -8,9 +8,10 @@ import androidx.fragment.app.strictmode.FragmentStrictMode import androidx.room.InvalidationTracker import org.acra.ReportField import org.acra.config.dialog -import org.acra.config.mailSender +import org.acra.config.httpSender import org.acra.data.StringFormat import org.acra.ktx.initAcra +import org.acra.sender.HttpSender import org.koin.android.ext.android.get import org.koin.android.ext.android.getKoin import org.koin.android.ext.koin.androidContext @@ -73,7 +74,7 @@ class KotatsuApp : Application() { appWidgetModule, suggestionsModule, shikimoriModule, - bookmarksModule + bookmarksModule, ) } } @@ -82,16 +83,25 @@ class KotatsuApp : Application() { super.attachBaseContext(base) initAcra { buildConfigClass = BuildConfig::class.java - reportFormat = StringFormat.KEY_VALUE_LIST + reportFormat = StringFormat.JSON + excludeMatchingSharedPreferencesKeys = listOf( + "sources_\\w+", + ) + httpSender { + uri = getString(R.string.url_error_report) + basicAuthLogin = getString(R.string.acra_login) + basicAuthPassword = getString(R.string.acra_password) + httpMethod = HttpSender.Method.POST + } reportContent = listOf( ReportField.PACKAGE_NAME, ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME, ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL, - ReportField.CRASH_CONFIGURATION, ReportField.STACK_TRACE, - ReportField.SHARED_PREFERENCES + ReportField.CRASH_CONFIGURATION, + ReportField.SHARED_PREFERENCES, ) dialog { text = getString(R.string.crash_text) @@ -100,11 +110,6 @@ class KotatsuApp : Application() { resIcon = R.drawable.ic_alert_outline resTheme = android.R.style.Theme_Material_Light_Dialog_Alert } - mailSender { - mailTo = getString(R.string.email_error_report) - reportAsFile = true - reportFileName = "stacktrace.txt" - } } } @@ -129,7 +134,7 @@ class KotatsuApp : Application() { StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyLog() - .build() + .build(), ) StrictMode.setVmPolicy( StrictMode.VmPolicy.Builder() @@ -138,7 +143,7 @@ class KotatsuApp : Application() { .setClassInstanceLimit(PagesCache::class.java, 1) .setClassInstanceLimit(MangaLoaderContext::class.java, 1) .penaltyLog() - .build() + .build(), ) FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder() .penaltyDeath() diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index 2691718af..097e800e7 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -6,7 +6,7 @@ https://twitter.com/kotatsuapp https://reddit.com/user/kotatsuapp https://hosted.weblate.org/engage/kotatsu - kotatsu@waifu.club + https://acra.rumblur.space/report -1 1 From 553a85ef86ed6d51f64d454ff5b1da31aba2ced6 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Wed, 14 Sep 2022 18:47:06 +0300 Subject: [PATCH 02/53] Widget theme fix #225 --- app/src/main/res/layout/item_recent.xml | 2 +- app/src/main/res/layout/item_shelf.xml | 1 + app/src/main/res/values-night-v31/themes.xml | 7 ++++++- app/src/main/res/values-v31/themes.xml | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/item_recent.xml b/app/src/main/res/layout/item_recent.xml index 26cd12560..3c73fc7eb 100644 --- a/app/src/main/res/layout/item_recent.xml +++ b/app/src/main/res/layout/item_recent.xml @@ -6,4 +6,4 @@ android:layout_width="@dimen/widget_cover_width" android:layout_height="@dimen/widget_cover_height" android:scaleType="centerCrop" - tools:ignore="ContentDescription" /> \ No newline at end of file + tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout/item_shelf.xml b/app/src/main/res/layout/item_shelf.xml index 41ad4e865..94c8d8948 100644 --- a/app/src/main/res/layout/item_shelf.xml +++ b/app/src/main/res/layout/item_shelf.xml @@ -31,6 +31,7 @@ android:elegantTextHeight="false" android:ellipsize="end" android:lines="2" + android:padding="2dp" android:paddingHorizontal="4dp" android:paddingBottom="4dp" android:textColor="?android:attr/textColorPrimary" /> diff --git a/app/src/main/res/values-night-v31/themes.xml b/app/src/main/res/values-night-v31/themes.xml index a19e8d7ad..e57b7c907 100644 --- a/app/src/main/res/values-night-v31/themes.xml +++ b/app/src/main/res/values-night-v31/themes.xml @@ -41,4 +41,9 @@ @color/m3_dynamic_highlighted_text @color/m3_dynamic_dark_default_color_primary_text - \ No newline at end of file + + + diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml index c84e6787a..f53d18f4b 100644 --- a/app/src/main/res/values-v31/themes.xml +++ b/app/src/main/res/values-v31/themes.xml @@ -47,4 +47,4 @@ @android:color/system_accent1_100 - \ No newline at end of file + From 12e5e3b35e7d43fad73e863c1e2915950e9e1461 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 22 Sep 2022 16:53:27 +0300 Subject: [PATCH 03/53] Update gitignore --- .gitignore | 1 + .idea/kotlinc.xml | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 .idea/kotlinc.xml diff --git a/.gitignore b/.gitignore index 5611db9cb..8023469c7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ /.idea/navEditor.xml /.idea/assetWizardSettings.xml /.idea/kotlinScripting.xml +/.idea/kotlinc.xml /.idea/deploymentTargetDropDown.xml /.idea/androidTestResultsUserPreferences.xml /.idea/render.experimental.xml diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index 0dd4b3546..000000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file From 8a63ca23103a1718e953d7c7ddfd022f9bed3100 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 21 Sep 2022 18:35:38 +0300 Subject: [PATCH 04/53] Fix coroutines cancellation --- .idea/kotlinc.xml | 9 ++++ .../kotatsu/base/domain/ReversibleHandle.kt | 12 ++++- .../kotatsu/core/backup/BackupRepository.kt | 7 +-- .../kotatsu/core/os/ShortcutsUpdater.kt | 25 ++++++---- .../kotatsu/details/ui/DetailsViewModel.kt | 5 +- .../details/ui/MangaDetailsDelegate.kt | 5 +- .../download/domain/DownloadManager.kt | 13 ++++- .../ui/list/FavouritesListViewModel.kt | 6 ++- .../list/ui/filter/FilterCoordinator.kt | 15 ++++-- .../local/domain/LocalMangaRepository.kt | 24 +++++++-- .../kotatsu/local/ui/LocalListViewModel.kt | 17 ++++--- .../kotatsu/reader/ui/ReaderViewModel.kt | 15 +++--- .../reader/ui/pager/PageHolderDelegate.kt | 16 ++++-- .../ui/thumbnails/adapter/PageThumbnailAD.kt | 3 +- .../remotelist/ui/RemoteListViewModel.kt | 19 ++++++- .../kotatsu/scrobbling/domain/Scrobbler.kt | 15 ++++-- .../search/domain/MangaSearchRepository.kt | 7 +-- .../kotatsu/search/ui/SearchViewModel.kt | 18 +++++-- .../search/ui/multi/MultiSearchViewModel.kt | 8 ++- .../kotatsu/settings/AppUpdateChecker.kt | 15 +++--- .../settings/HistorySettingsFragment.kt | 15 ++++-- .../settings/SourceSettingsFragment.kt | 12 ++++- .../settings/backup/BackupDialogFragment.kt | 8 +-- .../suggestions/ui/SuggestionsWorker.kt | 8 +-- .../kotatsu/tracker/work/TrackWorker.kt | 3 +- .../kotatsu/utils/PausingDispatcher.kt | 50 ------------------- .../koitharu/kotatsu/utils/ext/AndroidExt.kt | 2 +- .../kotatsu/utils/ext/ThrowableExt.kt | 28 +++++++++-- 28 files changed, 241 insertions(+), 139 deletions(-) create mode 100644 .idea/kotlinc.xml delete mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/PausingDispatcher.kt diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 000000000..13639f500 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt index 43c9bf7e4..e15d59958 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt @@ -1,8 +1,12 @@ package org.koitharu.kotatsu.base.domain import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.processLifecycleScope +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable fun interface ReversibleHandle { @@ -10,7 +14,13 @@ fun interface ReversibleHandle { } fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) { - reverse() + runCatchingCancellable { + withContext(NonCancellable) { + reverse() + } + }.onFailure { + it.printStackTraceDebug() + } } operator fun ReversibleHandle.plus(other: ReversibleHandle) = ReversibleHandle { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt index 27dd10255..88b6048f2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt @@ -7,6 +7,7 @@ import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.parsers.util.json.JSONIterator import org.koitharu.kotatsu.parsers.util.json.mapJSON +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable private const val PAGE_SIZE = 10 @@ -84,7 +85,7 @@ class BackupRepository(private val db: MangaDatabase) { JsonDeserializer(it).toTagEntity() } val history = JsonDeserializer(item).toHistoryEntity() - result += runCatching { + result += runCatchingCancellable { db.withTransaction { db.tagsDao.upsert(tags) db.mangaDao.upsert(manga, tags) @@ -99,7 +100,7 @@ class BackupRepository(private val db: MangaDatabase) { val result = CompositeResult() for (item in entry.data.JSONIterator()) { val category = JsonDeserializer(item).toFavouriteCategoryEntity() - result += runCatching { + result += runCatchingCancellable { db.favouriteCategoriesDao.upsert(category) } } @@ -115,7 +116,7 @@ class BackupRepository(private val db: MangaDatabase) { JsonDeserializer(it).toTagEntity() } val favourite = JsonDeserializer(item).toFavouriteEntity() - result += runCatching { + result += runCatchingCancellable { db.withTransaction { db.tagsDao.upsert(tags) db.mangaDao.upsert(manga, tags) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt index a201295c5..c372605d9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt @@ -6,6 +6,7 @@ import android.content.pm.ShortcutManager import android.media.ThumbnailUtils import android.os.Build import android.util.Size +import androidx.annotation.RequiresApi import androidx.annotation.VisibleForTesting import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat @@ -25,6 +26,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.requireBitmap +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class ShortcutsUpdater( private val context: Context, @@ -37,10 +39,12 @@ class ShortcutsUpdater( private var shortcutsUpdateJob: Job? = null override fun onInvalidated(tables: MutableSet) { - val prevJob = shortcutsUpdateJob - shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) { - prevJob?.join() - updateShortcutsImpl() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + val prevJob = shortcutsUpdateJob + shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) { + prevJob?.join() + updateShortcutsImpl() + } } } @@ -48,7 +52,7 @@ class ShortcutsUpdater( return ShortcutManagerCompat.requestPinShortcut( context, buildShortcutInfo(manga).build(), - null + null, ) } @@ -57,7 +61,8 @@ class ShortcutsUpdater( return shortcutsUpdateJob?.join() != null } - private suspend fun updateShortcutsImpl() = runCatching { + @RequiresApi(Build.VERSION_CODES.N_MR1) + private suspend fun updateShortcutsImpl() = runCatchingCancellable { val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity) .filter { x -> x.title.isNotEmpty() } @@ -68,17 +73,17 @@ class ShortcutsUpdater( } private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder { - val icon = runCatching { + val icon = runCatchingCancellable { val bmp = coil.execute( ImageRequest.Builder(context) .data(manga.coverUrl) .size(iconSize.width, iconSize.height) - .build() + .build(), ).requireBitmap() ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0) }.fold( onSuccess = { IconCompat.createWithAdaptiveBitmap(it) }, - onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) } + onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) }, ) mangaRepository.storeManga(manga) return ShortcutInfoCompat.Builder(context, manga.id.toString()) @@ -87,7 +92,7 @@ class ShortcutsUpdater( .setIcon(icon) .setIntent( ReaderActivity.newIntent(context, manga.id) - .setAction(ReaderActivity.ACTION_MANGA_READ) + .setAction(ReaderActivity.ACTION_MANGA_READ), ) } 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 a8bade779..523d40e22 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 @@ -38,6 +38,7 @@ 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.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class DetailsViewModel( intent: MangaIntent, @@ -165,7 +166,7 @@ class DetailsViewModel( checkNotNull(manga) { "Cannot find saved manga for ${m.title}" } val original = localMangaRepository.getRemoteManga(manga) localMangaRepository.delete(manga) || throw IOException("Unable to delete file") - runCatching { + runCatchingCancellable { historyRepository.deleteOrSwap(manga, original) } onMangaRemoved.postCall(manga) @@ -204,7 +205,7 @@ class DetailsViewModel( reload() } else { viewModelScope.launch(Dispatchers.Default) { - runCatching { + runCatchingCancellable { localMangaRepository.getDetails(downloadedManga) }.onSuccess { delegate.relatedManga.value = it 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 index 49bbde5ce..8f0355aa5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt @@ -17,6 +17,7 @@ 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.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class MangaDetailsDelegate( private val intent: MangaIntent, @@ -44,9 +45,9 @@ class MangaDetailsDelegate( val hist = historyRepository.getOne(manga) selectedBranch.value = manga.getPreferredBranch(hist) mangaData.value = manga - relatedManga.value = runCatching { + relatedManga.value = runCatchingCancellable { if (manga.source == MangaSource.LOCAL) { - val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null + val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null MangaRepository(m.source).getDetails(m) } else { localMangaRepository.findSavedManga(manga) 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 8452512be..c37c2ab00 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 @@ -6,10 +6,18 @@ import coil.ImageLoader import coil.request.ImageRequest import coil.size.Scale import java.io.File -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.internal.closeQuietly @@ -28,6 +36,7 @@ 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.runCatchingCancellable import org.koitharu.kotatsu.utils.progress.PausingProgressJob private const val MAX_FAILSAFE_ATTEMPTS = 2 @@ -226,7 +235,7 @@ class DownloadManager( ) } - private suspend fun loadCover(manga: Manga) = runCatching { + private suspend fun loadCover(manga: Manga) = runCatchingCancellable { imageLoader.execute( ImageRequest.Builder(context) .data(manga.coverUrl) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt index 44f89c767..a8375ff65 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt @@ -22,6 +22,7 @@ import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class FavouritesListViewModel( private val categoryId: Long, @@ -45,7 +46,7 @@ class FavouritesListViewModel( } else { repository.observeAll(categoryId) }, - createListModeFlow() + createListModeFlow(), ) { list, mode -> when { list.isEmpty() -> listOf( @@ -58,8 +59,9 @@ class FavouritesListViewModel( R.string.favourites_category_empty }, actionStringRes = 0, - ) + ), ) + else -> list.toUi(mode, this) } }.catch { 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 5ea361168..7f8ec8b05 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 @@ -6,15 +6,22 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.update 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 org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import java.text.Collator -import java.util.* +import java.util.Locale +import java.util.TreeSet class FilterCoordinator( private val repository: RemoteMangaRepository, @@ -152,7 +159,7 @@ class FilterCoordinator( } private fun loadTagsAsync() = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) { - runCatching { + runCatchingCancellable { repository.getTags() }.onFailure { error -> error.printStackTraceDebug() @@ -203,4 +210,4 @@ class FilterCoordinator( return collator?.compare(t1, t2) ?: compareValues(t1, t2) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index e7d79dea3..ffc101543 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -8,19 +8,31 @@ import androidx.collection.ArraySet import androidx.core.net.toFile import androidx.core.net.toUri import java.io.File -import java.io.IOException -import java.util.* +import java.util.Enumeration import java.util.zip.ZipEntry import java.util.zip.ZipFile import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.withContext +import okio.IOException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.local.data.CbzFilter import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.MangaIndex import org.koitharu.kotatsu.local.data.TempFileFilter -import org.koitharu.kotatsu.parsers.model.* +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.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.toCamelCase import org.koitharu.kotatsu.utils.AlphanumComparator import org.koitharu.kotatsu.utils.CompositeMutex @@ -28,6 +40,7 @@ import org.koitharu.kotatsu.utils.ext.deleteAwait import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.readText import org.koitharu.kotatsu.utils.ext.resolveName +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable private const val MAX_PARALLELISM = 4 @@ -70,6 +83,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma manga.source != MangaSource.LOCAL -> requireNotNull(findSavedManga(manga)) { "Manga is not local or saved" } + else -> getFromFile(Uri.parse(manga.url).toFile()) } @@ -226,7 +240,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma context: CoroutineContext, ): Deferred = async(context) { runInterruptible { - runCatching { getFromFile(file) }.getOrNull() + runCatchingCancellable { getFromFile(file) }.getOrNull() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index ebd347913..fe8703130 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.local.ui import android.net.Uri import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -10,6 +11,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import okio.IOException import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.download.ui.service.DownloadService @@ -21,8 +23,8 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.progress.Progress -import java.io.IOException class LocalListViewModel( private val repository: LocalMangaRepository, @@ -40,7 +42,7 @@ class LocalListViewModel( override val content = combine( mangaList, createListModeFlow(), - listError + listError, ) { list, mode, error -> when { error != null -> listOf(error.toErrorState(canRetry = true)) @@ -51,8 +53,9 @@ class LocalListViewModel( textPrimary = R.string.text_local_holder_primary, textSecondary = R.string.text_local_holder_secondary, actionStringRes = R.string._import, - ) + ), ) + else -> ArrayList(list.size + 1).apply { add(headerModel) list.toUi(this, mode) @@ -60,7 +63,7 @@ class LocalListViewModel( } }.asLiveDataDistinct( viewModelScope.coroutineContext + Dispatchers.Default, - listOf(LoadingState) + listOf(LoadingState), ) init { @@ -97,7 +100,7 @@ class LocalListViewModel( for (manga in itemsToRemove) { val original = repository.getRemoteManga(manga) repository.delete(manga) || throw IOException("Unable to delete file") - runCatching { + runCatchingCancellable { historyRepository.deleteOrSwap(manga, original) } mangaList.update { list -> @@ -113,6 +116,8 @@ class LocalListViewModel( try { listError.value = null mangaList.value = repository.getList(0, null, null) + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { listError.value = e } @@ -121,7 +126,7 @@ class LocalListViewModel( private fun cleanup() { if (!DownloadService.isRunning) { viewModelScope.launch { - runCatching { + runCatchingCancellable { repository.cleanup() }.onFailure { error -> error.printStackTraceDebug() 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 895bb6628..3ffeeb818 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 @@ -6,6 +6,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import java.util.Date import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.koitharu.kotatsu.R @@ -31,7 +32,7 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.processLifecycleScope -import java.util.* +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable private const val BOUNDS_PAGE_OFFSET = 2 private const val PAGES_TRIM_THRESHOLD = 120 @@ -69,7 +70,7 @@ class ReaderViewModel( mangaName = manga?.title, chapterName = chapter?.name, chapterNumber = chapter?.number ?: 0, - chaptersTotal = chapters.size() + chaptersTotal = chapters.size(), ) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, null) @@ -80,7 +81,7 @@ class ReaderViewModel( val readerAnimation = settings.observeAsLiveData( context = viewModelScope.coroutineContext + Dispatchers.Default, key = AppSettings.KEY_READER_ANIMATION, - valueProducer = { readerAnimation } + valueProducer = { readerAnimation }, ) val isScreenshotsBlockEnabled = combine( @@ -123,12 +124,12 @@ class ReaderViewModel( val manga = checkNotNull(mangaData.value) dataRepository.savePreferences( manga = manga, - mode = newMode + mode = newMode, ) readerMode.value = newMode content.value?.run { content.value = copy( - state = getCurrentState() + state = getCurrentState(), ) } } @@ -358,7 +359,7 @@ class ReaderViewModel( ?: manga.chapters?.randomOrNull() ?: error("There are no chapters in this manga") val pages = repo.getPages(chapter) - return runCatching { + return runCatchingCancellable { val isWebtoon = MangaUtils.determineMangaIsWebtoon(pages) if (isWebtoon) ReaderMode.WEBTOON else defaultMode }.onSuccess { @@ -389,7 +390,7 @@ class ReaderViewModel( */ private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState, percent: Float): Job { return processLifecycleScope.launch(Dispatchers.Default) { - runCatching { + runCatchingCancellable { addOrUpdate( manga = manga, chapterId = state.chapterId, diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt index 5630cb1ba..b32ecc884 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt @@ -3,24 +3,30 @@ package org.koitharu.kotatsu.reader.ui.pager import android.net.Uri import androidx.core.net.toUri import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView -import kotlinx.coroutines.* +import java.io.File +import java.io.IOException +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.reader.domain.PageLoader -import java.io.File -import java.io.IOException class PageHolderDelegate( private val loader: PageLoader, private val settings: AppSettings, private val callback: Callback, - private val exceptionResolver: ExceptionResolver + private val exceptionResolver: ExceptionResolver, ) : SubsamplingScaleImageView.DefaultOnImageEventListener() { private val scope = loader.loaderScope + Dispatchers.Main.immediate @@ -88,6 +94,8 @@ class PageHolderDelegate( loader.convertInPlace(file) state = State.CONVERTED callback.onImageReady(file.toUri()) + } catch (ce: CancellationException) { + throw ce } catch (e2: Throwable) { e.addSuppressed(e2) state = State.ERROR diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt index 42529810b..c5ab420ea 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt @@ -15,6 +15,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail import org.koitharu.kotatsu.utils.ext.referer +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.ext.setTextColorAttr fun pageThumbnailAD( @@ -69,7 +70,7 @@ fun pageThumbnailAD( text = (item.number).toString() } job = scope.launch { - val drawable = runCatching { + val drawable = runCatchingCancellable { loadPageThumbnail(item) }.getOrNull() binding.imageViewThumb.setImageDrawable(drawable) 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 092194b96..7249b75bc 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 @@ -2,10 +2,16 @@ package org.koitharu.kotatsu.remotelist.ui import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.ui.widgets.ChipsView @@ -16,7 +22,14 @@ import org.koitharu.kotatsu.list.ui.filter.FilterCoordinator import org.koitharu.kotatsu.list.ui.filter.FilterItem import org.koitharu.kotatsu.list.ui.filter.FilterState import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener -import org.koitharu.kotatsu.list.ui.model.* +import org.koitharu.kotatsu.list.ui.model.CurrentFilterModel +import org.koitharu.kotatsu.list.ui.model.EmptyState +import org.koitharu.kotatsu.list.ui.model.ListHeader +import org.koitharu.kotatsu.list.ui.model.LoadingFooter +import org.koitharu.kotatsu.list.ui.model.LoadingState +import org.koitharu.kotatsu.list.ui.model.toErrorFooter +import org.koitharu.kotatsu.list.ui.model.toErrorState +import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct @@ -132,6 +145,8 @@ class RemoteListViewModel( mangaList.value = mangaList.value?.plus(list) ?: list } hasNextPage.value = list.isNotEmpty() + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { e.printStackTraceDebug() listError.value = e diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/domain/Scrobbler.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/domain/Scrobbler.kt index b730d19cd..1db58e204 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/domain/Scrobbler.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/domain/Scrobbler.kt @@ -3,15 +3,20 @@ package org.koitharu.kotatsu.scrobbling.domain import androidx.collection.LongSparseArray import androidx.collection.getOrElse import androidx.core.text.parseAsHtml -import java.util.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity -import org.koitharu.kotatsu.scrobbling.domain.model.* +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus import org.koitharu.kotatsu.utils.ext.findKeyByValue import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import java.util.EnumMap abstract class Scrobbler( protected val db: MangaDatabase, @@ -47,7 +52,7 @@ abstract class Scrobbler( private suspend fun ScrobblingEntity.toScrobblingInfo(mangaId: Long): ScrobblingInfo? { val mangaInfo = infoCache.getOrElse(targetId) { - runCatching { + runCatchingCancellable { getMangaInfo(targetId) }.onFailure { it.printStackTraceDebug() @@ -72,9 +77,9 @@ abstract class Scrobbler( } suspend fun Scrobbler.tryScrobble(mangaId: Long, chapter: MangaChapter): Boolean { - return runCatching { + return runCatchingCancellable { scrobble(mangaId, chapter) }.onFailure { it.printStackTraceDebug() }.isSuccess -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index 2c95fb632..b86bb5475 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -19,6 +19,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.util.levenshteinDistance import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class MangaSearchRepository( private val settings: AppSettings, @@ -30,7 +31,7 @@ class MangaSearchRepository( fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow = settings.getMangaSources(includeHidden = false).asFlow() .flatMapMerge(concurrency) { source -> - runCatching { + runCatchingCancellable { MangaRepository(source).getList( offset = 0, query = query, @@ -63,7 +64,7 @@ class MangaSearchRepository( SUGGESTION_PROJECTION, "${SearchManager.SUGGEST_COLUMN_QUERY} LIKE ?", arrayOf("%$query%"), - "date DESC" + "date DESC", )?.use { cursor -> val count = minOf(cursor.count, limit) if (count == 0) { @@ -113,7 +114,7 @@ class MangaSearchRepository( SUGGESTION_PROJECTION, null, arrayOfNulls(1), - null + null, )?.use { cursor -> cursor.count } ?: 0 } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt index 9615e84a6..713daf8f6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.search.ui import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -9,14 +10,20 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.list.ui.MangaListViewModel -import org.koitharu.kotatsu.list.ui.model.* +import org.koitharu.kotatsu.list.ui.model.EmptyState +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingFooter +import org.koitharu.kotatsu.list.ui.model.LoadingState +import org.koitharu.kotatsu.list.ui.model.toErrorFooter +import org.koitharu.kotatsu.list.ui.model.toErrorState +import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct class SearchViewModel( private val repository: MangaRepository, private val query: String, - settings: AppSettings + settings: AppSettings, ) : MangaListViewModel(settings) { private val mangaList = MutableStateFlow?>(null) @@ -28,7 +35,7 @@ class SearchViewModel( mangaList, createListModeFlow(), listError, - hasNextPage + hasNextPage, ) { list, mode, error, hasNext -> when { list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true)) @@ -39,8 +46,9 @@ class SearchViewModel( textPrimary = R.string.nothing_found, textSecondary = R.string.text_search_holder_secondary, actionStringRes = 0, - ) + ), ) + else -> { val result = ArrayList(list.size + 1) list.toUi(result, mode) @@ -88,6 +96,8 @@ class SearchViewModel( mangaList.value = mangaList.value?.plus(list) ?: list } hasNextPage.value = list.isNotEmpty() + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { listError.value = e } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt index b2ababf6d..cd0f6d199 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt @@ -17,6 +17,7 @@ import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable private const val MAX_PARALLELISM = 4 private const val MIN_HAS_MORE_ITEMS = 8 @@ -48,8 +49,9 @@ class MultiSearchViewModel( textSecondary = R.string.text_search_holder_secondary, actionStringRes = 0, ) - } + }, ) + loading -> list + LoadingFooter else -> list } @@ -81,6 +83,8 @@ class MultiSearchViewModel( loadingData.value = true query.postValue(q) searchImpl(q) + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { listError.value = e } finally { @@ -94,7 +98,7 @@ class MultiSearchViewModel( val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM) val deferredList = sources.map { source -> async(dispatcher) { - runCatching { + runCatchingCancellable { val list = MangaRepository(source).getList(offset = 0, query = q) .toUi(ListMode.GRID) if (list.isNotEmpty()) { 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 603fe1ce5..62fe1b7ba 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt @@ -8,6 +8,12 @@ import androidx.activity.ComponentActivity import androidx.annotation.MainThread import androidx.core.net.toUri import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.security.MessageDigest +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 @@ -20,12 +26,7 @@ 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.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class AppUpdateChecker(private val activity: ComponentActivity) { @@ -41,7 +42,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) { null } - suspend fun checkNow() = runCatching { + suspend fun checkNow() = runCatchingCancellable { val version = repo.getLatestVersion() val newVersionId = VersionId(version.name) val currentVersionId = VersionId(BuildConfig.VERSION_NAME) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt index c4c5f46ba..8d807be6b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt @@ -7,6 +7,7 @@ import android.view.View import androidx.preference.Preference import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch import org.koin.android.ext.android.get import org.koin.android.ext.android.inject @@ -65,18 +66,22 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach clearCache(preference, CacheDir.PAGES) true } + AppSettings.KEY_THUMBS_CACHE_CLEAR -> { clearCache(preference, CacheDir.THUMBS) true } + AppSettings.KEY_COOKIES_CLEAR -> { clearCookies() true } + AppSettings.KEY_SEARCH_HISTORY_CLEAR -> { clearSearchHistory(preference) true } + AppSettings.KEY_UPDATES_FEED_CLEAR -> { viewLifecycleScope.launch { trackerRepo.clearLogs() @@ -85,11 +90,12 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach Snackbar.make( view ?: return@launch, R.string.updates_feed_cleared, - Snackbar.LENGTH_SHORT + Snackbar.LENGTH_SHORT, ).show() } true } + AppSettings.KEY_SHIKIMORI -> { if (!shikimoriRepository.isAuthorized) { launchShikimoriAuth() @@ -98,6 +104,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach super.onPreferenceTreeClick(preference) } } + else -> super.onPreferenceTreeClick(preference) } } @@ -110,6 +117,8 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach storageManager.clearCache(cache) val size = storageManager.computeCacheSize(cache) preference.summary = FileSize.BYTES.format(ctx, size) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { preference.summary = e.getDisplayMessage(ctx.resources) } finally { @@ -136,7 +145,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach Snackbar.make( view ?: return@launch, R.string.search_history_cleared, - Snackbar.LENGTH_SHORT + Snackbar.LENGTH_SHORT, ).show() } }.show() @@ -154,7 +163,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach Snackbar.make( listView ?: return@launch, R.string.cookies_cleared, - Snackbar.LENGTH_SHORT + Snackbar.LENGTH_SHORT, ).show() } }.show() 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 b5209896b..463d43972 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt @@ -18,7 +18,13 @@ 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.* +import org.koitharu.kotatsu.utils.ext.awaitViewLifecycle +import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import org.koitharu.kotatsu.utils.ext.serializableArgument +import org.koitharu.kotatsu.utils.ext.viewLifecycleScope +import org.koitharu.kotatsu.utils.ext.withArgs class SourceSettingsFragment : BasePreferenceFragment(0) { @@ -60,12 +66,13 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { startActivity(SourceAuthActivity.newIntent(preference.context, source)) true } + else -> super.onPreferenceTreeClick(preference) } } private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch { - runCatching { + runCatchingCancellable { preference.summary = null withContext(Dispatchers.Default) { requireNotNull(repository?.getAuthProvider()?.getUsername()) @@ -85,6 +92,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { ).setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) } .show() } + else -> preference.summary = error.getDisplayMessage(preference.context.resources) } error.printStackTraceDebug() diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt index b309a588b..281f57a12 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt @@ -9,14 +9,14 @@ import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.isVisible import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.io.File +import java.io.FileOutputStream import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.AlertDialogFragment import org.koitharu.kotatsu.databinding.DialogProgressBinding import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.progress.Progress -import java.io.File -import java.io.FileOutputStream class BackupDialogFragment : AlertDialogFragment() { @@ -24,7 +24,7 @@ class BackupDialogFragment : AlertDialogFragment() { private var backup: File? = null private val saveFileContract = registerForActivityResult( - ActivityResultContracts.CreateDocument("*/*") + ActivityResultContracts.CreateDocument("*/*"), ) { uri -> val file = backup if (uri != null && file != null) { @@ -88,6 +88,8 @@ class BackupDialogFragment : AlertDialogFragment() { } Toast.makeText(requireContext(), R.string.backup_saved, Toast.LENGTH_LONG).show() dismiss() + } catch (e: InterruptedException) { + throw e } catch (e: Exception) { onError(e) } diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt index 7433e689b..eab01f9ee 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -8,6 +8,8 @@ import androidx.annotation.FloatRange import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.work.* +import java.util.concurrent.TimeUnit +import kotlin.math.pow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -24,8 +26,6 @@ import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.utils.ext.asArrayList import org.koitharu.kotatsu.utils.ext.trySetForeground -import java.util.concurrent.TimeUnit -import kotlin.math.pow class SuggestionsWorker(appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params), KoinComponent { @@ -47,7 +47,7 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) : val channel = NotificationChannel( WORKER_CHANNEL_ID, title, - NotificationManager.IMPORTANCE_LOW + NotificationManager.IMPORTANCE_LOW, ) channel.setShowBadge(false) channel.enableVibration(false) @@ -118,7 +118,7 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) : }.map { manga -> MangaSuggestion( manga = manga, - relevance = computeRelevance(manga.tags, allTags) + relevance = computeRelevance(manga.tags, allTags), ) }.sortedBy { it.relevance }.take(LIMIT) suggestionRepository.replace(suggestions) diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index 51aedc866..721f8080b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -27,6 +27,7 @@ import org.koitharu.kotatsu.tracker.domain.Tracker import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.ext.referer +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.ext.toBitmapOrNull import org.koitharu.kotatsu.utils.ext.trySetForeground @@ -80,7 +81,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : val deferredList = coroutineScope { tracks.map { (track, channelId) -> async(dispatcher) { - runCatching { + runCatchingCancellable { tracker.fetchUpdates(track, commit = true) }.onSuccess { updates -> if (updates.isValid && updates.isNotEmpty()) { diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/PausingDispatcher.kt b/app/src/main/java/org/koitharu/kotatsu/utils/PausingDispatcher.kt deleted file mode 100644 index 57eb100a4..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/PausingDispatcher.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.koitharu.kotatsu.utils - -import androidx.annotation.MainThread -import java.util.concurrent.ConcurrentLinkedQueue -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Runnable - -class PausingDispatcher( - private val dispatcher: CoroutineDispatcher, -) : CoroutineDispatcher() { - - @Volatile - private var isPaused = false - private val queue = ConcurrentLinkedQueue() - - override fun isDispatchNeeded(context: CoroutineContext): Boolean { - return isPaused || super.isDispatchNeeded(context) - } - - override fun dispatch(context: CoroutineContext, block: Runnable) { - if (isPaused) { - queue.add(Task(context, block)) - } else { - dispatcher.dispatch(context, block) - } - } - - @MainThread - fun pause() { - isPaused = true - } - - @MainThread - fun resume() { - if (!isPaused) { - return - } - isPaused = false - while (true) { - val task = queue.poll() ?: break - dispatcher.dispatch(task.context, task.block) - } - } - - private class Task( - val context: CoroutineContext, - val block: Runnable, - ) -} \ 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 28f50e7de..ba56a4b23 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 @@ -29,7 +29,7 @@ val Context.activityManager: ActivityManager? fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this) -suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatching { +suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable { val info = getForegroundInfo() setForeground(info) }.isSuccess diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt index d4f654525..0fe14cfd8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt @@ -4,10 +4,16 @@ import android.content.ActivityNotFoundException import android.content.res.Resources import androidx.collection.arraySetOf import java.net.SocketTimeoutException +import java.net.UnknownHostException +import kotlinx.coroutines.CancellationException import okio.FileNotFoundException import org.acra.ktx.sendWithAcra import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.exceptions.* +import org.koitharu.kotatsu.core.exceptions.CaughtException +import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException +import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException +import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException +import org.koitharu.kotatsu.core.exceptions.WrongPasswordException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.ContentUnavailableException import org.koitharu.kotatsu.parsers.exception.NotFoundException @@ -19,12 +25,16 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) { is ActivityNotFoundException, is UnsupportedOperationException, -> resources.getString(R.string.operation_not_supported) + is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported) is FileNotFoundException -> resources.getString(R.string.file_not_found) is EmptyHistoryException -> resources.getString(R.string.history_is_empty) is ContentUnavailableException -> message is ParseException -> shortMessage - is SocketTimeoutException -> resources.getString(R.string.network_error) + is UnknownHostException, + is SocketTimeoutException, + -> resources.getString(R.string.network_error) + is WrongPasswordException -> resources.getString(R.string.wrong_password) is NotFoundException -> resources.getString(R.string.not_found_404) else -> localizedMessage @@ -46,4 +56,16 @@ private val reportableExceptions = arraySetOf>( IllegalArgumentException::class.java, ConcurrentModificationException::class.java, UnsupportedOperationException::class.java, -) \ No newline at end of file +) + +inline fun runCatchingCancellable(block: () -> R): Result { + return try { + Result.success(block()) + } catch (e: InterruptedException) { + throw e + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + Result.failure(e) + } +} \ No newline at end of file From c94404446504b49b24f2f9224823064e567476e9 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 22 Sep 2022 17:38:08 +0300 Subject: [PATCH 05/53] Update version --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 43243690e..f1991accb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 427 - versionName '3.4.15' + versionCode 428 + versionName '3.4.16' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 93c6bec45293c04af4e26556851e4c8af6693b53 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 1 Oct 2022 10:33:51 +0300 Subject: [PATCH 06/53] Fix widgets colors --- app/src/main/res/values-night-v31/themes.xml | 5 +++-- app/src/main/res/values-v31/themes.xml | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-night-v31/themes.xml b/app/src/main/res/values-night-v31/themes.xml index e57b7c907..48f102679 100644 --- a/app/src/main/res/values-night-v31/themes.xml +++ b/app/src/main/res/values-night-v31/themes.xml @@ -43,7 +43,8 @@ + diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml index f53d18f4b..383cde2ae 100644 --- a/app/src/main/res/values-v31/themes.xml +++ b/app/src/main/res/values-v31/themes.xml @@ -43,8 +43,8 @@ From af2adeba138f63aef7a4c483fa7b49e4d03f5132 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 1 Oct 2022 12:19:24 +0300 Subject: [PATCH 07/53] Fix opening fingerprint dialog --- .../org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt index 0da2ee55f..22016baac 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt @@ -46,7 +46,10 @@ class ProtectActivity : startActivity(intent) finishAfterTransition() } + } + override fun onStart() { + super.onStart() if (!useFingerprint()) { binding.editPassword.requestFocus() } From 048efdf59fd733bae305d98a48e6fc550b7aeb1e Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 3 Oct 2022 08:02:49 +0300 Subject: [PATCH 08/53] Fix crash on slider --- app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt index 18fa66cc2..ffbfb41e6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt @@ -114,7 +114,8 @@ fun RecyclerView.ViewHolder.getItem(clazz: Class): T? { fun Slider.setValueRounded(newValue: Float) { val step = stepSize - value = (newValue / step).roundToInt() * step + val roundedValue = (newValue / step).roundToInt() * step + value = roundedValue.coerceIn(valueFrom, valueTo) } val RecyclerView.isScrolledToTop: Boolean From 4d838d290de389e30b431e2bb66f15d672c9d55c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 3 Oct 2022 08:41:32 +0300 Subject: [PATCH 09/53] Update dependencies --- app/build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f1991accb..19081b434 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 428 - versionName '3.4.16' + versionCode 429 + versionName '3.4.17' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -81,7 +81,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.KotatsuApp:kotatsu-parsers:b3a9c5fcda') { + implementation('com.github.KotatsuApp:kotatsu-parsers:b1990c7918') { exclude group: 'org.json', module: 'json' } @@ -89,7 +89,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.activity:activity-ktx:1.5.1' - implementation 'androidx.fragment:fragment-ktx:1.5.2' + implementation 'androidx.fragment:fragment-ktx:1.5.3' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-service:2.5.1' @@ -140,5 +140,5 @@ dependencies { androidTestImplementation 'io.insert-koin:koin-test-junit4:3.2.0' androidTestImplementation 'androidx.room:room-testing:2.4.3' - androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.13.0' + androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0' } \ No newline at end of file From eaeb11f9cee8f9835b97c454d04a9726bcb4a62c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 16 Oct 2022 10:36:02 +0300 Subject: [PATCH 10/53] Fix crash on page loading --- .../kotatsu/reader/domain/PageLoader.kt | 33 +++++++++++++++---- .../reader/ui/pager/PageHolderDelegate.kt | 2 +- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 91332872b..d32bc4fc4 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -7,14 +7,16 @@ import android.net.Uri import androidx.collection.LongSparseArray import androidx.collection.set import dagger.hilt.android.qualifiers.ApplicationContext -import java.io.File -import java.util.* -import java.util.concurrent.atomic.AtomicInteger -import java.util.zip.ZipFile -import javax.inject.Inject -import kotlinx.coroutines.* +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import okhttp3.OkHttpClient @@ -30,7 +32,15 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.utils.ext.connectivityManager +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.progress.ProgressDeferred +import java.io.File +import java.util.LinkedList +import java.util.concurrent.atomic.AtomicInteger +import java.util.zip.ZipFile +import javax.inject.Inject +import kotlin.coroutines.AbstractCoroutineContextElement +import kotlin.coroutines.CoroutineContext private const val PROGRESS_UNDEFINED = -1f private const val PREFETCH_LIMIT_DEFAULT = 10 @@ -43,7 +53,7 @@ class PageLoader @Inject constructor( private val mangaRepositoryFactory: MangaRepository.Factory, ) : Closeable { - val loaderScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + val loaderScope = CoroutineScope(SupervisorJob() + InternalErrorHandler() + Dispatchers.Default) private val connectivityManager = context.connectivityManager private val tasks = LongSparseArray>() @@ -197,4 +207,13 @@ class PageLoader @Inject constructor( val deferred = CompletableDeferred(file) return ProgressDeferred(deferred, emptyProgressFlow) } + + private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler), + CoroutineExceptionHandler { + + override fun handleException(context: CoroutineContext, exception: Throwable) { + exception.printStackTraceDebug() + } + + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt index 7bee7ca84..fa5aaaa22 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt @@ -132,7 +132,7 @@ class PageHolderDelegate( callback.onImageReady(file.toUri()) } catch (e: CancellationException) { throw e - } catch (e: Exception) { + } catch (e: Throwable) { state = State.ERROR error = e callback.onError(e) From ee65251bf501abb23eb060cd5eccbe1d0e6f8915 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 16 Oct 2022 10:38:23 +0300 Subject: [PATCH 11/53] Update parsers --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 19081b434..12750b879 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 429 - versionName '3.4.17' + versionCode 430 + versionName '3.5' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -81,7 +81,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.KotatsuApp:kotatsu-parsers:b1990c7918') { + implementation('com.github.KotatsuApp:kotatsu-parsers:5cb953eb86') { exclude group: 'org.json', module: 'json' } From 45dbd5aa44bf25bea117932d6da1816df357c4e4 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 16 Oct 2022 10:36:02 +0300 Subject: [PATCH 12/53] Fix crash on page loading --- .idea/compiler.xml | 2 +- .../kotatsu/reader/domain/PageLoader.kt | 31 +++++++++++++++---- .../reader/ui/pager/PageHolderDelegate.kt | 2 +- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8a4..b589d56e9 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 696f48cc3..e9947d64f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -6,9 +6,22 @@ import android.graphics.BitmapFactory import android.net.Uri import androidx.collection.LongSparseArray import androidx.collection.set -import kotlinx.coroutines.* +import java.io.File +import java.util.LinkedList +import java.util.concurrent.atomic.AtomicInteger +import java.util.zip.ZipFile +import kotlin.coroutines.AbstractCoroutineContextElement +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import okhttp3.OkHttpClient @@ -26,18 +39,15 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.utils.ext.connectivityManager +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.progress.ProgressDeferred -import java.io.File -import java.util.* -import java.util.concurrent.atomic.AtomicInteger -import java.util.zip.ZipFile private const val PROGRESS_UNDEFINED = -1f private const val PREFETCH_LIMIT_DEFAULT = 10 class PageLoader : KoinComponent, Closeable { - val loaderScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + val loaderScope = CoroutineScope(SupervisorJob() + InternalErrorHandler() + Dispatchers.Default) private val okHttp = get() private val cache = get() @@ -194,4 +204,13 @@ class PageLoader : KoinComponent, Closeable { val deferred = CompletableDeferred(file) return ProgressDeferred(deferred, emptyProgressFlow) } + + private class InternalErrorHandler : + AbstractCoroutineContextElement(CoroutineExceptionHandler), + CoroutineExceptionHandler { + + override fun handleException(context: CoroutineContext, exception: Throwable) { + exception.printStackTraceDebug() + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt index b32ecc884..3fba51f1f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt @@ -118,7 +118,7 @@ class PageHolderDelegate( callback.onImageReady(file.toUri()) } catch (e: CancellationException) { throw e - } catch (e: Exception) { + } catch (e: Throwable) { state = State.ERROR error = e callback.onError(e) From fb202f80a582f2cb5ea68122fd6b1ba7ad46e23b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 19 Oct 2022 10:10:25 +0300 Subject: [PATCH 13/53] User-friendly message for HttpStatusException --- .idea/kotlinc.xml | 2 +- app/build.gradle | 6 +++--- .../java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt | 7 +++++++ app/src/main/res/values/strings.xml | 1 + build.gradle | 4 ++-- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index d6504c977..dd185e22b 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -4,6 +4,6 @@ Server side error (%1$d). Please try again later Also clear information about new chapters + Compact From 06d1d5644819dec386c778a6b79ea8846d14463f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 20 Oct 2022 10:04:52 +0300 Subject: [PATCH 27/53] Update parsers --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 50ffce317..44f047d0b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -83,7 +83,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.KotatsuApp:kotatsu-parsers:a62662d116') { + implementation('com.github.KotatsuApp:kotatsu-parsers:a1441e7ed7') { exclude group: 'org.json', module: 'json' } @@ -103,7 +103,7 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' - implementation 'com.google.android.material:material:1.7.0-rc01' + implementation 'com.google.android.material:material:1.7.0' //noinspection LifecycleAnnotationProcessorWithJava8 kapt 'androidx.lifecycle:lifecycle-compiler:2.5.1' From 0758cfef64d0264cf86533123ae85c2bf344b830 Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Thu, 20 Oct 2022 09:02:48 +0200 Subject: [PATCH 28/53] Translated using Weblate (Russian) Currently translated at 99.7% (396 of 397 strings) Co-authored-by: Zakhar Timoshenko Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/ Translation: Kotatsu/Strings --- app/src/main/res/values-ru/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 40007274b..1e2afced6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -393,4 +393,6 @@ Включите Wi-Fi или передачу данных, чтобы читать мангу онлайн Разные языки Сеть недоступна + Также очистить информацию о новых главах + Внутренняя ошибка сервера (%1$d). Повторите попытку позже \ No newline at end of file From a9f435ae3d318e10b6c13f70e6b6212da95ac8a9 Mon Sep 17 00:00:00 2001 From: Dpper Date: Thu, 20 Oct 2022 09:02:49 +0200 Subject: [PATCH 29/53] Translated using Weblate (Ukrainian) Currently translated at 100.0% (397 of 397 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (8 of 8 strings) Co-authored-by: Dpper Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/uk/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/ Translation: Kotatsu/Strings Translation: Kotatsu/plurals --- app/src/main/res/values-uk/plurals.xml | 2 +- app/src/main/res/values-uk/strings.xml | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-uk/plurals.xml b/app/src/main/res/values-uk/plurals.xml index 1b47f743e..0ced943b7 100644 --- a/app/src/main/res/values-uk/plurals.xml +++ b/app/src/main/res/values-uk/plurals.xml @@ -2,7 +2,7 @@ %1$d новий розділ - %1$d нові розділи + %1$d нових розділи %1$d нових розділів %1$d нових розділів diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 79260b7e1..7cf1def64 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -211,7 +211,7 @@ Оновлення пропозицій Видалити вибрані елементи з пристрою назавжди\? Видалення завершено - Ви впевнені, що хочете завантажити всю вибрану манґу з усіма її розділами\? Це може споживати багато трафіку та пам’яті + Завантажити всю вибрану манґу з усіма її розділами\? Це може споживати багато трафіку та пам’яті. Завантажувати паралельно Сповільнення завантаження Обробка збереженої манґи @@ -292,7 +292,7 @@ Видалено з історії DNS через HTTPS Режим за замовчуванням - Автоматично визначати, чи є манга вебтуном + Автоматично визначати, чи є манґа вебтуном Автовизначення режиму читання Вимкнути оптимізацію акумулятора Допомагає з перевірками фонових оновлень @@ -383,6 +383,15 @@ Контрастність Скинути Вибрані параметри кольору будуть запам\'ятовані для цієї манґи - У вас є незбережені зміни, ви хочете зберегти чи скасувати їх\? + Зберегти чи скасувати незбережені зміни\? Скасувати + Помилка на стороні сервера (%1$d). Будь ласка спробуйте пізніше + Також очистити інформацію про нові розділи + На пристрої не залишилося вільного місця + Різні мови + Мережа недоступна + Увімкніть Wi-Fi або мобільну мережу, щоб читати манґу онлайн + Масштабування в режимі вебтуну + Дозволити жести збільшення/зменшення масштабу в режимі вебтуну (бета) + Відображати повзунок перемикання сторінок \ No newline at end of file From 18137ab48e37c4791aa46a1611de8bc058959c20 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 20 Oct 2022 09:02:49 +0200 Subject: [PATCH 30/53] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (397 of 397 strings) Co-authored-by: Eric Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/ Translation: Kotatsu/Strings --- app/src/main/res/values-zh-rCN/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 82b715988..843feee9c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -392,4 +392,6 @@ 不同语言 网络不可用 打开 Wi-Fi 或移动网络在线阅读漫画 + 同样清除新章节信息 + 服务器端错误 (%1$d)。请稍后再试 \ No newline at end of file From ccb8b0c8e7fbd49b2cb1f56055f5675d7b894548 Mon Sep 17 00:00:00 2001 From: kuragehime Date: Thu, 20 Oct 2022 09:02:49 +0200 Subject: [PATCH 31/53] Translated using Weblate (Japanese) Currently translated at 100.0% (397 of 397 strings) Co-authored-by: kuragehime Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/ Translation: Kotatsu/Strings --- app/src/main/res/values-ja/strings.xml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 426498448..1117570fe 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -267,7 +267,7 @@ サジェストで表示したくないジャンルを指定 選択した項目をデバイスから完全に削除しますか? 削除が完了しました - 本当に選択したマンガを全編ダウンロードしますか?この動作は多くのトラフィックとストレージを消費する可能性があります + 選択したマンガとそのチャプターをすべてダウンロードしますか?これは、多くのトラフィックとストレージを消費する可能性があります。 IPアドレスのブロックを回避することができます 保存されたマンガの処理 ダウンロードの速度低下 @@ -383,9 +383,16 @@ 輝度 コントラスト リセット - 未保存の変更がありますが、保存しますか、それとも破棄しますか? + 未保存の変更を保存または破棄しますか\? 選択した色の設定は、この漫画のために記憶されます 破棄 デバイスに空き容量がありません ページ切り替えスライダーを表示 + サーバーサイドエラー (%1$d) です。後で再試行してください + 新しいチャプターの情報も明確に + さまざまな言語 + ネットワークが利用できません + Wi-Fiまたはモバイルネットワークをオンにして、オンラインでマンガを読むことができます + Webtoonズーム + ウェブトゥーンモードでズームイン/ズームアウトのジェスチャーを可能にする(ベータ版) \ No newline at end of file From 347811abb627acf57d0b0e1816b59139522ea393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Thu, 20 Oct 2022 09:02:50 +0200 Subject: [PATCH 32/53] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 82.9% (330 of 398 strings) Co-authored-by: Allan Nordhøy Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/ Translation: Kotatsu/Strings --- app/src/main/res/values-nb-rNO/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index ba5ce1122..6d9e734d4 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -331,4 +331,12 @@ Lysstyrke Fargekorrigering Lagre eller forkast ulagrede endringer\? + Kompakt + Skru av alle + Bruk fingeravtrykk hvis tilgjengelig + Skru på Wi-Fi eller mobilnettverk for å lese manga på nett + Skru på merknader + Vis indikatorer for leseforløp + Datasletting + Avsluttingsbekreftelse \ No newline at end of file From 980988e68486280b688e43e4276ad53e457a5999 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 20 Oct 2022 09:02:50 +0200 Subject: [PATCH 33/53] Translated using Weblate (Russian) Currently translated at 99.7% (397 of 398 strings) Co-authored-by: Koitharu Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/ Translation: Kotatsu/Strings --- app/src/main/res/values-ru/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 1e2afced6..3d4a096bf 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -395,4 +395,5 @@ Сеть недоступна Также очистить информацию о новых главах Внутренняя ошибка сервера (%1$d). Повторите попытку позже + Компактно \ No newline at end of file From 3f76d22d672356b6aad267e94a8e97a1bc9aec6f Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 20 Oct 2022 09:02:50 +0200 Subject: [PATCH 34/53] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (398 of 398 strings) Co-authored-by: Eric Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/ Translation: Kotatsu/Strings --- app/src/main/res/values-zh-rCN/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 843feee9c..d38116018 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -394,4 +394,5 @@ 打开 Wi-Fi 或移动网络在线阅读漫画 同样清除新章节信息 服务器端错误 (%1$d)。请稍后再试 + 紧凑 \ No newline at end of file From 32cfbb327cf9fadd7700393b47edda9acf9742b9 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 21 Oct 2022 17:57:35 +0300 Subject: [PATCH 35/53] Fix potential crash related to slider --- .../java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt | 3 ++- .../reader/ui/colorfilter/ColorFilterConfigActivity.kt | 4 ++-- .../kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt | 6 +++++- .../kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt | 7 ++++--- 4 files changed, 13 insertions(+), 7 deletions(-) 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 3510318dc..e4f97f1f5 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 @@ -58,6 +58,7 @@ import org.koitharu.kotatsu.utils.ext.isReportable import org.koitharu.kotatsu.utils.ext.observeWithPrevious import org.koitharu.kotatsu.utils.ext.postDelayed import org.koitharu.kotatsu.utils.ext.report +import org.koitharu.kotatsu.utils.ext.setValueRounded import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -388,7 +389,7 @@ class ReaderActivity : } if (uiState.isSliderAvailable()) { binding.slider.valueTo = uiState.totalPages.toFloat() - 1 - binding.slider.value = uiState.currentPage.toFloat() + binding.slider.setValueRounded(uiState.currentPage.toFloat()) binding.slider.isVisible = true } else { binding.slider.isVisible = false diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt index 437e9b230..ee94a4f87 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/colorfilter/ColorFilterConfigActivity.kt @@ -103,8 +103,8 @@ class ColorFilterConfigActivity : } private fun onColorFilterChanged(readerColorFilter: ReaderColorFilter?) { - binding.sliderBrightness.value = readerColorFilter?.brightness ?: 0f - binding.sliderContrast.value = readerColorFilter?.contrast ?: 0f + binding.sliderBrightness.setValueRounded(readerColorFilter?.brightness ?: 0f) + binding.sliderContrast.setValueRounded(readerColorFilter?.contrast ?: 0f) binding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter() } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt index 0eb155935..51b33d0b3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/config/ReaderConfigBottomSheet.kt @@ -23,6 +23,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderViewModel import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.utils.ScreenOrientationHelper +import org.koitharu.kotatsu.utils.ext.setValueRounded import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import org.koitharu.kotatsu.utils.ext.withArgs @@ -65,7 +66,7 @@ class ReaderConfigBottomSheet : binding.sliderTimer.setLabelFormatter(PageSwitchTimer.DelayLabelFormatter(view.resources)) findCallback()?.run { - binding.sliderTimer.value = pageSwitchDelay + binding.sliderTimer.setValueRounded(pageSwitchDelay) } } @@ -75,13 +76,16 @@ class ReaderConfigBottomSheet : startActivity(SettingsActivity.newReaderSettingsIntent(v.context)) dismissAllowingStateLoss() } + R.id.button_save_page -> { val page = viewModel.getCurrentPage() ?: return viewModel.saveCurrentPage(page, savePageRequest) } + R.id.button_screen_rotate -> { orientationHelper?.toggleOrientation() } + R.id.button_color_filter -> { val page = viewModel.getCurrentPage() ?: return val manga = viewModel.manga ?: return diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt index dd84a891b..9677f8d4e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt @@ -8,13 +8,13 @@ import androidx.fragment.app.FragmentManager import com.google.android.material.slider.LabelFormatter import com.google.android.material.slider.Slider import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.databinding.SheetShelfSizeBinding import org.koitharu.kotatsu.utils.ext.setValueRounded import org.koitharu.kotatsu.utils.progress.IntPercentLabelFormatter +import javax.inject.Inject @AndroidEntryPoint class ShelfSizeBottomSheet : @@ -51,9 +51,10 @@ class ShelfSizeBottomSheet : } override fun onClick(v: View) { + val slider = binding.sliderGrid when (v.id) { - R.id.button_small -> binding.sliderGrid.value -= binding.sliderGrid.stepSize - R.id.button_large -> binding.sliderGrid.value += binding.sliderGrid.stepSize + R.id.button_small -> slider.setValueRounded(slider.value - slider.stepSize) + R.id.button_large -> slider.setValueRounded(slider.value + slider.stepSize) } } From d8e7689a94fd4682004ae70e46f16a901cee7afa Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 22 Oct 2022 17:53:53 +0300 Subject: [PATCH 36/53] Update ssiv --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 44f047d0b..63754bfb1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 33 - versionCode 500 - versionName '4.0' + versionCode 501 + versionName '4.0.1' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -125,7 +125,7 @@ dependencies { implementation 'io.coil-kt:coil-base:2.2.2' implementation 'io.coil-kt:coil-svg:2.2.2' - implementation 'com.github.KotatsuApp:subsampling-scale-image-view:0ff0278f0f' + implementation 'com.github.KotatsuApp:subsampling-scale-image-view:f8a38b08fe' implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'ch.acra:acra-http:5.9.6' From ff58539e2ef434f192a2a58746f9ff42d48cbec4 Mon Sep 17 00:00:00 2001 From: "J. Lavoie" Date: Fri, 21 Oct 2022 22:02:42 +0200 Subject: [PATCH 37/53] Translated using Weblate (French) Currently translated at 100.0% (398 of 398 strings) Co-authored-by: J. Lavoie Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/ Translation: Kotatsu/Strings --- app/src/main/res/values-fr/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 604610d7f..cd3975284 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -392,4 +392,7 @@ Activez le Wi-Fi ou le réseau mobile pour lire les mangas en ligne Différentes langues Le réseau n\'est pas disponible + Compact + Erreur côté serveur (%1$d). Veuillez réessayer plus tard + Effacer aussi les informations sur les nouveaux chapitres \ No newline at end of file From cec19c3db37b3a34729c893b8f8a162855f44c4b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 23 Oct 2022 09:17:31 +0300 Subject: [PATCH 38/53] Fix crash related to slider --- app/build.gradle | 4 ++-- app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 63754bfb1..3324502f5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 33 - versionCode 501 - versionName '4.0.1' + versionCode 502 + versionName '4.0.2' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt index 29d0953c7..5d00f4a11 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ViewExt.kt @@ -127,7 +127,11 @@ fun RecyclerView.ViewHolder.getItem(clazz: Class): T? { fun Slider.setValueRounded(newValue: Float) { val step = stepSize - val roundedValue = (newValue / step).roundToInt() * step + val roundedValue = if (step <= 0f) { + newValue + } else { + (newValue / step).roundToInt() * step + } value = roundedValue.coerceIn(valueFrom, valueTo) } From c663d1051525d70aa23dff9125eb93cc40ff2fa2 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 24 Oct 2022 19:32:28 +0300 Subject: [PATCH 39/53] Configure shelf sections --- .github/FUNDING.yml | 2 +- .../AdapterDelegateClickListenerAdapter.kt | 4 +- .../koitharu/kotatsu/core/prefs/AppSection.kt | 6 -- .../kotatsu/core/prefs/AppSettings.kt | 20 ++++-- .../kotatsu/shelf/domain/ShelfContent.kt | 35 ++++++++++ .../kotatsu/shelf/domain/ShelfRepository.kt | 11 +++ .../kotatsu/shelf/domain/ShelfSection.kt | 6 ++ .../kotatsu/shelf/ui/ShelfMenuProvider.kt | 8 ++- .../kotatsu/shelf/ui/ShelfViewModel.kt | 48 +++++++------ .../ShelfCategoriesConfigAdapter.kt | 32 --------- .../ShelfCategoriesConfigViewModel.kt | 30 -------- .../ui/config/categories/ShelfCategoryAD.kt | 21 ------ .../ui/config/categories/ShelfConfigAD.kt | 51 ++++++++++++++ .../config/categories/ShelfConfigAdapter.kt | 42 +++++++++++ .../ui/config/categories/ShelfConfigModel.kt | 60 ++++++++++++++++ ...riesConfigSheet.kt => ShelfConfigSheet.kt} | 15 ++-- .../config/categories/ShelfConfigViewModel.kt | 69 +++++++++++++++++++ app/src/main/res/menu/opt_shelf.xml | 2 +- 18 files changed, 330 insertions(+), 132 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigAdapter.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigViewModel.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoryAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAD.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAdapter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigModel.kt rename app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/{ShelfCategoriesConfigSheet.kt => ShelfConfigSheet.kt} (73%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigViewModel.kt diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 203df6bfe..03f83f88f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ +ko_fi: xtimms custom: ["https://yoomoney.ru/to/410012543938752"] -ko_fi: koitharu diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt index 650e816c5..19d1d5661 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/AdapterDelegateClickListenerAdapter.kt @@ -6,7 +6,7 @@ import android.view.View.OnLongClickListener import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder class AdapterDelegateClickListenerAdapter( - private val adapterDelegate: AdapterDelegateViewBindingViewHolder, + private val adapterDelegate: AdapterDelegateViewBindingViewHolder, private val clickListener: OnListItemClickListener, ) : OnClickListener, OnLongClickListener { @@ -17,4 +17,4 @@ class AdapterDelegateClickListenerAdapter( override fun onLongClick(v: View): Boolean { return clickListener.onItemLongClick(adapterDelegate.item, v) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt deleted file mode 100644 index 0efa45c92..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSection.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.koitharu.kotatsu.core.prefs - -enum class AppSection { - - LOCAL, FAVOURITES, HISTORY, FEED, SUGGESTIONS -} \ 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 20a29269b..6fa3a8803 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 @@ -16,6 +16,8 @@ import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.network.DoHProvider import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.util.mapToSet +import org.koitharu.kotatsu.shelf.domain.ShelfSection import org.koitharu.kotatsu.utils.ext.getEnumValue import org.koitharu.kotatsu.utils.ext.observe import org.koitharu.kotatsu.utils.ext.putEnumValue @@ -44,14 +46,23 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { val remoteMangaSources: Set get() = Collections.unmodifiableSet(remoteSources) + var shelfSections: Set + get() { + val raw = prefs.getStringSet(KEY_SHELF_SECTIONS, null) + if (raw == null) { + return EnumSet.allOf(ShelfSection::class.java) + } + return raw.mapTo(EnumSet.noneOf(ShelfSection::class.java)) { ShelfSection.valueOf(it) } + } + set(value) { + val raw = value.mapToSet { it.name } + prefs.edit { putStringSet(KEY_SHELF_SECTIONS, raw) } + } + var listMode: ListMode get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID) set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) } - var defaultSection: AppSection - get() = prefs.getEnumValue(KEY_APP_SECTION, AppSection.HISTORY) - set(value) = prefs.edit { putEnumValue(KEY_APP_SECTION, value) } - val theme: Int get() = prefs.getString(KEY_THEME, null)?.toIntOrNull() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM @@ -341,6 +352,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_READER_TAPS_LTR = "reader_taps_ltr" const val KEY_LOCAL_LIST_ORDER = "local_order" const val KEY_WEBTOON_ZOOM = "webtoon_zoom" + const val KEY_SHELF_SECTIONS = "shelf_sections" // About const val KEY_APP_UPDATE = "app_update" diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt new file mode 100644 index 000000000..c8089e431 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfContent.kt @@ -0,0 +1,35 @@ +package org.koitharu.kotatsu.shelf.domain + +import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.history.domain.MangaWithHistory +import org.koitharu.kotatsu.parsers.model.Manga + +class ShelfContent( + val history: List, + val favourites: Map>, + val updated: Map, + val local: List, +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ShelfContent + + if (history != other.history) return false + if (favourites != other.favourites) return false + if (updated != other.updated) return false + if (local != other.local) return false + + return true + } + + override fun hashCode(): Int { + var result = history.hashCode() + result = 31 * result + favourites.hashCode() + result = 31 * result + updated.hashCode() + result = 31 * result + local.hashCode() + return result + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt index c56a04a35..7ecfa7dbf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfRepository.kt @@ -21,15 +21,26 @@ 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.SortOrder +import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import javax.inject.Inject class ShelfRepository @Inject constructor( private val localMangaRepository: LocalMangaRepository, private val historyRepository: HistoryRepository, + private val trackingRepository: TrackingRepository, private val db: MangaDatabase, ) { + fun observeShelfContent(): Flow = combine( + historyRepository.observeAllWithHistory(), + observeLocalManga(SortOrder.UPDATED), + observeFavourites(), + trackingRepository.observeUpdatedManga(), + ) { history, local, favorites, updated -> + ShelfContent(history, favorites, updated, local) + } + fun observeLocalManga(sortOrder: SortOrder): Flow> { return flow { emit(null) diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt new file mode 100644 index 000000000..de24a9583 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/domain/ShelfSection.kt @@ -0,0 +1,6 @@ +package org.koitharu.kotatsu.shelf.domain + +enum class ShelfSection { + + HISTORY, LOCAL, UPDATED, FAVORITES; +} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt index 589432ed7..b4d8fb5a2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt @@ -12,7 +12,7 @@ import java.util.* import java.util.concurrent.TimeUnit import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.dialog.RememberSelectionDialogListener -import org.koitharu.kotatsu.shelf.ui.config.categories.ShelfCategoriesConfigSheet +import org.koitharu.kotatsu.shelf.ui.config.categories.ShelfConfigSheet import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet import org.koitharu.kotatsu.local.ui.ImportDialogFragment import org.koitharu.kotatsu.utils.ext.startOfDay @@ -33,18 +33,22 @@ class ShelfMenuProvider( showClearHistoryDialog() true } + R.id.action_grid_size -> { ShelfSizeBottomSheet.show(fragmentManager) true } + R.id.action_import -> { ImportDialogFragment.show(fragmentManager) true } + R.id.action_categories -> { - ShelfCategoriesConfigSheet.show(fragmentManager) + ShelfConfigSheet.show(fragmentManager) true } + else -> false } } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt index 2e88331b2..53ca50f88 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt @@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.os.NetworkStateObserver 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.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.MangaWithHistory @@ -29,8 +30,9 @@ import org.koitharu.kotatsu.list.ui.model.toGridModel import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource -import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.shelf.domain.ShelfContent import org.koitharu.kotatsu.shelf.domain.ShelfRepository +import org.koitharu.kotatsu.shelf.domain.ShelfSection import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent @@ -44,19 +46,17 @@ class ShelfViewModel @Inject constructor( private val favouritesRepository: FavouritesRepository, private val trackingRepository: TrackingRepository, private val settings: AppSettings, - private val networkStateObserver: NetworkStateObserver, + networkStateObserver: NetworkStateObserver, ) : BaseViewModel(), ListExtraProvider { val onActionDone = SingleLiveEvent() val content: LiveData> = combine( + settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections }, networkStateObserver, - historyRepository.observeAllWithHistory(), - repository.observeLocalManga(SortOrder.UPDATED), - repository.observeFavourites(), - trackingRepository.observeUpdatedManga(), - ) { isConnected, history, local, favourites, updated -> - mapList(history, favourites, updated, local, isConnected) + repository.observeShelfContent(), + ) { sections, isConnected, content -> + mapList(content, sections, isConnected) }.debounce(500) .catch { e -> emit(listOf(e.toErrorState(canRetry = false))) @@ -134,25 +134,23 @@ class ShelfViewModel @Inject constructor( } private suspend fun mapList( - history: List, - favourites: Map>, - updated: Map, - local: List, + content: ShelfContent, + sections: Set, isNetworkAvailable: Boolean, ): List { - val result = ArrayList(favourites.keys.size + 3) + val result = ArrayList(content.favourites.keys.size + 3) if (isNetworkAvailable) { - if (history.isNotEmpty()) { - mapHistory(result, history) + if (content.history.isNotEmpty() && ShelfSection.HISTORY in sections) { + mapHistory(result, content.history) } - if (local.isNotEmpty()) { - mapLocal(result, local) + if (content.local.isNotEmpty() && ShelfSection.LOCAL in sections) { + mapLocal(result, content.local) } - if (updated.isNotEmpty()) { - mapUpdated(result, updated) + if (content.updated.isNotEmpty() && ShelfSection.UPDATED in sections) { + mapUpdated(result, content.updated) } - if (favourites.isNotEmpty()) { - mapFavourites(result, favourites) + if (content.favourites.isNotEmpty() && ShelfSection.FAVORITES in sections) { + mapFavourites(result, content.favourites) } } else { result += EmptyHint( @@ -161,12 +159,12 @@ class ShelfViewModel @Inject constructor( textSecondary = R.string.network_unavailable_hint, actionStringRes = R.string.manage, ) - val offlineHistory = history.filter { it.manga.source == MangaSource.LOCAL } - if (offlineHistory.isNotEmpty()) { + val offlineHistory = content.history.filter { it.manga.source == MangaSource.LOCAL } + if (offlineHistory.isNotEmpty() && ShelfSection.HISTORY in sections) { mapHistory(result, offlineHistory) } - if (local.isNotEmpty()) { - mapLocal(result, local) + if (content.local.isNotEmpty() && ShelfSection.LOCAL in sections) { + mapLocal(result, content.local) } } if (result.isEmpty()) { diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigAdapter.kt deleted file mode 100644 index 12d0b4a5d..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigAdapter.kt +++ /dev/null @@ -1,32 +0,0 @@ -package org.koitharu.kotatsu.shelf.ui.config.categories - -import androidx.recyclerview.widget.DiffUtil -import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.core.model.FavouriteCategory - -class ShelfCategoriesConfigAdapter( - listener: OnListItemClickListener, -) : AsyncListDifferDelegationAdapter(DiffCallback()) { - - init { - delegatesManager.addDelegate(shelfCategoryAD(listener)) - } - - class DiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean { - return oldItem.isVisibleInLibrary == newItem.isVisibleInLibrary && oldItem.title == newItem.title - } - - override fun getChangePayload(oldItem: FavouriteCategory, newItem: FavouriteCategory): Any? { - return if (oldItem.isVisibleInLibrary == newItem.isVisibleInLibrary) { - super.getChangePayload(oldItem, newItem) - } else Unit - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigViewModel.kt deleted file mode 100644 index 1c77b243e..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigViewModel.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.koitharu.kotatsu.shelf.ui.config.categories - -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import org.koitharu.kotatsu.base.ui.BaseViewModel -import org.koitharu.kotatsu.core.model.FavouriteCategory -import org.koitharu.kotatsu.favourites.domain.FavouritesRepository -import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct - -@HiltViewModel -class ShelfCategoriesConfigViewModel @Inject constructor( - private val favouritesRepository: FavouritesRepository, -) : BaseViewModel() { - - val content = favouritesRepository.observeCategories() - .asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) - - private var updateJob: Job? = null - - fun toggleItem(category: FavouriteCategory) { - val prevJob = updateJob - updateJob = launchJob(Dispatchers.Default) { - prevJob?.join() - favouritesRepository.updateCategory(category.id, !category.isVisibleInLibrary) - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoryAD.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoryAD.kt deleted file mode 100644 index 9aa529210..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoryAD.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.koitharu.kotatsu.shelf.ui.config.categories - -import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding -import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.core.model.FavouriteCategory -import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding - -fun shelfCategoryAD( - listener: OnListItemClickListener, -) = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) }, -) { - val eventListener = AdapterDelegateClickListenerAdapter(this, listener) - itemView.setOnClickListener(eventListener) - - bind { - binding.root.text = item.title - binding.root.isChecked = item.isVisibleInLibrary - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAD.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAD.kt new file mode 100644 index 000000000..a83180efb --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAD.kt @@ -0,0 +1,51 @@ +package org.koitharu.kotatsu.shelf.ui.config.categories + +import androidx.core.view.updatePaddingRelative +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener +import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding +import org.koitharu.kotatsu.shelf.domain.ShelfSection + +fun shelfSectionAD( + listener: OnListItemClickListener, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) }, +) { + + val eventListener = AdapterDelegateClickListenerAdapter(this, listener) + itemView.setOnClickListener(eventListener) + + bind { + binding.root.setText(item.section.titleResId) + binding.root.isChecked = item.isChecked + } +} + +fun shelfCategoryAD( + listener: OnListItemClickListener, +) = + adapterDelegateViewBinding( + { layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) }, + ) { + val eventListener = AdapterDelegateClickListenerAdapter(this, listener) + itemView.setOnClickListener(eventListener) + binding.root.updatePaddingRelative( + start = binding.root.paddingStart * 2, + end = binding.root.paddingStart, + ) + + bind { + binding.root.text = item.title + binding.root.isChecked = item.isChecked + } + } + +private val ShelfSection.titleResId: Int + get() = when (this) { + ShelfSection.HISTORY -> R.string.history + ShelfSection.LOCAL -> R.string.local_storage + ShelfSection.UPDATED -> R.string.updated + ShelfSection.FAVORITES -> R.string.favourites + } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAdapter.kt new file mode 100644 index 000000000..166dcf680 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAdapter.kt @@ -0,0 +1,42 @@ +package org.koitharu.kotatsu.shelf.ui.config.categories + +import androidx.recyclerview.widget.DiffUtil +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter +import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener + +class ShelfConfigAdapter( + listener: OnListItemClickListener, +) : AsyncListDifferDelegationAdapter(DiffCallback()) { + + init { + delegatesManager.addDelegate(shelfCategoryAD(listener)) + .addDelegate(shelfSectionAD(listener)) + } + + class DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ShelfConfigModel, newItem: ShelfConfigModel): Boolean { + return when { + oldItem is ShelfConfigModel.Section && newItem is ShelfConfigModel.Section -> { + oldItem.section == newItem.section + } + + oldItem is ShelfConfigModel.FavouriteCategory && newItem is ShelfConfigModel.FavouriteCategory -> { + oldItem.id == newItem.id + } + + else -> false + } + } + + override fun areContentsTheSame(oldItem: ShelfConfigModel, newItem: ShelfConfigModel): Boolean { + return oldItem == newItem + } + + override fun getChangePayload(oldItem: ShelfConfigModel, newItem: ShelfConfigModel): Any? { + return if (oldItem.isChecked == newItem.isChecked) { + super.getChangePayload(oldItem, newItem) + } else Unit + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigModel.kt new file mode 100644 index 000000000..f385b84be --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigModel.kt @@ -0,0 +1,60 @@ +package org.koitharu.kotatsu.shelf.ui.config.categories + +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.shelf.domain.ShelfSection + +sealed interface ShelfConfigModel : ListModel { + + val isChecked: Boolean + + class Section( + val section: ShelfSection, + override val isChecked: Boolean, + ) : ShelfConfigModel { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Section + + if (section != other.section) return false + if (isChecked != other.isChecked) return false + + return true + } + + override fun hashCode(): Int { + var result = section.hashCode() + result = 31 * result + isChecked.hashCode() + return result + } + } + + class FavouriteCategory( + val id: Long, + val title: String, + override val isChecked: Boolean, + ) : ShelfConfigModel { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FavouriteCategory + + if (id != other.id) return false + if (title != other.title) return false + if (isChecked != other.isChecked) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + title.hashCode() + result = 31 * result + isChecked.hashCode() + return result + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigSheet.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigSheet.kt similarity index 73% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigSheet.kt rename to app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigSheet.kt index 5df721191..0f2b7ab88 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfCategoriesConfigSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigSheet.kt @@ -11,16 +11,15 @@ import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.databinding.SheetBaseBinding @AndroidEntryPoint -class ShelfCategoriesConfigSheet : +class ShelfConfigSheet : BaseBottomSheet(), - OnListItemClickListener, + OnListItemClickListener, View.OnClickListener { - private val viewModel by viewModels() + private val viewModel by viewModels() override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding { return SheetBaseBinding.inflate(inflater, container, false) @@ -28,16 +27,16 @@ class ShelfCategoriesConfigSheet : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.headerBar.toolbar.setTitle(R.string.favourites_categories) + binding.headerBar.setTitle(R.string.settings) binding.buttonDone.isVisible = true binding.buttonDone.setOnClickListener(this) - val adapter = ShelfCategoriesConfigAdapter(this) + val adapter = ShelfConfigAdapter(this) binding.recyclerView.adapter = adapter viewModel.content.observe(viewLifecycleOwner) { adapter.items = it } } - override fun onItemClick(item: FavouriteCategory, view: View) { + override fun onItemClick(item: ShelfConfigModel, view: View) { viewModel.toggleItem(item) } @@ -49,6 +48,6 @@ class ShelfCategoriesConfigSheet : private const val TAG = "ShelfCategoriesConfigSheet" - fun show(fm: FragmentManager) = ShelfCategoriesConfigSheet().show(fm, TAG) + fun show(fm: FragmentManager) = ShelfConfigSheet().show(fm, TAG) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigViewModel.kt new file mode 100644 index 000000000..5e9cb38da --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigViewModel.kt @@ -0,0 +1,69 @@ +package org.koitharu.kotatsu.shelf.ui.config.categories + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +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.shelf.domain.ShelfSection +import org.koitharu.kotatsu.utils.asFlowLiveData +import javax.inject.Inject + +@HiltViewModel +class ShelfConfigViewModel @Inject constructor( + private val favouritesRepository: FavouritesRepository, + private val settings: AppSettings, +) : BaseViewModel() { + + val content = combine( + settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections }, + favouritesRepository.observeCategories(), + ) { sections, categories -> + buildList(sections, categories) + }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) + + private var updateJob: Job? = null + + fun toggleItem(item: ShelfConfigModel) { + val prevJob = updateJob + updateJob = launchJob(Dispatchers.Default) { + prevJob?.join() + when (item) { + is ShelfConfigModel.FavouriteCategory -> { + favouritesRepository.updateCategory(item.id, !item.isChecked) + } + + is ShelfConfigModel.Section -> { + if (item.isChecked) { + settings.shelfSections -= item.section + } else { + settings.shelfSections += item.section + } + } + } + } + } + + private fun buildList(sections: Set, categories: List): List { + val result = ArrayList() + for (section in ShelfSection.values()) { + val isEnabled = section in sections + result.add(ShelfConfigModel.Section(section, isEnabled)) + if (section == ShelfSection.FAVORITES && isEnabled) { + categories.mapTo(result) { + ShelfConfigModel.FavouriteCategory( + id = it.id, + title = it.title, + isChecked = it.isVisibleInLibrary, + ) + } + } + } + return result + } +} diff --git a/app/src/main/res/menu/opt_shelf.xml b/app/src/main/res/menu/opt_shelf.xml index 3cefebcb3..3a88eac5c 100644 --- a/app/src/main/res/menu/opt_shelf.xml +++ b/app/src/main/res/menu/opt_shelf.xml @@ -12,7 +12,7 @@ Date: Mon, 24 Oct 2022 19:40:08 +0300 Subject: [PATCH 40/53] Configure shelf sections (2) --- .../koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt | 10 +++++----- .../ui/config/{categories => }/ShelfConfigAD.kt | 2 +- .../config/{categories => }/ShelfConfigAdapter.kt | 2 +- .../ui/config/{categories => }/ShelfConfigModel.kt | 2 +- .../ui/config/{categories => }/ShelfConfigSheet.kt | 2 +- .../config/{categories => }/ShelfConfigViewModel.kt | 13 +++++++++---- 6 files changed, 18 insertions(+), 13 deletions(-) rename app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/{categories => }/ShelfConfigAD.kt (97%) rename app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/{categories => }/ShelfConfigAdapter.kt (95%) rename app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/{categories => }/ShelfConfigModel.kt (95%) rename app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/{categories => }/ShelfConfigSheet.kt (96%) rename app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/{categories => }/ShelfConfigViewModel.kt (87%) diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt index b4d8fb5a2..8b6f5dfeb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt @@ -6,16 +6,16 @@ import android.view.MenuInflater import android.view.MenuItem import androidx.core.view.MenuProvider import androidx.fragment.app.FragmentManager -import com.google.android.material.R as materialR import com.google.android.material.dialog.MaterialAlertDialogBuilder -import java.util.* -import java.util.concurrent.TimeUnit import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.dialog.RememberSelectionDialogListener -import org.koitharu.kotatsu.shelf.ui.config.categories.ShelfConfigSheet -import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet import org.koitharu.kotatsu.local.ui.ImportDialogFragment +import org.koitharu.kotatsu.shelf.ui.config.ShelfConfigSheet +import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet import org.koitharu.kotatsu.utils.ext.startOfDay +import java.util.Date +import java.util.concurrent.TimeUnit +import com.google.android.material.R as materialR class ShelfMenuProvider( private val context: Context, diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAD.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAD.kt similarity index 97% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAD.kt rename to app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAD.kt index a83180efb..fae0f67f0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAD.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.shelf.ui.config.categories +package org.koitharu.kotatsu.shelf.ui.config import androidx.core.view.updatePaddingRelative import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAdapter.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAdapter.kt rename to app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAdapter.kt index 166dcf680..7b8417830 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAdapter.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.shelf.ui.config.categories +package org.koitharu.kotatsu.shelf.ui.config import androidx.recyclerview.widget.DiffUtil import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigModel.kt similarity index 95% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigModel.kt rename to app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigModel.kt index f385b84be..996840a06 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigModel.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.shelf.ui.config.categories +package org.koitharu.kotatsu.shelf.ui.config import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.shelf.domain.ShelfSection diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigSheet.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigSheet.kt similarity index 96% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigSheet.kt rename to app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigSheet.kt index 0f2b7ab88..24fa59482 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigSheet.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.shelf.ui.config.categories +package org.koitharu.kotatsu.shelf.ui.config import android.os.Bundle import android.view.LayoutInflater diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigViewModel.kt similarity index 87% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigViewModel.kt rename to app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigViewModel.kt index 5e9cb38da..bd80a7976 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/categories/ShelfConfigViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigViewModel.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.shelf.ui.config.categories +package org.koitharu.kotatsu.shelf.ui.config import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel @@ -39,10 +39,15 @@ class ShelfConfigViewModel @Inject constructor( } is ShelfConfigModel.Section -> { - if (item.isChecked) { - settings.shelfSections -= item.section + val sections = settings.shelfSections + settings.shelfSections = if (item.isChecked) { + if (sections.size > 1) { + sections - item.section + } else { + return@launchJob + } } else { - settings.shelfSections += item.section + sections + item.section } } } From 0e5221fa6e4f906723bb106af5497773d4be9fbe Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 24 Oct 2022 19:52:30 +0300 Subject: [PATCH 41/53] Fix NetworkStateObserver --- .../kotatsu/core/os/NetworkStateObserver.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt index 159ef8a4f..6baefd34d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt @@ -7,6 +7,7 @@ import android.net.NetworkRequest import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.onSuccess import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.StateFlow @@ -26,7 +27,8 @@ class NetworkStateObserver @Inject constructor( override val replayCache: List get() = listOf(value) - override var value: Boolean = connectivityManager.isNetworkAvailable + override val value: Boolean + get() = connectivityManager.isNetworkAvailable override suspend fun collect(collector: FlowCollector): Nothing { collector.emit(value) @@ -44,9 +46,12 @@ class NetworkStateObserver @Inject constructor( } } - inner class FlowNetworkCallback( + private inner class FlowNetworkCallback( private val producerScope: ProducerScope, ) : NetworkCallback() { + + private var prevValue = value + override fun onAvailable(network: Network) = update() override fun onLost(network: Network) = update() @@ -55,9 +60,10 @@ class NetworkStateObserver @Inject constructor( private fun update() { val newValue = connectivityManager.isNetworkAvailable - if (value != newValue) { - value = newValue - producerScope.trySendBlocking(newValue) + if (newValue != prevValue) { + producerScope.trySendBlocking(newValue).onSuccess { + prevValue = newValue + } } } } From 38d4274ece36cd4aa517428619246ff8986416dd Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 28 Oct 2022 07:56:29 +0300 Subject: [PATCH 42/53] Shelf settings --- app/src/main/AndroidManifest.xml | 3 + .../kotatsu/core/prefs/AppSettings.kt | 20 ++-- .../kotatsu/shelf/ui/ShelfMenuProvider.kt | 4 +- .../kotatsu/shelf/ui/ShelfViewModel.kt | 49 +++++---- .../kotatsu/shelf/ui/config/ShelfConfigAD.kt | 51 --------- .../shelf/ui/config/ShelfConfigAdapter.kt | 42 -------- .../shelf/ui/config/ShelfConfigSheet.kt | 53 --------- .../shelf/ui/config/ShelfConfigViewModel.kt | 74 ------------- .../shelf/ui/config/ShelfSettingsActivity.kt | 101 ++++++++++++++++++ .../shelf/ui/config/ShelfSettingsAdapter.kt | 41 +++++++ .../config/ShelfSettingsAdapterDelegates.kt | 75 +++++++++++++ ...nfigModel.kt => ShelfSettingsItemModel.kt} | 6 +- .../shelf/ui/config/ShelfSettingsListener.kt | 10 ++ .../shelf/ui/config/ShelfSettingsViewModel.kt | 101 ++++++++++++++++++ .../res/layout/activity_shelf_settings.xml | 36 +++++++ .../layout/item_shelf_section_draggable.xml | 36 +++++++ 16 files changed, 450 insertions(+), 252 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAD.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAdapter.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigSheet.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigViewModel.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt rename app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/{ShelfConfigModel.kt => ShelfSettingsItemModel.kt} (91%) create mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt create mode 100644 app/src/main/res/layout/activity_shelf_settings.xml create mode 100644 app/src/main/res/layout/item_shelf_section_draggable.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3faa30c69..1183591b1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -139,6 +139,9 @@ + get() = Collections.unmodifiableSet(remoteSources) - var shelfSections: Set + var shelfSections: List get() { - val raw = prefs.getStringSet(KEY_SHELF_SECTIONS, null) - if (raw == null) { - return EnumSet.allOf(ShelfSection::class.java) + val raw = prefs.getString(KEY_SHELF_SECTIONS, null) + val values = enumValues() + if (raw.isNullOrEmpty()) { + return values.toList() } - return raw.mapTo(EnumSet.noneOf(ShelfSection::class.java)) { ShelfSection.valueOf(it) } + return raw.split('|') + .mapNotNull { values.getOrNull(it.toIntOrNull() ?: -1) } + .distinct() } set(value) { - val raw = value.mapToSet { it.name } - prefs.edit { putStringSet(KEY_SHELF_SECTIONS, raw) } + val raw = value.joinToString("|") { it.ordinal.toString() } + prefs.edit { putString(KEY_SHELF_SECTIONS, raw) } } var listMode: ListMode @@ -352,7 +354,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_READER_TAPS_LTR = "reader_taps_ltr" const val KEY_LOCAL_LIST_ORDER = "local_order" const val KEY_WEBTOON_ZOOM = "webtoon_zoom" - const val KEY_SHELF_SECTIONS = "shelf_sections" + const val KEY_SHELF_SECTIONS = "shelf_sections_2" // About const val KEY_APP_UPDATE = "app_update" diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt index 8b6f5dfeb..d00601406 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt @@ -10,7 +10,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.dialog.RememberSelectionDialogListener import org.koitharu.kotatsu.local.ui.ImportDialogFragment -import org.koitharu.kotatsu.shelf.ui.config.ShelfConfigSheet +import org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet import org.koitharu.kotatsu.utils.ext.startOfDay import java.util.Date @@ -45,7 +45,7 @@ class ShelfMenuProvider( } R.id.action_categories -> { - ShelfConfigSheet.show(fragmentManager) + context.startActivity(ShelfSettingsActivity.newIntent(context)) true } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt index 53ca50f88..a56d848ea 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt @@ -135,22 +135,18 @@ class ShelfViewModel @Inject constructor( private suspend fun mapList( content: ShelfContent, - sections: Set, + sections: List, isNetworkAvailable: Boolean, ): List { val result = ArrayList(content.favourites.keys.size + 3) if (isNetworkAvailable) { - if (content.history.isNotEmpty() && ShelfSection.HISTORY in sections) { - mapHistory(result, content.history) - } - if (content.local.isNotEmpty() && ShelfSection.LOCAL in sections) { - mapLocal(result, content.local) - } - if (content.updated.isNotEmpty() && ShelfSection.UPDATED in sections) { - mapUpdated(result, content.updated) - } - if (content.favourites.isNotEmpty() && ShelfSection.FAVORITES in sections) { - mapFavourites(result, content.favourites) + for (section in sections) { + when (section) { + ShelfSection.HISTORY -> mapHistory(result, content.history) + ShelfSection.LOCAL -> mapLocal(result, content.local) + ShelfSection.UPDATED -> mapUpdated(result, content.updated) + ShelfSection.FAVORITES -> mapFavourites(result, content.favourites) + } } } else { result += EmptyHint( @@ -159,12 +155,17 @@ class ShelfViewModel @Inject constructor( textSecondary = R.string.network_unavailable_hint, actionStringRes = R.string.manage, ) - val offlineHistory = content.history.filter { it.manga.source == MangaSource.LOCAL } - if (offlineHistory.isNotEmpty() && ShelfSection.HISTORY in sections) { - mapHistory(result, offlineHistory) - } - if (content.local.isNotEmpty() && ShelfSection.LOCAL in sections) { - mapLocal(result, content.local) + for (section in sections) { + when (section) { + ShelfSection.HISTORY -> mapHistory( + result, + content.history.filter { it.manga.source == MangaSource.LOCAL }, + ) + + ShelfSection.LOCAL -> mapLocal(result, content.local) + ShelfSection.UPDATED -> Unit + ShelfSection.FAVORITES -> Unit + } } } if (result.isEmpty()) { @@ -187,6 +188,9 @@ class ShelfViewModel @Inject constructor( destination: MutableList, list: List, ) { + if (list.isEmpty()) { + return + } val showPercent = settings.isReadingIndicatorsEnabled destination += ShelfSectionModel.History( items = list.map { (manga, history) -> @@ -202,6 +206,9 @@ class ShelfViewModel @Inject constructor( destination: MutableList, updated: Map, ) { + if (updated.isEmpty()) { + return + } val showPercent = settings.isReadingIndicatorsEnabled destination += ShelfSectionModel.Updated( items = updated.map { (manga, counter) -> @@ -216,6 +223,9 @@ class ShelfViewModel @Inject constructor( destination: MutableList, local: List, ) { + if (local.isEmpty()) { + return + } destination += ShelfSectionModel.Local( items = local.toUi(ListMode.GRID, this), showAllButtonText = R.string.show_all, @@ -226,6 +236,9 @@ class ShelfViewModel @Inject constructor( destination: MutableList, favourites: Map>, ) { + if (favourites.isEmpty()) { + return + } for ((category, list) in favourites) { if (list.isNotEmpty()) { destination += ShelfSectionModel.Favourites( diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAD.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAD.kt deleted file mode 100644 index fae0f67f0..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAD.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.koitharu.kotatsu.shelf.ui.config - -import androidx.core.view.updatePaddingRelative -import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding -import org.koitharu.kotatsu.shelf.domain.ShelfSection - -fun shelfSectionAD( - listener: OnListItemClickListener, -) = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) }, -) { - - val eventListener = AdapterDelegateClickListenerAdapter(this, listener) - itemView.setOnClickListener(eventListener) - - bind { - binding.root.setText(item.section.titleResId) - binding.root.isChecked = item.isChecked - } -} - -fun shelfCategoryAD( - listener: OnListItemClickListener, -) = - adapterDelegateViewBinding( - { layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) }, - ) { - val eventListener = AdapterDelegateClickListenerAdapter(this, listener) - itemView.setOnClickListener(eventListener) - binding.root.updatePaddingRelative( - start = binding.root.paddingStart * 2, - end = binding.root.paddingStart, - ) - - bind { - binding.root.text = item.title - binding.root.isChecked = item.isChecked - } - } - -private val ShelfSection.titleResId: Int - get() = when (this) { - ShelfSection.HISTORY -> R.string.history - ShelfSection.LOCAL -> R.string.local_storage - ShelfSection.UPDATED -> R.string.updated - ShelfSection.FAVORITES -> R.string.favourites - } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAdapter.kt deleted file mode 100644 index 7b8417830..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigAdapter.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.koitharu.kotatsu.shelf.ui.config - -import androidx.recyclerview.widget.DiffUtil -import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener - -class ShelfConfigAdapter( - listener: OnListItemClickListener, -) : AsyncListDifferDelegationAdapter(DiffCallback()) { - - init { - delegatesManager.addDelegate(shelfCategoryAD(listener)) - .addDelegate(shelfSectionAD(listener)) - } - - class DiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: ShelfConfigModel, newItem: ShelfConfigModel): Boolean { - return when { - oldItem is ShelfConfigModel.Section && newItem is ShelfConfigModel.Section -> { - oldItem.section == newItem.section - } - - oldItem is ShelfConfigModel.FavouriteCategory && newItem is ShelfConfigModel.FavouriteCategory -> { - oldItem.id == newItem.id - } - - else -> false - } - } - - override fun areContentsTheSame(oldItem: ShelfConfigModel, newItem: ShelfConfigModel): Boolean { - return oldItem == newItem - } - - override fun getChangePayload(oldItem: ShelfConfigModel, newItem: ShelfConfigModel): Any? { - return if (oldItem.isChecked == newItem.isChecked) { - super.getChangePayload(oldItem, newItem) - } else Unit - } - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigSheet.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigSheet.kt deleted file mode 100644 index 24fa59482..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigSheet.kt +++ /dev/null @@ -1,53 +0,0 @@ -package org.koitharu.kotatsu.shelf.ui.config - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.viewModels -import dagger.hilt.android.AndroidEntryPoint -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.ui.BaseBottomSheet -import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.databinding.SheetBaseBinding - -@AndroidEntryPoint -class ShelfConfigSheet : - BaseBottomSheet(), - OnListItemClickListener, - View.OnClickListener { - - private val viewModel by viewModels() - - override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding { - return SheetBaseBinding.inflate(inflater, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.headerBar.setTitle(R.string.settings) - binding.buttonDone.isVisible = true - binding.buttonDone.setOnClickListener(this) - val adapter = ShelfConfigAdapter(this) - binding.recyclerView.adapter = adapter - - viewModel.content.observe(viewLifecycleOwner) { adapter.items = it } - } - - override fun onItemClick(item: ShelfConfigModel, view: View) { - viewModel.toggleItem(item) - } - - override fun onClick(v: View?) { - dismiss() - } - - companion object { - - private const val TAG = "ShelfCategoriesConfigSheet" - - fun show(fm: FragmentManager) = ShelfConfigSheet().show(fm, TAG) - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigViewModel.kt deleted file mode 100644 index bd80a7976..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigViewModel.kt +++ /dev/null @@ -1,74 +0,0 @@ -package org.koitharu.kotatsu.shelf.ui.config - -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -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.shelf.domain.ShelfSection -import org.koitharu.kotatsu.utils.asFlowLiveData -import javax.inject.Inject - -@HiltViewModel -class ShelfConfigViewModel @Inject constructor( - private val favouritesRepository: FavouritesRepository, - private val settings: AppSettings, -) : BaseViewModel() { - - val content = combine( - settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections }, - favouritesRepository.observeCategories(), - ) { sections, categories -> - buildList(sections, categories) - }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) - - private var updateJob: Job? = null - - fun toggleItem(item: ShelfConfigModel) { - val prevJob = updateJob - updateJob = launchJob(Dispatchers.Default) { - prevJob?.join() - when (item) { - is ShelfConfigModel.FavouriteCategory -> { - favouritesRepository.updateCategory(item.id, !item.isChecked) - } - - is ShelfConfigModel.Section -> { - val sections = settings.shelfSections - settings.shelfSections = if (item.isChecked) { - if (sections.size > 1) { - sections - item.section - } else { - return@launchJob - } - } else { - sections + item.section - } - } - } - } - } - - private fun buildList(sections: Set, categories: List): List { - val result = ArrayList() - for (section in ShelfSection.values()) { - val isEnabled = section in sections - result.add(ShelfConfigModel.Section(section, isEnabled)) - if (section == ShelfSection.FAVORITES && isEnabled) { - categories.mapTo(result) { - ShelfConfigModel.FavouriteCategory( - id = it.id, - title = it.title, - isChecked = it.isVisibleInLibrary, - ) - } - } - } - return result - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt new file mode 100644 index 000000000..ef28b06a5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt @@ -0,0 +1,101 @@ +package org.koitharu.kotatsu.shelf.ui.config + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.activity.viewModels +import androidx.core.graphics.Insets +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.base.ui.BaseActivity +import org.koitharu.kotatsu.databinding.ActivityShelfSettingsBinding + +@AndroidEntryPoint +class ShelfSettingsActivity : + BaseActivity(), + View.OnClickListener, ShelfSettingsListener { + + private val viewModel by viewModels() + private lateinit var reorderHelper: ItemTouchHelper + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivityShelfSettingsBinding.inflate(layoutInflater)) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setHomeAsUpIndicator(com.google.android.material.R.drawable.abc_ic_clear_material) + } + binding.buttonDone.setOnClickListener(this) + val settingsAdapter = ShelfSettingsAdapter(this) + with(binding.recyclerView) { + setHasFixedSize(true) + adapter = settingsAdapter + reorderHelper = ItemTouchHelper(SectionsReorderCallback()).also { + it.attachToRecyclerView(this) + } + } + + + viewModel.content.observe(this) { settingsAdapter.items = it } + } + + override fun onItemCheckedChanged(item: ShelfSettingsItemModel, isChecked: Boolean) { + viewModel.setItemChecked(item, isChecked) + } + + override fun onDragHandleTouch(holder: RecyclerView.ViewHolder) { + reorderHelper.startDrag(holder) + } + + override fun onClick(v: View?) { + finishAfterTransition() + } + + override fun onWindowInsetsChanged(insets: Insets) { + binding.root.updatePadding( + left = insets.left, + right = insets.right, + ) + binding.recyclerView.updatePadding( + bottom = insets.bottom, + ) + binding.toolbar.updateLayoutParams { + topMargin = insets.top + } + } + + private inner class SectionsReorderCallback : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.DOWN or ItemTouchHelper.UP, + 0, + ) { + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean = viewHolder.itemViewType == target.itemViewType && viewModel.reorderSections( + viewHolder.bindingAdapterPosition, + target.bindingAdapterPosition, + ) + + override fun canDropOver( + recyclerView: RecyclerView, + current: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean = current.itemViewType == target.itemViewType + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit + + override fun isLongPressDragEnabled() = false + } + + companion object { + + fun newIntent(context: Context) = Intent(context, ShelfSettingsActivity::class.java) + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt new file mode 100644 index 000000000..19aaa81cf --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt @@ -0,0 +1,41 @@ +package org.koitharu.kotatsu.shelf.ui.config + +import androidx.recyclerview.widget.DiffUtil +import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter + +class ShelfSettingsAdapter( + listener: ShelfSettingsListener, +) : AsyncListDifferDelegationAdapter(DiffCallback()) { + + init { + delegatesManager.addDelegate(shelfCategoryAD(listener)) + .addDelegate(shelfSectionAD(listener)) + } + + class DiffCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Boolean { + return when { + oldItem is ShelfSettingsItemModel.Section && newItem is ShelfSettingsItemModel.Section -> { + oldItem.section == newItem.section + } + + oldItem is ShelfSettingsItemModel.FavouriteCategory && newItem is ShelfSettingsItemModel.FavouriteCategory -> { + oldItem.id == newItem.id + } + + else -> false + } + } + + override fun areContentsTheSame(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Boolean { + return oldItem == newItem + } + + override fun getChangePayload(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Any? { + return if (oldItem.isChecked == newItem.isChecked) { + super.getChangePayload(oldItem, newItem) + } else Unit + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt new file mode 100644 index 000000000..973391190 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt @@ -0,0 +1,75 @@ +package org.koitharu.kotatsu.shelf.ui.config + +import android.annotation.SuppressLint +import android.view.MotionEvent +import android.view.View +import android.widget.CompoundButton +import androidx.core.view.updatePaddingRelative +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding +import org.koitharu.kotatsu.databinding.ItemShelfSectionDraggableBinding +import org.koitharu.kotatsu.shelf.domain.ShelfSection + +@SuppressLint("ClickableViewAccessibility") +fun shelfSectionAD( + listener: ShelfSettingsListener, +) = + adapterDelegateViewBinding( + { layoutInflater, parent -> ItemShelfSectionDraggableBinding.inflate(layoutInflater, parent, false) }, + ) { + + val eventListener = object : + View.OnTouchListener, + CompoundButton.OnCheckedChangeListener { + + override fun onTouch(v: View?, event: MotionEvent): Boolean { + return if (event.actionMasked == MotionEvent.ACTION_DOWN) { + listener.onDragHandleTouch(this@adapterDelegateViewBinding) + true + } else { + false + } + } + + override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) { + listener.onItemCheckedChanged(item, isChecked) + } + } + + binding.switchToggle.setOnCheckedChangeListener(eventListener) + binding.imageViewHandle.setOnTouchListener(eventListener) + + bind { + binding.textViewTitle.setText(item.section.titleResId) + binding.switchToggle.isChecked = item.isChecked + } + } + +fun shelfCategoryAD( + listener: ShelfSettingsListener, +) = + adapterDelegateViewBinding( + { layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) }, + ) { + itemView.setOnClickListener { + listener.onItemCheckedChanged(item, !item.isChecked) + } + binding.root.updatePaddingRelative( + start = binding.root.paddingStart * 2, + end = binding.root.paddingStart, + ) + + bind { + binding.root.text = item.title + binding.root.isChecked = item.isChecked + } + } + +private val ShelfSection.titleResId: Int + get() = when (this) { + ShelfSection.HISTORY -> R.string.history + ShelfSection.LOCAL -> R.string.local_storage + ShelfSection.UPDATED -> R.string.updated + ShelfSection.FAVORITES -> R.string.favourites + } diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt similarity index 91% rename from app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigModel.kt rename to app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt index 996840a06..e75f329de 100644 --- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfConfigModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt @@ -3,14 +3,14 @@ package org.koitharu.kotatsu.shelf.ui.config import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.shelf.domain.ShelfSection -sealed interface ShelfConfigModel : ListModel { +sealed interface ShelfSettingsItemModel : ListModel { val isChecked: Boolean class Section( val section: ShelfSection, override val isChecked: Boolean, - ) : ShelfConfigModel { + ) : ShelfSettingsItemModel { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -35,7 +35,7 @@ sealed interface ShelfConfigModel : ListModel { val id: Long, val title: String, override val isChecked: Boolean, - ) : ShelfConfigModel { + ) : ShelfSettingsItemModel { override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt new file mode 100644 index 000000000..b8fa749a1 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt @@ -0,0 +1,10 @@ +package org.koitharu.kotatsu.shelf.ui.config + +import androidx.recyclerview.widget.RecyclerView + +interface ShelfSettingsListener { + + fun onItemCheckedChanged(item: ShelfSettingsItemModel, isChecked: Boolean) + + fun onDragHandleTouch(holder: RecyclerView.ViewHolder) +} diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt new file mode 100644 index 000000000..15323d9b8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt @@ -0,0 +1,101 @@ +package org.koitharu.kotatsu.shelf.ui.config + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +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.shelf.domain.ShelfSection +import org.koitharu.kotatsu.utils.asFlowLiveData +import org.koitharu.kotatsu.utils.ext.move +import javax.inject.Inject + +@HiltViewModel +class ShelfSettingsViewModel @Inject constructor( + private val favouritesRepository: FavouritesRepository, + private val settings: AppSettings, +) : BaseViewModel() { + + val content = combine( + settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections }, + favouritesRepository.observeCategories(), + ) { sections, categories -> + buildList(sections, categories) + }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) + + private var updateJob: Job? = null + + fun setItemChecked(item: ShelfSettingsItemModel, isChecked: Boolean) { + val prevJob = updateJob + updateJob = launchJob(Dispatchers.Default) { + prevJob?.join() + when (item) { + is ShelfSettingsItemModel.FavouriteCategory -> { + favouritesRepository.updateCategory(item.id, isChecked) + } + + is ShelfSettingsItemModel.Section -> { + val sections = settings.shelfSections + settings.shelfSections = if (isChecked) { + sections + item.section + } else { + if (sections.size > 1) { + sections - item.section + } else { + return@launchJob + } + } + } + } + } + } + + fun reorderSections(oldPos: Int, newPos: Int): Boolean { + val snapshot = content.value?.toMutableList() ?: return false + snapshot.move(oldPos, newPos) + settings.shelfSections = snapshot.sections() + return true + } + + private fun buildList( + sections: List, + categories: List + ): List { + val result = ArrayList() + val sectionsList = ShelfSection.values().toMutableList() + for (section in sections) { + sectionsList.remove(section) + result.addSection(section, true, categories) + } + for (section in sectionsList) { + result.addSection(section, false, categories) + } + return result + } + + private fun MutableList.addSection( + section: ShelfSection, + isEnabled: Boolean, + favouriteCategories: List, + ) { + add(ShelfSettingsItemModel.Section(section, isEnabled)) + if (isEnabled && section == ShelfSection.FAVORITES) { + favouriteCategories.mapTo(this) { + ShelfSettingsItemModel.FavouriteCategory( + id = it.id, + title = it.title, + isChecked = it.isVisibleInLibrary, + ) + } + } + } + + private fun List.sections(): List { + return mapNotNull { (it as? ShelfSettingsItemModel.Section)?.takeIf { x -> x.isChecked }?.section } + } +} diff --git a/app/src/main/res/layout/activity_shelf_settings.xml b/app/src/main/res/layout/activity_shelf_settings.xml new file mode 100644 index 000000000..a4c9781a8 --- /dev/null +++ b/app/src/main/res/layout/activity_shelf_settings.xml @@ -0,0 +1,36 @@ + + + + + +