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"> +