Merge pull request #2 from recloudstream/merge-reduplicated

This commit is contained in:
Cloudburst 2022-08-07 11:21:46 +02:00 committed by GitHub
commit 3a2b7e8639
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 4673 additions and 2376 deletions

View File

@ -36,7 +36,7 @@ android {
targetSdkVersion 30
versionCode 50
versionName "3.0.2"
versionName "3.1.2"
resValue "string", "app_version",
"${defaultConfig.versionName}${versionNameSuffix ?: ""}"

View File

@ -260,10 +260,10 @@ object CommonActivity {
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
PlayerEventType.SeekBack
}
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1 -> {
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> {
PlayerEventType.NextEpisode
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1 -> {
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> {
PlayerEventType.PrevEpisode
}
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
@ -294,6 +294,9 @@ object CommonActivity {
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0 -> {
PlayerEventType.Resize
}
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4 -> {
PlayerEventType.SkipOp
}
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
PlayerEventType.PlayPauseToggle
}

View File

@ -17,6 +17,7 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.loadExtractor
import okhttp3.Interceptor
import java.text.SimpleDateFormat
@ -80,7 +81,7 @@ object APIHolder {
return null
}
fun getLoadResponseIdFromUrl(url: String, apiName: String): Int {
private fun getLoadResponseIdFromUrl(url: String, apiName: String): Int {
return url.replace(getApiFromName(apiName).mainUrl, "").replace("/", "").hashCode()
}
@ -527,6 +528,7 @@ enum class ShowStatus {
}
enum class DubStatus(val id: Int) {
None(-1),
Dubbed(1),
Subbed(0),
}
@ -861,6 +863,10 @@ interface LoadResponse {
private val aniListIdPrefix = aniListApi.idPrefix
var isTrailersEnabled = true
fun LoadResponse.isMovie(): Boolean {
return this.type.isMovieType()
}
@JvmName("addActorNames")
fun LoadResponse.addActors(actors: List<String>?) {
this.actors = actors?.map { ActorData(Actor(it)) }
@ -902,25 +908,71 @@ interface LoadResponse {
}
/**better to call addTrailer with mutible trailers directly instead of calling this multiple times*/
suspend fun LoadResponse.addTrailer(trailerUrl: String?, referer: String? = null) {
if (!isTrailersEnabled || trailerUrl == null) return
suspend fun LoadResponse.addTrailer(
trailerUrl: String?,
referer: String? = null,
addRaw: Boolean = false
) {
if (!isTrailersEnabled || trailerUrl.isNullOrBlank()) return
val links = arrayListOf<ExtractorLink>()
val subs = arrayListOf<SubtitleFile>()
loadExtractor(trailerUrl, referer, { subs.add(it) }, { links.add(it) })
this.trailers.add(TrailerData(links, subs))
if (!loadExtractor(
trailerUrl,
referer,
{ subs.add(it) },
{ links.add(it) }) && addRaw
) {
this.trailers.add(
TrailerData(
listOf(
ExtractorLink(
"",
"Trailer",
trailerUrl,
referer ?: "",
Qualities.Unknown.value,
trailerUrl.contains(".m3u8")
)
), listOf()
)
)
} else {
this.trailers.add(TrailerData(links, subs))
}
}
fun LoadResponse.addTrailer(newTrailers: List<ExtractorLink>) {
trailers.addAll(newTrailers.map { TrailerData(listOf(it)) })
}
suspend fun LoadResponse.addTrailer(trailerUrls: List<String>?, referer: String? = null) {
suspend fun LoadResponse.addTrailer(
trailerUrls: List<String>?,
referer: String? = null,
addRaw: Boolean = false
) {
if (!isTrailersEnabled || trailerUrls == null) return
val trailers = trailerUrls.apmap { trailerUrl ->
val trailers = trailerUrls.filter { it.isNotBlank() }.apmap { trailerUrl ->
val links = arrayListOf<ExtractorLink>()
val subs = arrayListOf<SubtitleFile>()
loadExtractor(trailerUrl, referer, { subs.add(it) }, { links.add(it) })
links to subs
if (!loadExtractor(
trailerUrl,
referer,
{ subs.add(it) },
{ links.add(it) }) && addRaw
) {
arrayListOf(
ExtractorLink(
"",
"Trailer",
trailerUrl,
referer ?: "",
Qualities.Unknown.value,
trailerUrl.contains(".m3u8")
)
) to arrayListOf()
} else {
links to subs
}
}.map { (links, subs) -> TrailerData(links, subs) }
this.trailers.addAll(trailers)
}
@ -1001,6 +1053,7 @@ data class NextAiring(
data class SeasonData(
val season: Int,
val name: String? = null,
val displaySeason: Int? = null, // will use season if null
)
interface EpisodeResponse {

View File

@ -151,7 +151,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// Fucks up anime info layout since that has its own layout
cast_mini_controller_holder?.isVisible =
!listOf(R.id.navigation_results, R.id.navigation_player).contains(destination.id)
!listOf(
R.id.navigation_results_phone,
R.id.navigation_results_tv,
R.id.navigation_player
).contains(destination.id)
val isNavVisible = listOf(
R.id.navigation_home,
@ -327,6 +331,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (str.startsWith("https://cs.repo")) {
val realUrl = "https://" + str.substringAfter("?")
println("Repository url: $realUrl")
val activity = this
ioSafe {
val repo = RepositoryManager.parseRepository(realUrl) ?: return@ioSafe
RepositoryManager.addRepository(
@ -337,8 +342,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
)
main {
showToast(
this,
this.getString(R.string.player_loaded_subtitles, repo.name),
activity,
getString(R.string.player_loaded_subtitles, repo.name),
Toast.LENGTH_LONG
)
}
@ -346,6 +351,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} else if (str.contains(appString)) {
for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) {
val activity = this
ioSafe {
Log.i(TAG, "handleAppIntent $str")
val isSuccessful = api.handleRedirect(str)
@ -356,10 +362,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
Log.i(TAG, "failed to authenticate ${api.name}")
}
this.runOnUiThread {
activity.runOnUiThread {
try {
showToast(
this,
activity,
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
api.name
)

View File

@ -51,6 +51,32 @@ fun <T> LifecycleOwner.observeDirectly(liveData: LiveData<T>, action: (t: T) ->
action(currentValue)
}
inline fun <reified T : Any> some(value: T?): Some<T> {
return if (value == null) {
Some.None
} else {
Some.Success(value)
}
}
sealed class Some<out T> {
data class Success<out T>(val value: T) : Some<T>()
object None : Some<Nothing>()
override fun toString(): String {
return when(this) {
is None -> "None"
is Success -> "Some(${value.toString()})"
}
}
}
sealed class ResourceSome<out T> {
data class Success<out T>(val value: T) : ResourceSome<T>()
object None : ResourceSome<Nothing>()
data class Loading(val data: Any? = null) : ResourceSome<Nothing>()
}
sealed class Resource<out T> {
data class Success<out T>(val value: T) : Resource<T>()
data class Failure(

View File

@ -31,6 +31,8 @@ class APIRepository(val api: MainAPI) {
val mainUrl = api.mainUrl
val mainPage = api.mainPage
val hasQuickSearch = api.hasQuickSearch
val vpnStatus = api.vpnStatus
val providerType = api.providerType
suspend fun load(url: String): Resource<LoadResponse> {
return safeApiCall {

View File

@ -18,7 +18,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager
object DownloadButtonSetup {
fun handleDownloadClick(activity: Activity?, headerName: String?, click: DownloadClickEvent) {
fun handleDownloadClick(activity: Activity?, click: DownloadClickEvent) {
val id = click.data.id
if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return
when (click.action) {

View File

@ -84,7 +84,7 @@ class DownloadChildFragment : Fragment() {
DownloadChildAdapter(
ArrayList(),
) { click ->
handleDownloadClick(activity, name, click)
handleDownloadClick(activity, click)
}
downloadDeleteEventListener = { id: Int ->

View File

@ -153,7 +153,7 @@ class DownloadFragment : Fragment() {
},
{ downloadClickEvent ->
if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter
handleDownloadClick(activity, downloadClickEvent.data.name, downloadClickEvent)
handleDownloadClick(activity, downloadClickEvent)
if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
context?.let { ctx ->
downloadsViewModel.updateList(ctx)

View File

@ -98,7 +98,7 @@ class ParentItemAdapter(
recyclerView?.apply {
// this loops every viewHolder in the recycle view and checks the position to see if it is within the update range
val missingUpdates = (position until (position + count)).toMutableSet()
for (i in 0 until mAdapter.itemCount) {
for (i in 0 until itemCount) {
val viewHolder = getChildViewHolder(getChildAt(i))
val absolutePosition = viewHolder.absoluteAdapterPosition
if (absolutePosition >= position && absolutePosition < position + count) {

View File

@ -1164,6 +1164,9 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
openOnlineSubPicker(view.context, null) {}
}
}
PlayerEventType.SkipOp -> {
skipOp()
}
}
}

View File

@ -125,6 +125,7 @@ class GeneratorPlayer : FullScreenPlayer() {
private fun loadExtractorJob(extractorLink: ExtractorLink?) {
currentVerifyLink?.cancel()
extractorLink?.let {
currentVerifyLink = ioSafe {
if (it.extractorData != null) {
@ -488,7 +489,9 @@ class GeneratorPlayer : FullScreenPlayer() {
.setView(R.layout.player_select_source_and_subs)
val sourceDialog = sourceBuilder.create()
selectSourceDialog = sourceDialog
sourceDialog.show()
val providerList = sourceDialog.sort_providers
val subtitleList = sourceDialog.sort_subtitles

View File

@ -23,6 +23,7 @@ enum class PlayerEventType(val value: Int) {
ShowMirrors(12),
Resize(13),
SearchSubtitlesOnline(14),
SkipOp(15),
}
enum class CSPlayerEvent(val value: Int) {

View File

@ -10,6 +10,7 @@ import androidx.annotation.LayoutRes
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.R
@ -56,11 +57,11 @@ const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
class EpisodeAdapter(
var cardList: List<ResultEpisode>,
private val hasDownloadSupport: Boolean,
private val clickCallback: (EpisodeClickEvent) -> Unit,
private val downloadClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var cardList: MutableList<ResultEpisode> = mutableListOf()
private val mBoundViewHolders: HashSet<DownloadButtonViewHolder> = HashSet()
private fun getAllBoundViewHolders(): Set<DownloadButtonViewHolder?>? {
@ -74,6 +75,9 @@ class EpisodeAdapter(
}
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
if(holder.itemView.hasFocus()) {
holder.itemView.clearFocus()
}
if (holder is DownloadButtonViewHolder) {
holder.downloadButton.dispose()
}
@ -92,15 +96,19 @@ class EpisodeAdapter(
}
}
@LayoutRes
private var layout: Int = 0
fun updateLayout() {
// layout =
// if (cardList.filter { it.poster != null }.size >= cardList.size / 2f) // If over half has posters then use the large layout
// R.layout.result_episode_large
// else R.layout.result_episode
fun updateList(newList: List<ResultEpisode>) {
val diffResult = DiffUtil.calculateDiff(
ResultDiffCallback(this.cardList, newList)
)
cardList.clear()
cardList.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
var layout = R.layout.result_episode_both
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
/*val layout = if (cardList.filter { it.poster != null }.size >= cardList.size / 2)
R.layout.result_episode_large
@ -108,7 +116,7 @@ class EpisodeAdapter(
return EpisodeCardViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.result_episode_both, parent, false),
.inflate(layout, parent, false),
hasDownloadSupport,
clickCallback,
downloadClickCallback
@ -146,6 +154,8 @@ class EpisodeAdapter(
fun bind(card: ResultEpisode) {
localCard = card
val isTrueTv = itemView.context?.isTrueTvSettings() == true
val (parentView, otherView) = if (card.poster == null) {
itemView.episode_holder to itemView.episode_holder_large
} else {
@ -196,20 +206,22 @@ class EpisodeAdapter(
}
}
episodePoster?.setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
}
if (!isTrueTv) {
episodePoster?.setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
}
episodePoster?.setOnLongClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card))
return@setOnLongClickListener true
episodePoster?.setOnLongClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card))
return@setOnLongClickListener true
}
}
parentView.setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
}
if (parentView.context.isTrueTvSettings()) {
if (isTrueTv) {
parentView.isFocusable = true
parentView.isFocusableInTouchMode = true
parentView.touchscreenBlocksFocus = false
@ -263,3 +275,19 @@ class EpisodeAdapter(
}
}
}
class ResultDiffCallback(
private val oldList: List<ResultEpisode>,
private val newList: List<ResultEpisode>
) :
DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition].id == newList[newItemPosition].id
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition] == newList[newItemPosition]
}

View File

@ -0,0 +1,401 @@
package com.lagradost.cloudstream3.ui.result
import android.app.Dialog
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import com.discord.panels.OverlappingPanelsLayout
import com.discord.panels.PanelsChildGestureRegionObserver
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.mvvm.Some
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.fragment_trailer.*
import kotlinx.android.synthetic.main.result_recommendations.*
import kotlinx.android.synthetic.main.trailer_custom_layout.*
class ResultFragmentPhone : ResultFragment() {
var currentTrailers: List<ExtractorLink> = emptyList()
var currentTrailerIndex = 0
override fun nextMirror() {
currentTrailerIndex++
loadTrailer()
}
override fun hasNextMirror(): Boolean {
return currentTrailerIndex + 1 < currentTrailers.size
}
override fun playerError(exception: Exception) {
if (player.getIsPlaying()) { // because we dont want random toasts in player
super.playerError(exception)
} else {
nextMirror()
}
}
private fun loadTrailer(index: Int? = null) {
val isSuccess =
currentTrailers.getOrNull(index ?: currentTrailerIndex)?.let { trailer ->
context?.let { ctx ->
player.onPause()
player.loadPlayer(
ctx,
false,
trailer,
null,
startPosition = 0L,
subtitles = emptySet(),
subtitle = null,
autoPlay = false
)
true
} ?: run {
false
}
} ?: run {
false
}
result_trailer_loading?.isVisible = isSuccess
result_smallscreen_holder?.isVisible = !isSuccess && !isFullScreenPlayer
// We don't want the trailer to be focusable if it's not visible
result_smallscreen_holder?.descendantFocusability = if (isSuccess) {
ViewGroup.FOCUS_AFTER_DESCENDANTS
} else {
ViewGroup.FOCUS_BLOCK_DESCENDANTS
}
result_fullscreen_holder?.isVisible = !isSuccess && isFullScreenPlayer
}
override fun setTrailers(trailers: List<ExtractorLink>?) {
context?.updateHasTrailers()
if (!LoadResponse.isTrailersEnabled) return
currentTrailers = trailers?.sortedBy { -it.quality } ?: emptyList()
loadTrailer()
}
override fun onDestroyView() {
//somehow this still leaks and I dont know why????
// todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt
PanelsChildGestureRegionObserver.Provider.get().let { obs ->
result_cast_items?.let {
obs.unregister(it)
}
obs.removeGestureRegionsUpdateListener(this)
}
super.onDestroyView()
}
var loadingDialog: Dialog? = null
var popupDialog: Dialog? = null
/**
* Sets next focus to allow navigation up and down between 2 views
* if either of them is null nothing happens.
**/
private fun setFocusUpAndDown(upper: View?, down: View?) {
if (upper == null || down == null) return
upper.nextFocusDownId = down.id
down.nextFocusUpId = upper.id
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
player_open_source?.setOnClickListener {
currentTrailers.getOrNull(currentTrailerIndex)?.let {
context?.openBrowser(it.url)
}
}
result_overlapping_panels?.setStartPanelLockState(OverlappingPanelsLayout.LockState.CLOSE)
result_overlapping_panels?.setEndPanelLockState(OverlappingPanelsLayout.LockState.CLOSE)
result_recommendations?.spanCount = 3
result_recommendations?.adapter =
SearchAdapter(
ArrayList(),
result_recommendations,
) { callback ->
SearchHelper.handleSearchClickCallback(activity, callback)
}
PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(this)
result_cast_items?.let {
PanelsChildGestureRegionObserver.Provider.get().register(it)
}
result_back?.setOnClickListener {
activity?.popCurrentPage()
}
result_bookmark_button?.setOnClickListener {
it.popupMenuNoIcons(
items = WatchType.values()
.map { watchType -> Pair(watchType.internalId, watchType.stringRes) },
//.map { watchType -> Triple(watchType.internalId, watchType.iconRes, watchType.stringRes) },
) {
viewModel.updateWatchStatus(WatchType.fromInternalId(this.itemId))
}
}
result_mini_sync?.adapter = ImageAdapter(
R.layout.result_mini_image,
nextFocusDown = R.id.result_sync_set_score,
clickCallback = { action ->
if (action == IMAGE_CLICK || action == IMAGE_LONG_CLICK) {
if (result_overlapping_panels?.getSelectedPanel()?.ordinal == 1) {
result_overlapping_panels?.openStartPanel()
} else {
result_overlapping_panels?.closePanels()
}
}
})
result_scroll?.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
val dy = scrollY - oldScrollY
if (dy > 0) { //check for scroll down
result_bookmark_fab?.shrink()
} else if (dy < -5) {
result_bookmark_fab?.extend()
}
if (!isFullScreenPlayer && player.getIsPlaying()) {
if (scrollY > (player_background?.height ?: scrollY)) {
player.handleEvent(CSPlayerEvent.Pause)
}
}
//result_poster_blur_holder?.translationY = -scrollY.toFloat()
})
observe(viewModel.episodesCountText) { count ->
result_episodes_text.setText(count)
}
observe(viewModel.selectPopup) { popup ->
when (popup) {
is Some.Success -> {
popupDialog?.dismissSafe(activity)
popupDialog = activity?.let { act ->
val pop = popup.value
val options = pop.getOptions(act)
val title = pop.getTitle(act)
act.showBottomDialogInstant(
options, title, {
popupDialog = null
pop.callback(null)
}, {
popupDialog = null
pop.callback(it)
}
)
}
}
is Some.None -> {
popupDialog?.dismissSafe(activity)
popupDialog = null
}
}
}
observe(viewModel.loadedLinks) { load ->
when (load) {
is Some.Success -> {
if (loadingDialog?.isShowing != true) {
loadingDialog?.dismissSafe(activity)
loadingDialog = null
}
loadingDialog = loadingDialog ?: context?.let { ctx ->
val builder =
BottomSheetDialog(ctx)
builder.setContentView(R.layout.bottom_loading)
builder.setOnDismissListener {
loadingDialog = null
viewModel.cancelLinks()
}
//builder.setOnCancelListener {
// it?.dismiss()
//}
builder.setCanceledOnTouchOutside(true)
builder.show()
builder
}
}
is Some.None -> {
loadingDialog?.dismissSafe(activity)
loadingDialog = null
}
}
}
observe(viewModel.selectedSeason) { text ->
result_season_button.setText(text)
// If the season button is visible the result season button will be next focus down
if (result_season_button?.isVisible == true)
if (result_resume_parent?.isVisible == true)
setFocusUpAndDown(result_resume_series_button, result_season_button)
else
setFocusUpAndDown(result_bookmark_button, result_season_button)
}
observe(viewModel.selectedDubStatus) { status ->
result_dub_select?.setText(status)
if (result_dub_select?.isVisible == true)
if (result_season_button?.isVisible != true && result_episode_select?.isVisible != true) {
if (result_resume_parent?.isVisible == true)
setFocusUpAndDown(result_resume_series_button, result_dub_select)
else
setFocusUpAndDown(result_bookmark_button, result_dub_select)
}
}
observe(viewModel.selectedRange) { range ->
result_episode_select.setText(range)
// If Season button is invisible then the bookmark button next focus is episode select
if (result_episode_select?.isVisible == true)
if (result_season_button?.isVisible != true) {
if (result_resume_parent?.isVisible == true)
setFocusUpAndDown(result_resume_series_button, result_episode_select)
else
setFocusUpAndDown(result_bookmark_button, result_episode_select)
}
}
// val preferDub = context?.getApiDubstatusSettings()?.all { it == DubStatus.Dubbed } == true
observe(viewModel.dubSubSelections) { range ->
result_dub_select.setOnClickListener { view ->
view?.context?.let { ctx ->
view.popupMenuNoIconsAndNoStringRes(range
.mapNotNull { (text, status) ->
Pair(
status.ordinal,
text?.asStringNull(ctx) ?: return@mapNotNull null
)
}) {
viewModel.changeDubStatus(DubStatus.values()[itemId])
}
}
}
}
observe(viewModel.rangeSelections) { range ->
result_episode_select?.setOnClickListener { view ->
view?.context?.let { ctx ->
val names = range
.mapNotNull { (text, r) ->
r to (text?.asStringNull(ctx) ?: return@mapNotNull null)
}
view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) ->
index to name
}) {
viewModel.changeRange(names[itemId].first)
}
}
}
}
observe(viewModel.seasonSelections) { seasonList ->
result_season_button?.setOnClickListener { view ->
view?.context?.let { ctx ->
val names = seasonList
.mapNotNull { (text, r) ->
r to (text?.asStringNull(ctx) ?: return@mapNotNull null)
}
view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) ->
index to name
}) {
viewModel.changeSeason(names[itemId].first)
}
}
}
}
}
override fun onPause() {
super.onPause()
PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(this)
}
override fun onGestureRegionsUpdate(gestureRegions: List<Rect>) {
result_overlapping_panels?.setChildGestureRegions(gestureRegions)
}
override fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
val isInvalid = rec.isNullOrEmpty()
result_recommendations?.isGone = isInvalid
result_recommendations_btt?.isGone = isInvalid
result_recommendations_btt?.setOnClickListener {
val nextFocusDown = if (result_overlapping_panels?.getSelectedPanel()?.ordinal == 1) {
result_overlapping_panels?.openEndPanel()
R.id.result_recommendations
} else {
result_overlapping_panels?.closePanels()
R.id.result_description
}
result_recommendations_btt?.nextFocusDownId = nextFocusDown
result_search?.nextFocusDownId = nextFocusDown
result_open_in_browser?.nextFocusDownId = nextFocusDown
result_share?.nextFocusDownId = nextFocusDown
}
result_overlapping_panels?.setEndPanelLockState(if (isInvalid) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED)
val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName
rec?.map { it.apiName }?.distinct()?.let { apiNames ->
// very dirty selection
result_recommendations_filter_button?.isVisible = apiNames.size > 1
result_recommendations_filter_button?.text = matchAgainst
result_recommendations_filter_button?.setOnClickListener { _ ->
activity?.showBottomDialog(
apiNames,
apiNames.indexOf(matchAgainst),
getString(R.string.home_change_provider_img_des), false, {}
) {
setRecommendations(rec, apiNames[it])
}
}
} ?: run {
result_recommendations_filter_button?.isVisible = false
}
result_recommendations?.post {
rec?.let { list ->
(result_recommendations?.adapter as SearchAdapter?)?.updateList(list.filter { it.apiName == matchAgainst })
}
}
}
}

View File

@ -0,0 +1,208 @@
package com.lagradost.cloudstream3.ui.result
import android.app.Dialog
import android.os.Bundle
import android.view.View
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.mvvm.ResourceSome
import com.lagradost.cloudstream3.mvvm.Some
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.fragment_result_tv.*
class ResultFragmentTv : ResultFragment() {
override val resultLayout = R.layout.fragment_result_tv
private var currentRecommendations: List<SearchResponse> = emptyList()
private fun handleSelection(data: Any) {
when (data) {
is EpisodeRange -> {
viewModel.changeRange(data)
}
is Int -> {
viewModel.changeSeason(data)
}
is DubStatus -> {
viewModel.changeDubStatus(data)
}
is String -> {
setRecommendations(currentRecommendations, data)
}
}
}
private fun RecyclerView?.select(index: Int) {
(this?.adapter as? SelectAdaptor?)?.select(index, this)
}
private fun RecyclerView?.update(data: List<SelectData>) {
(this?.adapter as? SelectAdaptor?)?.updateSelectionList(data)
this?.isVisible = data.size > 1
}
private fun RecyclerView?.setAdapter() {
this?.adapter = SelectAdaptor { data ->
handleSelection(data)
}
}
private fun hasNoFocus(): Boolean {
val focus = activity?.currentFocus
if (focus == null || !focus.isVisible) return true
return focus == this.result_root
}
override fun updateEpisodes(episodes: ResourceSome<List<ResultEpisode>>) {
super.updateEpisodes(episodes)
if (episodes is ResourceSome.Success && hasNoFocus()) {
result_episodes?.requestFocus()
}
}
override fun updateMovie(data: ResourceSome<Pair<UiText, ResultEpisode>>) {
super.updateMovie(data)
if (data is ResourceSome.Success && hasNoFocus()) {
result_play_movie?.requestFocus()
}
}
override fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
currentRecommendations = rec ?: emptyList()
val isInvalid = rec.isNullOrEmpty()
result_recommendations?.isGone = isInvalid
result_recommendations_holder?.isGone = isInvalid
val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName
(result_recommendations?.adapter as SearchAdapter?)?.updateList(rec?.filter { it.apiName == matchAgainst }
?: emptyList())
rec?.map { it.apiName }?.distinct()?.let { apiNames ->
// very dirty selection
result_recommendations_filter_selection?.isVisible = apiNames.size > 1
result_recommendations_filter_selection?.update(apiNames.map { txt(it) to it })
result_recommendations_filter_selection?.select(apiNames.indexOf(matchAgainst))
} ?: run {
result_recommendations_filter_selection?.isVisible = false
}
}
var loadingDialog: Dialog? = null
var popupDialog: Dialog? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(result_episodes?.adapter as EpisodeAdapter?)?.apply {
layout = R.layout.result_episode_both_tv
}
result_season_selection.setAdapter()
result_range_selection.setAdapter()
result_dub_selection.setAdapter()
result_recommendations_filter_selection.setAdapter()
observe(viewModel.selectPopup) { popup ->
when (popup) {
is Some.Success -> {
popupDialog?.dismissSafe(activity)
popupDialog = activity?.let { act ->
val pop = popup.value
val options = pop.getOptions(act)
val title = pop.getTitle(act)
act.showBottomDialogInstant(
options, title, {
popupDialog = null
pop.callback(null)
}, {
popupDialog = null
pop.callback(it)
}
)
}
}
is Some.None -> {
popupDialog?.dismissSafe(activity)
popupDialog = null
}
}
}
observe(viewModel.loadedLinks) { load ->
when (load) {
is Some.Success -> {
if (loadingDialog?.isShowing != true) {
loadingDialog?.dismissSafe(activity)
loadingDialog = null
}
loadingDialog = loadingDialog ?: context?.let { ctx ->
val builder =
BottomSheetDialog(ctx)
builder.setContentView(R.layout.bottom_loading)
builder.setOnDismissListener {
loadingDialog = null
viewModel.cancelLinks()
}
//builder.setOnCancelListener {
// it?.dismiss()
//}
builder.setCanceledOnTouchOutside(true)
builder.show()
builder
}
}
is Some.None -> {
loadingDialog?.dismissSafe(activity)
loadingDialog = null
}
}
}
observe(viewModel.episodesCountText) { count ->
result_episodes_text.setText(count)
}
observe(viewModel.selectedRangeIndex) { selected ->
result_range_selection.select(selected)
}
observe(viewModel.selectedSeasonIndex) { selected ->
result_season_selection.select(selected)
}
observe(viewModel.selectedDubStatusIndex) { selected ->
result_dub_selection.select(selected)
}
observe(viewModel.rangeSelections) {
result_range_selection.update(it)
}
observe(viewModel.dubSubSelections) {
result_dub_selection.update(it)
}
observe(viewModel.seasonSelections) {
result_season_selection.update(it)
}
result_back?.setOnClickListener {
activity?.popCurrentPage()
}
result_recommendations?.spanCount = 8
result_recommendations?.adapter =
SearchAdapter(
ArrayList(),
result_recommendations,
) { callback ->
SearchHelper.handleSearchClickCallback(activity, callback)
}
}
}

View File

@ -1,631 +0,0 @@
package com.lagradost.cloudstream3.ui.result
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.getApiFromUrlNull
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
import com.lagradost.cloudstream3.metaproviders.SyncRedirector
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu.getEpisodesDetails
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.player.IGenerator
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.getFillerEpisodes
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.collections.set
const val EPISODE_RANGE_SIZE = 50
const val EPISODE_RANGE_OVERLOAD = 60
class ResultViewModel : ViewModel() {
private var repo: APIRepository? = null
private var generator: IGenerator? = null
private val _resultResponse: MutableLiveData<Resource<LoadResponse>> = MutableLiveData()
private val _episodes: MutableLiveData<List<ResultEpisode>> = MutableLiveData()
private val episodeById: MutableLiveData<HashMap<Int, Int>> =
MutableLiveData() // lookup by ID to get Index
private val _publicEpisodes: MutableLiveData<Resource<List<ResultEpisode>>> = MutableLiveData()
private val _publicEpisodesCount: MutableLiveData<Int> = MutableLiveData() // before the sorting
private val _rangeOptions: MutableLiveData<List<String>> = MutableLiveData()
val selectedRange: MutableLiveData<String> = MutableLiveData()
private val selectedRangeInt: MutableLiveData<Int> = MutableLiveData()
val rangeOptions: LiveData<List<String>> = _rangeOptions
val result: LiveData<Resource<LoadResponse>> get() = _resultResponse
val episodes: LiveData<List<ResultEpisode>> get() = _episodes
val publicEpisodes: LiveData<Resource<List<ResultEpisode>>> get() = _publicEpisodes
val publicEpisodesCount: LiveData<Int> get() = _publicEpisodesCount
val dubStatus: LiveData<DubStatus> get() = _dubStatus
private val _dubStatus: MutableLiveData<DubStatus> = MutableLiveData()
val id: MutableLiveData<Int> = MutableLiveData()
val selectedSeason: MutableLiveData<Int> = MutableLiveData(-2)
val seasonSelections: MutableLiveData<List<Int?>> = MutableLiveData()
val dubSubSelections: LiveData<Set<DubStatus>> get() = _dubSubSelections
private val _dubSubSelections: MutableLiveData<Set<DubStatus>> = MutableLiveData()
val dubSubEpisodes: LiveData<Map<DubStatus, List<ResultEpisode>>?> get() = _dubSubEpisodes
private val _dubSubEpisodes: MutableLiveData<Map<DubStatus, List<ResultEpisode>>?> =
MutableLiveData()
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData()
val watchStatus: LiveData<WatchType> get() = _watchStatus
fun updateWatchStatus(status: WatchType) = viewModelScope.launch {
val currentId = id.value ?: return@launch
_watchStatus.postValue(status)
val resultPage = _resultResponse.value
withContext(Dispatchers.IO) {
setResultWatchState(currentId, status.internalId)
if (resultPage != null && resultPage is Resource.Success) {
val resultPageData = resultPage.value
val current = getBookmarkedData(currentId)
val currentTime = System.currentTimeMillis()
setBookmarkedData(
currentId,
DataStoreHelper.BookmarkedData(
currentId,
current?.bookmarkedTime ?: currentTime,
currentTime,
resultPageData.name,
resultPageData.url,
resultPageData.apiName,
resultPageData.type,
resultPageData.posterUrl,
resultPageData.year
)
)
}
}
}
companion object {
const val TAG = "RVM"
}
var lastMeta: SyncAPI.SyncResult? = null
var lastSync: Map<String, String>? = null
private suspend fun applyMeta(
resp: LoadResponse,
meta: SyncAPI.SyncResult?,
syncs: Map<String, String>? = null
): Pair<LoadResponse, Boolean> {
if (meta == null) return resp to false
var updateEpisodes = false
val out = resp.apply {
Log.i(TAG, "applyMeta")
duration = duration ?: meta.duration
rating = rating ?: meta.publicScore
tags = tags ?: meta.genres
plot = if (plot.isNullOrBlank()) meta.synopsis else plot
posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl
actors = actors ?: meta.actors
if (this is EpisodeResponse) {
nextAiring = nextAiring ?: meta.nextAiring
}
for ((k, v) in syncs ?: emptyMap()) {
syncData[k] = v
}
val realRecommendations = ArrayList<SearchResponse>()
// TODO: fix
//val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
// meta.recommendations?.forEach { rec ->
// apiNames.forEach { name ->
// realRecommendations.add(rec.copy(apiName = name))
// }
// }
recommendations = recommendations?.union(realRecommendations)?.toList()
?: realRecommendations
argamap({
addTrailer(meta.trailers)
}, {
if (this !is AnimeLoadResponse) return@argamap
val map = getEpisodesDetails(getMalId(), getAniListId(), isResponseRequired = false)
if (map.isNullOrEmpty()) return@argamap
updateEpisodes = DubStatus.values().map { dubStatus ->
val current =
this.episodes[dubStatus]?.mapIndexed { index, episode ->
episode.apply {
this.episode = this.episode ?: (index + 1)
}
}?.sortedBy { it.episode ?: 0 }?.toMutableList()
if (current.isNullOrEmpty()) return@map false
val episodeNumbers = current.map { ep -> ep.episode!! }
var updateCount = 0
map.forEach { (episode, node) ->
episodeNumbers.binarySearch(episode).let { index ->
current.getOrNull(index)?.let { currentEp ->
current[index] = currentEp.apply {
updateCount++
val currentBack = this
this.description = this.description ?: node.description?.en
this.name = this.name ?: node.titles?.canonical
this.episode = this.episode ?: node.num ?: episodeNumbers[index]
this.posterUrl = this.posterUrl ?: node.thumbnail?.original?.url
}
}
}
}
this.episodes[dubStatus] = current
updateCount > 0
}.any { it }
})
}
return out to updateEpisodes
}
fun setMeta(meta: SyncAPI.SyncResult, syncs: Map<String, String>?) =
viewModelScope.launch {
Log.i(TAG, "setMeta")
lastMeta = meta
lastSync = syncs
val (value, updateEpisodes) = ioWork {
(result.value as? Resource.Success<LoadResponse>?)?.value?.let { resp ->
return@ioWork applyMeta(resp, meta, syncs)
}
return@ioWork null to null
}
_resultResponse.postValue(Resource.Success(value ?: return@launch))
if (updateEpisodes ?: return@launch) updateEpisodes(value, lastShowFillers)
}
private fun loadWatchStatus(localId: Int? = null) {
val currentId = localId ?: id.value ?: return
val currentWatch = getResultWatchState(currentId)
_watchStatus.postValue(currentWatch)
}
private fun filterEpisodes(list: List<ResultEpisode>?, selection: Int?, range: Int?) {
if (list == null) return
val seasonTypes = HashMap<Int?, Boolean>()
for (i in list) {
if (!seasonTypes.containsKey(i.season)) {
seasonTypes[i.season] = true
}
}
val seasons = seasonTypes.toList().map { it.first }.sortedBy { it }
seasonSelections.postValue(seasons)
if (seasons.isEmpty()) { // WHAT THE FUCK DID YOU DO????? HOW DID YOU DO THIS
_publicEpisodes.postValue(Resource.Success(emptyList()))
return
}
val realSelection = if (!seasonTypes.containsKey(selection)) seasons.first() else selection
val internalId = id.value
if (internalId != null) setResultSeason(internalId, realSelection)
selectedSeason.postValue(realSelection ?: -2)
var currentList = list.filter { it.season == realSelection }
_publicEpisodesCount.postValue(currentList.size)
val rangeList = ArrayList<String>()
for (i in currentList.indices step EPISODE_RANGE_SIZE) {
if (i + EPISODE_RANGE_SIZE < currentList.size) {
rangeList.add("${i + 1}-${i + EPISODE_RANGE_SIZE}")
} else {
rangeList.add("${i + 1}-${currentList.size}")
}
}
val cRange = range ?: if (selection != null) {
0
} else {
selectedRangeInt.value ?: 0
}
val realRange = if (cRange * EPISODE_RANGE_SIZE > currentList.size) {
currentList.size / EPISODE_RANGE_SIZE
} else {
cRange
}
if (currentList.size > EPISODE_RANGE_OVERLOAD) {
currentList = currentList.subList(
realRange * EPISODE_RANGE_SIZE,
minOf(currentList.size, (realRange + 1) * EPISODE_RANGE_SIZE)
)
_rangeOptions.postValue(rangeList)
selectedRangeInt.postValue(realRange)
selectedRange.postValue(rangeList[realRange])
} else {
val allRange = "1-${currentList.size}"
_rangeOptions.postValue(listOf(allRange))
selectedRangeInt.postValue(0)
selectedRange.postValue(allRange)
}
_publicEpisodes.postValue(Resource.Success(currentList))
}
fun changeSeason(selection: Int?) {
filterEpisodes(_episodes.value, selection, null)
}
fun changeRange(range: Int?) {
filterEpisodes(_episodes.value, null, range)
}
fun changeDubStatus(status: DubStatus?) {
if (status == null) return
dubSubEpisodes.value?.get(status)?.let { episodes ->
id.value?.let {
setDub(it, status)
}
_dubStatus.postValue(status!!)
updateEpisodes(null, episodes, null)
}
}
suspend fun loadEpisode(
episode: ResultEpisode,
isCasting: Boolean,
clearCache: Boolean = false
): Resource<Pair<Set<ExtractorLink>, Set<SubtitleData>>> {
return safeApiCall {
val index = _episodes.value?.indexOf(episode) ?: episode.index
val currentLinks = mutableSetOf<ExtractorLink>()
val currentSubs = mutableSetOf<SubtitleData>()
generator?.goto(index)
generator?.generateLinks(clearCache, isCasting, {
it.first?.let { link ->
currentLinks.add(link)
}
}, { sub ->
currentSubs.add(sub)
})
return@safeApiCall Pair(
currentLinks.toSet(),
currentSubs.toSet()
)
}
}
fun getGenerator(episode: ResultEpisode): IGenerator? {
val index = _episodes.value?.indexOf(episode) ?: episode.index
generator?.goto(index)
return generator
}
private fun updateEpisodes(localId: Int?, list: List<ResultEpisode>, selection: Int?) {
_episodes.postValue(list)
generator = RepoLinkGenerator(list)
val set = HashMap<Int, Int>()
val range = selectedRangeInt.value
list.withIndex().forEach { set[it.value.id] = it.index }
episodeById.postValue(set)
filterEpisodes(
list,
if (selection == -1) getResultSeason(localId ?: id.value ?: return) else selection,
range
)
}
fun reloadEpisodes() {
val current = _episodes.value ?: return
val copy = current.map {
val posDur = getViewPos(it.id)
it.copy(position = posDur?.position ?: 0, duration = posDur?.duration ?: 0)
}
updateEpisodes(null, copy, selectedSeason.value)
}
private fun filterName(name: String?): String? {
if (name == null) return null
Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let {
if (it.isEmpty())
return null
}
return name
}
var lastShowFillers = false
private suspend fun updateEpisodes(loadResponse: LoadResponse, showFillers: Boolean) {
Log.i(TAG, "updateEpisodes")
try {
lastShowFillers = showFillers
val mainId = loadResponse.getId()
when (loadResponse) {
is AnimeLoadResponse -> {
if (loadResponse.episodes.isEmpty()) {
_dubSubEpisodes.postValue(emptyMap())
return
}
// val status = getDub(mainId)
val statuses = loadResponse.episodes.map { it.key }
// Extremely bruh to have to take in context here, but I'm not sure how to do this in a better way :(
val preferDub = context?.getApiDubstatusSettings()
?.contains(DubStatus.Dubbed) == true
// 3 statements because there can be only dub even if you do not prefer it.
val dubStatus =
if (preferDub && statuses.contains(DubStatus.Dubbed)) DubStatus.Dubbed
else if (!preferDub && statuses.contains(DubStatus.Subbed)) DubStatus.Subbed
else statuses.first()
val fillerEpisodes =
if (showFillers) safeApiCall { getFillerEpisodes(loadResponse.name) } else null
val existingEpisodes = HashSet<Int>()
val res = loadResponse.episodes.map { ep ->
val episodes = ArrayList<ResultEpisode>()
val idIndex = ep.key.id
for ((index, i) in ep.value.withIndex()) {
val episode = i.episode ?: (index + 1)
val id = mainId + episode + idIndex * 1000000
if (!existingEpisodes.contains(episode)) {
existingEpisodes.add(id)
episodes.add(buildResultEpisode(
loadResponse.name,
filterName(i.name),
i.posterUrl,
episode,
i.season,
i.data,
loadResponse.apiName,
id,
index,
i.rating,
i.description,
if (fillerEpisodes is Resource.Success) fillerEpisodes.value?.let {
it.contains(episode) && it[episode] == true
} ?: false else false,
loadResponse.type,
mainId
))
}
}
Pair(ep.key, episodes)
}.toMap()
// These posts needs to be in this order as to make the preferDub in ResultFragment work
_dubSubEpisodes.postValue(res)
res[dubStatus]?.let { episodes ->
updateEpisodes(mainId, episodes, -1)
}
_dubStatus.postValue(dubStatus)
_dubSubSelections.postValue(loadResponse.episodes.keys)
}
is TvSeriesLoadResponse -> {
val episodes = ArrayList<ResultEpisode>()
val existingEpisodes = HashSet<Int>()
for ((index, episode) in loadResponse.episodes.sortedBy {
(it.season?.times(10000) ?: 0) + (it.episode ?: 0)
}.withIndex()) {
val episodeIndex = episode.episode ?: (index + 1)
val id =
mainId + (episode.season?.times(100000) ?: 0) + episodeIndex + 1
if (!existingEpisodes.contains(id)) {
existingEpisodes.add(id)
episodes.add(
buildResultEpisode(
loadResponse.name,
filterName(episode.name),
episode.posterUrl,
episodeIndex,
episode.season,
episode.data,
loadResponse.apiName,
id,
index,
episode.rating,
episode.description,
null,
loadResponse.type,
mainId
)
)
}
}
updateEpisodes(mainId, episodes, -1)
}
is MovieLoadResponse -> {
buildResultEpisode(
loadResponse.name,
loadResponse.name,
null,
0,
null,
loadResponse.dataUrl,
loadResponse.apiName,
(mainId), // HAS SAME ID
0,
null,
null,
null,
loadResponse.type,
mainId
).let {
updateEpisodes(mainId, listOf(it), -1)
}
}
is LiveStreamLoadResponse -> {
buildResultEpisode(
loadResponse.name,
loadResponse.name,
null,
0,
null,
loadResponse.dataUrl,
loadResponse.apiName,
(mainId), // HAS SAME ID
0,
null,
null,
null,
loadResponse.type,
mainId
).let {
updateEpisodes(mainId, listOf(it), -1)
}
}
is TorrentLoadResponse -> {
updateEpisodes(
mainId, listOf(
buildResultEpisode(
loadResponse.name,
loadResponse.name,
null,
0,
null,
loadResponse.torrent ?: loadResponse.magnet ?: "",
loadResponse.apiName,
(mainId), // HAS SAME ID
0,
null,
null,
null,
loadResponse.type,
mainId
)
), -1
)
}
}
} catch (e: Exception) {
logError(e)
}
}
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
_publicEpisodes.postValue(Resource.Loading())
_resultResponse.postValue(Resource.Loading(url))
val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url)
if (api == null) {
_resultResponse.postValue(
Resource.Failure(
false,
null,
null,
"This provider does not exist"
)
)
return@launch
}
var validUrlResource = safeApiCall {
SyncRedirector.redirect(
url,
api.mainUrl
)
}
// TODO: fix
// val validUrlResource = safeApiCall {
// SyncRedirector.redirect(
// url,
// api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime")
// .replace(GogoanimeProvider().mainUrl, "gogoanime")
// )
// }
if (validUrlResource !is Resource.Success) {
if (validUrlResource is Resource.Failure) {
_resultResponse.postValue(validUrlResource)
}
return@launch
}
val validUrl = validUrlResource.value
_resultResponse.postValue(Resource.Loading(validUrl))
_apiName.postValue(apiName)
repo = APIRepository(api)
val data = repo?.load(validUrl) ?: return@launch
_resultResponse.postValue(data)
when (data) {
is Resource.Success -> {
val loadResponse = if (lastMeta != null || lastSync != null) ioWork {
applyMeta(data.value, lastMeta, lastSync).first
} else data.value
_resultResponse.postValue(Resource.Success(loadResponse))
val mainId = loadResponse.getId()
id.postValue(mainId)
loadWatchStatus(mainId)
setKey(
DOWNLOAD_HEADER_CACHE,
mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
apiName,
validUrl,
loadResponse.type,
loadResponse.name,
loadResponse.posterUrl,
mainId,
System.currentTimeMillis(),
)
)
updateEpisodes(loadResponse, showFillers)
}
else -> Unit
}
}
private var _apiName: MutableLiveData<String> = MutableLiveData()
val apiName: LiveData<String> get() = _apiName
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,128 @@
package com.lagradost.cloudstream3.ui.result
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.ActorData
import com.lagradost.cloudstream3.ActorRole
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.download.DownloadButtonViewHolder
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
import com.lagradost.cloudstream3.ui.settings.AccountAdapter
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.cast_item.view.*
import org.schabi.newpipe.extractor.timeago.patterns.it
typealias SelectData = Pair<UiText?, Any>
class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val selection: MutableList<SelectData> = mutableListOf()
private var selectedIndex: Int = -1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return SelectViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.result_selection, parent, false),
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is SelectViewHolder -> {
holder.bind(selection[position], position == selectedIndex, callback)
}
}
}
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
if(holder.itemView.hasFocus()) {
holder.itemView.clearFocus()
}
}
override fun getItemCount(): Int {
return selection.size
}
fun select(newIndex: Int, recyclerView: RecyclerView?) {
if(recyclerView == null) return
if(newIndex == selectedIndex) return
val oldIndex = selectedIndex
selectedIndex = newIndex
recyclerView.apply {
for (i in 0 until itemCount) {
val viewHolder = getChildViewHolder( getChildAt(i) ?: continue) ?: continue
val pos = viewHolder.absoluteAdapterPosition
if (viewHolder is SelectViewHolder) {
if (pos == oldIndex) {
viewHolder.update(false)
} else if (pos == newIndex) {
viewHolder.update(true)
}
}
}
}
}
fun updateSelectionList(newList: List<SelectData>) {
val diffResult = DiffUtil.calculateDiff(
SelectDataCallback(this.selection, newList)
)
selection.clear()
selection.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
private class SelectViewHolder
constructor(
itemView: View,
) :
RecyclerView.ViewHolder(itemView) {
private val item: MaterialButton = itemView as MaterialButton
fun update(isSelected: Boolean) {
item.isSelected = isSelected
}
fun bind(
data: SelectData, isSelected: Boolean, callback: (Any) -> Unit
) {
val isTrueTv = itemView.context?.isTrueTvSettings() == true
if (isTrueTv) {
item.isFocusable = true
item.isFocusableInTouchMode = true
}
item.isSelected = isSelected
item.setText(data.first)
item.setOnClickListener {
callback.invoke(data.second)
}
}
}
}
class SelectDataCallback(
private val oldList: List<SelectData>,
private val newList: List<SelectData>
) :
DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition].second == newList[newItemPosition].second
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition] == newList[newItemPosition]
}

View File

@ -4,7 +4,6 @@ import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
@ -12,8 +11,8 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.SyncUtil
import kotlinx.coroutines.launch
import java.util.*
@ -44,9 +43,13 @@ class SyncViewModel : ViewModel() {
// prefix, id
private var syncs = mutableMapOf<String, String>()
private val _syncIds: MutableLiveData<MutableMap<String, String>> =
MutableLiveData(mutableMapOf())
val syncIds: LiveData<MutableMap<String, String>> get() = _syncIds
//private val _syncIds: MutableLiveData<MutableMap<String, String>> =
// MutableLiveData(mutableMapOf())
//val syncIds: LiveData<MutableMap<String, String>> get() = _syncIds
fun getSyncs() : Map<String,String> {
return syncs
}
private val _currentSynced: MutableLiveData<List<CurrentSynced>> =
MutableLiveData(getMissing())
@ -76,7 +79,7 @@ class SyncViewModel : ViewModel() {
Log.i(TAG, "addSync $idPrefix = $id")
syncs[idPrefix] = id
_syncIds.postValue(syncs)
//_syncIds.postValue(syncs)
return true
}
@ -99,10 +102,12 @@ class SyncViewModel : ViewModel() {
var hasAddedFromUrl: HashSet<String> = hashSetOf()
fun addFromUrl(url: String?) = viewModelScope.launch {
fun addFromUrl(url: String?) = ioSafe {
Log.i(TAG, "addFromUrl = $url")
if (url == null || hasAddedFromUrl.contains(url)) return@launch
if (url == null || hasAddedFromUrl.contains(url)) return@ioSafe
if(!url.startsWith("http")) return@ioSafe
SyncUtil.getIdsFromUrl(url)?.let { (malId, aniListId) ->
hasAddedFromUrl.add(url)
@ -166,7 +171,7 @@ class SyncViewModel : ViewModel() {
}
}
fun publishUserData() = viewModelScope.launch {
fun publishUserData() = ioSafe {
Log.i(TAG, "publishUserData")
val user = userData.value
if (user is Resource.Success) {
@ -191,7 +196,7 @@ class SyncViewModel : ViewModel() {
/// modifies the current sync data, return null if you don't want to change it
private fun modifyData(update: ((SyncAPI.SyncStatus) -> (SyncAPI.SyncStatus?))) =
viewModelScope.launch {
ioSafe {
syncs.apmap { (prefix, id) ->
repos.firstOrNull { it.idPrefix == prefix }?.let { repo ->
if (repo.hasAccount()) {
@ -209,7 +214,7 @@ class SyncViewModel : ViewModel() {
}
}
fun updateUserData() = viewModelScope.launch {
fun updateUserData() = ioSafe {
Log.i(TAG, "updateUserData")
_userDataResponse.postValue(Resource.Loading())
var lastError: Resource<SyncAPI.SyncStatus> = Resource.Failure(false, null, null, "No data")
@ -219,7 +224,7 @@ class SyncViewModel : ViewModel() {
val result = repo.getStatus(id)
if (result is Resource.Success) {
_userDataResponse.postValue(result)
return@launch
return@ioSafe
} else if (result is Resource.Failure) {
Log.e(TAG, "updateUserData error ${result.errorString}")
lastError = result
@ -230,7 +235,7 @@ class SyncViewModel : ViewModel() {
_userDataResponse.postValue(lastError)
}
private fun updateMetadata() = viewModelScope.launch {
private fun updateMetadata() = ioSafe {
Log.i(TAG, "updateMetadata")
_metaResponse.postValue(Resource.Loading())
@ -253,7 +258,7 @@ class SyncViewModel : ViewModel() {
val result = repo.getResult(id)
if (result is Resource.Success) {
_metaResponse.postValue(result)
return@launch
return@ioSafe
} else if (result is Resource.Failure) {
Log.e(
TAG,

View File

@ -0,0 +1,172 @@
package com.lagradost.cloudstream3.ui.result
import android.content.Context
import android.util.Log
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.lagradost.cloudstream3.mvvm.Some
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.UIHelper.setImage
sealed class UiText {
companion object {
const val TAG = "UiText"
}
data class DynamicString(val value: String) : UiText() {
override fun toString(): String = value
}
class StringResource(
@StringRes val resId: Int,
val args: List<Any>
) : UiText() {
override fun toString(): String =
"resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}"
}
fun asStringNull(context: Context?): String? {
try {
return asString(context ?: return null)
} catch (e: Exception) {
Log.e(TAG, "Got invalid data from $this")
logError(e)
return null
}
}
fun asString(context: Context): String {
return when (this) {
is DynamicString -> value
is StringResource -> {
val str = context.getString(resId)
if (args.isEmpty()) {
str
} else {
str.format(*args.map {
when (it) {
is UiText -> it.asString(context)
else -> it
}
}.toTypedArray())
}
}
}
}
}
sealed class UiImage {
data class Image(
val url: String,
val headers: Map<String, String>? = null,
@DrawableRes val errorDrawable: Int? = null
) : UiImage()
data class Drawable(@DrawableRes val resId: Int) : UiImage()
}
fun ImageView?.setImage(value: UiImage?) {
when (value) {
is UiImage.Image -> setImageImage(value)
is UiImage.Drawable -> setImageDrawable(value)
null -> {
this?.isVisible = false
}
}
}
fun ImageView?.setImageImage(value: UiImage.Image) {
if (this == null) return
this.isVisible = setImage(value.url, value.headers, value.errorDrawable)
}
fun ImageView?.setImageDrawable(value: UiImage.Drawable) {
if (this == null) return
this.isVisible = true
setImageResource(value.resId)
}
@JvmName("imgNull")
fun img(
url: String?,
headers: Map<String, String>? = null,
@DrawableRes errorDrawable: Int? = null
): UiImage? {
if (url.isNullOrBlank()) return null
return UiImage.Image(url, headers, errorDrawable)
}
fun img(
url: String,
headers: Map<String, String>? = null,
@DrawableRes errorDrawable: Int? = null
): UiImage {
return UiImage.Image(url, headers, errorDrawable)
}
fun img(@DrawableRes drawable: Int): UiImage {
return UiImage.Drawable(drawable)
}
fun txt(value: String): UiText {
return UiText.DynamicString(value)
}
@JvmName("txtNull")
fun txt(value: String?): UiText? {
return UiText.DynamicString(value ?: return null)
}
fun txt(@StringRes resId: Int, vararg args: Any): UiText {
return UiText.StringResource(resId, args.toList())
}
@JvmName("txtNull")
fun txt(@StringRes resId: Int?, vararg args: Any?): UiText? {
if (resId == null || args.any { it == null }) {
return null
}
return UiText.StringResource(resId, args.filterNotNull().toList())
}
fun TextView?.setText(text: UiText?) {
if (this == null) return
if (text == null) {
this.isVisible = false
} else {
val str = text.asStringNull(context)?.let {
if (this.maxLines == 1) {
it.replace("\n", " ")
} else {
it
}
}
this.isGone = str.isNullOrBlank()
this.text = str
}
}
fun TextView?.setTextHtml(text: UiText?) {
if (this == null) return
if (text == null) {
this.isVisible = false
} else {
val str = text.asStringNull(context)
this.isGone = str.isNullOrBlank()
this.text = str.html()
}
}
fun TextView?.setTextHtml(text: Some<UiText>?) {
setTextHtml(if (text is Some.Success) text.value else null)
}
fun TextView?.setText(text: Some<UiText>?) {
setText(if (text is Some.Success) text.value else null)
}

View File

@ -27,7 +27,7 @@ object SearchHelper {
} else {
if (card.isFromDownload) {
handleDownloadClick(
activity, card.name, DownloadClickEvent(
activity, DownloadClickEvent(
DOWNLOAD_ACTION_PLAY_FILE,
VideoDownloadHelper.DownloadEpisodeCached(
card.name,

View File

@ -44,6 +44,9 @@ import com.lagradost.cloudstream3.isMovieType
import com.lagradost.cloudstream3.mapper
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.toClassDir
import com.lagradost.cloudstream3.utils.JsUnpacker.Companion.load
@ -187,21 +190,21 @@ object AppUtils {
@WorkerThread
fun Context.addProgramsToContinueWatching(data: List<DataStoreHelper.ResumeWatchingResult>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val context = this
ioSafe {
data.forEach { episodeInfo ->
try {
val (program, id) = getWatchNextProgramByVideoId(episodeInfo.url, this)
val nextProgram = buildWatchNextProgramUri(this, episodeInfo)
val (program, id) = getWatchNextProgramByVideoId(episodeInfo.url, context)
val nextProgram = buildWatchNextProgramUri(context, episodeInfo)
// If the program is already in the Watch Next row, update it
if (program != null && id != null) {
PreviewChannelHelper(this).updateWatchNextProgram(
PreviewChannelHelper(context).updateWatchNextProgram(
nextProgram,
id,
)
} else {
PreviewChannelHelper(this)
PreviewChannelHelper(context)
.publishWatchNextProgram(nextProgram)
}
} catch (e: Exception) {
@ -313,6 +316,14 @@ object AppUtils {
//private val viewModel: ResultViewModel by activityViewModels()
private fun getResultsId(context: Context) : Int {
return if(context.isTrueTvSettings()) {
R.id.global_to_navigation_results_tv
} else {
R.id.global_to_navigation_results_phone
}
}
fun AppCompatActivity.loadResult(
url: String,
apiName: String,
@ -322,7 +333,7 @@ object AppUtils {
this.runOnUiThread {
// viewModelStore.clear()
this.navigate(
R.id.global_to_navigation_results,
getResultsId(this.applicationContext ?: return@runOnUiThread),
ResultFragment.newInstance(url, apiName, startAction, startValue)
)
}
@ -336,7 +347,7 @@ object AppUtils {
this?.runOnUiThread {
// viewModelStore.clear()
this.navigate(
R.id.global_to_navigation_results,
getResultsId(this),
ResultFragment.newInstance(card, startAction, startValue)
)
}

View File

@ -89,7 +89,9 @@ object BackupUtils {
val newFile = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.TITLE, displayName)
put(MediaStore.MediaColumns.MIME_TYPE, "application/json")
// While it a json file we store as txt because not
// all file managers support mimetype json
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
//put(MediaStore.MediaColumns.RELATIVE_PATH, folder)
}

View File

@ -12,7 +12,7 @@ object Coroutines {
}
}
fun ioSafe(work: suspend (() -> Unit)): Job {
fun ioSafe(work: suspend (CoroutineScope.() -> Unit)): Job {
return CoroutineScope(Dispatchers.IO).launch {
try {
work()
@ -22,7 +22,7 @@ object Coroutines {
}
}
suspend fun <T> ioWork(work: suspend (() -> T)): T {
suspend fun <T> ioWork(work: suspend (CoroutineScope.() -> T)): T {
return withContext(Dispatchers.IO) {
work()
}

View File

@ -18,6 +18,7 @@ const val RESULT_WATCH_STATE_DATA = "result_watch_state_data"
const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes
const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching"
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"
@ -163,7 +164,7 @@ object DataStoreHelper {
)
}
fun getLastWatchedOld(id: Int?): VideoDownloadHelper.ResumeWatching? {
private fun getLastWatchedOld(id: Int?): VideoDownloadHelper.ResumeWatching? {
if (id == null) return null
return getKey(
"$currentAccount/$RESULT_RESUME_WATCHING_OLD",
@ -192,8 +193,9 @@ object DataStoreHelper {
return getKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), null)
}
fun getDub(id: Int): DubStatus {
return DubStatus.values()[getKey("$currentAccount/$RESULT_DUB", id.toString()) ?: 0]
fun getDub(id: Int): DubStatus? {
return DubStatus.values()
.getOrNull(getKey("$currentAccount/$RESULT_DUB", id.toString(), -1) ?: -1)
}
fun setDub(id: Int, status: DubStatus) {
@ -221,14 +223,22 @@ object DataStoreHelper {
)
}
fun getResultSeason(id: Int): Int {
return getKey("$currentAccount/$RESULT_SEASON", id.toString()) ?: -1
fun getResultSeason(id: Int): Int? {
return getKey("$currentAccount/$RESULT_SEASON", id.toString(), null)
}
fun setResultSeason(id: Int, value: Int?) {
setKey("$currentAccount/$RESULT_SEASON", id.toString(), value)
}
fun getResultEpisode(id: Int): Int? {
return getKey("$currentAccount/$RESULT_EPISODE", id.toString(), null)
}
fun setResultEpisode(id: Int, value: Int?) {
setKey("$currentAccount/$RESULT_EPISODE", id.toString(), value)
}
fun addSync(id: Int, idPrefix: String, url: String) {
setKey("${idPrefix}_sync", id.toString(), url)
}

View File

@ -12,6 +12,9 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.add_account_input.*
import kotlinx.android.synthetic.main.add_account_input.text1
import kotlinx.android.synthetic.main.bottom_selection_dialog_direct.*
object SingleSelectionHelper {
fun Activity?.showOptionSelectStringRes(
@ -21,7 +24,7 @@ object SingleSelectionHelper {
tvOptions: List<Int> = listOf(),
callback: (Pair<Boolean, Int>) -> Unit
) {
if(this == null) return
if (this == null) return
this.showOptionSelect(
view,
@ -39,7 +42,7 @@ object SingleSelectionHelper {
tvOptions: List<String>,
callback: (Pair<Boolean, Int>) -> Unit
) {
if(this == null) return
if (this == null) return
if (this.isTvSettings()) {
val builder =
@ -86,42 +89,44 @@ object SingleSelectionHelper {
showApply: Boolean,
isMultiSelect: Boolean,
callback: (List<Int>) -> Unit,
dismissCallback: () -> Unit
dismissCallback: () -> Unit,
itemLayout: Int = R.layout.sort_bottom_single_choice
) {
if(this == null) return
if (this == null) return
val realShowApply = showApply || isMultiSelect
val listView = dialog.findViewById<ListView>(R.id.listview1)!!
val textView = dialog.findViewById<TextView>(R.id.text1)!!
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!!
val listView = dialog.listview1//.findViewById<ListView>(R.id.listview1)!!
val textView = dialog.text1//.findViewById<TextView>(R.id.text1)!!
val applyButton = dialog.apply_btt//.findViewById<TextView>(R.id.apply_btt)
val cancelButton = dialog.cancel_btt//findViewById<TextView>(R.id.cancel_btt)
val applyHolder = dialog.apply_btt_holder//.findViewById<LinearLayout>(R.id.apply_btt_holder)
applyHolder.isVisible = realShowApply
applyHolder?.isVisible = realShowApply
if (!realShowApply) {
val params = listView.layoutParams as LinearLayout.LayoutParams
params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0)
listView.layoutParams = params
}
textView.text = name
textView?.text = name
textView?.isGone = name.isBlank()
val arrayAdapter = ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
val arrayAdapter = ArrayAdapter<String>(this, itemLayout)
arrayAdapter.addAll(items)
listView.adapter = arrayAdapter
listView?.adapter = arrayAdapter
if (isMultiSelect) {
listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
listView?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
} else {
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
listView?.choiceMode = AbsListView.CHOICE_MODE_SINGLE
}
for (select in selectedIndex) {
listView.setItemChecked(select, true)
listView?.setItemChecked(select, true)
}
selectedIndex.minOrNull()?.let {
listView.setSelection(it)
listView?.setSelection(it)
}
// var lastSelectedIndex = if(selectedIndex.isNotEmpty()) selectedIndex.first() else -1
@ -130,7 +135,7 @@ object SingleSelectionHelper {
dismissCallback.invoke()
}
listView.setOnItemClickListener { _, _, which, _ ->
listView?.setOnItemClickListener { _, _, which, _ ->
// lastSelectedIndex = which
if (realShowApply) {
if (!isMultiSelect) {
@ -142,7 +147,7 @@ object SingleSelectionHelper {
}
}
if (realShowApply) {
applyButton.setOnClickListener {
applyButton?.setOnClickListener {
val list = ArrayList<Int>()
for (index in 0 until listView.count) {
if (listView.checkedItemPositions[index])
@ -151,7 +156,7 @@ object SingleSelectionHelper {
callback.invoke(list)
dialog.dismissSafe(this)
}
cancelButton.setOnClickListener {
cancelButton?.setOnClickListener {
dialog.dismissSafe(this)
}
}
@ -166,7 +171,7 @@ object SingleSelectionHelper {
callback: (String) -> Unit,
dismissCallback: () -> Unit
) {
if(this == null) return
if (this == null) return
val inputView = dialog.findViewById<EditText>(R.id.nginx_text_input)!!
val textView = dialog.findViewById<TextView>(R.id.text1)!!
@ -205,7 +210,7 @@ object SingleSelectionHelper {
dismissCallback: () -> Unit,
callback: (List<Int>) -> Unit,
) {
if(this == null) return
if (this == null) return
val builder =
AlertDialog.Builder(this, R.style.AlertDialogCustom)
@ -224,7 +229,7 @@ object SingleSelectionHelper {
dismissCallback: () -> Unit,
callback: (Int) -> Unit,
) {
if(this == null) return
if (this == null) return
val builder =
AlertDialog.Builder(this, R.style.AlertDialogCustom)
@ -271,6 +276,31 @@ object SingleSelectionHelper {
)
}
fun Activity.showBottomDialogInstant(
items: List<String>,
name: String,
dismissCallback: () -> Unit,
callback: (Int) -> Unit,
): BottomSheetDialog {
val builder =
BottomSheetDialog(this)
builder.setContentView(R.layout.bottom_selection_dialog_direct)
builder.show()
showDialog(
builder,
items,
listOf(),
name,
showApply = false,
isMultiSelect = false,
callback = { if (it.isNotEmpty()) callback.invoke(it.first()) },
dismissCallback = dismissCallback,
itemLayout = R.layout.sort_bottom_single_choice_no_checkmark
)
return builder
}
fun Activity.showNginxTextInputDialog(
name: String,
value: String,

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:color="?attr/iconGrayBackground" />
<item android:color="?attr/textColor" />
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:color="?attr/textColor"/>
<item android:color="?attr/iconGrayBackground"/>
</selector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true" android:drawable="@drawable/outline_less" /> <!-- focused -->
</selector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<stroke android:width="2dp"
android:color="?attr/white"/>
<corners
android:bottomLeftRadius="@dimen/rounded_button_radius"
android:bottomRightRadius="@dimen/rounded_button_radius"
android:topLeftRadius="@dimen/rounded_button_radius"
android:topRightRadius="@dimen/rounded_button_radius" />
</shape>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text1"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:textStyle="bold"
android:textSize="20sp"
android:textColor="?attr/textColor"
android:layout_width="match_parent"
android:layout_rowWeight="1"
android:text="@string/loading"
android:layout_height="wrap_content" />
<androidx.core.widget.ContentLoadingProgressBar
android:layout_marginBottom="-6.5dp"
android:indeterminate="true"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_gravity="center"
android:indeterminateTint="?attr/colorPrimary"
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:progressTint="?attr/colorPrimary"
android:layout_height="15dp">
</androidx.core.widget.ContentLoadingProgressBar>
</LinearLayout>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text1"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:textStyle="bold"
android:textSize="20sp"
android:textColor="?attr/textColor"
android:layout_width="match_parent"
android:layout_rowWeight="1"
tools:text="Test"
android:layout_height="wrap_content" />
<ListView
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/apply_btt"
android:id="@+id/listview1"
android:layout_marginBottom="60dp"
android:paddingTop="10dp"
android:requiresFadingEdge="vertical"
tools:listitem="@layout/sort_bottom_single_choice_no_checkmark"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_rowWeight="1" />
</LinearLayout>

View File

@ -12,7 +12,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading_chromecast"
android:text="@string/loading"
android:layout_gravity="center"
android:textColor="@color/textColor"
android:textSize="20sp"

View File

@ -5,6 +5,7 @@
android:id="@+id/result_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/DarkFragment"
android:background="?attr/primaryBlackBackground"
android:clickable="true"
android:focusable="true">
@ -290,15 +291,15 @@
<androidx.cardview.widget.CardView
android:id="@+id/result_poster_holder"
android:layout_width="100dp"
android:layout_height="140dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="@dimen/rounded_image_radius">
<ImageView
android:id="@+id/result_poster"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="100dp"
android:layout_height="140dp"
android:contentDescription="@string/result_poster_img_des"
android:foreground="@drawable/outline_drawable"
android:scaleType="centerCrop"
@ -465,6 +466,15 @@
android:textSize="15sp"
tools:text="@string/provider_info_meta" />
<TextView
android:id="@+id/result_no_episodes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:textColor="?attr/grayTextColor"
android:textSize="15sp"
tools:text="@string/no_episodes_found" />
<TextView
android:id="@+id/result_tag_holder"
android:layout_width="wrap_content"
@ -669,7 +679,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/result_series_parent"
android:id="@+id/result_resume_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
@ -835,7 +845,6 @@
<LinearLayout
android:id="@+id/result_next_airing_holder"
android:layout_gravity="start"
android:paddingBottom="15dp"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">

View File

@ -2,6 +2,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
style="@style/AlertDialogCustom"
android:layout_width="match_parent"
android:layout_height="match_parent">

View File

@ -0,0 +1,788 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/result_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/DarkFragment"
android:background="?attr/primaryBlackBackground">
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/result_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:orientation="vertical"
app:shimmer_auto_start="true"
app:shimmer_base_alpha="0.2"
app:shimmer_duration="@integer/loading_time"
app:shimmer_highlight_alpha="0.3"
tools:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/result_padding"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/loading_margin"
android:orientation="horizontal">
<include layout="@layout/loading_poster" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/loading_margin"
android:layout_marginEnd="@dimen/loading_margin"
android:orientation="vertical">
<include layout="@layout/loading_line" />
<include layout="@layout/loading_line" />
<include layout="@layout/loading_line" />
<include layout="@layout/loading_line" />
<include layout="@layout/loading_line_short" />
</LinearLayout>
</LinearLayout>
<ImageView
android:layout_width="match_parent"
android:layout_height="20dp"
tools:ignore="ContentDescription" />
<include layout="@layout/loading_episode" />
<include layout="@layout/loading_episode" />
<include layout="@layout/loading_episode" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<LinearLayout
android:id="@+id/result_loading_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="gone">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_reload_connectionerror"
style="@style/WhiteButton"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:minWidth="200dp"
android:text="@string/reload_error"
app:icon="@drawable/ic_baseline_autorenew_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_reload_connection_open_in_browser"
style="@style/BlackButton"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:minWidth="200dp"
android:text="@string/result_open_in_browser"
app:icon="@drawable/ic_baseline_public_24" />
<TextView
android:id="@+id/result_error_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:gravity="center"
android:textColor="?attr/textColor" />
</LinearLayout>
<LinearLayout
android:id="@+id/result_finish_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible"
android:orientation="vertical">
<androidx.core.widget.NestedScrollView
android:id="@+id/result_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/primaryGrayBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/primaryBlackBackground"
android:orientation="vertical">
<com.facebook.shimmer.ShimmerFrameLayout
tools:visibility="gone"
android:visibility="gone"
android:id="@+id/result_trailer_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
app:shimmer_auto_start="true"
app:shimmer_base_alpha="0.2"
app:shimmer_duration="@integer/loading_time"
app:shimmer_highlight_alpha="0.3">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/result_padding"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:background="@color/grayShimmer"
app:cardCornerRadius="@dimen/loading_radius"
android:layout_width="match_parent"
android:layout_height="150dp"
android:foreground="@drawable/outline_drawable" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<FrameLayout
android:descendantFocusability="blocksDescendants"
android:id="@+id/result_smallscreen_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<include layout="@layout/fragment_trailer" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/result_padding"
android:paddingEnd="@dimen/result_padding">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="15dp"
android:orientation="horizontal"
android:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_marginBottom="10dp"
android:layout_height="30dp">
<ImageView
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:layout_marginEnd="10dp"
android:nextFocusDown="@id/result_description"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:id="@+id/result_back"
android:clickable="true"
android:focusable="true"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@drawable/ic_baseline_arrow_back_24"
android:contentDescription="@string/go_back"
app:tint="?attr/white" />
<TextView
android:id="@+id/result_title"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:gravity="center_vertical"
android:layout_marginBottom="5dp"
android:maxLines="1"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
tools:text="The Perfect Run The Perfect Run" />
</LinearLayout>
<com.lagradost.cloudstream3.widget.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemSpacing="10dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_meta_site"
style="@style/SmallBlackButton"
android:layout_gravity="center_vertical"
tools:text="Gogoanime" />
<TextView
android:id="@+id/result_meta_type"
style="@style/ResultInfoText"
tools:text="Movie" />
<TextView
android:id="@+id/result_meta_year"
style="@style/ResultInfoText"
tools:text="2022" />
<TextView
android:id="@+id/result_meta_rating"
style="@style/ResultInfoText"
tools:text="Rated: 8.5/10.0" />
<TextView
android:id="@+id/result_meta_status"
style="@style/ResultInfoText"
tools:text="Ongoing" />
<TextView
android:id="@+id/result_meta_duration"
style="@style/ResultInfoText"
tools:text="121min" />
</com.lagradost.cloudstream3.widget.FlowLayout>
<!--
This has half margin and half padding to make TV focus on description look better.
The focus outline now settles between the poster and text.
-->
<TextView
android:padding="5dp"
android:maxLength="1000"
android:ellipsize="end"
android:id="@+id/result_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="@drawable/outline_drawable"
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_bookmark_button"
android:textColor="?attr/textColor"
android:textSize="15sp"
tools:text="Ryan Quicksave Romano is an eccentric adventurer with a strange power: he can create a save-point in time and redo his life whenever he dies. Arriving in New Rome, the glitzy capital of sin of a rebuilding Europe, he finds the city torn between mega-corporations, sponsored heroes, superpowered criminals, and true monsters. It's a time of chaos, where potions can grant the power to rule the world and dangers lurk everywhere. " />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/result_cast_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?attr/grayTextColor"
android:textSize="15sp"
tools:text="Cast: Joe Ligma" />
<androidx.recyclerview.widget.RecyclerView
tools:visibility="visible"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusDown="@id/result_play_movie"
android:id="@+id/result_cast_items"
android:layout_width="match_parent"
android:descendantFocusability="afterDescendants"
android:layout_height="wrap_content"
android:fadingEdge="horizontal"
android:focusableInTouchMode="false"
android:focusable="false"
android:orientation="horizontal"
android:paddingTop="5dp"
android:requiresFadingEdge="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="2"
tools:listitem="@layout/cast_item" />
<TextView
android:id="@+id/result_vpn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/grayTextColor"
android:textSize="15sp"
tools:text="@string/vpn_torrent" />
<TextView
android:id="@+id/result_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:textColor="?attr/grayTextColor"
android:textSize="15sp"
tools:text="@string/provider_info_meta" />
<TextView
android:id="@+id/result_no_episodes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:textColor="?attr/grayTextColor"
android:textSize="15sp"
tools:text="@string/no_episodes_found" />
<TextView
android:id="@+id/result_tag_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:text="@string/result_tags"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
android:visibility="gone" />
<com.lagradost.cloudstream3.widget.FlowLayout
android:id="@+id/result_tag"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/result_coming_soon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:paddingTop="50dp"
android:text="@string/coming_soon"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
android:visibility="gone" />
<LinearLayout
android:id="@+id/result_data_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_add_sync"
style="@style/WhiteButton"
android:layout_width="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginStart="0dp"
android:layout_marginBottom="10dp"
android:text="@string/add_sync"
android:visibility="gone"
app:icon="@drawable/ic_baseline_add_24" />
<LinearLayout
android:id="@+id/result_movie_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="horizontal"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton
android:nextFocusRight="@id/result_download_movie"
android:nextFocusUp="@id/result_cast_items"
android:nextFocusDown="@id/result_resume_series_button_play"
android:id="@+id/result_play_movie"
style="@style/WhiteButton"
android:layout_width="wrap_content"
android:layout_weight="1"
android:minWidth="250dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="10dp"
android:text="@string/play_movie_button"
android:visibility="visible"
app:icon="@drawable/ic_baseline_play_arrow_24">
</com.google.android.material.button.MaterialButton>
<FrameLayout
android:minWidth="250dp"
android:layout_marginStart="5dp"
android:id="@+id/result_movie_progress_downloaded_holder"
android:layout_width="wrap_content"
android:layout_weight="1"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:nextFocusLeft="@id/result_play_movie"
android:nextFocusUp="@id/result_cast_items"
android:nextFocusDown="@id/result_resume_series_button_play"
android:id="@+id/result_download_movie"
style="@style/BlackButton"
android:layout_width="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:clickable="true"
android:focusable="true"
android:visibility="visible" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal">
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/result_movie_progress_downloaded"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="end|center_vertical"
android:layout_margin="5dp"
android:background="@drawable/circle_shape"
android:indeterminate="false"
android:max="100"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:progress="30"
android:progressDrawable="@drawable/circular_progress_bar_filled"
android:visibility="visible" />
<ImageView
android:id="@+id/result_movie_download_icon"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/download"
android:src="@drawable/ic_baseline_play_arrow_24"
android:visibility="visible"
app:tint="?attr/white" />
<TextView
android:id="@+id/result_movie_download_text"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:letterSpacing="0.09"
android:textAllCaps="false"
android:textColor="?attr/textColor"
android:textSize="15sp"
android:textStyle="bold"
tools:text="Downloading" />
<TextView
android:id="@+id/result_movie_download_text_precentage"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:letterSpacing="0.09"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:textAllCaps="false"
android:textColor="?attr/textColor"
android:textSize="15sp"
android:textStyle="bold"
android:visibility="gone"
tools:text="68%" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/result_resume_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_next_series_button"
style="@style/WhiteButton"
android:layout_width="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="10dp"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusDown="@id/result_download_movie"
android:text="@string/next_episode"
android:visibility="gone"
app:icon="@drawable/cast_ic_mini_controller_skip_next" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/result_resume_series_button_play"
android:nextFocusUp="@id/result_play_movie"
android:nextFocusDown="@id/result_season_selection"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/download"
android:src="@drawable/ic_baseline_play_arrow_24"
android:visibility="visible"
app:tint="?attr/white" />
<TextView
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:layout_gravity="center"
android:gravity="center"
android:id="@+id/result_resume_series_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="bold"
tools:text="S1E1 Episode 1" />
<TextView
android:maxLines="1"
android:id="@+id/result_resume_series_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="0"
android:gravity="center"
android:textColor="?attr/grayTextColor"
tools:ignore="RtlSymmetry"
tools:text="69m remaining" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/result_resume_progress_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:visibility="gone"
tools:visibility="visible">
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/result_resume_series_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_gravity="end|center_vertical"
android:layout_weight="1"
android:indeterminate="false"
android:max="100"
android:progress="0"
android:progressBackgroundTint="?attr/colorPrimary"
android:visibility="visible"
tools:progress="50"
tools:visibility="visible" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:nextFocusUp="@id/result_resume_series_button_play"
android:nextFocusDown="@id/result_range_selection"
android:id="@+id/result_season_selection"
android:paddingBottom="10dp"
tools:listitem="@layout/result_selection"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:nextFocusUp="@id/result_season_selection"
android:nextFocusDown="@id/result_dub_selection"
android:id="@+id/result_range_selection"
android:paddingBottom="10dp"
tools:listitem="@layout/result_selection"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:nextFocusUp="@id/result_range_selection"
android:nextFocusDown="@id/result_episodes"
android:id="@+id/result_dub_selection"
android:paddingBottom="10dp"
tools:listitem="@layout/result_selection"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_marginBottom="5dp"
android:id="@+id/result_next_airing_holder"
android:layout_gravity="start"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/result_episodes_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="20dp"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="8 Episodes" />
<TextView
android:gravity="center"
android:id="@+id/result_next_airing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/grayTextColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="Episode 1022 will be released in" />
<TextView
android:paddingEnd="5dp"
android:paddingStart="5dp"
android:gravity="center"
android:id="@+id/result_next_airing_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="5d 3h 30m" />
</LinearLayout>
<com.facebook.shimmer.ShimmerFrameLayout
tools:visibility="gone"
android:id="@+id/result_episode_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginTop="15dp"
android:orientation="vertical"
app:shimmer_auto_start="true"
app:shimmer_base_alpha="0.2"
app:shimmer_duration="@integer/loading_time"
app:shimmer_highlight_alpha="0.3">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/loading_episode" />
<include layout="@layout/loading_episode" />
<include layout="@layout/loading_episode" />
<include layout="@layout/loading_episode" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<!--<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/result_episode_loading"
style="@style/Widget.AppCompat.ProgressBar"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp" />-->
<androidx.recyclerview.widget.RecyclerView
android:nextFocusUp="@id/result_dub_selection"
android:nextFocusDown="@id/result_recommendations_filter_selection"
android:id="@+id/result_episodes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/result_episode" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/result_recommendations_holder"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
tools:itemCount="2"
android:nextFocusUp="@id/result_episodes"
android:nextFocusDown="@id/result_recommendations"
android:id="@+id/result_recommendations_filter_selection"
android:layout_marginEnd="10dp"
tools:listitem="@layout/result_selection"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:gravity="center_vertical"
android:layout_gravity="center_vertical"
android:textSize="17sp"
android:textColor="?attr/textColor"
android:text="@string/recommended"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<com.lagradost.cloudstream3.ui.AutofitRecyclerView
android:nextFocusUp="@id/result_recommendations_filter_selection"
android:descendantFocusability="afterDescendants"
android:background="?attr/primaryBlackBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
app:spanCount="8"
android:id="@+id/result_recommendations"
tools:listitem="@layout/search_result_grid"
android:orientation="vertical" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</FrameLayout>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/DarkFragment"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/quick_search_root"

View File

@ -2,8 +2,6 @@
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:nextFocusLeft="@id/result_episode_download"
android:nextFocusRight="@id/result_episode_download"
android:id="@+id/episode_holder"
@ -15,6 +13,10 @@
app:cardElevation="0dp"
android:foreground="@drawable/outline_drawable"
android:layout_marginBottom="5dp">
<!--
android:nextFocusLeft="@id/result_episode_download"
-->
<!-- IDK BUT THIS DOES NOT SEAM LIKE A GOOD WAY OF DOING IT -->
<!--<LinearLayout
android:layout_width="fill_parent"
@ -110,9 +112,10 @@
android:progress="0"
android:visibility="visible" />
<!--
android:nextFocusRight="@id/episode_holder"-->
<ImageView
android:nextFocusLeft="@id/episode_holder"
android:nextFocusRight="@id/episode_holder"
app:tint="?attr/white"
android:id="@+id/result_episode_download"
android:visibility="visible"

View File

@ -4,5 +4,5 @@
android:layout_height="wrap_content">
<include android:visibility="gone" layout="@layout/result_episode" />
<include android:visibility="gone" layout="@layout/result_episode_large" />
<include android:visibility="visible" layout="@layout/result_episode_large" />
</FrameLayout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools">
<include
tools:visibility="visible"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="50dp"
layout="@layout/result_episode_tv" />
<include
tools:visibility="gone"
android:visibility="gone"
android:layout_width="450dp"
android:layout_height="wrap_content"
layout="@layout/result_episode_large_tv" />
</FrameLayout>

View File

@ -3,11 +3,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:nextFocusLeft="@id/episode_poster"
android:nextFocusRight="@id/result_episode_download"
android:id="@+id/episode_holder_large"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:cardBackgroundColor="?attr/boxItemBackground"
@ -19,7 +18,7 @@
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:padding="10dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
@ -33,8 +32,7 @@
android:foreground="@drawable/outline_drawable">
<ImageView
android:nextFocusLeft="@id/result_episode_download"
android:nextFocusRight="@id/episode_holder"
android:nextFocusRight="@id/result_episode_download"
android:id="@+id/episode_poster"
tools:src="@drawable/example_poster"
@ -126,7 +124,6 @@
<ImageView
android:nextFocusLeft="@id/episode_poster"
android:nextFocusRight="@id/episode_holder"
android:id="@+id/result_episode_download"
android:visibility="visible"

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:nextFocusRight="@id/result_episode_download"
android:id="@+id/episode_holder_large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:cardBackgroundColor="?attr/boxItemBackground"
android:foreground="@drawable/outline_drawable"
android:layout_marginBottom="10dp">
<LinearLayout
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:padding="10dp"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="wrap_content">
<!--app:cardCornerRadius="@dimen/roundedImageRadius"-->
<androidx.cardview.widget.CardView
android:layout_width="126dp"
android:layout_height="72dp"
android:foreground="@drawable/outline_drawable">
<ImageView
android:id="@+id/episode_poster"
tools:src="@drawable/example_poster"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/episode_poster_img_des" />
<ImageView
android:src="@drawable/play_button"
android:layout_gravity="center"
android:layout_width="36dp"
android:layout_height="36dp"
android:contentDescription="@string/play_episode" />
<androidx.core.widget.ContentLoadingProgressBar
android:layout_marginBottom="-1.5dp"
android:id="@+id/episode_progress"
android:progressTint="?attr/colorPrimary"
android:progressBackgroundTint="?attr/colorPrimary"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="match_parent"
tools:progress="50"
android:layout_gravity="bottom"
android:layout_height="5dp" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_marginStart="15dp"
android:orientation="vertical"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_marginEnd="50dp"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:layout_gravity="start"
style="@style/SmallBlackButton"
android:layout_marginEnd="10dp"
android:text="@string/filler"
android:id="@+id/episode_filler" />
<TextView
android:layout_gravity="center_vertical"
android:id="@+id/episode_text"
tools:text="1. Jobless"
android:textStyle="bold"
android:textColor="?attr/textColor"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:id="@+id/episode_rating"
tools:text="Rated: 8.8"
android:textColor="?attr/grayTextColor"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
<TextView
android:maxLines="4"
android:ellipsize="end"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:id="@+id/episode_descript"
android:textColor="?attr/grayTextColor"
tools:text="Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart. Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart."
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:nextFocusRight="@id/result_episode_download"
android:id="@+id/episode_holder"
android:layout_width="wrap_content"
android:layout_height="50dp"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:cardBackgroundColor="@color/transparent"
app:cardElevation="0dp"
android:foreground="@drawable/outline_drawable"
android:layout_marginBottom="5dp">
<androidx.core.widget.ContentLoadingProgressBar
android:layout_marginBottom="-1.5dp"
android:id="@+id/episode_progress"
android:progressTint="?attr/colorPrimary"
android:progressBackgroundTint="?attr/colorPrimary"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="match_parent"
tools:progress="50"
android:layout_gravity="bottom"
android:layout_height="5dp" />
<LinearLayout
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:gravity="center"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--marquee_forever-->
<com.google.android.material.button.MaterialButton
android:layout_marginEnd="10dp"
tools:visibility="visible"
android:gravity="center"
android:layout_gravity="center"
style="@style/SmallBlackButton"
android:text="@string/filler"
android:id="@+id/episode_filler" />
<TextView
android:id="@+id/episode_text"
android:layout_gravity="center_vertical"
android:gravity="center"
tools:text="Episode 1"
android:textColor="?attr/textColor"
android:scrollHorizontally="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="0"
android:singleLine="true"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -12,6 +12,6 @@
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:src="@drawable/default_cover"
android:background="#fffff0"
android:background="?attr/primaryGrayBackground"
android:contentDescription="@string/poster_image" />
</LinearLayout>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/SelectableButton"
android:layout_marginStart="0dp"
android:layout_marginEnd="10dp"
tools:text="Season 1" />

View File

@ -0,0 +1,22 @@
<!--<CheckedTextView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:textAppearance="?android:attr/textAppearanceListItemSmall"
android:gravity="center_vertical"
android:textColor="?attr/textColor"
tools:text="Example Text"
android:background="?attr/bitDarkerGrayBackground"
android:checkMark="?android:attr/listChoiceIndicatorSingle"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"/>
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/NoCheckLabel"
tools:text="hello"
android:textStyle="normal"
android:textColor="?attr/textColor"
android:id="@android:id/text1" />

View File

@ -4,10 +4,35 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
<action
android:id="@+id/global_to_navigation_results"
app:destination="@id/navigation_results"
android:id="@+id/global_to_navigation_results_tv"
app:destination="@id/navigation_results_tv"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim">
<argument
android:name="url"
app:argType="string" />
<argument
android:name="apiName"
app:argType="string" />
<argument
android:name="startAction"
android:defaultValue="0"
app:argType="integer" />
<argument
android:name="startValue"
android:defaultValue="0"
app:argType="integer" />
<argument
android:name="restart"
android:defaultValue="false"
app:argType="boolean" />
</action>
<action
android:id="@+id/global_to_navigation_results_phone"
app:destination="@id/navigation_results_phone"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
@ -216,13 +241,6 @@
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim"
tools:layout="@layout/fragment_home">
<action
android:id="@+id/action_navigation_home_to_navigation_results"
app:destination="@id/navigation_results"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_home_to_navigation_quick_search"
app:destination="@id/navigation_quick_search"
@ -241,13 +259,6 @@
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim"
tools:layout="@layout/fragment_search">
<action
android:id="@+id/action_navigation_search_to_navigation_results"
app:destination="@id/navigation_results"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
</fragment>
<fragment
@ -274,13 +285,6 @@
android:name="folder"
app:argType="string" />
</action>
<action
android:id="@+id/action_navigation_downloads_to_navigation_results"
app:destination="@id/navigation_results"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_downloads_to_navigation_player"
app:destination="@id/navigation_player"
@ -402,6 +406,56 @@
</fragment>
<fragment
android:id="@+id/navigation_results_phone"
android:name="com.lagradost.cloudstream3.ui.result.ResultFragmentPhone"
android:layout_height="match_parent"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim"
tools:layout="@layout/fragment_result_swipe">
<action
android:id="@+id/action_navigation_results_phone_to_navigation_quick_search"
app:destination="@id/navigation_quick_search"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_results_phone_to_navigation_player"
app:destination="@id/navigation_player"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
</fragment>
<fragment
android:id="@+id/navigation_results_tv"
android:name="com.lagradost.cloudstream3.ui.result.ResultFragmentTv"
android:layout_height="match_parent"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim"
tools:layout="@layout/fragment_result_swipe">
<action
android:id="@+id/action_navigation_results_tv_to_navigation_quick_search"
app:destination="@id/navigation_quick_search"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_results_tv_to_navigation_player"
app:destination="@id/navigation_player"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
</fragment>
<!--<fragment
android:id="@+id/navigation_results"
android:name="com.lagradost.cloudstream3.ui.result.ResultFragment"
android:layout_height="match_parent"
@ -424,7 +478,7 @@
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
</fragment>
</fragment>-->
<fragment
android:id="@+id/navigation_player"

View File

@ -29,7 +29,7 @@
<string name="result_share">شارك</string>
<string name="result_open_in_browser">فتح في الويب </string>
<string name="skip_loading">تخطي التحميل</string>
<string name="loading_chromecast">…تحميل</string>
<string name="loading">…تحميل</string>
<string name="type_watching">مشاهدة</string>
<string name="type_on_hold">في الانتظار</string>

View File

@ -107,7 +107,7 @@
<string name="result_share">Compartilhar</string>
<string name="result_open_in_browser">Abrir no Navegador</string>
<string name="skip_loading">Pular Carregamento</string>
<string name="loading_chromecast">Carregando…</string>
<string name="loading">Carregando…</string>
<string name="type_watching">Assistindo</string>
<string name="type_on_hold">Em espera</string>

View File

@ -100,7 +100,7 @@
<string name="result_share">Sdílet</string>
<string name="result_open_in_browser">Otevřít v prohlížeči</string>
<string name="skip_loading">Přeskočit načítání</string>
<string name="loading_chromecast">Načítání…</string>
<string name="loading">Načítání…</string>
<string name="type_watching">Sledování</string>
<string name="type_on_hold">Pozastaveno</string>

View File

@ -24,7 +24,7 @@
<string name="result_share">Teilen</string>
<string name="result_open_in_browser">Im Browser öffnen</string>
<string name="skip_loading">Buffern überspringen</string>
<string name="loading_chromecast">Lädt…</string>
<string name="loading">Lädt…</string>
<string name="type_watching">Am schauen</string>
<string name="type_on_hold">Pausiert</string>
<string name="type_completed">Abgeschlossen</string>

View File

@ -17,7 +17,7 @@
<string name="result_share">Μοίρασε</string>
<string name="result_open_in_browser">Άνοιγμα στον περιηγητή</string>
<string name="skip_loading">Προσπέραση φορτώματος</string>
<string name="loading_chromecast">Φόρτωση…</string>
<string name="loading">Φόρτωση…</string>
<string name="type_watching">Watching</string>
<string name="type_on_hold">On-Hold</string>

View File

@ -56,7 +56,7 @@
<string name="result_share">Compartir</string>
<string name="result_open_in_browser">Abrir en el navegador</string>
<string name="skip_loading">Omitir carga</string>
<string name="loading_chromecast">Cargando…</string>
<string name="loading">Cargando…</string>
<string name="type_watching">Viendo</string>
<string name="type_on_hold">En espera</string>

View File

@ -16,7 +16,7 @@
<string name="result_share">Partager</string>
<string name="result_open_in_browser">Ouvrir dans le naviguateur</string>
<string name="skip_loading">Passer le chargement</string>
<string name="loading_chromecast">Chargement…</string>
<string name="loading">Chargement…</string>
<string name="type_watching">En visionnage</string>
<string name="type_on_hold">En pose</string>
<string name="type_completed">Terminé</string>

View File

@ -52,7 +52,7 @@
<string name="result_share">Bagikan</string>
<string name="result_open_in_browser">Buka Di Browser</string>
<string name="skip_loading">Skip Loading</string>
<string name="loading_chromecast">Loading…</string>
<string name="loading">Loading…</string>
<string name="type_watching">Sedang Menonton</string>
<string name="type_on_hold">Tertahan</string>

View File

@ -52,7 +52,7 @@
<string name="result_share">Condividi</string>
<string name="result_open_in_browser">Apri nel browser</string>
<string name="skip_loading">Salta caricamento</string>
<string name="loading_chromecast">Caricamento…</string>
<string name="loading">Caricamento…</string>
<string name="type_watching">Guardando</string>
<string name="type_on_hold">In attesa</string>

View File

@ -20,7 +20,7 @@
<string name="result_share">Сподели</string>
<string name="result_open_in_browser">Отвори во прелистувач</string>
<string name="skip_loading">Прескокни вчитување</string>
<string name="loading_chromecast">Вчитување…</string>
<string name="loading">Вчитување…</string>
<string name="type_watching">Моментални гледања</string>
<string name="type_on_hold">Ставено на чекање</string>

View File

@ -17,7 +17,7 @@
<string name="result_share">aauuh</string>
<string name="result_open_in_browser">oooohh oooohhhaaaoouuh</string>
<string name="skip_loading">oooohhooooo</string>
<string name="loading_chromecast">ooh aaahhu</string>
<string name="loading">ooh aaahhu</string>
<string name="type_watching">aaaghh ooo-ahah</string>
<string name="type_on_hold">aaahhu</string>
<string name="type_completed">ahhahooo</string>

View File

@ -17,7 +17,7 @@
<string name="result_share">Deel</string>
<string name="result_open_in_browser">Openen in Browser</string>
<string name="skip_loading">Laden overslaan</string>
<string name="loading_chromecast">Laden…</string>
<string name="loading">Laden…</string>
<string name="type_watching">Aan het kijken</string>
<string name="type_on_hold">In de wacht</string>

View File

@ -28,7 +28,7 @@
<string name="result_share">Dele</string>
<string name="result_open_in_browser">Åpne i nettleseren</string>
<string name="skip_loading">Hopp over</string>
<string name="loading_chromecast">Laster inn…</string>
<string name="loading">Laster inn…</string>
<string name="type_watching">Ser på</string>
<string name="type_on_hold">På vent</string>

View File

@ -31,7 +31,7 @@
<string name="result_share">Udostępnij</string>
<string name="result_open_in_browser">Otwórz w przeglądarce</string>
<string name="skip_loading">Pomiń ładowanie</string>
<string name="loading_chromecast">Ładowanie…</string>
<string name="loading">Ładowanie…</string>
<string name="type_watching">W trakcie</string>
<string name="type_on_hold">Zawieszone</string>

View File

@ -31,7 +31,7 @@
<string name="result_share">Compartir</string>
<string name="result_open_in_browser">Abrir no Navegador</string>
<string name="skip_loading">Saltar Carga</string>
<string name="loading_chromecast">Cargando…</string>
<string name="loading">Cargando…</string>
<string name="type_watching">Assistindo</string>
<string name="type_on_hold">Em espera</string>

View File

@ -52,7 +52,7 @@
<string name="result_share">Distribuie</string>
<string name="result_open_in_browser">Deschide în browser</string>
<string name="skip_loading">Săriți încărcarea</string>
<string name="loading_chromecast">Se încarcă...</string>
<string name="loading">Se încarcă...</string>
<string name="type_watching">În curs de vizualizare</string>
<string name="type_on_hold">În așteptare</string>

View File

@ -20,7 +20,7 @@
<string name="result_share">Dela</string>
<string name="result_open_in_browser">Öppna i webbläsaren</string>
<string name="skip_loading">Hoppa över</string>
<string name="loading_chromecast">Laddar…</string>
<string name="loading">Laddar…</string>
<string name="type_watching">Tittar på</string>
<string name="type_on_hold">Pausad</string>

View File

@ -35,7 +35,7 @@
<string name="result_share">I-share</string>
<string name="result_open_in_browser">Buksan sa browser</string>
<string name="skip_loading">Skip Loading…</string>
<string name="loading_chromecast">Loading…</string>
<string name="loading">Loading…</string>
<string name="type_watching">Pinapanood</string>
<string name="type_on_hold">Inihinto</string>

View File

@ -56,7 +56,7 @@
<string name="result_share">Paylaş</string>
<string name="result_open_in_browser">Tarayıcıda aç</string>
<string name="skip_loading">Yüklemeyi atla</string>
<string name="loading_chromecast">Yükleniyor…</string>
<string name="loading">Yükleniyor…</string>
<string name="type_watching">İzleniyor</string>
<string name="type_on_hold">Beklemede</string>

View File

@ -107,7 +107,7 @@
<string name="result_share">Chia sẻ</string>
<string name="result_open_in_browser">Mở bằng trình duyệt</string>
<string name="skip_loading">Bỏ qua</string>
<string name="loading_chromecast">Đang tải…</string>
<string name="loading">Đang tải…</string>
<string name="type_watching">Đang xem</string>
<string name="type_on_hold">Đang chờ</string>

View File

@ -38,7 +38,7 @@
<string name="result_share">分享</string>
<string name="result_open_in_browser">在浏览器中打开</string>
<string name="skip_loading">跳过加载</string>
<string name="loading_chromecast">正在加载…</string>
<string name="loading">正在加载…</string>
<string name="type_watching">正在观看</string>
<string name="type_on_hold">暂时搁置</string>

View File

@ -3,6 +3,8 @@
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="rounded_image_radius">10dp</dimen>
<dimen name="rounded_button_radius">4dp</dimen>
<dimen name="navbar_height">0dp</dimen>
<dimen name="card_corner_radius">2dp</dimen>
<dimen name="result_padding">15dp</dimen>

View File

@ -109,7 +109,7 @@
<string name="result_share">Share</string>
<string name="result_open_in_browser">Open In Browser</string>
<string name="skip_loading">Skip Loading</string>
<string name="loading_chromecast">Loading…</string>
<string name="loading">Loading…</string>
<string name="type_watching">Watching</string>
<string name="type_on_hold">On-Hold</string>
@ -283,9 +283,12 @@
</string>
<string name="season">Season</string>
<string name="season_format">%s %d</string>
<string name="no_season">No Season</string>
<string name="episode">Episode</string>
<string name="episodes">Episodes</string>
<string name="episodes_range">%d-%d</string>
<string name="episode_format" formatted="true">%d %s</string>
<string name="season_short">S</string>
<string name="episode_short">E</string>
<string name="no_episodes_found">No Episodes found</string>

View File

@ -266,6 +266,7 @@
</style>
<style name="AppBottomSheetDialogTheme">
<item name="android:navigationBarColor">?attr/boxItemBackground</item>
<item name="android:windowCloseOnTouchOutside">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowAnimationStyle">@style/Animation.Design.BottomSheetDialog</item>
@ -278,7 +279,7 @@
<item name="behavior_skipCollapsed">true</item>
<item name="shapeAppearance">@null</item>
<item name="shapeAppearanceOverlay">@null</item>
<item name="backgroundTint">?android:attr/colorBackground</item>
<item name="backgroundTint">@color/transparent</item>
<item name="android:background">@drawable/rounded_dialog</item>
<item name="behavior_peekHeight">512dp</item>
</style>
@ -334,6 +335,10 @@
<item name="tabMode">scrollable</item>-->
</style>
<style name="DarkFragment" parent="AppTheme">
<item name="android:navigationBarColor">?attr/colorPrimary</item>
</style>
<style name="AlertDialogCustom" parent="Theme.AppCompat.Dialog.Alert">
<item name="android:windowFullscreen">true</item>
<item name="android:textColor">?attr/textColor</item>
@ -427,12 +432,12 @@
<item name="android:textAllCaps">false</item>
<item name="iconGravity">textStart</item>
<item name="iconSize">20dp</item>
<item name="cornerRadius">4dp</item>
<item name="cornerRadius">@dimen/rounded_button_radius</item>
<item name="android:textSize">15sp</item>
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="android:foreground">@drawable/outline_drawable</item>
<item name="android:foreground">@drawable/outline_drawable_less</item>
</style>
<style name="WhiteButton" parent="NiceButton">
@ -448,7 +453,15 @@
<item name="android:textColor">?attr/textColor</item>
</style>
<style name="CheckLabel" parent="@style/AppTextViewStyle">
<style name="CheckLabel" parent="@style/NoCheckLabel">
<!-- <item name="drawableTint">@color/check_selection_color</item>-->
<!-- Set color in the drawable instead of tint to allow multiple drawables-->
<item name="android:checkMark">?android:attr/listChoiceIndicatorSingle</item>
<item name="drawableStartCompat">@drawable/ic_baseline_check_24_listview</item>
</style>
<style name="NoCheckLabel" parent="@style/AppTextViewStyle">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:minHeight">?android:attr/listPreferredItemHeightSmall</item>
@ -458,15 +471,12 @@
<item name="android:gravity">center_vertical</item>
<item name="android:paddingStart">12dp</item>
<item name="android:paddingEnd">12dp</item>
<item name="android:checkMark">?android:attr/listChoiceIndicatorSingle</item>
<item name="android:ellipsize">marquee</item>
<item name="android:foreground">?attr/selectableItemBackgroundBorderless</item>
<item name="android:drawablePadding">20dp</item>
<!-- <item name="drawableTint">@color/check_selection_color</item>-->
<!-- Set color in the drawable instead of tint to allow multiple drawables-->
<item name="drawableStartCompat">@drawable/ic_baseline_check_24_listview</item>
</style>
<style name="BlackButton" parent="NiceButton">
<item name="strokeColor">?attr/textColor</item>
<item name="backgroundTint">?attr/iconGrayBackground</item>
@ -534,6 +544,23 @@
<style name="MultiSelectButton" parent="BlackButton">
<item name="android:layout_height">40dp</item>
<item name="android:layout_width">wrap_content</item>
<item name="strokeColor">?attr/textColor</item>
<item name="backgroundTint">?attr/iconGrayBackground</item>
<item name="iconTint">?attr/textColor</item>
<item name="android:textColor">?attr/textColor</item>
<item name="rippleColor">?attr/textColor</item>
</style>
<style name="SelectableButton" parent="NiceButton">
<item name="android:layout_height">40dp</item>
<item name="android:layout_width">wrap_content</item>
<item name="strokeColor">@color/selectable_black</item>
<item name="backgroundTint">@color/selectable_white</item>
<item name="iconTint">@color/selectable_black</item>
<item name="android:textColor">@color/selectable_black</item>
<item name="rippleColor">@color/selectable_black</item>
</style>
<style name="VideoButton">