mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-26 06:43:45 +03:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
95e63cf336
30 changed files with 667 additions and 367 deletions
|
@ -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>
|
||||
|
|
|
@ -44,6 +44,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||
backupCategories(),
|
||||
backupAnime(databaseAnime, flags),
|
||||
backupCategoriesAnime(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
backupExtensionInfo(databaseManga),
|
||||
backupAnimeExtensionInfo(databaseAnime)
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = "",
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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, "")
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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() }
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue