From f229a5e2ec66c45847db36e64efe73f52cc86b8e Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 19 Nov 2021 16:05:39 +0100 Subject: [PATCH 1/6] Tweak relative date function (#6249) * Tweak relative date function * Cleanup --- .../tachiyomi/util/lang/DateExtensions.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt index c39cdd9f0..f5c86a8fb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/DateExtensions.kt @@ -6,7 +6,6 @@ import java.text.DateFormat import java.util.Calendar import java.util.Date import java.util.TimeZone -import kotlin.math.floor fun Date.toDateTimestampString(dateFormatter: DateFormat): String { val date = dateFormatter.format(this) @@ -98,7 +97,7 @@ fun Long.toLocalCalendar(): Calendar? { } } -private const val MILLISECONDS_IN_DAY = 86_400_000.0 +private const val MILLISECONDS_IN_DAY = 86_400_000L fun Date.toRelativeString( context: Context, @@ -109,8 +108,8 @@ fun Date.toRelativeString( return dateFormat.format(this) } val now = Date() - val difference = now.time - this.time - val days = floor(difference / MILLISECONDS_IN_DAY).toInt() + val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) + val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt() return when { difference < 0 -> context.getString(R.string.recently) difference < MILLISECONDS_IN_DAY -> context.getString(R.string.relative_time_today) @@ -122,3 +121,15 @@ fun Date.toRelativeString( else -> dateFormat.format(this) } } + +private val Date.timeWithOffset: Long + get() { + return Calendar.getInstance().run { + time = this@timeWithOffset + this@timeWithOffset.time + timeZone.rawOffset + } + } + +fun Long.floorNearest(to: Long): Long { + return this.floorDiv(to) * to +} From bdef2cfdfb50800c35c028a7d41343a99bf95f60 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Fri, 19 Nov 2021 22:16:39 +0700 Subject: [PATCH 2/6] Replace Resume FAB reveal animation with container transform (#6250) --- app/build.gradle.kts | 2 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 8 ++ .../tachiyomi/ui/manga/MangaController.kt | 47 ++++-------- .../tachiyomi/ui/reader/ReaderActivity.kt | 25 +++++++ .../tachiyomi/widget/RevealAnimationView.kt | 73 ------------------- .../res/layout-sw720dp/manga_controller.xml | 8 -- app/src/main/res/layout/manga_controller.xml | 8 -- 7 files changed, 48 insertions(+), 123 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/RevealAnimationView.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2983289cf..8aefa99df 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -244,7 +244,7 @@ dependencies { implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") { exclude(group = "androidx.viewpager", module = "viewpager") } - implementation("dev.chrisbanes.insetter:insetter:0.6.0") + implementation("dev.chrisbanes.insetter:insetter:0.6.1") // Conductor val conductorVersion = "3.0.0" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 435fcf73c..5048fe413 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -8,6 +8,7 @@ import android.os.Build import android.os.Bundle import android.view.Gravity import android.view.ViewGroup +import android.view.Window import android.widget.Toast import androidx.appcompat.view.ActionMode import androidx.core.animation.doOnEnd @@ -28,6 +29,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.Router import com.google.android.material.appbar.AppBarLayout import com.google.android.material.navigation.NavigationBarView +import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback import dev.chrisbanes.insetter.applyInsetter import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.Migrations @@ -99,6 +101,11 @@ class MainActivity : BaseViewBindingActivity() { // Prevent splash screen showing up on configuration changes val splashScreen = if (savedInstanceState == null) installSplashScreen() else null + // Set up shared element transition and disable overlay so views don't show above system bars + window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) + setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback()) + window.sharedElementsUseOverlay = false + super.onCreate(savedInstanceState) val didMigration = if (savedInstanceState == null) Migrations.upgrade(preferences) else false @@ -117,6 +124,7 @@ class MainActivity : BaseViewBindingActivity() { // Draw edge-to-edge WindowCompat.setDecorFitsSystemWindows(window, false) binding.fabLayout.rootFab.applyInsetter { + ignoreVisibility(true) type(navigationBars = true) { margin() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index d1628f3cf..60fb4a9e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -1,8 +1,7 @@ package eu.kanade.tachiyomi.ui.manga -import android.animation.Animator -import android.animation.AnimatorListenerAdapter import android.app.Activity +import android.app.ActivityOptions import android.content.Context import android.content.Intent import android.graphics.Bitmap @@ -90,7 +89,6 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.getCoordinates import eu.kanade.tachiyomi.util.view.shrinkOnScroll import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView @@ -369,18 +367,7 @@ class MangaController : fab.setOnClickListener { val item = presenter.getNextUnreadChapter() if (item != null) { - // Get coordinates and start animation - actionFab?.getCoordinates()?.let { coordinates -> - binding.revealView.showRevealEffect( - coordinates.x, - coordinates.y, - object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator?) { - openChapter(item.chapter, true) - } - } - ) - } + openChapter(item.chapter, it) } } } @@ -413,20 +400,6 @@ class MangaController : super.onDestroyView(view) } - override fun onActivityResumed(activity: Activity) { - if (view == null) return - - // Check if animation view is visible - if (binding.revealView.isVisible) { - // Show the unreveal effect - actionFab?.getCoordinates()?.let { coordinates -> - binding.revealView.hideRevealEffect(coordinates.x, coordinates.y, 1920) - } - } - - super.onActivityResumed(activity) - } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.manga, menu) } @@ -914,13 +887,21 @@ class MangaController : } } - fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { + private fun openChapter(chapter: Chapter, sharedElement: View? = null) { val activity = activity ?: return val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter) - if (hasAnimation) { - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + activity.apply { + if (sharedElement != null) { + val activityOptions = ActivityOptions.makeSceneTransitionAnimation( + activity, + sharedElement, + ReaderActivity.SHARED_ELEMENT_NAME + ) + startActivity(intent, activityOptions.toBundle()) + } else { + startActivity(intent) + } } - startActivity(intent) } override fun onItemClick(view: View?, position: Int): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 45979cfdf..82ce65c28 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -22,7 +22,9 @@ import android.view.KeyEvent import android.view.Menu import android.view.MenuItem import android.view.MotionEvent +import android.view.View import android.view.View.LAYER_TYPE_HARDWARE +import android.view.Window import android.view.WindowManager import android.view.animation.Animation import android.view.animation.AnimationUtils @@ -39,6 +41,8 @@ import androidx.lifecycle.lifecycleScope import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.slider.Slider +import com.google.android.material.transition.platform.MaterialContainerTransform +import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback import dev.chrisbanes.insetter.applyInsetter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Chapter @@ -105,6 +109,8 @@ class ReaderActivity : BaseRxActivity() private const val ENABLED_BUTTON_IMAGE_ALPHA = 255 private const val DISABLED_BUTTON_IMAGE_ALPHA = 64 + + const val SHARED_ELEMENT_NAME = "reader_shared_element_root" } private val preferences: PreferencesHelper by injectLazy() @@ -150,6 +156,17 @@ class ReaderActivity : BaseRxActivity() */ override fun onCreate(savedInstanceState: Bundle?) { applyAppTheme(preferences) + + // Setup shared element transitions + window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) + findViewById(android.R.id.content).transitionName = SHARED_ELEMENT_NAME + setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback()) + window.sharedElementEnterTransition = buildContainerTransform(true) + window.sharedElementReturnTransition = buildContainerTransform(false) + + // Postpone custom transition until manga ready + postponeEnterTransition() + super.onCreate(savedInstanceState) binding = ReaderActivityBinding.inflate(layoutInflater) @@ -295,6 +312,12 @@ class ReaderActivity : BaseRxActivity() return handled || super.dispatchGenericMotionEvent(event) } + private fun buildContainerTransform(entering: Boolean): MaterialContainerTransform { + return MaterialContainerTransform(this, entering).apply { + addTarget(android.R.id.content) + } + } + /** * Initializes the reader menu. It sets up click listeners and the initial visibility. */ @@ -613,6 +636,8 @@ class ReaderActivity : BaseRxActivity() } } binding.readerContainer.addView(loadingIndicator) + + startPostponedEnterTransition() } private fun showReadingModeToast(mode: Int) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/RevealAnimationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/RevealAnimationView.kt deleted file mode 100644 index 6bd1546f1..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/RevealAnimationView.kt +++ /dev/null @@ -1,73 +0,0 @@ -package eu.kanade.tachiyomi.widget - -import android.animation.Animator -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.view.ViewAnimationUtils -import androidx.core.animation.doOnEnd -import androidx.core.view.isInvisible -import androidx.core.view.isVisible - -class RevealAnimationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - View(context, attrs) { - - /** - * Hides the animation view with a animation - * - * @param centerX x starting point - * @param centerY y starting point - * @param initialRadius size of radius of animation - */ - fun hideRevealEffect(centerX: Int, centerY: Int, initialRadius: Int) { - // Make the view visible. - this.isVisible = true - - // Create the animation (the final radius is zero). - val anim = ViewAnimationUtils.createCircularReveal( - this, - centerX, - centerY, - initialRadius.toFloat(), - 0f - ) - - // Set duration of animation. - anim.duration = 500 - - // make the view invisible when the animation is done - anim.doOnEnd { - this@RevealAnimationView.isInvisible = true - } - - anim.start() - } - - /** - * Fills the animation view with a animation - * - * @param centerX x starting point - * @param centerY y starting point - * @param listener animation listener - */ - fun showRevealEffect(centerX: Int, centerY: Int, listener: Animator.AnimatorListener) { - this.isVisible = true - - val height = this.height - - // Create animation - val anim = ViewAnimationUtils.createCircularReveal( - this, - centerX, - centerY, - 0f, - height.toFloat() - ) - - // Set duration of animation - anim.duration = 350 - - anim.addListener(listener) - anim.start() - } -} diff --git a/app/src/main/res/layout-sw720dp/manga_controller.xml b/app/src/main/res/layout-sw720dp/manga_controller.xml index 3a727142b..e4fa42b14 100644 --- a/app/src/main/res/layout-sw720dp/manga_controller.xml +++ b/app/src/main/res/layout-sw720dp/manga_controller.xml @@ -6,14 +6,6 @@ android:layout_height="match_parent" android:orientation="vertical"> - - - - Date: Fri, 19 Nov 2021 10:35:48 -0500 Subject: [PATCH 3/6] Tweak app theme preference selection (closes #5866) --- .../eu/kanade/tachiyomi/widget/preference/ThemesPreference.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ThemesPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ThemesPreference.kt index 1eb5dcd98..f51372fef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ThemesPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ThemesPreference.kt @@ -47,8 +47,8 @@ class ThemesPreference @JvmOverloads constructor(context: Context, attrs: Attrib } override fun onItemClick(position: Int) { - value = entries[position].name callChangeListener(value) + value = entries[position].name } override fun onClick() { From 98822a39d9524b08f5954f6bb58ac1d9c704b76d Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 19 Nov 2021 10:50:52 -0500 Subject: [PATCH 4/6] Option to clear chapter cache when MainActivity is closed (closes #5651) --- .../kanade/tachiyomi/data/preference/PreferenceKeys.kt | 2 ++ .../tachiyomi/data/preference/PreferencesHelper.kt | 2 ++ .../java/eu/kanade/tachiyomi/ui/main/MainActivity.kt | 9 ++++++++- .../tachiyomi/ui/setting/SettingsAdvancedController.kt | 5 +++++ app/src/main/res/values/strings.xml | 1 + 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index bb85ce045..7440d3b5c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -233,6 +233,8 @@ object PreferenceKeys { const val verboseLogging = "verbose_logging" + const val autoClearChapterCache = "auto_clear_chapter_cache" + fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index d304bb8ae..2d60d4b60 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -334,6 +334,8 @@ class PreferencesHelper(val context: Context) { fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, false) + fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false) + fun setChapterSettingsDefault(manga: Manga) { prefs.edit { putInt(Keys.defaultChapterFilterByRead, manga.readFilter) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 5048fe413..b972b1243 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -34,6 +34,7 @@ import dev.chrisbanes.insetter.applyInsetter import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.Migrations import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.preference.asImmediateFlow import eu.kanade.tachiyomi.data.updater.AppUpdateChecker @@ -72,6 +73,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import logcat.LogPriority +import uy.kohesive.injekt.injectLazy class MainActivity : BaseViewBindingActivity() { @@ -94,6 +96,8 @@ class MainActivity : BaseViewBindingActivity() { */ private val backstackLiftState = mutableMapOf() + private val chapterCache: ChapterCache by injectLazy() + // To be checked by splash screen. If true then splash screen will be removed. var ready = false @@ -464,7 +468,10 @@ class MainActivity : BaseViewBindingActivity() { // Exit confirmation (resets after 2 seconds) lifecycleScope.launchUI { resetExitConfirmation() } } else if (backstackSize == 1 || !router.handleBack()) { - // Regular back + // Regular back (i.e. closing the app) + if (preferences.autoClearChapterCache()) { + chapterCache.clear() + } super.onBackPressed() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 810a84801..4ab35e05e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -132,6 +132,11 @@ class SettingsAdvancedController : SettingsController() { onClick { clearChapterCache() } } + switchPreference { + key = Keys.autoClearChapterCache + titleRes = R.string.pref_auto_clear_chapter_cache + defaultValue = false + } preference { key = "pref_clear_database" titleRes = R.string.pref_clear_database diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb1febd2d..57dc9f0f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -458,6 +458,7 @@ Used: %1$s Cache cleared. %1$d files have been deleted An error occurred while clearing cache + Clear chapter cache on app close Clear database Delete history for manga that are not saved in your library Are you sure? Read chapters and progress of non-library manga will be lost From 9fe1a7e2ae14c4d7e70bfd85516d91c44514f04a Mon Sep 17 00:00:00 2001 From: Hunter Nickel Date: Fri, 19 Nov 2021 09:24:46 -0700 Subject: [PATCH 5/6] Add feature to clear database manga by source (#6241) * Implement feature to selectively clear manga from database based on it's source * Code cleanup and refactoring --- .../database/models/SourceIdMangaCount.kt | 3 + .../data/database/queries/MangaQueries.kt | 20 +- .../data/database/queries/RawQueries.kt | 12 ++ .../SourceIdMangaCountGetResolver.kt | 23 +++ .../ui/setting/SettingsAdvancedController.kt | 27 +-- .../database/ClearDatabaseController.kt | 172 ++++++++++++++++++ .../database/ClearDatabasePresenter.kt | 39 ++++ .../database/ClearDatabaseSourceItem.kt | 55 ++++++ .../res/layout/clear_database_controller.xml | 32 ++++ .../res/layout/clear_database_source_item.xml | 68 +++++++ app/src/main/res/values/strings.xml | 2 + 11 files changed, 426 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/database/models/SourceIdMangaCount.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/SourceIdMangaCountGetResolver.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseController.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseSourceItem.kt create mode 100644 app/src/main/res/layout/clear_database_controller.xml create mode 100644 app/src/main/res/layout/clear_database_source_item.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/SourceIdMangaCount.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/SourceIdMangaCount.kt new file mode 100644 index 000000000..bb91e1337 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/SourceIdMangaCount.kt @@ -0,0 +1,3 @@ +package eu.kanade.tachiyomi.data.database.models + +data class SourceIdMangaCount(val source: Long, val count: Int) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt index 667720aa0..e82305c07 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.data.database.queries +import com.pushtorefresh.storio.Queries import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects import com.pushtorefresh.storio.sqlite.queries.DeleteQuery import com.pushtorefresh.storio.sqlite.queries.Query @@ -7,6 +8,7 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver @@ -14,6 +16,7 @@ import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaNextUpdatedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver +import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver import eu.kanade.tachiyomi.data.database.tables.CategoryTable import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable @@ -70,6 +73,17 @@ interface MangaQueries : DbProvider { ) .prepare() + fun getSourceIdsWithNonLibraryManga() = db.get() + .listOfObjects(SourceIdMangaCount::class.java) + .withQuery( + RawQuery.builder() + .query(getSourceIdsWithNonLibraryMangaQuery()) + .observesTables(MangaTable.TABLE) + .build() + ) + .withGetResolver(SourceIdMangaCountGetResolver.INSTANCE) + .prepare() + fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() fun insertMangas(mangas: List) = db.put().objects(mangas).prepare() @@ -123,12 +137,12 @@ interface MangaQueries : DbProvider { fun deleteMangas(mangas: List) = db.delete().objects(mangas).prepare() - fun deleteMangasNotInLibrary() = db.delete() + fun deleteMangasNotInLibraryBySourceIds(sourceIds: List) = db.delete() .byQuery( DeleteQuery.builder() .table(MangaTable.TABLE) - .where("${MangaTable.COL_FAVORITE} = ?") - .whereArgs(0) + .where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)})") + .whereArgs(0, *sourceIds.toTypedArray()) .build() ) .prepare() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt index c79246e6b..57091cd07 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.data.database.queries +import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History @@ -142,3 +143,14 @@ fun getCategoriesForMangaQuery() = ${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID} WHERE ${MangaCategory.COL_MANGA_ID} = ? """ + +/** Query to get the list of sources in the database that have + * non-library manga, and how many + */ +fun getSourceIdsWithNonLibraryMangaQuery() = + """ + SELECT ${Manga.COL_SOURCE}, COUNT(*) as ${SourceIdMangaCountGetResolver.COL_COUNT} + FROM ${Manga.TABLE} + WHERE ${Manga.COL_FAVORITE} = 0 + GROUP BY ${Manga.COL_SOURCE} + """ diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/SourceIdMangaCountGetResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/SourceIdMangaCountGetResolver.kt new file mode 100644 index 000000000..ace4cc252 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/SourceIdMangaCountGetResolver.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.data.database.resolvers + +import android.annotation.SuppressLint +import android.database.Cursor +import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver +import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount +import eu.kanade.tachiyomi.data.database.tables.MangaTable + +class SourceIdMangaCountGetResolver : DefaultGetResolver() { + + companion object { + val INSTANCE = SourceIdMangaCountGetResolver() + const val COL_COUNT = "manga_count" + } + + @SuppressLint("Range") + override fun mapFromCursor(cursor: Cursor): SourceIdMangaCount { + val sourceID = cursor.getLong(cursor.getColumnIndex(MangaTable.COL_SOURCE)) + val count = cursor.getInt(cursor.getColumnIndex(COL_COUNT)) + + return SourceIdMangaCount(sourceID, count) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 4ab35e05e..858df6d16 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -1,10 +1,8 @@ package eu.kanade.tachiyomi.ui.setting import android.annotation.SuppressLint -import android.app.Dialog import android.content.ActivityNotFoundException import android.content.Intent -import android.os.Bundle import android.provider.Settings import androidx.core.net.toUri import androidx.preference.PreferenceScreen @@ -20,8 +18,9 @@ import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE -import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.openInBrowser +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.setting.database.ClearDatabaseController import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withUIContext @@ -143,9 +142,7 @@ class SettingsAdvancedController : SettingsController() { summaryRes = R.string.pref_clear_database_summary onClick { - val ctrl = ClearDatabaseDialogController() - ctrl.targetController = this@SettingsAdvancedController - ctrl.showDialog(router) + router.pushController(ClearDatabaseController().withFadeTransaction()) } } } @@ -278,24 +275,6 @@ class SettingsAdvancedController : SettingsController() { } } } - - class ClearDatabaseDialogController : DialogController() { - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialAlertDialogBuilder(activity!!) - .setMessage(R.string.clear_database_confirmation) - .setPositiveButton(android.R.string.ok) { _, _ -> - (targetController as? SettingsAdvancedController)?.clearDatabase() - } - .setNegativeButton(android.R.string.cancel, null) - .create() - } - } - - private fun clearDatabase() { - db.deleteMangasNotInLibrary().executeAsBlocking() - db.deleteHistoryNoLastRead().executeAsBlocking() - activity?.toast(R.string.clear_database_completed) - } } private const val CLEAR_CACHE_KEY = "pref_clear_cache_key" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseController.kt new file mode 100644 index 000000000..2b6601881 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseController.kt @@ -0,0 +1,172 @@ +package eu.kanade.tachiyomi.ui.setting.database + +import android.annotation.SuppressLint +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.core.view.forEach +import androidx.core.view.get +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import dev.chrisbanes.insetter.applyInsetter +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.Payload +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.databinding.ClearDatabaseControllerBinding +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.ui.base.controller.FabController +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.util.system.toast + +class ClearDatabaseController : + NucleusController(), + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnUpdateListener, + FabController { + + private var recycler: RecyclerView? = null + private var adapter: FlexibleAdapter? = null + + private var menu: Menu? = null + + private var actionFab: ExtendedFloatingActionButton? = null + private var actionFabScrollListener: RecyclerView.OnScrollListener? = null + + init { + setHasOptionsMenu(true) + } + + override fun createBinding(inflater: LayoutInflater): ClearDatabaseControllerBinding { + return ClearDatabaseControllerBinding.inflate(inflater) + } + + override fun createPresenter(): ClearDatabasePresenter { + return ClearDatabasePresenter() + } + + override fun getTitle(): String? { + return activity?.getString(R.string.pref_clear_database) + } + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + binding.recycler.applyInsetter { + type(navigationBars = true) { + padding() + } + } + + adapter = FlexibleAdapter(null, this, true) + binding.recycler.adapter = adapter + binding.recycler.layoutManager = LinearLayoutManager(activity) + binding.recycler.setHasFixedSize(true) + adapter?.fastScroller = binding.fastScroller + recycler = binding.recycler + } + + override fun onDestroyView(view: View) { + adapter = null + super.onDestroyView(view) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.generic_selection, menu) + this.menu = menu + menu.forEach { menuItem -> menuItem.isVisible = (adapter?.itemCount ?: 0) > 0 } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val adapter = adapter ?: return false + when (item.itemId) { + R.id.action_select_all -> adapter.selectAll() + R.id.action_select_inverse -> { + val currentSelection = adapter.selectedPositionsAsSet + val invertedSelection = (0..adapter.itemCount) + .filterNot { currentSelection.contains(it) } + currentSelection.clear() + currentSelection.addAll(invertedSelection) + } + } + updateFab() + adapter.notifyItemRangeChanged(0, adapter.itemCount, Payload.SELECTION) + return super.onOptionsItemSelected(item) + } + + override fun onUpdateEmptyView(size: Int) { + if (size > 0) { + binding.emptyView.hide() + } else { + binding.emptyView.show(activity!!.getString(R.string.database_clean)) + } + + menu?.forEach { menuItem -> menuItem.isVisible = size > 0 } + } + + override fun onItemClick(view: View?, position: Int): Boolean { + val adapter = adapter ?: return false + adapter.toggleSelection(position) + adapter.notifyItemChanged(position, Payload.SELECTION) + updateFab() + return true + } + + fun setItems(items: List) { + adapter?.updateDataSet(items) + } + + override fun configureFab(fab: ExtendedFloatingActionButton) { + fab.setIconResource(R.drawable.ic_delete_24dp) + fab.setText(R.string.action_delete) + fab.isVisible = false + fab.setOnClickListener { + val ctrl = ClearDatabaseSourcesDialog() + ctrl.targetController = this + ctrl.showDialog(router) + } + actionFab = fab + } + + private fun updateFab() { + val adapter = adapter ?: return + actionFab?.isVisible = adapter.selectedItemCount > 0 + } + + override fun cleanupFab(fab: ExtendedFloatingActionButton) { + actionFab?.setOnClickListener(null) + actionFabScrollListener?.let { recycler?.removeOnScrollListener(it) } + actionFab = null + } + + class ClearDatabaseSourcesDialog : DialogController() { + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(activity!!) + .setMessage(R.string.clear_database_confirmation) + .setPositiveButton(android.R.string.ok) { _, _ -> + (targetController as? ClearDatabaseController)?.clearDatabaseForSelectedSources() + } + .setNegativeButton(android.R.string.cancel, null) + .create() + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun clearDatabaseForSelectedSources() { + val adapter = adapter ?: return + val selectedSourceIds = adapter.selectedPositions.mapNotNull { position -> + adapter.getItem(position)?.source?.id + } + presenter.clearDatabaseForSourceIds(selectedSourceIds) + actionFab!!.isVisible = false + adapter.clearSelection() + adapter.notifyDataSetChanged() + activity?.toast(R.string.clear_database_completed) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt new file mode 100644 index 000000000..05dafd73c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt @@ -0,0 +1,39 @@ +package eu.kanade.tachiyomi.ui.setting.database + +import android.os.Bundle +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import rx.Observable +import rx.schedulers.Schedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class ClearDatabasePresenter : BasePresenter() { + + private val db = Injekt.get() + + private val sourceManager = Injekt.get() + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + getDatabaseSourcesObservable() + .subscribeOn(Schedulers.io()) + .subscribeLatestCache(ClearDatabaseController::setItems) + } + + fun clearDatabaseForSourceIds(sources: List) { + db.deleteMangasNotInLibraryBySourceIds(sources).executeAsBlocking() + db.deleteHistoryNoLastRead().executeAsBlocking() + } + + private fun getDatabaseSourcesObservable(): Observable> { + return db.getSourceIdsWithNonLibraryManga().asRxObservable() + .map { sourceCounts -> + sourceCounts.map { + val sourceObj = sourceManager.getOrStub(it.source) + ClearDatabaseSourceItem(sourceObj, it.count) + }.sortedBy { it.source.name } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseSourceItem.kt new file mode 100644 index 000000000..f599c8e80 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseSourceItem.kt @@ -0,0 +1,55 @@ +package eu.kanade.tachiyomi.ui.setting.database + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.databinding.ClearDatabaseSourceItemBinding +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.icon + +data class ClearDatabaseSourceItem(val source: Source, private val mangaCount: Int) : AbstractFlexibleItem() { + + override fun getLayoutRes(): Int { + return R.layout.clear_database_source_item + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { + return Holder(view, adapter) + } + + override fun bindViewHolder(adapter: FlexibleAdapter>?, holder: Holder?, position: Int, payloads: MutableList?) { + if (payloads.isNullOrEmpty()) { + holder?.bind(source, mangaCount) + } else { + holder?.updateCheckbox() + } + } + + class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { + + private val binding = ClearDatabaseSourceItemBinding.bind(view) + + fun bind(source: Source, count: Int) { + binding.title.text = source.toString() + binding.description.text = itemView.context.getString(R.string.clear_database_source_item_count, count) + + itemView.post { + when { + source.id == LocalSource.ID -> binding.thumbnail.setImageResource(R.mipmap.ic_local_source) + source is SourceManager.StubSource -> binding.thumbnail.setImageDrawable(null) + source.icon() != null -> binding.thumbnail.setImageDrawable(source.icon()) + } + } + } + + fun updateCheckbox() { + binding.checkbox.isChecked = (bindingAdapter as FlexibleAdapter<*>).isSelected(bindingAdapterPosition) + } + } +} diff --git a/app/src/main/res/layout/clear_database_controller.xml b/app/src/main/res/layout/clear_database_controller.xml new file mode 100644 index 000000000..70027af45 --- /dev/null +++ b/app/src/main/res/layout/clear_database_controller.xml @@ -0,0 +1,32 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/clear_database_source_item.xml b/app/src/main/res/layout/clear_database_source_item.xml new file mode 100644 index 000000000..823668a53 --- /dev/null +++ b/app/src/main/res/layout/clear_database_source_item.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57dc9f0f2..a6ff4ad9b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -461,8 +461,10 @@ Clear chapter cache on app close Clear database Delete history for manga that are not saved in your library + %1$d non-library manga in database Are you sure? Read chapters and progress of non-library manga will be lost Entries deleted + Database clean Refresh library manga covers Refresh tracking Updates status, score and last chapter read from the tracking services From bba7372556b7760350d507bb9484043aebfb2050 Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 19 Nov 2021 11:28:59 -0500 Subject: [PATCH 6/6] Add ability to clear cookies per-extension (closes #3153) --- .../details/ExtensionDetailsController.kt | 19 +++++++++++++++++++ .../ui/setting/SettingsAdvancedController.kt | 3 +-- app/src/main/res/menu/extension_details.xml | 5 +++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt index 8c7cfb2c8..89acafbb3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt @@ -25,10 +25,12 @@ import eu.kanade.tachiyomi.data.preference.minusAssign import eu.kanade.tachiyomi.data.preference.plusAssign import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.getPreferenceKey +import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.openInBrowser import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction @@ -38,8 +40,10 @@ import eu.kanade.tachiyomi.util.preference.preferenceCategory import eu.kanade.tachiyomi.util.preference.switchPreference import eu.kanade.tachiyomi.util.preference.switchSettingsPreference import eu.kanade.tachiyomi.util.system.LocaleHelper +import eu.kanade.tachiyomi.util.system.logcat import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import okhttp3.HttpUrl.Companion.toHttpUrl import uy.kohesive.injekt.injectLazy @SuppressLint("RestrictedApi") @@ -47,6 +51,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) : NucleusController(bundle) { private val preferences: PreferencesHelper by injectLazy() + private val network: NetworkHelper by injectLazy() private var preferenceScreen: PreferenceScreen? = null @@ -199,6 +204,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) : R.id.action_history -> openCommitHistory() R.id.action_enable_all -> toggleAllSources(true) R.id.action_disable_all -> toggleAllSources(false) + R.id.action_clear_cookies -> clearCookies() } return super.onOptionsItemSelected(item) } @@ -229,6 +235,19 @@ class ExtensionDetailsController(bundle: Bundle? = null) : openInBrowser(url) } + private fun clearCookies() { + val urls = presenter.extension?.sources + ?.filterIsInstance() + ?.map { it.baseUrl } + ?.distinct() ?: emptyList() + + urls.forEach { + network.cookieManager.remove(it.toHttpUrl()) + } + + logcat { "Cleared cookies for: ${urls.joinToString()}" } + } + private fun Source.isEnabled(): Boolean { return id.toString() !in preferences.disabledSources().get() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 858df6d16..b40608c60 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -43,10 +43,9 @@ import uy.kohesive.injekt.injectLazy import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys class SettingsAdvancedController : SettingsController() { + private val network: NetworkHelper by injectLazy() - private val chapterCache: ChapterCache by injectLazy() - private val db: DatabaseHelper by injectLazy() @SuppressLint("BatteryLife") diff --git a/app/src/main/res/menu/extension_details.xml b/app/src/main/res/menu/extension_details.xml index 81978095f..d31bb377f 100644 --- a/app/src/main/res/menu/extension_details.xml +++ b/app/src/main/res/menu/extension_details.xml @@ -18,4 +18,9 @@ android:title="@string/action_disable_all" app:showAsAction="never" /> + +