Snackbar when mark read/unread from library (#1175)

* Normalize manga titles to avoid different apostrophe

* Remove logs normalized

* Add snackbar when mark as read/unread from library

* Improve snackbar when mark as read/unread from library

* Add language in pinned sources and migration

* Fix oldChapters being readded

* Remove tracking library

* Change reader webview url to chapter instead of manga

* add chapterUrl to ReaderActivity.onProvideAssistContent

* remove normalized db titles

* remove setTitleNormalized

* add toNormalized for manualSearch in migration and globalSearch from manga_details

* add confirmation for mark read/unread from library

* add helper method getChapterUrl to presenter
This commit is contained in:
nzoba 2022-04-16 22:46:28 +02:00 committed by GitHub
parent e2351b0a18
commit 94fcad3ef5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 189 additions and 52 deletions

View file

@ -29,5 +29,25 @@ interface Chapter : SChapter, Serializable {
fun create(): Chapter = ChapterImpl().apply { fun create(): Chapter = ChapterImpl().apply {
chapter_number = -1f chapter_number = -1f
} }
fun List<Chapter>.copy(): List<Chapter> {
return map {
ChapterImpl().apply {
copyFrom(it)
}
}
}
}
fun copyFrom(other: Chapter) {
id = other.id
manga_id = other.manga_id
read = other.read
bookmark = other.bookmark
last_page_read = other.last_page_read
pages_left = other.pages_left
date_fetch = other.date_fetch
source_order = other.source_order
copyFrom(other as SChapter)
} }
} }

View file

