diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61196b7000..7e1caf8f8e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -148,7 +148,7 @@ dependencies { implementation("com.github.inorichi:unifile:e9ee588") // HTML parser - implementation("org.jsoup:jsoup:1.12.2") + implementation("org.jsoup:jsoup:1.13.1") // Job scheduling implementation("com.evernote:android-job:1.4.2") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt index e7c8621780..69bf1a57f2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt @@ -49,6 +49,7 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.system.sendLocalBroadcast import kotlinx.coroutines.Dispatchers @@ -286,7 +287,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { */ suspend fun restoreMangaFetch(source: Source, manga: Manga): Manga { return withContext(Dispatchers.IO) { - val networkManga = source.fetchMangaDetails(manga).toBlocking().single() + val networkManga = source.fetchMangaDetailsAsync(manga)!! manga.copyFrom(networkManga) manga.favorite = true manga.initialized = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt b/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt index 91000505f5..9fb88c7337 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt @@ -48,11 +48,17 @@ interface Source { fun fetchPageList(chapter: SChapter): Observable> } -suspend fun Source.fetchMangaDetails(manga: SManga): SManga? { +suspend fun Source.fetchMangaDetailsAsync(manga: SManga): SManga? { return withContext(Dispatchers.IO) { fetchMangaDetails(manga).toBlocking().single() } } +suspend fun Source.fetchChapterListAsync(manga: SManga): List? { + return withContext(Dispatchers.IO) { + fetchChapterList(manga).toBlocking().single() + } +} + fun Source.icon(): Drawable? = Injekt.get().getAppIconForSource(this) 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 9bf4345532..f59d6e120f 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 @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.library import android.app.Activity import android.content.Context import android.content.res.ColorStateList +import android.graphics.Color import android.os.Bundle import android.util.TypedValue import android.view.LayoutInflater @@ -67,11 +68,8 @@ import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.util.view.updatePaddingRelative import kotlinx.android.synthetic.main.filter_bottom_sheet.* import kotlinx.android.synthetic.main.library_grid_recycler.* -import kotlinx.android.synthetic.main.library_grid_recycler.recycler import kotlinx.android.synthetic.main.library_list_controller.* -import kotlinx.android.synthetic.main.library_list_controller.swipe_refresh import kotlinx.android.synthetic.main.main_activity.* -import kotlinx.android.synthetic.main.recents_controller.* import kotlinx.coroutines.delay import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -199,9 +197,8 @@ class LibraryController( context, R.drawable.fast_scroll_background ) else null fast_scroller.textColor = ColorStateList.valueOf( - context.getResourceColor( - if (!alwaysShowScroller) android.R.attr.textColorPrimaryInverse else android.R.attr.textColorPrimary - ) + if (!alwaysShowScroller) Color.WHITE + else context.getResourceColor(android.R.attr.textColorPrimary) ) fast_scroller.iconColor = fast_scroller.textColor } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsAdapter.kt index a2b38e4f8f..5b8d04cfee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsAdapter.kt @@ -24,6 +24,7 @@ class MangaDetailsAdapter( private var isAnimating = false val delegate: MangaDetailsInterface = controller + val presenter = controller.presenter val readColor = context.getResourceColor(android.R.attr.textColorHint) @@ -67,6 +68,104 @@ class MangaDetailsAdapter( } } + fun getSectionText(position: Int): String? { + val chapter = getItem(position) as? ChapterItem ?: return null + if (position == itemCount - 1) return "-" + return when (presenter.scrollType) { + MangaDetailsPresenter.MULTIPLE_VOLUMES, MangaDetailsPresenter.MULTIPLE_SEASONS -> + presenter.getGroupNumber(chapter)?.toString() ?: "*" + MangaDetailsPresenter.HUNDREDS_OF_CHAPTERS -> + if (chapter.chapter_number < 0) "*" + else (chapter.chapter_number / 100).toInt().toString() + MangaDetailsPresenter.TENS_OF_CHAPTERS -> + if (chapter.chapter_number < 0) "*" + else (chapter.chapter_number / 10).toInt().toString() + else -> null + } + } + + fun getFullText(position: Int): String { + val chapter = + getItem(position) as? ChapterItem ?: return recyclerView.context.getString(R.string.top) + if (position == itemCount - 1) return recyclerView.context.getString(R.string.bottom) + return when (val scrollType = presenter.scrollType) { + MangaDetailsPresenter.MULTIPLE_VOLUMES, MangaDetailsPresenter.MULTIPLE_SEASONS -> { + val volume = presenter.getGroupNumber(chapter) + if (volume != null) recyclerView.context.getString( + if (scrollType == MangaDetailsPresenter.MULTIPLE_SEASONS) R.string.season_x + else R.string.volume_x, volume) + else recyclerView.context.getString(R.string.unknown) + } + MangaDetailsPresenter.HUNDREDS_OF_CHAPTERS -> recyclerView.context.getString( + R.string.chapters_x, get100sRange( + chapter.chapter_number + ) + ) + MangaDetailsPresenter.TENS_OF_CHAPTERS -> recyclerView.context.getString( + R.string.chapters_x, get10sRange( + chapter.chapter_number + ) + ) + else -> recyclerView.context.getString(R.string.unknown) + } + } + + private fun get100sRange(value: Float): String { + return when (value.toInt()) { + in 0..99 -> "0-99" + in 100..199 -> "100-199" + in 200..299 -> "200-299" + in 300..399 -> "300-399" + in 400..499 -> "400-499" + in 500..599 -> "500-599" + in 600..699 -> "600-699" + in 700..799 -> "700-799" + in 800..899 -> "800-899" + in 900..Int.MAX_VALUE -> "900+" + else -> "None" + } + } + + private fun get10sRange(value: Float): String { + return when (value.toInt()) { + in 0..9 -> "0-9" + in 10..19 -> "10-19" + in 20..29 -> "20-29" + in 30..39 -> "30-39" + in 40..49 -> "40-49" + in 50..59 -> "50-59" + in 60..69 -> "60-69" + in 70..79 -> "70-79" + in 80..89 -> "80-89" + in 80..89 -> "80-89" + in 90..99 -> "90-99" + in 100..109 -> "100-109" + in 110..119 -> "110-119" + in 120..129 -> "120-129" + in 130..139 -> "130-139" + in 140..149 -> "140-149" + in 150..159 -> "150-159" + in 160..169 -> "160-169" + in 170..179 -> "170-179" + in 180..189 -> "180-189" + in 190..199 -> "190-199" + in 190..199 -> "190-199" + in 200..209 -> "200-209" + in 210..219 -> "210-219" + in 220..229 -> "220-229" + in 230..239 -> "230-239" + in 240..249 -> "240-249" + in 250..259 -> "250-259" + in 260..269 -> "260-269" + in 270..279 -> "270-279" + in 280..289 -> "280-289" + in 290..299 -> "290-299" + in 290..299 -> "290-299" + in 300..Int.MAX_VALUE -> "300+" + else -> recyclerView.context.getString(R.string.unknown) + } + } + interface MangaDetailsInterface : MangaHeaderInterface, DownloadInterface interface MangaHeaderInterface { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt index 4c3fc1f368..cba600b32d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -25,6 +25,7 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.view.ViewPropertyAnimator import android.view.animation.DecelerateInterpolator import android.view.inputmethod.InputMethodManager import androidx.appcompat.app.AppCompatActivity @@ -56,6 +57,8 @@ import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.signature.ObjectKey import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar +import com.reddit.indicatorfastscroll.FastScrollItemIndicator +import com.reddit.indicatorfastscroll.FastScrollerView import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.SelectableAdapter import eu.kanade.tachiyomi.R @@ -165,6 +168,8 @@ class MangaDetailsController : BaseController, private var startingDLChapterPos: Int? = null private var editMangaDialog: EditMangaDialog? = null var refreshTracker: Int? = null + private var textAnim: ViewPropertyAnimator? = null + private var scrollAnim: ViewPropertyAnimator? = null /** * Library search query. @@ -184,6 +189,7 @@ class MangaDetailsController : BaseController, // Hold a reference to the current animator, so that it can be canceled mid-way. private var currentAnimator: Animator? = null + var showScroll = false var headerHeight = 0 init { @@ -212,7 +218,6 @@ class MangaDetailsController : BaseController, ) ) recycler.setHasFixedSize(true) - adapter?.fastScroller = fast_scroller val attrsArray = intArrayOf(android.R.attr.actionBarSize) val array = view.context.obtainStyledAttributes(attrsArray) val appbarHeight = array.getDimensionPixelSize(0, 0) @@ -228,15 +233,15 @@ class MangaDetailsController : BaseController, swipe_refresh.setProgressViewOffset(false, (-40).dpToPx, headerHeight + offset) (recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder) ?.setTopHeight(headerHeight) - fast_scroller?.updateLayoutParams { - topMargin = statusBarHeight + fast_scroll_layout.updateLayoutParams { + topMargin = headerHeight bottomMargin = insets.systemWindowInsetBottom } v.updatePaddingRelative(bottom = insets.systemWindowInsetBottom) } presenter.onCreate() - + fast_scroller.translationX = if (showScroll) 0f else 22f.dpToPx recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) @@ -259,6 +264,19 @@ class MangaDetailsController : BaseController, getHeader()?.backdrop?.translationY = 0f activity!!.appbar.y = 0f } + val fPosition = + (recycler.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + if (fPosition > 0 && !showScroll) { + showScroll = true + scrollAnim?.cancel() + scrollAnim = fast_scroller.animate().setDuration(100).translationX(0f) + scrollAnim?.start() + } else if (fPosition <= 0 && showScroll) { + showScroll = false + scrollAnim?.cancel() + scrollAnim = fast_scroller.animate().setDuration(100).translationX(22f.dpToPx) + scrollAnim?.start() + } } override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { @@ -291,6 +309,38 @@ class MangaDetailsController : BaseController, }) setPaletteColor() + fast_scroller.setupWithRecyclerView(recycler, { position -> + val letter = adapter?.getSectionText(position) + when { + presenter.scrollType == 0 -> null + letter != null -> FastScrollItemIndicator.Text(letter) + else -> FastScrollItemIndicator.Icon(R.drawable.star) + } + }) + fast_scroller.useDefaultScroller = false + fast_scroller.itemIndicatorSelectedCallbacks += object : + FastScrollerView.ItemIndicatorSelectedCallback { + override fun onItemIndicatorSelected( + indicator: FastScrollItemIndicator, + indicatorCenterY: Int, + itemPosition: Int + ) { + textAnim?.cancel() + textAnim = text_view_m.animate().alpha(0f).setDuration(250L).setStartDelay(1000) + textAnim?.start() + + text_view_m.translationY = indicatorCenterY.toFloat() - text_view_m.height / 2 + text_view_m.alpha = 1f + text_view_m.text = adapter?.getFullText(itemPosition) + val appbar = activity?.appbar + appbar?.y = 0f + (recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( + itemPosition, headerHeight + ) + colorToolbar(itemPosition > 0, false) + } + } + swipe_refresh.isRefreshing = presenter.isLoading if (manga?.initialized != true) swipe_refresh.post { swipe_refresh.isRefreshing = true } @@ -298,7 +348,8 @@ class MangaDetailsController : BaseController, swipe_refresh.setOnRefreshListener { presenter.refreshAll() } } - fun colorToolbar(isColor: Boolean) { + fun colorToolbar(isColor: Boolean, animate: Boolean = true) { + if (isColor == toolbarIsColored) return toolbarIsColored = isColor val isCurrentController = router?.backstack?.lastOrNull()?.controller() == this@MangaDetailsController @@ -319,6 +370,7 @@ class MangaDetailsController : BaseController, color, if (toolbarIsColored) 175 else 0 ) colorAnimator?.cancel() + if (animate) { colorAnimator = ValueAnimator.ofObject( android.animation.ArgbEvaluator(), colorFrom, colorTo ) @@ -328,6 +380,10 @@ class MangaDetailsController : BaseController, activity?.window?.statusBarColor = (animator.animatedValue as Int) } colorAnimator?.start() + } else { + (activity as MainActivity).toolbar.setBackgroundColor(colorTo) + activity?.window?.statusBarColor = colorTo + } } fun setPaletteColor() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt index 1df060a098..e0054247e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt @@ -24,6 +24,8 @@ import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.fetchChapterListAsync +import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem import eu.kanade.tachiyomi.ui.manga.track.TrackItem @@ -60,6 +62,9 @@ class MangaDetailsPresenter( var isLockedFromSearch = false var hasRequested = false var isLoading = false + var scrollType = 0 + private val volumeRegex = Regex("""(vol|volume)\.? *([0-9]+)?""", RegexOption.IGNORE_CASE) + private val seasonRegex = Regex("""(Season |S)([0-9]+)?""") private val loggedServices by lazy { Injekt.get().services.filter { it.isLogged } } var tracks = emptyList() @@ -242,9 +247,71 @@ class MangaDetailsPresenter( else -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) } } chapters = chapters.sortedWith(Comparator(sortFunction)) + getScrollType(chapters) return chapters } + private fun getScrollType(chapters: List) { + scrollType = when { + hasMultipleVolumes(chapters) -> MULTIPLE_VOLUMES + hasMultipleSeasons(chapters) -> MULTIPLE_SEASONS + hasHundredsOfChapters(chapters) -> HUNDREDS_OF_CHAPTERS + hasTensOfChapters(chapters) -> TENS_OF_CHAPTERS + else -> 0 + } + } + + fun getGroupNumber(chapter: ChapterItem): Int? { + val groups = volumeRegex.find(chapter.name)?.groups + if (groups != null) return groups[2]?.value?.toIntOrNull() + val seasonGroups = seasonRegex.find(chapter.name)?.groups + if (seasonGroups != null) return seasonGroups[2]?.value?.toIntOrNull() + return null + } + + private fun getVolumeNumber(chapter: ChapterItem): Int? { + val groups = volumeRegex.find(chapter.name)?.groups + if (groups != null) return groups[2]?.value?.toIntOrNull() + return null + } + + private fun getSeasonNumber(chapter: ChapterItem): Int? { + val groups = seasonRegex.find(chapter.name)?.groups + if (groups != null) return groups[2]?.value?.toIntOrNull() + return null + } + + private fun hasMultipleVolumes(chapters: List): Boolean { + val volumeSet = mutableSetOf() + chapters.forEach { + val volNum = getVolumeNumber(it) + if (volNum != null) { + volumeSet.add(volNum) + if (volumeSet.size >= 3) return true + } + } + return false + } + + private fun hasMultipleSeasons(chapters: List): Boolean { + val volumeSet = mutableSetOf() + chapters.forEach { + val volNum = getSeasonNumber(it) + if (volNum != null) { + volumeSet.add(volNum) + if (volumeSet.size >= 3) return true + } + } + return false + } + + private fun hasHundredsOfChapters(chapters: List): Boolean { + return chapters.size > 300 + } + + private fun hasTensOfChapters(chapters: List): Boolean { + return chapters.size in 21..300 + } /** * Returns the next unread chapter or null if everything is read. */ @@ -309,7 +376,7 @@ class MangaDetailsPresenter( var chapterError: java.lang.Exception? = null val chapters = async(Dispatchers.IO) { try { - source.fetchChapterList(manga).toBlocking().single() + source.fetchChapterListAsync(manga) } catch (e: Exception) { chapterError = e emptyList() @@ -318,7 +385,7 @@ class MangaDetailsPresenter( val thumbnailUrl = manga.thumbnail_url val nManga = async(Dispatchers.IO) { try { - source.fetchMangaDetails(manga).toBlocking().single() + source.fetchMangaDetailsAsync(manga) } catch (e: java.lang.Exception) { mangaError = e null @@ -342,6 +409,14 @@ class MangaDetailsPresenter( } isLoading = false if (chapterError == null) withContext(Dispatchers.Main) { controller.updateChapters(this@MangaDetailsPresenter.chapters) } + else { + withContext(Dispatchers.Main) { + controller.showError( + trimException(mangaError!!) + ) + } + return@launch + } if (mangaError != null) withContext(Dispatchers.Main) { controller.showError( trimException(mangaError!!) @@ -359,7 +434,7 @@ class MangaDetailsPresenter( scope.launch(Dispatchers.IO) { val chapters = try { - source.fetchChapterList(manga).toBlocking().single() + source.fetchChapterListAsync(manga) } catch (e: Exception) { withContext(Dispatchers.Main) { controller.showError(trimException(e)) } return@launch @@ -760,4 +835,11 @@ class MangaDetailsPresenter( track.last_chapter_read = chapterNumber updateRemote(track, item.service) } + + companion object { + const val MULTIPLE_VOLUMES = 1 + const val TENS_OF_CHAPTERS = 2 + const val HUNDREDS_OF_CHAPTERS = 3 + const val MULTIPLE_SEASONS = 4 + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 360ab9841e..f3003dcf31 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -5,8 +5,6 @@ import android.os.Bundle import android.widget.Toast import androidx.preference.PreferenceScreen import com.afollestad.materialdialogs.MaterialDialog -import com.bluelinelabs.conductor.RouterTransaction -import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.database.DatabaseHelper @@ -16,7 +14,6 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.CoroutineStart @@ -152,13 +149,6 @@ class SettingsAdvancedController : SettingsController() { } private fun clearDatabase() { - // Avoid weird behavior by going back to the library. - val newBackstack = listOf(RouterTransaction.with( - LibraryController())) + - router.backstack.drop(1) - - router.setBackstack(newBackstack, FadeChangeHandler()) - db.deleteMangasNotInLibrary().executeAsBlocking() db.deleteHistoryNoLastRead().executeAsBlocking() activity?.toast(R.string.clear_database_completed) diff --git a/app/src/main/res/layout/chapters_item.xml b/app/src/main/res/layout/chapters_item.xml index 55c89824e6..05bc229ff7 100644 --- a/app/src/main/res/layout/chapters_item.xml +++ b/app/src/main/res/layout/chapters_item.xml @@ -94,7 +94,7 @@ layout="@layout/download_button" android:layout_width="50dp" android:layout_height="0dp" - android:layout_marginEnd="8dp" + android:layout_marginEnd="12dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/library_list_controller.xml b/app/src/main/res/layout/library_list_controller.xml index fab06492e0..ac95878d60 100644 --- a/app/src/main/res/layout/library_list_controller.xml +++ b/app/src/main/res/layout/library_list_controller.xml @@ -32,6 +32,7 @@ android:elevation="10dp" android:layout_gravity="end" android:background="@drawable/fast_scroll_background" + android:backgroundTint="@color/md_grey_800" android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingStart="3dp" diff --git a/app/src/main/res/layout/manga_details_controller.xml b/app/src/main/res/layout/manga_details_controller.xml index b3fdf262ed..28288f0ada 100644 --- a/app/src/main/res/layout/manga_details_controller.xml +++ b/app/src/main/res/layout/manga_details_controller.xml @@ -25,13 +25,44 @@ app:layout_constraintTop_toBottomOf="@id/chapters_title" tools:listitem="@layout/chapters_item"/> - + + + + + + + %d categories Top + Bottom + Volume %1$d + Season %1$d + Chapters %1$s Updates Library update frequency