mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 04:21:52 +01:00
Manga cover updates (#3101)
* cover caching overhaul * add ui for removing custom cover * skip some loading work * minor cleanup * allow refresh library metadata to refresh local manga * rename metadata_date to cover_last_modified * rearrange removeMangaFromLibrary * change custom cover directory add setting for updating cover when refreshing library * remove toggle and explicit action for updating covers
This commit is contained in:
parent
436253dd63
commit
dc54299e24
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.cache
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -17,51 +18,89 @@ import java.io.InputStream
|
|||||||
*/
|
*/
|
||||||
class CoverCache(private val context: Context) {
|
class CoverCache(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val COVERS_DIR = "covers"
|
||||||
|
private const val CUSTOM_COVERS_DIR = "covers/custom"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache directory used for cache management.
|
* Cache directory used for cache management.
|
||||||
*/
|
*/
|
||||||
private val cacheDir = context.getExternalFilesDir("covers")
|
private val cacheDir = getCacheDir(COVERS_DIR)
|
||||||
?: File(context.filesDir, "covers").also { it.mkdirs() }
|
|
||||||
|
private val customCoverCacheDir = getCacheDir(CUSTOM_COVERS_DIR)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cover from cache.
|
* Returns the cover from cache.
|
||||||
*
|
*
|
||||||
* @param thumbnailUrl the thumbnail url.
|
* @param manga the manga.
|
||||||
* @return cover image.
|
* @return cover image.
|
||||||
*/
|
*/
|
||||||
fun getCoverFile(thumbnailUrl: String): File {
|
fun getCoverFile(manga: Manga): File? {
|
||||||
return File(cacheDir, DiskUtil.hashKeyForDisk(thumbnailUrl))
|
return manga.thumbnail_url?.let {
|
||||||
|
File(cacheDir, DiskUtil.hashKeyForDisk(it))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy the given stream to this cache.
|
* Returns the custom cover from cache.
|
||||||
*
|
*
|
||||||
* @param thumbnailUrl url of the thumbnail.
|
* @param manga the manga.
|
||||||
|
* @return cover image.
|
||||||
|
*/
|
||||||
|
fun getCustomCoverFile(manga: Manga): File {
|
||||||
|
return File(customCoverCacheDir, DiskUtil.hashKeyForDisk(manga.id.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the given stream as the manga's custom cover to cache.
|
||||||
|
*
|
||||||
|
* @param manga the manga.
|
||||||
* @param inputStream the stream to copy.
|
* @param inputStream the stream to copy.
|
||||||
* @throws IOException if there's any error.
|
* @throws IOException if there's any error.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
|
fun setCustomCoverToCache(manga: Manga, inputStream: InputStream) {
|
||||||
// Get destination file.
|
getCustomCoverFile(manga).outputStream().use {
|
||||||
val destFile = getCoverFile(thumbnailUrl)
|
inputStream.copyTo(it)
|
||||||
|
}
|
||||||
destFile.outputStream().use { inputStream.copyTo(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the cover file from the cache.
|
* Delete the cover files of the manga from the cache.
|
||||||
*
|
*
|
||||||
* @param thumbnailUrl the thumbnail url.
|
* @param manga the manga.
|
||||||
* @return status of deletion.
|
* @param deleteCustomCover whether the custom cover should be deleted.
|
||||||
|
* @return number of files that were deleted.
|
||||||
*/
|
*/
|
||||||
fun deleteFromCache(thumbnailUrl: String?): Boolean {
|
fun deleteFromCache(manga: Manga, deleteCustomCover: Boolean = false): Int {
|
||||||
// Check if url is empty.
|
var deleted = 0
|
||||||
if (thumbnailUrl.isNullOrEmpty()) {
|
|
||||||
return false
|
getCoverFile(manga)?.let {
|
||||||
|
if (it.exists() && it.delete()) ++deleted
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove file.
|
if (deleteCustomCover) {
|
||||||
val file = getCoverFile(thumbnailUrl)
|
if (deleteCustomCover(manga)) ++deleted
|
||||||
return file.exists() && file.delete()
|
}
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete custom cover of the manga from the cache
|
||||||
|
*
|
||||||
|
* @param manga the manga.
|
||||||
|
* @return whether the cover was deleted.
|
||||||
|
*/
|
||||||
|
fun deleteCustomCover(manga: Manga): Boolean {
|
||||||
|
return getCustomCoverFile(manga).let {
|
||||||
|
it.exists() && it.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCacheDir(dir: String): File {
|
||||||
|
return context.getExternalFilesDir(dir)
|
||||||
|
?: File(context.filesDir, dir).also { it.mkdirs() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 9
|
const val DATABASE_VERSION = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@ -75,6 +75,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
db.execSQL(TrackTable.addStartDate)
|
db.execSQL(TrackTable.addStartDate)
|
||||||
db.execSQL(TrackTable.addFinishDate)
|
db.execSQL(TrackTable.addFinishDate)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 10) {
|
||||||
|
db.execSQL(MangaTable.addCoverLastModified)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFIED
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||||
@ -62,6 +63,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
put(COL_INITIALIZED, obj.initialized)
|
put(COL_INITIALIZED, obj.initialized)
|
||||||
put(COL_VIEWER, obj.viewer)
|
put(COL_VIEWER, obj.viewer)
|
||||||
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
||||||
|
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +84,7 @@ interface BaseMangaGetResolver {
|
|||||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
||||||
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||||
|
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
var chapter_flags: Int
|
var chapter_flags: Int
|
||||||
|
|
||||||
|
var cover_last_modified: Long
|
||||||
|
|
||||||
fun setChapterOrder(order: Int) {
|
fun setChapterOrder(order: Int) {
|
||||||
setFlags(order, SORT_MASK)
|
setFlags(order, SORT_MASK)
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,8 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var chapter_flags: Int = 0
|
override var chapter_flags: Int = 0
|
||||||
|
|
||||||
|
override var cover_last_modified: Long = 0
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.DbProvider
|
|||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||||
@ -102,6 +103,11 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaTitlePutResolver())
|
.withPutResolver(MangaTitlePutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun updateMangaCoverLastModified(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaCoverLastModifiedPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||||
|
|
||||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
|
class MangaCoverLastModifiedPutResolver : PutResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
|
val contentValues = mapToContentValues(manga)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||||
|
put(MangaTable.COL_COVER_LAST_MODIFIED, manga.cover_last_modified)
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,8 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_CATEGORY = "category"
|
const val COL_CATEGORY = "category"
|
||||||
|
|
||||||
|
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
|
||||||
|
|
||||||
val createTableQuery: String
|
val createTableQuery: String
|
||||||
get() =
|
get() =
|
||||||
"""CREATE TABLE $TABLE(
|
"""CREATE TABLE $TABLE(
|
||||||
@ -55,7 +57,8 @@ object MangaTable {
|
|||||||
$COL_LAST_UPDATE LONG,
|
$COL_LAST_UPDATE LONG,
|
||||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||||
$COL_VIEWER INTEGER NOT NULL,
|
$COL_VIEWER INTEGER NOT NULL,
|
||||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL
|
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
||||||
|
$COL_COVER_LAST_MODIFIED LONG NOT NULL
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
val createUrlIndexQuery: String
|
val createUrlIndexQuery: String
|
||||||
@ -64,4 +67,7 @@ object MangaTable {
|
|||||||
val createLibraryIndexQuery: String
|
val createLibraryIndexQuery: String
|
||||||
get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
|
get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
|
||||||
"WHERE $COL_FAVORITE = 1"
|
"WHERE $COL_FAVORITE = 1"
|
||||||
|
|
||||||
|
val addCoverLastModified: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_COVER_LAST_MODIFIED LONG NOT NULL DEFAULT 0"
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import java.io.IOException
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
|
||||||
|
|
||||||
private var data: InputStream? = null
|
private var data: InputStream? = null
|
||||||
|
|
||||||
@ -20,7 +20,11 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
|||||||
loadFromFile(callback)
|
loadFromFile(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
private fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
|
loadFromFile(File(filePath), callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun loadFromFile(file: File, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
try {
|
try {
|
||||||
data = FileInputStream(file)
|
data = FileInputStream(file)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
|
import com.bumptech.glide.Priority
|
||||||
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
open class LibraryMangaCustomCoverFetcher(
|
||||||
|
private val manga: Manga,
|
||||||
|
private val coverCache: CoverCache
|
||||||
|
) : FileFetcher() {
|
||||||
|
|
||||||
|
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
|
getCustomCoverFile()?.let {
|
||||||
|
loadFromFile(it, callback)
|
||||||
|
} ?: callback.onLoadFailed(Exception("Custom cover file not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getCustomCoverFile(): File? {
|
||||||
|
return coverCache.getCustomCoverFile(manga).takeIf { it.exists() }
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.glide
|
|||||||
|
|
||||||
import com.bumptech.glide.Priority
|
import com.bumptech.glide.Priority
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
@ -19,31 +20,41 @@ import java.io.InputStream
|
|||||||
class LibraryMangaUrlFetcher(
|
class LibraryMangaUrlFetcher(
|
||||||
private val networkFetcher: DataFetcher<InputStream>,
|
private val networkFetcher: DataFetcher<InputStream>,
|
||||||
private val manga: Manga,
|
private val manga: Manga,
|
||||||
private val file: File
|
private val coverCache: CoverCache
|
||||||
) :
|
) : LibraryMangaCustomCoverFetcher(manga, coverCache) {
|
||||||
FileFetcher(file) {
|
|
||||||
|
|
||||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
||||||
if (!file.exists()) {
|
getCustomCoverFile()?.let {
|
||||||
|
loadFromFile(it, callback)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val cover = coverCache.getCoverFile(manga)
|
||||||
|
if (cover == null) {
|
||||||
|
callback.onLoadFailed(Exception("Null thumbnail url"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cover.exists()) {
|
||||||
networkFetcher.loadData(
|
networkFetcher.loadData(
|
||||||
priority,
|
priority,
|
||||||
object : DataFetcher.DataCallback<InputStream> {
|
object : DataFetcher.DataCallback<InputStream> {
|
||||||
override fun onDataReady(data: InputStream?) {
|
override fun onDataReady(data: InputStream?) {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
val tmpFile = File(file.path + ".tmp")
|
val tmpFile = File(cover.path + ".tmp")
|
||||||
try {
|
try {
|
||||||
// Retrieve destination stream, create parent folders if needed.
|
// Retrieve destination stream, create parent folders if needed.
|
||||||
val output = try {
|
val output = try {
|
||||||
tmpFile.outputStream()
|
tmpFile.outputStream()
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
tmpFile.parentFile.mkdirs()
|
tmpFile.parentFile!!.mkdirs()
|
||||||
tmpFile.outputStream()
|
tmpFile.outputStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the file and rename to the original.
|
// Copy the file and rename to the original.
|
||||||
data.use { output.use { data.copyTo(output) } }
|
data.use { output.use { data.copyTo(output) } }
|
||||||
tmpFile.renameTo(file)
|
tmpFile.renameTo(cover)
|
||||||
loadFromFile(callback)
|
loadFromFile(cover, callback)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
tmpFile.delete()
|
tmpFile.delete()
|
||||||
callback.onLoadFailed(e)
|
callback.onLoadFailed(e)
|
||||||
@ -59,7 +70,7 @@ class LibraryMangaUrlFetcher(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
loadFromFile(callback)
|
loadFromFile(cover, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import com.bumptech.glide.load.Key
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import java.io.File
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
class MangaSignature(manga: Manga, file: File) : Key {
|
|
||||||
|
|
||||||
private val key = manga.thumbnail_url + file.lastModified()
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
return if (other is MangaSignature) {
|
|
||||||
key == other.key
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return key.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateDiskCacheKey(md: MessageDigest) {
|
|
||||||
md.update(key.toByteArray(Key.CHARSET))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
|
import com.bumptech.glide.load.Key
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
data class MangaThumbnail(val manga: Manga, val url: String?)
|
data class MangaThumbnail(val manga: Manga, val coverLastModified: Long) : Key {
|
||||||
|
val key = manga.url + coverLastModified
|
||||||
|
|
||||||
fun Manga.toMangaThumbnail() = MangaThumbnail(this, this.thumbnail_url)
|
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||||
|
messageDigest.update(key.toByteArray(Key.CHARSET))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Manga.toMangaThumbnail() = MangaThumbnail(this, cover_last_modified)
|
||||||
|
@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import java.io.File
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -48,12 +48,6 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
|||||||
*/
|
*/
|
||||||
private val defaultClient = Injekt.get<NetworkHelper>().client
|
private val defaultClient = Injekt.get<NetworkHelper>().client
|
||||||
|
|
||||||
/**
|
|
||||||
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
|
|
||||||
* and the file where it should be stored in case the manga is a favorite.
|
|
||||||
*/
|
|
||||||
private val lruCache = LruCache<GlideUrl, File>(100)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map where request headers are stored for a source.
|
* Map where request headers are stored for a source.
|
||||||
*/
|
*/
|
||||||
@ -78,7 +72,7 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
|||||||
/**
|
/**
|
||||||
* Returns a fetcher for the given manga or null if the url is empty.
|
* Returns a fetcher for the given manga or null if the url is empty.
|
||||||
*
|
*
|
||||||
* @param manga the model.
|
* @param mangaThumbnail the model.
|
||||||
* @param width the width of the view where the resource will be loaded.
|
* @param width the width of the view where the resource will be loaded.
|
||||||
* @param height the height of the view where the resource will be loaded.
|
* @param height the height of the view where the resource will be loaded.
|
||||||
*/
|
*/
|
||||||
@ -88,13 +82,16 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
|||||||
height: Int,
|
height: Int,
|
||||||
options: Options
|
options: Options
|
||||||
): ModelLoader.LoadData<InputStream>? {
|
): ModelLoader.LoadData<InputStream>? {
|
||||||
// Check thumbnail is not null or empty
|
|
||||||
val url = mangaThumbnail.url
|
|
||||||
if (url == null || url.isEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val manga = mangaThumbnail.manga
|
val manga = mangaThumbnail.manga
|
||||||
|
val url = manga.thumbnail_url
|
||||||
|
|
||||||
|
if (url.isNullOrEmpty()) {
|
||||||
|
return if (!manga.favorite || manga.isLocal()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
ModelLoader.LoadData(mangaThumbnail, LibraryMangaCustomCoverFetcher(manga, coverCache))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (url.startsWith("http", true)) {
|
if (url.startsWith("http", true)) {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
@ -107,19 +104,13 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
|||||||
return ModelLoader.LoadData(glideUrl, networkFetcher)
|
return ModelLoader.LoadData(glideUrl, networkFetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain the file for this url from the LRU cache, or retrieve and add it to the cache.
|
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, coverCache)
|
||||||
val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) }
|
|
||||||
|
|
||||||
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file)
|
|
||||||
|
|
||||||
// Return an instance of the fetcher providing the needed elements.
|
// Return an instance of the fetcher providing the needed elements.
|
||||||
return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
|
return ModelLoader.LoadData(mangaThumbnail, libraryFetcher)
|
||||||
} else {
|
} else {
|
||||||
// Get the file from the url, removing the scheme if present.
|
|
||||||
val file = File(url.substringAfter("file://"))
|
|
||||||
|
|
||||||
// Return an instance of the fetcher providing the needed elements.
|
// Return an instance of the fetcher providing the needed elements.
|
||||||
return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
|
return ModelLoader.LoadData(mangaThumbnail, FileFetcher(url.removePrefix("file://")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY
|
|||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
@ -31,10 +32,10 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.lang.chop
|
import eu.kanade.tachiyomi.util.lang.chop
|
||||||
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
@ -64,7 +65,8 @@ class LibraryUpdateService(
|
|||||||
val sourceManager: SourceManager = Injekt.get(),
|
val sourceManager: SourceManager = Injekt.get(),
|
||||||
val preferences: PreferencesHelper = Injekt.get(),
|
val preferences: PreferencesHelper = Injekt.get(),
|
||||||
val downloadManager: DownloadManager = Injekt.get(),
|
val downloadManager: DownloadManager = Injekt.get(),
|
||||||
val trackManager: TrackManager = Injekt.get()
|
val trackManager: TrackManager = Injekt.get(),
|
||||||
|
val coverCache: CoverCache = Injekt.get()
|
||||||
) : Service() {
|
) : Service() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -110,6 +112,7 @@ class LibraryUpdateService(
|
|||||||
*/
|
*/
|
||||||
enum class Target {
|
enum class Target {
|
||||||
CHAPTERS, // Manga chapters
|
CHAPTERS, // Manga chapters
|
||||||
|
COVERS, // Manga covers
|
||||||
TRACKING // Tracking metadata
|
TRACKING // Tracking metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,6 +236,7 @@ class LibraryUpdateService(
|
|||||||
// Update either chapter list or manga details.
|
// Update either chapter list or manga details.
|
||||||
when (target) {
|
when (target) {
|
||||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||||
|
Target.COVERS -> updateCovers(mangaList)
|
||||||
Target.TRACKING -> updateTrackings(mangaList)
|
Target.TRACKING -> updateTrackings(mangaList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -387,11 +391,14 @@ class LibraryUpdateService(
|
|||||||
* @return a pair of the inserted and removed chapters.
|
* @return a pair of the inserted and removed chapters.
|
||||||
*/
|
*/
|
||||||
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
|
val source = sourceManager.get(manga.source) ?: return Observable.empty()
|
||||||
|
|
||||||
// Update manga details metadata in the background
|
// Update manga details metadata in the background
|
||||||
source.fetchMangaDetails(manga)
|
source.fetchMangaDetails(manga)
|
||||||
.map { networkManga ->
|
.map { networkManga ->
|
||||||
|
if (manga.thumbnail_url != networkManga.thumbnail_url) {
|
||||||
|
manga.prepUpdateCover(coverCache)
|
||||||
|
}
|
||||||
manga.copyFrom(networkManga)
|
manga.copyFrom(networkManga)
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
manga
|
manga
|
||||||
@ -404,6 +411,21 @@ class LibraryUpdateService(
|
|||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateCovers(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||||
|
var count = 0
|
||||||
|
|
||||||
|
return Observable.from(mangaToUpdate)
|
||||||
|
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
|
||||||
|
.map { manga ->
|
||||||
|
manga.prepUpdateCover(coverCache)
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
.doOnCompleted {
|
||||||
|
cancelProgressNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method that updates the metadata of the connected tracking services. It's called in a
|
* Method that updates the metadata of the connected tracking services. It's called in a
|
||||||
* background thread, so it's safe to do heavy operations or network calls here.
|
* background thread, so it's safe to do heavy operations or network calls here.
|
||||||
|
@ -28,6 +28,7 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TextItem
|
|||||||
import eu.kanade.tachiyomi.ui.browse.source.filter.TextSectionItem
|
import eu.kanade.tachiyomi.ui.browse.source.filter.TextSectionItem
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem
|
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
|
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
|
||||||
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@ -279,7 +280,7 @@ open class BrowseSourcePresenter(
|
|||||||
fun changeMangaFavorite(manga: Manga) {
|
fun changeMangaFavorite(manga: Manga) {
|
||||||
manga.favorite = !manga.favorite
|
manga.favorite = !manga.favorite
|
||||||
if (!manga.favorite) {
|
if (!manga.favorite) {
|
||||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
manga.removeCovers(coverCache)
|
||||||
}
|
}
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
|
||||||
|
class ChangeMangaCoverDialog<T>(bundle: Bundle? = null) :
|
||||||
|
DialogController(bundle) where T : Controller, T : ChangeMangaCoverDialog.Listener {
|
||||||
|
|
||||||
|
private lateinit var manga: Manga
|
||||||
|
|
||||||
|
constructor(target: T, manga: Manga) : this() {
|
||||||
|
targetController = target
|
||||||
|
this.manga = manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
|
return MaterialDialog(activity!!)
|
||||||
|
.title(R.string.action_edit_cover)
|
||||||
|
.positiveButton(R.string.action_edit) {
|
||||||
|
(targetController as? Listener)?.openMangaCoverPicker(manga)
|
||||||
|
}
|
||||||
|
.negativeButton(android.R.string.cancel)
|
||||||
|
.neutralButton(R.string.action_delete) {
|
||||||
|
(targetController as? Listener)?.deleteMangaCover(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun deleteMangaCover(manga: Manga)
|
||||||
|
|
||||||
|
fun openMangaCoverPicker(manga: Manga)
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.library
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@ -22,6 +21,7 @@ import com.google.android.material.tabs.TabLayout
|
|||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
@ -37,7 +37,6 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
|
|||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
import eu.kanade.tachiyomi.util.view.visible
|
||||||
import java.io.IOException
|
|
||||||
import kotlinx.android.synthetic.main.main_activity.tabs
|
import kotlinx.android.synthetic.main.main_activity.tabs
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@ -51,11 +50,13 @@ import uy.kohesive.injekt.api.get
|
|||||||
|
|
||||||
class LibraryController(
|
class LibraryController(
|
||||||
bundle: Bundle? = null,
|
bundle: Bundle? = null,
|
||||||
private val preferences: PreferencesHelper = Injekt.get()
|
private val preferences: PreferencesHelper = Injekt.get(),
|
||||||
|
private val coverCache: CoverCache = Injekt.get()
|
||||||
) : NucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
|
) : NucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
|
||||||
RootController,
|
RootController,
|
||||||
TabbedController,
|
TabbedController,
|
||||||
ActionMode.Callback,
|
ActionMode.Callback,
|
||||||
|
ChangeMangaCoverDialog.Listener,
|
||||||
ChangeMangaCategoriesDialog.Listener,
|
ChangeMangaCategoriesDialog.Listener,
|
||||||
DeleteLibraryMangasDialog.Listener {
|
DeleteLibraryMangasDialog.Listener {
|
||||||
|
|
||||||
@ -424,10 +425,7 @@ class LibraryController(
|
|||||||
|
|
||||||
private fun onActionItemClicked(item: MenuItem): Boolean {
|
private fun onActionItemClicked(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_edit_cover -> {
|
R.id.action_edit_cover -> handleChangeCover()
|
||||||
changeSelectedCover()
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
}
|
|
||||||
R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
|
R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
|
||||||
R.id.action_delete -> showDeleteMangaDialog()
|
R.id.action_delete -> showDeleteMangaDialog()
|
||||||
R.id.action_select_all -> selectAllCategoryManga()
|
R.id.action_select_all -> selectAllCategoryManga()
|
||||||
@ -486,6 +484,23 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleChangeCover() {
|
||||||
|
val manga = selectedMangas.firstOrNull() ?: return
|
||||||
|
|
||||||
|
if (coverCache.getCustomCoverFile(manga).exists()) {
|
||||||
|
showEditCoverDialog(manga)
|
||||||
|
} else {
|
||||||
|
openMangaCoverPicker(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit custom cover for selected manga.
|
||||||
|
*/
|
||||||
|
private fun showEditCoverDialog(manga: Manga) {
|
||||||
|
ChangeMangaCoverDialog(this, manga).showDialog(router)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the selected manga to a list of categories.
|
* Move the selected manga to a list of categories.
|
||||||
*/
|
*/
|
||||||
@ -509,21 +524,7 @@ class LibraryController(
|
|||||||
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
|
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
override fun openMangaCoverPicker(manga: Manga) {
|
||||||
presenter.moveMangasToCategories(categories, mangas)
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
|
|
||||||
presenter.removeMangaFromLibrary(mangas, deleteChapters)
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the cover for the selected manga.
|
|
||||||
*/
|
|
||||||
private fun changeSelectedCover() {
|
|
||||||
val manga = selectedMangas.firstOrNull() ?: return
|
|
||||||
selectedCoverManga = manga
|
selectedCoverManga = manga
|
||||||
|
|
||||||
if (manga.favorite) {
|
if (manga.favorite) {
|
||||||
@ -539,6 +540,23 @@ class LibraryController(
|
|||||||
} else {
|
} else {
|
||||||
activity?.toast(R.string.notification_first_add_to_library)
|
activity?.toast(R.string.notification_first_add_to_library)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroyActionModeIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteMangaCover(manga: Manga) {
|
||||||
|
presenter.deleteCustomCover(manga)
|
||||||
|
destroyActionModeIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
||||||
|
presenter.moveMangasToCategories(categories, mangas)
|
||||||
|
destroyActionModeIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
|
||||||
|
presenter.removeMangaFromLibrary(mangas, deleteChapters)
|
||||||
|
destroyActionModeIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selectAllCategoryManga() {
|
private fun selectAllCategoryManga() {
|
||||||
@ -555,28 +573,25 @@ class LibraryController(
|
|||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
if (requestCode == REQUEST_IMAGE_OPEN) {
|
if (requestCode == REQUEST_IMAGE_OPEN) {
|
||||||
if (data == null || resultCode != Activity.RESULT_OK) return
|
val dataUri = data?.data
|
||||||
|
if (dataUri == null || resultCode != Activity.RESULT_OK) return
|
||||||
val activity = activity ?: return
|
val activity = activity ?: return
|
||||||
val manga = selectedCoverManga ?: return
|
val manga = selectedCoverManga ?: return
|
||||||
|
|
||||||
try {
|
|
||||||
// Get the file's input stream from the incoming Intent
|
|
||||||
activity.contentResolver.openInputStream(data.data ?: Uri.EMPTY).use {
|
|
||||||
// Update cover to selected file, show error if something went wrong
|
|
||||||
if (it != null && presenter.editCoverWithStream(it, manga)) {
|
|
||||||
// TODO refresh cover
|
|
||||||
} else {
|
|
||||||
activity.toast(R.string.notification_cover_update_failed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: IOException) {
|
|
||||||
activity.toast(R.string.notification_cover_update_failed)
|
|
||||||
Timber.e(error)
|
|
||||||
}
|
|
||||||
selectedCoverManga = null
|
selectedCoverManga = null
|
||||||
|
presenter.editCover(manga, activity, dataUri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onSetCoverSuccess() {
|
||||||
|
activity?.toast(R.string.cover_updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSetCoverError(error: Throwable) {
|
||||||
|
activity?.toast(R.string.notification_cover_update_failed)
|
||||||
|
Timber.e(error)
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
/**
|
/**
|
||||||
* Key to change the cover of a manga in [onActivityResult].
|
* Key to change the cover of a manga in [onActivityResult].
|
||||||
|
@ -5,7 +5,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
import eu.kanade.tachiyomi.util.view.visibleIf
|
import eu.kanade.tachiyomi.util.view.visibleIf
|
||||||
import kotlinx.android.synthetic.main.source_grid_item.download_text
|
import kotlinx.android.synthetic.main.source_grid_item.download_text
|
||||||
import kotlinx.android.synthetic.main.source_grid_item.local_text
|
import kotlinx.android.synthetic.main.source_grid_item.local_text
|
||||||
@ -48,7 +48,7 @@ class LibraryGridHolder(
|
|||||||
text = item.downloadCount.toString()
|
text = item.downloadCount.toString()
|
||||||
}
|
}
|
||||||
// set local visibility if its local manga
|
// set local visibility if its local manga
|
||||||
local_text.visibleIf { item.manga.source == LocalSource.ID }
|
local_text.visibleIf { item.manga.isLocal() }
|
||||||
|
|
||||||
// Update the cover.
|
// Update the cover.
|
||||||
GlideApp.with(view.context).clear(thumbnail)
|
GlideApp.with(view.context).clear(thumbnail)
|
||||||
|
@ -5,7 +5,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
import eu.kanade.tachiyomi.util.view.visibleIf
|
import eu.kanade.tachiyomi.util.view.visibleIf
|
||||||
import kotlinx.android.synthetic.main.source_list_item.download_text
|
import kotlinx.android.synthetic.main.source_list_item.download_text
|
||||||
import kotlinx.android.synthetic.main.source_list_item.local_text
|
import kotlinx.android.synthetic.main.source_list_item.local_text
|
||||||
@ -49,7 +49,7 @@ class LibraryListHolder(
|
|||||||
text = "${item.downloadCount}"
|
text = "${item.downloadCount}"
|
||||||
}
|
}
|
||||||
// show local text badge if local manga
|
// show local text badge if local manga
|
||||||
local_text.visibleIf { item.manga.source == LocalSource.ID }
|
local_text.visibleIf { item.manga.isLocal() }
|
||||||
|
|
||||||
// Create thumbnail onclick to simulate long click
|
// Create thumbnail onclick to simulate long click
|
||||||
thumbnail.setOnClickListener {
|
thumbnail.setOnClickListener {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
@ -14,11 +16,12 @@ import eu.kanade.tachiyomi.source.SourceManager
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
import eu.kanade.tachiyomi.util.lang.combineLatest
|
import eu.kanade.tachiyomi.util.lang.combineLatest
|
||||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import java.io.IOException
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
import java.io.InputStream
|
import eu.kanade.tachiyomi.util.updateCoverLastModified
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.Comparator
|
import java.util.Comparator
|
||||||
@ -128,7 +131,7 @@ class LibraryPresenter(
|
|||||||
// Filter when there are no downloads.
|
// Filter when there are no downloads.
|
||||||
if (filterDownloaded) {
|
if (filterDownloaded) {
|
||||||
// Local manga are always downloaded
|
// Local manga are always downloaded
|
||||||
if (item.manga.source == LocalSource.ID) {
|
if (item.manga.isLocal()) {
|
||||||
return@f true
|
return@f true
|
||||||
}
|
}
|
||||||
// Don't bother with directory checking if download count has been set.
|
// Don't bother with directory checking if download count has been set.
|
||||||
@ -318,16 +321,17 @@ class LibraryPresenter(
|
|||||||
* @param deleteChapters whether to also delete downloaded chapters.
|
* @param deleteChapters whether to also delete downloaded chapters.
|
||||||
*/
|
*/
|
||||||
fun removeMangaFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
|
fun removeMangaFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) {
|
||||||
// Create a set of the list
|
|
||||||
val mangaToDelete = mangas.distinctBy { it.id }
|
|
||||||
mangaToDelete.forEach { it.favorite = false }
|
|
||||||
|
|
||||||
launchIO {
|
launchIO {
|
||||||
|
val mangaToDelete = mangas.distinctBy { it.id }
|
||||||
|
|
||||||
|
mangaToDelete.forEach {
|
||||||
|
it.favorite = false
|
||||||
|
it.removeCovers(coverCache)
|
||||||
|
}
|
||||||
db.insertMangas(mangaToDelete).executeAsBlocking()
|
db.insertMangas(mangaToDelete).executeAsBlocking()
|
||||||
|
|
||||||
mangaToDelete.forEach { manga ->
|
if (deleteChapters) {
|
||||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
mangaToDelete.forEach { manga ->
|
||||||
if (deleteChapters) {
|
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
downloadManager.deleteManga(manga, source)
|
downloadManager.deleteManga(manga, source)
|
||||||
@ -358,21 +362,42 @@ class LibraryPresenter(
|
|||||||
/**
|
/**
|
||||||
* Update cover with local file.
|
* Update cover with local file.
|
||||||
*
|
*
|
||||||
* @param inputStream the new cover.
|
|
||||||
* @param manga the manga edited.
|
* @param manga the manga edited.
|
||||||
* @return true if the cover is updated, false otherwise
|
* @param context Context.
|
||||||
|
* @param data uri of the cover resource.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
fun editCover(manga: Manga, context: Context, data: Uri) {
|
||||||
fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean {
|
Observable
|
||||||
if (manga.source == LocalSource.ID) {
|
.fromCallable {
|
||||||
LocalSource.updateCover(context, manga, inputStream)
|
context.contentResolver.openInputStream(data)?.use {
|
||||||
return true
|
if (manga.isLocal()) {
|
||||||
}
|
LocalSource.updateCover(context, manga, it)
|
||||||
|
manga.updateCoverLastModified(db)
|
||||||
|
} else if (manga.favorite) {
|
||||||
|
coverCache.setCustomCoverToCache(manga, it)
|
||||||
|
manga.updateCoverLastModified(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeFirst(
|
||||||
|
{ view, _ -> view.onSetCoverSuccess() },
|
||||||
|
{ view, e -> view.onSetCoverError(e) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (manga.thumbnail_url != null && manga.favorite) {
|
fun deleteCustomCover(manga: Manga) {
|
||||||
coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
|
Observable
|
||||||
return true
|
.fromCallable {
|
||||||
}
|
coverCache.deleteCustomCover(manga)
|
||||||
return false
|
manga.updateCoverLastModified(db)
|
||||||
|
}
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeFirst(
|
||||||
|
{ view, _ -> view.onSetCoverSuccess() },
|
||||||
|
{ view, e -> view.onSetCoverError(e) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@ -189,7 +189,7 @@ class ChaptersPresenter(
|
|||||||
observable = observable.filter { it.read }
|
observable = observable.filter { it.read }
|
||||||
}
|
}
|
||||||
if (onlyDownloaded()) {
|
if (onlyDownloaded()) {
|
||||||
observable = observable.filter { it.isDownloaded || it.manga.source == LocalSource.ID }
|
observable = observable.filter { it.isDownloaded || it.manga.isLocal() }
|
||||||
}
|
}
|
||||||
if (onlyBookmarked()) {
|
if (onlyBookmarked()) {
|
||||||
observable = observable.filter { it.bookmark }
|
observable = observable.filter { it.bookmark }
|
||||||
|
@ -67,8 +67,6 @@ class MangaInfoController(private val fromSource: Boolean = false) :
|
|||||||
|
|
||||||
private var initialLoad: Boolean = true
|
private var initialLoad: Boolean = true
|
||||||
|
|
||||||
private var thumbnailUrl: String? = null
|
|
||||||
|
|
||||||
override fun createPresenter(): MangaInfoPresenter {
|
override fun createPresenter(): MangaInfoPresenter {
|
||||||
val ctrl = parentController as MangaController
|
val ctrl = parentController as MangaController
|
||||||
return MangaInfoPresenter(
|
return MangaInfoPresenter(
|
||||||
@ -113,7 +111,7 @@ class MangaInfoController(private val fromSource: Boolean = false) :
|
|||||||
|
|
||||||
// Set SwipeRefresh to refresh manga data.
|
// Set SwipeRefresh to refresh manga data.
|
||||||
binding.swipeRefresh.refreshes()
|
binding.swipeRefresh.refreshes()
|
||||||
.onEach { fetchMangaFromSource() }
|
.onEach { fetchMangaFromSource(manualFetch = true) }
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
binding.mangaFullTitle.longClicks()
|
binding.mangaFullTitle.longClicks()
|
||||||
@ -241,23 +239,20 @@ class MangaInfoController(private val fromSource: Boolean = false) :
|
|||||||
setFavoriteButtonState(manga.favorite)
|
setFavoriteButtonState(manga.favorite)
|
||||||
|
|
||||||
// Set cover if it wasn't already.
|
// Set cover if it wasn't already.
|
||||||
if (binding.mangaCover.drawable == null || manga.thumbnail_url != thumbnailUrl) {
|
val mangaThumbnail = manga.toMangaThumbnail()
|
||||||
thumbnailUrl = manga.thumbnail_url
|
|
||||||
val mangaThumbnail = manga.toMangaThumbnail()
|
|
||||||
|
|
||||||
|
GlideApp.with(view.context)
|
||||||
|
.load(mangaThumbnail)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
|
.centerCrop()
|
||||||
|
.into(binding.mangaCover)
|
||||||
|
|
||||||
|
binding.backdrop?.let {
|
||||||
GlideApp.with(view.context)
|
GlideApp.with(view.context)
|
||||||
.load(mangaThumbnail)
|
.load(mangaThumbnail)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.into(binding.mangaCover)
|
.into(it)
|
||||||
|
|
||||||
if (binding.backdrop != null) {
|
|
||||||
GlideApp.with(view.context)
|
|
||||||
.load(mangaThumbnail)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
|
||||||
.centerCrop()
|
|
||||||
.into(binding.backdrop!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manga info section
|
// Manga info section
|
||||||
@ -422,10 +417,10 @@ class MangaInfoController(private val fromSource: Boolean = false) :
|
|||||||
/**
|
/**
|
||||||
* Start fetching manga information from source.
|
* Start fetching manga information from source.
|
||||||
*/
|
*/
|
||||||
private fun fetchMangaFromSource() {
|
private fun fetchMangaFromSource(manualFetch: Boolean = false) {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
// Call presenter and start fetching manga information
|
// Call presenter and start fetching manga information
|
||||||
presenter.fetchMangaFromSource()
|
presenter.fetchMangaFromSource(manualFetch)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,6 +12,8 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
||||||
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
@ -36,11 +38,6 @@ class MangaInfoPresenter(
|
|||||||
private val coverCache: CoverCache = Injekt.get()
|
private val coverCache: CoverCache = Injekt.get()
|
||||||
) : BasePresenter<MangaInfoController>() {
|
) : BasePresenter<MangaInfoController>() {
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription to send the manga to the view.
|
|
||||||
*/
|
|
||||||
private var viewMangaSubscription: Subscription? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription to update the manga from the source.
|
* Subscription to update the manga from the source.
|
||||||
*/
|
*/
|
||||||
@ -48,7 +45,9 @@ class MangaInfoPresenter(
|
|||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
sendMangaToView()
|
|
||||||
|
getMangaObservable()
|
||||||
|
.subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
|
||||||
|
|
||||||
// Update chapter count
|
// Update chapter count
|
||||||
chapterCountRelay.observeOn(AndroidSchedulers.mainThread())
|
chapterCountRelay.observeOn(AndroidSchedulers.mainThread())
|
||||||
@ -64,22 +63,21 @@ class MangaInfoPresenter(
|
|||||||
.subscribeLatestCache(MangaInfoController::setLastUpdateDate)
|
.subscribeLatestCache(MangaInfoController::setLastUpdateDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun getMangaObservable(): Observable<Manga> {
|
||||||
* Sends the active manga to the view.
|
return db.getManga(manga.url, manga.source).asRxObservable()
|
||||||
*/
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
fun sendMangaToView() {
|
|
||||||
viewMangaSubscription?.let { remove(it) }
|
|
||||||
viewMangaSubscription = Observable.just(manga)
|
|
||||||
.subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch manga information from source.
|
* Fetch manga information from source.
|
||||||
*/
|
*/
|
||||||
fun fetchMangaFromSource() {
|
fun fetchMangaFromSource(manualFetch: Boolean = false) {
|
||||||
if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
|
if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
|
||||||
fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
|
fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
|
||||||
.map { networkManga ->
|
.map { networkManga ->
|
||||||
|
if (manualFetch || manga.thumbnail_url != networkManga.thumbnail_url) {
|
||||||
|
manga.prepUpdateCover(coverCache)
|
||||||
|
}
|
||||||
manga.copyFrom(networkManga)
|
manga.copyFrom(networkManga)
|
||||||
manga.initialized = true
|
manga.initialized = true
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
@ -87,7 +85,6 @@ class MangaInfoPresenter(
|
|||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnNext { sendMangaToView() }
|
|
||||||
.subscribeFirst(
|
.subscribeFirst(
|
||||||
{ view, _ ->
|
{ view, _ ->
|
||||||
view.onFetchMangaDone()
|
view.onFetchMangaDone()
|
||||||
@ -104,10 +101,9 @@ class MangaInfoPresenter(
|
|||||||
fun toggleFavorite(): Boolean {
|
fun toggleFavorite(): Boolean {
|
||||||
manga.favorite = !manga.favorite
|
manga.favorite = !manga.favorite
|
||||||
if (!manga.favorite) {
|
if (!manga.favorite) {
|
||||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
manga.removeCovers(coverCache)
|
||||||
}
|
}
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
sendMangaToView()
|
|
||||||
return manga.favorite
|
return manga.favorite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,11 +21,13 @@ import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
|
|||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||||
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
import eu.kanade.tachiyomi.util.lang.byteSize
|
import eu.kanade.tachiyomi.util.lang.byteSize
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.takeBytes
|
import eu.kanade.tachiyomi.util.lang.takeBytes
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import eu.kanade.tachiyomi.util.updateCoverLastModified
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -565,15 +567,16 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
Observable
|
Observable
|
||||||
.fromCallable {
|
.fromCallable {
|
||||||
if (manga.source == LocalSource.ID) {
|
if (manga.isLocal()) {
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
LocalSource.updateCover(context, manga, stream())
|
LocalSource.updateCover(context, manga, stream())
|
||||||
|
manga.updateCoverLastModified(db)
|
||||||
R.string.cover_updated
|
R.string.cover_updated
|
||||||
SetAsCoverResult.Success
|
SetAsCoverResult.Success
|
||||||
} else {
|
} else {
|
||||||
val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found")
|
|
||||||
if (manga.favorite) {
|
if (manga.favorite) {
|
||||||
coverCache.copyToCache(thumbUrl, stream())
|
coverCache.setCustomCoverToCache(manga, stream())
|
||||||
|
manga.updateCoverLastModified(db)
|
||||||
SetAsCoverResult.Success
|
SetAsCoverResult.Success
|
||||||
} else {
|
} else {
|
||||||
SetAsCoverResult.AddToLibraryFirst
|
SetAsCoverResult.AddToLibraryFirst
|
||||||
|
@ -64,12 +64,10 @@ class HistoryHolder(
|
|||||||
|
|
||||||
// Set cover
|
// Set cover
|
||||||
GlideApp.with(itemView.context).clear(cover)
|
GlideApp.with(itemView.context).clear(cover)
|
||||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
GlideApp.with(itemView.context)
|
||||||
GlideApp.with(itemView.context)
|
.load(manga.toMangaThumbnail())
|
||||||
.load(manga.toMangaThumbnail())
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
.centerCrop()
|
||||||
.centerCrop()
|
.into(cover)
|
||||||
.into(cover)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,13 +56,11 @@ class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter)
|
|||||||
|
|
||||||
// Set cover
|
// Set cover
|
||||||
GlideApp.with(itemView.context).clear(manga_cover)
|
GlideApp.with(itemView.context).clear(manga_cover)
|
||||||
if (!item.manga.thumbnail_url.isNullOrEmpty()) {
|
GlideApp.with(itemView.context)
|
||||||
GlideApp.with(itemView.context)
|
.load(item.manga.toMangaThumbnail())
|
||||||
.load(item.manga.toMangaThumbnail())
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
.circleCrop()
|
||||||
.circleCrop()
|
.into(manga_cover)
|
||||||
.into(manga_cover)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if chapter is read and set correct color
|
// Check if chapter is read and set correct color
|
||||||
if (item.chapter.read) {
|
if (item.chapter.read) {
|
||||||
|
@ -76,6 +76,11 @@ class SettingsAdvancedController : SettingsController() {
|
|||||||
ctrl.showDialog(router)
|
ctrl.showDialog(router)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
preference {
|
||||||
|
titleRes = R.string.pref_refresh_library_covers
|
||||||
|
|
||||||
|
onClick { LibraryUpdateService.start(context, target = Target.COVERS) }
|
||||||
|
}
|
||||||
preference {
|
preference {
|
||||||
titleRes = R.string.pref_refresh_library_tracking
|
titleRes = R.string.pref_refresh_library_tracking
|
||||||
summaryRes = R.string.pref_refresh_library_tracking_summary
|
summaryRes = R.string.pref_refresh_library_tracking_summary
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
package eu.kanade.tachiyomi.util
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
fun Manga.isLocal() = source == LocalSource.ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call before updating [Manga.thumbnail_url] to ensure old cover can be cleared from cache
|
||||||
|
*/
|
||||||
|
fun Manga.prepUpdateCover(coverCache: CoverCache) {
|
||||||
|
cover_last_modified = Date().time
|
||||||
|
|
||||||
|
if (!isLocal()) {
|
||||||
|
coverCache.deleteFromCache(this, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Manga.removeCovers(coverCache: CoverCache) {
|
||||||
|
if (isLocal()) return
|
||||||
|
|
||||||
|
cover_last_modified = Date().time
|
||||||
|
coverCache.deleteFromCache(this, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Manga.updateCoverLastModified(db: DatabaseHelper) {
|
||||||
|
cover_last_modified = Date().time
|
||||||
|
db.updateMangaCoverLastModified(this).executeAsBlocking()
|
||||||
|
}
|
@ -354,6 +354,7 @@
|
|||||||
<string name="pref_clear_database_summary">Delete manga and chapters that are not in your library</string>
|
<string name="pref_clear_database_summary">Delete manga and chapters that are not in your library</string>
|
||||||
<string name="clear_database_confirmation">Are you sure? Read chapters and progress of non-library manga will be lost</string>
|
<string name="clear_database_confirmation">Are you sure? Read chapters and progress of non-library manga will be lost</string>
|
||||||
<string name="clear_database_completed">Entries deleted</string>
|
<string name="clear_database_completed">Entries deleted</string>
|
||||||
|
<string name="pref_refresh_library_covers">Refresh library manga covers</string>
|
||||||
<string name="pref_refresh_library_tracking">Refresh tracking</string>
|
<string name="pref_refresh_library_tracking">Refresh tracking</string>
|
||||||
<string name="pref_refresh_library_tracking_summary">Updates status, score and last chapter read from the tracking services</string>
|
<string name="pref_refresh_library_tracking_summary">Updates status, score and last chapter read from the tracking services</string>
|
||||||
<string name="pref_disable_battery_optimization">Disable battery optimization</string>
|
<string name="pref_disable_battery_optimization">Disable battery optimization</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user