mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-25 22:29:45 +03:00
Merge remote-tracking branch 'upstream/master'
need to see how the open animation of the player performs also still need to add anime sources to the cache clearing controller thingy
This commit is contained in:
commit
b952fd44ec
30 changed files with 668 additions and 215 deletions
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
data class SourceIdAnimeCount(val source: Long, val count: Int)
|
|
@ -0,0 +1,3 @@
|
|||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
data class SourceIdMangaCount(val source: Long, val count: Int)
|
|
@ -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.Anime
|
||||
import eu.kanade.tachiyomi.data.database.models.AnimelibAnime
|
||||
import eu.kanade.tachiyomi.data.database.models.SourceIdAnimeCount
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.AnimeCoverLastModifiedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.AnimeFavoritePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.AnimeFlagsPutResolver
|
||||
|
@ -14,6 +16,7 @@ import eu.kanade.tachiyomi.data.database.resolvers.AnimeLastUpdatedPutResolver
|
|||
import eu.kanade.tachiyomi.data.database.resolvers.AnimeNextUpdatedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.AnimeTitlePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.AnimelibAnimeGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdAnimeCountGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.AnimeCategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.AnimeTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||
|
@ -70,6 +73,17 @@ interface AnimeQueries : DbProvider {
|
|||
)
|
||||
.prepare()
|
||||
|
||||
fun getSourceIdsWithNonLibraryAnime() = db.get()
|
||||
.listOfObjects(SourceIdAnimeCount::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getSourceIdsWithNonLibraryAnimeQuery())
|
||||
.observesTables(AnimeTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.withGetResolver(SourceIdAnimeCountGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
fun insertAnime(anime: Anime) = db.put().`object`(anime).prepare()
|
||||
|
||||
fun insertAnimes(animes: List<Anime>) = db.put().objects(animes).prepare()
|
||||
|
@ -123,12 +137,12 @@ interface AnimeQueries : DbProvider {
|
|||
|
||||
fun deleteAnimes(animes: List<Anime>) = db.delete().objects(animes).prepare()
|
||||
|
||||
fun deleteAnimesNotInAnimelib() = db.delete()
|
||||
fun deleteAnimesNotInLibraryBySourceIds(sourceIds: List<Long>) = db.delete()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(AnimeTable.TABLE)
|
||||
.where("${AnimeTable.COL_FAVORITE} = ?")
|
||||
.whereArgs(0)
|
||||
.where("${AnimeTable.COL_FAVORITE} = ? AND ${AnimeTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)})")
|
||||
.whereArgs(0, *sourceIds.toTypedArray())
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
|
|
@ -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<Manga>) = db.put().objects(mangas).prepare()
|
||||
|
@ -123,12 +137,12 @@ interface MangaQueries : DbProvider {
|
|||
|
||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||
|
||||
fun deleteMangasNotInLibrary() = db.delete()
|
||||
fun deleteMangasNotInLibraryBySourceIds(sourceIds: List<Long>) = 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()
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdAnimeCountGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.AnimeCategoryTable as AnimeCategory
|
||||
import eu.kanade.tachiyomi.data.database.tables.AnimeHistoryTable as AnimeHistory
|
||||
import eu.kanade.tachiyomi.data.database.tables.AnimeTable as Anime
|
||||
|
@ -274,3 +276,25 @@ fun getCategoriesForAnimeQuery() =
|
|||
${AnimeCategory.TABLE}.${AnimeCategory.COL_CATEGORY_ID}
|
||||
WHERE ${AnimeCategory.COL_ANIME_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}
|
||||
"""
|
||||
|
||||
/** Query to get the list of sources in the database that have
|
||||
* non-library manga, and how many
|
||||
*/
|
||||
fun getSourceIdsWithNonLibraryAnimeQuery() =
|
||||
"""
|
||||
SELECT ${Anime.COL_SOURCE}, COUNT(*) as ${SourceIdAnimeCountGetResolver.COL_COUNT}
|
||||
FROM ${Anime.TABLE}
|
||||
WHERE ${Anime.COL_FAVORITE} = 0
|
||||
GROUP BY ${Anime.COL_SOURCE}
|
||||
"""
|
||||
|
|
|
@ -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.SourceIdAnimeCount
|
||||
import eu.kanade.tachiyomi.data.database.tables.AnimeTable
|
||||
|
||||
class SourceIdAnimeCountGetResolver : DefaultGetResolver<SourceIdAnimeCount>() {
|
||||
|
||||
companion object {
|
||||
val INSTANCE = SourceIdAnimeCountGetResolver()
|
||||
const val COL_COUNT = "anime_count"
|
||||
}
|
||||
|
||||
@SuppressLint("Range")
|
||||
override fun mapFromCursor(cursor: Cursor): SourceIdAnimeCount {
|
||||
val sourceID = cursor.getLong(cursor.getColumnIndex(AnimeTable.COL_SOURCE))
|
||||
val count = cursor.getInt(cursor.getColumnIndex(COL_COUNT))
|
||||
|
||||
return SourceIdAnimeCount(sourceID, count)
|
||||
}
|
||||
}
|
|
@ -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<SourceIdMangaCount>() {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -285,6 +285,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"
|
||||
|
|
|
@ -409,6 +409,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)
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
package eu.kanade.tachiyomi.ui.anime
|
||||
|
||||
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
|
||||
|
@ -102,7 +101,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
|
||||
|
@ -392,18 +390,7 @@ class AnimeController :
|
|||
fab.setOnClickListener {
|
||||
val item = presenter.getNextUnseenEpisode()
|
||||
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?) {
|
||||
openEpisode(item.episode)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
openEpisode(item.episode, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -436,20 +423,6 @@ class AnimeController :
|
|||
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.anime, menu)
|
||||
}
|
||||
|
@ -1030,29 +1003,40 @@ class AnimeController :
|
|||
}
|
||||
}
|
||||
|
||||
fun openEpisode(episode: Episode, playerChangeRequested: Boolean = false) {
|
||||
private fun openEpisode(episode: Episode, sharedElement: View? = null, playerChangeRequested: Boolean = false) {
|
||||
val context = view?.context ?: return
|
||||
launchIO {
|
||||
val intent = PlayerActivity.newIntent(context, presenter.anime, episode)
|
||||
val useInternal = preferences.alwaysUseExternalPlayer() == playerChangeRequested
|
||||
if (useInternal) {
|
||||
startActivity(intent)
|
||||
val activity = activity ?: return
|
||||
val intent = PlayerActivity.newIntent(context, presenter.anime, episode)
|
||||
val useInternal = preferences.alwaysUseExternalPlayer() == playerChangeRequested
|
||||
|
||||
if (!useInternal) launchIO {
|
||||
val video = EpisodeLoader.getLink(episode, anime!!, source!!).awaitSingle()
|
||||
if (video != null) {
|
||||
val videoUri = video.uri
|
||||
val videoUrl = Uri.parse(video.videoUrl)
|
||||
currentExtEpisode = episode
|
||||
val pkgName = preferences.externalPlayerPreference()
|
||||
|
||||
val uri = if (videoUri != null && Build.VERSION.SDK_INT >= 24 && videoUri.scheme == "file") {
|
||||
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", videoUri.toFile())
|
||||
} else videoUri ?: videoUrl
|
||||
|
||||
val extIntent = getExternalIntent(pkgName, uri, episode, video, context)
|
||||
startActivityForResult(extIntent, REQUEST_EXTERNAL)
|
||||
} else {
|
||||
val video = EpisodeLoader.getLink(episode, anime!!, source!!).awaitSingle()
|
||||
if (video != null) {
|
||||
val videoUri = video.uri
|
||||
val videoUrl = Uri.parse(video.videoUrl)
|
||||
currentExtEpisode = episode
|
||||
val pkgName = preferences.externalPlayerPreference()
|
||||
|
||||
val uri = if (videoUri != null && Build.VERSION.SDK_INT >= 24 && videoUri.scheme == "file") {
|
||||
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", videoUri.toFile())
|
||||
} else videoUri ?: videoUrl
|
||||
|
||||
val extIntent = getExternalIntent(pkgName, uri, episode, video, context)
|
||||
startActivityForResult(extIntent, REQUEST_EXTERNAL)
|
||||
context.toast("Cannot open episode")
|
||||
}
|
||||
} else {
|
||||
activity.apply {
|
||||
if (sharedElement != null) {
|
||||
val activityOptions = ActivityOptions.makeSceneTransitionAnimation(
|
||||
activity,
|
||||
sharedElement,
|
||||
PlayerActivity.SHARED_ELEMENT_NAME
|
||||
)
|
||||
startActivity(intent, activityOptions.toBundle())
|
||||
} else {
|
||||
context.toast("Cannot open episode")
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1233,8 +1217,8 @@ class AnimeController :
|
|||
R.id.action_mark_as_read -> markAsRead(getSelectedEpisodes())
|
||||
R.id.action_mark_as_unread -> markAsUnread(getSelectedEpisodes())
|
||||
R.id.action_mark_previous_as_read -> markPreviousAsRead(getSelectedEpisodes())
|
||||
R.id.action_play_internally -> openEpisode(getSelectedEpisodes().last().episode, true)
|
||||
R.id.action_play_externally -> openEpisode(getSelectedEpisodes().last().episode, true)
|
||||
R.id.action_play_internally -> openEpisode(getSelectedEpisodes().last().episode, playerChangeRequested = true)
|
||||
R.id.action_play_externally -> openEpisode(getSelectedEpisodes().last().episode, playerChangeRequested = true)
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
|
|
|
@ -23,12 +23,14 @@ import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
|||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.getPreferenceKey
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
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.AnimeExtension
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
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 AnimeExtensionDetailsController(bundle: Bundle? = null) :
|
|||
NucleusController<ExtensionDetailControllerBinding, AnimeExtensionDetailsPresenter>(bundle) {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
private val network: NetworkHelper by injectLazy()
|
||||
|
||||
private var preferenceScreen: PreferenceScreen? = null
|
||||
|
||||
|
@ -199,6 +204,7 @@ class AnimeExtensionDetailsController(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 AnimeExtensionDetailsController(bundle: Bundle? = null) :
|
|||
openInBrowser(url)
|
||||
}
|
||||
|
||||
private fun clearCookies() {
|
||||
val urls = presenter.extension?.sources
|
||||
?.filterIsInstance<AnimeHttpSource>()
|
||||
?.map { it.baseUrl }
|
||||
?.distinct() ?: emptyList()
|
||||
|
||||
urls.forEach {
|
||||
network.cookieManager.remove(it.toHttpUrl())
|
||||
}
|
||||
|
||||
logcat { "Cleared cookies for: ${urls.joinToString()}" }
|
||||
}
|
||||
|
||||
private fun AnimeSource.isEnabled(): Boolean {
|
||||
return id.toString() !in preferences.disabledAnimeSources().get()
|
||||
}
|
||||
|
|
|
@ -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<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(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<HttpSource>()
|
||||
?.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()
|
||||
}
|
||||
|
|
|
@ -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,10 +29,13 @@ 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
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.cache.EpisodeCache
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
|
||||
|
@ -74,6 +78,7 @@ import kotlinx.coroutines.flow.launchIn
|
|||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import logcat.LogPriority
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import eu.kanade.tachiyomi.ui.download.anime.DownloadController as AnimeDownloadController
|
||||
import eu.kanade.tachiyomi.ui.download.manga.DownloadController as MangaDownloadController
|
||||
|
||||
|
@ -99,6 +104,9 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
|||
*/
|
||||
private val backstackLiftState = mutableMapOf<String, Boolean>()
|
||||
|
||||
private val chapterCache: ChapterCache by injectLazy()
|
||||
private val episodeCache: EpisodeCache by injectLazy()
|
||||
|
||||
// To be checked by splash screen. If true then splash screen will be removed.
|
||||
var ready = false
|
||||
|
||||
|
@ -106,6 +114,11 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
|||
// 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
|
||||
|
@ -124,6 +137,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
|||
// Draw edge-to-edge
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
binding.fabLayout.rootFab.applyInsetter {
|
||||
ignoreVisibility(true)
|
||||
type(navigationBars = true) {
|
||||
margin()
|
||||
}
|
||||
|
@ -493,7 +507,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
|||
|
||||
// Binding sometimes isn't actually instantiated yet somehow
|
||||
nav?.setOnItemSelectedListener(null)
|
||||
binding?.toolbar.setNavigationOnClickListener(null)
|
||||
binding?.toolbar?.setNavigationOnClickListener(null)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
|
@ -505,7 +519,11 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
|||
// 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()
|
||||
episodeCache.clear()
|
||||
}
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -92,7 +91,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
|
||||
|
@ -371,18 +369,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -415,20 +402,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)
|
||||
}
|
||||
|
@ -918,13 +891,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 {
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Intent
|
|||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.webkit.WebSettings
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
|
@ -42,6 +43,8 @@ import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvicto
|
|||
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||
import com.google.android.exoplayer2.util.Clock
|
||||
import com.google.android.exoplayer2.util.MimeTypes
|
||||
import com.google.android.material.transition.platform.MaterialContainerTransform
|
||||
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
|
||||
|
@ -128,6 +131,16 @@ class PlayerActivity : AppCompatActivity() {
|
|||
.build()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Setup shared element transitions
|
||||
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
|
||||
findViewById<View>(android.R.id.content).transitionName = SHARED_ELEMENT_NAME
|
||||
setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback())
|
||||
window.sharedElementEnterTransition = buildContainerTransform(true)
|
||||
window.sharedElementReturnTransition = buildContainerTransform(false)
|
||||
|
||||
// Postpone custom transition until anime ready
|
||||
postponeEnterTransition()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.watcher_activity)
|
||||
window.decorView.setOnSystemUiVisibilityChangeListener {
|
||||
|
@ -169,6 +182,14 @@ class PlayerActivity : AppCompatActivity() {
|
|||
isFullscreen = savedInstanceState.getBoolean(STATE_PLAYER_FULLSCREEN)
|
||||
isPlayerPlaying = savedInstanceState.getBoolean(STATE_PLAYER_PLAYING)
|
||||
}
|
||||
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
|
||||
private fun buildContainerTransform(entering: Boolean): MaterialContainerTransform {
|
||||
return MaterialContainerTransform(this, entering).apply {
|
||||
addTarget(android.R.id.content)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initDummyPlayer() {
|
||||
|
@ -742,5 +763,6 @@ class PlayerActivity : AppCompatActivity() {
|
|||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
}
|
||||
}
|
||||
const val SHARED_ELEMENT_NAME = "reader_shared_element_root"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ReaderActivityBinding, ReaderPresenter>()
|
|||
|
||||
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<ReaderActivityBinding, ReaderPresenter>()
|
|||
*/
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
applyAppTheme(preferences)
|
||||
|
||||
// Setup shared element transitions
|
||||
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
|
||||
findViewById<View>(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<ReaderActivityBinding, ReaderPresenter>()
|
|||
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<ReaderActivityBinding, ReaderPresenter>()
|
|||
}
|
||||
}
|
||||
binding.readerContainer.addView(loadingIndicator)
|
||||
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
|
||||
private fun showReadingModeToast(mode: Int) {
|
||||
|
|
|
@ -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
|
||||
|
@ -23,8 +21,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
|
||||
|
@ -47,11 +46,10 @@ 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 network: NetworkHelper by injectLazy()
|
||||
private val chapterCache: ChapterCache by injectLazy()
|
||||
private val episodeCache: EpisodeCache by injectLazy()
|
||||
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
private val animedb: AnimeDatabaseHelper by injectLazy()
|
||||
|
||||
|
@ -137,15 +135,18 @@ class SettingsAdvancedController : SettingsController() {
|
|||
|
||||
onClick { clearChapterAndEpisodeCache() }
|
||||
}
|
||||
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
|
||||
summaryRes = R.string.pref_clear_database_summary
|
||||
|
||||
onClick {
|
||||
val ctrl = ClearDatabaseDialogController()
|
||||
ctrl.targetController = this@SettingsAdvancedController
|
||||
ctrl.showDialog(router)
|
||||
router.pushController(ClearDatabaseController().withFadeTransaction())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -284,26 +285,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()
|
||||
animedb.deleteAnimesNotInAnimelib().executeAsBlocking()
|
||||
db.deleteHistoryNoLastRead().executeAsBlocking()
|
||||
animedb.deleteHistoryNoLastSeen().executeAsBlocking()
|
||||
activity?.toast(R.string.clear_database_completed)
|
||||
}
|
||||
}
|
||||
|
||||
private const val CLEAR_CACHE_KEY = "pref_clear_cache_key"
|
||||
|
|
|
@ -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<ClearDatabaseControllerBinding, ClearDatabasePresenter>(),
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnUpdateListener,
|
||||
FabController {
|
||||
|
||||
private var recycler: RecyclerView? = null
|
||||
private var adapter: FlexibleAdapter<ClearDatabaseSourceItem>? = 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<ClearDatabaseSourceItem>(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<ClearDatabaseSourceItem>) {
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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<ClearDatabaseController>() {
|
||||
|
||||
private val db = Injekt.get<DatabaseHelper>()
|
||||
|
||||
private val sourceManager = Injekt.get<SourceManager>()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
getDatabaseSourcesObservable()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribeLatestCache(ClearDatabaseController::setItems)
|
||||
}
|
||||
|
||||
fun clearDatabaseForSourceIds(sources: List<Long>) {
|
||||
db.deleteMangasNotInLibraryBySourceIds(sources).executeAsBlocking()
|
||||
db.deleteHistoryNoLastRead().executeAsBlocking()
|
||||
}
|
||||
|
||||
private fun getDatabaseSourcesObservable(): Observable<List<ClearDatabaseSourceItem>> {
|
||||
return db.getSourceIdsWithNonLibraryManga().asRxObservable()
|
||||
.map { sourceCounts ->
|
||||
sourceCounts.map {
|
||||
val sourceObj = sourceManager.getOrStub(it.source)
|
||||
ClearDatabaseSourceItem(sourceObj, it.count)
|
||||
}.sortedBy { it.source.name }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ClearDatabaseSourceItem.Holder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.clear_database_source_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?, holder: Holder?, position: Int, payloads: MutableList<Any>?) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -6,14 +6,6 @@
|
|||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<eu.kanade.tachiyomi.widget.RevealAnimationView
|
||||
android:id="@+id/reveal_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorAccent"
|
||||
android:elevation="5dp"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
android:layout_width="match_parent"
|
||||
|
|
32
app/src/main/res/layout/clear_database_controller.xml
Normal file
32
app/src/main/res/layout/clear_database_controller.xml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:choiceMode="multipleChoice"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="@dimen/fab_list_padding"
|
||||
tools:listitem="@layout/clear_database_source_item" />
|
||||
|
||||
<eu.kanade.tachiyomi.widget.MaterialFastScroll
|
||||
android:id="@+id/fast_scroller"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
app:fastScrollerBubbleEnabled="false"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<eu.kanade.tachiyomi.widget.EmptyView
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
68
app/src/main/res/layout/clear_database_source_item.xml
Normal file
68
app/src/main/res/layout/clear_database_source_item.xml
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:background="@drawable/list_item_selector_background"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:paddingHorizontal="8dp"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@mipmap/ic_launcher" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="?attr/textAppearanceBodyMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:ellipsize="middle"
|
||||
android:maxLines="1"
|
||||
app:layout_constraintStart_toEndOf="@+id/thumbnail"
|
||||
app:layout_constraintEnd_toStartOf="@id/checkbox"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/description"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Source Name (LN)" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
app:layout_constraintStart_toEndOf="@+id/thumbnail"
|
||||
app:layout_constraintEnd_toStartOf="@id/checkbox"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:text="999 non-library manga in database" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:clickable="false"
|
||||
android:background="@android:color/transparent"
|
||||
android:longClickable="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:2"
|
||||
/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -6,14 +6,6 @@
|
|||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<eu.kanade.tachiyomi.widget.RevealAnimationView
|
||||
android:id="@+id/reveal_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorAccent"
|
||||
android:elevation="5dp"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -18,4 +18,9 @@
|
|||
android:title="@string/action_disable_all"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_clear_cookies"
|
||||
android:title="@string/pref_clear_cookies"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
|
|
|
@ -527,10 +527,13 @@
|
|||
<string name="used_cache_both">Used by anime: %1$s, used by manga; %2$s</string>
|
||||
<string name="cache_deleted">Cache cleared. %1$d files have been deleted</string>
|
||||
<string name="cache_delete_error">An error occurred while clearing cache</string>
|
||||
<string name="pref_auto_clear_chapter_cache">Clear chapter/episode cache on app close</string>
|
||||
<string name="pref_clear_database">Clear database</string>
|
||||
<string name="pref_clear_database_summary">Delete history for anime and manga that are not saved in your library</string>
|
||||
<string name="clear_database_source_item_count">%1$d non-library entries in database</string>
|
||||
<string name="clear_database_confirmation">Are you sure? Completed episodes and chapters and progress of non-library entries will be lost</string>
|
||||
<string name="clear_database_completed">Entries deleted</string>
|
||||
<string name="database_clean">Database clean</string>
|
||||
<string name="pref_refresh_library_covers">Refresh library covers</string>
|
||||
<string name="pref_refresh_library_tracking">Refresh tracking</string>
|
||||
<string name="pref_refresh_library_tracking_summary">Updates status, score and progress from the tracking services</string>
|
||||
|
|
Loading…
Reference in a new issue