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)
This commit is contained in:
Jay 2020-05-20 01:04:19 -04:00
parent 0809a7b7ff
commit d3ec230d4b
25 changed files with 409 additions and 183 deletions

View File

@ -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<DatabaseHelper>() }
GlobalScope.launch { get<DownloadManager>() }
GlobalScope.launch { get<CustomMangaManager>() }
}
}

View File

@ -78,6 +78,9 @@ object Migrations {
BackupCreatorJob.setupTask()
ExtensionUpdateJob.setupTask()
}
if (oldVersion < 66) {
LibraryPresenter.updateCustoms()
}
return true
}
return false

View File

@ -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)

View File

@ -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() }
}
}

View File

@ -52,11 +52,11 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
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)

View File

@ -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

View File

@ -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()
}
}

View File

@ -26,11 +26,11 @@ class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
.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 {

View File

@ -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

View File

@ -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)
}
/**

View File

@ -28,7 +28,11 @@ import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
class MangaFetcher() : Fetcher<Manga> {
class MangaFetcher : Fetcher<Manga> {
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<Manga> {
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<Manga> {
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;
}
}

View File

@ -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<Long, Manga>()
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<String>? = 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()
}
}
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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()
}
}
}
}
}
}

View File

@ -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 {

View File

@ -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()
}
}
}
R.id.action_open_in_web_view -> openInWebView()
R.id.action_refresh_tracking -> presenter.refreshTracking(true)

View File

@ -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,53 +709,55 @@ class MangaDetailsPresenter(
artist: String?,
uri: Uri?,
description: String?,
tags: Array<String>?
tags: Array<String>?,
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
}
if (uri != null) editCoverWithStream(uri)
val manga = CustomMangaManager.MangaJson(
manga.id!!,
title?.trimOrNull(),
author?.trimOrNull(),
artist?.trimOrNull(),
description?.trimOrNull(),
genre
)
customMangaManager.saveMangaInfo(manga)
}
/**
* 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)
if (uri != null) {
editCoverWithStream(uri)
} else if (resetCover) {
coverCache.deleteCustomCover(manga)
controller.setPaletteColor()
}
controller.updateHeader()
}
fun editCoverWithStream(uri: Uri): Boolean {
val inputStream =
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

View File

@ -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()

View File

@ -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<MangaHeaderHolder>() {
@ -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()
}
}

View File

@ -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

View File

@ -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))

View File

@ -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`.

View File

@ -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">
<ImageView
android:id="@+id/manga_cover"
android:layout_width="wrap_content"
android:adjustViewBounds="true"
android:minWidth="50dp"
android:minWidth="75dp"
android:layout_height="150dp"
android:contentDescription="@string/cover_of_image"
android:background="@drawable/image_border_background"
android:src="@mipmap/ic_launcher"/>
</FrameLayout>
<Button
android:id="@+id/reset_cover"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Theme.Widget.Button.Primary"
android:textAllCaps="false"
android:layout_gravity="center"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:text="@string/reset_cover" />
<EditText
android:id="@+id/title"
android:layout_width="match_parent"
@ -85,7 +97,7 @@
android:id="@+id/reset_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Theme.Widget.Button.Primary"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:textAllCaps="false"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"

View File

@ -380,7 +380,7 @@
<string name="newest_first">Newest to oldest</string>
<string name="oldest_first">Oldest to newest</string>
<string name="clear_tags">Clear Tags</string>
<string name="edit_cover">Edit cover</string>
<string name="reset_tags">Reset Tags</string>
<string name="reset_cover">Reset cover</string>
<string name="failed_to_update_cover">Failed to update cover</string>
<string name="must_be_in_library_to_edit">Manga must be in your library to edit</string>