@ -4,6 +4,7 @@ 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.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.lang.toNormalized
import eu.kanade.tachiyomi.util.system.await import eu.kanade.tachiyomi.util.system.await
import info.debatty.java.stringsimilarity.NormalizedLevenshtein import info.debatty.java.stringsimilarity.NormalizedLevenshtein
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -54,18 +55,21 @@ class SmartSearchEngine(
}*/ }*/
suspend fun normalSearch(source: CatalogueSource, title: String): SManga? { suspend fun normalSearch(source: CatalogueSource, title: String): SManga? {
val titleNormalized = title.toNormalized()
val eligibleManga = supervisorScope { val eligibleManga = supervisorScope {
val searchQuery = if (extraSearchParams != null) { val searchQuery = if (extraSearchParams != null) {
"$title ${extraSearchParams.trim()}" "$titleNormalized ${extraSearchParams.trim()}"
} else title } else titleNormalized
val searchResults = source.fetchSearchManga(1, searchQuery, source.getFilterList()).toSingle().await(Schedulers.io()) val searchResults =
source.fetchSearchManga(1, searchQuery, source.getFilterList()).toSingle()
.await(Schedulers.io())
if (searchResults.mangas.size == 1) { if (searchResults.mangas.size == 1) {
return@supervisorScope listOf(SearchEntry(searchResults.mangas.first(), 0.0)) return@supervisorScope listOf(SearchEntry(searchResults.mangas.first(), 0.0))
} }
searchResults.mangas.map { searchResults.mangas.map {
val normalizedDistance = normalizedLevenshtein.similarity(title, it.title) val normalizedDistance = normalizedLevenshtein.similarity(titleNormalized, it.title.toNormalized())
SearchEntry(it, normalizedDistance) SearchEntry(it, normalizedDistance)
}.filter { (_, normalizedDistance) -> }.filter { (_, normalizedDistance) ->
normalizedDistance >= MIN_NORMAL_ELIGIBLE_THRESHOLD normalizedDistance >= MIN_NORMAL_ELIGIBLE_THRESHOLD

View file

@ -112,7 +112,6 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
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.ArrayList
import java.util.Locale import java.util.Locale
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
@ -1751,12 +1750,22 @@ class LibraryController(
presenter.downloadUnread(selectedMangas.toList()) presenter.downloadUnread(selectedMangas.toList())
} }
R.id.action_mark_as_read -> { R.id.action_mark_as_read -> {
presenter.markReadStatus(selectedMangas.toList(), true) activity!!.materialAlertDialog()
destroyActionModeIfNeeded() .setMessage(R.string.mark_all_chapters_as_read)
.setPositiveButton(R.string.mark_as_read) { _, _ ->
markReadStatus(R.string.marked_as_read, true)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
} }
R.id.action_mark_as_unread -> { R.id.action_mark_as_unread -> {
presenter.markReadStatus(selectedMangas.toList(), false) activity!!.materialAlertDialog()
destroyActionModeIfNeeded() .setMessage(R.string.mark_all_chapters_as_unread)
.setPositiveButton(R.string.mark_as_unread) { _, _ ->
markReadStatus(R.string.marked_as_unread, false)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
} }
R.id.action_migrate -> { R.id.action_migrate -> {
val skipPre = preferences.skipPreMigration().get() val skipPre = preferences.skipPreMigration().get()
@ -1772,6 +1781,35 @@ class LibraryController(
return true return true
} }
private fun markReadStatus(resource: Int, markRead: Boolean) {
val mapMangaChapters = presenter.markReadStatus(selectedMangas.toList(), markRead)
destroyActionModeIfNeeded()
snack?.dismiss()
snack = view?.snack(resource, Snackbar.LENGTH_INDEFINITE) {
anchorView = anchorView()
view.elevation = 15f.dpToPx
var undoing = false
setAction(R.string.undo) {
presenter.undoMarkReadStatus(mapMangaChapters)
undoing = true
}
addCallback(
object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(
transientBottomBar: Snackbar?,
event: Int
) {
super.onDismissed(transientBottomBar, event)
if (!undoing) presenter.confirmMarkReadStatus(
mapMangaChapters, markRead
)
}
}
)
}
(activity as? MainActivity)?.setUndoSnackBar(snack)
}
private fun shareManga() { private fun shareManga() {
val context = view?.context ?: return val context = view?.context ?: return
val mangas = selectedMangas.toList() val mangas = selectedMangas.toList()

View file

@ -5,6 +5,7 @@ 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
import eu.kanade.tachiyomi.data.database.models.Chapter.Companion.copy
import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
@ -1064,23 +1065,49 @@ class LibraryPresenter(
} }
} }
fun markReadStatus(mangaList: List<Manga>, markRead: Boolean) { fun markReadStatus(
presenterScope.launch { mangaList: List<Manga>,
withContext(Dispatchers.IO) { markRead: Boolean
mangaList.forEach { ): HashMap<Manga, List<Chapter>> {
withContext(Dispatchers.IO) { val mapMangaChapters = HashMap<Manga, List<Chapter>>()
val chapters = db.getChapters(it).executeAsBlocking() presenterScope.launchIO {
chapters.forEach { mangaList.forEach { manga ->
it.read = markRead val oldChapters = db.getChapters(manga).executeAsBlocking()
it.last_page_read = 0 val chapters = oldChapters.copy()
} chapters.forEach {
db.updateChaptersProgress(chapters).executeAsBlocking() it.read = markRead
if (markRead && preferences.removeAfterMarkedAsRead()) { it.last_page_read = 0
deleteChapters(it, chapters)
}
}
} }
getLibrary() db.updateChaptersProgress(chapters).executeAsBlocking()
mapMangaChapters[manga] = oldChapters
}
getLibrary()
}
return mapMangaChapters
}
fun undoMarkReadStatus(
mangaList: HashMap<Manga, List<Chapter>>,
) {
launchIO {
mangaList.forEach { (_, chapters) ->
db.updateChaptersProgress(chapters).executeAsBlocking()
}
getLibrary()
}
}
fun confirmMarkReadStatus(
mangaList: HashMap<Manga, List<Chapter>>,
markRead: Boolean
) {
if (preferences.removeAfterMarkedAsRead() && markRead) {
mangaList.forEach { (manga, oldChapters) ->
deleteChapters(manga, oldChapters)
}
if (preferences.downloadBadge().get()) {
requestDownloadBadgesUpdate()
} }
} }
} }

View file

@ -36,6 +36,7 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.lang.toNormalized
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.isInNightMode import eu.kanade.tachiyomi.util.system.isInNightMode
import eu.kanade.tachiyomi.util.system.isLTR import eu.kanade.tachiyomi.util.system.isLTR
@ -126,7 +127,7 @@ class MangaHeaderHolder(
adapter.delegate.favoriteManga(false) adapter.delegate.favoriteManga(false)
} }
title.setOnClickListener { title.setOnClickListener {
title.text?.let { adapter.delegate.globalSearch(it.toString()) } title.text?.let { adapter.delegate.globalSearch(it.toString().toNormalized()) }
} }
title.setOnLongClickListener { title.setOnLongClickListener {
adapter.delegate.copyToClipboard(title.text.toString(), R.string.title) adapter.delegate.copyToClipboard(title.text.toString(), R.string.title)

View file

@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.migration
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/** /**
* Adapter that holds the catalogue cards. * Adapter that holds the catalogue cards.
@ -13,6 +16,9 @@ class SourceAdapter(val allClickListener: OnAllClickListener) :
private var items: List<IFlexible<*>>? = null private var items: List<IFlexible<*>>? = null
val isMultiLanguage =
Injekt.get<PreferencesHelper>().enabledLanguages().get().filterNot { it == "all" }.size > 1
init { init {
setDisplayHeadersAtStartUp(true) setDisplayHeadersAtStartUp(true)
} }

View file

@ -19,7 +19,9 @@ class SourceHolder(view: View, val adapter: SourceAdapter) :
val source = item.source val source = item.source
// Set source name // Set source name
binding.title.text = source.name val sourceName =
if (adapter.isMultiLanguage) source.toString() else source.name.capitalize()
binding.title.text = sourceName
// Set circle letter image. // Set circle letter image.
itemView.post { itemView.post {

View file

@ -33,6 +33,7 @@ import eu.kanade.tachiyomi.ui.migration.MigrationMangaDialog
import eu.kanade.tachiyomi.ui.migration.SearchController import eu.kanade.tachiyomi.ui.migration.SearchController
import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.toNormalized
import eu.kanade.tachiyomi.util.system.executeOnIO import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
@ -330,6 +331,7 @@ class MigrationListController(bundle: Bundle? = null) :
} else { } else {
sources.filter { it.id != manga.source } sources.filter { it.id != manga.source }
} }
manga.title = manga.title.toNormalized()
val searchController = SearchController(manga, validSources) val searchController = SearchController(manga, validSources)
searchController.targetController = this@MigrationListController searchController.targetController = this@MigrationListController
router.pushController(searchController.withFadeTransaction()) router.pushController(searchController.withFadeTransaction())

View file

@ -51,7 +51,6 @@ import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
import eu.kanade.tachiyomi.data.preference.toggle import eu.kanade.tachiyomi.data.preference.toggle
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
@ -1379,14 +1378,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
override fun onProvideAssistContent(outContent: AssistContent) { override fun onProvideAssistContent(outContent: AssistContent) {
super.onProvideAssistContent(outContent) super.onProvideAssistContent(outContent)
val manga = presenter.manga ?: return val chapterUrl = presenter.getChapterUrl() ?: return
val source = presenter.source as? HttpSource ?: return outContent.webUri = Uri.parse(chapterUrl)
val url = try {
source.mangaDetailsRequest(manga).url.toString()
} catch (e: Exception) {
return
}
outContent.webUri = Uri.parse(url)
} }
/** /**
@ -1526,16 +1519,12 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
private fun openMangaInBrowser() { private fun openMangaInBrowser() {
val source = presenter.getSource() ?: return val source = presenter.getSource() ?: return
val url = try { val chapterUrl = presenter.getChapterUrl() ?: return
source.mangaDetailsRequest(presenter.manga!!).url.toString()
} catch (e: Exception) {
return
}
val intent = WebViewActivity.newIntent( val intent = WebViewActivity.newIntent(
applicationContext, applicationContext,
source.id, source.id,
url, chapterUrl,
presenter.manga!!.title presenter.manga!!.title
) )
startActivity(intent) startActivity(intent)

View file

@ -35,6 +35,7 @@ import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType
import eu.kanade.tachiyomi.util.chapter.ChapterFilter import eu.kanade.tachiyomi.util.chapter.ChapterFilter
import eu.kanade.tachiyomi.util.chapter.ChapterSort import eu.kanade.tachiyomi.util.chapter.ChapterSort
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.getUrlWithoutDomain
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.executeOnIO import eu.kanade.tachiyomi.util.system.executeOnIO
@ -565,6 +566,18 @@ class ReaderPresenter(
return viewerChaptersRelay.value?.currChapter return viewerChaptersRelay.value?.currChapter
} }
fun getChapterUrl(): String? {
val source = getSource() ?: return null
val chapterUrl = getCurrentChapter()?.chapter?.url?.getUrlWithoutDomain()
return if (chapterUrl.isNullOrBlank()) try {
val manga = manga ?: return null
source.mangaDetailsRequest(manga).url.toString()
} catch (e: Exception) {
null
} else source.baseUrl + chapterUrl
}
fun getSource() = sourceManager.getOrStub(manga!!.source) as? HttpSource fun getSource() = sourceManager.getOrStub(manga!!.source) as? HttpSource
/** /**

View file

@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.source
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/** /**
* Adapter that holds the catalogue cards. * Adapter that holds the catalogue cards.
@ -17,6 +20,9 @@ class SourceAdapter(val controller: BrowseController) :
val sourceListener: SourceListener = controller val sourceListener: SourceListener = controller
val isMultiLanguage =
Injekt.get<PreferencesHelper>().enabledLanguages().get().filterNot { it == "all" }.size > 1
override fun onItemSwiped(position: Int, direction: Int) { override fun onItemSwiped(position: Int, direction: Int) {
super.onItemSwiped(position, direction) super.onItemSwiped(position, direction)
controller.hideCatalogue(position) controller.hideCatalogue(position)

View file

@ -29,10 +29,13 @@ class SourceHolder(view: View, val adapter: SourceAdapter) :
val source = item.source val source = item.source
// setCardEdges(item) // setCardEdges(item)
val underPinnedSection = item.header?.code?.equals(SourcePresenter.PINNED_KEY) ?: false
val isPinned = item.isPinned ?: underPinnedSection
// Set source name // Set source name
binding.title.text = source.name val sourceName =
if (adapter.isMultiLanguage && underPinnedSection) source.toString() else source.name
binding.title.text = sourceName
val isPinned = item.isPinned ?: item.header?.code?.equals(SourcePresenter.PINNED_KEY) ?: false
binding.sourcePin.apply { binding.sourcePin.apply {
imageTintList = ColorStateList.valueOf( imageTintList = ColorStateList.valueOf(
context.getResourceColor( context.getResourceColor(

View file

@ -128,14 +128,20 @@ fun syncChaptersWithSource(
var now = Date().time var now = Date().time
for (i in toAdd.indices.reversed()) { for (i in toAdd.indices.reversed()) {
val c = toAdd[i] val chapter = toAdd[i]
c.date_fetch = now++ chapter.date_fetch = now++
// Try to mark already read chapters as read when the source deletes them if (chapter.isRecognizedNumber && chapter.chapter_number in deletedChapterNumbers) {
if (c.isRecognizedNumber && c.chapter_number in deletedReadChapterNumbers) { // Try to mark already read chapters as read when the source deletes them
c.read = true if (chapter.chapter_number in deletedReadChapterNumbers) {
} chapter.read = true
if (c.isRecognizedNumber && c.chapter_number in deletedChapterNumbers) { }
readded.add(c) // Try to to use the fetch date it originally had to not pollute 'Updates' tab
toDelete.filter { it.chapter_number == chapter.chapter_number }
.minByOrNull { it.date_fetch }?.let {
chapter.date_fetch = it.date_fetch
}
readded.add(chapter)
} }
} }
val chapters = db.insertChapters(toAdd).executeAsBlocking() val chapters = db.insertChapters(toAdd).executeAsBlocking()

View file

@ -16,6 +16,8 @@ import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
import java.net.URI
import java.net.URISyntaxException
import kotlin.math.floor import kotlin.math.floor
/** /**
@ -146,3 +148,21 @@ fun String.addBetaTag(context: Context): Spanned {
betaSpan.setSpan(ForegroundColorSpan(context.getResourceColor(R.attr.colorSecondary)), length, length + betaText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) betaSpan.setSpan(ForegroundColorSpan(context.getResourceColor(R.attr.colorSecondary)), length, length + betaText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return betaSpan return betaSpan
} }
fun String.toNormalized(): String = replace("", "'")
fun String.getUrlWithoutDomain(): String {
return try {
val uri = URI(this.replace(" ", "%20"))
var out = uri.path
if (uri.query != null) {
out += "?" + uri.query
}
if (uri.fragment != null) {
out += "#" + uri.fragment
}
out
} catch (e: URISyntaxException) {
this
}
}