mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge branch 'master' of https://github.com/reduplicated/Cloudstream
This commit is contained in:
commit
718199ee39
68 changed files with 3921 additions and 2673 deletions
2
.github/ISSUE_TEMPLATE/application-bug.yml
vendored
2
.github/ISSUE_TEMPLATE/application-bug.yml
vendored
|
@ -75,7 +75,7 @@ body:
|
|||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I have updated the app to pre-release version **[Latest](https://github.com/LagradOst/CloudStream-3/releases)**.
|
||||
- label: I have updated the app to pre-release version **[Latest](https://github.com/rereleased/release/releases)**.
|
||||
required: true
|
||||
- label: If related to a provider, I have checked the site and it works, but not the app.
|
||||
required: true
|
||||
|
|
18
.github/workflows/prerelease.yml
vendored
18
.github/workflows/prerelease.yml
vendored
|
@ -44,12 +44,12 @@ jobs:
|
|||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
- name: Create pre-release
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
automatic_release_tag: "pre-release"
|
||||
prerelease: true
|
||||
title: "Pre-release Build"
|
||||
files: |
|
||||
app/build/outputs/apk/prerelease/*.apk
|
||||
#- name: Create pre-release
|
||||
# uses: "marvinpinto/action-automatic-releases@latest"
|
||||
# with:
|
||||
# repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# automatic_release_tag: "pre-release"
|
||||
# prerelease: true
|
||||
# title: "Pre-release Build"
|
||||
# files: |
|
||||
# app/build/outputs/apk/prerelease/*.apk
|
||||
|
|
17
README.md
17
README.md
|
@ -1,15 +1,14 @@
|
|||
# CloudStream-3
|
||||
|
||||
<!-- ![Maintenance](https://img.shields.io/maintenance/yes/2022?color=blue&style=for-the-badge) -->
|
||||
![GitHub release](https://img.shields.io/github/v/release/LagradOst/cloudstream-3?sort=semver&style=for-the-badge)
|
||||
![Downloads](https://img.shields.io/github/downloads/lagradost/CloudStream-3/total?color=blue&style=for-the-badge)
|
||||
![Build](https://img.shields.io/github/workflow/status/lagradost/CloudStream-3/Pre-release?style=for-the-badge)
|
||||
<!--![GitHub release](https://img.shields.io/github/v/release/LagradOst/cloudstream-3?sort=semver&style=for-the-badge)-->
|
||||
<!--![Downloads](https://img.shields.io/github/downloads/lagradost/CloudStream-3/total?color=blue&style=for-the-badge)-->
|
||||
<!--![Build](https://img.shields.io/github/workflow/status/lagradost/CloudStream-3/Pre-release?style=for-the-badge)-->
|
||||
[![Discord](https://img.shields.io/discord/737724143126052974?style=for-the-badge)](https://discord.gg/5Hus6fM)
|
||||
|
||||
**Download:** (Third-party distributor, not related to this repository)
|
||||
|
||||
**DOWNLOAD:**
|
||||
https://github.com/LagradOst/CloudStream-3/releases
|
||||
|
||||
https://github.com/rereleased/release/releases/
|
||||
|
||||
***Features:***
|
||||
+ **AdFree**, No ads whatsoever
|
||||
|
@ -19,7 +18,6 @@ https://github.com/LagradOst/CloudStream-3/releases
|
|||
+ Chromecast
|
||||
|
||||
***Screenshots:***
|
||||
(All the images are blurred because of DMCA reasons, but are actually not blurred in the app)
|
||||
|
||||
<img src="./.github/home.jpg" height="400"/><img src="./.github/search.jpg" height="400"/><img src="./.github/downloads.jpg" height="400"/><img src="./.github/results.jpg" height="400"/>
|
||||
<img src="./.github/player.jpg" height="200"/>
|
||||
|
@ -58,8 +56,3 @@ The app is purely for educational and personal use.
|
|||
CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface.
|
||||
|
||||
It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk.
|
||||
|
||||
|
||||
***Sites used:***
|
||||
|
||||
Look [here](https://lagradost.github.io/CloudStream-3/) for a comprehensive list
|
||||
|
|
|
@ -35,8 +35,8 @@ android {
|
|||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
|
||||
versionCode 49
|
||||
versionName "3.0.1"
|
||||
versionCode 50
|
||||
versionName "3.0.2"
|
||||
|
||||
resValue "string", "app_version",
|
||||
"${defaultConfig.versionName}${versionNameSuffix ?: ""}"
|
||||
|
@ -98,10 +98,10 @@ dependencies {
|
|||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||
implementation 'com.google.android.material:material:1.5.0' // dont change this to 1.6.0 it looks ugly af
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.1'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
|
|
@ -198,7 +198,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()
|
||||
}
|
||||
|
||||
|
@ -371,7 +371,7 @@ object APIHolder {
|
|||
*/
|
||||
const val PROVIDER_STATUS_KEY = "PROVIDER_STATUS_KEY"
|
||||
const val PROVIDER_STATUS_URL =
|
||||
"https://raw.githubusercontent.com/LagradOst/CloudStream-3/master/docs/providers.json"
|
||||
"https://raw.githubusercontent.com/reduplicated/Cloudstream/master/docs/providers.json"
|
||||
const val PROVIDER_STATUS_BETA_ONLY = 3
|
||||
const val PROVIDER_STATUS_SLOW = 2
|
||||
const val PROVIDER_STATUS_OK = 1
|
||||
|
@ -645,6 +645,7 @@ enum class ShowStatus {
|
|||
}
|
||||
|
||||
enum class DubStatus(val id: Int) {
|
||||
None(-1),
|
||||
Dubbed(1),
|
||||
Subbed(0),
|
||||
}
|
||||
|
@ -979,6 +980,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)) }
|
||||
|
@ -1119,6 +1124,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 {
|
||||
|
|
|
@ -156,7 +156,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,
|
||||
|
@ -332,6 +336,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
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)
|
||||
|
@ -342,10 +347,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
|
||||
)
|
||||
|
|
|
@ -54,7 +54,7 @@ class GogoanimeProvider : MainAPI() {
|
|||
secretKeyString: String,
|
||||
encrypt: Boolean = true
|
||||
): String {
|
||||
println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string")
|
||||
//println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string")
|
||||
val ivParameterSpec = IvParameterSpec(iv.toByteArray())
|
||||
val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES")
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
|
|
|
@ -29,7 +29,7 @@ class WcoHelper {
|
|||
private var newKeys: NewExternalKeys? = null
|
||||
private suspend fun getKeys() {
|
||||
keys = keys
|
||||
?: app.get("https://raw.githubusercontent.com/LagradOst/CloudStream-3/master/docs/keys.json")
|
||||
?: app.get("https://raw.githubusercontent.com/reduplicated/Cloudstream/master/docs/keys.json")
|
||||
.parsedSafe<ExternalKeys>()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey(
|
||||
BACKUP_KEY_DATA
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -84,7 +84,7 @@ class DownloadChildFragment : Fragment() {
|
|||
DownloadChildAdapter(
|
||||
ArrayList(),
|
||||
) { click ->
|
||||
handleDownloadClick(activity, name, click)
|
||||
handleDownloadClick(activity, click)
|
||||
}
|
||||
|
||||
downloadDeleteEventListener = { id: Int ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,7 +57,7 @@ const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14
|
|||
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
|
||||
|
||||
class EpisodeAdapter(
|
||||
var cardList: List<ResultEpisode>,
|
||||
private var cardList: MutableList<ResultEpisode>,
|
||||
private val hasDownloadSupport: Boolean,
|
||||
private val clickCallback: (EpisodeClickEvent) -> Unit,
|
||||
private val downloadClickCallback: (DownloadClickEvent) -> Unit,
|
||||
|
@ -92,13 +93,15 @@ 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)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
@ -263,3 +266,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]
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,380 @@
|
|||
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 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.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.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_recommendations?.spanCount = 3
|
||||
result_overlapping_panels?.setStartPanelLockState(OverlappingPanelsLayout.LockState.CLOSE)
|
||||
result_overlapping_panels?.setEndPanelLockState(OverlappingPanelsLayout.LockState.CLOSE)
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
//showBottomDialogInstant
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.lagradost.cloudstream3.ui.result
|
||||
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
|
||||
class ResultFragmentTv : ResultFragment() {
|
||||
override val resultLayout = R.layout.fragment_result_tv
|
||||
override fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
|
||||
|
||||
}
|
||||
}
|
|
@ -1,625 +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.animeproviders.GogoanimeProvider
|
||||
import com.lagradost.cloudstream3.animeproviders.NineAnimeProvider
|
||||
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>()
|
||||
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
|
||||
}
|
||||
|
||||
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
|
@ -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,10 @@ 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
|
||||
SyncUtil.getIdsFromUrl(url)?.let { (malId, aniListId) ->
|
||||
hasAddedFromUrl.add(url)
|
||||
|
||||
|
@ -166,7 +169,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 +194,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 +212,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 +222,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 +233,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 +256,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,
|
||||
|
|
172
app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt
Normal file
172
app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt
Normal 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)
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -44,6 +44,8 @@ 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.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 +189,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 +315,15 @@ object AppUtils {
|
|||
|
||||
//private val viewModel: ResultViewModel by activityViewModels()
|
||||
|
||||
private fun getResultsId(context: Context) : Int {
|
||||
return R.id.global_to_navigation_results_phone
|
||||
//return if(context.isTvSettings()) {
|
||||
// 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)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -31,6 +31,9 @@ import kotlin.concurrent.thread
|
|||
|
||||
class InAppUpdater {
|
||||
companion object {
|
||||
const val GITHUB_USER_NAME = "rereleased"
|
||||
const val GITHUB_REPO = "release"
|
||||
|
||||
// === IN APP UPDATER ===
|
||||
data class GithubAsset(
|
||||
@JsonProperty("name") val name: String,
|
||||
|
@ -81,7 +84,7 @@ class InAppUpdater {
|
|||
}
|
||||
|
||||
private fun Activity.getReleaseUpdate(): Update {
|
||||
val url = "https://api.github.com/repos/LagradOst/CloudStream-3/releases"
|
||||
val url = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases"
|
||||
val headers = mapOf("Accept" to "application/vnd.github.v3+json")
|
||||
val response =
|
||||
parseJson<List<GithubRelease>>(runBlocking {
|
||||
|
@ -148,8 +151,8 @@ class InAppUpdater {
|
|||
|
||||
private fun Activity.getPreReleaseUpdate(): Update = runBlocking {
|
||||
val tagUrl =
|
||||
"https://api.github.com/repos/LagradOst/CloudStream-3/git/ref/tags/pre-release"
|
||||
val releaseUrl = "https://api.github.com/repos/LagradOst/CloudStream-3/releases"
|
||||
"https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/git/ref/tags/pre-release"
|
||||
val releaseUrl = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases"
|
||||
val headers = mapOf("Accept" to "application/vnd.github.v3+json")
|
||||
val response =
|
||||
parseJson<List<GithubRelease>>(app.get(releaseUrl, headers = headers).text)
|
||||
|
|
|
@ -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,
|
||||
|
|
5
app/src/main/res/color/selectable_black.xml
Normal file
5
app/src/main/res/color/selectable_black.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/textColor"/>
|
||||
<item android:state_focused="true" android:color="?attr/iconGrayBackground"/>
|
||||
</selector>
|
5
app/src/main/res/color/selectable_white.xml
Normal file
5
app/src/main/res/color/selectable_white.xml
Normal 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_focused="true" android:color="?attr/textColor"/>
|
||||
<item android:color="?attr/iconGrayBackground"/>
|
||||
</selector>
|
33
app/src/main/res/layout/bottom_loading.xml
Normal file
33
app/src/main/res/layout/bottom_loading.xml
Normal 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>
|
34
app/src/main/res/layout/bottom_selection_dialog_direct.xml
Normal file
34
app/src/main/res/layout/bottom_selection_dialog_direct.xml
Normal 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>
|
|
@ -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"
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
||||
|
|
699
app/src/main/res/layout/fragment_result_tv.xml
Normal file
699
app/src/main/res/layout/fragment_result_tv.xml
Normal file
|
@ -0,0 +1,699 @@
|
|||
<?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"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:maxLines="2"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="The Perfect Run The Perfect Run" />
|
||||
|
||||
<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="gone"
|
||||
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="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:orientation="horizontal"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/result_play_movie"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:nextFocusUp="@id/result_bookmark_button"
|
||||
android:nextFocusDown="@id/result_download_movie"
|
||||
android:text="@string/play_movie_button"
|
||||
android:visibility="visible"
|
||||
app:icon="@drawable/ic_baseline_play_arrow_24">
|
||||
|
||||
<requestFocus />
|
||||
|
||||
</com.google.android.material.button.MaterialButton>
|
||||
|
||||
|
||||
<FrameLayout
|
||||
android:layout_marginStart="5dp"
|
||||
android:id="@+id/result_movie_progress_downloaded_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
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:nextFocusUp="@id/result_play_movie"
|
||||
android:nextFocusDown="@id/result_season_button"
|
||||
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:layout_marginEnd="10dp"
|
||||
android:id="@+id/result_resume_series_button_play"
|
||||
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: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:paddingStart="10dp"
|
||||
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:id="@+id/result_episodes_tab"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:paddingBottom="10dp"
|
||||
android:id="@+id/result_season_selection"
|
||||
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>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:paddingBottom="10dp"
|
||||
android:id="@+id/result_range_selection"
|
||||
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>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:paddingBottom="10dp"
|
||||
android:id="@+id/result_dub_selection"
|
||||
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>
|
||||
|
||||
<LinearLayout
|
||||
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: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
|
||||
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"
|
||||
tools:visibility="visible">
|
||||
|
||||
<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:id="@+id/result_episodes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="0dp"
|
||||
android:clipToPadding="false"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:paddingBottom="100dp"
|
||||
tools:listitem="@layout/result_episode" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
8
app/src/main/res/layout/result_selection.xml
Normal file
8
app/src/main/res/layout/result_selection.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.button.MaterialButton android:id="@+id/result_season_button"
|
||||
style="@style/SelectableButton"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
tools:text="Season 1"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" />
|
|
@ -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" />
|
|
@ -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"
|
||||
|
@ -181,13 +206,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"
|
||||
|
@ -206,13 +224,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
|
||||
|
@ -239,13 +250,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"
|
||||
|
@ -360,6 +364,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"
|
||||
|
@ -382,7 +436,7 @@
|
|||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
</fragment>
|
||||
</fragment>-->
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_player"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -108,7 +108,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>
|
||||
|
|
|
@ -101,7 +101,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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -108,7 +108,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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -110,7 +110,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>
|
||||
|
@ -286,9 +286,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>
|
||||
|
|
|
@ -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>
|
||||
|
@ -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">
|
||||
|
|
|
@ -26,10 +26,10 @@
|
|||
<Preference
|
||||
android:title="@string/github"
|
||||
android:icon="@drawable/ic_github_logo"
|
||||
app:summary="https://github.com/LagradOst/CloudStream-3">
|
||||
app:summary="https://github.com/rereleased/release/releases">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="https://github.com/LagradOst/CloudStream-3" />
|
||||
android:data="https://github.com/rereleased/release/releases" />
|
||||
</Preference>
|
||||
|
||||
<Preference
|
||||
|
|
|
@ -543,7 +543,7 @@
|
|||
"TrailersTwoProvider": {
|
||||
"language": "en",
|
||||
"name": "Trailers.to",
|
||||
"status": 1,
|
||||
"status": 0,
|
||||
"url": "https://trailers.to"
|
||||
},
|
||||
"TwoEmbedProvider": {
|
||||
|
|
292
providers.json
292
providers.json
|
@ -1,292 +0,0 @@
|
|||
{
|
||||
"AkwamProvider": {
|
||||
"name": "Akwam",
|
||||
"url": "https://akwam.to",
|
||||
"status": 1
|
||||
},
|
||||
"AllAnimeProvider": {
|
||||
"name": "AllAnime",
|
||||
"url": "https://allanime.site",
|
||||
"status": 1
|
||||
},
|
||||
"AllMoviesForYouProvider": {
|
||||
"name": "AllMoviesForYou",
|
||||
"url": "https://allmoviesforyou.net",
|
||||
"status": 1
|
||||
},
|
||||
"AnimeFlickProvider": {
|
||||
"name": "AnimeFlick",
|
||||
"url": "https://animeflick.net",
|
||||
"status": 1
|
||||
},
|
||||
"AnimePaheProvider": {
|
||||
"name": "AnimePahe",
|
||||
"url": "https://animepahe.com",
|
||||
"status": 0
|
||||
},
|
||||
"AnimeWorldProvider": {
|
||||
"name": "AnimeWorld",
|
||||
"url": "https://www.animeworld.tv",
|
||||
"status": 1
|
||||
},
|
||||
"AnimeflvnetProvider": {
|
||||
"name": "Animeflv.net",
|
||||
"url": "https://www3.animeflv.net",
|
||||
"status": 1
|
||||
},
|
||||
"AnimekisaProvider": {
|
||||
"name": "Animekisa",
|
||||
"url": "https://animekisa.in",
|
||||
"status": 1
|
||||
},
|
||||
"AsianLoadProvider": {
|
||||
"name": "AsianLoad",
|
||||
"url": "https://asianembed.io",
|
||||
"status": 1
|
||||
},
|
||||
"AsiaFlixProvider": {
|
||||
"name": "AsiaFlix",
|
||||
"url": "https://asiaflix.app",
|
||||
"status": 0
|
||||
},
|
||||
"BflixProvider": {
|
||||
"name": "Bflix",
|
||||
"url": "https://bflix.ru",
|
||||
"status": 0
|
||||
},
|
||||
"FmoviesToProvider": {
|
||||
"name": "Fmovies.to",
|
||||
"url": "https://fmovies.to",
|
||||
"status": 0
|
||||
},
|
||||
"SflixProProvider": {
|
||||
"name": "Sflix.pro",
|
||||
"url": "https://sflix.pro",
|
||||
"status": 0
|
||||
},
|
||||
"CinecalidadProvider": {
|
||||
"name": "Cinecalidad",
|
||||
"url": "https://cinecalidad.lol",
|
||||
"status": 1
|
||||
},
|
||||
"CrossTmdbProvider": {
|
||||
"name": "MultiMovie",
|
||||
"url": "NONE",
|
||||
"status": 1
|
||||
},
|
||||
"CuevanaProvider": {
|
||||
"name": "Cuevana",
|
||||
"url": "https://cuevana3.me",
|
||||
"status": 1
|
||||
},
|
||||
"DoramasYTProvider": {
|
||||
"name": "DoramasYT",
|
||||
"url": "https://doramasyt.com",
|
||||
"status": 1
|
||||
},
|
||||
"DramaSeeProvider": {
|
||||
"name": "DramaSee",
|
||||
"url": "https://dramasee.net",
|
||||
"status": 1
|
||||
},
|
||||
"DubbedAnimeProvider": {
|
||||
"name": "DubbedAnime",
|
||||
"url": "https://bestdubbedanime.com",
|
||||
"status": 1
|
||||
},
|
||||
"EgyBestProvider": {
|
||||
"name": "EgyBest",
|
||||
"url": "https://egy.best",
|
||||
"status": 0
|
||||
},
|
||||
"EntrepeliculasyseriesProvider": {
|
||||
"name": "EntrePeliculasySeries",
|
||||
"url": "https://entrepeliculasyseries.nu",
|
||||
"status": 1
|
||||
},
|
||||
"FilmanProvider": {
|
||||
"name": "filman.cc",
|
||||
"url": "https://filman.cc",
|
||||
"status": 1
|
||||
},
|
||||
"FrenchStreamProvider": {
|
||||
"name": "French Stream",
|
||||
"url": "https://french-stream.re",
|
||||
"status": 1
|
||||
},
|
||||
"GogoanimeProvider": {
|
||||
"name": "GogoAnime",
|
||||
"url": "https://gogoanime.sk",
|
||||
"status": 1
|
||||
},
|
||||
"KawaiifuProvider": {
|
||||
"name": "Kawaiifu",
|
||||
"url": "https://kawaiifu.com",
|
||||
"status": 1
|
||||
},
|
||||
"HDMProvider": {
|
||||
"name": "HD Movies",
|
||||
"url": "https://hdm.to",
|
||||
"status": 0
|
||||
},
|
||||
"IHaveNoTvProvider": {
|
||||
"name": "I Have No TV",
|
||||
"url": "https://ihavenotv.com",
|
||||
"status": 1
|
||||
},
|
||||
"KdramaHoodProvider": {
|
||||
"name": "KDramaHood",
|
||||
"url": "https://kdramahood.com",
|
||||
"status": 1
|
||||
},
|
||||
"LookMovieProvider": {
|
||||
"name": "LookMovie",
|
||||
"url": "https://lookmovie.io",
|
||||
"status": 0
|
||||
},
|
||||
"MeloMovieProvider": {
|
||||
"name": "MeloMovie",
|
||||
"url": "https://melomovie.com",
|
||||
"status": 0
|
||||
},
|
||||
"MonoschinosProvider": {
|
||||
"name": "Monoschinos",
|
||||
"url": "https://monoschinos2.com",
|
||||
"status": 1
|
||||
},
|
||||
"MyCimaProvider": {
|
||||
"name": "MyCima",
|
||||
"url": "https://mycima.tv",
|
||||
"status": 1
|
||||
},
|
||||
"NineAnimeProvider": {
|
||||
"name": "9Anime",
|
||||
"url": "https://9anime.id",
|
||||
"status": 0
|
||||
},
|
||||
"PeliSmartProvider": {
|
||||
"name": "PeliSmart",
|
||||
"url": "https://pelismart.com",
|
||||
"status": 1
|
||||
},
|
||||
"PelisflixProvider": {
|
||||
"name": "Pelisflix",
|
||||
"url": "https://pelisflix.li",
|
||||
"status": 1
|
||||
},
|
||||
"PelisplusHDProvider": {
|
||||
"name": "PelisplusHD",
|
||||
"url": "https://pelisplushd.net",
|
||||
"status": 1
|
||||
},
|
||||
"PelisplusProvider": {
|
||||
"name": "Pelisplus",
|
||||
"url": "https://pelisplus.icu",
|
||||
"status": 1
|
||||
},
|
||||
"PinoyHDXyzProvider": {
|
||||
"name": "Pinoy-HD",
|
||||
"url": "https://www.pinoy-hd.xyz",
|
||||
"status": 1
|
||||
},
|
||||
"PinoyMoviePediaProvider": {
|
||||
"name": "Pinoy Moviepedia",
|
||||
"url": "https://pinoymoviepedia.ru",
|
||||
"status": 1
|
||||
},
|
||||
"PinoyMoviesEsProvider": {
|
||||
"name": "Pinoy Movies",
|
||||
"url": "https://pinoymovies.es",
|
||||
"status": 1
|
||||
},
|
||||
"SflixProvider": {
|
||||
"name": "Sflix.to",
|
||||
"url": "https://sflix.to",
|
||||
"status": 1
|
||||
},
|
||||
"DopeboxProvider": {
|
||||
"name": "Dopebox",
|
||||
"url": "https://dopebox.to",
|
||||
"status": 1
|
||||
},
|
||||
"SolarmovieProvider": {
|
||||
"name": "Solarmovie",
|
||||
"url": "https://solarmovie.pe",
|
||||
"status": 1
|
||||
},
|
||||
"SeriesflixProvider": {
|
||||
"name": "Seriesflix",
|
||||
"url": "https://seriesflix.video",
|
||||
"status": 1
|
||||
},
|
||||
"SoaptwoDayProvider": {
|
||||
"name": "Soap2Day",
|
||||
"url": "https://secretlink.xyz",
|
||||
"status": 0
|
||||
},
|
||||
"TenshiProvider": {
|
||||
"name": "Tenshi.moe",
|
||||
"url": "https://tenshi.moe",
|
||||
"status": 1
|
||||
},
|
||||
"TrailersTwoProvider": {
|
||||
"name": "Trailers.to",
|
||||
"url": "https://trailers.to",
|
||||
"status": 1
|
||||
},
|
||||
"TheFlixToProvider": {
|
||||
"name": "TheFlix.to",
|
||||
"url": "https://theflix.to",
|
||||
"status": 0
|
||||
},
|
||||
"TwoEmbedProvider": {
|
||||
"name": "2Embed",
|
||||
"url": "https://www.2embed.to",
|
||||
"status": 1
|
||||
},
|
||||
"VMoveeProvider": {
|
||||
"name": "VMovee",
|
||||
"url": "https://www.vmovee.watch",
|
||||
"status": 1
|
||||
},
|
||||
"VfFilmProvider": {
|
||||
"name": "vf-film.me",
|
||||
"url": "https://vf-film.me",
|
||||
"status": 1
|
||||
},
|
||||
"VfSerieProvider": {
|
||||
"name": "vf-serie.org",
|
||||
"url": "https://vf-serie.org",
|
||||
"status": 1
|
||||
},
|
||||
"VidEmbedProvider": {
|
||||
"name": "VidEmbed",
|
||||
"url": "https://vidembed.cc",
|
||||
"status": 1
|
||||
},
|
||||
"YomoviesProvider": {
|
||||
"name": "Yomovies",
|
||||
"url": "https://yomovies.vip",
|
||||
"status": 1
|
||||
},
|
||||
"WatchAsianProvider": {
|
||||
"name": "WatchAsian",
|
||||
"url": "https://watchasian.cx",
|
||||
"status": 1
|
||||
},
|
||||
"WatchCartoonOnlineProvider": {
|
||||
"name": "WatchCartoonOnline",
|
||||
"url": "https://www.wcostream.com",
|
||||
"status": 1
|
||||
},
|
||||
"WcoProvider": {
|
||||
"name": "WCO Stream",
|
||||
"url": "https://wcostream.cc",
|
||||
"status": 1
|
||||
},
|
||||
"ZoroProvider": {
|
||||
"name": "Zoro",
|
||||
"url": "https://zoro.to",
|
||||
"status": 0
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue