mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +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
(cherry picked from commit dc54299e24)
# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt
			
			
This commit is contained in:
		| @@ -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() } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -24,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { | ||||
|         /** | ||||
|          * Version of the database. | ||||
|          */ | ||||
|         const val DATABASE_VERSION = 1 // [SY] | ||||
|         const val DATABASE_VERSION = 2 // [SY] | ||||
|     } | ||||
|  | ||||
|     override fun onCreate(db: SupportSQLiteDatabase) = with(db) { | ||||
| @@ -63,7 +63,8 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { | ||||
|     } | ||||
|  | ||||
|     override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) { | ||||
|         if (oldVersion < 1) { | ||||
|         if (oldVersion < 2) { | ||||
|             db.execSQL(MangaTable.addCoverLastModified) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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<Manga>() { | ||||
|         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)) | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -16,6 +16,8 @@ interface Manga : SManga { | ||||
|  | ||||
|     var chapter_flags: Int | ||||
|  | ||||
|     var cover_last_modified: Long | ||||
|  | ||||
|     fun setChapterOrder(order: Int) { | ||||
|         setFlags(order, SORT_MASK) | ||||
|     } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
| @@ -112,6 +113,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<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_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" | ||||
| } | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import java.io.IOException | ||||
| import java.io.InputStream | ||||
| 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 | ||||
|  | ||||
| @@ -20,7 +20,11 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> { | ||||
|         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 { | ||||
|             data = FileInputStream(file) | ||||
|         } 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.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<InputStream>, | ||||
|     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<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( | ||||
|                 priority, | ||||
|                 object : DataFetcher.DataCallback<InputStream> { | ||||
|                     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) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
| 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) | ||||
|   | ||||
| @@ -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<MangaThumbnail, InputStream> { | ||||
|      */ | ||||
|     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. | ||||
|      */ | ||||
| @@ -78,7 +72,7 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> { | ||||
|     /** | ||||
|      * 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<MangaThumbnail, InputStream> { | ||||
|         height: Int, | ||||
|         options: Options | ||||
|     ): 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 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<MangaThumbnail, InputStream> { | ||||
|                 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://"))) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| @@ -65,7 +66,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() { | ||||
|  | ||||
|     /** | ||||
| @@ -111,6 +113,7 @@ class LibraryUpdateService( | ||||
|      */ | ||||
|     enum class Target { | ||||
|         CHAPTERS, // Manga chapters | ||||
|         COVERS, // Manga covers | ||||
|         TRACKING // Tracking metadata | ||||
|     } | ||||
|  | ||||
| @@ -234,6 +237,7 @@ class LibraryUpdateService( | ||||
|                 // Update either chapter list or manga details. | ||||
|                 when (target) { | ||||
|                     Target.CHAPTERS -> updateChapterList(mangaList) | ||||
|                     Target.COVERS -> updateCovers(mangaList) | ||||
|                     Target.TRACKING -> updateTrackings(mangaList) | ||||
|                 } | ||||
|             } | ||||
| @@ -393,11 +397,14 @@ class LibraryUpdateService( | ||||
|      * @return a pair of the inserted and removed chapters. | ||||
|      */ | ||||
|     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 | ||||
|         source.fetchMangaDetails(manga) | ||||
|             .map { networkManga -> | ||||
|                 if (manga.thumbnail_url != networkManga.thumbnail_url) { | ||||
|                     manga.prepUpdateCover(coverCache) | ||||
|                 } | ||||
|                 manga.copyFrom(networkManga) | ||||
|                 db.insertManga(manga).executeAsBlocking() | ||||
|                 manga | ||||
| @@ -410,6 +417,21 @@ class LibraryUpdateService( | ||||
|             .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 | ||||
|      * background thread, so it's safe to do heavy operations or network calls here. | ||||
|   | ||||
| @@ -35,6 +35,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 exh.EXHSavedSearch | ||||
| import java.lang.RuntimeException | ||||
| import rx.Observable | ||||
| @@ -290,7 +291,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() | ||||
|     } | ||||
|   | ||||
| @@ -6,13 +6,10 @@ import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.glide.GlideApp | ||||
| import eu.kanade.tachiyomi.data.glide.toMangaThumbnail | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.widget.StateImageViewTarget | ||||
| import kotlinx.android.synthetic.main.source_grid_item.progress | ||||
| import kotlinx.android.synthetic.main.source_grid_item.thumbnail | ||||
| import kotlinx.android.synthetic.main.source_grid_item.title | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| /** | ||||
|  * Class used to hold the displayed data of a manga in the catalogue, like the cover or the title. | ||||
| @@ -25,8 +22,6 @@ import uy.kohesive.injekt.api.get | ||||
| class SourceGridHolder(private val view: View, private val adapter: FlexibleAdapter<*>) : | ||||
|     SourceHolder(view, adapter) { | ||||
|  | ||||
|     private val preferences: PreferencesHelper = Injekt.get() | ||||
|  | ||||
|     /** | ||||
|      * Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this | ||||
|      * holder with the given manga. | ||||
|   | ||||
| @@ -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.content.Intent | ||||
| import android.content.res.Configuration | ||||
| import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.Menu | ||||
| @@ -25,6 +24,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 | ||||
| @@ -46,7 +46,6 @@ import eu.kanade.tachiyomi.util.view.visible | ||||
| import exh.favorites.FavoritesIntroDialog | ||||
| import exh.favorites.FavoritesSyncStatus | ||||
| import exh.ui.LoaderManager | ||||
| import java.io.IOException | ||||
| import java.util.concurrent.TimeUnit | ||||
| import kotlinx.android.synthetic.main.main_activity.tabs | ||||
| import kotlinx.coroutines.flow.filter | ||||
| @@ -62,11 +61,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<LibraryControllerBinding, LibraryPresenter>(bundle), | ||||
|     RootController, | ||||
|     TabbedController, | ||||
|     ActionMode.Callback, | ||||
|     ChangeMangaCoverDialog.Listener, | ||||
|     ChangeMangaCategoriesDialog.Listener, | ||||
|     DeleteLibraryMangasDialog.Listener { | ||||
|  | ||||
| @@ -481,10 +482,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() | ||||
| @@ -548,6 +546,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. | ||||
|      */ | ||||
| @@ -571,21 +586,7 @@ class LibraryController( | ||||
|         DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router) | ||||
|     } | ||||
|  | ||||
|     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() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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) { | ||||
| @@ -601,6 +602,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<Manga>, categories: List<Category>) { | ||||
|         presenter.moveMangasToCategories(categories, mangas) | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     override fun deleteMangasFromLibrary(mangas: List<Manga>, deleteChapters: Boolean) { | ||||
|         presenter.removeMangaFromLibrary(mangas, deleteChapters) | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     override fun onAttach(view: View) { | ||||
| @@ -743,28 +761,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]. | ||||
|   | ||||
| @@ -7,16 +7,13 @@ import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.IFlexible | ||||
| import eu.kanade.tachiyomi.data.glide.GlideApp | ||||
| import eu.kanade.tachiyomi.data.glide.toMangaThumbnail | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| 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 | ||||
| import kotlinx.android.synthetic.main.source_grid_item.thumbnail | ||||
| import kotlinx.android.synthetic.main.source_grid_item.title | ||||
| import kotlinx.android.synthetic.main.source_grid_item.unread_text | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| /** | ||||
|  * Class used to hold the displayed data of a manga in the library, like the cover or the title. | ||||
| @@ -31,7 +28,7 @@ class LibraryGridHolder( | ||||
|     private val view: View, | ||||
|     adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>> | ||||
| ) : LibraryHolder(view, adapter) { | ||||
|     private val preferences: PreferencesHelper = Injekt.get() | ||||
|  | ||||
|     /** | ||||
|      * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this | ||||
|      * holder with the given manga. | ||||
| @@ -53,7 +50,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) | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter | ||||
| import eu.davidea.flexibleadapter.items.IFlexible | ||||
| 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 | ||||
| @@ -51,7 +51,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 { | ||||
|   | ||||
| @@ -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 | ||||
| @@ -21,12 +23,13 @@ import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.ui.migration.MigrationFlags | ||||
| import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource | ||||
| 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 eu.kanade.tachiyomi.util.removeCovers | ||||
| import eu.kanade.tachiyomi.util.updateCoverLastModified | ||||
| import exh.favorites.FavoritesSyncHelper | ||||
| import java.io.IOException | ||||
| import java.io.InputStream | ||||
| import java.util.ArrayList | ||||
| import java.util.Collections | ||||
| import java.util.Comparator | ||||
| @@ -145,7 +148,7 @@ class LibraryPresenter( | ||||
|             // Filter when there are no downloads. | ||||
|             if (filterDownloaded != STATE_IGNORE || filterDownloadedOnly) { | ||||
|                 val isDownloaded = when { | ||||
|                     item.manga.source == LocalSource.ID -> true | ||||
|                     item.manga.isLocal() -> true | ||||
|                     item.downloadCount != -1 -> item.downloadCount > 0 | ||||
|                     else -> downloadManager.getDownloadCount(item.manga) > 0 | ||||
|                 } | ||||
| @@ -340,16 +343,17 @@ class LibraryPresenter( | ||||
|      * @param deleteChapters whether to also delete downloaded chapters. | ||||
|      */ | ||||
|     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 { | ||||
|             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) | ||||
| @@ -457,21 +461,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) } | ||||
|             ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 exh.EH_SOURCE_ID | ||||
| import exh.EXH_SOURCE_ID | ||||
| @@ -225,7 +225,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 } | ||||
|   | ||||
| @@ -84,8 +84,6 @@ class MangaInfoController(private val fromSource: Boolean = false) : | ||||
|  | ||||
|     private var initialLoad: Boolean = true | ||||
|  | ||||
|     private var thumbnailUrl: String? = null | ||||
|  | ||||
|     // EXH --> | ||||
|     private var lastMangaThumbnail: String? = null | ||||
|  | ||||
| @@ -161,7 +159,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() | ||||
| @@ -362,23 +360,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 | ||||
| @@ -550,10 +545,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) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -15,6 +15,8 @@ import eu.kanade.tachiyomi.source.online.all.MergedSource | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.ui.browse.source.SourceController | ||||
| import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed | ||||
| import eu.kanade.tachiyomi.util.prepUpdateCover | ||||
| import eu.kanade.tachiyomi.util.removeCovers | ||||
| import exh.MERGED_SOURCE_ID | ||||
| import exh.util.await | ||||
| import java.util.Date | ||||
| @@ -45,11 +47,6 @@ class MangaInfoPresenter( | ||||
|     private val gson: Gson = Injekt.get() | ||||
| ) : BasePresenter<MangaInfoController>() { | ||||
|  | ||||
|     /** | ||||
|      * Subscription to send the manga to the view. | ||||
|      */ | ||||
|     private var viewMangaSubscription: Subscription? = null | ||||
|  | ||||
|     /** | ||||
|      * Subscription to update the manga from the source. | ||||
|      */ | ||||
| @@ -57,7 +54,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()) | ||||
| @@ -73,22 +72,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<Manga> { | ||||
|         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() | ||||
| @@ -96,7 +94,6 @@ class MangaInfoPresenter( | ||||
|             } | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .doOnNext { sendMangaToView() } | ||||
|             .subscribeFirst( | ||||
|                 { view, _ -> | ||||
|                     view.onFetchMangaDone() | ||||
| @@ -113,10 +110,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 | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -54,7 +54,7 @@ class AboutController : SettingsController() { | ||||
|         preference { | ||||
|             titleRes = R.string.version | ||||
|             summary = if (syDebugVersion != "0") { | ||||
|                 "Preview r${syDebugVersion} (${BuildConfig.COMMIT_SHA})" | ||||
|                 "Preview r$syDebugVersion (${BuildConfig.COMMIT_SHA})" | ||||
|             } else { | ||||
|                 "Stable ${BuildConfig.VERSION_NAME}" | ||||
|             } | ||||
|   | ||||
| @@ -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<Application>() | ||||
|                     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 | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -78,6 +78,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 | ||||
|   | ||||
| @@ -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="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="pref_refresh_library_covers">Refresh library manga covers</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_disable_battery_optimization">Disable battery optimization</string> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user