mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
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:
parent
e2351b0a18
commit
94fcad3ef5
14 changed files with 189 additions and 52 deletions
|
@ -29,5 +29,25 @@ interface Chapter : SChapter, Serializable {
|
|||
fun create(): Chapter = ChapterImpl().apply {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.lang.toNormalized
|
||||
import eu.kanade.tachiyomi.util.system.await
|
||||
import info.debatty.java.stringsimilarity.NormalizedLevenshtein
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -54,18 +55,21 @@ class SmartSearchEngine(
|
|||
}*/
|
||||
|
||||
suspend fun normalSearch(source: CatalogueSource, title: String): SManga? {
|
||||
val titleNormalized = title.toNormalized()
|
||||
val eligibleManga = supervisorScope {
|
||||
val searchQuery = if (extraSearchParams != null) {
|
||||
"$title ${extraSearchParams.trim()}"
|
||||
} else title
|
||||
val searchResults = source.fetchSearchManga(1, searchQuery, source.getFilterList()).toSingle().await(Schedulers.io())
|
||||
"$titleNormalized ${extraSearchParams.trim()}"
|
||||
} else titleNormalized
|
||||
val searchResults =
|
||||
source.fetchSearchManga(1, searchQuery, source.getFilterList()).toSingle()
|
||||
.await(Schedulers.io())
|
||||
|
||||
if (searchResults.mangas.size == 1) {
|
||||
return@supervisorScope listOf(SearchEntry(searchResults.mangas.first(), 0.0))
|
||||
}
|
||||
|
||||
searchResults.mangas.map {
|
||||
val normalizedDistance = normalizedLevenshtein.similarity(title, it.title)
|
||||
val normalizedDistance = normalizedLevenshtein.similarity(titleNormalized, it.title.toNormalized())
|
||||
SearchEntry(it, normalizedDistance)
|
||||
}.filter { (_, normalizedDistance) ->
|
||||
normalizedDistance >= MIN_NORMAL_ELIGIBLE_THRESHOLD
|
||||
|
|
|
@ -112,7 +112,6 @@ import kotlinx.coroutines.flow.launchIn
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.ArrayList
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
@ -1751,12 +1750,22 @@ class LibraryController(
|
|||
presenter.downloadUnread(selectedMangas.toList())
|
||||
}
|
||||
R.id.action_mark_as_read -> {
|
||||
presenter.markReadStatus(selectedMangas.toList(), true)
|
||||
destroyActionModeIfNeeded()
|
||||
activity!!.materialAlertDialog()
|
||||
.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 -> {
|
||||
presenter.markReadStatus(selectedMangas.toList(), false)
|
||||
destroyActionModeIfNeeded()
|
||||
activity!!.materialAlertDialog()
|
||||
.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 -> {
|
||||
val skipPre = preferences.skipPreMigration().get()
|
||||
|
@ -1772,6 +1781,35 @@ class LibraryController(
|
|||
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() {
|
||||
val context = view?.context ?: return
|
||||
val mangas = selectedMangas.toList()
|
||||
|
|
|
@ -5,6 +5,7 @@ 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
|
||||
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.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
|
@ -1064,23 +1065,49 @@ class LibraryPresenter(
|
|||
}
|
||||
}
|
||||
|
||||
fun markReadStatus(mangaList: List<Manga>, markRead: Boolean) {
|
||||
presenterScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
mangaList.forEach {
|
||||
withContext(Dispatchers.IO) {
|
||||
val chapters = db.getChapters(it).executeAsBlocking()
|
||||
chapters.forEach {
|
||||
it.read = markRead
|
||||
it.last_page_read = 0
|
||||
}
|
||||
db.updateChaptersProgress(chapters).executeAsBlocking()
|
||||
if (markRead && preferences.removeAfterMarkedAsRead()) {
|
||||
deleteChapters(it, chapters)
|
||||
}
|
||||
}
|
||||
fun markReadStatus(
|
||||
mangaList: List<Manga>,
|
||||
markRead: Boolean
|
||||
): HashMap<Manga, List<Chapter>> {
|
||||
val mapMangaChapters = HashMap<Manga, List<Chapter>>()
|
||||
presenterScope.launchIO {
|
||||
mangaList.forEach { manga ->
|
||||
val oldChapters = db.getChapters(manga).executeAsBlocking()
|
||||
val chapters = oldChapters.copy()
|
||||
chapters.forEach {
|
||||
it.read = markRead
|
||||
it.last_page_read = 0
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import eu.kanade.tachiyomi.source.LocalSource
|
|||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
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.isInNightMode
|
||||
import eu.kanade.tachiyomi.util.system.isLTR
|
||||
|
@ -126,7 +127,7 @@ class MangaHeaderHolder(
|
|||
adapter.delegate.favoriteManga(false)
|
||||
}
|
||||
title.setOnClickListener {
|
||||
title.text?.let { adapter.delegate.globalSearch(it.toString()) }
|
||||
title.text?.let { adapter.delegate.globalSearch(it.toString().toNormalized()) }
|
||||
}
|
||||
title.setOnLongClickListener {
|
||||
adapter.delegate.copyToClipboard(title.text.toString(), R.string.title)
|
||||
|
|
|
@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.migration
|
|||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
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.
|
||||
|
@ -13,6 +16,9 @@ class SourceAdapter(val allClickListener: OnAllClickListener) :
|
|||
|
||||
private var items: List<IFlexible<*>>? = null
|
||||
|
||||
val isMultiLanguage =
|
||||
Injekt.get<PreferencesHelper>().enabledLanguages().get().filterNot { it == "all" }.size > 1
|
||||
|
||||
init {
|
||||
setDisplayHeadersAtStartUp(true)
|
||||
}
|
||||
|
|
|
@ -19,7 +19,9 @@ class SourceHolder(view: View, val adapter: SourceAdapter) :
|
|||
val source = item.source
|
||||
|
||||
// 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.
|
||||
itemView.post {
|
||||
|
|
|
@ -33,6 +33,7 @@ import eu.kanade.tachiyomi.ui.migration.MigrationMangaDialog
|
|||
import eu.kanade.tachiyomi.ui.migration.SearchController
|
||||
import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController
|
||||
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.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.launchUI
|
||||
|
@ -330,6 +331,7 @@ class MigrationListController(bundle: Bundle? = null) :
|
|||
} else {
|
||||
sources.filter { it.id != manga.source }
|
||||
}
|
||||
manga.title = manga.title.toNormalized()
|
||||
val searchController = SearchController(manga, validSources)
|
||||
searchController.targetController = this@MigrationListController
|
||||
router.pushController(searchController.withFadeTransaction())
|
||||
|
|
|
@ -51,7 +51,6 @@ import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
|
|||
import eu.kanade.tachiyomi.data.preference.toggle
|
||||
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
|
||||
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.activity.BaseRxActivity
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
|
@ -1379,14 +1378,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
|
||||
override fun onProvideAssistContent(outContent: AssistContent) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
val manga = presenter.manga ?: return
|
||||
val source = presenter.source as? HttpSource ?: return
|
||||
val url = try {
|
||||
source.mangaDetailsRequest(manga).url.toString()
|
||||
} catch (e: Exception) {
|
||||
return
|
||||
}
|
||||
outContent.webUri = Uri.parse(url)
|
||||
val chapterUrl = presenter.getChapterUrl() ?: return
|
||||
outContent.webUri = Uri.parse(chapterUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1526,16 +1519,12 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
|
||||
private fun openMangaInBrowser() {
|
||||
val source = presenter.getSource() ?: return
|
||||
val url = try {
|
||||
source.mangaDetailsRequest(presenter.manga!!).url.toString()
|
||||
} catch (e: Exception) {
|
||||
return
|
||||
}
|
||||
val chapterUrl = presenter.getChapterUrl() ?: return
|
||||
|
||||
val intent = WebViewActivity.newIntent(
|
||||
applicationContext,
|
||||
source.id,
|
||||
url,
|
||||
chapterUrl,
|
||||
presenter.manga!!.title
|
||||
)
|
||||
startActivity(intent)
|
||||
|
|
|
@ -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.ChapterSort
|
||||
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.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.executeOnIO
|
||||
|
@ -565,6 +566,18 @@ class ReaderPresenter(
|
|||
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
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.source
|
|||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
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.
|
||||
|
@ -17,6 +20,9 @@ class SourceAdapter(val controller: BrowseController) :
|
|||
|
||||
val sourceListener: SourceListener = controller
|
||||
|
||||
val isMultiLanguage =
|
||||
Injekt.get<PreferencesHelper>().enabledLanguages().get().filterNot { it == "all" }.size > 1
|
||||
|
||||
override fun onItemSwiped(position: Int, direction: Int) {
|
||||
super.onItemSwiped(position, direction)
|
||||
controller.hideCatalogue(position)
|
||||
|
|
|
@ -29,10 +29,13 @@ class SourceHolder(view: View, val adapter: SourceAdapter) :
|
|||
val source = item.source
|
||||
// setCardEdges(item)
|
||||
|
||||
val underPinnedSection = item.header?.code?.equals(SourcePresenter.PINNED_KEY) ?: false
|
||||
val isPinned = item.isPinned ?: underPinnedSection
|
||||
// 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 {
|
||||
imageTintList = ColorStateList.valueOf(
|
||||
context.getResourceColor(
|
||||
|
|
|
@ -128,14 +128,20 @@ fun syncChaptersWithSource(
|
|||
var now = Date().time
|
||||
|
||||
for (i in toAdd.indices.reversed()) {
|
||||
val c = toAdd[i]
|
||||
c.date_fetch = now++
|
||||
// Try to mark already read chapters as read when the source deletes them
|
||||
if (c.isRecognizedNumber && c.chapter_number in deletedReadChapterNumbers) {
|
||||
c.read = true
|
||||
}
|
||||
if (c.isRecognizedNumber && c.chapter_number in deletedChapterNumbers) {
|
||||
readded.add(c)
|
||||
val chapter = toAdd[i]
|
||||
chapter.date_fetch = now++
|
||||
if (chapter.isRecognizedNumber && chapter.chapter_number in deletedChapterNumbers) {
|
||||
// Try to mark already read chapters as read when the source deletes them
|
||||
if (chapter.chapter_number in deletedReadChapterNumbers) {
|
||||
chapter.read = true
|
||||
}
|
||||
// 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()
|
||||
|
|
|
@ -16,6 +16,8 @@ import androidx.annotation.StringRes
|
|||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue