Feature: episode sort button (#1565)

- Add sort direction indicator (▲/▼) to button text
This commit is contained in:
Gaurang Khatavkar
2025-03-01 01:56:40 +05:30
committed by GitHub
parent 846ff38c3b
commit ab23f23c71
6 changed files with 281 additions and 72 deletions

View File

@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.result
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Rect
@ -36,6 +37,7 @@ import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding
import com.lagradost.cloudstream3.databinding.FragmentResultBinding
import com.lagradost.cloudstream3.databinding.FragmentResultSwipeBinding
import com.lagradost.cloudstream3.databinding.ResultRecommendationsBinding
@ -253,6 +255,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
var selectSeason: String? = null
var selectEpisodeRange: String? = null
var selectSort: EpisodeSortType? = null
private fun setUrl(url: String?) {
if (url == null) {
@ -358,7 +361,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
binding?.resultSearch?.setOnClickListener {
QuickSearchFragment.pushSearch(activity, storedData.name)
}
resultBinding?.apply {
resultReloadConnectionerror.setOnClickListener {
viewModel.load(
@ -406,8 +409,32 @@ open class ResultFragmentPhone : FullScreenPlayer() {
{ downloadClickEvent ->
DownloadButtonSetup.handleDownloadClick(downloadClickEvent)
}
)
observeNullable(viewModel.selectedSorting) {
resultSortButton.setText(it)
}
observe(viewModel.sortSelections) { sort ->
resultBinding?.resultSortButton?.setOnClickListener { view ->
view?.context?.let { ctx ->
val names = sort
.mapNotNull { (text, r) ->
r to (text.asStringNull(ctx) ?: return@mapNotNull null)
}
activity?.showDialog(
names.map { it.second },
viewModel.selectedSortingIndex.value ?: -1,
"",
false,
{}) { itemId ->
viewModel.setSort(names[itemId].first)
}
}
}
}
resultScroll.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
val dy = scrollY - oldScrollY
@ -458,8 +485,12 @@ open class ResultFragmentPhone : FullScreenPlayer() {
}
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: com.lagradost.cloudstream3.utils.txt(R.string.no_data).asStringNull(context) ?: ""
showToast(com.lagradost.cloudstream3.utils.txt(message, name), Toast.LENGTH_SHORT)
?: com.lagradost.cloudstream3.utils.txt(R.string.no_data)
.asStringNull(context) ?: ""
showToast(
com.lagradost.cloudstream3.utils.txt(message, name),
Toast.LENGTH_SHORT
)
}
context?.let { openBatteryOptimizationSettings(it) }
}
@ -474,8 +505,12 @@ open class ResultFragmentPhone : FullScreenPlayer() {
}
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: com.lagradost.cloudstream3.utils.txt(R.string.no_data).asStringNull(context) ?: ""
showToast(com.lagradost.cloudstream3.utils.txt(message, name), Toast.LENGTH_SHORT)
?: com.lagradost.cloudstream3.utils.txt(R.string.no_data)
.asStringNull(context) ?: ""
showToast(
com.lagradost.cloudstream3.utils.txt(message, name),
Toast.LENGTH_SHORT
)
}
}
mediaRouteButton.apply {
@ -627,6 +662,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
// no failure?
resultEpisodeLoading.isVisible = episodes is Resource.Loading
resultEpisodes.isVisible = episodes is Resource.Success
resultSortButton.isVisible = episodes is Resource.Success
if (episodes is Resource.Success) {
(resultEpisodes.adapter as? EpisodeAdapter)?.updateList(episodes.value)
}
@ -713,10 +749,23 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultNextAiring.setText(d.nextAiringEpisode)
resultNextAiringTime.setText(d.nextAiringDate)
resultPoster.loadImage(d.posterImage, headers = d.posterHeaders) {
error{ getImageFromDrawable(context?: return@error null, R.drawable.default_cover) }
error {
getImageFromDrawable(
context ?: return@error null,
R.drawable.default_cover
)
}
}
resultPosterBackground.loadImage(d.posterBackgroundImage, headers = d.posterHeaders) {
error{ getImageFromDrawable(context?: return@error null, R.drawable.default_cover) }
resultPosterBackground.loadImage(
d.posterBackgroundImage,
headers = d.posterHeaders
) {
error {
getImageFromDrawable(
context ?: return@error null,
R.drawable.default_cover
)
}
}
var isExpanded = false
@ -790,7 +839,10 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultReloadConnectionOpenInBrowser.isVisible = data is Resource.Failure
resultTitle.setOnLongClickListener {
clipboardHelper(com.lagradost.cloudstream3.utils.txt(R.string.title), resultTitle.text)
clipboardHelper(
com.lagradost.cloudstream3.utils.txt(R.string.title),
resultTitle.text
)
true
}
}

View File

@ -145,6 +145,15 @@ enum class LibraryListType {
SUBSCRIPTIONS
}
enum class EpisodeSortType {
NUMBER_ASC,
NUMBER_DESC,
RATING_HIGH_LOW,
RATING_LOW_HIGH,
DATE_NEWEST,
DATE_OLDEST
}
fun txt(status: DubStatus?): UiText? {
return txt(
when (status) {
@ -399,6 +408,7 @@ class ResultViewModel2 : ViewModel() {
private var currentMeta: SyncAPI.SyncResult? = null
private var currentSync: Map<String, String>? = null
private var currentIndex: EpisodeIndexer? = null
private var currentSorting: EpisodeSortType? = null
private var currentRange: EpisodeRange? = null
private var currentShowFillers: Boolean = false
var currentRepo: APIRepository? = null
@ -452,6 +462,18 @@ class ResultViewModel2 : ViewModel() {
MutableLiveData(null)
val selectedRange: LiveData<UiText?> = _selectedRange
private val _selectedSorting: MutableLiveData<UiText?> =
MutableLiveData(null)
val selectedSorting: LiveData<UiText?> = _selectedSorting
private val _selectedSortingIndex: MutableLiveData<Int> =
MutableLiveData(-1)
val selectedSortingIndex: LiveData<Int> = _selectedSortingIndex
private val _sortSelections: MutableLiveData<List<Pair<UiText, EpisodeSortType>>> =
MutableLiveData(emptyList())
val sortSelections: LiveData<List<Pair<UiText, EpisodeSortType>>> = _sortSelections
private val _selectedSeason: MutableLiveData<UiText?> =
MutableLiveData(null)
val selectedSeason: LiveData<UiText?> = _selectedSeason
@ -770,13 +792,17 @@ class ResultViewModel2 : ViewModel() {
val generator = RepoLinkGenerator(listOf(episode))
val currentLinks = mutableSetOf<ExtractorLink>()
val currentSubs = mutableSetOf<SubtitleData>()
generator.generateLinks(clearCache = false, allowedTypes = LOADTYPE_INAPP_DOWNLOAD, callback = {
it.first?.let { link ->
currentLinks.add(link)
}
}, subtitleCallback = { sub ->
currentSubs.add(sub)
})
generator.generateLinks(
clearCache = false,
allowedTypes = LOADTYPE_INAPP_DOWNLOAD,
callback = {
it.first?.let { link ->
currentLinks.add(link)
}
},
subtitleCallback = { sub ->
currentSubs.add(sub)
})
if (currentLinks.isEmpty()) {
main {
@ -921,7 +947,12 @@ class ResultViewModel2 : ViewModel() {
isVisible: Boolean = true
) {
if (activity == null) return
loadLinks(result, isVisible = isVisible, sourceTypes = LOADTYPE_CHROMECAST, isCasting = true) { data ->
loadLinks(
result,
isVisible = isVisible,
sourceTypes = LOADTYPE_CHROMECAST,
isCasting = true
) { data ->
startChromecast(activity, result, data.links, data.subs, 0)
}
}
@ -1344,7 +1375,8 @@ class ResultViewModel2 : ViewModel() {
}
try {
updatePage()
tempGenerator.generateLinks(clearCache,
tempGenerator.generateLinks(
clearCache,
allowedTypes = sourceTypes,
callback = { (link, _) ->
if (link != null) {
@ -1353,10 +1385,11 @@ class ResultViewModel2 : ViewModel() {
}
},
subtitleCallback = { sub ->
subs += sub
updatePage()
},
isCasting = isCasting)
subs += sub
updatePage()
},
isCasting = isCasting
)
} catch (e: Exception) {
logError(e)
} finally {
@ -1806,15 +1839,21 @@ class ResultViewModel2 : ViewModel() {
}
fun changeDubStatus(status: DubStatus) {
postEpisodeRange(currentIndex?.copy(dubStatus = status), currentRange)
postEpisodeRange(currentIndex?.copy(dubStatus = status), currentRange, currentSorting)
}
fun changeRange(range: EpisodeRange) {
postEpisodeRange(currentIndex, range)
postEpisodeRange(currentIndex, range, currentSorting)
}
fun changeSeason(season: Int) {
postEpisodeRange(currentIndex?.copy(season = season), currentRange)
postEpisodeRange(currentIndex?.copy(season = season), currentRange, currentSorting)
}
fun setSort(sortType: EpisodeSortType) {
// we only update here as postEpisodeRange might change the sorting mode if it does not fit
DataStoreHelper.resultsSortingMode = sortType
postEpisodeRange(currentIndex, currentRange, sortType)
}
private fun getMovie(): ResultEpisode? {
@ -1824,26 +1863,37 @@ class ResultViewModel2 : ViewModel() {
}
}
private fun getEpisodes(indexer: EpisodeIndexer, range: EpisodeRange): List<ResultEpisode> {
val startIndex = range.startIndex
val length = range.length
return currentEpisodes[indexer]
?.let { list ->
val start = minOf(list.size, startIndex)
val end = minOf(list.size, start + length)
list.subList(start, end).map {
val posDur = getViewPos(it.id)
val watchState =
getVideoWatchState(it.id) ?: VideoWatchState.None
it.copy(
position = posDur?.position ?: 0,
duration = posDur?.duration ?: 0,
videoWatchState = watchState
)
}
private fun getEpisodes(
indexer: EpisodeIndexer,
range: EpisodeRange,
): List<ResultEpisode> {
return currentEpisodes[indexer]?.let { list ->
val start = minOf(list.size, range.startIndex)
val end = minOf(list.size, start + range.length)
list.subList(start, end).map {
val posDur = getViewPos(it.id)
val watchState = getVideoWatchState(it.id) ?: VideoWatchState.None
it.copy(
position = posDur?.position ?: 0,
duration = posDur?.duration ?: 0,
videoWatchState = watchState
)
}
?: emptyList()
} ?: emptyList()
}
private fun getSortedEpisodes(
episodes: List<ResultEpisode>,
sorting: EpisodeSortType
): List<ResultEpisode> {
return when (sorting) {
EpisodeSortType.NUMBER_ASC -> episodes.sortedBy { it.episode }
EpisodeSortType.NUMBER_DESC -> episodes.sortedByDescending { it.episode }
EpisodeSortType.RATING_HIGH_LOW -> episodes.sortedByDescending { it.rating ?: 0 }
EpisodeSortType.RATING_LOW_HIGH -> episodes.sortedBy { it.rating ?: 0 }
EpisodeSortType.DATE_NEWEST -> episodes.sortedByDescending { it.airDate }
EpisodeSortType.DATE_OLDEST -> episodes.sortedBy { it.airDate }
}
}
private fun postMovie() {
@ -1882,9 +1932,11 @@ class ResultViewModel2 : ViewModel() {
} else {
_episodes.postValue(
Resource.Success(
getEpisodes(
currentIndex ?: return,
currentRange ?: return
getSortedEpisodes(
getEpisodes(
currentIndex ?: return,
currentRange ?: return,
), currentSorting ?: return
)
)
)
@ -1912,8 +1964,24 @@ class ResultViewModel2 : ViewModel() {
_favoriteStatus.postValue(isFavorite)
}
private fun postEpisodeRange(indexer: EpisodeIndexer?, range: EpisodeRange?) {
if (range == null || indexer == null) {
private fun shouldEnableSort(type: EpisodeSortType, episodes: List<ResultEpisode>?): Boolean {
if (episodes.isNullOrEmpty()) return false
return when (type) {
EpisodeSortType.NUMBER_ASC, EpisodeSortType.NUMBER_DESC -> true
EpisodeSortType.RATING_HIGH_LOW, EpisodeSortType.RATING_LOW_HIGH ->
episodes.any { it.rating != null }
EpisodeSortType.DATE_NEWEST, EpisodeSortType.DATE_OLDEST ->
episodes.any { it.airDate != null }
}
}
private fun postEpisodeRange(
indexer: EpisodeIndexer?,
range: EpisodeRange?,
sorting: EpisodeSortType?
) {
if (range == null || indexer == null || sorting == null) {
return
}
@ -1921,10 +1989,10 @@ class ResultViewModel2 : ViewModel() {
if (ranges?.contains(range) != true) {
// if the current ranges does not include the range then select the range with the closest matching start episode
// this usually happends when dub has less episodes then sub -> the range does not exist
// this usually happens when dub has less episodes then sub -> the range does not exist
ranges?.minByOrNull { kotlin.math.abs(it.startEpisode - range.startEpisode) }
?.let { r ->
postEpisodeRange(indexer, r)
postEpisodeRange(indexer, r, sorting)
return
}
}
@ -2027,16 +2095,64 @@ class ResultViewModel2 : ViewModel() {
}
if (isMovie) {
_sortSelections.postValue(emptyList())
_selectedSortingIndex.postValue(-1)
_selectedSorting.postValue(null)
postMovie()
} else {
val ret = getEpisodes(indexer, range)
/*if (ret.isEmpty()) {
val index = ranges?.indexOf(range)
if(index != null && index > 0) {
if (ret.size <= 1) {
// we cant sort on an empty list or a list with only 1 episode
_sortSelections.postValue(emptyList())
_selectedSortingIndex.postValue(-1)
_selectedSorting.postValue(null)
_episodes.postValue(Resource.Success(ret))
} else {
val sortOptions = mutableListOf<Pair<UiText, EpisodeSortType>>().apply {
// Episode number sorting is always available
add(txt(R.string.sort_episodes_number_asc) to EpisodeSortType.NUMBER_ASC)
add(txt(R.string.sort_episodes_number_desc) to EpisodeSortType.NUMBER_DESC)
// Only add rating options if any episodes have ratings
if (shouldEnableSort(EpisodeSortType.RATING_HIGH_LOW, ret)) {
add(txt(R.string.sort_episodes_rating_high_low) to EpisodeSortType.RATING_HIGH_LOW)
add(txt(R.string.sort_episodes_rating_low_high) to EpisodeSortType.RATING_LOW_HIGH)
}
// Only add air date options if any episodes have air dates
if (shouldEnableSort(EpisodeSortType.DATE_NEWEST, ret)) {
add(txt(R.string.sort_episodes_date_newest) to EpisodeSortType.DATE_NEWEST)
add(txt(R.string.sort_episodes_date_oldest) to EpisodeSortType.DATE_OLDEST)
}
}
}*/
_episodes.postValue(Resource.Success(ret))
var sortIndex = sortOptions.indexOfFirst { it.second == sorting }
// correct the sorting order so if we have a selected that is not possible we just choose the default NUMBER_ASC
val correctedSorting = if (sortIndex == -1) {
sortIndex = 0
EpisodeSortType.NUMBER_ASC
} else {
sorting
}
currentSorting = correctedSorting
_sortSelections.postValue(sortOptions)
_selectedSortingIndex.postValue(sortIndex)
_selectedSorting.postValue(
when (correctedSorting) {
EpisodeSortType.NUMBER_ASC -> txt(R.string.sort_button_episode, "")
EpisodeSortType.NUMBER_DESC -> txt(R.string.sort_button_episode, "")
EpisodeSortType.RATING_HIGH_LOW -> txt(R.string.sort_button_rating, "")
EpisodeSortType.RATING_LOW_HIGH -> txt(R.string.sort_button_rating, "")
EpisodeSortType.DATE_NEWEST -> txt(R.string.sort_button_date, "")
EpisodeSortType.DATE_OLDEST -> txt(R.string.sort_button_date, "")
}
)
_episodes.postValue(Resource.Success(getSortedEpisodes(ret, correctedSorting)))
}
}
}
@ -2303,7 +2419,7 @@ class ResultViewModel2 : ViewModel() {
it.startEpisode >= (preferStartEpisode ?: 0)
} ?: ranger?.lastOrNull()
postEpisodeRange(min, range)
postEpisodeRange(min, range, DataStoreHelper.resultsSortingMode)
postResume()
}

View File

@ -22,6 +22,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.EpisodeSortType
import com.lagradost.cloudstream3.ui.result.VideoWatchState
import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia
import java.util.Calendar
@ -42,6 +43,7 @@ const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated
const val RESULT_EPISODE = "result_episode"
const val RESULT_SEASON = "result_season"
const val RESULT_DUB = "result_dub"
const val KEY_RESULT_SORT = "result_sort"
class UserPreferenceDelegate<T : Any>(
@ -119,6 +121,12 @@ object DataStoreHelper {
var playBackSpeed : Float by UserPreferenceDelegate("playback_speed", 1.0f)
var resizeMode : Int by UserPreferenceDelegate("resize_mode", 0)
var librarySortingMode : Int by UserPreferenceDelegate("library_sorting_mode", ListSorting.AlphabeticalA.ordinal)
private var _resultsSortingMode : Int by UserPreferenceDelegate("results_sorting_mode", EpisodeSortType.NUMBER_ASC.ordinal)
var resultsSortingMode : EpisodeSortType
get() = EpisodeSortType.entries.getOrNull(_resultsSortingMode) ?: EpisodeSortType.NUMBER_ASC
set(value) {
_resultsSortingMode = value.ordinal
}
data class Account(
@JsonProperty("keyIndex")

View File

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.core.view.marginEnd
import com.lagradost.cloudstream3.R
import kotlin.math.max
@ -32,10 +33,12 @@ class FlowLayout : ViewGroup {
val childCount = this.childCount
for (i in 0 until childCount) {
val child = getChildAt(i)
if (!child.isVisible) {
continue
}
measureChild(child, widthMeasureSpec, heightMeasureSpec)
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
currentHeight = max(currentHeight, currentChildHookPointy + childHeight)
//check if child can be placed in the current row, else go to next line
if (currentChildHookPointx + childWidth - child.marginEnd - child.paddingEnd > realWidth) {
@ -44,8 +47,10 @@ class FlowLayout : ViewGroup {
//reset for new line
currentChildHookPointx = 0
currentChildHookPointy += childHeight
currentChildHookPointy += childHeight + itemSpacing
}
currentHeight = max(currentHeight, currentChildHookPointy + childHeight)
val nextChildHookPointx =
currentChildHookPointx + childWidth + if (childWidth == 0) 0 else itemSpacing

View File

@ -360,14 +360,14 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/result_meta_site"
android:layout_gravity="center_vertical"
style="@style/SmallBlackButton"
android:layout_gravity="center_vertical"
tools:text="Gogoanime" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_meta_content_rating"
android:layout_gravity="center_vertical"
style="@style/SmallBlackButton"
android:layout_gravity="center_vertical"
tools:text="PG-13" />
<TextView
@ -409,8 +409,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="10"
android:foreground="@drawable/outline_drawable"
android:maxLines="10"
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_bookmark_Button"
android:paddingTop="5dp"
@ -733,19 +733,20 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
<com.lagradost.cloudstream3.widget.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
android:orientation="horizontal"
app:itemSpacing="10dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_season_button"
style="@style/MultiSelectButton"
android:layout_gravity="center_vertical"
android:layout_marginStart="0dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:drawableEnd="@drawable/ic_baseline_keyboard_arrow_down_24"
android:nextFocusLeft="@id/result_episode_select"
android:nextFocusRight="@id/result_episode_select"
@ -761,8 +762,8 @@
android:id="@+id/result_episode_select"
style="@style/MultiSelectButton"
android:layout_gravity="center_vertical"
android:layout_marginStart="0dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:drawableEnd="@drawable/ic_baseline_keyboard_arrow_down_24"
android:nextFocusLeft="@id/result_season_button"
android:nextFocusRight="@id/result_season_button"
@ -778,8 +779,8 @@
android:id="@+id/result_dub_select"
style="@style/MultiSelectButton"
android:layout_gravity="center_vertical"
android:layout_marginStart="0dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:drawableEnd="@drawable/ic_baseline_keyboard_arrow_down_24"
android:nextFocusLeft="@id/result_season_button"
android:nextFocusRight="@id/result_season_button"
@ -788,7 +789,23 @@
android:paddingStart="10dp"
android:paddingEnd="5dp"
android:visibility="gone"
tools:text="Dubbed"
tools:text="Dubbed1"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_sort_button"
style="@style/MultiSelectButton"
android:layout_gravity="center_vertical"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:nextFocusLeft="@id/result_dub_select"
android:nextFocusUp="@id/result_description"
android:nextFocusDown="@id/result_episodes"
android:paddingStart="10dp"
android:paddingEnd="5dp"
android:text="Sort"
android:visibility="gone"
tools:visibility="visible" />
<TextView
@ -796,13 +813,15 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="8 Episodes" />
</LinearLayout>
</com.lagradost.cloudstream3.widget.FlowLayout>
<!--TODO add next airing-->

View File

@ -714,6 +714,15 @@
<string name="sort_updated_old">Updated (Old to New)</string>
<string name="sort_alphabetical_a">Alphabetical (A to Z)</string>
<string name="sort_alphabetical_z">Alphabetical (Z to A)</string>
<string name="sort_episodes_number_asc">Episode (Ascending)</string>
<string name="sort_episodes_number_desc">Episode (Descending)</string>
<string name="sort_episodes_rating_high_low">Rating (Highest)</string>
<string name="sort_episodes_rating_low_high">Rating (Lowest)</string>
<string name="sort_episodes_date_newest">Air Date (Newest)</string>
<string name="sort_episodes_date_oldest">Air Date (Oldest)</string>
<string name="sort_button_episode">Ep %s</string>
<string name="sort_button_rating">Rating %s</string>
<string name="sort_button_date">Date %s</string>
<string name="select_library">Select Library</string>
<string name="open_with">Open with</string>
<string name="empty_library_no_accounts_message">Your library is empty :(