Merge remote-tracking branch 'upstream/master'

This commit is contained in:
jmir1 2021-09-12 13:03:34 +02:00
commit 95e63cf336
30 changed files with 667 additions and 367 deletions

View file

@ -230,6 +230,11 @@
android:resource="@xml/provider_paths" />
</provider>
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="false" />
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
</application>
</manifest>

View file

@ -44,6 +44,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
backupCategories(),
backupAnime(databaseAnime, flags),
backupCategoriesAnime(),
emptyList(),
emptyList(),
backupExtensionInfo(databaseManga),
backupAnimeExtensionInfo(databaseAnime)
)

View file

@ -5,8 +5,20 @@ import android.net.Uri
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.backup.full.models.*
import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.backup.full.models.BackupAnime
import eu.kanade.tachiyomi.data.backup.full.models.BackupAnimeHistory
import eu.kanade.tachiyomi.data.backup.full.models.BackupAnimeSource
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.AnimeTrack
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import okio.buffer
import okio.gzip
import okio.source
@ -33,8 +45,10 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
}
// Store source mapping for error messages
sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap() +
backup.backupAnimeSources.map { it.sourceId to it.name }.toMap()
val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
val backupMapsAnime = backup.backupBrokenAnimeSources.map { BackupAnimeSource(it.name, it.sourceId) } + backup.backupAnimeSources
sourceMapping = backupMaps.map { it.sourceId to it.name }.toMap() +
backupMapsAnime.map { it.sourceId to it.name }.toMap()
// Restore individual manga
backup.backupManga.forEach {
@ -81,7 +95,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
val manga = backupManga.getMangaImpl()
val chapters = backupManga.getChaptersImpl()
val categories = backupManga.categories
val history = backupManga.history
val history = backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead) } + backupManga.history
val tracks = backupManga.getTrackingImpl()
try {
@ -99,7 +113,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
val anime = backupAnime.getAnimeImpl()
val episodes = backupAnime.getEpisodesImpl()
val categories = backupAnime.categories
val history = backupAnime.history
val history = backupAnime.brokenHistory.map { BackupAnimeHistory(it.url, it.lastSeen) } + backupAnime.history
val tracks = backupAnime.getTrackingImpl()
try {

View file

@ -10,6 +10,8 @@ data class Backup(
@ProtoNumber(3) val backupAnime: List<BackupAnime> = emptyList(),
@ProtoNumber(4) var backupCategoriesAnime: List<BackupCategory> = emptyList(),
// Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
@ProtoNumber(100) var backupBrokenAnimeSources: List<BrokenBackupAnimeSource> = emptyList(),
@ProtoNumber(100) var backupSources: List<BackupSource> = emptyList(),
@ProtoNumber(101) var backupAnimeSources: List<BackupAnimeSource> = emptyList(),
@ProtoNumber(101) var backupAnimeSources: List<BackupAnimeSource> = emptyList()
)

View file

@ -32,8 +32,9 @@ data class BackupAnime(
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
@ProtoNumber(100) var favorite: Boolean = true,
@ProtoNumber(101) var episodeFlags: Int = 0,
@ProtoNumber(102) var history: List<BackupAnimeHistory> = emptyList(),
@ProtoNumber(103) var viewer_flags: Int = 0
@ProtoNumber(102) var brokenHistory: List<BrokenBackupAnimeHistory> = emptyList(),
@ProtoNumber(103) var viewer_flags: Int = 0,
@ProtoNumber(104) var history: List<BackupAnimeHistory> = emptyList()
) {
fun getAnimeImpl(): AnimeImpl {
return AnimeImpl().apply {

View file

@ -3,6 +3,12 @@ package eu.kanade.tachiyomi.data.backup.full.models
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BrokenBackupAnimeHistory(
@ProtoNumber(0) var url: String,
@ProtoNumber(1) var lastSeen: Long
)
@Serializable
data class BackupAnimeHistory(
@ProtoNumber(0) var url: String,

View file

@ -4,6 +4,12 @@ import eu.kanade.tachiyomi.animesource.AnimeSource
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BrokenBackupAnimeSource(
@ProtoNumber(0) var name: String = "",
@ProtoNumber(1) var sourceId: Long
)
@Serializable
data class BackupAnimeSource(
@ProtoNumber(0) var name: String = "",

View file

@ -4,7 +4,13 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BackupHistory(
data class BrokenBackupHistory(
@ProtoNumber(0) var url: String,
@ProtoNumber(1) var lastRead: Long
)
@Serializable
data class BackupHistory(
@ProtoNumber(1) var url: String,
@ProtoNumber(2) var lastRead: Long
)

View file

@ -33,8 +33,9 @@ data class BackupManga(
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
@ProtoNumber(100) var favorite: Boolean = true,
@ProtoNumber(101) var chapterFlags: Int = 0,
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
@ProtoNumber(103) var viewer_flags: Int? = null
@ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(),
@ProtoNumber(103) var viewer_flags: Int? = null,
@ProtoNumber(104) var history: List<BackupHistory> = emptyList()
) {
fun getMangaImpl(): MangaImpl {
return MangaImpl().apply {

View file

@ -5,9 +5,15 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BackupSource(
data class BrokenBackupSource(
@ProtoNumber(0) var name: String = "",
@ProtoNumber(1) var sourceId: Long
)
@Serializable
data class BackupSource(
@ProtoNumber(1) var name: String = "",
@ProtoNumber(2) var sourceId: Long
) {
companion object {
fun copyFrom(source: Source): BackupSource {

View file

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
@ -15,6 +16,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.lang.launchIO
import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
/**
@ -24,7 +27,10 @@ import uy.kohesive.injekt.injectLazy
*
* @param context the application context.
*/
class AnimeDownloadManager(private val context: Context) {
class AnimeDownloadManager(
private val context: Context,
private val db: AnimeDatabaseHelper = Injekt.get()
) {
private val sourceManager: AnimeSourceManager by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
@ -249,7 +255,7 @@ class AnimeDownloadManager(private val context: Context) {
val filteredEpisodes = if (isCancelling) {
episodes
} else {
getEpisodesToDelete(episodes)
getEpisodesToDelete(episodes, anime)
}
launchIO {
removeFromDownloadQueue(filteredEpisodes)
@ -303,7 +309,7 @@ class AnimeDownloadManager(private val context: Context) {
* @param anime the anime of the episodes.
*/
fun enqueueDeleteEpisodes(episodes: List<Episode>, anime: Anime) {
pendingDeleter.addEpisodes(getEpisodesToDelete(episodes), anime)
pendingDeleter.addEpisodes(getEpisodesToDelete(episodes, anime), anime)
}
/**
@ -343,8 +349,17 @@ class AnimeDownloadManager(private val context: Context) {
}
}
private fun getEpisodesToDelete(episodes: List<Episode>): List<Episode> {
return if (!preferences.removeBookmarkedEpisodes()) {
private fun getEpisodesToDelete(episodes: List<Episode>, anime: Anime): List<Episode> {
// Retrieve the categories that are set to exclude from being deleted on read
val categoriesToExclude = preferences.removeExcludeAnimeCategories().get().map(String::toInt)
val categoriesForAnime = db.getCategoriesForAnime(anime).executeAsBlocking()
.mapNotNull { it.id }
.takeUnless { it.isEmpty() }
?: listOf(0)
return if (categoriesForAnime.intersect(categoriesToExclude).isNotEmpty()) {
episodes.filterNot { it.seen }
} else if (!preferences.removeBookmarkedChapters()) {
episodes.filterNot { it.bookmark }
} else {
episodes

View file

@ -4,6 +4,7 @@ import android.content.Context
import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download
@ -15,6 +16,8 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.lang.launchIO
import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
/**
@ -24,7 +27,10 @@ import uy.kohesive.injekt.injectLazy
*
* @param context the application context.
*/
class DownloadManager(private val context: Context) {
class DownloadManager(
private val context: Context,
private val db: DatabaseHelper = Injekt.get()
) {
private val sourceManager: SourceManager by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
@ -241,7 +247,7 @@ class DownloadManager(private val context: Context) {
val filteredChapters = if (isCancelling) {
chapters
} else {
getChaptersToDelete(chapters)
getChaptersToDelete(chapters, manga)
}
launchIO {
@ -296,7 +302,7 @@ class DownloadManager(private val context: Context) {
* @param manga the manga of the chapters.
*/
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
pendingDeleter.addChapters(getChaptersToDelete(chapters), manga)
pendingDeleter.addChapters(getChaptersToDelete(chapters, manga), manga)
}
/**
@ -336,8 +342,17 @@ class DownloadManager(private val context: Context) {
}
}
private fun getChaptersToDelete(chapters: List<Chapter>): List<Chapter> {
return if (!preferences.removeBookmarkedChapters()) {
private fun getChaptersToDelete(chapters: List<Chapter>, manga: Manga): List<Chapter> {
// Retrieve the categories that are set to exclude from being deleted on read
val categoriesToExclude = preferences.removeExcludeCategories().get().map(String::toInt)
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
.mapNotNull { it.id }
.takeUnless { it.isEmpty() }
?: listOf(0)
return if (categoriesForManga.intersect(categoriesToExclude).isNotEmpty()) {
chapters.filterNot { it.read }
} else if (!preferences.removeBookmarkedChapters()) {
chapters.filterNot { it.bookmark }
} else {
chapters

View file

@ -152,8 +152,6 @@ object PreferenceKeys {
const val removeBookmarkedChapters = "pref_remove_bookmarked"
const val removeBookmarkedEpisodes = "pref_remove_bookmarked_episodes"
const val libraryUpdateInterval = "pref_library_update_interval_key"
const val libraryUpdateRestriction = "library_update_restriction"
@ -205,7 +203,11 @@ object PreferenceKeys {
const val downloadNew = "download_new"
const val downloadNewCategories = "download_new_categories"
const val downloadNewCategoriesAnime = "download_new_categories_anime"
const val downloadNewCategoriesExclude = "download_new_categories_exclude"
const val downloadNewCategoriesExcludeAnime = "download_new_categories_exclude_anime"
const val removeExcludeCategories = "remove_exclude_categories"
const val removeExcludeCategoriesAnime = "remove_exclude_categories_anime"
const val libraryDisplayMode = "pref_display_mode_library"

View file

@ -264,7 +264,8 @@ class PreferencesHelper(val context: Context) {
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
fun removeBookmarkedEpisodes() = prefs.getBoolean(Keys.removeBookmarkedEpisodes, false)
fun removeExcludeCategories() = flowPrefs.getStringSet(Keys.removeExcludeCategories, emptySet())
fun removeExcludeAnimeCategories() = flowPrefs.getStringSet(Keys.removeExcludeCategoriesAnime, emptySet())
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
@ -334,7 +335,9 @@ class PreferencesHelper(val context: Context) {
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
fun downloadNewCategoriesAnime() = flowPrefs.getStringSet(Keys.downloadNewCategoriesAnime, emptySet())
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
fun downloadNewCategoriesAnimeExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExcludeAnime, emptySet())
fun lang() = flowPrefs.getString(Keys.lang, "")

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.anime.info
import android.app.Dialog
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.util.TypedValue
@ -12,7 +11,6 @@ import androidx.core.view.WindowCompat
import coil.imageLoader
import coil.request.Disposable
import coil.request.ImageRequest
import com.davemorrissey.labs.subscaleview.ImageSource
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
@ -20,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.databinding.MangaFullCoverDialogBinding
import eu.kanade.tachiyomi.ui.anime.AnimeController
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat
import eu.kanade.tachiyomi.widget.TachiyomiFullscreenDialog
import uy.kohesive.injekt.Injekt
@ -63,12 +62,6 @@ class AnimeFullCoverDialog : DialogController {
menu?.findItem(R.id.action_edit_cover)?.isVisible = anime?.favorite ?: false
}
binding?.fullCover?.apply {
setOnClickListener {
dialog?.dismiss()
}
setMinimumDpi(45)
}
setImage(anime)
binding?.appbar?.applyInsetter {
@ -77,11 +70,10 @@ class AnimeFullCoverDialog : DialogController {
}
}
binding?.fullCover?.applyInsetter {
binding?.container?.onViewClicked = { dialog?.dismiss() }
binding?.container?.applyInsetter {
type(navigationBars = true) {
// Padding will make to image top align
// This is likely an issue with SubsamplingScaleImageView
margin(bottom = true)
padding(bottom = true)
}
}
@ -108,12 +100,16 @@ class AnimeFullCoverDialog : DialogController {
}
fun setImage(anime: Anime?) {
val anime = anime ?: return
if (anime == null) return
val request = ImageRequest.Builder(applicationContext!!)
.data(anime)
.target {
val bitmap = (it as BitmapDrawable).bitmap
binding?.fullCover?.setImage(ImageSource.cachedBitmap(bitmap))
binding?.container?.setImage(
it,
ReaderPageImageView.Config(
zoomDuration = 500
)
)
}
.build()

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.manga.info
import android.app.Dialog
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.util.TypedValue
@ -12,7 +11,6 @@ import androidx.core.view.WindowCompat
import coil.imageLoader
import coil.request.Disposable
import coil.request.ImageRequest
import com.davemorrissey.labs.subscaleview.ImageSource
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -20,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.MangaFullCoverDialogBinding
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat
import eu.kanade.tachiyomi.widget.TachiyomiFullscreenDialog
import uy.kohesive.injekt.Injekt
@ -63,12 +62,6 @@ class MangaFullCoverDialog : DialogController {
menu?.findItem(R.id.action_edit_cover)?.isVisible = manga?.favorite ?: false
}
binding?.fullCover?.apply {
setOnClickListener {
dialog?.dismiss()
}
setMinimumDpi(45)
}
setImage(manga)
binding?.appbar?.applyInsetter {
@ -77,11 +70,10 @@ class MangaFullCoverDialog : DialogController {
}
}
binding?.fullCover?.applyInsetter {
binding?.container?.onViewClicked = { dialog?.dismiss() }
binding?.container?.applyInsetter {
type(navigationBars = true) {
// Padding will make to image top align
// This is likely an issue with SubsamplingScaleImageView
margin(bottom = true)
padding(bottom = true)
}
}
@ -108,12 +100,16 @@ class MangaFullCoverDialog : DialogController {
}
fun setImage(manga: Manga?) {
val manga = manga ?: return
if (manga == null) return
val request = ImageRequest.Builder(applicationContext!!)
.data(manga)
.target {
val bitmap = (it as BitmapDrawable).bitmap
binding?.fullCover?.setImage(ImageSource.cachedBitmap(bitmap))
binding?.container?.setImage(
it,
ReaderPageImageView.Config(
zoomDuration = 500
)
)
}
.build()

View file

@ -51,7 +51,6 @@ import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.AnimeHistory
import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.data.download.AnimeDownloadManager
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
@ -85,6 +84,7 @@ class PlayerActivity : AppCompatActivity() {
private val preferences: PreferencesHelper = Injekt.get()
private val incognitoMode = preferences.incognitoMode().get()
private val db: AnimeDatabaseHelper = Injekt.get()
private val downloadManager: AnimeDownloadManager = Injekt.get()
private val delayedTrackingStore: DelayedTrackingStore = Injekt.get()
private lateinit var exoPlayer: SimpleExoPlayer
private lateinit var dataSourceFactory: DataSource.Factory
@ -363,6 +363,9 @@ class PlayerActivity : AppCompatActivity() {
override fun onBackPressed() {
releasePlayer()
deletePendingEpisodes()
saveEpisodeHistory(EpisodeItem(episode, anime))
setEpisodeProgress(episode, anime, exoPlayer.currentPosition, exoPlayer.duration)
super.onBackPressed()
}
@ -371,8 +374,6 @@ class PlayerActivity : AppCompatActivity() {
isPlayerPlaying = exoPlayer.playWhenReady
playbackPosition = exoPlayer.currentPosition
currentWindow = exoPlayer.currentWindowIndex
saveEpisodeHistory(EpisodeItem(episode, anime))
setEpisodeProgress(episode, anime, exoPlayer.currentPosition, exoPlayer.duration)
exoPlayer.release()
}
@ -598,27 +599,60 @@ class PlayerActivity : AppCompatActivity() {
if (preferences.autoUpdateTrack() && episode.seen) {
updateTrackEpisodeSeen(episode)
}
if (preferences.removeAfterMarkedAsRead()) {
launchIO {
try {
val downloadManager: AnimeDownloadManager = Injekt.get()
val source: AnimeSource = Injekt.get<AnimeSourceManager>().getOrStub(anime.source)
downloadManager.deleteEpisodes(episodes, anime, source).forEach {
if (it is EpisodeItem) {
it.status = AnimeDownload.State.NOT_DOWNLOADED
it.download = null
}
}
} catch (e: Throwable) {
throw e
}
}
}
deleteEpisodeIfNeeded(episode)
deleteEpisodeFromDownloadQueue(episode)
}
}
}
}
private fun deleteEpisodeFromDownloadQueue(episode: Episode) {
downloadManager.getEpisodeDownloadOrNull(episode)?.let { download ->
downloadManager.deletePendingDownload(download)
}
}
private fun enqueueDeleteSeenEpisodes(episode: Episode) {
if (!episode.seen) return
val anime = anime
launchIO {
downloadManager.enqueueDeleteEpisodes(listOf(episode), anime)
}
}
private fun deleteEpisodeIfNeeded(episode: Episode) {
// Determine which chapter should be deleted and enqueue
val sortFunction: (Episode, Episode) -> Int = when (anime.sorting) {
Anime.EPISODE_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
Anime.EPISODE_SORTING_NUMBER -> { c1, c2 -> c1.episode_number.compareTo(c2.episode_number) }
Anime.EPISODE_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
else -> throw NotImplementedError("Unknown sorting method")
}
val episodes = db.getEpisodes(anime).executeAsBlocking()
.sortedWith { e1, e2 -> sortFunction(e1, e2) }
val currentEpisodePosition = episodes.indexOf(episode)
val removeAfterReadSlots = preferences.removeAfterReadSlots()
val episodeToDelete = episodes.getOrNull(currentEpisodePosition - removeAfterReadSlots)
// Check if deleting option is enabled and chapter exists
if (removeAfterReadSlots != -1 && episodeToDelete != null) {
enqueueDeleteSeenEpisodes(episodeToDelete)
}
}
/**
* Deletes all the pending episodes. This operation will run in a background thread and errors
* are ignored.
*/
private fun deletePendingEpisodes() {
launchIO {
downloadManager.deletePendingEpisodes()
}
}
private fun updateTrackEpisodeSeen(episode: Episode) {
val episodeSeen = episode.episode_number

View file

@ -65,7 +65,6 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
import eu.kanade.tachiyomi.util.system.getThemeColor
@ -109,11 +108,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
private val preferences: PreferencesHelper by injectLazy()
/**
* The maximum bitmap size supported by the device.
*/
val maxBitmapSize by lazy { GLUtil.maxTextureSize }
val hasCutout by lazy { hasDisplayCutout() }
/**

View file

@ -409,6 +409,7 @@ class ReaderPresenter(
val currentChapterPosition = chapterList.indexOf(currentChapter)
val removeAfterReadSlots = preferences.removeAfterReadSlots()
val chapterToDelete = chapterList.getOrNull(currentChapterPosition - removeAfterReadSlots)
// Check if deleting option is enabled and chapter exists
if (removeAfterReadSlots != -1 && chapterToDelete != null) {
enqueueDeleteReadChapters(chapterToDelete)

View file

@ -0,0 +1,264 @@
package eu.kanade.tachiyomi.ui.reader.viewer
import android.content.Context
import android.graphics.PointF
import android.graphics.drawable.Animatable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import androidx.annotation.AttrRes
import androidx.annotation.CallSuper
import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible
import coil.clear
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
import com.github.chrisbanes.photoview.PhotoView
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import java.io.InputStream
import java.nio.ByteBuffer
/**
* A wrapper view for showing page image.
*
* Animated image will be drawn by [PhotoView] while [SubsamplingScaleImageView] will take non-animated image.
*
* @param isWebtoon if true, [WebtoonSubsamplingImageView] will be used instead of [SubsamplingScaleImageView]
* and [AppCompatImageView] will be used instead of [PhotoView]
*/
open class ReaderPageImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttrs: Int = 0,
@StyleRes defStyleRes: Int = 0,
private val isWebtoon: Boolean = false
) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) {
private var pageView: View? = null
var onImageLoaded: (() -> Unit)? = null
var onImageLoadError: (() -> Unit)? = null
var onScaleChanged: ((newScale: Float) -> Unit)? = null
var onViewClicked: (() -> Unit)? = null
@CallSuper
open fun onImageLoaded() {
onImageLoaded?.invoke()
}
@CallSuper
open fun onImageLoadError() {
onImageLoadError?.invoke()
}
@CallSuper
open fun onScaleChanged(newScale: Float) {
onScaleChanged?.invoke(newScale)
}
@CallSuper
open fun onViewClicked() {
onViewClicked?.invoke()
}
fun setImage(drawable: Drawable, config: Config) {
if (drawable is Animatable) {
prepareAnimatedImageView()
setAnimatedImage(drawable, config)
} else {
prepareNonAnimatedImageView()
setNonAnimatedImage(drawable, config)
}
}
fun setImage(inputStream: InputStream, isAnimated: Boolean, config: Config) {
if (isAnimated) {
prepareAnimatedImageView()
setAnimatedImage(inputStream, config)
} else {
prepareNonAnimatedImageView()
setNonAnimatedImage(inputStream, config)
}
}
fun recycle() = pageView?.let {
when (it) {
is SubsamplingScaleImageView -> it.recycle()
is AppCompatImageView -> it.clear()
}
it.isVisible = false
}
private fun prepareNonAnimatedImageView() {
if (pageView is SubsamplingScaleImageView) return
removeView(pageView)
pageView = if (isWebtoon) {
WebtoonSubsamplingImageView(context)
} else {
SubsamplingScaleImageView(context)
}.apply {
setMaxTileSize(GLUtil.maxTextureSize)
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
setMinimumTileDpi(180)
setOnStateChangedListener(
object : SubsamplingScaleImageView.OnStateChangedListener {
override fun onScaleChanged(newScale: Float, origin: Int) {
this@ReaderPageImageView.onScaleChanged(newScale)
}
override fun onCenterChanged(newCenter: PointF?, origin: Int) {
// Not used
}
}
)
setOnClickListener { this@ReaderPageImageView.onViewClicked() }
}
addView(pageView, MATCH_PARENT, MATCH_PARENT)
}
private fun setNonAnimatedImage(
image: Any,
config: Config
) = (pageView as? SubsamplingScaleImageView)?.apply {
setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration())
setMinimumScaleType(config.minimumScaleType)
setMinimumDpi(1) // Just so that very small image will be fit for initial load
setCropBorders(config.cropBorders)
setOnImageEventListener(
object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
override fun onReady() {
// 3x zoom
maxScale = scale * MAX_ZOOM_SCALE
setDoubleTapZoomScale(scale * 2)
when (config.zoomStartPosition) {
ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F))
ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F))
ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center.also { it?.y = 0F })
}
this@ReaderPageImageView.onImageLoaded()
}
override fun onImageLoadError(e: Exception) {
this@ReaderPageImageView.onImageLoadError()
}
}
)
when (image) {
is Drawable -> {
val bitmap = (image as BitmapDrawable).bitmap
setImage(ImageSource.bitmap(bitmap))
}
is InputStream -> setImage(ImageSource.inputStream(image))
else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}")
}
isVisible = true
}
private fun prepareAnimatedImageView() {
if (pageView is AppCompatImageView) return
removeView(pageView)
pageView = if (isWebtoon) {
AppCompatImageView(context)
} else {
PhotoView(context)
}.apply {
adjustViewBounds = true
if (this is PhotoView) {
setScaleLevels(1F, 2F, MAX_ZOOM_SCALE)
// Force 2 scale levels on double tap
setOnDoubleTapListener(
object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
if (scale > 1F) {
setScale(1F, e.x, e.y, true)
} else {
setScale(2F, e.x, e.y, true)
}
return true
}
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
this@ReaderPageImageView.onViewClicked()
return super.onSingleTapConfirmed(e)
}
}
)
setOnScaleChangeListener { _, _, _ ->
this@ReaderPageImageView.onScaleChanged(scale)
}
}
}
addView(pageView, MATCH_PARENT, MATCH_PARENT)
}
private fun setAnimatedImage(
image: Any,
config: Config
) = (pageView as? AppCompatImageView)?.apply {
if (this is PhotoView) {
setZoomTransitionDuration(config.zoomDuration.getSystemScaledDuration())
}
val data = when (image) {
is Drawable -> image
is InputStream -> ByteBuffer.wrap(image.readBytes())
else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}")
}
val request = ImageRequest.Builder(context)
.data(data)
.memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.DISABLED)
.target(
onSuccess = { result ->
setImageDrawable(result)
(result as? Animatable)?.start()
isVisible = true
this@ReaderPageImageView.onImageLoaded()
},
onError = {
this@ReaderPageImageView.onImageLoadError()
}
)
.crossfade(false)
.build()
context.imageLoader.enqueue(request)
}
private fun Int.getSystemScaledDuration(): Int {
return (this * context.animatorDurationScale).toInt().coerceAtLeast(1)
}
/**
* All of the config except [zoomDuration] will only be used for non-animated image.
*/
data class Config(
val zoomDuration: Int,
val minimumScaleType: Int = SCALE_TYPE_CENTER_INSIDE,
val cropBorders: Boolean = false,
val zoomStartPosition: ZoomStartPosition = ZoomStartPosition.CENTER
)
enum class ZoomStartPosition {
LEFT, CENTER, RIGHT
}
}
private const val MAX_ZOOM_SCALE = 3F

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation
@ -34,7 +35,7 @@ class PagerConfig(
var imageScaleType = 1
private set
var imageZoomType = ZoomType.Left
var imageZoomType = ReaderPageImageView.ZoomStartPosition.LEFT
private set
var imageCropBorders = false
@ -86,16 +87,16 @@ class PagerConfig(
imageZoomType = when (value) {
// Auto
1 -> when (viewer) {
is L2RPagerViewer -> ZoomType.Left
is R2LPagerViewer -> ZoomType.Right
else -> ZoomType.Center
is L2RPagerViewer -> ReaderPageImageView.ZoomStartPosition.LEFT
is R2LPagerViewer -> ReaderPageImageView.ZoomStartPosition.RIGHT
else -> ReaderPageImageView.ZoomStartPosition.CENTER
}
// Left
2 -> ZoomType.Left
2 -> ReaderPageImageView.ZoomStartPosition.LEFT
// Right
3 -> ZoomType.Right
3 -> ReaderPageImageView.ZoomStartPosition.RIGHT
// Center
else -> ZoomType.Center
else -> ReaderPageImageView.ZoomStartPosition.CENTER
}
}
@ -122,8 +123,4 @@ class PagerConfig(
}
navigationModeChangedListener?.invoke()
}
enum class ZoomType {
Left, Center, Right
}
}

View file

@ -1,35 +1,21 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint
import android.app.ActionBar
import android.content.Context
import android.graphics.PointF
import android.graphics.drawable.Animatable
import android.view.GestureDetector
import android.view.Gravity
import android.view.MotionEvent
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.core.view.setMargins
import androidx.core.view.updateLayoutParams
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.github.chrisbanes.photoview.PhotoView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.dpToPx
@ -40,7 +26,6 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.nio.ByteBuffer
import java.util.concurrent.TimeUnit
/**
@ -51,7 +36,7 @@ class PagerPageHolder(
readerThemedContext: Context,
val viewer: PagerViewer,
val page: ReaderPage
) : FrameLayout(readerThemedContext), ViewPagerAdapter.PositionableView {
) : ReaderPageImageView(readerThemedContext), ViewPagerAdapter.PositionableView {
/**
* Item that identifies this view. Needed by the adapter to not recreate views.
@ -62,17 +47,11 @@ class PagerPageHolder(
/**
* Loading progress bar to indicate the current progress.
*/
private val progressIndicator: ReaderProgressIndicator
/**
* Image view that supports subsampling on zoom.
*/
private var subsamplingImageView: SubsamplingScaleImageView? = null
/**
* Simple image view only used on GIFs.
*/
private var imageView: ImageView? = null
private val progressIndicator: ReaderProgressIndicator = ReaderProgressIndicator(readerThemedContext).apply {
updateLayoutParams<LayoutParams> {
gravity = Gravity.CENTER
}
}
/**
* Retry button used to allow retrying.
@ -100,36 +79,9 @@ class PagerPageHolder(
*/
private var readImageHeaderSubscription: Subscription? = null
val stateChangedListener = object : SubsamplingScaleImageView.OnStateChangedListener {
override fun onScaleChanged(newScale: Float, origin: Int) {
viewer.activity.hideMenu()
}
override fun onCenterChanged(newCenter: PointF?, origin: Int) {
viewer.activity.hideMenu()
}
}
private var visibilityListener = ActionBar.OnMenuVisibilityListener { isVisible ->
if (isVisible.not()) {
subsamplingImageView?.setOnStateChangedListener(null)
return@OnMenuVisibilityListener
}
subsamplingImageView?.setOnStateChangedListener(stateChangedListener)
}
init {
progressIndicator = ReaderProgressIndicator(readerThemedContext).apply {
updateLayoutParams<LayoutParams> {
gravity = Gravity.CENTER
}
}
addView(progressIndicator)
observeStatus()
viewer.activity.addOnMenuVisibilityListener(visibilityListener)
if (viewer.activity.menuVisible) {
// Listener will not be available if user changed page with seek bar
subsamplingImageView?.setOnStateChangedListener(stateChangedListener)
}
}
/**
@ -141,9 +93,6 @@ class PagerPageHolder(
unsubscribeProgress()
unsubscribeStatus()
unsubscribeReadImageHeader()
subsamplingImageView?.setOnImageEventListener(null)
subsamplingImageView?.setOnStateChangedListener(null)
viewer.activity.removeOnMenuVisibilityListener(visibilityListener)
}
/**
@ -284,13 +233,18 @@ class PagerPageHolder(
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { (bais, isAnimated, background) ->
bais.use {
setImage(
it,
isAnimated,
Config(
zoomDuration = viewer.config.doubleTapAnimDuration,
minimumScaleType = viewer.config.imageScaleType,
cropBorders = viewer.config.imageCropBorders,
zoomStartPosition = viewer.config.imageZoomType
)
)
if (!isAnimated) {
this.background = background
initSubsamplingImageView().apply {
setImage(ImageSource.inputStream(it))
}
} else {
initImageView().setImage(it)
}
}
}
@ -351,76 +305,18 @@ class PagerPageHolder(
/**
* Called when an image fails to decode.
*/
private fun onImageDecodeError() {
override fun onImageLoadError() {
super.onImageLoadError()
progressIndicator.hide()
initDecodeErrorLayout().isVisible = true
}
/**
* Initializes a subsampling scale view.
* Called when an image is zoomed in/out.
*/
private fun initSubsamplingImageView(): SubsamplingScaleImageView {
if (subsamplingImageView != null) return subsamplingImageView!!
val config = viewer.config
subsamplingImageView = SubsamplingScaleImageView(context).apply {
layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT)
setMaxTileSize(viewer.activity.maxBitmapSize)
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
setDoubleTapZoomDuration(config.doubleTapAnimDuration)
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
setMinimumScaleType(config.imageScaleType)
setMinimumDpi(90)
setMinimumTileDpi(180)
setCropBorders(config.imageCropBorders)
setOnImageEventListener(
object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
override fun onReady() {
when (config.imageZoomType) {
ZoomType.Left -> setScaleAndCenter(scale, PointF(0f, 0f))
ZoomType.Right -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f))
ZoomType.Center -> setScaleAndCenter(scale, center.also { it?.y = 0f })
}
}
override fun onImageLoadError(e: Exception) {
onImageDecodeError()
}
}
)
}
addView(subsamplingImageView)
return subsamplingImageView!!
}
/**
* Initializes an image view, used for GIFs.
*/
private fun initImageView(): ImageView {
if (imageView != null) return imageView!!
imageView = PhotoView(context, null).apply {
layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT)
adjustViewBounds = true
setZoomTransitionDuration(viewer.config.doubleTapAnimDuration)
setScaleLevels(1f, 2f, 3f)
// Force 2 scale levels on double tap
setOnDoubleTapListener(
object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
if (scale > 1f) {
setScale(1f, e.x, e.y, true)
} else {
setScale(2f, e.x, e.y, true)
}
return true
}
}
)
}
addView(imageView)
return imageView!!
override fun onScaleChanged(newScale: Float) {
super.onScaleChanged(newScale)
viewer.activity.hideMenu()
}
/**
@ -497,28 +393,4 @@ class PagerPageHolder(
addView(decodeLayout)
return decodeLayout
}
/**
* Extension method to set a [stream] into this ImageView.
*/
private fun ImageView.setImage(stream: InputStream) {
val request = ImageRequest.Builder(context)
.data(ByteBuffer.wrap(stream.readBytes()))
.memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.DISABLED)
.target(
onSuccess = { result ->
if (result is Animatable) {
result.start()
}
setImageDrawable(result)
},
onError = {
onImageDecodeError()
}
)
.crossfade(false)
.build()
context.imageLoader.enqueue(request)
}
}

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
@ -9,6 +8,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.hasMissingChapters
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
@ -112,7 +112,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
PAGE_VIEW -> {
val view = FrameLayout(readerThemedContext)
val view = ReaderPageImageView(readerThemedContext, isWebtoon = true)
WebtoonPageHolder(view, viewer)
}
TRANSITION_VIEW -> {

View file

@ -1,29 +1,22 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.content.res.Resources
import android.graphics.drawable.Animatable
import android.view.Gravity
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import coil.clear
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.ImageUtil
@ -33,7 +26,6 @@ import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import java.io.InputStream
import java.nio.ByteBuffer
import java.util.concurrent.TimeUnit
/**
@ -44,7 +36,7 @@ import java.util.concurrent.TimeUnit
* @constructor creates a new webtoon holder.
*/
class WebtoonPageHolder(
private val frame: FrameLayout,
private val frame: ReaderPageImageView,
viewer: WebtoonViewer
) : WebtoonBaseHolder(frame, viewer) {
@ -59,17 +51,6 @@ class WebtoonPageHolder(
*/
private lateinit var progressContainer: ViewGroup
/**
* Image view that supports subsampling on zoom.
*/
private var subsamplingImageView: SubsamplingScaleImageView? = null
private var cropBorders: Boolean = false
/**
* Simple image view only used on GIFs.
*/
private var imageView: ImageView? = null
/**
* Retry button container used to allow retrying.
*/
@ -109,6 +90,10 @@ class WebtoonPageHolder(
init {
refreshLayoutParams()
frame.onImageLoaded = { onImageDecoded() }
frame.onImageLoadError = { onImageDecodeError() }
frame.onScaleChanged = { viewer.activity.hideMenu() }
}
/**
@ -141,10 +126,7 @@ class WebtoonPageHolder(
unsubscribeReadImageHeader()
removeDecodeErrorLayout()
subsamplingImageView?.recycle()
subsamplingImageView?.isVisible = false
imageView?.clear()
imageView?.isVisible = false
frame.recycle()
progressIndicator.setProgress(0, animated = false)
}
@ -283,15 +265,15 @@ class WebtoonPageHolder(
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { isAnimated ->
if (!isAnimated) {
val subsamplingView = initSubsamplingImageView()
subsamplingView.isVisible = true
subsamplingView.setImage(ImageSource.inputStream(openStream!!))
} else {
val imageView = initImageView()
imageView.isVisible = true
imageView.setImage(openStream!!)
}
frame.setImage(
openStream!!,
isAnimated,
ReaderPageImageView.Config(
zoomDuration = viewer.config.doubleTapAnimDuration,
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH,
cropBorders = viewer.config.imageCropBorders
)
)
}
// Keep the Rx stream alive to close the input stream only when unsubscribed
.flatMap { Observable.never<Unit>() }
@ -355,58 +337,6 @@ class WebtoonPageHolder(
return progress
}
/**
* Initializes a subsampling scale view.
*/
private fun initSubsamplingImageView(): SubsamplingScaleImageView {
val config = viewer.config
if (subsamplingImageView != null) {
if (config.imageCropBorders != cropBorders) {
cropBorders = config.imageCropBorders
subsamplingImageView!!.setCropBorders(config.imageCropBorders)
}
return subsamplingImageView!!
}
cropBorders = config.imageCropBorders
subsamplingImageView = WebtoonSubsamplingImageView(context).apply {
setMaxTileSize(viewer.activity.maxBitmapSize)
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH)
setMinimumDpi(90)
setMinimumTileDpi(180)
setCropBorders(cropBorders)
setOnImageEventListener(
object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
override fun onReady() {
onImageDecoded()
}
override fun onImageLoadError(e: Exception) {
onImageDecodeError()
}
}
)
}
frame.addView(subsamplingImageView, MATCH_PARENT, MATCH_PARENT)
return subsamplingImageView!!
}
/**
* Initializes an image view, used for GIFs.
*/
private fun initImageView(): ImageView {
if (imageView != null) return imageView!!
imageView = AppCompatImageView(context).apply {
adjustViewBounds = true
}
frame.addView(imageView, MATCH_PARENT, MATCH_PARENT)
return imageView!!
}
/**
* Initializes a button to retry pages.
*/
@ -500,29 +430,4 @@ class WebtoonPageHolder(
decodeErrorLayout = null
}
}
/**
* Extension method to set a [stream] into this ImageView.
*/
private fun ImageView.setImage(stream: InputStream) {
val request = ImageRequest.Builder(context)
.data(ByteBuffer.wrap(stream.readBytes()))
.memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.DISABLED)
.target(
onSuccess = { result ->
if (result is Animatable) {
result.start()
}
setImageDrawable(result)
onImageDecoded()
},
onError = {
onImageDecodeError()
}
)
.crossfade(false)
.build()
context.imageLoader.enqueue(request)
}
}

View file

@ -13,12 +13,22 @@ import androidx.preference.PreferenceScreen
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.preference.*
import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.entriesRes
import eu.kanade.tachiyomi.util.preference.intListPreference
import eu.kanade.tachiyomi.util.preference.listPreference
import eu.kanade.tachiyomi.util.preference.multiSelectListPreference
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import eu.kanade.tachiyomi.widget.materialdialogs.setQuadStateMultiChoiceItems
@ -33,10 +43,16 @@ import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsDownloadController : SettingsController() {
private val db: DatabaseHelper by injectLazy()
private val adb: AnimeDatabaseHelper by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_downloads
val dbCategories = db.getCategories().executeAsBlocking()
val dbAnimeCategories = adb.getCategories().executeAsBlocking()
val mangaCategories = listOf(Category.createDefault(context)) + dbCategories
val animeCategories = listOf(Category.createDefault(context)) + dbAnimeCategories
preference {
key = Keys.downloadsDirectory
titleRes = R.string.pref_download_directory
@ -86,10 +102,45 @@ class SettingsDownloadController : SettingsController() {
titleRes = R.string.pref_remove_bookmarked_chapters
defaultValue = false
}
}
multiSelectListPreference {
key = Keys.removeExcludeCategoriesAnime
titleRes = R.string.pref_remove_exclude_categories_anime
entries = animeCategories.map { it.name }.toTypedArray()
entryValues = animeCategories.map { it.id.toString() }.toTypedArray()
val dbCategories = db.getCategories().executeAsBlocking()
val categories = listOf(Category.createDefault(context)) + dbCategories
preferences.removeExcludeAnimeCategories().asFlow()
.onEach { mutable ->
val selected = mutable
.mapNotNull { id -> animeCategories.find { it.id == id.toInt() } }
.sortedBy { it.order }
summary = if (selected.isEmpty()) {
resources?.getString(R.string.none)
} else {
selected.joinToString { it.name }
}
}.launchIn(viewScope)
}
multiSelectListPreference {
key = Keys.removeExcludeCategories
titleRes = R.string.pref_remove_exclude_categories_manga
entries = mangaCategories.map { it.name }.toTypedArray()
entryValues = mangaCategories.map { it.id.toString() }.toTypedArray()
preferences.removeExcludeCategories().asFlow()
.onEach { mutable ->
val selected = mutable
.mapNotNull { id -> mangaCategories.find { it.id == id.toInt() } }
.sortedBy { it.order }
summary = if (selected.isEmpty()) {
resources?.getString(R.string.none)
} else {
selected.joinToString { it.name }
}
}.launchIn(viewScope)
}
}
preferenceCategory {
titleRes = R.string.pref_category_auto_download
@ -99,6 +150,49 @@ class SettingsDownloadController : SettingsController() {
titleRes = R.string.pref_download_new
defaultValue = false
}
preference {
key = Keys.downloadNewCategoriesAnime
titleRes = R.string.anime_categories
onClick {
DownloadAnimeCategoriesDialog().showDialog(router)
}
preferences.downloadNew().asImmediateFlow { isVisible = it }
.launchIn(viewScope)
fun updateSummary() {
val selectedCategories = preferences.downloadNewCategoriesAnime().get()
.mapNotNull { id -> animeCategories.find { it.id == id.toInt() } }
.sortedBy { it.order }
val includedItemsText = if (selectedCategories.isEmpty()) {
context.getString(R.string.all)
} else {
selectedCategories.joinToString { it.name }
}
val excludedCategories = preferences.downloadNewCategoriesAnimeExclude().get()
.mapNotNull { id -> animeCategories.find { it.id == id.toInt() } }
.sortedBy { it.order }
val excludedItemsText = if (excludedCategories.isEmpty()) {
context.getString(R.string.none)
} else {
excludedCategories.joinToString { it.name }
}
summary = buildSpannedString {
append(context.getString(R.string.include, includedItemsText))
appendLine()
append(context.getString(R.string.exclude, excludedItemsText))
}
}
preferences.downloadNewCategoriesAnime().asFlow()
.onEach { updateSummary() }
.launchIn(viewScope)
preferences.downloadNewCategoriesAnimeExclude().asFlow()
.onEach { updateSummary() }
.launchIn(viewScope)
}
preference {
key = Keys.downloadNewCategories
titleRes = R.string.categories
@ -111,7 +205,7 @@ class SettingsDownloadController : SettingsController() {
fun updateSummary() {
val selectedCategories = preferences.downloadNewCategories().get()
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
.mapNotNull { id -> mangaCategories.find { it.id == id.toInt() } }
.sortedBy { it.order }
val includedItemsText = if (selectedCategories.isEmpty()) {
context.getString(R.string.all)
@ -120,7 +214,7 @@ class SettingsDownloadController : SettingsController() {
}
val excludedCategories = preferences.downloadNewCategoriesExclude().get()
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
.mapNotNull { id -> mangaCategories.find { it.id == id.toInt() } }
.sortedBy { it.order }
val excludedItemsText = if (excludedCategories.isEmpty()) {
context.getString(R.string.none)
@ -295,6 +389,54 @@ class SettingsDownloadController : SettingsController() {
.create()
}
}
class DownloadAnimeCategoriesDialog : DialogController() {
private val preferences: PreferencesHelper = Injekt.get()
private val db: AnimeDatabaseHelper = Injekt.get()
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val dbCategories = db.getCategories().executeAsBlocking()
val categories = listOf(Category.createDefault(activity!!)) + dbCategories
val items = categories.map { it.name }
var selected = categories
.map {
when (it.id.toString()) {
in preferences.downloadNewCategoriesAnime().get() -> QuadStateTextView.State.CHECKED.ordinal
in preferences.downloadNewCategoriesAnimeExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
else -> QuadStateTextView.State.UNCHECKED.ordinal
}
}
.toIntArray()
return MaterialAlertDialogBuilder(activity!!)
.setTitle(R.string.anime_categories)
.setQuadStateMultiChoiceItems(
message = R.string.pref_download_new_categories_details,
items = items,
initialSelected = selected
) { selections ->
selected = selections
}
.setPositiveButton(android.R.string.ok) { _, _ ->
val included = selected
.mapIndexed { index, value -> if (value == QuadStateTextView.State.CHECKED.ordinal) index else null }
.filterNotNull()
.map { categories[it].id.toString() }
.toSet()
val excluded = selected
.mapIndexed { index, value -> if (value == QuadStateTextView.State.INVERSED.ordinal) index else null }
.filterNotNull()
.map { categories[it].id.toString() }
.toSet()
preferences.downloadNewCategoriesAnime().set(included)
preferences.downloadNewCategoriesAnimeExclude().set(excluded)
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
}
}
private const val DOWNLOAD_DIR = 104

View file

@ -59,7 +59,7 @@ fun Anime.shouldDownloadNewEpisodes(db: AnimeDatabaseHelper, prefs: PreferencesH
val downloadNew = prefs.downloadNew().get()
if (!downloadNew) return false
val categoriesToDownload = prefs.downloadNewCategories().get().map(String::toInt)
val categoriesToDownload = prefs.downloadNewCategoriesAnime().get().map(String::toInt)
if (categoriesToDownload.isEmpty()) return true
// Get all categories, else default category (0)
@ -68,7 +68,7 @@ fun Anime.shouldDownloadNewEpisodes(db: AnimeDatabaseHelper, prefs: PreferencesH
.mapNotNull { it.id }
.takeUnless { it.isEmpty() } ?: listOf(0)
val categoriesToExclude = prefs.downloadNewCategoriesExclude().get().map(String::toInt)
val categoriesToExclude = prefs.downloadNewCategoriesAnimeExclude().get().map(String::toInt)
if (categoriesForAnime.intersect(categoriesToExclude).isNotEmpty()) return false
return categoriesForAnime.intersect(categoriesToDownload).isNotEmpty()

View file

@ -238,6 +238,8 @@
android:layout_marginBottom="-4dp"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:background="@android:color/transparent"
android:contentDescription="@string/manga_info_expand"
android:src="@drawable/ic_expand_more_24dp"
app:layout_constraintBottom_toBottomOf="@id/manga_summary_text"
app:layout_constraintEnd_toEndOf="parent"
@ -251,6 +253,8 @@
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:background="@android:color/transparent"
android:contentDescription="@string/manga_info_collapse"
android:src="@drawable/ic_expand_less_24dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -23,10 +23,12 @@
</com.google.android.material.appbar.AppBarLayout>
<com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
android:id="@+id/full_cover"
<eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
android:id="@+id/container"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:clipChildren="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -249,6 +249,8 @@
android:layout_marginBottom="-4dp"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:background="@android:color/transparent"
android:contentDescription="@string/manga_info_expand"
android:src="@drawable/ic_expand_more_24dp"
app:layout_constraintBottom_toBottomOf="@id/manga_summary_text"
app:layout_constraintEnd_toEndOf="parent"
@ -262,6 +264,8 @@
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:background="@android:color/transparent"
android:contentDescription="@string/manga_info_collapse"
android:src="@drawable/ic_expand_less_24dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -424,7 +424,11 @@
<string name="pref_category_delete_chapters">Delete chapters</string>
<string name="pref_remove_after_marked_as_read">After marked as read</string>
<string name="pref_remove_after_read">Automatically after reading</string>
<string name="pref_remove_bookmarked_chapters">Allow deleting bookmarked chapters</string>
<string name="pref_remove_bookmarked_chapters">Allow deleting bookmarked chapters/episodes</string>
<string name="pref_remove_exclude_categories_manga">Excluded manga categories</string>
<string name="pref_remove_exclude_categories_anime">Excluded anime categories</string>
<string name="pref_remove_exclude_categories_anime_prefix">Anime: </string>
<string name="pref_remove_exclude_categories_manga_prefix">Manga: </string>
<string name="custom_dir">Custom location</string>
<string name="disabled">Disabled</string>
<string name="last_read_chapter">Last read chapter</string>
@ -438,6 +442,7 @@
<string name="pref_external_downloader_selection">Downloader app preference</string>
<string name="pref_download_new">Download new chapters</string>
<string name="pref_download_new_categories_details">Manga in excluded categories will not be downloaded even if they are also in included categories.</string>
<string name="pref_download_new_anime_categories_details">Anime in excluded categories will not be downloaded even if they are also in included categories.</string>
<!-- Tracking section -->
<string name="tracking_guide">Tracking guide</string>