MangaController overhaul (#7244)

This commit is contained in:
Ivan Iskandar
2022-06-25 22:03:48 +07:00
committed by GitHub
parent cf7ca5bd28
commit 33a778873a
57 changed files with 3701 additions and 2955 deletions
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) {