This commit is contained in:
reduplicated 2022-08-05 15:38:05 +02:00
commit 718199ee39
68 changed files with 3921 additions and 2673 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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 {

View file

@ -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
)

View file

@ -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")

View file

@ -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
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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]
}

View file

@ -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 })
}
}
}
}

View file

@ -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?) {
}
}

View file

@ -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

View file

@ -4,7 +4,6 @@ import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
@ -12,8 +11,8 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.SyncUtil
import kotlinx.coroutines.launch
import java.util.*
@ -44,9 +43,13 @@ class SyncViewModel : ViewModel() {
// prefix, id
private var syncs = mutableMapOf<String, String>()
private val _syncIds: MutableLiveData<MutableMap<String, String>> =
MutableLiveData(mutableMapOf())
val syncIds: LiveData<MutableMap<String, String>> get() = _syncIds
//private val _syncIds: MutableLiveData<MutableMap<String, String>> =
// MutableLiveData(mutableMapOf())
//val syncIds: LiveData<MutableMap<String, String>> get() = _syncIds
fun getSyncs() : Map<String,String> {
return syncs
}
private val _currentSynced: MutableLiveData<List<CurrentSynced>> =
MutableLiveData(getMissing())
@ -76,7 +79,7 @@ class SyncViewModel : ViewModel() {
Log.i(TAG, "addSync $idPrefix = $id")
syncs[idPrefix] = id
_syncIds.postValue(syncs)
//_syncIds.postValue(syncs)
return true
}
@ -99,10 +102,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,

View file

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

View file

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

View file

@ -44,6 +44,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)
)
}

View file

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

View file

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

View file

@ -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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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>

View file

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

View file

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

View file

@ -0,0 +1,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" />

View file

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

View file

@ -4,10 +4,35 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
<action
android:id="@+id/global_to_navigation_results"
app:destination="@id/navigation_results"
android:id="@+id/global_to_navigation_results_tv"
app:destination="@id/navigation_results_tv"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim">
<argument
android:name="url"
app:argType="string" />
<argument
android:name="apiName"
app:argType="string" />
<argument
android:name="startAction"
android:defaultValue="0"
app:argType="integer" />
<argument
android:name="startValue"
android:defaultValue="0"
app:argType="integer" />
<argument
android:name="restart"
android:defaultValue="false"
app:argType="boolean" />
</action>
<action
android:id="@+id/global_to_navigation_results_phone"
app:destination="@id/navigation_results_phone"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
@ -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"

View file

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

View file

@ -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>

View file

@ -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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>

View file

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

View file

@ -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>

View file

@ -266,6 +266,7 @@
</style>
<style name="AppBottomSheetDialogTheme">
<item name="android:navigationBarColor">?attr/boxItemBackground</item>
<item name="android:windowCloseOnTouchOutside">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowAnimationStyle">@style/Animation.Design.BottomSheetDialog</item>
@ -278,7 +279,7 @@
<item name="behavior_skipCollapsed">true</item>
<item name="shapeAppearance">@null</item>
<item name="shapeAppearanceOverlay">@null</item>
<item name="backgroundTint">?android:attr/colorBackground</item>
<item name="backgroundTint">@color/transparent</item>
<item name="android:background">@drawable/rounded_dialog</item>
<item name="behavior_peekHeight">512dp</item>
</style>
@ -334,6 +335,10 @@
<item name="tabMode">scrollable</item>-->
</style>
<style name="DarkFragment" parent="AppTheme">
<item name="android:navigationBarColor">?attr/colorPrimary</item>
</style>
<style name="AlertDialogCustom" parent="Theme.AppCompat.Dialog.Alert">
<item name="android:windowFullscreen">true</item>
<item name="android:textColor">?attr/textColor</item>
@ -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">

View file

@ -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

View file

@ -543,7 +543,7 @@
"TrailersTwoProvider": {
"language": "en",
"name": "Trailers.to",
"status": 1,
"status": 0,
"url": "https://trailers.to"
},
"TwoEmbedProvider": {

View file

@ -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
}
}