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 | 81 ++++++++++--- .../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 | 85 ++++++-------- .../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, 407 insertions(+), 181 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 f9aa0a6dc3f0..652cf6e214e7 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 f739ace2af70..88a7cf16c74a 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 1192f39b4afa..e10adc2fabe7 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 b8844908051d..e341151d8eb7 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() } @@ -66,28 +72,59 @@ class CoverCache(val context: Context) { } /** - * Returns the cover from cache. + * Returns the custom cover from cache. * - * @param thumbnailUrl the thumbnail url. + * @param manga the manga. * @return cover image. */ - fun getCoverFile(manga: Manga): File { - return File(cacheDir, manga.key()) + fun getCustomCoverFile(manga: Manga): File { + return File(customCoverCacheDir, DiskUtil.hashKeyForDisk(manga.id.toString())) } /** - * Copy the given stream to this cache. + * Saves the given stream as the manga's custom cover to cache. * - * @param thumbnailUrl url of the thumbnail. + * @param manga the manga. * @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) + 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 + } - destFile.outputStream().use { inputStream.copyTo(it) } + /** + * Returns the cover from cache. + * + * @param thumbnailUrl the thumbnail url. + * @return cover image. + */ + fun getCoverFile(manga: Manga): File { + return File(cacheDir, manga.key()) + } + + 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 73dfa7f0f05c..8cc35cbd8c74 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 fc00c99672ef..13281b74fb0b 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 33fc80a2fd98..22f8527d8beb 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 eb50bf2f947f..b1a2c826c4e9 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 c42d86fdbb40..5444e20ba120 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 ab4563e1538c..963151ab89a5 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 7c469d84904e..dc5d8488cacf 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 000000000000..6ce0ce311ff0 --- /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 8bcc845c63ce..cc64e372b02c 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 123affee085e..9144cba791c6 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 0bb65da0c777..e51c7456dadb 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 e81336ebc14d..a339e4bb0215 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 143536c45e4e..f581ad3786fd 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 e83dd1adcd6a..063dc27b7bbb 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) + } else if (resetCover) { + coverCache.deleteCustomCover(manga) + controller.setPaletteColor() } - if (uri != null) editCoverWithStream(uri) - } - - /** - * Remvoe custom cover - */ - fun clearCustomCover() { - if (manga.hasCustomCover()) { - coverCache.deleteFromCache(manga) - manga.removeCustomThumbnailUrl() - db.insertManga(manga).executeAsBlocking() - forceUpdateCovers() - } - } - - 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 6b49dac3fe4b..ce46919bce77 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 566646d06d7a..3e1c9c698460 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 891983a25ee9..551a73ea041d 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 d0da5f6fb050..469153412a6a 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 ed901e2cd4b8..d8a550a76230 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 8e40120de7cb..9ceed8d3fc6f 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"> +