diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt index 118344ddb7..bf11df0a28 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt @@ -29,5 +29,25 @@ interface Chapter : SChapter, Serializable { fun create(): Chapter = ChapterImpl().apply { chapter_number = -1f } + + fun List.copy(): List { + 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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt b/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt index 82fbed935d..aea4d15a08 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/smartsearch/SmartSearchEngine.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index cb0cae4756..ea3c4591ce 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -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() { + 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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 957154f4e8..21db7b2772 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -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, 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, + markRead: Boolean + ): HashMap> { + val mapMangaChapters = HashMap>() + 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>, + ) { + launchIO { + mangaList.forEach { (_, chapters) -> + db.updateChaptersProgress(chapters).executeAsBlocking() + } + getLibrary() + } + } + + fun confirmMarkReadStatus( + mangaList: HashMap>, + markRead: Boolean + ) { + if (preferences.removeAfterMarkedAsRead() && markRead) { + mangaList.forEach { (manga, oldChapters) -> + deleteChapters(manga, oldChapters) + } + if (preferences.downloadBadge().get()) { + requestDownloadBadgesUpdate() } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt index e6ba81c19d..8f319b3f19 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceAdapter.kt index 3be604dd7a..7976eaa933 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceAdapter.kt @@ -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>? = null + val isMultiLanguage = + Injekt.get().enabledLanguages().get().filterNot { it == "all" }.size > 1 + init { setDisplayHeadersAtStartUp(true) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt index b6cb79266e..221755504a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt @@ -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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt index dfebcfc801..41409f855f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt @@ -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()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 8e20633f6a..f2e19bd69e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -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() { 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() { 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index d26ef84b02..8da3667d37 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -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 /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourceAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourceAdapter.kt index f732cd60ea..0551a08069 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourceAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourceAdapter.kt @@ -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().enabledLanguages().get().filterNot { it == "all" }.size > 1 + override fun onItemSwiped(position: Int, direction: Int) { super.onItemSwiped(position, direction) controller.hideCatalogue(position) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourceHolder.kt index 388a784dcf..1dea2fdfe6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourceHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/SourceHolder.kt @@ -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( diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index 1a8816c0af..a0df17c7c8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt index 48842c70c6..ad06c31e13 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt @@ -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 + } +}