From f18c18230b25632935439233a89327dfd6ff202f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 18 Jul 2022 18:29:29 +0300 Subject: [PATCH 1/4] Update favourites categories empty state image --- .../kotatsu/favourites/ui/FavouritesContainerFragment.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt index 22939d9d9..bd12423c0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt @@ -148,7 +148,12 @@ class FavouritesContainerFragment : menu.setOnMenuItemClickListener { when (it.itemId) { R.id.action_remove -> editDelegate.deleteCategory(category) - R.id.action_edit -> startActivity(FavouritesCategoryEditActivity.newIntent(tabView.context, category.id)) + R.id.action_edit -> startActivity( + FavouritesCategoryEditActivity.newIntent( + tabView.context, + category.id + ) + ) else -> return@setOnMenuItemClickListener false } true @@ -172,7 +177,7 @@ class FavouritesContainerFragment : private fun showStub() { val stub = stubBinding ?: ItemEmptyStateBinding.bind(binding.stubEmptyState.inflate()) stub.root.isVisible = true - stub.icon.setImageResource(R.drawable.ic_heart_outline) + stub.icon.setImageResource(R.drawable.ic_empty_favourites) stub.textPrimary.setText(R.string.text_empty_holder_primary) stub.textSecondary.setText(R.string.empty_favourite_categories) stub.buttonRetry.setText(R.string.add) From b5bb8efe0a43c1002fa023a3efda697f48cc9bf5 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 19 Jul 2022 10:01:23 +0300 Subject: [PATCH 2/4] Improve database tests --- .../kotatsu/core/db/MangaDatabaseTest.kt | 40 ++++++++++--------- .../koitharu/kotatsu/core/db/MangaDatabase.kt | 40 ++++++++++--------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt b/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt index 63b302b47..23c8b9796 100644 --- a/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt +++ b/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt @@ -6,8 +6,6 @@ import androidx.test.platform.app.InstrumentationRegistry import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.koitharu.kotatsu.core.db.migrations.* -import java.io.IOException import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) @@ -19,10 +17,20 @@ class MangaDatabaseTest { MangaDatabase::class.java, ) + private val migrations = databaseMigrations + @Test - @Throws(IOException::class) - fun migrateAll() { + fun versions() { + assertEquals(1, migrations.first().startVersion) + repeat(migrations.size) { i -> + assertEquals(i + 1, migrations[i].startVersion) + assertEquals(i + 2, migrations[i].endVersion) + } assertEquals(DATABASE_VERSION, migrations.last().endVersion) + } + + @Test + fun migrateAll() { helper.createDatabase(TEST_DB, 1).close() for (migration in migrations) { helper.runMigrationsAndValidate( @@ -34,22 +42,16 @@ class MangaDatabaseTest { } } + @Test + fun prePopulate() { + val resources = InstrumentationRegistry.getInstrumentation().targetContext.resources + helper.createDatabase(TEST_DB, DATABASE_VERSION).use { + DatabasePrePopulateCallback(resources).onCreate(it) + } + } + private companion object { const val TEST_DB = "test-db" - - val migrations = arrayOf( - Migration1To2(), - Migration2To3(), - Migration3To4(), - Migration4To5(), - Migration5To6(), - Migration6To7(), - Migration7To8(), - Migration8To9(), - Migration9To10(), - Migration10To11(), - Migration11To12(), - ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt index a648911bf..d3dff3cc6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.migration.Migration import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity import org.koitharu.kotatsu.bookmarks.data.BookmarksDao import org.koitharu.kotatsu.core.db.dao.MangaDao @@ -65,22 +66,23 @@ abstract class MangaDatabase : RoomDatabase() { abstract val scrobblingDao: ScrobblingDao } -fun MangaDatabase(context: Context): MangaDatabase = Room.databaseBuilder( - context, - MangaDatabase::class.java, - "kotatsu-db" -).addMigrations( - Migration1To2(), - Migration2To3(), - Migration3To4(), - Migration4To5(), - Migration5To6(), - Migration6To7(), - Migration7To8(), - Migration8To9(), - Migration9To10(), - Migration10To11(), - Migration11To12(), -).addCallback( - DatabasePrePopulateCallback(context.resources) -).build() \ No newline at end of file +val databaseMigrations: Array + get() = arrayOf( + Migration1To2(), + Migration2To3(), + Migration3To4(), + Migration4To5(), + Migration5To6(), + Migration6To7(), + Migration7To8(), + Migration8To9(), + Migration9To10(), + Migration10To11(), + Migration11To12(), + ) + +fun MangaDatabase(context: Context): MangaDatabase = Room + .databaseBuilder(context, MangaDatabase::class.java, "kotatsu-db") + .addMigrations(*databaseMigrations) + .addCallback(DatabasePrePopulateCallback(context.resources)) + .build() \ No newline at end of file From 18d45aa1a3a7b543481a1f1f804d66369691351d Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 19 Jul 2022 10:29:39 +0300 Subject: [PATCH 3/4] Chapters range selection --- .../kotatsu/details/ui/ChaptersFragment.kt | 36 +++++++++++++++++-- app/src/main/res/drawable/ic_select_range.xml | 15 ++++++++ app/src/main/res/menu/mode_chapters.xml | 6 ++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/drawable/ic_select_range.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt index 04e79d18d..9f1ad93e9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt @@ -135,6 +135,26 @@ class ChaptersFragment : mode.finish() true } + R.id.action_select_range -> { + val controller = selectionController ?: return false + val items = chaptersAdapter?.items ?: return false + val ids = HashSet(controller.peekCheckedIds()) + val buffer = HashSet() + var isAdding = false + for (x in items) { + if (x.chapter.id in ids) { + isAdding = true + if (buffer.isNotEmpty()) { + ids.addAll(buffer) + buffer.clear() + } + } else if (isAdding) { + buffer.add(x.chapter.id) + } + } + controller.addAll(ids) + true + } R.id.action_select_all -> { val ids = chaptersAdapter?.items?.map { it.chapter.id } ?: return false selectionController?.addAll(ids) @@ -158,14 +178,24 @@ class ChaptersFragment : override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { val selectedIds = selectionController?.peekCheckedIds() ?: return false - val items = chaptersAdapter?.items?.filter { x -> x.chapter.id in selectedIds }.orEmpty() - menu.findItem(R.id.action_save).isVisible = items.none { x -> + val allItems = chaptersAdapter?.items.orEmpty() + val items = allItems.withIndex().filter { (_, x) -> x.chapter.id in selectedIds } + menu.findItem(R.id.action_save).isVisible = items.none { (_, x) -> x.chapter.source == MangaSource.LOCAL } - menu.findItem(R.id.action_delete).isVisible = items.all { x -> + menu.findItem(R.id.action_delete).isVisible = items.all { (_, x) -> x.chapter.source == MangaSource.LOCAL } + menu.findItem(R.id.action_select_all).isVisible = items.size < allItems.size mode.title = items.size.toString() + var hasGap = false + for (i in 0 until items.size - 1) { + if (items[i].index + 1 != items[i + 1].index) { + hasGap = true + break + } + } + menu.findItem(R.id.action_select_range).isVisible = hasGap return true } diff --git a/app/src/main/res/drawable/ic_select_range.xml b/app/src/main/res/drawable/ic_select_range.xml new file mode 100644 index 000000000..617fdc0cb --- /dev/null +++ b/app/src/main/res/drawable/ic_select_range.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/menu/mode_chapters.xml b/app/src/main/res/menu/mode_chapters.xml index 8e3d52bab..6b5169c05 100644 --- a/app/src/main/res/menu/mode_chapters.xml +++ b/app/src/main/res/menu/mode_chapters.xml @@ -15,6 +15,12 @@ android:title="@string/delete" app:showAsAction="ifRoom|withText" /> + + Can help in case of some issues. All authorizations will be invalidated Show all Invalid domain + Select range \ No newline at end of file From bdcc3bb1f55c47a5d63b53509be7ad4f731ff14c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 19 Jul 2022 12:07:21 +0300 Subject: [PATCH 4/4] Update parsers and adjust ParseException reporting --- app/build.gradle | 8 +++--- .../core/exceptions/CaughtException.kt | 3 +++ .../details/ui/MangaDetailsDelegate.kt | 4 --- .../kotatsu/reader/ui/ReaderActivity.kt | 8 +++--- .../kotatsu/reader/ui/ReaderViewModel.kt | 9 +++---- .../kotatsu/utils/ext/ThrowableExt.kt | 25 ++++++++----------- 6 files changed, 24 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/exceptions/CaughtException.kt diff --git a/app/build.gradle b/app/build.gradle index 7c904e60a..b1a41250d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 416 - versionName '3.4.4' + versionCode 417 + versionName '3.4.5' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -79,7 +79,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.nv95:kotatsu-parsers:6af8cec134') { + implementation('com.github.nv95:kotatsu-parsers:30071709af') { exclude group: 'org.json', module: 'json' } @@ -99,7 +99,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-alpha04' - implementation 'com.google.android.material:material:1.7.0-alpha02' + implementation 'com.google.android.material:material:1.7.0-alpha03' //noinspection LifecycleAnnotationProcessorWithJava8 kapt 'androidx.lifecycle:lifecycle-compiler:2.5.0' diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CaughtException.kt b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CaughtException.kt new file mode 100644 index 000000000..907917537 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/CaughtException.kt @@ -0,0 +1,3 @@ +package org.koitharu.kotatsu.core.exceptions + +class CaughtException(cause: Throwable, override val message: String?) : RuntimeException(cause) \ No newline at end of file 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 c6c45ecc1..39ad3003b 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 @@ -3,7 +3,6 @@ package org.koitharu.kotatsu.details.ui import androidx.core.os.LocaleListCompat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import org.acra.ACRA import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException @@ -14,7 +13,6 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.toListItem import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository -import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource @@ -22,7 +20,6 @@ import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.utils.ext.iterator import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import org.koitharu.kotatsu.utils.ext.setCurrentManga class MangaDetailsDelegate( private val intent: MangaIntent, @@ -45,7 +42,6 @@ class MangaDetailsDelegate( suspend fun doLoad() { var manga = mangaDataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga") - ACRA.setCurrentManga(manga) mangaData.value = manga manga = MangaRepository(manga.source).getDetails(manga) // find default branch 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 a9f563d1f..e761ce1c6 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 @@ -346,14 +346,14 @@ class ReaderActivity : menuItem.setIcon(if (isAdded) R.drawable.ic_bookmark_added else R.drawable.ic_bookmark) } - private fun onUiStateChanged(uiState: ReaderUiState, previous: ReaderUiState?) { - title = uiState.chapterName ?: uiState.mangaName ?: getString(R.string.loading_) - supportActionBar?.subtitle = if (uiState.chapterNumber in 1..uiState.chaptersTotal) { + private fun onUiStateChanged(uiState: ReaderUiState?, previous: ReaderUiState?) { + title = uiState?.chapterName ?: uiState?.mangaName ?: getString(R.string.loading_) + supportActionBar?.subtitle = if (uiState != null && uiState.chapterNumber in 1..uiState.chaptersTotal) { getString(R.string.chapter_d_of_d, uiState.chapterNumber, uiState.chaptersTotal) } else { null } - if (previous?.chapterName != null && uiState.chapterName != previous.chapterName) { + if (uiState != null && previous?.chapterName != null && uiState.chapterName != previous.chapterName) { if (!uiState.chapterName.isNullOrEmpty()) { binding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION) } 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 ea93e7267..6d8b1f0f6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -8,7 +8,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import org.acra.ACRA import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaIntent @@ -32,7 +31,6 @@ 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 org.koitharu.kotatsu.utils.ext.setCurrentManga import java.util.* private const val BOUNDS_PAGE_OFFSET = 2 @@ -73,7 +71,7 @@ class ReaderViewModel( chapterNumber = chapter?.number ?: 0, chaptersTotal = chapters.size() ) - }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, null) val content = MutableLiveData(ReaderContent(emptyList(), null)) val manga: Manga? @@ -91,7 +89,7 @@ class ReaderViewModel( ) { manga, policy -> policy == ScreenshotsPolicy.BLOCK_ALL || (policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw) - }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false) val onZoomChanged = SingleLiveEvent() @@ -103,7 +101,7 @@ class ReaderViewModel( bookmarksRepository.observeBookmark(manga, state.chapterId, state.page) .map { it != null } } - }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) + }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false) init { loadImpl() @@ -262,7 +260,6 @@ class ReaderViewModel( private fun loadImpl() { loadingJob = launchLoadingJob(Dispatchers.Default) { var manga = dataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga") - ACRA.setCurrentManga(manga) mangaData.value = manga val repo = MangaRepository(manga.source) manga = repo.getDetails(manga) 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 7b7f76408..79ee12775 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 @@ -3,19 +3,15 @@ package org.koitharu.kotatsu.utils.ext import android.content.ActivityNotFoundException import android.content.res.Resources import okio.FileNotFoundException -import org.acra.ACRA import org.acra.ktx.sendWithAcra import org.koitharu.kotatsu.R -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.core.exceptions.* import org.koitharu.kotatsu.parsers.exception.AuthRequiredException +import org.koitharu.kotatsu.parsers.exception.ContentUnavailableException import org.koitharu.kotatsu.parsers.exception.ParseException -import org.koitharu.kotatsu.parsers.model.Manga import java.net.SocketTimeoutException -fun Throwable.getDisplayMessage(resources: Resources) = when (this) { +fun Throwable.getDisplayMessage(resources: Resources): String = when (this) { is AuthRequiredException -> resources.getString(R.string.auth_required) is CloudFlareProtectedException -> resources.getString(R.string.captcha_required) is ActivityNotFoundException, @@ -23,22 +19,21 @@ fun Throwable.getDisplayMessage(resources: Resources) = when (this) { 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 WrongPasswordException -> resources.getString(R.string.wrong_password) - else -> localizedMessage ?: resources.getString(R.string.error_occurred) -} + else -> localizedMessage +} ?: resources.getString(R.string.error_occurred) fun Throwable.isReportable(): Boolean { if (this !is Exception) { return true } - return this is ParseException || this is IllegalArgumentException || this is IllegalStateException + return this is ParseException || this is IllegalArgumentException || + this is IllegalStateException || this is RuntimeException } fun Throwable.report(message: String?) { CaughtException(this, message).sendWithAcra() -} - -fun ACRA.setCurrentManga(manga: Manga?) = errorReporter.putCustomData("manga", manga?.publicUrl.toString()) - -private class CaughtException(cause: Throwable, override val message: String?) : RuntimeException(cause) \ No newline at end of file +} \ No newline at end of file