From dc54299e2487bc7c7c97734c2d5aa3f8ca379dae Mon Sep 17 00:00:00 2001 From: MCAxiaz Date: Sun, 10 May 2020 08:15:25 -0700 Subject: [PATCH] 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 --- .../kanade/tachiyomi/data/cache/CoverCache.kt | 83 ++++++++++++----- .../tachiyomi/data/database/DbOpenCallback.kt | 5 +- .../data/database/mappers/MangaTypeMapping.kt | 3 + .../tachiyomi/data/database/models/Manga.kt | 2 + .../data/database/models/MangaImpl.kt | 2 + .../data/database/queries/MangaQueries.kt | 6 ++ .../MangaCoverLastModifiedPutResolver.kt | 31 +++++++ .../data/database/tables/MangaTable.kt | 8 +- .../tachiyomi/data/glide/FileFetcher.kt | 8 +- .../glide/LibraryMangaCustomCoverFetcher.kt | 25 ++++++ .../data/glide/LibraryMangaUrlFetcher.kt | 29 ++++-- .../tachiyomi/data/glide/MangaSignature.kt | 27 ------ .../tachiyomi/data/glide/MangaThumbnail.kt | 12 ++- .../data/glide/MangaThumbnailModelLoader.kt | 37 +++----- .../data/library/LibraryUpdateService.kt | 28 +++++- .../source/browse/BrowseSourcePresenter.kt | 3 +- .../ui/library/ChangeMangaCoverDialog.kt | 38 ++++++++ .../tachiyomi/ui/library/LibraryController.kt | 89 +++++++++++-------- .../tachiyomi/ui/library/LibraryGridHolder.kt | 4 +- .../tachiyomi/ui/library/LibraryListHolder.kt | 4 +- .../tachiyomi/ui/library/LibraryPresenter.kt | 71 ++++++++++----- .../ui/manga/chapter/ChaptersPresenter.kt | 4 +- .../ui/manga/info/MangaInfoController.kt | 29 +++--- .../ui/manga/info/MangaInfoPresenter.kt | 30 +++---- .../tachiyomi/ui/reader/ReaderPresenter.kt | 9 +- .../ui/recent/history/HistoryHolder.kt | 12 ++- .../ui/recent/updates/UpdatesHolder.kt | 12 ++- .../ui/setting/SettingsAdvancedController.kt | 5 ++ .../kanade/tachiyomi/util/MangaExtensions.kt | 32 +++++++ app/src/main/res/values/strings.xml | 1 + 30 files changed, 441 insertions(+), 208 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaCoverLastModifiedPutResolver.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaCustomCoverFetcher.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaSignature.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCoverDialog.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt index a2bfab8a2..4d1a00607 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.data.cache import android.content.Context +import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.util.storage.DiskUtil import java.io.File import java.io.IOException @@ -17,51 +18,89 @@ import java.io.InputStream */ 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. */ - private val cacheDir = context.getExternalFilesDir("covers") - ?: File(context.filesDir, "covers").also { it.mkdirs() } + private val cacheDir = getCacheDir(COVERS_DIR) + + private val customCoverCacheDir = getCacheDir(CUSTOM_COVERS_DIR) /** * Returns the cover from cache. * - * @param thumbnailUrl the thumbnail url. + * @param manga the manga. * @return cover image. */ - fun getCoverFile(thumbnailUrl: String): File { - return File(cacheDir, DiskUtil.hashKeyForDisk(thumbnailUrl)) + fun getCoverFile(manga: Manga): File? { + 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. * @throws IOException if there's any error. */ @Throws(IOException::class) - fun copyToCache(thumbnailUrl: String, inputStream: InputStream) { - // Get destination file. - val destFile = getCoverFile(thumbnailUrl) - - destFile.outputStream().use { inputStream.copyTo(it) } + fun setCustomCoverToCache(manga: Manga, inputStream: InputStream) { + getCustomCoverFile(manga).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. - * @return status of deletion. + * @param manga the manga. + * @param deleteCustomCover whether the custom cover should be deleted. + * @return number of files that were deleted. */ - fun deleteFromCache(thumbnailUrl: String?): Boolean { - // Check if url is empty. - if (thumbnailUrl.isNullOrEmpty()) { - return false + fun deleteFromCache(manga: Manga, deleteCustomCover: Boolean = false): Int { + var deleted = 0 + + getCoverFile(manga)?.let { + if (it.exists() && it.delete()) ++deleted } - // Remove file. - val file = getCoverFile(thumbnailUrl) - return file.exists() && file.delete() + if (deleteCustomCover) { + if (deleteCustomCover(manga)) ++deleted + } + + 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() } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt index d7c887b1b..a8be59830 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt @@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { /** * Version of the database. */ - const val DATABASE_VERSION = 9 + const val DATABASE_VERSION = 10 } override fun onCreate(db: SupportSQLiteDatabase) = with(db) { @@ -75,6 +75,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { db.execSQL(TrackTable.addStartDate) db.execSQL(TrackTable.addFinishDate) } + if (oldVersion < 10) { + db.execSQL(MangaTable.addCoverLastModified) + } } override fun onConfigure(db: SupportSQLiteDatabase) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt index 001a83c00..910bcd67c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt @@ -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_AUTHOR 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_FAVORITE import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE @@ -62,6 +63,7 @@ class MangaPutResolver : DefaultPutResolver() { put(COL_INITIALIZED, obj.initialized) put(COL_VIEWER, obj.viewer) 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 viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER)) chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS)) + cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index ac91a41da..69c9cc6b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -16,6 +16,8 @@ interface Manga : SManga { var chapter_flags: Int + var cover_last_modified: Long + fun setChapterOrder(order: Int) { setFlags(order, SORT_MASK) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt index 1b2420b98..eda1d4abc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt @@ -32,6 +32,8 @@ open class MangaImpl : Manga { override var chapter_flags: Int = 0 + override var cover_last_modified: Long = 0 + override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt index a4c9630c3..d5d20d87a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt @@ -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.Manga 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.MangaFlagsPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver @@ -102,6 +103,11 @@ interface MangaQueries : DbProvider { .withPutResolver(MangaTitlePutResolver()) .prepare() + fun updateMangaCoverLastModified(manga: Manga) = db.put() + .`object`(manga) + .withPutResolver(MangaCoverLastModifiedPutResolver()) + .prepare() + fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() fun deleteMangas(mangas: List) = db.delete().objects(mangas).prepare() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaCoverLastModifiedPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaCoverLastModifiedPutResolver.kt new file mode 100644 index 000000000..98d6ba4a2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaCoverLastModifiedPutResolver.kt @@ -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() { + + 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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt index cbea44d65..1004340b2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt @@ -38,6 +38,8 @@ object MangaTable { const val COL_CATEGORY = "category" + const val COL_COVER_LAST_MODIFIED = "cover_last_modified" + val createTableQuery: String get() = """CREATE TABLE $TABLE( @@ -55,7 +57,8 @@ object MangaTable { $COL_LAST_UPDATE LONG, $COL_INITIALIZED BOOLEAN 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 @@ -64,4 +67,7 @@ object MangaTable { val createLibraryIndexQuery: String get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " + "WHERE $COL_FAVORITE = 1" + + val addCoverLastModified: String + get() = "ALTER TABLE $TABLE ADD COLUMN $COL_COVER_LAST_MODIFIED LONG NOT NULL DEFAULT 0" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt index 555314675..91da7c558 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt @@ -12,7 +12,7 @@ import java.io.IOException import java.io.InputStream import timber.log.Timber -open class FileFetcher(private val file: File) : DataFetcher { +open class FileFetcher(private val filePath: String = "") : DataFetcher { private var data: InputStream? = null @@ -20,7 +20,11 @@ open class FileFetcher(private val file: File) : DataFetcher { loadFromFile(callback) } - protected fun loadFromFile(callback: DataFetcher.DataCallback) { + private fun loadFromFile(callback: DataFetcher.DataCallback) { + loadFromFile(File(filePath), callback) + } + + protected fun loadFromFile(file: File, callback: DataFetcher.DataCallback) { try { data = FileInputStream(file) } catch (e: FileNotFoundException) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaCustomCoverFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaCustomCoverFetcher.kt new file mode 100644 index 000000000..3d04f40c2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaCustomCoverFetcher.kt @@ -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) { + getCustomCoverFile()?.let { + loadFromFile(it, callback) + } ?: callback.onLoadFailed(Exception("Custom cover file not found")) + } + + protected fun getCustomCoverFile(): File? { + return coverCache.getCustomCoverFile(manga).takeIf { it.exists() } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt index 79c81af4f..5d40dd6a4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt @@ -2,6 +2,7 @@ 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.FileNotFoundException @@ -19,31 +20,41 @@ import java.io.InputStream class LibraryMangaUrlFetcher( private val networkFetcher: DataFetcher, private val manga: Manga, - private val file: File -) : - FileFetcher(file) { + private val coverCache: CoverCache +) : LibraryMangaCustomCoverFetcher(manga, coverCache) { override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { - 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( priority, object : DataFetcher.DataCallback { override fun onDataReady(data: InputStream?) { if (data != null) { - val tmpFile = File(file.path + ".tmp") + val tmpFile = File(cover.path + ".tmp") try { // Retrieve destination stream, create parent folders if needed. val output = try { tmpFile.outputStream() } catch (e: FileNotFoundException) { - tmpFile.parentFile.mkdirs() + tmpFile.parentFile!!.mkdirs() tmpFile.outputStream() } // Copy the file and rename to the original. data.use { output.use { data.copyTo(output) } } - tmpFile.renameTo(file) - loadFromFile(callback) + tmpFile.renameTo(cover) + loadFromFile(cover, callback) } catch (e: Exception) { tmpFile.delete() callback.onLoadFailed(e) @@ -59,7 +70,7 @@ class LibraryMangaUrlFetcher( } ) } else { - loadFromFile(callback) + loadFromFile(cover, callback) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaSignature.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaSignature.kt deleted file mode 100644 index cdf880e42..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaSignature.kt +++ /dev/null @@ -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)) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnail.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnail.kt index 8fcab915c..ae0057eb4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnail.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnail.kt @@ -1,7 +1,15 @@ package eu.kanade.tachiyomi.data.glide +import com.bumptech.glide.load.Key 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnailModelLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnailModelLoader.kt index 44f23243a..3573246f5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnailModelLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnailModelLoader.kt @@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource -import java.io.File +import eu.kanade.tachiyomi.util.isLocal import java.io.InputStream import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -48,12 +48,6 @@ class MangaThumbnailModelLoader : ModelLoader { */ private val defaultClient = Injekt.get().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(100) - /** * Map where request headers are stored for a source. */ @@ -78,7 +72,7 @@ class MangaThumbnailModelLoader : ModelLoader { /** * 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 height the height of the view where the resource will be loaded. */ @@ -88,13 +82,16 @@ class MangaThumbnailModelLoader : ModelLoader { height: Int, options: Options ): ModelLoader.LoadData? { - // Check thumbnail is not null or empty - val url = mangaThumbnail.url - if (url == null || url.isEmpty()) { - return null - } - 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)) { val source = sourceManager.get(manga.source) as? HttpSource @@ -107,19 +104,13 @@ class MangaThumbnailModelLoader : ModelLoader { return ModelLoader.LoadData(glideUrl, networkFetcher) } - // Obtain the file for this url from the LRU cache, or retrieve and add it to the cache. - val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) } - - val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file) + val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, coverCache) // Return an instance of the fetcher providing the needed elements. - return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher) + return ModelLoader.LoadData(mangaThumbnail, libraryFetcher) } 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 ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file)) + return ModelLoader.LoadData(mangaThumbnail, FileFetcher(url.removePrefix("file://"))) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 17fe96dbc..c3ffec763 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -15,6 +15,7 @@ import androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY import androidx.core.app.NotificationManagerCompat import com.bumptech.glide.Glide 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.models.Category 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.source.SourceManager 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.util.chapter.syncChaptersWithSource 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.isServiceRunning import eu.kanade.tachiyomi.util.system.notification @@ -64,7 +65,8 @@ class LibraryUpdateService( val sourceManager: SourceManager = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(), val downloadManager: DownloadManager = Injekt.get(), - val trackManager: TrackManager = Injekt.get() + val trackManager: TrackManager = Injekt.get(), + val coverCache: CoverCache = Injekt.get() ) : Service() { /** @@ -110,6 +112,7 @@ class LibraryUpdateService( */ enum class Target { CHAPTERS, // Manga chapters + COVERS, // Manga covers TRACKING // Tracking metadata } @@ -233,6 +236,7 @@ class LibraryUpdateService( // Update either chapter list or manga details. when (target) { Target.CHAPTERS -> updateChapterList(mangaList) + Target.COVERS -> updateCovers(mangaList) Target.TRACKING -> updateTrackings(mangaList) } } @@ -387,11 +391,14 @@ class LibraryUpdateService( * @return a pair of the inserted and removed chapters. */ fun updateManga(manga: Manga): Observable, List>> { - 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 source.fetchMangaDetails(manga) .map { networkManga -> + if (manga.thumbnail_url != networkManga.thumbnail_url) { + manga.prepUpdateCover(coverCache) + } manga.copyFrom(networkManga) db.insertManga(manga).executeAsBlocking() manga @@ -404,6 +411,21 @@ class LibraryUpdateService( .map { syncChaptersWithSource(db, it, manga, source) } } + private fun updateCovers(mangaToUpdate: List): Observable { + 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 * background thread, so it's safe to do heavy operations or network calls here. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt index 8c3ac34ef..3eae04b1d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt @@ -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.TriStateItem import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem +import eu.kanade.tachiyomi.util.removeCovers import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers @@ -279,7 +280,7 @@ open class BrowseSourcePresenter( fun changeMangaFavorite(manga: Manga) { manga.favorite = !manga.favorite if (!manga.favorite) { - coverCache.deleteFromCache(manga.thumbnail_url) + manga.removeCovers(coverCache) } db.insertManga(manga).executeAsBlocking() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCoverDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCoverDialog.kt new file mode 100644 index 000000000..c12b0f13f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCoverDialog.kt @@ -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(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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 955a225d4..264626f0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.library import android.app.Activity import android.content.Intent import android.content.res.Configuration -import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.Menu @@ -22,6 +21,7 @@ import com.google.android.material.tabs.TabLayout import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.PublishRelay 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.Manga 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.toast import eu.kanade.tachiyomi.util.view.visible -import java.io.IOException import kotlinx.android.synthetic.main.main_activity.tabs import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn @@ -51,11 +50,13 @@ import uy.kohesive.injekt.api.get class LibraryController( bundle: Bundle? = null, - private val preferences: PreferencesHelper = Injekt.get() + private val preferences: PreferencesHelper = Injekt.get(), + private val coverCache: CoverCache = Injekt.get() ) : NucleusController(bundle), RootController, TabbedController, ActionMode.Callback, + ChangeMangaCoverDialog.Listener, ChangeMangaCategoriesDialog.Listener, DeleteLibraryMangasDialog.Listener { @@ -424,10 +425,7 @@ class LibraryController( private fun onActionItemClicked(item: MenuItem): Boolean { when (item.itemId) { - R.id.action_edit_cover -> { - changeSelectedCover() - destroyActionModeIfNeeded() - } + R.id.action_edit_cover -> handleChangeCover() R.id.action_move_to_category -> showChangeMangaCategoriesDialog() R.id.action_delete -> showDeleteMangaDialog() 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. */ @@ -509,21 +524,7 @@ class LibraryController( DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router) } - override fun updateCategoriesForMangas(mangas: List, categories: List) { - presenter.moveMangasToCategories(categories, mangas) - destroyActionModeIfNeeded() - } - - override fun deleteMangasFromLibrary(mangas: List, deleteChapters: Boolean) { - presenter.removeMangaFromLibrary(mangas, deleteChapters) - destroyActionModeIfNeeded() - } - - /** - * Changes the cover for the selected manga. - */ - private fun changeSelectedCover() { - val manga = selectedMangas.firstOrNull() ?: return + override fun openMangaCoverPicker(manga: Manga) { selectedCoverManga = manga if (manga.favorite) { @@ -539,6 +540,23 @@ class LibraryController( } else { activity?.toast(R.string.notification_first_add_to_library) } + + destroyActionModeIfNeeded() + } + + override fun deleteMangaCover(manga: Manga) { + presenter.deleteCustomCover(manga) + destroyActionModeIfNeeded() + } + + override fun updateCategoriesForMangas(mangas: List, categories: List) { + presenter.moveMangasToCategories(categories, mangas) + destroyActionModeIfNeeded() + } + + override fun deleteMangasFromLibrary(mangas: List, deleteChapters: Boolean) { + presenter.removeMangaFromLibrary(mangas, deleteChapters) + destroyActionModeIfNeeded() } private fun selectAllCategoryManga() { @@ -555,28 +573,25 @@ class LibraryController( override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 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 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 + 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 { /** * Key to change the cover of a manga in [onActivityResult]. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt index 5db7cc413..59cc8f965 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt @@ -5,7 +5,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.data.glide.GlideApp 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 kotlinx.android.synthetic.main.source_grid_item.download_text import kotlinx.android.synthetic.main.source_grid_item.local_text @@ -48,7 +48,7 @@ class LibraryGridHolder( text = item.downloadCount.toString() } // set local visibility if its local manga - local_text.visibleIf { item.manga.source == LocalSource.ID } + local_text.visibleIf { item.manga.isLocal() } // Update the cover. GlideApp.with(view.context).clear(thumbnail) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt index 9fcb3a346..946fbe346 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt @@ -5,7 +5,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.data.glide.GlideApp 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 kotlinx.android.synthetic.main.source_list_item.download_text import kotlinx.android.synthetic.main.source_list_item.local_text @@ -49,7 +49,7 @@ class LibraryListHolder( text = "${item.downloadCount}" } // 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 thumbnail.setOnClickListener { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 2751eceef..f004641bb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -1,5 +1,7 @@ package eu.kanade.tachiyomi.ui.library +import android.content.Context +import android.net.Uri import android.os.Bundle import com.jakewharton.rxrelay.BehaviorRelay 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.online.HttpSource 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.isNullOrUnsubscribed import eu.kanade.tachiyomi.util.lang.launchIO -import java.io.IOException -import java.io.InputStream +import eu.kanade.tachiyomi.util.removeCovers +import eu.kanade.tachiyomi.util.updateCoverLastModified import java.util.ArrayList import java.util.Collections import java.util.Comparator @@ -128,7 +131,7 @@ class LibraryPresenter( // Filter when there are no downloads. if (filterDownloaded) { // Local manga are always downloaded - if (item.manga.source == LocalSource.ID) { + if (item.manga.isLocal()) { return@f true } // 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. */ fun removeMangaFromLibrary(mangas: List, deleteChapters: Boolean) { - // Create a set of the list - val mangaToDelete = mangas.distinctBy { it.id } - mangaToDelete.forEach { it.favorite = false } - launchIO { + val mangaToDelete = mangas.distinctBy { it.id } + + mangaToDelete.forEach { + it.favorite = false + it.removeCovers(coverCache) + } db.insertMangas(mangaToDelete).executeAsBlocking() - mangaToDelete.forEach { manga -> - coverCache.deleteFromCache(manga.thumbnail_url) - if (deleteChapters) { + if (deleteChapters) { + mangaToDelete.forEach { manga -> val source = sourceManager.get(manga.source) as? HttpSource if (source != null) { downloadManager.deleteManga(manga, source) @@ -358,21 +362,42 @@ class LibraryPresenter( /** * Update cover with local file. * - * @param inputStream the new cover. * @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 editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean { - if (manga.source == LocalSource.ID) { - LocalSource.updateCover(context, manga, inputStream) - return true - } + fun editCover(manga: Manga, context: Context, data: Uri) { + Observable + .fromCallable { + context.contentResolver.openInputStream(data)?.use { + 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) { - coverCache.copyToCache(manga.thumbnail_url!!, inputStream) - return true - } - return false + fun deleteCustomCover(manga: Manga) { + Observable + .fromCallable { + coverCache.deleteCustomCover(manga) + manga.updateCoverLastModified(db) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst( + { view, _ -> view.onSetCoverSuccess() }, + { view, e -> view.onSetCoverError(e) } + ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt index 261e9ad22..e108a8eb7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt @@ -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.model.Download import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource +import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed import java.util.Date import rx.Observable @@ -189,7 +189,7 @@ class ChaptersPresenter( observable = observable.filter { it.read } } if (onlyDownloaded()) { - observable = observable.filter { it.isDownloaded || it.manga.source == LocalSource.ID } + observable = observable.filter { it.isDownloaded || it.manga.isLocal() } } if (onlyBookmarked()) { observable = observable.filter { it.bookmark } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt index eddfdca73..32e56c87d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt @@ -67,8 +67,6 @@ class MangaInfoController(private val fromSource: Boolean = false) : private var initialLoad: Boolean = true - private var thumbnailUrl: String? = null - override fun createPresenter(): MangaInfoPresenter { val ctrl = parentController as MangaController return MangaInfoPresenter( @@ -113,7 +111,7 @@ class MangaInfoController(private val fromSource: Boolean = false) : // Set SwipeRefresh to refresh manga data. binding.swipeRefresh.refreshes() - .onEach { fetchMangaFromSource() } + .onEach { fetchMangaFromSource(manualFetch = true) } .launchIn(scope) binding.mangaFullTitle.longClicks() @@ -241,23 +239,20 @@ class MangaInfoController(private val fromSource: Boolean = false) : setFavoriteButtonState(manga.favorite) // Set cover if it wasn't already. - if (binding.mangaCover.drawable == null || manga.thumbnail_url != thumbnailUrl) { - thumbnailUrl = manga.thumbnail_url - val mangaThumbnail = manga.toMangaThumbnail() + val mangaThumbnail = manga.toMangaThumbnail() + GlideApp.with(view.context) + .load(mangaThumbnail) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .centerCrop() + .into(binding.mangaCover) + + binding.backdrop?.let { GlideApp.with(view.context) .load(mangaThumbnail) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .centerCrop() - .into(binding.mangaCover) - - if (binding.backdrop != null) { - GlideApp.with(view.context) - .load(mangaThumbnail) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(binding.backdrop!!) - } + .into(it) } // Manga info section @@ -422,10 +417,10 @@ class MangaInfoController(private val fromSource: Boolean = false) : /** * Start fetching manga information from source. */ - private fun fetchMangaFromSource() { + private fun fetchMangaFromSource(manualFetch: Boolean = false) { setRefreshing(true) // Call presenter and start fetching manga information - presenter.fetchMangaFromSource() + presenter.fetchMangaFromSource(manualFetch) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt index e92257841..59d40515e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt @@ -12,6 +12,8 @@ import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed +import eu.kanade.tachiyomi.util.prepUpdateCover +import eu.kanade.tachiyomi.util.removeCovers import java.util.Date import rx.Observable import rx.Subscription @@ -36,11 +38,6 @@ class MangaInfoPresenter( private val coverCache: CoverCache = Injekt.get() ) : BasePresenter() { - /** - * Subscription to send the manga to the view. - */ - private var viewMangaSubscription: Subscription? = null - /** * Subscription to update the manga from the source. */ @@ -48,7 +45,9 @@ class MangaInfoPresenter( override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - sendMangaToView() + + getMangaObservable() + .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) }) // Update chapter count chapterCountRelay.observeOn(AndroidSchedulers.mainThread()) @@ -64,22 +63,21 @@ class MangaInfoPresenter( .subscribeLatestCache(MangaInfoController::setLastUpdateDate) } - /** - * Sends the active manga to the view. - */ - fun sendMangaToView() { - viewMangaSubscription?.let { remove(it) } - viewMangaSubscription = Observable.just(manga) - .subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) }) + private fun getMangaObservable(): Observable { + return db.getManga(manga.url, manga.source).asRxObservable() + .observeOn(AndroidSchedulers.mainThread()) } /** * Fetch manga information from source. */ - fun fetchMangaFromSource() { + fun fetchMangaFromSource(manualFetch: Boolean = false) { if (!fetchMangaSubscription.isNullOrUnsubscribed()) return fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) } .map { networkManga -> + if (manualFetch || manga.thumbnail_url != networkManga.thumbnail_url) { + manga.prepUpdateCover(coverCache) + } manga.copyFrom(networkManga) manga.initialized = true db.insertManga(manga).executeAsBlocking() @@ -87,7 +85,6 @@ class MangaInfoPresenter( } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { sendMangaToView() } .subscribeFirst( { view, _ -> view.onFetchMangaDone() @@ -104,10 +101,9 @@ class MangaInfoPresenter( fun toggleFavorite(): Boolean { manga.favorite = !manga.favorite if (!manga.favorite) { - coverCache.deleteFromCache(manga.thumbnail_url) + manga.removeCovers(coverCache) } db.insertManga(manga).executeAsBlocking() - sendMangaToView() return manga.favorite } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 8bd0f4e8a..329a62a44 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -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.ReaderPage 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.launchIO import eu.kanade.tachiyomi.util.lang.takeBytes import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.ImageUtil +import eu.kanade.tachiyomi.util.updateCoverLastModified import java.io.File import java.util.Date import java.util.concurrent.TimeUnit @@ -565,15 +567,16 @@ class ReaderPresenter( Observable .fromCallable { - if (manga.source == LocalSource.ID) { + if (manga.isLocal()) { val context = Injekt.get() LocalSource.updateCover(context, manga, stream()) + manga.updateCoverLastModified(db) R.string.cover_updated SetAsCoverResult.Success } else { - val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found") if (manga.favorite) { - coverCache.copyToCache(thumbUrl, stream()) + coverCache.setCustomCoverToCache(manga, stream()) + manga.updateCoverLastModified(db) SetAsCoverResult.Success } else { SetAsCoverResult.AddToLibraryFirst diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt index ed7b76eee..2fc5ab056 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt @@ -64,12 +64,10 @@ class HistoryHolder( // Set cover GlideApp.with(itemView.context).clear(cover) - if (!manga.thumbnail_url.isNullOrEmpty()) { - GlideApp.with(itemView.context) - .load(manga.toMangaThumbnail()) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(cover) - } + GlideApp.with(itemView.context) + .load(manga.toMangaThumbnail()) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .centerCrop() + .into(cover) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt index 08a4015f1..db627344c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt @@ -56,13 +56,11 @@ class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter) // Set cover GlideApp.with(itemView.context).clear(manga_cover) - if (!item.manga.thumbnail_url.isNullOrEmpty()) { - GlideApp.with(itemView.context) - .load(item.manga.toMangaThumbnail()) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .circleCrop() - .into(manga_cover) - } + GlideApp.with(itemView.context) + .load(item.manga.toMangaThumbnail()) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .circleCrop() + .into(manga_cover) // Check if chapter is read and set correct color if (item.chapter.read) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 582cd4a23..879f303ad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -76,6 +76,11 @@ class SettingsAdvancedController : SettingsController() { ctrl.showDialog(router) } } + preference { + titleRes = R.string.pref_refresh_library_covers + + onClick { LibraryUpdateService.start(context, target = Target.COVERS) } + } preference { titleRes = R.string.pref_refresh_library_tracking summaryRes = R.string.pref_refresh_library_tracking_summary diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt new file mode 100644 index 000000000..55da8e53e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -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() +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b1c99981c..f1f1fd0da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -354,6 +354,7 @@ Delete manga and chapters that are not in your library Are you sure? Read chapters and progress of non-library manga will be lost Entries deleted + Refresh library manga covers Refresh tracking Updates status, score and last chapter read from the tracking services Disable battery optimization