mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-14 04:58:56 +01:00
MangaController overhaul (#7244)
This commit is contained in:
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import java.io.Serializable
|
||||
import eu.kanade.domain.chapter.model.Chapter as DomainChapter
|
||||
|
||||
interface Chapter : SChapter, Serializable {
|
||||
|
||||
@@ -29,3 +30,21 @@ interface Chapter : SChapter, Serializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Chapter.toDomainChapter(): DomainChapter? {
|
||||
if (id == null || manga_id == null) return null
|
||||
return DomainChapter(
|
||||
id = id!!,
|
||||
mangaId = manga_id!!,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
lastPageRead = last_page_read.toLong(),
|
||||
dateFetch = date_fetch,
|
||||
sourceOrder = source_order.toLong(),
|
||||
url = url,
|
||||
name = name,
|
||||
dateUpload = date_upload,
|
||||
chapterNumber = chapter_number,
|
||||
scanlator = scanlator,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,4 +12,24 @@ class LibraryManga : MangaImpl() {
|
||||
get() = readCount > 0
|
||||
|
||||
var category: Int = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is LibraryManga) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
if (unreadCount != other.unreadCount) return false
|
||||
if (readCount != other.readCount) return false
|
||||
if (category != other.category) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = super.hashCode()
|
||||
result = 31 * result + unreadCount
|
||||
result = 31 * result + readCount
|
||||
result = 31 * result + category
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,3 +121,5 @@ fun Source.getNameForMangaInfo(): String {
|
||||
else -> toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun Source.isLocalOrStub(): Boolean = id == LocalSource.ID || this is SourceManager.StubSource
|
||||
|
||||
@@ -57,6 +57,33 @@ open class Page(
|
||||
statusCallback = f
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Page) return false
|
||||
|
||||
if (index != other.index) return false
|
||||
if (url != other.url) return false
|
||||
if (imageUrl != other.imageUrl) return false
|
||||
if (number != other.number) return false
|
||||
if (status != other.status) return false
|
||||
if (progress != other.progress) return false
|
||||
if (statusSubject != other.statusSubject) return false
|
||||
if (statusCallback != other.statusCallback) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = index
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + (imageUrl?.hashCode() ?: 0)
|
||||
result = 31 * result + status
|
||||
result = 31 * result + progress
|
||||
result = 31 * result + (statusSubject?.hashCode() ?: 0)
|
||||
result = 31 * result + (statusCallback?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val QUEUE = 0
|
||||
const val LOAD_PAGE = 1
|
||||
|
||||
@@ -83,7 +83,7 @@ class SearchController(
|
||||
binding.progress.isVisible = isReplacingManga
|
||||
if (!isReplacingManga) {
|
||||
router.popController(this)
|
||||
if (newManga != null) {
|
||||
if (newManga?.id != null) {
|
||||
val newMangaController = RouterTransaction.with(MangaController(newManga.id!!))
|
||||
if (router.backstack.lastOrNull()?.controller is MangaController) {
|
||||
// Replace old MangaController
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class ChangeMangaCoverDialog<T>(bundle: Bundle? = null) :
|
||||
DialogController(bundle) where T : Controller, T : ChangeMangaCoverDialog.Listener {
|
||||
|
||||
private lateinit var manga: Manga
|
||||
|
||||
constructor(target: T, manga: Manga) : this() {
|
||||
targetController = target
|
||||
this.manga = manga
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setTitle(R.string.action_edit_cover)
|
||||
.setPositiveButton(R.string.action_edit) { _, _ ->
|
||||
(targetController as? Listener)?.openMangaCoverPicker(manga)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNeutralButton(R.string.action_delete) { _, _ ->
|
||||
(targetController as? Listener)?.deleteMangaCover(manga)
|
||||
}
|
||||
.create()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun deleteMangaCover(manga: Manga)
|
||||
|
||||
fun openMangaCoverPicker(manga: Manga)
|
||||
}
|
||||
}
|
||||
@@ -420,7 +420,7 @@ class MainActivity : BaseActivity() {
|
||||
SHORTCUT_MANGA -> {
|
||||
val extras = intent.extras ?: return false
|
||||
val fgController = router.backstack.lastOrNull()?.controller as? MangaController
|
||||
if (fgController?.manga?.id != extras.getLong(MangaController.MANGA_EXTRA)) {
|
||||
if (fgController?.mangaId != extras.getLong(MangaController.MANGA_EXTRA)) {
|
||||
router.popToRoot()
|
||||
setSelectedNavItem(R.id.nav_library)
|
||||
router.pushController(RouterTransaction.with(MangaController(extras)))
|
||||
@@ -601,6 +601,9 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
val isFullComposeController = internalTo is FullComposeController<*>
|
||||
binding.appbar.isVisible = !isFullComposeController
|
||||
binding.controllerContainer.enableScrollingBehavior(!isFullComposeController)
|
||||
|
||||
if (!isTablet()) {
|
||||
// Save lift state
|
||||
if (isPush) {
|
||||
@@ -623,17 +626,6 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
binding.root.isLiftAppBarOnScroll = internalTo !is NoAppBarElevationController
|
||||
|
||||
binding.appbar.isVisible = !isFullComposeController
|
||||
binding.controllerContainer.enableScrollingBehavior(!isFullComposeController)
|
||||
|
||||
// TODO: Remove when MangaController is full compose
|
||||
if (!isFullComposeController) {
|
||||
binding.appbar.isTransparentWhenNotLifted = internalTo is MangaController
|
||||
binding.controllerContainer.overlapHeader = internalTo is MangaController
|
||||
}
|
||||
} else {
|
||||
binding.appbar.isVisible = !isFullComposeController
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,127 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.View
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.color
|
||||
import androidx.core.view.isVisible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.ChaptersItemBinding
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterHolder
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import java.util.Date
|
||||
|
||||
class ChapterHolder(
|
||||
view: View,
|
||||
private val adapter: ChaptersAdapter,
|
||||
) : BaseChapterHolder(view, adapter) {
|
||||
|
||||
private val binding = ChaptersItemBinding.bind(view)
|
||||
|
||||
init {
|
||||
binding.download.listener = downloadActionListener
|
||||
}
|
||||
|
||||
fun bind(item: ChapterItem, manga: Manga) {
|
||||
val chapter = item.chapter
|
||||
|
||||
binding.chapterTitle.text = when (manga.displayMode) {
|
||||
Manga.CHAPTER_DISPLAY_NUMBER -> {
|
||||
val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
|
||||
itemView.context.getString(R.string.display_mode_chapter, number)
|
||||
}
|
||||
else -> chapter.name
|
||||
// TODO: show cleaned name consistently around the app
|
||||
// else -> cleanChapterName(chapter, manga)
|
||||
}
|
||||
|
||||
// Set correct text color
|
||||
val chapterTitleColor = when {
|
||||
chapter.read -> adapter.readColor
|
||||
chapter.bookmark -> adapter.bookmarkedColor
|
||||
else -> adapter.unreadColor
|
||||
}
|
||||
binding.chapterTitle.setTextColor(chapterTitleColor)
|
||||
|
||||
val chapterDescriptionColor = when {
|
||||
chapter.read -> adapter.readColor
|
||||
chapter.bookmark -> adapter.bookmarkedColor
|
||||
else -> adapter.unreadColorSecondary
|
||||
}
|
||||
binding.chapterDescription.setTextColor(chapterDescriptionColor)
|
||||
|
||||
binding.bookmarkIcon.isVisible = chapter.bookmark
|
||||
|
||||
val descriptions = mutableListOf<CharSequence>()
|
||||
|
||||
if (chapter.date_upload > 0) {
|
||||
descriptions.add(Date(chapter.date_upload).toRelativeString(itemView.context, adapter.relativeTime, adapter.dateFormat))
|
||||
}
|
||||
if (!chapter.read && chapter.last_page_read > 0) {
|
||||
val lastPageRead = buildSpannedString {
|
||||
color(adapter.readColor) {
|
||||
append(itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1))
|
||||
}
|
||||
}
|
||||
descriptions.add(lastPageRead)
|
||||
}
|
||||
if (!chapter.scanlator.isNullOrBlank()) {
|
||||
descriptions.add(chapter.scanlator!!)
|
||||
}
|
||||
|
||||
if (descriptions.isNotEmpty()) {
|
||||
binding.chapterDescription.text = descriptions.joinTo(SpannableStringBuilder(), " • ")
|
||||
} else {
|
||||
binding.chapterDescription.text = ""
|
||||
}
|
||||
|
||||
binding.download.isVisible = item.manga.source != LocalSource.ID
|
||||
binding.download.setState(item.status, item.progress)
|
||||
}
|
||||
|
||||
private fun cleanChapterName(chapter: Chapter, manga: Manga): String {
|
||||
return chapter.name
|
||||
.trim()
|
||||
.removePrefix(manga.title)
|
||||
.trim(*CHAPTER_TRIM_CHARS)
|
||||
}
|
||||
}
|
||||
|
||||
private val CHAPTER_TRIM_CHARS = arrayOf(
|
||||
// Whitespace
|
||||
' ',
|
||||
'\u0009',
|
||||
'\u000A',
|
||||
'\u000B',
|
||||
'\u000C',
|
||||
'\u000D',
|
||||
'\u0020',
|
||||
'\u0085',
|
||||
'\u00A0',
|
||||
'\u1680',
|
||||
'\u2000',
|
||||
'\u2001',
|
||||
'\u2002',
|
||||
'\u2003',
|
||||
'\u2004',
|
||||
'\u2005',
|
||||
'\u2006',
|
||||
'\u2007',
|
||||
'\u2008',
|
||||
'\u2009',
|
||||
'\u200A',
|
||||
'\u2028',
|
||||
'\u2029',
|
||||
'\u202F',
|
||||
'\u205F',
|
||||
'\u3000',
|
||||
|
||||
// Separators
|
||||
'-',
|
||||
'_',
|
||||
',',
|
||||
':',
|
||||
).toCharArray()
|
||||
@@ -1,33 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterItem
|
||||
|
||||
class ChapterItem(chapter: Chapter, val manga: Manga) :
|
||||
BaseChapterItem<ChapterHolder, AbstractHeaderItem<FlexibleViewHolder>>(chapter) {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.chapters_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): ChapterHolder {
|
||||
return ChapterHolder(view, adapter as ChaptersAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: ChapterHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?,
|
||||
) {
|
||||
holder.bind(this, manga)
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChaptersAdapter
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DateFormat
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
|
||||
class ChaptersAdapter(
|
||||
controller: MangaController,
|
||||
context: Context,
|
||||
) : BaseChaptersAdapter<ChapterItem>(controller) {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
var items: List<ChapterItem> = emptyList()
|
||||
|
||||
val readColor = context.getResourceColor(R.attr.colorOnSurface, 0.38f)
|
||||
val unreadColor = context.getResourceColor(R.attr.colorOnSurface)
|
||||
val unreadColorSecondary = context.getResourceColor(android.R.attr.textColorSecondary)
|
||||
|
||||
val bookmarkedColor = context.getResourceColor(R.attr.colorAccent)
|
||||
|
||||
val decimalFormat = DecimalFormat(
|
||||
"#.###",
|
||||
DecimalFormatSymbols()
|
||||
.apply { decimalSeparator = '.' },
|
||||
)
|
||||
|
||||
val relativeTime: Int = preferences.relativeTime().get()
|
||||
val dateFormat: DateFormat = preferences.dateFormat()
|
||||
|
||||
override fun updateDataSet(items: List<ChapterItem>?) {
|
||||
this.items = items ?: emptyList()
|
||||
super.updateDataSet(items)
|
||||
}
|
||||
|
||||
fun indexOf(item: ChapterItem): Int {
|
||||
return items.indexOf(item)
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,11 @@ import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.toDbManga
|
||||
import eu.kanade.domain.manga.model.toTriStateGroupState
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaPresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenState
|
||||
import eu.kanade.tachiyomi.util.view.popupMenu
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
|
||||
@@ -18,6 +19,9 @@ import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ChaptersSettingsSheet(
|
||||
private val router: Router,
|
||||
@@ -28,7 +32,7 @@ class ChaptersSettingsSheet(
|
||||
|
||||
private var manga: Manga? = null
|
||||
|
||||
val filters = Filter(context)
|
||||
private val filters = Filter(context)
|
||||
private val sort = Sort(context)
|
||||
private val display = Display(context)
|
||||
|
||||
@@ -42,8 +46,14 @@ class ChaptersSettingsSheet(
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
scope = MainScope()
|
||||
// TODO: Listen to changes
|
||||
updateManga()
|
||||
scope.launch {
|
||||
presenter.state
|
||||
.filterIsInstance<MangaScreenState.Success>()
|
||||
.collectLatest {
|
||||
manga = it.manga
|
||||
getTabViews().forEach { settings -> (settings as Settings).updateView() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
@@ -63,17 +73,13 @@ class ChaptersSettingsSheet(
|
||||
R.string.action_display,
|
||||
)
|
||||
|
||||
private fun updateManga() {
|
||||
manga = presenter.manga.toDomainManga()
|
||||
}
|
||||
|
||||
private fun showPopupMenu(view: View) {
|
||||
view.popupMenu(
|
||||
menuRes = R.menu.default_chapter_filter,
|
||||
onMenuItemClick = {
|
||||
when (itemId) {
|
||||
R.id.set_as_default -> {
|
||||
SetChapterSettingsDialog(presenter.manga).showDialog(router)
|
||||
SetChapterSettingsDialog(presenter.manga!!.toDbManga()).showDialog(router)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -144,10 +150,6 @@ class ChaptersSettingsSheet(
|
||||
bookmarked -> presenter.setBookmarkedFilter(newState)
|
||||
else -> {}
|
||||
}
|
||||
|
||||
// TODO: Remove
|
||||
updateManga()
|
||||
updateView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,16 +204,11 @@ class ChaptersSettingsSheet(
|
||||
|
||||
override fun onItemClicked(item: Item) {
|
||||
when (item) {
|
||||
source -> presenter.setSorting(Manga.CHAPTER_SORTING_SOURCE.toInt())
|
||||
chapterNum -> presenter.setSorting(Manga.CHAPTER_SORTING_NUMBER.toInt())
|
||||
uploadDate -> presenter.setSorting(Manga.CHAPTER_SORTING_UPLOAD_DATE.toInt())
|
||||
source -> presenter.setSorting(Manga.CHAPTER_SORTING_SOURCE)
|
||||
chapterNum -> presenter.setSorting(Manga.CHAPTER_SORTING_NUMBER)
|
||||
uploadDate -> presenter.setSorting(Manga.CHAPTER_SORTING_UPLOAD_DATE)
|
||||
else -> throw Exception("Unknown sorting")
|
||||
}
|
||||
|
||||
// TODO: Remove
|
||||
presenter.reverseSortOrder()
|
||||
updateManga()
|
||||
updateView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,14 +254,10 @@ class ChaptersSettingsSheet(
|
||||
if (item.checked) return
|
||||
|
||||
when (item) {
|
||||
displayTitle -> presenter.setDisplayMode(Manga.CHAPTER_DISPLAY_NAME.toInt())
|
||||
displayChapterNum -> presenter.setDisplayMode(Manga.CHAPTER_DISPLAY_NUMBER.toInt())
|
||||
displayTitle -> presenter.setDisplayMode(Manga.CHAPTER_DISPLAY_NAME)
|
||||
displayChapterNum -> presenter.setDisplayMode(Manga.CHAPTER_DISPLAY_NUMBER)
|
||||
else -> throw NotImplementedError("Unknown display mode")
|
||||
}
|
||||
|
||||
// TODO: Remove
|
||||
updateManga()
|
||||
updateView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class DeleteChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : DeleteChaptersDialog.Listener {
|
||||
|
||||
constructor(target: T) : this() {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setMessage(R.string.confirm_delete_chapters)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
(targetController as? Listener)?.deleteChapters()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun deleteChapters()
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.MangaChaptersHeaderBinding
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
|
||||
class MangaChaptersHeaderAdapter(
|
||||
private val controller: MangaController,
|
||||
) :
|
||||
RecyclerView.Adapter<MangaChaptersHeaderAdapter.HeaderViewHolder>() {
|
||||
|
||||
private var numChapters: Int? = null
|
||||
private var hasActiveFilters: Boolean = false
|
||||
|
||||
private lateinit var binding: MangaChaptersHeaderBinding
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
|
||||
binding = MangaChaptersHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return HeaderViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
override fun getItemId(position: Int): Long = hashCode().toLong()
|
||||
|
||||
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
|
||||
holder.bind()
|
||||
}
|
||||
|
||||
fun setNumChapters(numChapters: Int) {
|
||||
this.numChapters = numChapters
|
||||
notifyItemChanged(0, this)
|
||||
}
|
||||
|
||||
fun setHasActiveFilters(hasActiveFilters: Boolean) {
|
||||
this.hasActiveFilters = hasActiveFilters
|
||||
notifyItemChanged(0, this)
|
||||
}
|
||||
|
||||
inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
||||
fun bind() {
|
||||
binding.chaptersLabel.text = if (numChapters == null) {
|
||||
view.context.getString(R.string.chapters)
|
||||
} else {
|
||||
view.context.resources.getQuantityString(R.plurals.manga_num_chapters, numChapters!!, numChapters)
|
||||
}
|
||||
|
||||
val filterColor = if (hasActiveFilters) {
|
||||
view.context.getResourceColor(R.attr.colorFilterActive)
|
||||
} else {
|
||||
view.context.getResourceColor(R.attr.colorOnBackground)
|
||||
}
|
||||
binding.btnChaptersFilter.drawable.setTint(filterColor)
|
||||
|
||||
merge(view.clicks(), binding.btnChaptersFilter.clicks())
|
||||
.onEach { controller.showSettingsSheet() }
|
||||
.launchIn(controller.viewScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.info
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.databinding.MangaInfoHeaderBinding
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
import reactivecircus.flowbinding.android.view.longClicks
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MangaInfoHeaderAdapter(
|
||||
private val controller: MangaController,
|
||||
private val fromSource: Boolean,
|
||||
private val isTablet: Boolean,
|
||||
) :
|
||||
RecyclerView.Adapter<MangaInfoHeaderAdapter.HeaderViewHolder>() {
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
private var manga: Manga = controller.presenter.manga
|
||||
private var source: Source = controller.presenter.source
|
||||
private var trackCount: Int = 0
|
||||
|
||||
private lateinit var binding: MangaInfoHeaderBinding
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
|
||||
binding = MangaInfoHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
updateCoverPosition()
|
||||
|
||||
// Expand manga info if navigated from source listing or explicitly set to
|
||||
// (e.g. on tablets)
|
||||
binding.mangaSummarySection.expanded = fromSource || isTablet
|
||||
|
||||
return HeaderViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
override fun getItemId(position: Int): Long = hashCode().toLong()
|
||||
|
||||
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
|
||||
holder.bind()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the view with manga information.
|
||||
*
|
||||
* @param manga manga object containing information about manga.
|
||||
* @param source the source of the manga.
|
||||
*/
|
||||
fun update(manga: Manga, source: Source) {
|
||||
this.manga = manga
|
||||
this.source = source
|
||||
update()
|
||||
}
|
||||
|
||||
fun update() {
|
||||
notifyItemChanged(0, this)
|
||||
}
|
||||
|
||||
fun setTrackingCount(trackCount: Int) {
|
||||
this.trackCount = trackCount
|
||||
update()
|
||||
}
|
||||
|
||||
private fun updateCoverPosition() {
|
||||
if (isTablet) return
|
||||
val appBarHeight = controller.getMainAppBarHeight()
|
||||
binding.mangaCover.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin += appBarHeight
|
||||
}
|
||||
}
|
||||
|
||||
inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
||||
fun bind() {
|
||||
// For rounded corners
|
||||
binding.mangaCover.clipToOutline = true
|
||||
|
||||
binding.btnFavorite.clicks()
|
||||
.onEach { controller.onFavoriteClick() }
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
if (controller.presenter.manga.favorite) {
|
||||
binding.btnFavorite.longClicks()
|
||||
.onEach { controller.onCategoriesClick() }
|
||||
.launchIn(controller.viewScope)
|
||||
}
|
||||
|
||||
with(binding.btnTracking) {
|
||||
if (trackManager.hasLoggedServices()) {
|
||||
isVisible = true
|
||||
|
||||
if (trackCount > 0) {
|
||||
setIconResource(R.drawable.ic_done_24dp)
|
||||
text = view.context.resources.getQuantityString(
|
||||
R.plurals.num_trackers,
|
||||
trackCount,
|
||||
trackCount,
|
||||
)
|
||||
isActivated = true
|
||||
} else {
|
||||
setIconResource(R.drawable.ic_sync_24dp)
|
||||
text = view.context.getString(R.string.manga_tracking_tab)
|
||||
isActivated = false
|
||||
}
|
||||
|
||||
clicks()
|
||||
.onEach { controller.onTrackingClick() }
|
||||
.launchIn(controller.viewScope)
|
||||
} else {
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
if (controller.presenter.source is HttpSource) {
|
||||
binding.btnWebview.isVisible = true
|
||||
binding.btnWebview.clicks()
|
||||
.onEach { controller.openMangaInWebView() }
|
||||
.launchIn(controller.viewScope)
|
||||
}
|
||||
|
||||
binding.mangaFullTitle.longClicks()
|
||||
.onEach {
|
||||
controller.activity?.copyToClipboard(
|
||||
view.context.getString(R.string.title),
|
||||
binding.mangaFullTitle.text.toString(),
|
||||
)
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
binding.mangaFullTitle.clicks()
|
||||
.onEach {
|
||||
controller.performGlobalSearch(binding.mangaFullTitle.text.toString())
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
binding.mangaAuthor.longClicks()
|
||||
.onEach {
|
||||
controller.activity?.copyToClipboard(
|
||||
binding.mangaAuthor.text.toString(),
|
||||
binding.mangaAuthor.text.toString(),
|
||||
)
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
binding.mangaAuthor.clicks()
|
||||
.onEach {
|
||||
controller.performGlobalSearch(binding.mangaAuthor.text.toString())
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
binding.mangaArtist.longClicks()
|
||||
.onEach {
|
||||
controller.activity?.copyToClipboard(
|
||||
binding.mangaArtist.text.toString(),
|
||||
binding.mangaArtist.text.toString(),
|
||||
)
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
binding.mangaArtist.clicks()
|
||||
.onEach {
|
||||
controller.performGlobalSearch(binding.mangaArtist.text.toString())
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
binding.mangaCover.clicks()
|
||||
.onEach {
|
||||
controller.showFullCoverDialog()
|
||||
}
|
||||
.launchIn(controller.viewScope)
|
||||
|
||||
setMangaInfo()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the view with manga information.
|
||||
*
|
||||
* @param manga manga object containing information about manga.
|
||||
* @param source the source of the manga.
|
||||
*/
|
||||
private fun setMangaInfo() {
|
||||
// Update full title TextView.
|
||||
binding.mangaFullTitle.text = manga.title.ifBlank { view.context.getString(R.string.unknown) }
|
||||
|
||||
// Update author TextView.
|
||||
binding.mangaAuthor.text = if (manga.author.isNullOrBlank()) {
|
||||
view.context.getString(R.string.unknown_author)
|
||||
} else {
|
||||
manga.author
|
||||
}
|
||||
|
||||
// Update artist TextView.
|
||||
val hasArtist = !manga.artist.isNullOrBlank() && manga.artist != manga.author
|
||||
binding.mangaArtist.isVisible = hasArtist
|
||||
if (hasArtist) {
|
||||
binding.mangaArtist.text = manga.artist
|
||||
}
|
||||
|
||||
// If manga source is known update source TextView.
|
||||
binding.mangaMissingSourceIcon.isVisible = source is SourceManager.StubSource
|
||||
|
||||
with(binding.mangaSource) {
|
||||
text = source.getNameForMangaInfo()
|
||||
|
||||
setOnClickListener {
|
||||
controller.performSearch(sourceManager.getOrStub(source.id).name)
|
||||
}
|
||||
}
|
||||
|
||||
// Update manga status.
|
||||
val (statusDrawable, statusString) = when (manga.status) {
|
||||
SManga.ONGOING -> R.drawable.ic_status_ongoing_24dp to R.string.ongoing
|
||||
SManga.COMPLETED -> R.drawable.ic_status_completed_24dp to R.string.completed
|
||||
SManga.LICENSED -> R.drawable.ic_status_licensed_24dp to R.string.licensed
|
||||
SManga.PUBLISHING_FINISHED -> R.drawable.ic_done_24dp to R.string.publishing_finished
|
||||
SManga.CANCELLED -> R.drawable.ic_close_24dp to R.string.cancelled
|
||||
SManga.ON_HIATUS -> R.drawable.ic_pause_24dp to R.string.on_hiatus
|
||||
else -> R.drawable.ic_status_unknown_24dp to R.string.unknown
|
||||
}
|
||||
binding.mangaStatusIcon.setImageResource(statusDrawable)
|
||||
binding.mangaStatus.setText(statusString)
|
||||
|
||||
// Set the favorite drawable to the correct one.
|
||||
setFavoriteButtonState(manga.favorite)
|
||||
|
||||
// Set cover if changed.
|
||||
binding.backdrop.loadAutoPause(manga)
|
||||
binding.mangaCover.loadAutoPause(manga)
|
||||
|
||||
// Manga info section
|
||||
binding.mangaSummarySection.setTags(manga.getGenres(), controller::performGenreSearch)
|
||||
binding.mangaSummarySection.description = manga.description
|
||||
binding.mangaSummarySection.isVisible = !manga.description.isNullOrBlank() || !manga.genre.isNullOrBlank()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update favorite button with correct drawable and text.
|
||||
*
|
||||
* @param isFavorite determines if manga is favorite or not.
|
||||
*/
|
||||
private fun setFavoriteButtonState(isFavorite: Boolean) {
|
||||
// Set the Favorite drawable to the correct one.
|
||||
// Border drawable if false, filled drawable if true.
|
||||
val (iconResource, stringResource) = when (isFavorite) {
|
||||
true -> R.drawable.ic_favorite_24dp to R.string.in_library
|
||||
false -> R.drawable.ic_favorite_border_24dp to R.string.add_to_library
|
||||
}
|
||||
binding.btnFavorite.apply {
|
||||
setIconResource(iconResource)
|
||||
text = context.getString(stringResource)
|
||||
isActivated = isFavorite
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ class TrackSearchDialog : DialogController {
|
||||
|
||||
// Do an initial search based on the manga's title
|
||||
if (savedViewState == null) {
|
||||
currentlySearched = trackController.presenter.manga.title
|
||||
currentlySearched = trackController.presenter.manga!!.title
|
||||
binding!!.titleInput.editText?.append(currentlySearched)
|
||||
}
|
||||
search(currentlySearched)
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.google.android.material.datepicker.CalendarConstraints
|
||||
import com.google.android.material.datepicker.DateValidatorPointBackward
|
||||
import com.google.android.material.datepicker.DateValidatorPointForward
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import eu.kanade.domain.manga.model.toDbManga
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||
import eu.kanade.tachiyomi.databinding.TrackControllerBinding
|
||||
@@ -25,7 +26,7 @@ import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
|
||||
|
||||
class TrackSheet(
|
||||
val controller: MangaController,
|
||||
val fragmentManager: FragmentManager,
|
||||
private val fragmentManager: FragmentManager,
|
||||
) : BaseBottomSheetDialog(controller.activity!!),
|
||||
TrackAdapter.OnClickListener,
|
||||
SetTrackStatusDialog.Listener,
|
||||
@@ -74,8 +75,8 @@ class TrackSheet(
|
||||
|
||||
override fun onSetClick(position: Int) {
|
||||
val item = adapter.getItem(position) ?: return
|
||||
val manga = controller.presenter.manga
|
||||
val source = controller.presenter.source
|
||||
val manga = controller.presenter.manga?.toDbManga() ?: return
|
||||
val source = controller.presenter.source ?: return
|
||||
|
||||
if (item.service is EnhancedTrackService) {
|
||||
if (item.track != null) {
|
||||
|
||||
@@ -34,7 +34,7 @@ class HistoryController : ComposeController<HistoryPresenter>(), RootController
|
||||
nestedScrollInterop = nestedScrollInterop,
|
||||
presenter = presenter,
|
||||
onClickCover = { history ->
|
||||
router.pushController(MangaController(history))
|
||||
router.pushController(MangaController(history.id))
|
||||
},
|
||||
onClickResume = { history ->
|
||||
presenter.getNextChapterForManga(history.mangaId, history.chapterId)
|
||||
|
||||
@@ -7,6 +7,7 @@ import eu.kanade.domain.manga.model.toDbManga
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
@@ -48,19 +49,18 @@ fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
||||
return coverCache.getCustomCoverFile(id).exists()
|
||||
}
|
||||
|
||||
fun Manga.removeCovers(coverCache: CoverCache) {
|
||||
if (isLocal()) return
|
||||
fun Manga.removeCovers(coverCache: CoverCache = Injekt.get()): Int {
|
||||
if (isLocal()) return 0
|
||||
|
||||
cover_last_modified = Date().time
|
||||
coverCache.deleteFromCache(this, true)
|
||||
}
|
||||
|
||||
fun Manga.updateCoverLastModified(db: DatabaseHelper) {
|
||||
cover_last_modified = Date().time
|
||||
db.updateMangaCoverLastModified(this).executeAsBlocking()
|
||||
return coverCache.deleteFromCache(this, true)
|
||||
}
|
||||
|
||||
fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper): Boolean {
|
||||
return toDomainManga()?.shouldDownloadNewChapters(db, prefs) ?: false
|
||||
}
|
||||
|
||||
fun DomainManga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper): Boolean {
|
||||
if (!favorite) return false
|
||||
|
||||
// Boolean to determine if user wants to automatically download new chapters.
|
||||
@@ -75,7 +75,7 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
|
||||
|
||||
// Get all categories, else default category (0)
|
||||
val categoriesForManga =
|
||||
db.getCategoriesForManga(this).executeAsBlocking()
|
||||
db.getCategoriesForManga(toDbManga()).executeAsBlocking()
|
||||
.mapNotNull { it.id }
|
||||
.takeUnless { it.isEmpty() } ?: listOf(0)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.util.chapter
|
||||
|
||||
import eu.kanade.domain.manga.interactor.SetMangaChapterFlags
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@@ -34,6 +35,18 @@ object ChapterSettingsHelper {
|
||||
db.updateChapterFlags(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
suspend fun applySettingDefaults(mangaId: Long, setMangaChapterFlags: SetMangaChapterFlags) {
|
||||
setMangaChapterFlags.awaitSetAllFlags(
|
||||
mangaId = mangaId,
|
||||
unreadFilter = prefs.filterChapterByRead().toLong(),
|
||||
downloadedFilter = prefs.filterChapterByDownloaded().toLong(),
|
||||
bookmarkedFilter = prefs.filterChapterByBookmarked().toLong(),
|
||||
sortingMode = prefs.sortChapterBySourceOrNumber().toLong(),
|
||||
sortingDirection = prefs.sortChapterByAscendingOrDescending().toLong(),
|
||||
displayMode = prefs.displayChapterByNameOrNumber().toLong(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all mangas in library with global Chapter Settings.
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.util.chapter
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.domain.chapter.model.Chapter as DomainChapter
|
||||
import eu.kanade.domain.manga.model.Manga as DomainManga
|
||||
|
||||
fun getChapterSort(manga: Manga, sortDescending: Boolean = manga.sortDescending()): (Chapter, Chapter) -> Int {
|
||||
return when (manga.sorting) {
|
||||
@@ -20,3 +23,28 @@ fun getChapterSort(manga: Manga, sortDescending: Boolean = manga.sortDescending(
|
||||
else -> throw NotImplementedError("Invalid chapter sorting method: ${manga.sorting}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getChapterSort(
|
||||
manga: DomainManga,
|
||||
sortDescending: Boolean = manga.sortDescending(),
|
||||
): (DomainChapter, DomainChapter) -> Int {
|
||||
return when (manga.sorting) {
|
||||
DomainManga.CHAPTER_SORTING_SOURCE -> when (sortDescending) {
|
||||
true -> { c1, c2 -> c1.sourceOrder.compareTo(c2.sourceOrder) }
|
||||
false -> { c1, c2 -> c2.sourceOrder.compareTo(c1.sourceOrder) }
|
||||
}
|
||||
DomainManga.CHAPTER_SORTING_NUMBER -> when (sortDescending) {
|
||||
true -> { c1, c2 ->
|
||||
c2.chapterNumber.toString().compareToCaseInsensitiveNaturalOrder(c1.chapterNumber.toString())
|
||||
}
|
||||
false -> { c1, c2 ->
|
||||
c1.chapterNumber.toString().compareToCaseInsensitiveNaturalOrder(c2.chapterNumber.toString())
|
||||
}
|
||||
}
|
||||
DomainManga.CHAPTER_SORTING_UPLOAD_DATE -> when (sortDescending) {
|
||||
true -> { c1, c2 -> c2.dateUpload.compareTo(c1.dateUpload) }
|
||||
false -> { c1, c2 -> c1.dateUpload.compareTo(c2.dateUpload) }
|
||||
}
|
||||
else -> throw NotImplementedError("Unimplemented sorting method")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
package eu.kanade.tachiyomi.widget
|
||||
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.MangaSummaryBinding
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.view.setChips
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
class MangaSummaryView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
@StyleRes defStyleRes: Int = 0,
|
||||
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
|
||||
|
||||
private val binding = MangaSummaryBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
|
||||
private var animatorSet: AnimatorSet? = null
|
||||
|
||||
private var recalculateHeights = false
|
||||
private var descExpandedHeight = -1
|
||||
private var descShrunkHeight = -1
|
||||
|
||||
var expanded = false
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
updateExpandState()
|
||||
}
|
||||
}
|
||||
|
||||
var description: CharSequence? = null
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = if (value.isNullOrBlank()) {
|
||||
context.getString(R.string.unknown)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
binding.descriptionText.text = field
|
||||
recalculateHeights = true
|
||||
doOnNextLayout {
|
||||
updateExpandState()
|
||||
}
|
||||
if (!isInLayout) {
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setTags(items: List<String>?, onClick: (item: String) -> Unit) {
|
||||
listOfNotNull(binding.tagChipsShrunk, binding.tagChipsExpanded).forEach { chips ->
|
||||
chips.setChips(items, onClick) { tag -> context.copyToClipboard(tag, tag) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExpandState() = binding.apply {
|
||||
val initialSetup = descriptionText.maxHeight < 0
|
||||
|
||||
val maxHeightTarget = if (expanded) descExpandedHeight else descShrunkHeight
|
||||
val maxHeightStart = if (initialSetup) maxHeightTarget else descriptionText.maxHeight
|
||||
val descMaxHeightAnimator = ValueAnimator().apply {
|
||||
setIntValues(maxHeightStart, maxHeightTarget)
|
||||
addUpdateListener {
|
||||
descriptionText.maxHeight = it.animatedValue as Int
|
||||
}
|
||||
}
|
||||
|
||||
val toggleDrawable = ContextCompat.getDrawable(
|
||||
context,
|
||||
if (expanded) R.drawable.anim_caret_up else R.drawable.anim_caret_down,
|
||||
)
|
||||
toggleMore.setImageDrawable(toggleDrawable)
|
||||
|
||||
var pastHalf = false
|
||||
val toggleTarget = if (expanded) 1F else 0F
|
||||
val toggleStart = if (initialSetup) {
|
||||
toggleTarget
|
||||
} else {
|
||||
toggleMore.translationY / toggleMore.height
|
||||
}
|
||||
val toggleAnimator = ValueAnimator().apply {
|
||||
setFloatValues(toggleStart, toggleTarget)
|
||||
addUpdateListener {
|
||||
val value = it.animatedValue as Float
|
||||
|
||||
toggleMore.translationY = toggleMore.height * value
|
||||
descriptionScrim.translationY = toggleMore.translationY
|
||||
toggleMoreScrim.translationY = toggleMore.translationY
|
||||
tagChipsShrunkContainer.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
topMargin = toggleMore.translationY.roundToInt()
|
||||
}
|
||||
tagChipsExpanded.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
topMargin = toggleMore.translationY.roundToInt()
|
||||
}
|
||||
|
||||
// Update non-animatable objects mid-animation makes it feel less abrupt
|
||||
if (it.animatedFraction >= 0.5F && !pastHalf) {
|
||||
pastHalf = true
|
||||
descriptionText.text = trimWhenNeeded(description)
|
||||
tagChipsShrunkContainer.scrollX = 0
|
||||
tagChipsShrunkContainer.isVisible = !expanded
|
||||
tagChipsExpanded.isVisible = expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animatorSet?.cancel()
|
||||
animatorSet = AnimatorSet().apply {
|
||||
interpolator = FastOutSlowInInterpolator()
|
||||
duration = (TOGGLE_ANIM_DURATION * context.animatorDurationScale).roundToLong()
|
||||
playTogether(toggleAnimator, descMaxHeightAnimator)
|
||||
start()
|
||||
}
|
||||
(toggleDrawable as? Animatable)?.start()
|
||||
}
|
||||
|
||||
private fun trimWhenNeeded(text: CharSequence?): CharSequence? {
|
||||
return if (!expanded) {
|
||||
text
|
||||
?.replace(Regex(" +\$", setOf(RegexOption.MULTILINE)), "")
|
||||
?.replace(Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)), "\n")
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
// Wait until parent view has determined the exact width
|
||||
// because this affect the description line count
|
||||
val measureWidthFreely = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY
|
||||
if (!recalculateHeights || measureWidthFreely) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
return
|
||||
}
|
||||
recalculateHeights = false
|
||||
|
||||
// Measure with expanded lines
|
||||
binding.descriptionText.maxLines = Int.MAX_VALUE
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
descExpandedHeight = binding.descriptionText.measuredHeight
|
||||
|
||||
// Measure with shrunk lines
|
||||
binding.descriptionText.maxLines = SHRUNK_DESC_MAX_LINES
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
descShrunkHeight = binding.descriptionText.measuredHeight
|
||||
}
|
||||
|
||||
init {
|
||||
binding.descriptionText.apply {
|
||||
// So that 1 line of text won't be hidden by scrim
|
||||
minLines = DESC_MIN_LINES
|
||||
|
||||
setOnLongClickListener {
|
||||
description?.let {
|
||||
context.copyToClipboard(
|
||||
context.getString(R.string.description),
|
||||
it.toString(),
|
||||
)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
arrayOf(
|
||||
binding.descriptionText,
|
||||
binding.descriptionScrim,
|
||||
binding.toggleMoreScrim,
|
||||
binding.toggleMore,
|
||||
).forEach {
|
||||
it.setOnClickListener { expanded = !expanded }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TOGGLE_ANIM_DURATION = 300L
|
||||
|
||||
private const val DESC_MIN_LINES = 2
|
||||
private const val SHRUNK_DESC_MAX_LINES = 3
|
||||
@@ -4,6 +4,7 @@ import android.view.LayoutInflater
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
@@ -11,6 +12,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.databinding.DialogStubQuadstatemultichoiceBinding
|
||||
import eu.kanade.tachiyomi.databinding.DialogStubTextinputBinding
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
fun MaterialAlertDialogBuilder.setTextInput(
|
||||
hint: String? = null,
|
||||
@@ -71,3 +74,19 @@ fun MaterialAlertDialogBuilder.setQuadStateMultiChoiceItems(
|
||||
}
|
||||
return setView(binding.root)
|
||||
}
|
||||
|
||||
suspend fun MaterialAlertDialogBuilder.await(
|
||||
@StringRes positiveLabelId: Int,
|
||||
@StringRes negativeLabelId: Int,
|
||||
@StringRes neutralLabelId: Int? = null,
|
||||
) = suspendCancellableCoroutine<Int> { cont ->
|
||||
setPositiveButton(positiveLabelId) { _, _ -> cont.resume(AlertDialog.BUTTON_POSITIVE) }
|
||||
setNegativeButton(negativeLabelId) { _, _ -> cont.resume(AlertDialog.BUTTON_NEGATIVE) }
|
||||
if (neutralLabelId != null) {
|
||||
setNeutralButton(neutralLabelId) { _, _ -> cont.resume(AlertDialog.BUTTON_NEUTRAL) }
|
||||
}
|
||||
setOnDismissListener { cont.cancel() }
|
||||
|
||||
val dialog = show()
|
||||
cont.invokeOnCancellation { dialog.dismiss() }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user