From d3ec230d4baa8584118dc30807728305715db25b Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 20 May 2020 01:04:19 -0400 Subject: [PATCH] Edit info for online manga + Custom covers update Yes you read that right. It's back! Oh god it's back Instead of modifying the db, an external json file is made holding the custom info for your library (meaning it's even easier to remove should I so choose) Reworking to just override the variable and use said var instead of having the current/original logic that existed before Custom covers are now saved in a new folder, likewise to upstream Also like upstream, custom covers can be added to manga without covers (closes #49) (I'm so sorry Carlos) --- .../java/eu/kanade/tachiyomi/AppModule.kt | 5 + .../java/eu/kanade/tachiyomi/Migrations.kt | 3 + .../backup/serializer/MangaTypeAdapter.kt | 2 +- .../kanade/tachiyomi/data/cache/CoverCache.kt | 87 ++++++++++---- .../data/database/mappers/MangaTypeMapping.kt | 10 +- .../tachiyomi/data/database/models/Manga.kt | 10 -- .../data/database/models/MangaImpl.kt | 56 +++++++-- .../resolvers/MangaInfoPutResolver.kt | 10 +- .../tachiyomi/data/download/DownloadCache.kt | 4 +- .../data/download/DownloadProvider.kt | 2 +- .../data/download/coil/MangaFetcher.kt | 27 ++--- .../data/library/CustomMangaManager.kt | 111 ++++++++++++++++++ .../data/library/LibraryUpdateService.kt | 13 +- .../kanade/tachiyomi/source/model/SManga.kt | 21 +++- .../tachiyomi/ui/library/LibraryPresenter.kt | 20 ++++ .../tachiyomi/ui/manga/EditMangaDialog.kt | 46 +++++++- .../ui/manga/MangaDetailsController.kt | 38 ++---- .../ui/manga/MangaDetailsPresenter.kt | 83 ++++++------- .../tachiyomi/ui/manga/MangaHeaderHolder.kt | 10 +- .../tachiyomi/ui/manga/MangaHeaderItem.kt | 3 +- .../tachiyomi/ui/reader/ReaderPresenter.kt | 6 +- .../util/chapter/ChapterRecognition.kt | 2 +- .../tachiyomi/util/lang/StringExtensions.kt | 5 + app/src/main/res/layout/edit_manga_dialog.xml | 16 ++- app/src/main/res/values/strings.xml | 2 +- 25 files changed, 409 insertions(+), 183 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/library/CustomMangaManager.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt index f9aa0a6dc3..652cf6e214 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.extension.ExtensionManager @@ -41,6 +42,8 @@ class AppModule(val app: Application) : InjektModule { addSingletonFactory { DownloadManager(app) } + addSingletonFactory { CustomMangaManager(app) } + addSingletonFactory { TrackManager(app) } addSingletonFactory { Gson() } @@ -56,5 +59,7 @@ class AppModule(val app: Application) : InjektModule { GlobalScope.launch { get() } GlobalScope.launch { get() } + + GlobalScope.launch { get() } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index f739ace2af..88a7cf16c7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -78,6 +78,9 @@ object Migrations { BackupCreatorJob.setupTask() ExtensionUpdateJob.setupTask() } + if (oldVersion < 66) { + LibraryPresenter.updateCustoms() + } return true } return false diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MangaTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MangaTypeAdapter.kt index 1192f39b4a..e10adc2fab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MangaTypeAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MangaTypeAdapter.kt @@ -15,7 +15,7 @@ object MangaTypeAdapter { write { beginArray() value(it.url) - value(it.title) + value(it.originalTitle) value(it.source) value(max(0, it.viewer)) value(it.chapter_flags) 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 b884490805..e341151d8e 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 @@ -6,6 +6,7 @@ import coil.Coil import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.executeOnIO import eu.kanade.tachiyomi.util.system.toast @@ -29,11 +30,16 @@ import java.io.InputStream */ class CoverCache(val context: Context) { - /** - * Cache directory used for cache management. - */ - private val cacheDir = context.getExternalFilesDir("covers") - ?: File(context.filesDir, "covers").also { it.mkdirs() } + 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 = getCacheDir(COVERS_DIR) + + /** Cache directory used for custom cover cache management.*/ + private val customCoverCacheDir = getCacheDir(CUSTOM_COVERS_DIR) fun getChapterCacheSize(): String { return Formatter.formatFileSize(context, DiskUtil.getDirectorySize(cacheDir)) @@ -50,7 +56,7 @@ class CoverCache(val context: Context) { val files = cacheDir.listFiles()?.iterator() ?: return@launch while (files.hasNext()) { val file = files.next() - if (file.name !in urls) { + if (file.isFile && file.name !in urls) { deletedSize += file.length() file.delete() } @@ -65,6 +71,45 @@ class CoverCache(val context: Context) { } } + /** + * Returns the custom cover from cache. + * + * @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 setCustomCoverToCache(manga: Manga, inputStream: InputStream) { + getCustomCoverFile(manga).outputStream().use { + inputStream.copyTo(it) + Coil.imageLoader(context).invalidate(manga.key()) + } + } + + /** + * Delete custom cover of the manga from the cache + * + * @param manga the manga. + * @return whether the cover was deleted. + */ + fun deleteCustomCover(manga: Manga): Boolean { + val result = getCustomCoverFile(manga).let { + it.exists() && it.delete() + } + Coil.imageLoader(context).invalidate(manga.key()) + return result + } + /** * Returns the cover from cache. * @@ -75,19 +120,11 @@ class CoverCache(val context: Context) { return File(cacheDir, manga.key()) } - /** - * Copy the given stream to this cache. - * - * @param thumbnailUrl url of the thumbnail. - * @param inputStream the stream to copy. - * @throws IOException if there's any error. - */ - @Throws(IOException::class) - fun copyToCache(manga: Manga, inputStream: InputStream) { - // Get destination file. - val destFile = getCoverFile(manga) - - destFile.outputStream().use { inputStream.copyTo(it) } + fun deleteFromCache(name: String?) { + if (name.isNullOrEmpty()) return + val file = getCoverFile(MangaImpl().apply { thumbnail_url = name }) + Coil.imageLoader(context).invalidate(file.name) + if (file.exists()) file.delete() } /** @@ -96,13 +133,21 @@ class CoverCache(val context: Context) { * @param thumbnailUrl the thumbnail url. * @return status of deletion. */ - fun deleteFromCache(manga: Manga, deleteMemoryCache: Boolean = true) { + fun deleteFromCache( + manga: Manga, + deleteCustom: Boolean = true + ) { // Check if url is empty. if (manga.thumbnail_url.isNullOrEmpty()) return // Remove file val file = getCoverFile(manga) - if (deleteMemoryCache) Coil.imageLoader(context).invalidate(file.name) + if (deleteCustom) deleteCustomCover(manga) if (file.exists()) file.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/mappers/MangaTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt index 73dfa7f0f0..8cc35cbd8c 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 @@ -52,11 +52,11 @@ class MangaPutResolver : DefaultPutResolver() { put(COL_ID, obj.id) put(COL_SOURCE, obj.source) put(COL_URL, obj.url) - put(COL_ARTIST, obj.artist) - put(COL_AUTHOR, obj.author) - put(COL_DESCRIPTION, obj.description) - put(COL_GENRE, obj.genre) - put(COL_TITLE, obj.title) + put(COL_ARTIST, obj.originalArtist) + put(COL_AUTHOR, obj.originalAuthor) + put(COL_DESCRIPTION, obj.originalDescription) + put(COL_GENRE, obj.originalGenre) + put(COL_TITLE, obj.originalTitle) put(COL_STATUS, obj.status) put(COL_THUMBNAIL_URL, obj.thumbnail_url) put(COL_FAVORITE, obj.favorite) 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 fc00c99672..13281b74fb 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 @@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.Locale -import kotlin.random.Random interface Manga : SManga { @@ -149,15 +148,6 @@ interface Manga : SManga { return DiskUtil.hashKeyForDisk(thumbnail_url.orEmpty()) } - fun setCustomThumbnailUrl() { - removeCustomThumbnailUrl() - thumbnail_url = "Custom-${Random.nextInt(0, 1000)}-J2K-${thumbnail_url ?: id!!}" - } - - fun removeCustomThumbnailUrl() { - thumbnail_url = thumbnail_url?.substringAfter("-J2K-")?.substringAfter("Custom-") - } - // Used to display the chapter's title one way or another var displayMode: Int get() = chapter_flags and DISPLAY_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 33fc80a2fd..22f8527d8b 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 @@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.data.database.models import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadProvider +import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.source.model.SManga import uy.kohesive.injekt.injectLazy -import kotlin.collections.set open class MangaImpl : Manga { @@ -14,15 +14,34 @@ open class MangaImpl : Manga { override lateinit var url: String - override lateinit var title: String + private val customMangaManager: CustomMangaManager by injectLazy() - override var artist: String? = null + override var title: String + get() = if (favorite) { + val customTitle = customMangaManager.getManga(this)?.title + if (customTitle.isNullOrBlank()) ogTitle else customTitle + } else { + ogTitle + } + set(value) { + ogTitle = value + } - override var author: String? = null + override var author: String? + get() = if (favorite) customMangaManager.getManga(this)?.author ?: ogAuthor else ogAuthor + set(value) { ogAuthor = value } - override var description: String? = null + override var artist: String? + get() = if (favorite) customMangaManager.getManga(this)?.artist ?: ogArtist else ogArtist + set(value) { ogArtist = value } - override var genre: String? = null + override var description: String? + get() = if (favorite) customMangaManager.getManga(this)?.description ?: ogDesc else ogDesc + set(value) { ogDesc = value } + + override var genre: String? + get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre + set(value) { ogGenre = value } override var status: Int = 0 @@ -42,14 +61,25 @@ open class MangaImpl : Manga { override var date_added: Long = 0 + lateinit var ogTitle: String + private set + var ogAuthor: String? = null + private set + var ogArtist: String? = null + private set + var ogDesc: String? = null + private set + var ogGenre: String? = null + private set + override fun copyFrom(other: SManga) { - if (other is MangaImpl && (other as MangaImpl)::title.isInitialized && - !other.title.isBlank() && other.title != title) { - val oldTitle = title - title = other.title + if (other is MangaImpl && other::ogTitle.isInitialized && + !other.title.isBlank() && other.ogTitle != ogTitle) { + val oldTitle = ogTitle + title = other.ogTitle val db: DownloadManager by injectLazy() val provider = DownloadProvider(db.context) - provider.renameMangaFolder(oldTitle, title, source) + provider.renameMangaFolder(oldTitle, ogTitle, source) } super.copyFrom(other) } @@ -64,7 +94,7 @@ open class MangaImpl : Manga { } override fun hashCode(): Int { - if (::url.isInitialized) return url.hashCode() - else return (id ?: 0L).hashCode() + return if (::url.isInitialized) url.hashCode() + else (id ?: 0L).hashCode() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaInfoPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaInfoPutResolver.kt index eb50bf2f94..b1a2c826c4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaInfoPutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaInfoPutResolver.kt @@ -26,11 +26,11 @@ class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver() { .build() fun mapToContentValues(manga: Manga) = ContentValues(1).apply { - put(MangaTable.COL_TITLE, manga.title) - put(MangaTable.COL_GENRE, manga.genre) - put(MangaTable.COL_AUTHOR, manga.author) - put(MangaTable.COL_ARTIST, manga.artist) - put(MangaTable.COL_DESCRIPTION, manga.description) + put(MangaTable.COL_TITLE, manga.originalTitle) + put(MangaTable.COL_GENRE, manga.originalGenre) + put(MangaTable.COL_AUTHOR, manga.originalAuthor) + put(MangaTable.COL_ARTIST, manga.originalArtist) + put(MangaTable.COL_DESCRIPTION, manga.originalDescription) } fun resetToContentValues(manga: Manga) = ContentValues(1).apply { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index c42d86fdbb..5444e20ba1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -138,11 +138,11 @@ class DownloadCache( val trueMangaDirs = mangaDirs.mapNotNull { mangaDir -> val manga = sourceMangas.firstOrNull()?.find { DiskUtil.buildValidFilename( - it.title + it.originalTitle ).toLowerCase() == mangaDir.key.toLowerCase() && it.source == sourceValue.key } ?: sourceMangas.lastOrNull()?.find { DiskUtil.buildValidFilename( - it.title + it.originalTitle ).toLowerCase() == mangaDir.key.toLowerCase() && it.source == sourceValue.key } val id = manga?.id ?: return@mapNotNull null diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index ab4563e153..963151ab89 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -185,7 +185,7 @@ class DownloadProvider(private val context: Context) { * @param manga the manga to query. */ fun getMangaDirName(manga: Manga): String { - return DiskUtil.buildValidFilename(manga.title) + return DiskUtil.buildValidFilename(manga.originalTitle) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/coil/MangaFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/coil/MangaFetcher.kt index 7c469d8490..dc5d8488ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/coil/MangaFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/coil/MangaFetcher.kt @@ -28,7 +28,11 @@ import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.File -class MangaFetcher() : Fetcher { +class MangaFetcher : Fetcher { + + companion object { + const val realCover = "real_cover" + } private val coverCache: CoverCache by injectLazy() private val sourceManager: SourceManager by injectLazy() @@ -46,23 +50,17 @@ class MangaFetcher() : Fetcher { override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult { val cover = data.thumbnail_url return when (getResourceType(cover)) { - Type.File -> fileLoader(data) Type.URL -> httpLoader(data, options) - Type.CUSTOM -> customLoader(data, options) + Type.File -> fileLoader(data) null -> error("Invalid image") } } - private suspend fun customLoader(manga: Manga, options: Options): FetchResult { - val coverFile = coverCache.getCoverFile(manga) - if (coverFile.exists()) { - return fileLoader(coverFile) - } - manga.thumbnail_url = manga.thumbnail_url!!.substringAfter("-J2K-").substringAfter("CUSTOM-") - return httpLoader(manga, options) - } - private suspend fun httpLoader(manga: Manga, options: Options): FetchResult { + val customCoverFile = coverCache.getCustomCoverFile(manga) + if (customCoverFile.exists() && options.parameters.value(realCover) != true) { + return fileLoader(customCoverFile) + } val coverFile = coverCache.getCoverFile(manga) if (coverFile.exists()) { return fileLoader(coverFile) @@ -158,14 +156,13 @@ class MangaFetcher() : Fetcher { private fun getResourceType(cover: String?): Type? { return when { cover.isNullOrEmpty() -> null - cover.startsWith("http") -> Type.URL - cover.startsWith("Custom-") -> Type.CUSTOM + cover.startsWith("http") || cover.startsWith("Custom-", true) -> Type.URL cover.startsWith("/") || cover.startsWith("file://") -> Type.File else -> null } } private enum class Type { - File, CUSTOM, URL; + File, URL; } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/CustomMangaManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/CustomMangaManager.kt new file mode 100644 index 0000000000..6ce0ce311f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/CustomMangaManager.kt @@ -0,0 +1,111 @@ +package eu.kanade.tachiyomi.data.library + +import android.content.Context +import com.github.salomonbrys.kotson.nullLong +import com.github.salomonbrys.kotson.nullString +import com.github.salomonbrys.kotson.set +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonObject +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaImpl +import java.io.File +import java.util.Scanner + +class CustomMangaManager(val context: Context) { + + private val editJson = File(context.getExternalFilesDir(null), "edits.json") + + private var customMangaMap = mutableMapOf() + + init { + fetchCustomData() + } + + fun getManga(manga: Manga): Manga? = customMangaMap[manga.id] + + private fun fetchCustomData() { + if (!editJson.exists() || !editJson.isFile) return + + val json = try { + Gson().fromJson( + Scanner(editJson).useDelimiter("\\Z").next(), JsonObject::class.java + ) + } catch (e: Exception) { + null + } ?: return + + val mangasJson = json.get("mangas").asJsonArray ?: return + customMangaMap = mangasJson.mapNotNull { element -> + val mangaObject = element.asJsonObject ?: return@mapNotNull null + val id = mangaObject["id"]?.nullLong ?: return@mapNotNull null + val manga = MangaImpl().apply { + this.id = id + title = mangaObject["title"]?.nullString ?: "" + author = mangaObject["author"]?.nullString + artist = mangaObject["artist"]?.nullString + description = mangaObject["description"]?.nullString + genre = mangaObject["genre"]?.asJsonArray?.mapNotNull { it.nullString } + ?.joinToString(", ") + } + id to manga + }.toMap().toMutableMap() + } + + fun saveMangaInfo(manga: MangaJson) { + if (manga.title == null && manga.author == null && manga.artist == null && manga.description == null && manga.genre == null) { + customMangaMap.remove(manga.id) + } else { + customMangaMap[manga.id] = MangaImpl().apply { + id = manga.id + title = manga.title ?: "" + author = manga.author + artist = manga.artist + description = manga.description + genre = manga.genre?.joinToString(", ") + } + } + saveCustomInfo() + } + + private fun saveCustomInfo() { + val jsonElements = customMangaMap.values.map { it.toJson() } + if (jsonElements.isNotEmpty()) { + val gson = GsonBuilder().create() + val root = JsonObject() + val mangaEntries = gson.toJsonTree(jsonElements) + + root["mangas"] = mangaEntries + editJson.delete() + editJson.writeText(gson.toJson(root)) + } + } + + fun Manga.toJson(): MangaJson { + return MangaJson( + id!!, title, author, artist, description, genre?.split(", ")?.toTypedArray() + ) + } + + data class MangaJson( + val id: Long, + val title: String? = null, + val author: String? = null, + val artist: String? = null, + val description: String? = null, + val genre: Array? = null + ) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as MangaJson + if (id != other.id) return false + return true + } + + override fun hashCode(): Int { + return id.hashCode() + } + } +} 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 8bcc845c63..cc64e372b0 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 @@ -20,6 +20,7 @@ import coil.request.GetRequest import coil.request.LoadRequest import coil.transform.CircleCropTransformation 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 @@ -73,6 +74,7 @@ import java.util.concurrent.atomic.AtomicInteger */ class LibraryUpdateService( val db: DatabaseHelper = Injekt.get(), + val coverCache: CoverCache = Injekt.get(), val sourceManager: SourceManager = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(), val downloadManager: DownloadManager = Injekt.get(), @@ -533,15 +535,14 @@ class LibraryUpdateService( val thumbnailUrl = manga.thumbnail_url manga.copyFrom(networkManga) manga.initialized = true + if (thumbnailUrl != manga.thumbnail_url) { + coverCache.deleteFromCache(thumbnailUrl) // load new covers in background - if (!manga.hasCustomCover()) { - val request = LoadRequest.Builder(this@LibraryUpdateService) - .data(manga) - .memoryCachePolicy(CachePolicy.DISABLED) - .build() + val request = + LoadRequest.Builder(this@LibraryUpdateService).data(manga) + .memoryCachePolicy(CachePolicy.DISABLED).build() Coil.imageLoader(this@LibraryUpdateService).execute(request) } - db.insertManga(manga).executeAsBlocking() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt index 123affee08..9144cba791 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt @@ -23,22 +23,31 @@ interface SManga : Serializable { var initialized: Boolean - fun hasCustomCover() = thumbnail_url?.startsWith("Custom-") == true + val originalTitle: String + get() = (this as? MangaImpl)?.ogTitle ?: title + val originalAuthor: String? + get() = (this as? MangaImpl)?.ogAuthor ?: author + val originalArtist: String? + get() = (this as? MangaImpl)?.ogArtist ?: artist + val originalDescription: String? + get() = (this as? MangaImpl)?.ogDesc ?: description + val originalGenre: String? + get() = (this as? MangaImpl)?.ogGenre ?: genre fun copyFrom(other: SManga) { if (other.author != null) - author = other.author + author = other.originalAuthor if (other.artist != null) - artist = other.artist + artist = other.originalArtist if (other.description != null) - description = other.description + description = other.originalDescription if (other.genre != null) - genre = other.genre + genre = other.originalGenre - if (other.thumbnail_url != null && !hasCustomCover()) + if (other.thumbnail_url != null) thumbnail_url = other.thumbnail_url status = other.status 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 0bb65da0c7..e51c7456da 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 @@ -37,6 +37,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.ArrayList import java.util.Comparator +import java.util.Locale /** * Presenter of [LibraryController]. @@ -874,5 +875,24 @@ class LibraryPresenter( } } } + + fun updateCustoms() { + val db: DatabaseHelper = Injekt.get() + val cc: CoverCache = Injekt.get() + db.inTransaction { + val libraryManga = db.getLibraryMangas().executeAsBlocking() + libraryManga.forEach { manga -> + if (manga.thumbnail_url?.startsWith("custom", ignoreCase = true) == true) { + val file = cc.getCoverFile(manga) + if (file.exists()) { + file.renameTo(cc.getCustomCoverFile(manga)) + } + manga.thumbnail_url = + manga.thumbnail_url!!.toLowerCase(Locale.ROOT).substringAfter("custom-") + db.insertManga(manga).executeAsBlocking() + } + } + } + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt index e81336ebc1..a339e4bb02 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt @@ -5,13 +5,17 @@ import android.net.Uri import android.os.Bundle import android.view.View import coil.api.loadAny +import coil.request.Parameters import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.customview.customView import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.coil.MangaFetcher import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.util.lang.chop +import eu.kanade.tachiyomi.util.view.visibleIf import kotlinx.android.synthetic.main.edit_manga_dialog.view.* import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -24,6 +28,8 @@ class EditMangaDialog : DialogController { private var customCoverUri: Uri? = null + private var willResetCover = false + private val infoController get() = targetController as MangaDetailsController @@ -68,22 +74,58 @@ class EditMangaDialog : DialogController { view.manga_artist.append(manga.artist ?: "") view.manga_description.append(manga.description ?: "") view.manga_genres_tags.setTags(manga.genre?.split(", ") ?: emptyList()) + } else { + if (manga.title != manga.originalTitle) { + view.title.append(manga.title) + } + if (manga.author != manga.originalAuthor) { + view.manga_author.append(manga.author ?: "") + } + if (manga.artist != manga.originalArtist) { + view.manga_artist.append(manga.artist ?: "") + } + if (manga.description != manga.originalDescription) { + view.manga_description.append(manga.description ?: "") + } + view.manga_genres_tags.setTags(manga.genre?.split(", ") ?: emptyList()) + + view.title.hint = "${resources?.getString(R.string.title)}: ${manga.originalTitle}" + if (manga.originalAuthor != null) { + view.manga_author.hint = "${resources?.getString(R.string.author)}: ${manga.originalAuthor}" + } + if (manga.originalArtist != null) { + view.manga_artist.hint = "${resources?.getString(R.string.artist)}: ${manga.originalArtist}" + } + if (manga.originalDescription != null) { + view.manga_description.hint = + "${resources?.getString(R.string.description)}: ${manga.originalDescription?.replace( + "\n", " " + )?.chop(20)}" + } } view.manga_genres_tags.clearFocus() view.cover_layout.setOnClickListener { infoController.changeCover() } view.reset_tags.setOnClickListener { resetTags() } + view.reset_cover.visibleIf(!isLocal) + view.reset_cover.setOnClickListener { + view.manga_cover.loadAny(manga, builder = { + parameters(Parameters.Builder().set(MangaFetcher.realCover, true).build()) + }) + willResetCover = true + } } private fun resetTags() { if (manga.genre.isNullOrBlank() || manga.source == LocalSource.ID) dialogView?.manga_genres_tags?.setTags( emptyList() ) - else dialogView?.manga_genres_tags?.setTags(manga.genre?.split(", ")) + else dialogView?.manga_genres_tags?.setTags(manga.originalGenre?.split(", ")) } fun updateCover(uri: Uri) { + willResetCover = false dialogView!!.manga_cover.loadAny(uri) customCoverUri = uri } @@ -97,7 +139,7 @@ class EditMangaDialog : DialogController { infoController.presenter.updateManga(dialogView?.title?.text.toString(), dialogView?.manga_author?.text.toString(), dialogView?.manga_artist?.text.toString(), customCoverUri, dialogView?.manga_description?.text.toString(), - dialogView?.manga_genres_tags?.tags) + dialogView?.manga_genres_tags?.tags, willResetCover) } private companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt index 143536c45e..f581ad3786 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -43,7 +43,6 @@ import coil.request.LoadRequest import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.checkbox.checkBoxPrompt import com.afollestad.materialdialogs.checkbox.isCheckPromptChecked -import com.afollestad.materialdialogs.list.listItems import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType import com.google.android.material.snackbar.BaseTransientBottomBar @@ -307,7 +306,7 @@ class MangaDetailsController : BaseController, fun setPaletteColor() { val view = view ?: return - val request = LoadRequest.Builder(view.context).data(manga).allowHardware(false) + val request = LoadRequest.Builder(view.context).data(presenter.manga).allowHardware(false) .target { drawable -> val bitmap = (drawable as BitmapDrawable).bitmap // Generate the Palette on a background thread. @@ -393,8 +392,8 @@ class MangaDetailsController : BaseController, presenter.refreshTracking() refreshTracker = null } - // reset the covers and palette cause user might have set a custom cover - presenter.forceUpdateCovers(false) + // fetch cover again in case the user set a new cover while reading + setPaletteColor() val isCurrentController = router?.backstack?.lastOrNull()?.controller() == this if (isCurrentController) { @@ -693,10 +692,6 @@ class MangaDetailsController : BaseController, inflater.inflate(R.menu.manga_details, menu) val editItem = menu.findItem(R.id.action_edit) editItem.isVisible = presenter.manga.favorite && !presenter.isLockedFromSearch - editItem.title = view?.context?.getString( - if (manga?.source == LocalSource.ID) - R.string.edit else R.string.edit_cover - ) menu.findItem(R.id.action_download).isVisible = !presenter.isLockedFromSearch && manga?.source != LocalSource.ID menu.findItem(R.id.action_mark_all_as_read).isVisible = @@ -745,29 +740,10 @@ class MangaDetailsController : BaseController, override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_edit -> { - if (manga?.source == LocalSource.ID) { - editMangaDialog = EditMangaDialog( - this, presenter.manga - ) - editMangaDialog?.showDialog(router) - } else { - if (manga?.hasCustomCover() == true) { - MaterialDialog(activity!!).listItems(items = listOf( - view!!.context.getString( - R.string.edit_cover - ), view!!.context.getString( - R.string.reset_cover - ) - ), waitForPositiveButton = false, selection = { _, index, _ -> - when (index) { - 0 -> changeCover() - else -> presenter.clearCustomCover() - } - }).show() - } else { - changeCover() - } - } + editMangaDialog = EditMangaDialog( + this, presenter.manga + ) + editMangaDialog?.showDialog(router) } R.id.action_open_in_web_view -> openInWebView() R.id.action_refresh_tracking -> presenter.refreshTracking(true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt index e83dd1adcd..063dc27b7b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt @@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.DownloadQueue +import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.data.library.LibraryServiceListener import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.preference.PreferencesHelper @@ -26,11 +27,11 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.fetchChapterListAsync import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource +import eu.kanade.tachiyomi.util.lang.trimOrNull import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.executeOnIO import kotlinx.coroutines.CoroutineScope @@ -43,6 +44,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy import java.io.File import java.io.FileOutputStream import java.io.OutputStream @@ -60,6 +62,8 @@ class MangaDetailsPresenter( private var scope = CoroutineScope(Job() + Dispatchers.Default) + private val customMangaManager: CustomMangaManager by injectLazy() + var isLockedFromSearch = false var hasRequested = false var isLoading = false @@ -405,11 +409,10 @@ class MangaDetailsPresenter( manga.copyFrom(networkManga) manga.initialized = true - if (shouldUpdateCover(thumbnailUrl, networkManga)) { - coverCache.deleteFromCache(manga, false) - manga.thumbnail_url = networkManga.thumbnail_url + if (thumbnailUrl != networkManga.thumbnail_url) { + coverCache.deleteFromCache(thumbnailUrl) withContext(Dispatchers.Main) { - forceUpdateCovers() + controller.setPaletteColor() } } db.insertManga(manga).executeAsBlocking() @@ -460,19 +463,6 @@ class MangaDetailsPresenter( } } - private fun shouldUpdateCover(thumbnailUrl: String?, networkManga: SManga): Boolean { - val refreshCovers = preferences.refreshCoversToo().getOrDefault() - if (thumbnailUrl == networkManga.thumbnail_url && !refreshCovers) { - return false - } - if (thumbnailUrl != networkManga.thumbnail_url && !manga.hasCustomCover()) { - return true - } - if (manga.hasCustomCover()) return false - - return refreshCovers - } - /** * Requests an updated list of chapters from the source. */ @@ -666,6 +656,7 @@ class MangaDetailsPresenter( coverCache.deleteFromCache(manga) db.resetMangaInfo(manga).executeAsBlocking() downloadManager.deleteManga(manga, source) + customMangaManager.saveMangaInfo(CustomMangaManager.MangaJson(manga.id!!)) asyncUpdateMangaAndChapters(true) } @@ -718,36 +709,41 @@ class MangaDetailsPresenter( artist: String?, uri: Uri?, description: String?, - tags: Array? + tags: Array?, + resetCover: Boolean = false ) { if (manga.source == LocalSource.ID) { manga.title = if (title.isNullOrBlank()) manga.url else title.trim() - manga.author = author?.trim() - manga.artist = artist?.trim() - manga.description = description?.trim() + manga.author = author?.trimOrNull() + manga.artist = artist?.trimOrNull() + manga.description = description?.trimOrNull() val tagsString = tags?.joinToString(", ") { it.capitalize() } manga.genre = if (tags.isNullOrEmpty()) null else tagsString?.trim() LocalSource(downloadManager.context).updateMangaInfo(manga) db.updateMangaInfo(manga).executeAsBlocking() + } else { + val genre = if (!tags.isNullOrEmpty() && tags.joinToString(", ") != manga.genre) { + tags.map { it.capitalize() }.toTypedArray() + } else { + null + } + val manga = CustomMangaManager.MangaJson( + manga.id!!, + title?.trimOrNull(), + author?.trimOrNull(), + artist?.trimOrNull(), + description?.trimOrNull(), + genre + ) + customMangaManager.saveMangaInfo(manga) } - if (uri != null) editCoverWithStream(uri) - } - - /** - * Remvoe custom cover - */ - fun clearCustomCover() { - if (manga.hasCustomCover()) { - coverCache.deleteFromCache(manga) - manga.removeCustomThumbnailUrl() - db.insertManga(manga).executeAsBlocking() - forceUpdateCovers() + if (uri != null) { + editCoverWithStream(uri) + } else if (resetCover) { + coverCache.deleteCustomCover(manga) + controller.setPaletteColor() } - } - - fun forceUpdateCovers(deleteCache: Boolean = true) { - if (deleteCache) coverCache.deleteFromCache(manga) - controller.setPaletteColor() + controller.updateHeader() } fun editCoverWithStream(uri: Uri): Boolean { @@ -755,16 +751,13 @@ class MangaDetailsPresenter( downloadManager.context.contentResolver.openInputStream(uri) ?: return false if (manga.source == LocalSource.ID) { LocalSource.updateCover(downloadManager.context, manga, inputStream) - forceUpdateCovers() + controller.setPaletteColor() return true } if (manga.favorite) { - coverCache.deleteFromCache(manga) - manga.setCustomThumbnailUrl() - db.insertManga(manga).executeAsBlocking() - coverCache.copyToCache(manga, inputStream) - forceUpdateCovers(false) + coverCache.setCustomCoverToCache(manga, inputStream) + controller.setPaletteColor() return true } return false diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt index 6b49dac3fe..ce46919bce 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt @@ -137,7 +137,7 @@ class MangaHeaderHolder( title.text = manga.title if (manga.genre.isNullOrBlank().not()) manga_genres_tags.setTags( - manga.genre?.split(", ")?.map(String::trim) + manga.genre?.split(",")?.map(String::trim) ) else manga_genres_tags.setTags(emptyList()) @@ -323,14 +323,6 @@ class MangaHeaderHolder( }) } - private fun isCached(manga: Manga): Boolean { - if (manga.source == LocalSource.ID) return true - manga.thumbnail_url?.let { - return adapter.delegate.mangaPresenter().coverCache.getCoverFile(manga).exists() - } - return manga.initialized - } - fun expand() { sub_item_group.visible() if (!showMoreButton) more_button_group.gone() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderItem.kt index 566646d06d..3e1c9c6984 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderItem.kt @@ -7,7 +7,6 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.util.system.HashCode class MangaHeaderItem(val manga: Manga, var startExpanded: Boolean) : AbstractFlexibleItem() { @@ -46,6 +45,6 @@ class MangaHeaderItem(val manga: Manga, var startExpanded: Boolean) : } override fun hashCode(): Int { - return HashCode.generate(manga.id, manga.title) + return -(manga.id).hashCode() } } 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 891983a25e..551a73ea04 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 @@ -540,12 +540,8 @@ class ReaderPresenter( R.string.cover_updated SetAsCoverResult.Success } else { - manga.thumbnail_url ?: throw Exception("Image url not found") if (manga.favorite) { - coverCache.deleteFromCache(manga) - manga.setCustomThumbnailUrl() - db.insertManga(manga).executeAsBlocking() - coverCache.copyToCache(manga, stream()) + coverCache.setCustomCoverToCache(manga, stream()) SetAsCoverResult.Success } else { SetAsCoverResult.AddToLibraryFirst diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt index d0da5f6fb0..469153412a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt @@ -74,7 +74,7 @@ object ChapterRecognition { } // Remove manga title from chapter title. - val nameWithoutManga = name.replace(manga.title.toLowerCase(), "").trim() + val nameWithoutManga = name.replace(manga.originalTitle.toLowerCase(), "").trim() // Check if first value is number after title remove. if (updateChapter(withoutManga.find(nameWithoutManga), chapter)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt index ed901e2cd4..d8a550a762 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt @@ -22,6 +22,11 @@ fun String.removeArticles(): String { } } +fun String.trimOrNull(): String? { + val trimmed = trim() + return if (trimmed.isBlank()) null else trimmed +} + /** * Replaces the given string to have at most [count] characters using [replacement] near the center. * If [replacement] is longer than [count] an exception will be thrown when `length > count`. diff --git a/app/src/main/res/layout/edit_manga_dialog.xml b/app/src/main/res/layout/edit_manga_dialog.xml index 8e40120de7..9ceed8d3fc 100644 --- a/app/src/main/res/layout/edit_manga_dialog.xml +++ b/app/src/main/res/layout/edit_manga_dialog.xml @@ -10,19 +10,31 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" + android:foreground="?attr/selectableItemBackground" android:layout_marginBottom="10dp"> +