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.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.download.DownloadManager 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.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
@ -41,6 +42,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { DownloadManager(app) } addSingletonFactory { DownloadManager(app) }
addSingletonFactory { CustomMangaManager(app) }
addSingletonFactory { TrackManager(app) } addSingletonFactory { TrackManager(app) }
addSingletonFactory { Gson() } addSingletonFactory { Gson() }
@ -56,5 +59,7 @@ class AppModule(val app: Application) : InjektModule {
GlobalScope.launch { get<DatabaseHelper>() } GlobalScope.launch { get<DatabaseHelper>() }
GlobalScope.launch { get<DownloadManager>() } GlobalScope.launch { get<DownloadManager>() }
GlobalScope.launch { get<CustomMangaManager>() }
} }
} }

View File

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

View File

@ -15,7 +15,7 @@ object MangaTypeAdapter {
write { write {
beginArray() beginArray()
value(it.url) value(it.url)
value(it.title) value(it.originalTitle)
value(it.source) value(it.source)
value(max(0, it.viewer)) value(max(0, it.viewer))
value(it.chapter_flags) value(it.chapter_flags)

View File

@ -6,6 +6,7 @@ import coil.Coil
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga 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.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.executeOnIO import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@ -29,11 +30,16 @@ import java.io.InputStream
*/ */
class CoverCache(val context: Context) { class CoverCache(val context: Context) {
/** companion object {
* Cache directory used for cache management. private const val COVERS_DIR = "covers"
*/ private const val CUSTOM_COVERS_DIR = "covers/custom"
private val cacheDir = context.getExternalFilesDir("covers") }
?: File(context.filesDir, "covers").also { it.mkdirs() }
/** 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 { fun getChapterCacheSize(): String {
return Formatter.formatFileSize(context, DiskUtil.getDirectorySize(cacheDir)) return Formatter.formatFileSize(context, DiskUtil.getDirectorySize(cacheDir))
@ -50,7 +56,7 @@ class CoverCache(val context: Context) {
val files = cacheDir.listFiles()?.iterator() ?: return@launch val files = cacheDir.listFiles()?.iterator() ?: return@launch
while (files.hasNext()) { while (files.hasNext()) {
val file = files.next() val file = files.next()
if (file.name !in urls) { if (file.isFile && file.name !in urls) {
deletedSize += file.length() deletedSize += file.length()
file.delete() 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. * Returns the cover from cache.
* *
@ -75,19 +120,11 @@ class CoverCache(val context: Context) {
return File(cacheDir, manga.key()) return File(cacheDir, manga.key())
} }
/** fun deleteFromCache(name: String?) {
* Copy the given stream to this cache. if (name.isNullOrEmpty()) return
* val file = getCoverFile(MangaImpl().apply { thumbnail_url = name })
* @param thumbnailUrl url of the thumbnail. Coil.imageLoader(context).invalidate(file.name)
* @param inputStream the stream to copy. if (file.exists()) file.delete()
* @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) }
} }
/** /**
@ -96,13 +133,21 @@ class CoverCache(val context: Context) {
* @param thumbnailUrl the thumbnail url. * @param thumbnailUrl the thumbnail url.
* @return status of deletion. * @return status of deletion.
*/ */
fun deleteFromCache(manga: Manga, deleteMemoryCache: Boolean = true) { fun deleteFromCache(
manga: Manga,
deleteCustom: Boolean = true
) {
// Check if url is empty. // Check if url is empty.
if (manga.thumbnail_url.isNullOrEmpty()) return if (manga.thumbnail_url.isNullOrEmpty()) return
// Remove file // Remove file
val file = getCoverFile(manga) val file = getCoverFile(manga)
if (deleteMemoryCache) Coil.imageLoader(context).invalidate(file.name) if (deleteCustom) deleteCustomCover(manga)
if (file.exists()) file.delete() 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_ID, obj.id)
put(COL_SOURCE, obj.source) put(COL_SOURCE, obj.source)
put(COL_URL, obj.url) put(COL_URL, obj.url)
put(COL_ARTIST, obj.artist) put(COL_ARTIST, obj.originalArtist)
put(COL_AUTHOR, obj.author) put(COL_AUTHOR, obj.originalAuthor)
put(COL_DESCRIPTION, obj.description) put(COL_DESCRIPTION, obj.originalDescription)
put(COL_GENRE, obj.genre) put(COL_GENRE, obj.originalGenre)
put(COL_TITLE, obj.title) put(COL_TITLE, obj.originalTitle)
put(COL_STATUS, obj.status) put(COL_STATUS, obj.status)
put(COL_THUMBNAIL_URL, obj.thumbnail_url) put(COL_THUMBNAIL_URL, obj.thumbnail_url)
put(COL_FAVORITE, obj.favorite) 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Locale import java.util.Locale
import kotlin.random.Random
interface Manga : SManga { interface Manga : SManga {
@ -149,15 +148,6 @@ interface Manga : SManga {
return DiskUtil.hashKeyForDisk(thumbnail_url.orEmpty()) 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 // Used to display the chapter's title one way or another
var displayMode: Int var displayMode: Int
get() = chapter_flags and DISPLAY_MASK 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.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import kotlin.collections.set
open class MangaImpl : Manga { open class MangaImpl : Manga {
@ -14,15 +14,34 @@ open class MangaImpl : Manga {
override lateinit var url: String 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 override var status: Int = 0
@ -42,14 +61,25 @@ open class MangaImpl : Manga {
override var date_added: Long = 0 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) { override fun copyFrom(other: SManga) {
if (other is MangaImpl && (other as MangaImpl)::title.isInitialized && if (other is MangaImpl && other::ogTitle.isInitialized &&
!other.title.isBlank() && other.title != title) { !other.title.isBlank() && other.ogTitle != ogTitle) {
val oldTitle = title val oldTitle = ogTitle
title = other.title title = other.ogTitle
val db: DownloadManager by injectLazy() val db: DownloadManager by injectLazy()
val provider = DownloadProvider(db.context) val provider = DownloadProvider(db.context)
provider.renameMangaFolder(oldTitle, title, source) provider.renameMangaFolder(oldTitle, ogTitle, source)
} }
super.copyFrom(other) super.copyFrom(other)
} }
@ -64,7 +94,7 @@ open class MangaImpl : Manga {
} }
override fun hashCode(): Int { override fun hashCode(): Int {
if (::url.isInitialized) return url.hashCode() return if (::url.isInitialized) url.hashCode()
else return (id ?: 0L).hashCode() else (id ?: 0L).hashCode()
} }
} }

View File

@ -26,11 +26,11 @@ class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_TITLE, manga.title) put(MangaTable.COL_TITLE, manga.originalTitle)
put(MangaTable.COL_GENRE, manga.genre) put(MangaTable.COL_GENRE, manga.originalGenre)
put(MangaTable.COL_AUTHOR, manga.author) put(MangaTable.COL_AUTHOR, manga.originalAuthor)
put(MangaTable.COL_ARTIST, manga.artist) put(MangaTable.COL_ARTIST, manga.originalArtist)
put(MangaTable.COL_DESCRIPTION, manga.description) put(MangaTable.COL_DESCRIPTION, manga.originalDescription)
} }
fun resetToContentValues(manga: Manga) = ContentValues(1).apply { fun resetToContentValues(manga: Manga) = ContentValues(1).apply {

View File

@ -138,11 +138,11 @@ class DownloadCache(
val trueMangaDirs = mangaDirs.mapNotNull { mangaDir -> val trueMangaDirs = mangaDirs.mapNotNull { mangaDir ->
val manga = sourceMangas.firstOrNull()?.find { val manga = sourceMangas.firstOrNull()?.find {
DiskUtil.buildValidFilename( DiskUtil.buildValidFilename(
it.title it.originalTitle
).toLowerCase() == mangaDir.key.toLowerCase() && it.source == sourceValue.key ).toLowerCase() == mangaDir.key.toLowerCase() && it.source == sourceValue.key
} ?: sourceMangas.lastOrNull()?.find { } ?: sourceMangas.lastOrNull()?.find {
DiskUtil.buildValidFilename( DiskUtil.buildValidFilename(
it.title it.originalTitle
).toLowerCase() == mangaDir.key.toLowerCase() && it.source == sourceValue.key ).toLowerCase() == mangaDir.key.toLowerCase() && it.source == sourceValue.key
} }
val id = manga?.id ?: return@mapNotNull null 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. * @param manga the manga to query.
*/ */
fun getMangaDirName(manga: Manga): String { 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 uy.kohesive.injekt.injectLazy
import java.io.File 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 coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager 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 { override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
val cover = data.thumbnail_url val cover = data.thumbnail_url
return when (getResourceType(cover)) { return when (getResourceType(cover)) {
Type.File -> fileLoader(data)
Type.URL -> httpLoader(data, options) Type.URL -> httpLoader(data, options)
Type.CUSTOM -> customLoader(data, options) Type.File -> fileLoader(data)
null -> error("Invalid image") 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 { 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) val coverFile = coverCache.getCoverFile(manga)
if (coverFile.exists()) { if (coverFile.exists()) {
return fileLoader(coverFile) return fileLoader(coverFile)
@ -158,14 +156,13 @@ class MangaFetcher() : Fetcher<Manga> {
private fun getResourceType(cover: String?): Type? { private fun getResourceType(cover: String?): Type? {
return when { return when {
cover.isNullOrEmpty() -> null cover.isNullOrEmpty() -> null
cover.startsWith("http") -> Type.URL cover.startsWith("http") || cover.startsWith("Custom-", true) -> Type.URL
cover.startsWith("Custom-") -> Type.CUSTOM
cover.startsWith("/") || cover.startsWith("file://") -> Type.File cover.startsWith("/") || cover.startsWith("file://") -> Type.File
else -> null else -> null
} }
} }
private enum class Type { 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.request.LoadRequest
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import eu.kanade.tachiyomi.R 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.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
@ -73,6 +74,7 @@ import java.util.concurrent.atomic.AtomicInteger
*/ */
class LibraryUpdateService( class LibraryUpdateService(
val db: DatabaseHelper = Injekt.get(), val db: DatabaseHelper = Injekt.get(),
val coverCache: CoverCache = Injekt.get(),
val sourceManager: SourceManager = Injekt.get(), val sourceManager: SourceManager = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(),
val downloadManager: DownloadManager = Injekt.get(), val downloadManager: DownloadManager = Injekt.get(),
@ -533,15 +535,14 @@ class LibraryUpdateService(
val thumbnailUrl = manga.thumbnail_url val thumbnailUrl = manga.thumbnail_url
manga.copyFrom(networkManga) manga.copyFrom(networkManga)
manga.initialized = true manga.initialized = true
if (thumbnailUrl != manga.thumbnail_url) {
coverCache.deleteFromCache(thumbnailUrl)
// load new covers in background // load new covers in background
if (!manga.hasCustomCover()) { val request =
val request = LoadRequest.Builder(this@LibraryUpdateService) LoadRequest.Builder(this@LibraryUpdateService).data(manga)
.data(manga) .memoryCachePolicy(CachePolicy.DISABLED).build()
.memoryCachePolicy(CachePolicy.DISABLED)
.build()
Coil.imageLoader(this@LibraryUpdateService).execute(request) Coil.imageLoader(this@LibraryUpdateService).execute(request)
} }
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
} }
} }

View File

@ -23,22 +23,31 @@ interface SManga : Serializable {
var initialized: Boolean 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) { fun copyFrom(other: SManga) {
if (other.author != null) if (other.author != null)
author = other.author author = other.originalAuthor
if (other.artist != null) if (other.artist != null)
artist = other.artist artist = other.originalArtist
if (other.description != null) if (other.description != null)
description = other.description description = other.originalDescription
if (other.genre != null) 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 thumbnail_url = other.thumbnail_url
status = other.status status = other.status

View File

@ -37,6 +37,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.ArrayList import java.util.ArrayList
import java.util.Comparator import java.util.Comparator
import java.util.Locale
/** /**
* Presenter of [LibraryController]. * 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.os.Bundle
import android.view.View import android.view.View
import coil.api.loadAny import coil.api.loadAny
import coil.request.Parameters
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView import com.afollestad.materialdialogs.customview.customView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga 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.source.LocalSource
import eu.kanade.tachiyomi.ui.base.controller.DialogController 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 kotlinx.android.synthetic.main.edit_manga_dialog.view.*
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -24,6 +28,8 @@ class EditMangaDialog : DialogController {
private var customCoverUri: Uri? = null private var customCoverUri: Uri? = null
private var willResetCover = false
private val infoController private val infoController
get() = targetController as MangaDetailsController get() = targetController as MangaDetailsController
@ -68,22 +74,58 @@ class EditMangaDialog : DialogController {
view.manga_artist.append(manga.artist ?: "") view.manga_artist.append(manga.artist ?: "")
view.manga_description.append(manga.description ?: "") view.manga_description.append(manga.description ?: "")
view.manga_genres_tags.setTags(manga.genre?.split(", ") ?: emptyList()) 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.manga_genres_tags.clearFocus()
view.cover_layout.setOnClickListener { view.cover_layout.setOnClickListener {
infoController.changeCover() infoController.changeCover()
} }
view.reset_tags.setOnClickListener { resetTags() } 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() { private fun resetTags() {
if (manga.genre.isNullOrBlank() || manga.source == LocalSource.ID) dialogView?.manga_genres_tags?.setTags( if (manga.genre.isNullOrBlank() || manga.source == LocalSource.ID) dialogView?.manga_genres_tags?.setTags(
emptyList() emptyList()
) )
else dialogView?.manga_genres_tags?.setTags(manga.genre?.split(", ")) else dialogView?.manga_genres_tags?.setTags(manga.originalGenre?.split(", "))
} }
fun updateCover(uri: Uri) { fun updateCover(uri: Uri) {
willResetCover = false
dialogView!!.manga_cover.loadAny(uri) dialogView!!.manga_cover.loadAny(uri)
customCoverUri = uri customCoverUri = uri
} }
@ -97,7 +139,7 @@ class EditMangaDialog : DialogController {
infoController.presenter.updateManga(dialogView?.title?.text.toString(), infoController.presenter.updateManga(dialogView?.title?.text.toString(),
dialogView?.manga_author?.text.toString(), dialogView?.manga_artist?.text.toString(), dialogView?.manga_author?.text.toString(), dialogView?.manga_artist?.text.toString(),
customCoverUri, dialogView?.manga_description?.text.toString(), customCoverUri, dialogView?.manga_description?.text.toString(),
dialogView?.manga_genres_tags?.tags) dialogView?.manga_genres_tags?.tags, willResetCover)
} }
private companion object { private companion object {

View File

@ -43,7 +43,6 @@ import coil.request.LoadRequest
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.checkbox.checkBoxPrompt import com.afollestad.materialdialogs.checkbox.checkBoxPrompt
import com.afollestad.materialdialogs.checkbox.isCheckPromptChecked import com.afollestad.materialdialogs.checkbox.isCheckPromptChecked
import com.afollestad.materialdialogs.list.listItems
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.BaseTransientBottomBar
@ -307,7 +306,7 @@ class MangaDetailsController : BaseController,
fun setPaletteColor() { fun setPaletteColor() {
val view = view ?: return 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 -> .target { drawable ->
val bitmap = (drawable as BitmapDrawable).bitmap val bitmap = (drawable as BitmapDrawable).bitmap
// Generate the Palette on a background thread. // Generate the Palette on a background thread.
@ -393,8 +392,8 @@ class MangaDetailsController : BaseController,
presenter.refreshTracking() presenter.refreshTracking()
refreshTracker = null refreshTracker = null
} }
// reset the covers and palette cause user might have set a custom cover // fetch cover again in case the user set a new cover while reading
presenter.forceUpdateCovers(false) setPaletteColor()
val isCurrentController = router?.backstack?.lastOrNull()?.controller() == val isCurrentController = router?.backstack?.lastOrNull()?.controller() ==
this this
if (isCurrentController) { if (isCurrentController) {
@ -693,10 +692,6 @@ class MangaDetailsController : BaseController,
inflater.inflate(R.menu.manga_details, menu) inflater.inflate(R.menu.manga_details, menu)
val editItem = menu.findItem(R.id.action_edit) val editItem = menu.findItem(R.id.action_edit)
editItem.isVisible = presenter.manga.favorite && !presenter.isLockedFromSearch 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 && menu.findItem(R.id.action_download).isVisible = !presenter.isLockedFromSearch &&
manga?.source != LocalSource.ID manga?.source != LocalSource.ID
menu.findItem(R.id.action_mark_all_as_read).isVisible = menu.findItem(R.id.action_mark_all_as_read).isVisible =
@ -745,29 +740,10 @@ class MangaDetailsController : BaseController,
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_edit -> { R.id.action_edit -> {
if (manga?.source == LocalSource.ID) { editMangaDialog = EditMangaDialog(
editMangaDialog = EditMangaDialog( this, presenter.manga
this, presenter.manga )
) editMangaDialog?.showDialog(router)
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_open_in_web_view -> openInWebView()
R.id.action_refresh_tracking -> presenter.refreshTracking(true) 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.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue 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.LibraryServiceListener
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.fetchChapterListAsync
import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync
import eu.kanade.tachiyomi.source.model.SChapter 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.chapter.ChapterItem
import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource 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.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.executeOnIO import eu.kanade.tachiyomi.util.system.executeOnIO
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -43,6 +44,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.OutputStream import java.io.OutputStream
@ -60,6 +62,8 @@ class MangaDetailsPresenter(
private var scope = CoroutineScope(Job() + Dispatchers.Default) private var scope = CoroutineScope(Job() + Dispatchers.Default)
private val customMangaManager: CustomMangaManager by injectLazy()
var isLockedFromSearch = false var isLockedFromSearch = false
var hasRequested = false var hasRequested = false
var isLoading = false var isLoading = false
@ -405,11 +409,10 @@ class MangaDetailsPresenter(
manga.copyFrom(networkManga) manga.copyFrom(networkManga)
manga.initialized = true manga.initialized = true
if (shouldUpdateCover(thumbnailUrl, networkManga)) { if (thumbnailUrl != networkManga.thumbnail_url) {
coverCache.deleteFromCache(manga, false) coverCache.deleteFromCache(thumbnailUrl)
manga.thumbnail_url = networkManga.thumbnail_url
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
forceUpdateCovers() controller.setPaletteColor()
} }
} }
db.insertManga(manga).executeAsBlocking() 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. * Requests an updated list of chapters from the source.
*/ */
@ -666,6 +656,7 @@ class MangaDetailsPresenter(
coverCache.deleteFromCache(manga) coverCache.deleteFromCache(manga)
db.resetMangaInfo(manga).executeAsBlocking() db.resetMangaInfo(manga).executeAsBlocking()
downloadManager.deleteManga(manga, source) downloadManager.deleteManga(manga, source)
customMangaManager.saveMangaInfo(CustomMangaManager.MangaJson(manga.id!!))
asyncUpdateMangaAndChapters(true) asyncUpdateMangaAndChapters(true)
} }
@ -718,36 +709,41 @@ class MangaDetailsPresenter(
artist: String?, artist: String?,
uri: Uri?, uri: Uri?,
description: String?, description: String?,
tags: Array<String>? tags: Array<String>?,
resetCover: Boolean = false
) { ) {
if (manga.source == LocalSource.ID) { if (manga.source == LocalSource.ID) {
manga.title = if (title.isNullOrBlank()) manga.url else title.trim() manga.title = if (title.isNullOrBlank()) manga.url else title.trim()
manga.author = author?.trim() manga.author = author?.trimOrNull()
manga.artist = artist?.trim() manga.artist = artist?.trimOrNull()
manga.description = description?.trim() manga.description = description?.trimOrNull()
val tagsString = tags?.joinToString(", ") { it.capitalize() } val tagsString = tags?.joinToString(", ") { it.capitalize() }
manga.genre = if (tags.isNullOrEmpty()) null else tagsString?.trim() manga.genre = if (tags.isNullOrEmpty()) null else tagsString?.trim()
LocalSource(downloadManager.context).updateMangaInfo(manga) LocalSource(downloadManager.context).updateMangaInfo(manga)
db.updateMangaInfo(manga).executeAsBlocking() 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) if (uri != null) {
} editCoverWithStream(uri)
} else if (resetCover) {
/** coverCache.deleteCustomCover(manga)
* Remvoe custom cover controller.setPaletteColor()
*/
fun clearCustomCover() {
if (manga.hasCustomCover()) {
coverCache.deleteFromCache(manga)
manga.removeCustomThumbnailUrl()
db.insertManga(manga).executeAsBlocking()
forceUpdateCovers()
} }
} controller.updateHeader()
fun forceUpdateCovers(deleteCache: Boolean = true) {
if (deleteCache) coverCache.deleteFromCache(manga)
controller.setPaletteColor()
} }
fun editCoverWithStream(uri: Uri): Boolean { fun editCoverWithStream(uri: Uri): Boolean {
@ -755,16 +751,13 @@ class MangaDetailsPresenter(
downloadManager.context.contentResolver.openInputStream(uri) ?: return false downloadManager.context.contentResolver.openInputStream(uri) ?: return false
if (manga.source == LocalSource.ID) { if (manga.source == LocalSource.ID) {
LocalSource.updateCover(downloadManager.context, manga, inputStream) LocalSource.updateCover(downloadManager.context, manga, inputStream)
forceUpdateCovers() controller.setPaletteColor()
return true return true
} }
if (manga.favorite) { if (manga.favorite) {
coverCache.deleteFromCache(manga) coverCache.setCustomCoverToCache(manga, inputStream)
manga.setCustomThumbnailUrl() controller.setPaletteColor()
db.insertManga(manga).executeAsBlocking()
coverCache.copyToCache(manga, inputStream)
forceUpdateCovers(false)
return true return true
} }
return false return false

View File

@ -137,7 +137,7 @@ class MangaHeaderHolder(
title.text = manga.title title.text = manga.title
if (manga.genre.isNullOrBlank().not()) manga_genres_tags.setTags( 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()) 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() { fun expand() {
sub_item_group.visible() sub_item_group.visible()
if (!showMoreButton) more_button_group.gone() 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.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.system.HashCode
class MangaHeaderItem(val manga: Manga, var startExpanded: Boolean) : class MangaHeaderItem(val manga: Manga, var startExpanded: Boolean) :
AbstractFlexibleItem<MangaHeaderHolder>() { AbstractFlexibleItem<MangaHeaderHolder>() {
@ -46,6 +45,6 @@ class MangaHeaderItem(val manga: Manga, var startExpanded: Boolean) :
} }
override fun hashCode(): Int { 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 R.string.cover_updated
SetAsCoverResult.Success SetAsCoverResult.Success
} else { } else {
manga.thumbnail_url ?: throw Exception("Image url not found")
if (manga.favorite) { if (manga.favorite) {
coverCache.deleteFromCache(manga) coverCache.setCustomCoverToCache(manga, stream())
manga.setCustomThumbnailUrl()
db.insertManga(manga).executeAsBlocking()
coverCache.copyToCache(manga, stream())
SetAsCoverResult.Success SetAsCoverResult.Success
} else { } else {
SetAsCoverResult.AddToLibraryFirst SetAsCoverResult.AddToLibraryFirst

View File

@ -74,7 +74,7 @@ object ChapterRecognition {
} }
// Remove manga title from chapter title. // 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. // Check if first value is number after title remove.
if (updateChapter(withoutManga.find(nameWithoutManga), chapter)) 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. * 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`. * 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_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:foreground="?attr/selectableItemBackground"
android:layout_marginBottom="10dp"> android:layout_marginBottom="10dp">
<ImageView <ImageView
android:id="@+id/manga_cover" android:id="@+id/manga_cover"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:minWidth="50dp" android:minWidth="75dp"
android:layout_height="150dp" android:layout_height="150dp"
android:contentDescription="@string/cover_of_image" android:contentDescription="@string/cover_of_image"
android:background="@drawable/image_border_background" android:background="@drawable/image_border_background"
android:src="@mipmap/ic_launcher"/> android:src="@mipmap/ic_launcher"/>
</FrameLayout> </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 <EditText
android:id="@+id/title" android:id="@+id/title"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -85,7 +97,7 @@
android:id="@+id/reset_tags" android:id="@+id/reset_tags"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/Theme.Widget.Button.Primary" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:textAllCaps="false" android:textAllCaps="false"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"

View File

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