mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge pull request #2 from Luna712/master
Merge branch 'master' into feature/remote-sync
This commit is contained in:
commit
7b33af75d0
218 changed files with 5948 additions and 2700 deletions
7
.idea/gradle.xml
generated
7
.idea/gradle.xml
generated
|
@ -4,17 +4,16 @@
|
|||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="delegatedBuild" value="true" />
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="jbr-17" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/library" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
||||
import org.jetbrains.dokka.gradle.DokkaTask
|
||||
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.URL
|
||||
|
@ -15,6 +16,7 @@ plugins {
|
|||
|
||||
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
|
||||
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
|
||||
var isLibraryDebug = false
|
||||
|
||||
fun String.execute() = ByteArrayOutputStream().use { baot ->
|
||||
if (project.exec {
|
||||
|
@ -71,7 +73,7 @@ android {
|
|||
targetSdk = 33 /* Android 14 is Fu*ked
|
||||
^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
|
||||
versionCode = 63
|
||||
versionName = "4.3.1"
|
||||
versionName = "4.3.2"
|
||||
|
||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||
|
@ -81,9 +83,9 @@ android {
|
|||
val localProperties = gradleLocalProperties(rootDir)
|
||||
|
||||
buildConfigField(
|
||||
"String",
|
||||
"BUILDDATE",
|
||||
"new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
|
||||
"long",
|
||||
"BUILD_DATE",
|
||||
"${System.currentTimeMillis()}"
|
||||
)
|
||||
buildConfigField(
|
||||
"String",
|
||||
|
@ -114,6 +116,7 @@ android {
|
|||
)
|
||||
}
|
||||
debug {
|
||||
isLibraryDebug = true
|
||||
isDebuggable = true
|
||||
applicationIdSuffix = ".debug"
|
||||
proguardFiles(
|
||||
|
@ -175,24 +178,24 @@ repositories {
|
|||
dependencies {
|
||||
// Testing
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.json:json:20231013")
|
||||
testImplementation("org.json:json:20240303")
|
||||
androidTestImplementation("androidx.test:core")
|
||||
implementation("androidx.test.ext:junit-ktx:1.1.5")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
|
||||
// Android Core & Lifecycle
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
|
||||
|
||||
// Design & UI
|
||||
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation("com.google.android.material:material:1.10.0")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
|
||||
|
@ -203,7 +206,7 @@ dependencies {
|
|||
|
||||
// For KSP -> Official Annotation Processors are Not Yet Supported for KSP
|
||||
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
||||
implementation("com.google.guava:guava:32.1.3-android")
|
||||
implementation("com.google.guava:guava:33.2.0-android")
|
||||
implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
||||
|
||||
// Media 3 (ExoPlayer)
|
||||
|
@ -220,7 +223,7 @@ dependencies {
|
|||
// PlayBack
|
||||
implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker
|
||||
implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs
|
||||
implementation("com.github.teamnewpipe:NewPipeExtractor:6dc25f7") /* For Trailers
|
||||
implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") /* For Trailers
|
||||
^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */
|
||||
implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding
|
||||
|
||||
|
@ -237,9 +240,7 @@ dependencies {
|
|||
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
|
||||
|
||||
// Extensions & Other Libs
|
||||
implementation("org.mozilla:rhino:1.7.13") /* run JavaScript
|
||||
^ Don't Bump RhinoJS to 1.7.14,`NoClassDefFoundError` Occurs and Trailers won't play (even with Desugaring)
|
||||
NewPipeExtractor Issue */
|
||||
implementation("org.mozilla:rhino:1.7.15") // run JavaScript
|
||||
implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance
|
||||
implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9
|
||||
|
@ -273,19 +274,37 @@ dependencies {
|
|||
implementation("androidx.work:work-runtime:2.9.0")
|
||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
|
||||
|
||||
implementation(project(":library") {
|
||||
this.extra.set("isDebug", isLibraryDebug)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
tasks.register("androidSourcesJar", Jar::class) {
|
||||
tasks.register<Jar>("androidSourcesJar") {
|
||||
archiveClassifier.set("sources")
|
||||
from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources
|
||||
}
|
||||
|
||||
// For GradLew Plugin
|
||||
tasks.register("makeJar", Copy::class) {
|
||||
from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
|
||||
into("build")
|
||||
include("classes.jar")
|
||||
tasks.register<Copy>("copyJar") {
|
||||
from(
|
||||
"build/intermediates/compile_app_classes_jar/prereleaseDebug",
|
||||
"../library/build/libs"
|
||||
)
|
||||
into("build/app-classes")
|
||||
include("classes.jar", "library-jvm*.jar")
|
||||
// Remove the version
|
||||
rename("library-jvm.*.jar", "library-jvm.jar")
|
||||
}
|
||||
|
||||
// Merge the app classes and the library classes into classes.jar
|
||||
tasks.register<Jar>("makeJar") {
|
||||
dependsOn(tasks.getByName("copyJar"))
|
||||
from(
|
||||
zipTree("build/app-classes/classes.jar"),
|
||||
zipTree("build/app-classes/library-jvm.jar")
|
||||
)
|
||||
destinationDirectory.set(layout.buildDirectory)
|
||||
archivesName = "classes"
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
|
|
|
@ -11,7 +11,9 @@ import androidx.fragment.app.FragmentActivity
|
|||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
|
@ -31,7 +33,6 @@ import org.acra.sender.ReportSenderFactory
|
|||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.PrintStream
|
||||
import java.lang.Exception
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.system.exitProcess
|
||||
|
@ -211,7 +212,7 @@ class AcraApplication : Application() {
|
|||
fun openBrowser(url: String, activity: FragmentActivity?) {
|
||||
openBrowser(
|
||||
url,
|
||||
isTvSettings(),
|
||||
isLayout(TV or EMULATOR),
|
||||
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,11 +11,9 @@ import android.util.DisplayMetrics
|
|||
import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.NO_ID
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
@ -31,11 +29,13 @@ import com.google.android.material.chip.ChipGroup
|
|||
import com.google.android.material.navigationrail.NavigationRailView
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps
|
||||
import com.lagradost.cloudstream3.databinding.ToastBinding
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||
import com.lagradost.cloudstream3.ui.result.UiText
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
|
@ -99,8 +99,7 @@ object CommonActivity {
|
|||
var playerEventListener: ((PlayerEventType) -> Unit)? = null
|
||||
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
|
||||
|
||||
|
||||
var currentToast: Toast? = null
|
||||
private var currentToast: Toast? = null
|
||||
|
||||
fun showToast(@StringRes message: Int, duration: Int? = null) {
|
||||
val act = activity ?: return
|
||||
|
@ -156,25 +155,19 @@ object CommonActivity {
|
|||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
||||
try {
|
||||
val inflater =
|
||||
act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
|
||||
val layout: View = inflater.inflate(
|
||||
R.layout.toast,
|
||||
act.findViewById<View>(R.id.toast_layout_root) as ViewGroup?
|
||||
)
|
||||
|
||||
val text = layout.findViewById(R.id.text) as TextView
|
||||
text.text = message.trim()
|
||||
val binding = ToastBinding.inflate(act.layoutInflater)
|
||||
binding.text.text = message.trim()
|
||||
|
||||
// custom toasts are deprecated and won't appear when cs3 sets minSDK to api30 (A11)
|
||||
val toast = Toast(act)
|
||||
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
||||
toast.duration = duration ?: Toast.LENGTH_SHORT
|
||||
toast.view = layout
|
||||
//https://github.com/PureWriter/ToastCompat
|
||||
toast.show()
|
||||
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
||||
toast.view = binding.root
|
||||
currentToast = toast
|
||||
toast.show()
|
||||
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklAp
|
|||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
|
||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
|
||||
|
@ -30,19 +31,16 @@ import java.text.SimpleDateFormat
|
|||
import java.util.*
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
const val USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
|
||||
|
||||
//val baseHeader = mapOf("User-Agent" to USER_AGENT)
|
||||
val mapper = JsonMapper.builder().addModule(kotlinModule())
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
|
||||
|
||||
/**
|
||||
* Defines the constant for the all languages preference, if this is set then it is
|
||||
* the equivalent of all languages being set
|
||||
**/
|
||||
const val AllLanguagesName = "universal"
|
||||
|
||||
//val baseHeader = mapOf("User-Agent" to USER_AGENT)
|
||||
val mapper = JsonMapper.builder().addModule(kotlinModule())
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
|
||||
|
||||
object APIHolder {
|
||||
val unixTime: Long
|
||||
get() = System.currentTimeMillis() / 1000L
|
||||
|
@ -119,7 +117,9 @@ object APIHolder {
|
|||
}
|
||||
|
||||
fun LoadResponse.getId(): Int {
|
||||
return getLoadResponseIdFromUrl(url, apiName)
|
||||
// this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked
|
||||
return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null)
|
||||
?: getLoadResponseIdFromUrl(url, apiName)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -220,10 +220,15 @@ object APIHolder {
|
|||
} ?: false
|
||||
|
||||
val matchingTypes = types?.any { it.name.equals(media.format, true) } == true
|
||||
if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears
|
||||
if (lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears
|
||||
} ?: return null
|
||||
|
||||
Tracker(res.idMal, res.id.toString(), res.coverImage?.extraLarge ?: res.coverImage?.large, res.bannerImage)
|
||||
Tracker(
|
||||
res.idMal,
|
||||
res.id.toString(),
|
||||
res.coverImage?.extraLarge ?: res.coverImage?.large,
|
||||
res.bannerImage
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
null
|
||||
|
@ -741,8 +746,6 @@ fun base64Encode(array: ByteArray): String {
|
|||
}
|
||||
}
|
||||
|
||||
class ErrorLoadingException(message: String? = null) : Exception(message)
|
||||
|
||||
fun MainAPI.fixUrlNull(url: String?): String? {
|
||||
if (url.isNullOrEmpty()) {
|
||||
return null
|
||||
|
@ -863,7 +866,12 @@ enum class TvType(value: Int?) {
|
|||
AsianDrama(9),
|
||||
Live(10),
|
||||
NSFW(11),
|
||||
Others(12)
|
||||
Others(12),
|
||||
Music(13),
|
||||
AudioBook(14),
|
||||
|
||||
/** Wont load the built in player, make your own interaction */
|
||||
CustomMedia(15),
|
||||
}
|
||||
|
||||
public enum class AutoDownloadMode(val value: Int) {
|
||||
|
@ -1249,13 +1257,15 @@ interface LoadResponse {
|
|||
|
||||
fun LoadResponse.getImdbId(): String? {
|
||||
return normalSafeApiCall {
|
||||
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb)
|
||||
SimklApi.readIdFromString(this.syncData[simklIdPrefix])
|
||||
?.get(SimklApi.Companion.SyncServices.Imdb)
|
||||
}
|
||||
}
|
||||
|
||||
fun LoadResponse.getTMDbId(): String? {
|
||||
return normalSafeApiCall {
|
||||
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb)
|
||||
SimklApi.readIdFromString(this.syncData[simklIdPrefix])
|
||||
?.get(SimklApi.Companion.SyncServices.Tmdb)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1444,11 +1454,24 @@ fun TvType?.isEpisodeBased(): Boolean {
|
|||
return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama)
|
||||
}
|
||||
|
||||
|
||||
data class NextAiring(
|
||||
val episode: Int,
|
||||
val unixTime: Long,
|
||||
)
|
||||
val season: Int? = null,
|
||||
) {
|
||||
/**
|
||||
* Secondary constructor for backwards compatibility without season.
|
||||
* TODO Remove this constructor after there is a new stable release and extensions are updated to support season.
|
||||
*/
|
||||
constructor(
|
||||
episode: Int,
|
||||
unixTime: Long,
|
||||
) : this (
|
||||
episode,
|
||||
unixTime,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param season To be mapped with episode season, not shown in UI if displaySeason is defined
|
||||
|
@ -1539,8 +1562,26 @@ data class TorrentLoadResponse(
|
|||
posterHeaders: Map<String, String>? = null,
|
||||
backgroundPosterUrl: String? = null,
|
||||
) : this(
|
||||
name, url, apiName, magnet, torrent, plot, type, posterUrl, year, rating, tags, duration, trailers,
|
||||
recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
|
||||
name,
|
||||
url,
|
||||
apiName,
|
||||
magnet,
|
||||
torrent,
|
||||
plot,
|
||||
type,
|
||||
posterUrl,
|
||||
year,
|
||||
rating,
|
||||
tags,
|
||||
duration,
|
||||
trailers,
|
||||
recommendations,
|
||||
actors,
|
||||
comingSoon,
|
||||
syncData,
|
||||
posterHeaders,
|
||||
backgroundPosterUrl,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1592,7 +1633,8 @@ data class AnimeLoadResponse(
|
|||
return this.episodes.maxOf { (_, episodes) ->
|
||||
episodes.count { episodeData ->
|
||||
// Prioritize display season as actual season may be something random to fit multiple seasons into one.
|
||||
val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
|
||||
val episodeSeason =
|
||||
displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
|
||||
// Count all episodes from season 1 to below the current season.
|
||||
episodeSeason in 1..<season
|
||||
}
|
||||
|
@ -1629,9 +1671,31 @@ data class AnimeLoadResponse(
|
|||
seasonNames: List<SeasonData>? = null,
|
||||
backgroundPosterUrl: String? = null,
|
||||
) : this(
|
||||
engName, japName, name, url, apiName, type, posterUrl, year, episodes, showStatus, plot, tags,
|
||||
synonyms, rating, duration, trailers, recommendations, actors, comingSoon, syncData, posterHeaders,
|
||||
nextAiring, seasonNames, backgroundPosterUrl, null
|
||||
engName,
|
||||
japName,
|
||||
name,
|
||||
url,
|
||||
apiName,
|
||||
type,
|
||||
posterUrl,
|
||||
year,
|
||||
episodes,
|
||||
showStatus,
|
||||
plot,
|
||||
tags,
|
||||
synonyms,
|
||||
rating,
|
||||
duration,
|
||||
trailers,
|
||||
recommendations,
|
||||
actors,
|
||||
comingSoon,
|
||||
syncData,
|
||||
posterHeaders,
|
||||
nextAiring,
|
||||
seasonNames,
|
||||
backgroundPosterUrl,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1763,7 +1827,7 @@ data class MovieLoadResponse(
|
|||
backgroundPosterUrl: String? = null,
|
||||
) : this(
|
||||
name, url, apiName, type, dataUrl, posterUrl, year, plot, rating, tags, duration, trailers,
|
||||
recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl,null
|
||||
recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1906,7 +1970,8 @@ data class TvSeriesLoadResponse(
|
|||
|
||||
return episodes.count { episodeData ->
|
||||
// Prioritize display season as actual season may be something random to fit multiple seasons into one.
|
||||
val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
|
||||
val episodeSeason =
|
||||
displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
|
||||
// Count all episodes from season 1 to below the current season.
|
||||
episodeSeason in 1..<season
|
||||
} + episode
|
||||
|
@ -1939,9 +2004,28 @@ data class TvSeriesLoadResponse(
|
|||
seasonNames: List<SeasonData>? = null,
|
||||
backgroundPosterUrl: String? = null,
|
||||
) : this(
|
||||
name, url, apiName, type, episodes, posterUrl, year, plot, showStatus, rating, tags, duration,
|
||||
trailers, recommendations, actors, comingSoon, syncData, posterHeaders, nextAiring, seasonNames,
|
||||
backgroundPosterUrl, null
|
||||
name,
|
||||
url,
|
||||
apiName,
|
||||
type,
|
||||
episodes,
|
||||
posterUrl,
|
||||
year,
|
||||
plot,
|
||||
showStatus,
|
||||
rating,
|
||||
tags,
|
||||
duration,
|
||||
trailers,
|
||||
recommendations,
|
||||
actors,
|
||||
comingSoon,
|
||||
syncData,
|
||||
posterHeaders,
|
||||
nextAiring,
|
||||
seasonNames,
|
||||
backgroundPosterUrl,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2005,6 +2089,7 @@ data class AniSearch(
|
|||
@JsonProperty("extraLarge") var extraLarge: String? = null,
|
||||
@JsonProperty("large") var large: String? = null,
|
||||
)
|
||||
|
||||
data class Title(
|
||||
@JsonProperty("romaji") var romaji: String? = null,
|
||||
@JsonProperty("english") var english: String? = null,
|
||||
|
|
|
@ -86,6 +86,7 @@ import com.lagradost.cloudstream3.plugins.PluginManager
|
|||
import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
|
||||
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
|
||||
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.BackupApis
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
|
||||
|
@ -113,11 +114,11 @@ import com.lagradost.cloudstream3.ui.result.setText
|
|||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.search.SearchFragment
|
||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTruePhone
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
|
||||
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
|
||||
import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
|
||||
|
@ -135,7 +136,10 @@ import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
|||
import com.lagradost.cloudstream3.utils.BackupUtils.backup
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
|
@ -158,6 +162,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||
import com.lagradost.cloudstream3.utils.fcast.FcastManager
|
||||
import com.lagradost.nicehttp.Requests
|
||||
import com.lagradost.nicehttp.ResponseParser
|
||||
import com.lagradost.safefile.SafeFile
|
||||
|
@ -170,7 +175,6 @@ import java.net.URLDecoder
|
|||
import java.nio.charset.Charset
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
|
||||
|
@ -183,116 +187,93 @@ import kotlin.system.exitProcess
|
|||
|
||||
//https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225
|
||||
|
||||
const val VLC_PACKAGE = "org.videolan.vlc"
|
||||
const val MPV_PACKAGE = "is.xyz.mpv"
|
||||
const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo"
|
||||
|
||||
val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity")
|
||||
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
|
||||
|
||||
//TODO REFACTOR AF
|
||||
open class ResultResume(
|
||||
val packageString: String,
|
||||
val action: String = Intent.ACTION_VIEW,
|
||||
val position: String? = null,
|
||||
val duration: String? = null,
|
||||
var launcher: ActivityResultLauncher<Intent>? = null,
|
||||
) {
|
||||
val defaultTime = -1L
|
||||
|
||||
val lastId get() = "${packageString}_last_open_id"
|
||||
suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
|
||||
val intent = Intent(action)
|
||||
|
||||
if (id != null)
|
||||
setKey(lastId, id)
|
||||
else
|
||||
removeKey(lastId)
|
||||
|
||||
intent.setPackage(packageString)
|
||||
callback.invoke(intent)
|
||||
launcher?.launch(intent)
|
||||
}
|
||||
|
||||
open fun getPosition(intent: Intent?): Long {
|
||||
return defaultTime
|
||||
}
|
||||
|
||||
open fun getDuration(intent: Intent?): Long {
|
||||
return defaultTime
|
||||
}
|
||||
}
|
||||
|
||||
val VLC = object : ResultResume(
|
||||
VLC_PACKAGE,
|
||||
// Android 13 intent restrictions fucks up specifically launching the VLC player
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
"org.videolan.vlc.player.result"
|
||||
} else {
|
||||
Intent.ACTION_VIEW
|
||||
},
|
||||
"extra_position",
|
||||
"extra_duration",
|
||||
) {
|
||||
override fun getPosition(intent: Intent?): Long {
|
||||
return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
|
||||
}
|
||||
|
||||
override fun getDuration(intent: Intent?): Long {
|
||||
return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
|
||||
}
|
||||
}
|
||||
|
||||
val MPV = object : ResultResume(
|
||||
MPV_PACKAGE,
|
||||
//"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
|
||||
position = "position",
|
||||
duration = "duration",
|
||||
) {
|
||||
override fun getPosition(intent: Intent?): Long {
|
||||
return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime
|
||||
}
|
||||
|
||||
override fun getDuration(intent: Intent?): Long {
|
||||
return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime
|
||||
}
|
||||
}
|
||||
|
||||
val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
|
||||
|
||||
val resumeApps = arrayOf(
|
||||
VLC, MPV, WEB_VIDEO
|
||||
)
|
||||
|
||||
// Short name for requests client to make it nicer to use
|
||||
|
||||
var app = Requests(responseParser = object : ResponseParser {
|
||||
val mapper: ObjectMapper = jacksonObjectMapper().configure(
|
||||
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
|
||||
false
|
||||
)
|
||||
|
||||
override fun <T : Any> parse(text: String, kClass: KClass<T>): T {
|
||||
return mapper.readValue(text, kClass.java)
|
||||
}
|
||||
|
||||
override fun <T : Any> parseSafe(text: String, kClass: KClass<T>): T? {
|
||||
return try {
|
||||
mapper.readValue(text, kClass.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeValueAsString(obj: Any): String {
|
||||
return mapper.writeValueAsString(obj)
|
||||
}
|
||||
}).apply {
|
||||
defaultHeaders = mapOf("user-agent" to USER_AGENT)
|
||||
}
|
||||
|
||||
class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAuthenticator.BiometricAuthCallback {
|
||||
class MainActivity : AppCompatActivity(), ColorPickerDialogListener,
|
||||
BiometricAuthenticator.BiometricAuthCallback {
|
||||
companion object {
|
||||
const val VLC_PACKAGE = "org.videolan.vlc"
|
||||
const val MPV_PACKAGE = "is.xyz.mpv"
|
||||
const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo"
|
||||
|
||||
val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity")
|
||||
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
|
||||
|
||||
//TODO REFACTOR AF
|
||||
open class ResultResume(
|
||||
val packageString: String,
|
||||
val action: String = Intent.ACTION_VIEW,
|
||||
val position: String? = null,
|
||||
val duration: String? = null,
|
||||
var launcher: ActivityResultLauncher<Intent>? = null,
|
||||
) {
|
||||
val defaultTime = -1L
|
||||
|
||||
val lastId get() = "${packageString}_last_open_id"
|
||||
suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
|
||||
val intent = Intent(action)
|
||||
|
||||
if (id != null)
|
||||
setKey(lastId, id)
|
||||
else
|
||||
removeKey(lastId)
|
||||
|
||||
intent.setPackage(packageString)
|
||||
callback.invoke(intent)
|
||||
launcher?.launch(intent)
|
||||
}
|
||||
|
||||
open fun getPosition(intent: Intent?): Long {
|
||||
return defaultTime
|
||||
}
|
||||
|
||||
open fun getDuration(intent: Intent?): Long {
|
||||
return defaultTime
|
||||
}
|
||||
}
|
||||
|
||||
val VLC = object : ResultResume(
|
||||
VLC_PACKAGE,
|
||||
// Android 13 intent restrictions fucks up specifically launching the VLC player
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
"org.videolan.vlc.player.result"
|
||||
} else {
|
||||
Intent.ACTION_VIEW
|
||||
},
|
||||
"extra_position",
|
||||
"extra_duration",
|
||||
) {
|
||||
override fun getPosition(intent: Intent?): Long {
|
||||
return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
|
||||
}
|
||||
|
||||
override fun getDuration(intent: Intent?): Long {
|
||||
return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
|
||||
}
|
||||
}
|
||||
|
||||
val MPV = object : ResultResume(
|
||||
MPV_PACKAGE,
|
||||
//"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
|
||||
position = "position",
|
||||
duration = "duration",
|
||||
) {
|
||||
override fun getPosition(intent: Intent?): Long {
|
||||
return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong()
|
||||
?: defaultTime
|
||||
}
|
||||
|
||||
override fun getDuration(intent: Intent?): Long {
|
||||
return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong()
|
||||
?: defaultTime
|
||||
}
|
||||
}
|
||||
|
||||
val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
|
||||
|
||||
val resumeApps = arrayOf(
|
||||
VLC, MPV, WEB_VIDEO
|
||||
)
|
||||
|
||||
|
||||
const val TAG = "MAINACT"
|
||||
const val ANIMATED_OUTLINE: Boolean = false
|
||||
var lastError: String? = null
|
||||
|
@ -338,10 +319,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
|
||||
// kinda shitty solution, but cant com main->home otherwise for popups
|
||||
val bookmarksUpdatedEvent = Event<Boolean>()
|
||||
|
||||
/**
|
||||
* Used by DataStoreHelper to fully reload home when switching accounts
|
||||
*/
|
||||
val reloadHomeEvent = Event<Boolean>()
|
||||
|
||||
/**
|
||||
* Used by DataStoreHelper to fully reload library when switching accounts
|
||||
*/
|
||||
|
@ -469,7 +452,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
}
|
||||
|
||||
var lastPopup: SearchResponse? = null
|
||||
fun loadPopup(result: SearchResponse, load : Boolean = true) {
|
||||
fun loadPopup(result: SearchResponse, load: Boolean = true) {
|
||||
lastPopup = result
|
||||
val syncName = syncViewModel.syncName(result.apiName)
|
||||
|
||||
|
@ -490,8 +473,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
.contains(DubStatus.Dubbed)
|
||||
) DubStatus.Dubbed else DubStatus.Subbed, null
|
||||
)
|
||||
}else {
|
||||
viewModel.loadSmall(this,result)
|
||||
} else {
|
||||
viewModel.loadSmall(this, result)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -556,7 +539,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
binding?.navHostFragment?.apply {
|
||||
val params = layoutParams as ConstraintLayout.LayoutParams
|
||||
val push =
|
||||
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
|
||||
if (!dontPush && isLayout(TV or EMULATOR)) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
|
||||
|
||||
if (!this.isLtr()) {
|
||||
params.setMargins(
|
||||
|
@ -583,7 +566,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
}
|
||||
|
||||
Configuration.ORIENTATION_PORTRAIT -> {
|
||||
isTvSettings()
|
||||
isLayout(TV or EMULATOR)
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
@ -671,7 +654,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
}
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
val response = CommonActivity.dispatchKeyEvent(this, event)
|
||||
if (response != null)
|
||||
return response
|
||||
|
@ -791,9 +774,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
}
|
||||
|
||||
lateinit var viewModel: ResultViewModel2
|
||||
lateinit var syncViewModel : SyncViewModel
|
||||
lateinit var syncViewModel: SyncViewModel
|
||||
|
||||
/** kinda dirty, however it signals that we should use the watch status as sync or not*/
|
||||
var isLocalList : Boolean = false
|
||||
var isLocalList: Boolean = false
|
||||
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
|
||||
viewModel =
|
||||
ViewModelProvider(this)[ResultViewModel2::class.java]
|
||||
|
@ -1109,8 +1093,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
}
|
||||
}
|
||||
|
||||
private fun centerView(view : View?) {
|
||||
if(view == null) return
|
||||
private fun centerView(view: View?) {
|
||||
if (view == null) return
|
||||
try {
|
||||
Log.v(TAG, "centerView: $view")
|
||||
val r = Rect(0, 0, 0, 0)
|
||||
|
@ -1176,11 +1160,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
|
||||
// just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH
|
||||
binding = try {
|
||||
if (isTvSettings()) {
|
||||
if (isLayout(TV or EMULATOR)) {
|
||||
val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
|
||||
setContentView(newLocalBinding.root)
|
||||
|
||||
if (isTrueTvSettings() && ANIMATED_OUTLINE) {
|
||||
if (isLayout(TV) && ANIMATED_OUTLINE) {
|
||||
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
|
||||
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
|
||||
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
|
||||
|
@ -1192,7 +1176,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
newLocalBinding.focusOutline.isVisible = false
|
||||
}
|
||||
|
||||
if(isTrueTvSettings()) {
|
||||
if (isLayout(TV)) {
|
||||
// Put here any button you don't want focusing it to center the view
|
||||
val exceptionButtons = listOf(
|
||||
R.id.home_preview_play_btt,
|
||||
|
@ -1209,7 +1193,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
R.id.result_search_Button,
|
||||
R.id.result_episodes_show_button,
|
||||
)
|
||||
|
||||
|
||||
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
|
||||
if (exceptionButtons.contains(newFocus?.id)) return@addOnGlobalFocusChangeListener
|
||||
centerView(newFocus)
|
||||
|
@ -1227,18 +1211,20 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
null
|
||||
}
|
||||
|
||||
changeStatusBarState(isEmulatorSettings())
|
||||
changeStatusBarState(isLayout(EMULATOR))
|
||||
|
||||
/** Biometric stuff for users without accounts **/
|
||||
val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false)
|
||||
val noAccounts = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false) || accounts.count() <= 1
|
||||
val noAccounts = settingsManager.getBoolean(
|
||||
getString(R.string.skip_startup_account_select_key),
|
||||
false
|
||||
) || accounts.count() <= 1
|
||||
|
||||
if (isTruePhone() && authEnabled && noAccounts) {
|
||||
if (isLayout(PHONE) && isAuthEnabled(this) && noAccounts) {
|
||||
if (deviceHasPasswordPinLock(this)) {
|
||||
startBiometricAuthentication(this, R.string.biometric_authentication_title, false)
|
||||
|
||||
BiometricAuthenticator.promptInfo?.let {
|
||||
BiometricAuthenticator.biometricPrompt?.authenticate(it)
|
||||
promptInfo?.let { prompt ->
|
||||
biometricPrompt?.authenticate(prompt)
|
||||
}
|
||||
|
||||
// hide background while authenticating, Sorry moms & dads 🙏
|
||||
|
@ -1330,7 +1316,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
}
|
||||
|
||||
|
||||
fun setUserData(status : Resource<SyncAPI.AbstractSyncStatus>?) {
|
||||
fun setUserData(status: Resource<SyncAPI.AbstractSyncStatus>?) {
|
||||
if (isLocalList) return
|
||||
bottomPreviewBinding?.apply {
|
||||
when (status) {
|
||||
|
@ -1355,7 +1341,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
}
|
||||
}
|
||||
|
||||
fun setWatchStatus(state : WatchType?) {
|
||||
fun setWatchStatus(state: WatchType?) {
|
||||
if (!isLocalList || state == null) return
|
||||
|
||||
bottomPreviewBinding?.resultviewPreviewBookmark?.apply {
|
||||
|
@ -1364,13 +1350,42 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
}
|
||||
}
|
||||
|
||||
observe(viewModel.watchStatus) { state ->
|
||||
setWatchStatus(state)
|
||||
}
|
||||
observe(syncViewModel.userData) { status ->
|
||||
setUserData(status)
|
||||
fun setSubscribeStatus(state: Boolean?) {
|
||||
bottomPreviewBinding?.resultviewPreviewSubscribe?.apply {
|
||||
if (state != null) {
|
||||
val drawable = if (state) {
|
||||
R.drawable.ic_baseline_notifications_active_24
|
||||
} else {
|
||||
R.drawable.baseline_notifications_none_24
|
||||
}
|
||||
setImageResource(drawable)
|
||||
}
|
||||
isVisible = state != null
|
||||
|
||||
setOnClickListener {
|
||||
viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
|
||||
if (newStatus == null) return@toggleSubscriptionStatus
|
||||
|
||||
val message = if (newStatus) {
|
||||
// Kinda icky to have this here, but it works.
|
||||
SubscriptionWorkManager.enqueuePeriodicWork(context)
|
||||
R.string.subscription_new
|
||||
} else {
|
||||
R.string.subscription_deleted
|
||||
}
|
||||
|
||||
val name = (viewModel.page.value as? Resource.Success)?.value?.title
|
||||
?: txt(R.string.no_data).asStringNull(context) ?: ""
|
||||
showToast(txt(message, name), Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observe(viewModel.watchStatus, ::setWatchStatus)
|
||||
observe(syncViewModel.userData, ::setUserData)
|
||||
observeNullable(viewModel.subscribeStatus, ::setSubscribeStatus)
|
||||
|
||||
observeNullable(viewModel.page) { resource ->
|
||||
if (resource == null) {
|
||||
hidePreviewPopupDialog()
|
||||
|
@ -1412,6 +1427,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
|
||||
setUserData(syncViewModel.userData.value)
|
||||
setWatchStatus(viewModel.watchStatus.value)
|
||||
setSubscribeStatus(viewModel.subscribeStatus.value)
|
||||
|
||||
resultviewPreviewBookmark.setOnClickListener {
|
||||
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
|
||||
|
@ -1430,7 +1446,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
)
|
||||
}
|
||||
} else {
|
||||
val value = (syncViewModel.userData.value as? Resource.Success)?.value?.status ?: SyncWatchType.NONE
|
||||
val value =
|
||||
(syncViewModel.userData.value as? Resource.Success)?.value?.status
|
||||
?: SyncWatchType.NONE
|
||||
|
||||
this@MainActivity.showBottomDialog(
|
||||
SyncWatchType.values().map { getString(it.stringRes) }.toList(),
|
||||
|
@ -1457,7 +1475,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
resultviewPreviewFavorite.setImageResource(drawable)
|
||||
}
|
||||
|
||||
resultviewPreviewFavorite.setOnClickListener{
|
||||
resultviewPreviewFavorite.setOnClickListener {
|
||||
viewModel.toggleFavoriteStatus(this@MainActivity) { newStatus: Boolean? ->
|
||||
if (newStatus == null) return@toggleFavoriteStatus
|
||||
|
||||
|
@ -1473,7 +1491,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
}
|
||||
}
|
||||
|
||||
if (!isTvSettings()) // dont want this clickable on tv layout
|
||||
if (isLayout(PHONE)) // dont want this clickable on tv layout
|
||||
resultviewPreviewDescription.setOnClickListener { view ->
|
||||
view.context?.let { ctx ->
|
||||
val builder: AlertDialog.Builder =
|
||||
|
@ -1548,7 +1566,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
}
|
||||
}
|
||||
|
||||
if (isTvSettings()) {
|
||||
if (isLayout(TV or EMULATOR)) {
|
||||
if (navDestination.matchDestination(R.id.navigation_home)) {
|
||||
attachBackPressedCallback()
|
||||
} else detachBackPressedCallback()
|
||||
|
@ -1584,7 +1602,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
itemRippleColor = rippleColor
|
||||
itemActiveIndicatorColor = rippleColor
|
||||
setupWithNavController(navController)
|
||||
if (isTvSettings()) {
|
||||
if (isLayout(TV or EMULATOR)) {
|
||||
background?.alpha = 200
|
||||
} else {
|
||||
background?.alpha = 255
|
||||
|
@ -1718,6 +1736,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
runAutoUpdate()
|
||||
}
|
||||
|
||||
FcastManager().init(this, false)
|
||||
|
||||
APIRepository.dubStatusActive = getApiDubstatusSettings()
|
||||
|
||||
try {
|
||||
|
@ -1754,8 +1774,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
} finally {
|
||||
setKey(HAS_DONE_SETUP_KEY, true)
|
||||
}
|
||||
|
||||
// Used to check current focus for TV
|
||||
|
@ -1791,6 +1809,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
|||
binding?.navHostFragment?.isInvisible = false
|
||||
}
|
||||
|
||||
override fun onAuthenticationError() {
|
||||
finish()
|
||||
}
|
||||
|
||||
private var backPressedCallback: OnBackPressedCallback? = null
|
||||
|
||||
private fun attachBackPressedCallback() {
|
||||
|
|
|
@ -2,9 +2,7 @@ package com.lagradost.cloudstream3.extractors
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.extractors.helper.*
|
||||
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
|
@ -28,30 +26,39 @@ open class Chillx : ExtractorApi() {
|
|||
override val name = "Chillx"
|
||||
override val mainUrl = "https://chillx.top"
|
||||
override val requiresReferer = true
|
||||
private var key: String? = null
|
||||
|
||||
companion object {
|
||||
private var key: String? = null
|
||||
|
||||
suspend fun fetchKey(): String {
|
||||
return if (key != null) {
|
||||
key!!
|
||||
} else {
|
||||
val fetch = app.get("https://raw.githubusercontent.com/rushi-chavan/multi-keys/keys/keys.json").parsedSafe<Keys>()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key")
|
||||
key = fetch
|
||||
key!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NAME_SHADOWING")
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val master = Regex("\\s*=\\s*'([^']+)").find(
|
||||
val master = Regex("""JScript[\w+]?\s*=\s*'([^']+)""").find(
|
||||
app.get(
|
||||
url,
|
||||
referer = referer ?: "",
|
||||
headers = mapOf(
|
||||
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||
"Accept-Language" to "en-US,en;q=0.5",
|
||||
)
|
||||
referer = url,
|
||||
).text
|
||||
)?.groupValues?.get(1)
|
||||
val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||
|
||||
val key = fetchKey()
|
||||
val decrypt = cryptoAESHandler(master ?: "", key.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||
val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
||||
|
||||
val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
||||
val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex()
|
||||
val subtitlePattern = """\[(.*?)](https?://[^\s,]+)""".toRegex()
|
||||
val matches = subtitlePattern.findAll(subtitles ?: "")
|
||||
val languageUrlPairs = matches.map { matchResult ->
|
||||
val (language, url) = matchResult.destructured
|
||||
|
@ -83,23 +90,18 @@ open class Chillx : ExtractorApi() {
|
|||
headers = headers
|
||||
).forEach(callback)
|
||||
}
|
||||
|
||||
|
||||
private fun decodeUnicodeEscape(input: String): String {
|
||||
val regex = Regex("u([0-9a-fA-F]{4})")
|
||||
return regex.replace(input) {
|
||||
it.groupValues[1].toInt(16).toChar().toString()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getKey() = key ?: fetchKey().also { key = it }
|
||||
|
||||
private suspend fun fetchKey(): String {
|
||||
return app.get("https://raw.githubusercontent.com/Sofie99/Resources/main/chillix_key.json").parsed()
|
||||
}
|
||||
|
||||
data class Tracks(
|
||||
@JsonProperty("file") val file: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
@JsonProperty("kind") val kind: String? = null,
|
||||
|
||||
data class Keys(
|
||||
@JsonProperty("chillx") val key: List<String>
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
@ -9,10 +9,16 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
|||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||
import java.net.URL
|
||||
|
||||
class Geodailymotion : Dailymotion() {
|
||||
override val name = "GeoDailymotion"
|
||||
override val mainUrl = "https://geo.dailymotion.com"
|
||||
}
|
||||
|
||||
open class Dailymotion : ExtractorApi() {
|
||||
override val mainUrl = "https://www.dailymotion.com"
|
||||
override val name = "Dailymotion"
|
||||
override val requiresReferer = false
|
||||
private val baseUrl = "https://www.dailymotion.com"
|
||||
|
||||
@Suppress("RegExpSimplifiable")
|
||||
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
||||
|
@ -34,7 +40,7 @@ open class Dailymotion : ExtractorApi() {
|
|||
val dmV1st = config.dmInternalData.v1st
|
||||
val dmTs = config.dmInternalData.ts
|
||||
val embedder = config.context.embedder
|
||||
val metaDataUrl = "$mainUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||
val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies)
|
||||
.parsedSafe<MetaData>() ?: return
|
||||
metaData.qualities.forEach { (_, video) ->
|
||||
|
@ -45,16 +51,19 @@ open class Dailymotion : ExtractorApi() {
|
|||
}
|
||||
|
||||
private fun getEmbedUrl(url: String): String? {
|
||||
if (url.contains("/embed/")) {
|
||||
return url
|
||||
}
|
||||
val vid = getVideoId(url) ?: return null
|
||||
return "$mainUrl/embed/video/$vid"
|
||||
if (url.contains("/embed/") || url.contains("/video/")) {
|
||||
return url
|
||||
}
|
||||
if (url.contains("geo.dailymotion.com")) {
|
||||
val videoId = url.substringAfter("video=")
|
||||
return "$baseUrl/embed/video/$videoId"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getVideoId(url: String): String? {
|
||||
val path = URL(url).path
|
||||
val id = path.substringAfter("video/")
|
||||
val id = path.substringAfter("/video/")
|
||||
if (id.matches(videoIdRegex)) {
|
||||
return id
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
|
||||
open class EPlayExtractor : ExtractorApi() {
|
||||
override var name = "EPlay"
|
||||
override var mainUrl = "https://eplayvid.net"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val response = app.get(url).document
|
||||
val trueUrl = response.select("source").attr("src")
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
trueUrl,
|
||||
mainUrl,
|
||||
getQualityFromName(""), // this needs to be auto
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -20,4 +20,9 @@ class PlayRu : ContentX() {
|
|||
class FourPlayRu : ContentX() {
|
||||
override var name = "FourPlayRu"
|
||||
override var mainUrl = "https://four.playru.net"
|
||||
}
|
||||
}
|
||||
|
||||
class FourPichive : ContentX() {
|
||||
override var name = "FourPichive"
|
||||
override var mainUrl = "https://four.pichive.online"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import android.util.Base64
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.amap
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import java.net.URLDecoder
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class VidSrcTo : ExtractorApi() {
|
||||
override val name = "VidSrcTo"
|
||||
override val mainUrl = "https://vidsrc.to"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return
|
||||
val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe<VidsrctoEpisodeSources>() ?: return
|
||||
if (res.status != 200) return
|
||||
res.result?.amap { source ->
|
||||
try {
|
||||
val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe<VidsrctoEmbedSource>() ?: return@amap
|
||||
val finalUrl = DecryptUrl(embedRes.result.encUrl)
|
||||
if(finalUrl.equals(embedRes.result.encUrl)) return@amap
|
||||
when (source.title) {
|
||||
"Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback)
|
||||
"Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DecryptUrl(encUrl: String): String {
|
||||
var data = encUrl.toByteArray()
|
||||
data = Base64.decode(data, Base64.URL_SAFE)
|
||||
val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4")
|
||||
val cipher = Cipher.getInstance("RC4")
|
||||
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
|
||||
data = cipher.doFinal(data)
|
||||
return URLDecoder.decode(data.toString(Charsets.UTF_8), "utf-8")
|
||||
}
|
||||
|
||||
data class VidsrctoEpisodeSources(
|
||||
@JsonProperty("status") val status: Int,
|
||||
@JsonProperty("result") val result: List<VidsrctoResult>?
|
||||
)
|
||||
|
||||
data class VidsrctoResult(
|
||||
@JsonProperty("id") val id: String,
|
||||
@JsonProperty("title") val title: String
|
||||
)
|
||||
|
||||
data class VidsrctoEmbedSource(
|
||||
@JsonProperty("status") val status: Int,
|
||||
@JsonProperty("result") val result: VidsrctoUrl
|
||||
)
|
||||
|
||||
data class VidsrctoUrl(@JsonProperty("url") val encUrl: String)
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import android.util.Log
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import org.mozilla.javascript.Context
|
||||
import org.mozilla.javascript.NativeJSON
|
||||
import org.mozilla.javascript.NativeObject
|
||||
import org.mozilla.javascript.Scriptable
|
||||
import java.util.Base64
|
||||
|
||||
open class Vidguardto : ExtractorApi() {
|
||||
override val name = "Vidguard"
|
||||
override val mainUrl = "https://vidguard.to"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val res = app.get(url)
|
||||
val resc = res.document.select("script:containsData(eval)").firstOrNull()?.data()
|
||||
resc?.let {
|
||||
val jsonStr2 = AppUtils.parseJson<SvgObject>(runJS2(it))
|
||||
val watchlink = sigDecode(jsonStr2.stream)
|
||||
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
name,
|
||||
watchlink,
|
||||
this.mainUrl,
|
||||
Qualities.Unknown.value,
|
||||
INFER_TYPE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sigDecode(url: String): String {
|
||||
val sig = url.split("sig=")[1].split("&")[0]
|
||||
var t = ""
|
||||
for (v in sig.chunked(2)) {
|
||||
val byteValue = Integer.parseInt(v, 16) xor 2
|
||||
t += byteValue.toChar()
|
||||
}
|
||||
val padding = when (t.length % 4) {
|
||||
2 -> "=="
|
||||
3 -> "="
|
||||
else -> ""
|
||||
}
|
||||
val decoded = Base64.getDecoder().decode((t + padding).toByteArray(Charsets.UTF_8))
|
||||
t = String(decoded).dropLast(5).reversed()
|
||||
val charArray = t.toCharArray()
|
||||
for (i in 0 until charArray.size - 1 step 2) {
|
||||
val temp = charArray[i]
|
||||
charArray[i] = charArray[i + 1]
|
||||
charArray[i + 1] = temp
|
||||
}
|
||||
val modifiedSig = String(charArray).dropLast(5)
|
||||
return url.replace(sig, modifiedSig)
|
||||
}
|
||||
|
||||
private fun runJS2(hideMyHtmlContent: String): String {
|
||||
Log.d("runJS", "start")
|
||||
val rhino = Context.enter()
|
||||
rhino.initSafeStandardObjects()
|
||||
rhino.optimizationLevel = -1
|
||||
val scope: Scriptable = rhino.initSafeStandardObjects()
|
||||
scope.put("window", scope, scope)
|
||||
var result = ""
|
||||
try {
|
||||
Log.d("runJS", "Executing JavaScript: $hideMyHtmlContent")
|
||||
rhino.evaluateString(scope, hideMyHtmlContent, "JavaScript", 1, null)
|
||||
val svgObject = scope.get("svg", scope)
|
||||
result = if (svgObject is NativeObject) {
|
||||
NativeJSON.stringify(Context.getCurrentContext(), scope, svgObject, null, null).toString()
|
||||
} else {
|
||||
Context.toString(svgObject)
|
||||
}
|
||||
Log.d("runJS", "Result: $result")
|
||||
} catch (e: Exception) {
|
||||
Log.e("runJS", "Error executing JavaScript", e)
|
||||
} finally {
|
||||
Context.exit()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
data class SvgObject(
|
||||
val stream: String,
|
||||
val hash: String
|
||||
)
|
||||
}
|
|
@ -25,9 +25,13 @@ open class Vidmoly : ExtractorApi() {
|
|||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
|
||||
val headers = mapOf(
|
||||
"User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36",
|
||||
"Sec-Fetch-Dest" to "iframe"
|
||||
)
|
||||
val script = app.get(
|
||||
url,
|
||||
headers = headers,
|
||||
referer = referer,
|
||||
).document.select("script")
|
||||
.find { it.data().contains("sources:") }?.data()
|
||||
|
@ -66,4 +70,4 @@ open class Vidmoly : ExtractorApi() {
|
|||
@JsonProperty("kind") val kind: String? = null,
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,10 @@ import javax.crypto.spec.SecretKeySpec
|
|||
// Code found in https://github.com/KillerDogeEmpire/vidplay-keys
|
||||
// special credits to @KillerDogeEmpire for providing key
|
||||
|
||||
class AnyVidplay(hostUrl: String) : Vidplay() {
|
||||
override val mainUrl = hostUrl
|
||||
}
|
||||
|
||||
class MyCloud : Vidplay() {
|
||||
override val name = "MyCloud"
|
||||
override val mainUrl = "https://mcloud.bz"
|
||||
|
@ -66,7 +70,7 @@ open class Vidplay : ExtractorApi() {
|
|||
}
|
||||
|
||||
private suspend fun callFutoken(id: String, url: String): String? {
|
||||
val script = app.get("$mainUrl/futoken").text
|
||||
val script = app.get("$mainUrl/futoken", referer = url).text
|
||||
val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null
|
||||
val a = mutableListOf(k)
|
||||
for (i in id.indices) {
|
||||
|
|
|
@ -1,19 +1,46 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import android.util.Base64
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
|
||||
class Tubeless : Voe() {
|
||||
override var mainUrl = "https://tubelessceliolymph.com"
|
||||
override val name = "Tubeless"
|
||||
override val mainUrl = "https://tubelessceliolymph.com"
|
||||
}
|
||||
|
||||
class Simpulumlamerop : Voe() {
|
||||
override val name = "Simplum"
|
||||
override var mainUrl = "https://simpulumlamerop.com"
|
||||
}
|
||||
|
||||
class Urochsunloath : Voe() {
|
||||
override val name = "Uroch"
|
||||
override var mainUrl = "https://urochsunloath.com"
|
||||
}
|
||||
|
||||
class Yipsu : Voe() {
|
||||
override val name = "Yipsu"
|
||||
override var mainUrl = "https://yip.su"
|
||||
}
|
||||
|
||||
class MetaGnathTuggers : Voe() {
|
||||
override val name = "Metagnath"
|
||||
override val mainUrl = "https://metagnathtuggers.com"
|
||||
}
|
||||
|
||||
open class Voe : ExtractorApi() {
|
||||
override val name = "Voe"
|
||||
override val mainUrl = "https://voe.sx"
|
||||
override val requiresReferer = true
|
||||
|
||||
private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex()
|
||||
private val base64Regex = Regex("'.*'")
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
|
@ -25,12 +52,33 @@ open class Voe : ExtractorApi() {
|
|||
val script = res.select("script").find { it.data().contains("sources =") }?.data()
|
||||
val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1)
|
||||
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
link ?: return,
|
||||
"$mainUrl/",
|
||||
headers = mapOf("Origin" to "$mainUrl/")
|
||||
).forEach(callback)
|
||||
|
||||
val videoLinks = mutableListOf<String>()
|
||||
|
||||
if (!link.isNullOrBlank()) {
|
||||
videoLinks.add(
|
||||
when {
|
||||
linkRegex.matches(link) -> link
|
||||
else -> String(Base64.decode(link, Base64.DEFAULT))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
val link2 = base64Regex.find(script)?.value ?: return
|
||||
val decoded = Base64.decode(link2, Base64.DEFAULT).toString()
|
||||
val videoLinkDTO = AppUtils.parseJson<WcoSources>(decoded)
|
||||
videoLinkDTO.let { videoLinks.add(it.toString()) }
|
||||
}
|
||||
|
||||
videoLinks.forEach { videoLink ->
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
videoLink,
|
||||
"$mainUrl/",
|
||||
headers = mapOf("Origin" to "$mainUrl/")
|
||||
).forEach(callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class WcoSources(
|
||||
@JsonProperty("VideoLinkDTO") val VideoLinkDTO: String,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.JsUnpacker
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import java.net.URI
|
||||
|
||||
|
||||
open class Vtbe : ExtractorApi() {
|
||||
override var name = "Vtbe"
|
||||
override var mainUrl = "https://vtbe.to"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val response = app.get(url,referer=mainUrl).document
|
||||
val extractedpack =response.selectFirst("script:containsData(function(p,a,c,k,e,d))")?.data().toString()
|
||||
JsUnpacker(extractedpack).unpack()?.let { unPacked ->
|
||||
Regex("sources:\\[\\{file:\"(.*?)\"").find(unPacked)?.groupValues?.get(1)?.let { link ->
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
link,
|
||||
referer ?: "",
|
||||
Qualities.Unknown.value,
|
||||
URI(link).path.endsWith(".m3u8")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -105,6 +105,7 @@ open class TmdbProvider : MainAPI() {
|
|||
this.id,
|
||||
episode.episode_number,
|
||||
episode.season_number,
|
||||
this.name ?: this.original_name,
|
||||
).toJson(),
|
||||
episode.name,
|
||||
episode.season_number,
|
||||
|
@ -122,6 +123,7 @@ open class TmdbProvider : MainAPI() {
|
|||
this.id,
|
||||
episodeNum,
|
||||
season.season_number,
|
||||
this.name ?: this.original_name,
|
||||
).toJson(),
|
||||
season = season.season_number
|
||||
)
|
||||
|
|
|
@ -0,0 +1,441 @@
|
|||
package com.lagradost.cloudstream3.metaproviders
|
||||
|
||||
import android.net.Uri
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.fasterxml.jackson.annotation.JsonAlias
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import java.util.Locale
|
||||
import java.text.SimpleDateFormat
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
open class TraktProvider : MainAPI() {
|
||||
override var name = "Trakt"
|
||||
override val hasMainPage = true
|
||||
override val providerType = ProviderType.MetaProvider
|
||||
override val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
TvType.Anime,
|
||||
)
|
||||
|
||||
private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==")
|
||||
private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2")
|
||||
|
||||
override val mainPage = mainPageOf(
|
||||
"$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now
|
||||
"$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time
|
||||
"$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now
|
||||
"$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time
|
||||
)
|
||||
|
||||
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||
|
||||
val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page")
|
||||
|
||||
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
||||
element.toSearchResponse()
|
||||
}
|
||||
return newHomePageResponse(request.name, results)
|
||||
}
|
||||
|
||||
private fun MediaDetails.toSearchResponse(): SearchResponse {
|
||||
|
||||
val media = this.media ?: this
|
||||
val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries
|
||||
val poster = media.images?.poster?.firstOrNull()
|
||||
|
||||
if (mediaType == TvType.Movie) {
|
||||
return newMovieSearchResponse(
|
||||
name = media.title!!,
|
||||
url = Data(
|
||||
type = mediaType,
|
||||
mediaDetails = media,
|
||||
).toJson(),
|
||||
type = TvType.Movie,
|
||||
) {
|
||||
posterUrl = fixPath(poster)
|
||||
}
|
||||
} else {
|
||||
return newTvSeriesSearchResponse(
|
||||
name = media.title!!,
|
||||
url = Data(
|
||||
type = mediaType,
|
||||
mediaDetails = media,
|
||||
).toJson(),
|
||||
type = TvType.TvSeries,
|
||||
) {
|
||||
this.posterUrl = fixPath(poster)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<SearchResponse>? {
|
||||
val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query")
|
||||
|
||||
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
||||
element.toSearchResponse()
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
override suspend fun load(url: String): LoadResponse {
|
||||
|
||||
val data = parseJson<Data>(url)
|
||||
val mediaDetails = data.mediaDetails
|
||||
val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows"
|
||||
|
||||
val posterUrl = mediaDetails?.images?.poster?.firstOrNull()
|
||||
val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull()
|
||||
|
||||
val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full")
|
||||
|
||||
val actors = parseJson<People>(resActor).cast?.map {
|
||||
ActorData(
|
||||
Actor(
|
||||
name = it.person?.name!!,
|
||||
image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500")
|
||||
),
|
||||
roleString = it.character
|
||||
)
|
||||
}
|
||||
|
||||
val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20")
|
||||
|
||||
val relatedMedia = parseJson<List<MediaDetails>>(resRelated).map { it.toSearchResponse() }
|
||||
|
||||
val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true
|
||||
val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja")
|
||||
val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko")
|
||||
val isBollywood = mediaDetails?.country == "in"
|
||||
|
||||
if (data.type == TvType.Movie) {
|
||||
|
||||
val linkData = LinkData(
|
||||
id = mediaDetails?.ids?.tmdb,
|
||||
traktId = mediaDetails?.ids?.trakt,
|
||||
traktSlug = mediaDetails?.ids?.slug,
|
||||
tmdbId = mediaDetails?.ids?.tmdb,
|
||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||
tvdbId = mediaDetails?.ids?.tvdb,
|
||||
tvrageId = mediaDetails?.ids?.tvrage,
|
||||
type = data.type.toString(),
|
||||
title = mediaDetails?.title,
|
||||
year = mediaDetails?.year,
|
||||
orgTitle = mediaDetails?.title,
|
||||
isAnime = isAnime,
|
||||
//jpTitle = later if needed as it requires another network request,
|
||||
airedDate = mediaDetails?.released
|
||||
?: mediaDetails?.firstAired,
|
||||
isAsian = isAsian,
|
||||
isBollywood = isBollywood,
|
||||
).toJson()
|
||||
|
||||
return newMovieLoadResponse(
|
||||
name = mediaDetails?.title!!,
|
||||
url = data.toJson(),
|
||||
dataUrl = linkData.toJson(),
|
||||
type = if (isAnime) TvType.AnimeMovie else TvType.Movie,
|
||||
) {
|
||||
this.name = mediaDetails.title
|
||||
this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie
|
||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||
this.year = mediaDetails.year
|
||||
this.plot = mediaDetails.overview
|
||||
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
|
||||
this.tags = mediaDetails.genres
|
||||
this.duration = mediaDetails.runtime
|
||||
this.recommendations = relatedMedia
|
||||
this.actors = actors
|
||||
this.comingSoon = isUpcoming(mediaDetails.released)
|
||||
//posterHeaders
|
||||
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
|
||||
this.contentRating = mediaDetails.certification
|
||||
addTrailer(mediaDetails.trailer)
|
||||
addImdbId(mediaDetails.ids?.imdb)
|
||||
addTMDbId(mediaDetails.ids?.tmdb.toString())
|
||||
}
|
||||
} else {
|
||||
|
||||
val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes")
|
||||
val episodes = mutableListOf<Episode>()
|
||||
val seasons = parseJson<List<Seasons>>(resSeasons)
|
||||
var nextAir: NextAiring? = null
|
||||
|
||||
seasons.forEach { season ->
|
||||
|
||||
season.episodes?.map { episode ->
|
||||
|
||||
val linkData = LinkData(
|
||||
id = mediaDetails?.ids?.tmdb,
|
||||
traktId = mediaDetails?.ids?.trakt,
|
||||
traktSlug = mediaDetails?.ids?.slug,
|
||||
tmdbId = mediaDetails?.ids?.tmdb,
|
||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||
tvdbId = mediaDetails?.ids?.tvdb,
|
||||
tvrageId = mediaDetails?.ids?.tvrage,
|
||||
type = data.type.toString(),
|
||||
season = episode.season,
|
||||
episode = episode.number,
|
||||
title = mediaDetails?.title,
|
||||
year = mediaDetails?.year,
|
||||
orgTitle = mediaDetails?.title,
|
||||
isAnime = isAnime,
|
||||
airedYear = mediaDetails?.year,
|
||||
lastSeason = seasons.size,
|
||||
epsTitle = episode.title,
|
||||
//jpTitle = later if needed as it requires another network request,
|
||||
date = episode.firstAired,
|
||||
airedDate = episode.firstAired,
|
||||
isAsian = isAsian,
|
||||
isBollywood = isBollywood,
|
||||
isCartoon = isCartoon
|
||||
).toJson()
|
||||
|
||||
episodes.add(
|
||||
Episode(
|
||||
data = linkData.toJson(),
|
||||
name = episode.title,
|
||||
season = episode.season,
|
||||
episode = episode.number,
|
||||
posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()),
|
||||
rating = episode.rating?.times(10)?.roundToInt(),
|
||||
description = episode.overview,
|
||||
).apply {
|
||||
this.addDate(episode.firstAired)
|
||||
if (nextAir == null && this.date != null && this.date!! > unixTimeMS) {
|
||||
nextAir = NextAiring(
|
||||
episode = this.episode!!,
|
||||
unixTime = this.date!!.div(1000L),
|
||||
season = if (this.season == 1) null else this.season,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return newTvSeriesLoadResponse(
|
||||
name = mediaDetails?.title!!,
|
||||
url = data.toJson(),
|
||||
type = if (isAnime) TvType.Anime else TvType.TvSeries,
|
||||
episodes = episodes
|
||||
) {
|
||||
this.name = mediaDetails.title
|
||||
this.type = if (isAnime) TvType.Anime else TvType.TvSeries
|
||||
this.episodes = episodes
|
||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||
this.year = mediaDetails.year
|
||||
this.plot = mediaDetails.overview
|
||||
this.showStatus = getStatus(mediaDetails.status)
|
||||
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
|
||||
this.tags = mediaDetails.genres
|
||||
this.duration = mediaDetails.runtime
|
||||
this.recommendations = relatedMedia
|
||||
this.actors = actors
|
||||
this.comingSoon = isUpcoming(mediaDetails.released)
|
||||
//posterHeaders
|
||||
this.nextAiring = nextAir
|
||||
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
|
||||
this.contentRating = mediaDetails.certification
|
||||
addTrailer(mediaDetails.trailer)
|
||||
addImdbId(mediaDetails.ids?.imdb)
|
||||
addTMDbId(mediaDetails.ids?.tmdb.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getApi(url: String) : String {
|
||||
return app.get(
|
||||
url = url,
|
||||
headers = mapOf(
|
||||
"Content-Type" to "application/json",
|
||||
"trakt-api-version" to "2",
|
||||
"trakt-api-key" to traktClientId,
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
|
||||
private fun isUpcoming(dateString: String?): Boolean {
|
||||
return try {
|
||||
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
|
||||
APIHolder.unixTimeMS < dateTime
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatus(t: String?): ShowStatus {
|
||||
return when (t) {
|
||||
"returning series" -> ShowStatus.Ongoing
|
||||
"continuing" -> ShowStatus.Ongoing
|
||||
else -> ShowStatus.Completed
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixPath(url: String?): String? {
|
||||
url ?: return null
|
||||
return "https://$url"
|
||||
}
|
||||
|
||||
private fun getWidthImageUrl(path: String?, width: String) : String? {
|
||||
if (path == null) return null
|
||||
if (!path.contains("image.tmdb.org")) return fixPath(path)
|
||||
val fileName = Uri.parse(path).lastPathSegment ?: return null
|
||||
return "https://image.tmdb.org/t/p/${width}/${fileName}"
|
||||
}
|
||||
|
||||
private fun getOriginalWidthImageUrl(path: String?) : String? {
|
||||
if (path == null) return null
|
||||
if (!path.contains("image.tmdb.org")) return fixPath(path)
|
||||
return getWidthImageUrl(path, "original")
|
||||
}
|
||||
|
||||
data class Data(
|
||||
val type: TvType? = null,
|
||||
val mediaDetails: MediaDetails? = null,
|
||||
)
|
||||
|
||||
data class MediaDetails(
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("year") val year: Int? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("tagline") val tagline: String? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("released") val released: String? = null,
|
||||
@JsonProperty("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("country") val country: String? = null,
|
||||
@JsonProperty("updatedAt") val updatedAt: String? = null,
|
||||
@JsonProperty("trailer") val trailer: String? = null,
|
||||
@JsonProperty("homepage") val homepage: String? = null,
|
||||
@JsonProperty("status") val status: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("votes") val votes: Long? = null,
|
||||
@JsonProperty("comment_count") val commentCount: Long? = null,
|
||||
@JsonProperty("language") val language: String? = null,
|
||||
@JsonProperty("languages") val languages: List<String>? = null,
|
||||
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
||||
@JsonProperty("genres") val genres: List<String>? = null,
|
||||
@JsonProperty("certification") val certification: String? = null,
|
||||
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("airs") val airs: Airs? = null,
|
||||
@JsonProperty("network") val network: String? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null
|
||||
)
|
||||
|
||||
data class Airs(
|
||||
@JsonProperty("day") val day: String? = null,
|
||||
@JsonProperty("time") val time: String? = null,
|
||||
@JsonProperty("timezone") val timezone: String? = null,
|
||||
)
|
||||
|
||||
data class Ids(
|
||||
@JsonProperty("trakt") val trakt: Int? = null,
|
||||
@JsonProperty("slug") val slug: String? = null,
|
||||
@JsonProperty("tvdb") val tvdb: Int? = null,
|
||||
@JsonProperty("imdb") val imdb: String? = null,
|
||||
@JsonProperty("tmdb") val tmdb: Int? = null,
|
||||
@JsonProperty("tvrage") val tvrage: String? = null,
|
||||
)
|
||||
|
||||
data class Images(
|
||||
@JsonProperty("fanart") val fanart: List<String>? = null,
|
||||
@JsonProperty("poster") val poster: List<String>? = null,
|
||||
@JsonProperty("logo") val logo: List<String>? = null,
|
||||
@JsonProperty("clearart") val clearart: List<String>? = null,
|
||||
@JsonProperty("banner") val banner: List<String>? = null,
|
||||
@JsonProperty("thumb") val thumb: List<String>? = null,
|
||||
@JsonProperty("screenshot") val screenshot: List<String>? = null,
|
||||
@JsonProperty("headshot") val headshot: List<String>? = null,
|
||||
)
|
||||
|
||||
data class People(
|
||||
@JsonProperty("cast") val cast: List<Cast>? = null,
|
||||
)
|
||||
|
||||
data class Cast(
|
||||
@JsonProperty("character") val character: String? = null,
|
||||
@JsonProperty("characters") val characters: List<String>? = null,
|
||||
@JsonProperty("episode_count") val episodeCount: Long? = null,
|
||||
@JsonProperty("person") val person: Person? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
)
|
||||
|
||||
data class Person(
|
||||
@JsonProperty("name") val name: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
)
|
||||
|
||||
data class Seasons(
|
||||
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
||||
@JsonProperty("episode_count") val episodeCount: Int? = null,
|
||||
@JsonProperty("episodes") val episodes: List<TraktEpisode>? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("network") val network: String? = null,
|
||||
@JsonProperty("number") val number: Int? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("updated_at") val updatedAt: String? = null,
|
||||
@JsonProperty("votes") val votes: Int? = null,
|
||||
)
|
||||
|
||||
data class TraktEpisode(
|
||||
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
||||
@JsonProperty("comment_count") val commentCount: Int? = null,
|
||||
@JsonProperty("episode_type") val episodeType: String? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("number") val number: Int? = null,
|
||||
@JsonProperty("number_abs") val numberAbs: Int? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("season") val season: Int? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("updated_at") val updatedAt: String? = null,
|
||||
@JsonProperty("votes") val votes: Int? = null,
|
||||
)
|
||||
|
||||
data class LinkData(
|
||||
val id: Int? = null,
|
||||
val traktId: Int? = null,
|
||||
val traktSlug: String? = null,
|
||||
val tmdbId: Int? = null,
|
||||
val imdbId: String? = null,
|
||||
val tvdbId: Int? = null,
|
||||
val tvrageId: String? = null,
|
||||
val type: String? = null,
|
||||
val season: Int? = null,
|
||||
val episode: Int? = null,
|
||||
val aniId: String? = null,
|
||||
val animeId: String? = null,
|
||||
val title: String? = null,
|
||||
val year: Int? = null,
|
||||
val orgTitle: String? = null,
|
||||
val isAnime: Boolean = false,
|
||||
val airedYear: Int? = null,
|
||||
val lastSeason: Int? = null,
|
||||
val epsTitle: String? = null,
|
||||
val jpTitle: String? = null,
|
||||
val date: String? = null,
|
||||
val airedDate: String? = null,
|
||||
val isAsian: Boolean = false,
|
||||
val isBollywood: Boolean = false,
|
||||
val isCartoon: Boolean = false,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.lagradost.cloudstream3.mvvm
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
|
||||
/** NOTE: Only one observer at a time per value */
|
||||
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||
liveData.removeObservers(this)
|
||||
liveData.observe(this) { it?.let { t -> action(t) } }
|
||||
}
|
||||
|
||||
/** NOTE: Only one observer at a time per value */
|
||||
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||
liveData.removeObservers(this)
|
||||
liveData.observe(this) { action(it) }
|
||||
}
|
|
@ -429,7 +429,6 @@ object PluginManager {
|
|||
**/
|
||||
fun loadAllLocalPlugins(context: Context, forceReload: Boolean) {
|
||||
val dir = File(LOCAL_PLUGINS_PATH)
|
||||
removeKey(PLUGINS_KEY_LOCAL)
|
||||
|
||||
if (!dir.exists()) {
|
||||
val res = dir.mkdirs()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
|
@ -12,7 +13,7 @@ import com.lagradost.cloudstream3.*
|
|||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
|
||||
|
@ -97,128 +98,138 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
|
|||
)
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
override suspend fun doWork(): Result {
|
||||
try {
|
||||
// println("Update subscriptions!")
|
||||
context.createNotificationChannel(
|
||||
SUBSCRIPTION_CHANNEL_ID,
|
||||
SUBSCRIPTION_CHANNEL_NAME,
|
||||
SUBSCRIPTION_CHANNEL_DESCRIPTION
|
||||
)
|
||||
|
||||
setForeground(
|
||||
ForegroundInfo(
|
||||
SUBSCRIPTION_NOTIFICATION_ID,
|
||||
progressNotificationBuilder.build()
|
||||
context.createNotificationChannel(
|
||||
SUBSCRIPTION_CHANNEL_ID,
|
||||
SUBSCRIPTION_CHANNEL_NAME,
|
||||
SUBSCRIPTION_CHANNEL_DESCRIPTION
|
||||
)
|
||||
)
|
||||
|
||||
val subscriptions = getAllSubscriptions()
|
||||
setForeground(
|
||||
ForegroundInfo(
|
||||
SUBSCRIPTION_NOTIFICATION_ID,
|
||||
progressNotificationBuilder.build()
|
||||
)
|
||||
)
|
||||
|
||||
if (subscriptions.isEmpty()) {
|
||||
WorkManager.getInstance(context).cancelWorkById(this.id)
|
||||
val subscriptions = getAllSubscriptions()
|
||||
|
||||
if (subscriptions.isEmpty()) {
|
||||
WorkManager.getInstance(context).cancelWorkById(this.id)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val max = subscriptions.size
|
||||
var progress = 0
|
||||
|
||||
updateProgress(max, progress, true)
|
||||
|
||||
// We need all plugins loaded.
|
||||
PluginManager.loadAllOnlinePlugins(context)
|
||||
PluginManager.loadAllLocalPlugins(context, false)
|
||||
|
||||
subscriptions.apmap { savedData ->
|
||||
try {
|
||||
val id = savedData.id ?: return@apmap null
|
||||
val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null
|
||||
|
||||
// Reasonable timeout to prevent having this worker run forever.
|
||||
val response = withTimeoutOrNull(60_000) {
|
||||
api.load(savedData.url) as? EpisodeResponse
|
||||
} ?: return@apmap null
|
||||
|
||||
val dubPreference =
|
||||
getDub(id) ?: if (
|
||||
context.getApiDubstatusSettings().contains(DubStatus.Dubbed)
|
||||
) {
|
||||
DubStatus.Dubbed
|
||||
} else {
|
||||
DubStatus.Subbed
|
||||
}
|
||||
|
||||
val latestEpisodes = response.getLatestEpisodes()
|
||||
val latestPreferredEpisode = latestEpisodes[dubPreference]
|
||||
|
||||
val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) {
|
||||
val latestSeenEpisode =
|
||||
savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE
|
||||
val shouldUpdate = latestPreferredEpisode > latestSeenEpisode
|
||||
shouldUpdate to latestPreferredEpisode
|
||||
} else {
|
||||
val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE
|
||||
val latestSeenEpisode =
|
||||
savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE
|
||||
val shouldUpdate = latestEpisode > latestSeenEpisode
|
||||
shouldUpdate to latestEpisode
|
||||
}
|
||||
|
||||
DataStoreHelper.updateSubscribedData(
|
||||
id,
|
||||
savedData,
|
||||
response
|
||||
)
|
||||
|
||||
if (shouldUpdate) {
|
||||
val updateHeader = savedData.name
|
||||
val updateDescription = txt(
|
||||
R.string.subscription_episode_released,
|
||||
latestEpisode,
|
||||
savedData.name
|
||||
).asString(context)
|
||||
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
data = savedData.url.toUri()
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
val pendingIntent =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(context, 0, intent, 0)
|
||||
}
|
||||
|
||||
val poster = ioWork {
|
||||
savedData.posterUrl?.let { url ->
|
||||
context.getImageBitmapFromUrl(
|
||||
url,
|
||||
savedData.posterHeaders
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val updateNotification =
|
||||
updateNotificationBuilder.setContentTitle(updateHeader)
|
||||
.setContentText(updateDescription)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setLargeIcon(poster)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(id, updateNotification)
|
||||
}
|
||||
|
||||
// You can probably get some issues here since this is async but it does not matter much.
|
||||
updateProgress(max, ++progress, false)
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
// ye, while this is not correct, but because gods know why android just crashes
|
||||
// and this causes major battery usage as it retries it inf times. This is better, just
|
||||
// in case android decides to be android and fuck us
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val max = subscriptions.size
|
||||
var progress = 0
|
||||
|
||||
updateProgress(max, progress, true)
|
||||
|
||||
// We need all plugins loaded.
|
||||
PluginManager.loadAllOnlinePlugins(context)
|
||||
PluginManager.loadAllLocalPlugins(context, false)
|
||||
|
||||
subscriptions.apmap { savedData ->
|
||||
try {
|
||||
val id = savedData.id ?: return@apmap null
|
||||
val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null
|
||||
|
||||
// Reasonable timeout to prevent having this worker run forever.
|
||||
val response = withTimeoutOrNull(60_000) {
|
||||
api.load(savedData.url) as? EpisodeResponse
|
||||
} ?: return@apmap null
|
||||
|
||||
val dubPreference =
|
||||
getDub(id) ?: if (
|
||||
context.getApiDubstatusSettings().contains(DubStatus.Dubbed)
|
||||
) {
|
||||
DubStatus.Dubbed
|
||||
} else {
|
||||
DubStatus.Subbed
|
||||
}
|
||||
|
||||
val latestEpisodes = response.getLatestEpisodes()
|
||||
val latestPreferredEpisode = latestEpisodes[dubPreference]
|
||||
|
||||
val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) {
|
||||
val latestSeenEpisode =
|
||||
savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE
|
||||
val shouldUpdate = latestPreferredEpisode > latestSeenEpisode
|
||||
shouldUpdate to latestPreferredEpisode
|
||||
} else {
|
||||
val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE
|
||||
val latestSeenEpisode =
|
||||
savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE
|
||||
val shouldUpdate = latestEpisode > latestSeenEpisode
|
||||
shouldUpdate to latestEpisode
|
||||
}
|
||||
|
||||
DataStoreHelper.updateSubscribedData(
|
||||
id,
|
||||
savedData,
|
||||
response
|
||||
)
|
||||
|
||||
if (shouldUpdate) {
|
||||
val updateHeader = savedData.name
|
||||
val updateDescription = txt(
|
||||
R.string.subscription_episode_released,
|
||||
latestEpisode,
|
||||
savedData.name
|
||||
).asString(context)
|
||||
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
data = savedData.url.toUri()
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
val pendingIntent =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(context, 0, intent, 0)
|
||||
}
|
||||
|
||||
val poster = ioWork {
|
||||
savedData.posterUrl?.let { url ->
|
||||
context.getImageBitmapFromUrl(
|
||||
url,
|
||||
savedData.posterHeaders
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val updateNotification =
|
||||
updateNotificationBuilder.setContentTitle(updateHeader)
|
||||
.setContentText(updateDescription)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setLargeIcon(poster)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(id, updateNotification)
|
||||
}
|
||||
|
||||
// You can probably get some issues here since this is async but it does not matter much.
|
||||
updateProgress(max, ++progress, false)
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3.subtitles
|
||||
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
|
||||
class AbstractSubtitleEntities {
|
||||
|
@ -19,8 +20,11 @@ class AbstractSubtitleEntities {
|
|||
|
||||
data class SubtitleSearch(
|
||||
var query: String = "",
|
||||
var imdb: Long? = null,
|
||||
var lang: String? = null,
|
||||
var imdbId: String? = null,
|
||||
var tmdbId: Int? = null,
|
||||
var malId: Int? = null,
|
||||
var aniListId: Int? = null,
|
||||
var epNumber: Int? = null,
|
||||
var seasonNumber: Int? = null,
|
||||
var year: Int? = null
|
||||
|
|
|
@ -4,7 +4,6 @@ import androidx.annotation.WorkerThread
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.SubScene
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
@ -14,11 +13,10 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
|||
val aniListApi = AniListApi(0)
|
||||
val openSubtitlesApi = OpenSubtitlesApi(0)
|
||||
val simklApi = SimklApi(0)
|
||||
val addic7ed = Addic7ed()
|
||||
val subDlApi = SubDlApi(0)
|
||||
val googleDriveApi = GoogleDriveApi(0)
|
||||
val pcloudApi = PcloudApi(0)
|
||||
val indexSubtitlesApi = IndexSubtitleApi()
|
||||
val addic7ed = Addic7ed()
|
||||
val subScene = SubScene()
|
||||
val localListApi = LocalList()
|
||||
|
||||
// used to login via app intent
|
||||
|
@ -33,6 +31,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
|||
malApi,
|
||||
aniListApi,
|
||||
openSubtitlesApi,
|
||||
subDlApi,
|
||||
simklApi,
|
||||
googleDriveApi,
|
||||
pcloudApi //, nginxApi
|
||||
|
@ -52,15 +51,17 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
|||
|
||||
val inAppAuths
|
||||
get() = listOf(
|
||||
openSubtitlesApi, googleDriveApi, pcloudApi//, nginxApi
|
||||
)
|
||||
openSubtitlesApi,
|
||||
subDlApi,
|
||||
googleDriveApi,
|
||||
pcloudApi
|
||||
)//, nginxApi)
|
||||
|
||||
val subtitleProviders
|
||||
get() = listOf(
|
||||
openSubtitlesApi,
|
||||
indexSubtitlesApi, // they got anti scraping measures in place :(
|
||||
addic7ed,
|
||||
subScene
|
||||
subDlApi
|
||||
)
|
||||
|
||||
const val appString = "cloudstreamapp"
|
||||
|
|
|
@ -1,265 +0,0 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import android.util.Log
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.imdbUrlToIdNullable
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
|
||||
class IndexSubtitleApi : AbstractSubApi {
|
||||
override val name = "IndexSubtitle"
|
||||
override val idPrefix = "indexsubtitle"
|
||||
override val requiresLogin = false
|
||||
override val icon: Nothing? = null
|
||||
override val createAccountUrl: Nothing? = null
|
||||
|
||||
override fun loginInfo(): Nothing? = null
|
||||
|
||||
override fun logOut() {}
|
||||
|
||||
|
||||
companion object {
|
||||
const val host = "https://indexsubtitle.com"
|
||||
const val TAG = "INDEXSUBS"
|
||||
|
||||
fun getOrdinal(num: Int?): String? {
|
||||
return when (num) {
|
||||
1 -> "First"
|
||||
2 -> "Second"
|
||||
3 -> "Third"
|
||||
4 -> "Fourth"
|
||||
5 -> "Fifth"
|
||||
6 -> "Sixth"
|
||||
7 -> "Seventh"
|
||||
8 -> "Eighth"
|
||||
9 -> "Ninth"
|
||||
10 -> "Tenth"
|
||||
11 -> "Eleventh"
|
||||
12 -> "Twelfth"
|
||||
13 -> "Thirteenth"
|
||||
14 -> "Fourteenth"
|
||||
15 -> "Fifteenth"
|
||||
16 -> "Sixteenth"
|
||||
17 -> "Seventeenth"
|
||||
18 -> "Eighteenth"
|
||||
19 -> "Nineteenth"
|
||||
20 -> "Twentieth"
|
||||
21 -> "Twenty-First"
|
||||
22 -> "Twenty-Second"
|
||||
23 -> "Twenty-Third"
|
||||
24 -> "Twenty-Fourth"
|
||||
25 -> "Twenty-Fifth"
|
||||
26 -> "Twenty-Sixth"
|
||||
27 -> "Twenty-Seventh"
|
||||
28 -> "Twenty-Eighth"
|
||||
29 -> "Twenty-Ninth"
|
||||
30 -> "Thirtieth"
|
||||
31 -> "Thirty-First"
|
||||
32 -> "Thirty-Second"
|
||||
33 -> "Thirty-Third"
|
||||
34 -> "Thirty-Fourth"
|
||||
35 -> "Thirty-Fifth"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixUrl(url: String): String {
|
||||
if (url.startsWith("http")) {
|
||||
return url
|
||||
}
|
||||
if (url.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
val startsWithNoHttp = url.startsWith("//")
|
||||
if (startsWithNoHttp) {
|
||||
return "https:$url"
|
||||
} else {
|
||||
if (url.startsWith('/')) {
|
||||
return host + url
|
||||
}
|
||||
return "$host/$url"
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean {
|
||||
val FILTER_EPS_REGEX =
|
||||
Regex("(?i)((Chapter\\s?0?${epNum})|((Season)?\\s?0?${seasonNum}?\\s?(Episode)\\s?0?${epNum}[^0-9]))|(?i)((S?0?${seasonNum}?E0?${epNum}[^0-9])|(0?${seasonNum}[a-z]0?${epNum}[^0-9]))")
|
||||
return text.contains(FILTER_EPS_REGEX)
|
||||
}
|
||||
|
||||
private fun haveEps(text: String): Boolean {
|
||||
val HAVE_EPS_REGEX =
|
||||
Regex("(?i)((Chapter\\s?0?\\d)|((Season)?\\s?0?\\d?\\s?(Episode)\\s?0?\\d))|(?i)((S?0?\\d?E0?\\d)|(0?\\d[a-z]0?\\d))")
|
||||
return text.contains(HAVE_EPS_REGEX)
|
||||
}
|
||||
|
||||
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity> {
|
||||
val imdbId = query.imdb ?: 0
|
||||
val lang = query.lang
|
||||
val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString())
|
||||
val queryText = query.query
|
||||
val epNum = query.epNumber ?: 0
|
||||
val seasonNum = query.seasonNumber ?: 0
|
||||
val yearNum = query.year ?: 0
|
||||
|
||||
val urlItems = ArrayList<String>()
|
||||
|
||||
fun cleanResources(
|
||||
results: MutableList<AbstractSubtitleEntities.SubtitleEntity>,
|
||||
name: String,
|
||||
link: String
|
||||
) {
|
||||
results.add(
|
||||
AbstractSubtitleEntities.SubtitleEntity(
|
||||
idPrefix = idPrefix,
|
||||
name = name,
|
||||
lang = queryLang.toString(),
|
||||
data = link,
|
||||
source = this.name,
|
||||
type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie,
|
||||
epNumber = epNum,
|
||||
seasonNumber = seasonNum,
|
||||
year = yearNum,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val document = app.get("$host/?search=$queryText").document
|
||||
|
||||
document.select("div.my-3.p-3 div.media").map { block ->
|
||||
if (seasonNum > 0) {
|
||||
val name = block.select("strong.text-primary, strong.text-info").text().trim()
|
||||
val season = getOrdinal(seasonNum)
|
||||
if ((block.selectFirst("a")?.attr("href")
|
||||
?.contains(
|
||||
"$season",
|
||||
ignoreCase = true
|
||||
)!! || name.contains(
|
||||
"$season",
|
||||
ignoreCase = true
|
||||
)) && name.contains(queryText, ignoreCase = true)
|
||||
) {
|
||||
block.select("div.media").mapNotNull {
|
||||
urlItems.add(
|
||||
fixUrl(
|
||||
it.selectFirst("a")!!.attr("href")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (block.selectFirst("strong")!!.text().trim()
|
||||
.matches(Regex("(?i)^$queryText\$"))
|
||||
) {
|
||||
if (block.select("span[title=Release]").isNullOrEmpty()) {
|
||||
block.select("div.media").mapNotNull {
|
||||
val urlItem = fixUrl(
|
||||
it.selectFirst("a")!!.attr("href")
|
||||
)
|
||||
val itemDoc = app.get(urlItem).document
|
||||
val id = imdbUrlToIdNullable(
|
||||
itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent()
|
||||
?.attr("href")
|
||||
)?.toLongOrNull()
|
||||
val year = itemDoc.selectFirst("div.d-flex span.badge.badge-success")
|
||||
?.ownText()
|
||||
?.trim().toString()
|
||||
Log.i(TAG, "id => $id \nyear => $year||$yearNum")
|
||||
if (imdbId > 0) {
|
||||
if (id == imdbId) {
|
||||
urlItems.add(urlItem)
|
||||
}
|
||||
} else {
|
||||
if (year.contains("$yearNum")) {
|
||||
urlItems.add(urlItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (block.select("span[title=Release]").text().trim()
|
||||
.contains("$yearNum")
|
||||
) {
|
||||
block.select("div.media").mapNotNull {
|
||||
urlItems.add(
|
||||
fixUrl(
|
||||
it.selectFirst("a")!!.attr("href")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "urlItems => $urlItems")
|
||||
val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>()
|
||||
|
||||
urlItems.forEach { url ->
|
||||
val request = app.get(url)
|
||||
if (request.isSuccessful) {
|
||||
request.document.select("div.my-3.p-3 div.media").map { block ->
|
||||
if (block.select("span.d-block span[data-original-title=Language]").text()
|
||||
.trim()
|
||||
.contains("$queryLang")
|
||||
) {
|
||||
var name = block.select("strong.text-primary, strong.text-info").text().trim()
|
||||
val link = fixUrl(block.selectFirst("a")!!.attr("href"))
|
||||
if (seasonNum > 0) {
|
||||
when {
|
||||
isRightEps(name, seasonNum, epNum) -> {
|
||||
cleanResources(results, name, link)
|
||||
}
|
||||
!(haveEps(name)) -> {
|
||||
name = "$name (S${seasonNum}:E${epNum})"
|
||||
cleanResources(results, name, link)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cleanResources(results, name, link)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? {
|
||||
val seasonNum = data.seasonNumber
|
||||
val epNum = data.epNumber
|
||||
|
||||
val req = app.get(data.data)
|
||||
|
||||
if (req.isSuccessful) {
|
||||
val document = req.document
|
||||
val link = if (document.select("div.my-3.p-3 div.media").size == 1) {
|
||||
fixUrl(
|
||||
document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href")
|
||||
)
|
||||
} else {
|
||||
document.select("div.my-3.p-3 div.media").firstNotNullOf { block ->
|
||||
val name =
|
||||
block.selectFirst("strong.d-block")?.text()?.trim().toString()
|
||||
if (seasonNum!! > 0) {
|
||||
if (isRightEps(name, seasonNum, epNum)) {
|
||||
fixUrl(block.selectFirst("a")!!.attr("href"))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
fixUrl(block.selectFirst("a")!!.attr("href"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return link
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -8,7 +8,8 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
|||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
|
||||
|
@ -71,9 +72,9 @@ class LocalList : SyncAPI {
|
|||
}?.distinctBy { it.first } ?: return null
|
||||
|
||||
val list = ioWork {
|
||||
val isTrueTv = isTrueTvSettings()
|
||||
val isTrueTv = isLayout(TV)
|
||||
|
||||
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
|
||||
val baseMap = WatchType.entries.filter { it != WatchType.NONE }.associate {
|
||||
// None is not something to display
|
||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||
} + mapOf(
|
||||
|
|
|
@ -185,7 +185,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
|||
throwIfCantDoRequest()
|
||||
val fixedLang = fixLanguage(query.lang)
|
||||
|
||||
val imdbId = query.imdb ?: 0
|
||||
val imdbId = query.imdbId?.replace("tt", "")?.toInt() ?: 0
|
||||
val queryText = query.query
|
||||
val epNum = query.epNumber ?: 0
|
||||
val seasonNum = query.seasonNumber ?: 0
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.debugPrint
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubProvider
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||
import com.lagradost.cloudstream3.subtitles.SubtitleResource
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.IndexSubtitleApi.Companion.getOrdinal
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
|
||||
class SubScene : AbstractSubProvider {
|
||||
val mainUrl = "https://subscene.com"
|
||||
val name = "Subscene"
|
||||
override val idPrefix = "subscene"
|
||||
|
||||
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? {
|
||||
val seasonName =
|
||||
query.seasonNumber?.let { number ->
|
||||
// Need to translate "7" to "Seventh Season"
|
||||
getOrdinal(number)?.let { words -> " - $words Season" }
|
||||
} ?: ""
|
||||
|
||||
val fullQuery = query.query + seasonName
|
||||
|
||||
val doc = app.post(
|
||||
"$mainUrl/subtitles/searchbytitle",
|
||||
data = mapOf("query" to fullQuery, "l" to "")
|
||||
).document
|
||||
|
||||
return doc.select("div.title a").map { element ->
|
||||
val href = "$mainUrl${element.attr("href")}"
|
||||
val title = element.text()
|
||||
|
||||
AbstractSubtitleEntities.SubtitleEntity(
|
||||
idPrefix = idPrefix,
|
||||
name = title,
|
||||
source = name,
|
||||
data = href,
|
||||
lang = query.lang ?: "en",
|
||||
epNumber = query.epNumber
|
||||
)
|
||||
}.distinctBy { it.data }
|
||||
}
|
||||
|
||||
override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) {
|
||||
val resultDoc = app.get(data.data).document
|
||||
val queryLanguage = SubtitleHelper.fromTwoLettersToLanguage(data.lang) ?: "English"
|
||||
|
||||
val results = resultDoc.select("table tbody tr").mapNotNull { element ->
|
||||
val anchor = element.select("a")
|
||||
val href = anchor.attr("href") ?: return@mapNotNull null
|
||||
val fixedHref = "$mainUrl${href}"
|
||||
val spans = anchor.select("span")
|
||||
val language = spans.firstOrNull()?.text()
|
||||
val title = spans.getOrNull(1)?.text()
|
||||
val isPositive = anchor.select("span.positive-icon").isNotEmpty()
|
||||
|
||||
TableElement(title, language, fixedHref, isPositive)
|
||||
}.sortedBy {
|
||||
it.getScore(queryLanguage, data.epNumber)
|
||||
}
|
||||
|
||||
debugPrint { "$name found subtitles: ${results.takeLast(3)}" }
|
||||
// Last = highest score
|
||||
val selectedResult = results.lastOrNull() ?: return
|
||||
|
||||
val subtitleDocument = app.get(selectedResult.href).document
|
||||
val subtitleDownloadUrl =
|
||||
"$mainUrl${subtitleDocument.select("div.download a").attr("href")}"
|
||||
|
||||
this.addZipUrl(subtitleDownloadUrl) { name, _ ->
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to manage the various different subtitle results and rank them.
|
||||
*/
|
||||
data class TableElement(
|
||||
val title: String?,
|
||||
val language: String?,
|
||||
val href: String,
|
||||
val isPositive: Boolean
|
||||
) {
|
||||
private fun matchesLanguage(other: String): Boolean {
|
||||
return language != null && (language.contains(other, ignoreCase = true) ||
|
||||
other.contains(language, ignoreCase = true))
|
||||
}
|
||||
|
||||
/**
|
||||
* Scores in this order:
|
||||
* Preferred Language > Episode number > Positive rating > English Language
|
||||
*/
|
||||
fun getScore(queryLanguage: String, episodeNum: Int?): Int {
|
||||
var score = 0
|
||||
if (this.matchesLanguage(queryLanguage)) {
|
||||
score += 8
|
||||
}
|
||||
// Matches Episode 7 using "E07" with any number of leading zeroes
|
||||
if (episodeNum != null && title != null && title.contains(
|
||||
Regex(
|
||||
"""E0*${episodeNum}""",
|
||||
RegexOption.IGNORE_CASE
|
||||
)
|
||||
)
|
||||
) {
|
||||
score += 4
|
||||
}
|
||||
if (isPositive) {
|
||||
score += 2
|
||||
}
|
||||
if (this.matchesLanguage("English")) {
|
||||
score += 1
|
||||
}
|
||||
return score
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||
import com.lagradost.cloudstream3.subtitles.SubtitleResource
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI.LoginInfo
|
||||
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
|
||||
|
||||
class SubDlApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
|
||||
override val idPrefix = "subdl"
|
||||
override val name = "SubDL"
|
||||
override val icon = R.drawable.subdl_logo_big
|
||||
override val requiresPassword = true
|
||||
override val requiresEmail = true
|
||||
override val createAccountUrl = "https://subdl.com/login"
|
||||
|
||||
companion object {
|
||||
const val APIURL = "https://api.subdl.com"
|
||||
const val APIENDPOINT = "$APIURL/api/v1/subtitles"
|
||||
const val DOWNLOADENDPOINT = "https://dl.subdl.com"
|
||||
const val SUBDL_SUBTITLES_USER_KEY: String = "subdl_user"
|
||||
var currentSession: SubtitleOAuthEntity? = null
|
||||
}
|
||||
|
||||
override suspend fun initialize() {
|
||||
currentSession = getAuthKey()
|
||||
}
|
||||
|
||||
override fun logOut() {
|
||||
setAuthKey(null)
|
||||
removeAccountKeys()
|
||||
currentSession = getAuthKey()
|
||||
}
|
||||
override suspend fun login(data: InAppAuthAPI.LoginData): Boolean {
|
||||
val email = data.email ?: throw ErrorLoadingException("Requires Email")
|
||||
val password = data.password ?: throw ErrorLoadingException("Requires Password")
|
||||
switchToNewAccount()
|
||||
try {
|
||||
if (initLogin(email, password)) {
|
||||
registerAccount()
|
||||
return true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
switchToOldAccount()
|
||||
}
|
||||
switchToOldAccount()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getLatestLoginData(): InAppAuthAPI.LoginData? {
|
||||
val current = getAuthKey() ?: return null
|
||||
return InAppAuthAPI.LoginData(
|
||||
email = current.userEmail,
|
||||
password = current.pass
|
||||
)
|
||||
}
|
||||
|
||||
override fun loginInfo(): LoginInfo? {
|
||||
getAuthKey()?.let { user ->
|
||||
return LoginInfo(
|
||||
profilePicture = null,
|
||||
name = user.name ?: user.userEmail,
|
||||
accountIndex = accountIndex
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? {
|
||||
|
||||
val queryText = query.query
|
||||
val epNum = query.epNumber ?: 0
|
||||
val seasonNum = query.seasonNumber ?: 0
|
||||
val yearNum = query.year ?: 0
|
||||
|
||||
val idQuery = when {
|
||||
query.imdbId != null -> "&imdb_id=${query.imdbId}"
|
||||
query.tmdbId != null -> "&tmdb_id=${query.tmdbId}"
|
||||
else -> null
|
||||
}
|
||||
|
||||
val epQuery = if (epNum > 0) "&episode_number=$epNum" else ""
|
||||
val seasonQuery = if (seasonNum > 0) "&season_number=$seasonNum" else ""
|
||||
val yearQuery = if (yearNum > 0) "&year=$yearNum" else ""
|
||||
|
||||
val searchQueryUrl = when (idQuery) {
|
||||
//Use imdb/tmdb id to search if its valid
|
||||
null -> "$APIENDPOINT?api_key=${currentSession?.apiKey}&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery"
|
||||
else -> "$APIENDPOINT?api_key=${currentSession?.apiKey}$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery"
|
||||
}
|
||||
|
||||
val req = app.get(
|
||||
url = searchQueryUrl,
|
||||
headers = mapOf(
|
||||
"Accept" to "application/json"
|
||||
)
|
||||
)
|
||||
|
||||
return req.parsedSafe<ApiResponse>()?.subtitles?.map { subtitle ->
|
||||
|
||||
val lang = subtitle.lang.replaceFirstChar { it.uppercase() }
|
||||
val resEpNum = subtitle.episode ?: query.epNumber
|
||||
val resSeasonNum = subtitle.season ?: query.seasonNumber
|
||||
val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie
|
||||
|
||||
AbstractSubtitleEntities.SubtitleEntity(
|
||||
idPrefix = this.idPrefix,
|
||||
name = subtitle.releaseName,
|
||||
lang = lang,
|
||||
data = "${DOWNLOADENDPOINT}${subtitle.url}",
|
||||
type = type,
|
||||
source = this.name,
|
||||
epNumber = resEpNum,
|
||||
seasonNumber = resSeasonNum,
|
||||
isHearingImpaired = subtitle.hearingImpaired ?: false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) {
|
||||
this.addZipUrl(data.data) { name, _ ->
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initLogin(useremail: String, password: String): Boolean {
|
||||
|
||||
val tokenResponse = app.post(
|
||||
url = "$APIURL/login",
|
||||
data = mapOf(
|
||||
"email" to useremail,
|
||||
"password" to password
|
||||
)
|
||||
).parsedSafe<OAuthTokenResponse>()
|
||||
|
||||
if (tokenResponse?.token == null) return false
|
||||
|
||||
val apiResponse = app.get(
|
||||
url = "$APIURL/user/userApi",
|
||||
headers = mapOf(
|
||||
"Authorization" to "Bearer ${tokenResponse.token}"
|
||||
)
|
||||
).parsedSafe<ApiKeyResponse>()
|
||||
|
||||
if (apiResponse?.ok == false) return false
|
||||
|
||||
setAuthKey(
|
||||
SubtitleOAuthEntity(
|
||||
userEmail = useremail,
|
||||
pass = password,
|
||||
name = tokenResponse.userData?.username ?: tokenResponse.userData?.name,
|
||||
accessToken = tokenResponse.token,
|
||||
apiKey = apiResponse?.apiKey
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getAuthKey(): SubtitleOAuthEntity? {
|
||||
return getKey(accountId, SUBDL_SUBTITLES_USER_KEY)
|
||||
}
|
||||
|
||||
private fun setAuthKey(data: SubtitleOAuthEntity?) {
|
||||
if (data == null) removeKey(
|
||||
accountId,
|
||||
SUBDL_SUBTITLES_USER_KEY
|
||||
)
|
||||
currentSession = data
|
||||
setKey(accountId, SUBDL_SUBTITLES_USER_KEY, data)
|
||||
}
|
||||
|
||||
data class SubtitleOAuthEntity(
|
||||
@JsonProperty("userEmail") var userEmail: String,
|
||||
@JsonProperty("pass") var pass: String,
|
||||
@JsonProperty("name") var name: String? = null,
|
||||
@JsonProperty("accessToken") var accessToken: String? = null,
|
||||
@JsonProperty("apiKey") var apiKey: String? = null,
|
||||
)
|
||||
|
||||
data class OAuthTokenResponse(
|
||||
@JsonProperty("token") val token: String? = null,
|
||||
@JsonProperty("userData") val userData: UserData? = null,
|
||||
@JsonProperty("status") val status: Boolean? = null,
|
||||
@JsonProperty("message") val message: String? = null,
|
||||
)
|
||||
|
||||
data class UserData(
|
||||
@JsonProperty("email") val email: String,
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("country") val country: String,
|
||||
@JsonProperty("scStepCode") val scStepCode: String,
|
||||
@JsonProperty("scVerified") val scVerified: Boolean,
|
||||
@JsonProperty("username") val username: String? = null,
|
||||
@JsonProperty("scUsername") val scUsername: String,
|
||||
)
|
||||
|
||||
data class ApiKeyResponse(
|
||||
@JsonProperty("ok") val ok: Boolean? = false,
|
||||
@JsonProperty("api_key") val apiKey: String? = null,
|
||||
@JsonProperty("usage") val usage: Usage? = null,
|
||||
)
|
||||
|
||||
data class Usage(
|
||||
@JsonProperty("total") val total: Long? = 0,
|
||||
@JsonProperty("today") val today: Long? = 0,
|
||||
)
|
||||
|
||||
data class ApiResponse(
|
||||
@JsonProperty("status") val status: Boolean? = null,
|
||||
@JsonProperty("results") val results: List<Result>? = null,
|
||||
@JsonProperty("subtitles") val subtitles: List<Subtitle>? = null,
|
||||
)
|
||||
|
||||
data class Result(
|
||||
@JsonProperty("sd_id") val sdId: Int? = null,
|
||||
@JsonProperty("type") val type: String? = null,
|
||||
@JsonProperty("name") val name: String? = null,
|
||||
@JsonProperty("imdb_id") val imdbId: String? = null,
|
||||
@JsonProperty("tmdb_id") val tmdbId: Long? = null,
|
||||
@JsonProperty("first_air_date") val firstAirDate: String? = null,
|
||||
@JsonProperty("year") val year: Int? = null,
|
||||
)
|
||||
|
||||
data class Subtitle(
|
||||
@JsonProperty("release_name") val releaseName: String,
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("lang") val lang: String,
|
||||
@JsonProperty("author") val author: String? = null,
|
||||
@JsonProperty("url") val url: String? = null,
|
||||
@JsonProperty("subtitlePage") val subtitlePage: String? = null,
|
||||
@JsonProperty("season") val season: Int? = null,
|
||||
@JsonProperty("episode") val episode: Int? = null,
|
||||
@JsonProperty("language") val language: String? = null,
|
||||
@JsonProperty("hi") val hearingImpaired: Boolean? = null,
|
||||
)
|
||||
}
|
250
app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt
Normal file
250
app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt
Normal file
|
@ -0,0 +1,250 @@
|
|||
package com.lagradost.cloudstream3.ui
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.children
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
open class ViewHolderState<T>(val view: ViewBinding) : ViewHolder(view.root) {
|
||||
open fun save(): T? = null
|
||||
open fun restore(state: T) = Unit
|
||||
open fun onViewAttachedToWindow() = Unit
|
||||
open fun onViewDetachedFromWindow() = Unit
|
||||
open fun onViewRecycled() = Unit
|
||||
}
|
||||
|
||||
|
||||
// Based of the concept https://github.com/brahmkshatriya/echo/blob/main/app%2Fsrc%2Fmain%2Fjava%2Fdev%2Fbrahmkshatriya%2Fecho%2Fui%2Fadapters%2FMediaItemsContainerAdapter.kt#L108-L154
|
||||
class StateViewModel : ViewModel() {
|
||||
val layoutManagerStates = hashMapOf<Int, HashMap<Int, Any?>>()
|
||||
}
|
||||
|
||||
abstract class NoStateAdapter<T : Any>(fragment: Fragment) : BaseAdapter<T, Any>(fragment, 0)
|
||||
|
||||
/**
|
||||
* BaseAdapter is a persistent state stored adapter that supports headers and footers.
|
||||
* This should be used for restoring eg scroll or focus related to a view when it is recreated.
|
||||
*
|
||||
* Id is a per fragment based unique id used to store the underlying data done in an internal ViewModel.
|
||||
*
|
||||
* diffCallback is how the view should be handled when updating, override onUpdateContent for updates
|
||||
*
|
||||
* NOTE:
|
||||
*
|
||||
* By default it should save automatically, but you can also call save(recycle)
|
||||
*
|
||||
* By default no state is stored, but doing an id != 0 will store
|
||||
*
|
||||
* By default no headers or footers exist, override footers and headers count
|
||||
*/
|
||||
abstract class BaseAdapter<
|
||||
T : Any,
|
||||
S : Any>(
|
||||
fragment: Fragment,
|
||||
val id: Int = 0,
|
||||
diffCallback: DiffUtil.ItemCallback<T> = BaseDiffCallback()
|
||||
) : RecyclerView.Adapter<ViewHolderState<S>>() {
|
||||
open val footers: Int = 0
|
||||
open val headers: Int = 0
|
||||
|
||||
fun getItem(position: Int): T {
|
||||
return mDiffer.currentList[position]
|
||||
}
|
||||
|
||||
fun getItemOrNull(position: Int): T? {
|
||||
return mDiffer.currentList.getOrNull(position)
|
||||
}
|
||||
|
||||
private val mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
|
||||
object : NonFinalAdapterListUpdateCallback(this) {
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
super.onMoved(fromPosition + headers, toPosition + headers)
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
super.onRemoved(position + headers, count)
|
||||
}
|
||||
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
super.onChanged(position + headers, count, payload)
|
||||
}
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
super.onInserted(position + headers, count)
|
||||
}
|
||||
},
|
||||
AsyncDifferConfig.Builder(diffCallback).build()
|
||||
)
|
||||
|
||||
open fun submitList(list: List<T>?) {
|
||||
// deep copy at least the top list, because otherwise adapter can go crazy
|
||||
mDiffer.submitList(list?.let { CopyOnWriteArrayList(it) })
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return mDiffer.currentList.size + footers + headers
|
||||
}
|
||||
|
||||
open fun onUpdateContent(holder: ViewHolderState<S>, item: T, position: Int) =
|
||||
onBindContent(holder, item, position)
|
||||
|
||||
open fun onBindContent(holder: ViewHolderState<S>, item: T, position: Int) = Unit
|
||||
open fun onBindFooter(holder: ViewHolderState<S>) = Unit
|
||||
open fun onBindHeader(holder: ViewHolderState<S>) = Unit
|
||||
open fun onCreateContent(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
|
||||
open fun onCreateFooter(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
|
||||
open fun onCreateHeader(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
|
||||
|
||||
override fun onViewAttachedToWindow(holder: ViewHolderState<S>) {
|
||||
holder.onViewAttachedToWindow()
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(holder: ViewHolderState<S>) {
|
||||
holder.onViewDetachedFromWindow()
|
||||
}
|
||||
|
||||
fun save(recyclerView: RecyclerView) {
|
||||
for (child in recyclerView.children) {
|
||||
val holder =
|
||||
recyclerView.findContainingViewHolder(child) as? ViewHolderState<S> ?: continue
|
||||
setState(holder)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
stateViewModel.layoutManagerStates[id]?.clear()
|
||||
}
|
||||
|
||||
private fun getState(holder: ViewHolderState<S>): S? =
|
||||
stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S
|
||||
|
||||
private fun setState(holder: ViewHolderState<S>) {
|
||||
if(id == 0) return
|
||||
|
||||
if (!stateViewModel.layoutManagerStates.contains(id)) {
|
||||
stateViewModel.layoutManagerStates[id] = HashMap()
|
||||
}
|
||||
stateViewModel.layoutManagerStates[id]?.let { map ->
|
||||
map[holder.absoluteAdapterPosition] = holder.save()
|
||||
}
|
||||
}
|
||||
|
||||
private val attachListener = object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View) = Unit
|
||||
override fun onViewDetachedFromWindow(v: View) {
|
||||
if (v !is RecyclerView) return
|
||||
save(v)
|
||||
}
|
||||
}
|
||||
|
||||
final override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
recyclerView.addOnAttachStateChangeListener(attachListener)
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
}
|
||||
|
||||
final override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
recyclerView.removeOnAttachStateChangeListener(attachListener)
|
||||
super.onDetachedFromRecyclerView(recyclerView)
|
||||
}
|
||||
|
||||
final override fun getItemViewType(position: Int): Int {
|
||||
if (position < headers) {
|
||||
return HEADER
|
||||
}
|
||||
if (position - headers >= mDiffer.currentList.size) {
|
||||
return FOOTER
|
||||
}
|
||||
|
||||
return CONTENT
|
||||
}
|
||||
|
||||
private val stateViewModel: StateViewModel by fragment.viewModels()
|
||||
|
||||
final override fun onViewRecycled(holder: ViewHolderState<S>) {
|
||||
setState(holder)
|
||||
holder.onViewRecycled()
|
||||
super.onViewRecycled(holder)
|
||||
}
|
||||
|
||||
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderState<S> {
|
||||
return when (viewType) {
|
||||
CONTENT -> onCreateContent(parent)
|
||||
HEADER -> onCreateHeader(parent)
|
||||
FOOTER -> onCreateFooter(parent)
|
||||
else -> throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
|
||||
// https://medium.com/@domen.lanisnik/efficiently-updating-recyclerview-items-using-payloads-1305f65f3068
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolderState<S>,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
if (payloads.isEmpty()) {
|
||||
super.onBindViewHolder(holder, position, payloads)
|
||||
return
|
||||
}
|
||||
when (getItemViewType(position)) {
|
||||
CONTENT -> {
|
||||
val realPosition = position - headers
|
||||
val item = getItem(realPosition)
|
||||
onUpdateContent(holder, item, realPosition)
|
||||
}
|
||||
|
||||
FOOTER -> {
|
||||
onBindFooter(holder)
|
||||
}
|
||||
|
||||
HEADER -> {
|
||||
onBindHeader(holder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final override fun onBindViewHolder(holder: ViewHolderState<S>, position: Int) {
|
||||
when (getItemViewType(position)) {
|
||||
CONTENT -> {
|
||||
val realPosition = position - headers
|
||||
val item = getItem(realPosition)
|
||||
onBindContent(holder, item, realPosition)
|
||||
}
|
||||
|
||||
FOOTER -> {
|
||||
onBindFooter(holder)
|
||||
}
|
||||
|
||||
HEADER -> {
|
||||
onBindHeader(holder)
|
||||
}
|
||||
}
|
||||
|
||||
getState(holder)?.let { state ->
|
||||
holder.restore(state)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val HEADER: Int = 1
|
||||
private const val FOOTER: Int = 2
|
||||
private const val CONTENT: Int = 0
|
||||
}
|
||||
}
|
||||
|
||||
class BaseDiffCallback<T : Any>(
|
||||
val itemSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() },
|
||||
val contentSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() }
|
||||
) : DiffUtil.ItemCallback<T>() {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = itemSame(oldItem, newItem)
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = contentSame(oldItem, newItem)
|
||||
override fun getChangePayload(oldItem: T, newItem: T): Any = Any()
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package com.lagradost.cloudstream3.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
|
||||
/**
|
||||
* ListUpdateCallback that dispatches update events to the given adapter.
|
||||
*
|
||||
* @see DiffUtil.DiffResult.dispatchUpdatesTo
|
||||
*/
|
||||
open class NonFinalAdapterListUpdateCallback
|
||||
/**
|
||||
* Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter.
|
||||
*
|
||||
* @param adapter The Adapter to send updates to.
|
||||
*/(private var mAdapter: RecyclerView.Adapter<*>) :
|
||||
ListUpdateCallback {
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
mAdapter.notifyItemRangeInserted(position, count)
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
mAdapter.notifyItemRangeRemoved(position, count)
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
mAdapter.notifyItemMoved(fromPosition, toPosition)
|
||||
}
|
||||
|
||||
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
mAdapter.notifyItemRangeChanged(position, count, payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,9 @@ import com.lagradost.cloudstream3.databinding.AccountListItemBinding
|
|||
import com.lagradost.cloudstream3.databinding.AccountListItemEditBinding
|
||||
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountEditDialog
|
||||
import com.lagradost.cloudstream3.ui.result.setImage
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
|
||||
|
@ -38,7 +40,7 @@ class AccountAdapter(
|
|||
is AccountListItemBinding -> binding.apply {
|
||||
if (account == null) return@apply
|
||||
|
||||
val isTv = isTvSettings() || !root.isInTouchMode
|
||||
val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode
|
||||
|
||||
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
|
||||
|
||||
|
@ -80,7 +82,7 @@ class AccountAdapter(
|
|||
is AccountListItemEditBinding -> binding.apply {
|
||||
if (account == null) return@apply
|
||||
|
||||
val isTv = isTvSettings() || !root.isInTouchMode
|
||||
val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode
|
||||
|
||||
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
|
||||
|
||||
|
|
|
@ -18,10 +18,15 @@ import com.lagradost.cloudstream3.mvvm.observe
|
|||
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
||||
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_EDIT_ACCOUNT
|
||||
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_SELECT_ACCOUNT
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTruePhone
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex
|
||||
|
@ -46,7 +51,6 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
|
|||
)
|
||||
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false)
|
||||
val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false
|
||||
) || accounts.count() <= 1
|
||||
|
||||
|
@ -54,7 +58,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
|
|||
|
||||
fun askBiometricAuth() {
|
||||
|
||||
if (isTruePhone() && authEnabled) {
|
||||
if (isLayout(PHONE) && isAuthEnabled(this)) {
|
||||
if (deviceHasPasswordPinLock(this)) {
|
||||
startBiometricAuthentication(
|
||||
this,
|
||||
|
@ -62,8 +66,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
|
|||
false
|
||||
)
|
||||
|
||||
BiometricAuthenticator.promptInfo?.let {
|
||||
BiometricAuthenticator.biometricPrompt?.authenticate(it)
|
||||
promptInfo?.let { prompt ->
|
||||
biometricPrompt?.authenticate(prompt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -127,7 +131,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
|
|||
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
if (isTvSettings()) {
|
||||
if (isLayout(TV or EMULATOR)) {
|
||||
binding.editAccountButton.setBackgroundResource(
|
||||
R.drawable.player_button_tv_attr_no_bg
|
||||
)
|
||||
|
@ -168,7 +172,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
|
|||
viewModel.toggleIsEditing()
|
||||
}
|
||||
|
||||
if (isTvSettings()) {
|
||||
if (isLayout(TV or EMULATOR)) {
|
||||
recyclerView.spanCount = if (liveAccounts.count() + 1 <= 6) {
|
||||
liveAccounts.count() + 1
|
||||
} else 6
|
||||
|
@ -187,4 +191,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
|
|||
override fun onAuthenticationSuccess() {
|
||||
Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity")
|
||||
}
|
||||
|
||||
override fun onAuthenticationError() {
|
||||
finish()
|
||||
}
|
||||
}
|
|
@ -11,10 +11,14 @@ import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding
|
|||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -85,13 +89,15 @@ class DownloadChildFragment : Fragment() {
|
|||
|
||||
binding?.downloadChildToolbar?.apply {
|
||||
title = name
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
if (isLayout(PHONE or EMULATOR)) {
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
}
|
||||
setAppBarNoScrollFlagsOnTV()
|
||||
}
|
||||
|
||||
|
||||
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
||||
DownloadChildAdapter(
|
||||
ArrayList(),
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.ClipboardManager
|
|||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.format.Formatter.formatShortFileSize
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -13,17 +14,25 @@ import android.widget.Toast
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding
|
||||
import com.lagradost.cloudstream3.databinding.StreamInputBinding
|
||||
import com.lagradost.cloudstream3.isMovieType
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
||||
import com.lagradost.cloudstream3.ui.player.BasicLink
|
||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
||||
import com.lagradost.cloudstream3.ui.player.LinkGenerator
|
||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
|
||||
|
@ -32,17 +41,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
import android.text.format.Formatter.formatShortFileSize
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding
|
||||
import com.lagradost.cloudstream3.databinding.StreamInputBinding
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.ui.player.BasicLink
|
||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import java.net.URI
|
||||
|
||||
|
||||
|
@ -97,6 +98,8 @@ class DownloadFragment : Fragment() {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
hideKeyboard()
|
||||
|
||||
binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV()
|
||||
|
||||
observe(downloadsViewModel.noDownloadsText) {
|
||||
binding?.textNoDownloads?.text = it
|
||||
}
|
||||
|
@ -200,7 +203,7 @@ class DownloadFragment : Fragment() {
|
|||
}
|
||||
|
||||
// Should be visible in emulator layout
|
||||
binding?.downloadStreamButton?.isGone = isTrueTvSettings()
|
||||
binding?.downloadStreamButton?.isGone = isLayout(TV)
|
||||
binding?.downloadStreamButton?.setOnClickListener {
|
||||
val dialog =
|
||||
Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom)
|
||||
|
|
|
@ -2,16 +2,21 @@ package com.lagradost.cloudstream3.ui.download.button
|
|||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK
|
||||
|
@ -22,6 +27,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES
|
||||
|
||||
|
||||
open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||
|
@ -164,6 +170,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
|||
this.setPersistentId(card.id)
|
||||
view.setOnClickListener {
|
||||
if (isZeroBytes) {
|
||||
removeKey(KEY_RESUME_PACKAGES, card.id.toString())
|
||||
callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card))
|
||||
//callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data))
|
||||
} else {
|
||||
|
@ -241,40 +248,54 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
|||
}
|
||||
}*/
|
||||
|
||||
@MainThread
|
||||
private fun setStatusInternal(status : DownloadStatusTell?) {
|
||||
val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading
|
||||
if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) {
|
||||
val animation = AnimationUtils.loadAnimation(context, waitingAnimation)
|
||||
progressBarBackground.startAnimation(animation)
|
||||
} else {
|
||||
progressBarBackground.clearAnimation()
|
||||
}
|
||||
|
||||
val progressDrawable =
|
||||
if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline
|
||||
|
||||
progressBarBackground.background =
|
||||
ContextCompat.getDrawable(context, progressDrawable)
|
||||
|
||||
val drawable = getDrawableFromStatus(status)
|
||||
statusView.setImageDrawable(drawable)
|
||||
val isDrawable = drawable != null
|
||||
|
||||
statusView.isVisible = isDrawable
|
||||
val hide = hideWhenIcon && isDrawable
|
||||
if (hide) {
|
||||
progressBar.clearAnimation()
|
||||
progressBarBackground.clearAnimation()
|
||||
}
|
||||
progressBarBackground.isGone = hide
|
||||
progressBar.isGone = hide
|
||||
}
|
||||
|
||||
/** Also sets currentStatus */
|
||||
override fun setStatus(status: DownloadStatusTell?) {
|
||||
currentStatus = status
|
||||
|
||||
//progressBar.isVisible =
|
||||
// status != null && status != DownloadStatusTell.Complete && status != DownloadStatusTell.Error
|
||||
//progressBarBackground.isVisible = status != null && status != DownloadStatusTell.Complete
|
||||
progressBarBackground.post {
|
||||
val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading
|
||||
if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) {
|
||||
val animation = AnimationUtils.loadAnimation(context, waitingAnimation)
|
||||
progressBarBackground.startAnimation(animation)
|
||||
} else {
|
||||
progressBarBackground.clearAnimation()
|
||||
// runs on the main thread, but also instant if it already is
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
try {
|
||||
setStatusInternal(status)
|
||||
} catch (t : Throwable) {
|
||||
logError(t) // just in case setStatusInternal throws because thread
|
||||
progressBarBackground.post {
|
||||
setStatusInternal(status)
|
||||
}
|
||||
}
|
||||
|
||||
val progressDrawable =
|
||||
if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline
|
||||
|
||||
progressBarBackground.background =
|
||||
ContextCompat.getDrawable(context, progressDrawable)
|
||||
|
||||
val drawable = getDrawableFromStatus(status)
|
||||
statusView.setImageDrawable(drawable)
|
||||
val isDrawable = drawable != null
|
||||
|
||||
statusView.isVisible = isDrawable
|
||||
val hide = hideWhenIcon && isDrawable
|
||||
if (hide) {
|
||||
progressBar.clearAnimation()
|
||||
progressBarBackground.clearAnimation()
|
||||
} else {
|
||||
progressBarBackground.post {
|
||||
setStatusInternal(status)
|
||||
}
|
||||
progressBarBackground.isGone = hide
|
||||
progressBar.isGone = hide
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,31 +2,58 @@ package com.lagradost.cloudstream3.ui.home
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding
|
||||
import com.lagradost.cloudstream3.ui.BaseAdapter
|
||||
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
|
||||
class HomeChildItemAdapter(
|
||||
val cardList: MutableList<SearchResponse>,
|
||||
class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState<Boolean>(view) {
|
||||
/*private fun recursive(view : View) : Boolean {
|
||||
if (view.isFocused) {
|
||||
println("VIEW: $view | id=${view.id}")
|
||||
}
|
||||
return (view as? ViewGroup)?.children?.any { recursive(it) } ?: false
|
||||
}*/
|
||||
|
||||
// very shitty that we cant store the state when the view clears,
|
||||
// but this is because the focus clears before the view is removed
|
||||
// so we have to manually store it
|
||||
var wasFocused: Boolean = false
|
||||
override fun save(): Boolean = wasFocused
|
||||
override fun restore(state: Boolean) {
|
||||
if (state) {
|
||||
wasFocused = false
|
||||
// only refocus if tv
|
||||
if(isLayout(TV)) {
|
||||
itemView.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HomeChildItemAdapter(
|
||||
fragment: Fragment,
|
||||
id: Int,
|
||||
private val nextFocusUp: Int? = null,
|
||||
private val nextFocusDown: Int? = null,
|
||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||
) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
BaseAdapter<SearchResponse, Boolean>(fragment, id) {
|
||||
var isHorizontal: Boolean = false
|
||||
var hasNext: Boolean = false
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Boolean> {
|
||||
val expanded = parent.context.IsBottomLayout()
|
||||
/* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid
|
||||
|
||||
|
@ -39,164 +66,78 @@ class HomeChildItemAdapter(
|
|||
parent,
|
||||
false
|
||||
) else HomeResultGridBinding.inflate(inflater, parent, false)
|
||||
return HomeScrollViewHolderState(binding)
|
||||
}
|
||||
|
||||
override fun onBindContent(
|
||||
holder: ViewHolderState<Boolean>,
|
||||
item: SearchResponse,
|
||||
position: Int
|
||||
) {
|
||||
when (val binding = holder.view) {
|
||||
is HomeResultGridBinding -> {
|
||||
binding.backgroundCard.apply {
|
||||
val min = 114.toPx
|
||||
val max = 180.toPx
|
||||
|
||||
return CardViewHolder(
|
||||
binding,
|
||||
clickCallback,
|
||||
itemCount,
|
||||
layoutParams =
|
||||
layoutParams.apply {
|
||||
width = if (!isHorizontal) {
|
||||
min
|
||||
} else {
|
||||
max
|
||||
}
|
||||
height = if (!isHorizontal) {
|
||||
max
|
||||
} else {
|
||||
min
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is HomeResultGridExpandedBinding -> {
|
||||
binding.backgroundCard.apply {
|
||||
val min = 114.toPx
|
||||
val max = 180.toPx
|
||||
|
||||
layoutParams =
|
||||
layoutParams.apply {
|
||||
width = if (!isHorizontal) {
|
||||
min
|
||||
} else {
|
||||
max
|
||||
}
|
||||
height = if (!isHorizontal) {
|
||||
max
|
||||
} else {
|
||||
min
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (position == 0) { // to fix tv
|
||||
binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchResultBuilder.bind(
|
||||
clickCallback = { click ->
|
||||
// ok, so here we hijack the callback to fix the focus
|
||||
when (click.action) {
|
||||
SEARCH_ACTION_LOAD -> (holder as? HomeScrollViewHolderState)?.wasFocused = true
|
||||
}
|
||||
clickCallback(click)
|
||||
},
|
||||
item,
|
||||
position,
|
||||
holder.itemView,
|
||||
null, // nextFocusBehavior,
|
||||
nextFocusUp,
|
||||
nextFocusDown,
|
||||
isHorizontal,
|
||||
parent.isRtl()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is CardViewHolder -> {
|
||||
holder.itemCount = itemCount // i know ugly af
|
||||
holder.bind(cardList[position], position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return cardList.size
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return (cardList[position].id ?: position).toLong()
|
||||
}
|
||||
|
||||
fun updateList(newList: List<SearchResponse>) {
|
||||
val diffResult = DiffUtil.calculateDiff(
|
||||
HomeChildDiffCallback(this.cardList, newList)
|
||||
nextFocusDown
|
||||
)
|
||||
|
||||
cardList.clear()
|
||||
cardList.addAll(newList)
|
||||
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
class CardViewHolder
|
||||
constructor(
|
||||
val binding: ViewBinding,
|
||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||
var itemCount: Int,
|
||||
private val nextFocusUp: Int? = null,
|
||||
private val nextFocusDown: Int? = null,
|
||||
private val isHorizontal: Boolean = false,
|
||||
private val isRtl: Boolean
|
||||
) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(card: SearchResponse, position: Int) {
|
||||
|
||||
// TV focus fixing
|
||||
/*val nextFocusBehavior = when (position) {
|
||||
0 -> true
|
||||
itemCount - 1 -> false
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (position == 0) { // to fix tv
|
||||
if (isRtl) {
|
||||
itemView.nextFocusRightId = R.id.nav_rail_view
|
||||
itemView.nextFocusLeftId = -1
|
||||
}
|
||||
else {
|
||||
itemView.nextFocusLeftId = R.id.nav_rail_view
|
||||
itemView.nextFocusRightId = -1
|
||||
}
|
||||
} else {
|
||||
itemView.nextFocusRightId = -1
|
||||
itemView.nextFocusLeftId = -1
|
||||
}*/
|
||||
|
||||
|
||||
when (binding) {
|
||||
is HomeResultGridBinding -> {
|
||||
binding.backgroundCard.apply {
|
||||
val min = 114.toPx
|
||||
val max = 180.toPx
|
||||
|
||||
layoutParams =
|
||||
layoutParams.apply {
|
||||
width = if (!isHorizontal) {
|
||||
min
|
||||
} else {
|
||||
max
|
||||
}
|
||||
height = if (!isHorizontal) {
|
||||
max
|
||||
} else {
|
||||
min
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
is HomeResultGridExpandedBinding -> {
|
||||
binding.backgroundCard.apply {
|
||||
val min = 114.toPx
|
||||
val max = 180.toPx
|
||||
|
||||
layoutParams =
|
||||
layoutParams.apply {
|
||||
width = if (!isHorizontal) {
|
||||
min
|
||||
} else {
|
||||
max
|
||||
}
|
||||
height = if (!isHorizontal) {
|
||||
max
|
||||
} else {
|
||||
min
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (position == 0) { // to fix tv
|
||||
binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchResultBuilder.bind(
|
||||
clickCallback,
|
||||
card,
|
||||
position,
|
||||
itemView,
|
||||
null, // nextFocusBehavior,
|
||||
nextFocusUp,
|
||||
nextFocusDown
|
||||
)
|
||||
itemView.tag = position
|
||||
|
||||
//val ani = ScaleAnimation(0.9f, 1.0f, 0.9f, 1f)
|
||||
//ani.fillAfter = true
|
||||
//ani.duration = 200
|
||||
//itemView.startAnimation(ani)
|
||||
}
|
||||
holder.itemView.tag = position
|
||||
}
|
||||
}
|
||||
|
||||
class HomeChildDiffCallback(
|
||||
private val oldList: List<SearchResponse>,
|
||||
private val newList: List<SearchResponse>
|
||||
) :
|
||||
DiffUtil.Callback() {
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
oldList[oldItemPosition].name == newList[newItemPosition].name
|
||||
|
||||
override fun getOldListSize() = oldList.size
|
||||
|
||||
override fun getNewListSize() = newList.size
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
oldList[oldItemPosition] == newList[newItemPosition] && oldItemPosition < oldList.size - 1 // always update the last item
|
||||
}
|
|
@ -42,8 +42,10 @@ import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLine
|
|||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.search.*
|
||||
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.ownHide
|
||||
|
@ -311,7 +313,7 @@ class HomeFragment : Fragment() {
|
|||
button?.isVisible = isValid
|
||||
button?.isChecked = isValid && selectedTypes.any { types.contains(it) }
|
||||
button?.isFocusable = true
|
||||
if (isTrueTvSettings()) {
|
||||
if (isLayout(TV)) {
|
||||
button?.isFocusableInTouchMode = true
|
||||
}
|
||||
|
||||
|
@ -435,7 +437,7 @@ class HomeFragment : Fragment() {
|
|||
|
||||
bottomSheetDialog?.ownShow()
|
||||
val layout =
|
||||
if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home
|
||||
if (isLayout(TV or EMULATOR)) R.layout.fragment_home_tv else R.layout.fragment_home
|
||||
val root = inflater.inflate(layout, container, false)
|
||||
binding = try {
|
||||
FragmentHomeBinding.bind(root)
|
||||
|
@ -449,6 +451,7 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
||||
bottomSheetDialog?.ownHide()
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
|
@ -485,6 +488,10 @@ class HomeFragment : Fragment() {
|
|||
|
||||
private var bottomSheetDialog: BottomSheetDialog? = null
|
||||
|
||||
// https://github.com/vivchar/RendererRecyclerViewAdapter/blob/185251ee9d94fb6eb3e063b00d646b745186c365/example/src/main/java/com/github/vivchar/example/pages/github/GithubFragment.kt#L32
|
||||
// cry about it, but this is android we are talking about, we cant do the most simple shit without making a global variable
|
||||
private var instanceState: Bundle = Bundle()
|
||||
private var homeMasterAdapter: HomeParentItemAdapterPreview? = null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -505,15 +512,14 @@ class HomeFragment : Fragment() {
|
|||
activity.loadSearchResult(listHomepageItems.random())
|
||||
}
|
||||
}
|
||||
|
||||
homeMasterRecycler.adapter =
|
||||
HomeParentItemAdapterPreview(
|
||||
mutableListOf(),
|
||||
homeViewModel
|
||||
)
|
||||
homeMasterAdapter = HomeParentItemAdapterPreview(
|
||||
fragment = this@HomeFragment,
|
||||
homeViewModel,
|
||||
)
|
||||
homeMasterRecycler.adapter = homeMasterAdapter
|
||||
//fixPaddingStatusbar(homeLoadingStatusbar)
|
||||
|
||||
homeApiFab.isVisible = !isTvSettings()
|
||||
homeApiFab.isVisible = isLayout(PHONE)
|
||||
|
||||
homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
|
@ -521,7 +527,7 @@ class HomeFragment : Fragment() {
|
|||
homeApiFab.shrink() // hide
|
||||
homeRandom.shrink()
|
||||
} else if (dy < -5) {
|
||||
if (!isTvSettings()) {
|
||||
if (isLayout(PHONE)) {
|
||||
homeApiFab.extend() // show
|
||||
homeRandom.extend()
|
||||
}
|
||||
|
@ -540,7 +546,7 @@ class HomeFragment : Fragment() {
|
|||
settingsManager.getBoolean(
|
||||
getString(R.string.random_button_key),
|
||||
false
|
||||
) && !isTvSettings()
|
||||
) && isLayout(PHONE)
|
||||
binding?.homeRandom?.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
@ -560,10 +566,11 @@ class HomeFragment : Fragment() {
|
|||
val mutableListOfResponse = mutableListOf<SearchResponse>()
|
||||
listHomepageItems.clear()
|
||||
|
||||
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(
|
||||
d.values.toMutableList(),
|
||||
homeMasterRecycler
|
||||
)
|
||||
(homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(d.values.map {
|
||||
it.copy(
|
||||
list = it.list.copy(list = it.list.list.toMutableList())
|
||||
)
|
||||
}.toMutableList())
|
||||
|
||||
homeLoading.isVisible = false
|
||||
homeLoadingError.isVisible = false
|
||||
|
@ -612,7 +619,7 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
is Resource.Loading -> {
|
||||
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(listOf())
|
||||
(homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(listOf())
|
||||
homeLoadingShimmer.startShimmer()
|
||||
homeLoading.isVisible = true
|
||||
homeLoadingError.isVisible = false
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
package com.lagradost.cloudstream3.ui.home
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.lagradost.cloudstream3.HomePageList
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.BaseAdapter
|
||||
import com.lagradost.cloudstream3.ui.BaseDiffCallback
|
||||
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
|
||||
|
||||
class LoadClickCallback(
|
||||
|
@ -27,193 +32,89 @@ class LoadClickCallback(
|
|||
)
|
||||
|
||||
open class ParentItemAdapter(
|
||||
private var items: MutableList<HomeViewModel.ExpandableHomepageList>,
|
||||
//private val viewModel: HomeViewModel,
|
||||
open val fragment: Fragment,
|
||||
id: Int,
|
||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
|
||||
private val expandCallback: ((String) -> Unit)? = null,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
||||
val layoutResId = when {
|
||||
isTrueTvSettings() -> R.layout.homepage_parent_tv
|
||||
parent.context.isEmulatorSettings() -> R.layout.homepage_parent_emulator
|
||||
else -> R.layout.homepage_parent
|
||||
}
|
||||
|
||||
val root = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
|
||||
|
||||
val binding = HomepageParentBinding.bind(root)
|
||||
|
||||
return ParentViewHolder(
|
||||
binding,
|
||||
clickCallback,
|
||||
moreInfoClickCallback,
|
||||
expandCallback
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is ParentViewHolder -> {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return items[position].list.name.hashCode().toLong()
|
||||
}
|
||||
|
||||
@JvmName("updateListHomePageList")
|
||||
fun updateList(newList: List<HomePageList>) {
|
||||
updateList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
|
||||
.toMutableList())
|
||||
}
|
||||
|
||||
@JvmName("updateListExpandableHomepageList")
|
||||
fun updateList(
|
||||
newList: MutableList<HomeViewModel.ExpandableHomepageList>,
|
||||
recyclerView: RecyclerView? = null
|
||||
) {
|
||||
// this
|
||||
// 1. prevents deep copy that makes this.items == newList
|
||||
// 2. filters out undesirable results
|
||||
// 3. moves empty results to the bottom (sortedBy is a stable sort)
|
||||
val new =
|
||||
newList.map { it.copy(list = it.list.copy(list = it.list.list.filterSearchResponse())) }
|
||||
.sortedBy { it.list.list.isEmpty() }
|
||||
|
||||
val diffResult = DiffUtil.calculateDiff(
|
||||
SearchDiffCallback(items, new)
|
||||
)
|
||||
items.clear()
|
||||
items.addAll(new)
|
||||
|
||||
//val mAdapter = this
|
||||
val delta = if (this@ParentItemAdapter is HomeParentItemAdapterPreview) {
|
||||
headItems
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
//notifyItemRangeChanged(position + delta, count)
|
||||
notifyItemRangeInserted(position + delta, count)
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
notifyItemRangeRemoved(position + delta, count)
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
notifyItemMoved(fromPosition + delta, toPosition + delta)
|
||||
}
|
||||
|
||||
override fun onChanged(_position: Int, count: Int, payload: Any?) {
|
||||
|
||||
val position = _position + delta
|
||||
|
||||
// I know kinda messy, what this does is using the update or bind instead of onCreateViewHolder -> bind
|
||||
recyclerView?.apply {
|
||||
// this loops every viewHolder in the recycle view and checks the position to see if it is within the update range
|
||||
val missingUpdates = (position until (position + count)).toMutableSet()
|
||||
for (i in 0 until itemCount) {
|
||||
val child = getChildAt(i) ?: continue
|
||||
val viewHolder = getChildViewHolder(child) ?: continue
|
||||
if (viewHolder !is ParentViewHolder) continue
|
||||
|
||||
val absolutePosition = viewHolder.bindingAdapterPosition
|
||||
if (absolutePosition >= position && absolutePosition < position + count) {
|
||||
val expand = items.getOrNull(absolutePosition - delta) ?: continue
|
||||
missingUpdates -= absolutePosition
|
||||
//println("Updating ${viewHolder.title.text} ($absolutePosition $position) -> ${expand.list.name}")
|
||||
if (viewHolder.title.text == expand.list.name) {
|
||||
viewHolder.update(expand)
|
||||
} else {
|
||||
viewHolder.bind(expand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// just in case some item did not get updated
|
||||
for (i in missingUpdates) {
|
||||
notifyItemChanged(i, payload)
|
||||
}
|
||||
} ?: run {
|
||||
// in case we don't have a nice
|
||||
notifyItemRangeChanged(position, count, payload)
|
||||
}
|
||||
}
|
||||
) : BaseAdapter<HomeViewModel.ExpandableHomepageList, Bundle>(
|
||||
fragment,
|
||||
id,
|
||||
diffCallback = BaseDiffCallback(
|
||||
itemSame = { a, b -> a.list.name == b.list.name },
|
||||
contentSame = { a, b ->
|
||||
a.list.list == b.list.list
|
||||
})
|
||||
|
||||
//diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
class ParentViewHolder
|
||||
constructor(
|
||||
val binding: HomepageParentBinding,
|
||||
// val viewModel: HomeViewModel,
|
||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
|
||||
private val expandCallback: ((String) -> Unit)? = null,
|
||||
) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
val title: TextView = binding.homeChildMoreInfo
|
||||
private val recyclerView: RecyclerView = binding.homeChildRecyclerview
|
||||
private val startFocus = R.id.nav_rail_view
|
||||
private val endFocus = FOCUS_SELF
|
||||
fun update(expand: HomeViewModel.ExpandableHomepageList) {
|
||||
val info = expand.list
|
||||
(recyclerView.adapter as? HomeChildItemAdapter?)?.apply {
|
||||
updateList(info.list.toMutableList())
|
||||
hasNext = expand.hasNext
|
||||
} ?: run {
|
||||
recyclerView.adapter = HomeChildItemAdapter(
|
||||
info.list.toMutableList(),
|
||||
clickCallback = clickCallback,
|
||||
nextFocusUp = recyclerView.nextFocusUpId,
|
||||
nextFocusDown = recyclerView.nextFocusDownId,
|
||||
).apply {
|
||||
isHorizontal = info.isHorizontalImages
|
||||
hasNext = expand.hasNext
|
||||
}
|
||||
recyclerView.setLinearListLayout(
|
||||
isHorizontal = true,
|
||||
nextLeft = startFocus,
|
||||
nextRight = endFocus,
|
||||
)
|
||||
}
|
||||
) {
|
||||
data class ParentItemHolder(val binding: ViewBinding) : ViewHolderState<Bundle>(binding) {
|
||||
override fun save(): Bundle = Bundle().apply {
|
||||
val recyclerView = (binding as? HomepageParentBinding)?.homeChildRecyclerview
|
||||
putParcelable(
|
||||
"value",
|
||||
recyclerView?.layoutManager?.onSaveInstanceState()
|
||||
)
|
||||
(recyclerView?.adapter as? BaseAdapter<*,*>)?.save(recyclerView)
|
||||
}
|
||||
|
||||
fun bind(expand: HomeViewModel.ExpandableHomepageList) {
|
||||
val info = expand.list
|
||||
recyclerView.adapter = HomeChildItemAdapter(
|
||||
info.list.toMutableList(),
|
||||
override fun restore(state: Bundle) {
|
||||
(binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState(
|
||||
state.getParcelable("value")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun submitList(list: List<HomeViewModel.ExpandableHomepageList>?) {
|
||||
super.submitList(list?.sortedBy { it.list.list.isEmpty() })
|
||||
}
|
||||
|
||||
override fun onUpdateContent(
|
||||
holder: ViewHolderState<Bundle>,
|
||||
item: HomeViewModel.ExpandableHomepageList,
|
||||
position: Int
|
||||
) {
|
||||
val binding = holder.view
|
||||
if (binding !is HomepageParentBinding) return
|
||||
(binding.homeChildRecyclerview.adapter as? HomeChildItemAdapter)?.submitList(item.list.list)
|
||||
}
|
||||
|
||||
override fun onBindContent(
|
||||
holder: ViewHolderState<Bundle>,
|
||||
item: HomeViewModel.ExpandableHomepageList,
|
||||
position: Int
|
||||
) {
|
||||
val startFocus = R.id.nav_rail_view
|
||||
val endFocus = FOCUS_SELF
|
||||
val binding = holder.view
|
||||
if (binding !is HomepageParentBinding) return
|
||||
val info = item.list
|
||||
binding.apply {
|
||||
homeChildRecyclerview.adapter = HomeChildItemAdapter(
|
||||
fragment = fragment,
|
||||
id = id + position + 100,
|
||||
clickCallback = clickCallback,
|
||||
nextFocusUp = recyclerView.nextFocusUpId,
|
||||
nextFocusDown = recyclerView.nextFocusDownId,
|
||||
nextFocusUp = homeChildRecyclerview.nextFocusUpId,
|
||||
nextFocusDown = homeChildRecyclerview.nextFocusDownId,
|
||||
).apply {
|
||||
isHorizontal = info.isHorizontalImages
|
||||
hasNext = expand.hasNext
|
||||
hasNext = item.hasNext
|
||||
submitList(item.list.list)
|
||||
}
|
||||
recyclerView.setLinearListLayout(
|
||||
homeChildRecyclerview.setLinearListLayout(
|
||||
isHorizontal = true,
|
||||
nextLeft = startFocus,
|
||||
nextRight = endFocus,
|
||||
)
|
||||
title.text = info.name
|
||||
homeChildMoreInfo.text = info.name
|
||||
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
homeChildRecyclerview.addOnScrollListener(object :
|
||||
RecyclerView.OnScrollListener() {
|
||||
var expandCount = 0
|
||||
val name = expand.list.name
|
||||
val name = item.list.name
|
||||
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
override fun onScrollStateChanged(
|
||||
recyclerView: RecyclerView,
|
||||
newState: Int
|
||||
) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
|
||||
val adapter = recyclerView.adapter
|
||||
|
@ -237,27 +138,35 @@ open class ParentItemAdapter(
|
|||
})
|
||||
|
||||
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
|
||||
if (!isTrueTvSettings()) {
|
||||
title.setOnClickListener {
|
||||
moreInfoClickCallback.invoke(expand)
|
||||
if (isLayout(PHONE)) {
|
||||
homeChildMoreInfo.setOnClickListener {
|
||||
moreInfoClickCallback.invoke(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SearchDiffCallback(
|
||||
private val oldList: List<HomeViewModel.ExpandableHomepageList>,
|
||||
private val newList: List<HomeViewModel.ExpandableHomepageList>
|
||||
) :
|
||||
DiffUtil.Callback() {
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
oldList[oldItemPosition].list.name == newList[newItemPosition].list.name
|
||||
override fun onCreateContent(parent: ViewGroup): ParentItemHolder {
|
||||
val layoutResId = when {
|
||||
isLayout(TV) -> R.layout.homepage_parent_tv
|
||||
isLayout(EMULATOR) -> R.layout.homepage_parent_emulator
|
||||
else -> R.layout.homepage_parent
|
||||
}
|
||||
|
||||
override fun getOldListSize() = oldList.size
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val binding = try {
|
||||
HomepageParentBinding.bind(inflater.inflate(layoutResId, parent, false))
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
// just in case someone forgot we don't want to crash
|
||||
HomepageParentBinding.inflate(inflater)
|
||||
}
|
||||
|
||||
override fun getNewListSize() = newList.size
|
||||
return ParentItemHolder(binding)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||
oldList[oldItemPosition] == newList[newItemPosition]
|
||||
fun updateList(newList: List<HomePageList>) {
|
||||
submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
|
||||
.toMutableList())
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package com.lagradost.cloudstream3.ui.home
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -7,6 +9,7 @@ import androidx.appcompat.widget.SearchView
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
@ -26,6 +29,7 @@ import com.lagradost.cloudstream3.databinding.FragmentHomeHeadTvBinding
|
|||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.debugException
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear
|
||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage
|
||||
|
@ -36,8 +40,9 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
|||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes
|
||||
|
@ -46,113 +51,87 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.populateChips
|
||||
|
||||
class HomeParentItemAdapterPreview(
|
||||
items: MutableList<HomeViewModel.ExpandableHomepageList>,
|
||||
override val fragment: Fragment,
|
||||
private val viewModel: HomeViewModel,
|
||||
) : ParentItemAdapter(items, clickCallback = {
|
||||
viewModel.click(it)
|
||||
}, moreInfoClickCallback = {
|
||||
viewModel.popup(it)
|
||||
}, expandCallback = {
|
||||
viewModel.expand(it)
|
||||
}) {
|
||||
val headItems = 1
|
||||
) : ParentItemAdapter(fragment, id = "HomeParentItemAdapterPreview".hashCode(),
|
||||
clickCallback = {
|
||||
viewModel.click(it)
|
||||
}, moreInfoClickCallback = {
|
||||
viewModel.popup(it)
|
||||
}, expandCallback = {
|
||||
viewModel.expand(it)
|
||||
}) {
|
||||
override val headers = 1
|
||||
override fun onCreateHeader(parent: ViewGroup): ViewHolderState<Bundle> {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val binding = if (isLayout(TV or EMULATOR)) FragmentHomeHeadTvBinding.inflate(
|
||||
inflater,
|
||||
parent,
|
||||
false
|
||||
) else FragmentHomeHeadBinding.inflate(inflater, parent, false)
|
||||
|
||||
companion object {
|
||||
private const val VIEW_TYPE_HEADER = 2
|
||||
private const val VIEW_TYPE_ITEM = 1
|
||||
}
|
||||
if (binding is FragmentHomeHeadTvBinding && isLayout(EMULATOR)) {
|
||||
binding.homeBookmarkParentItemMoreInfo.isVisible = true
|
||||
|
||||
override fun getItemViewType(position: Int) = when (position) {
|
||||
0 -> VIEW_TYPE_HEADER
|
||||
else -> VIEW_TYPE_ITEM
|
||||
}
|
||||
val marginInDp = 50
|
||||
val density = binding.horizontalScrollChips.context.resources.displayMetrics.density
|
||||
val marginInPixels = (marginInDp * density).toInt()
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is HeaderViewHolder -> {}
|
||||
else -> super.onBindViewHolder(holder, position - headItems)
|
||||
val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams
|
||||
params.marginEnd = marginInPixels
|
||||
binding.horizontalScrollChips.layoutParams = params
|
||||
binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds(
|
||||
null,
|
||||
null,
|
||||
ContextCompat.getDrawable(
|
||||
parent.context,
|
||||
R.drawable.ic_baseline_arrow_forward_24
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
return HeaderViewHolder(binding, viewModel, fragment = fragment)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_HEADER -> {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val binding = if (isTvSettings()) FragmentHomeHeadTvBinding.inflate(
|
||||
inflater,
|
||||
parent,
|
||||
false
|
||||
) else FragmentHomeHeadBinding.inflate(inflater, parent, false)
|
||||
override fun onBindHeader(holder: ViewHolderState<Bundle>) {
|
||||
(holder as? HeaderViewHolder)?.bind()
|
||||
}
|
||||
|
||||
if (binding is FragmentHomeHeadTvBinding && parent.context.isEmulatorSettings()) {
|
||||
binding.homeBookmarkParentItemMoreInfo.isVisible = true
|
||||
private class HeaderViewHolder(
|
||||
val binding: ViewBinding, val viewModel: HomeViewModel, fragment: Fragment,
|
||||
) :
|
||||
ViewHolderState<Bundle>(binding) {
|
||||
|
||||
val marginInDp = 50
|
||||
val density = binding.horizontalScrollChips.context.resources.displayMetrics.density
|
||||
val marginInPixels = (marginInDp * density).toInt()
|
||||
|
||||
val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams
|
||||
params.marginEnd = marginInPixels
|
||||
binding.horizontalScrollChips.layoutParams = params
|
||||
binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds(
|
||||
null,
|
||||
null,
|
||||
ContextCompat.getDrawable(
|
||||
parent.context,
|
||||
R.drawable.ic_baseline_arrow_forward_24
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
HeaderViewHolder(
|
||||
binding,
|
||||
viewModel,
|
||||
override fun save(): Bundle =
|
||||
Bundle().apply {
|
||||
putParcelable(
|
||||
"resumeRecyclerView",
|
||||
resumeRecyclerView.layoutManager?.onSaveInstanceState()
|
||||
)
|
||||
putParcelable(
|
||||
"bookmarkRecyclerView",
|
||||
bookmarkRecyclerView.layoutManager?.onSaveInstanceState()
|
||||
)
|
||||
//putInt("previewViewpager", previewViewpager.currentItem)
|
||||
}
|
||||
|
||||
VIEW_TYPE_ITEM -> super.onCreateViewHolder(parent, viewType)
|
||||
else -> error("Unhandled viewType=$viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return super.getItemCount() + headItems
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
if (position == 0) return 0//previewData.hashCode().toLong()
|
||||
return super.getItemId(position - headItems)
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
||||
when (holder) {
|
||||
is HeaderViewHolder -> {
|
||||
holder.onViewDetachedFromWindow()
|
||||
override fun restore(state: Bundle) {
|
||||
state.getParcelable<Parcelable>("resumeRecyclerView")?.let { recycle ->
|
||||
resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
|
||||
}
|
||||
|
||||
else -> super.onViewDetachedFromWindow(holder)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
|
||||
when (holder) {
|
||||
is HeaderViewHolder -> {
|
||||
holder.onViewAttachedToWindow()
|
||||
state.getParcelable<Parcelable>("bookmarkRecyclerView")?.let { recycle ->
|
||||
bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
|
||||
}
|
||||
|
||||
else -> super.onViewAttachedToWindow(holder)
|
||||
//state.getInt("previewViewpager").let { recycle ->
|
||||
// previewViewpager.setCurrentItem(recycle,true)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
class HeaderViewHolder
|
||||
constructor(
|
||||
val binding: ViewBinding,
|
||||
val viewModel: HomeViewModel,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
private var previewAdapter: HomeScrollAdapter = HomeScrollAdapter()
|
||||
private var resumeAdapter: HomeChildItemAdapter = HomeChildItemAdapter(
|
||||
ArrayList(),
|
||||
val previewAdapter = HomeScrollAdapter(fragment = fragment)
|
||||
private val resumeAdapter = HomeChildItemAdapter(
|
||||
fragment,
|
||||
id = "resumeAdapter".hashCode(),
|
||||
nextFocusUp = itemView.nextFocusUpId,
|
||||
nextFocusDown = itemView.nextFocusDownId
|
||||
) { callback ->
|
||||
|
@ -207,8 +186,9 @@ class HomeParentItemAdapterPreview(
|
|||
}
|
||||
}
|
||||
}
|
||||
private var bookmarkAdapter: HomeChildItemAdapter = HomeChildItemAdapter(
|
||||
ArrayList(),
|
||||
private val bookmarkAdapter = HomeChildItemAdapter(
|
||||
fragment,
|
||||
id = "bookmarkAdapter".hashCode(),
|
||||
nextFocusUp = itemView.nextFocusUpId,
|
||||
nextFocusDown = itemView.nextFocusDownId
|
||||
) { callback ->
|
||||
|
@ -217,7 +197,10 @@ class HomeParentItemAdapterPreview(
|
|||
return@HomeChildItemAdapter
|
||||
}
|
||||
|
||||
(callback.view.context?.getActivity() as? MainActivity)?.loadPopup(callback.card, load = false)
|
||||
(callback.view.context?.getActivity() as? MainActivity)?.loadPopup(
|
||||
callback.card,
|
||||
load = false
|
||||
)
|
||||
/*
|
||||
callback.view.context?.getActivity()?.showOptionSelectStringRes(
|
||||
callback.view,
|
||||
|
@ -267,7 +250,6 @@ class HomeParentItemAdapterPreview(
|
|||
*/
|
||||
}
|
||||
|
||||
|
||||
private val previewViewpager: ViewPager2 =
|
||||
itemView.findViewById(R.id.home_preview_viewpager)
|
||||
|
||||
|
@ -275,38 +257,24 @@ class HomeParentItemAdapterPreview(
|
|||
itemView.findViewById(R.id.home_preview_viewpager_text)
|
||||
|
||||
// private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview)
|
||||
private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder)
|
||||
private var resumeRecyclerView: RecyclerView =
|
||||
private val resumeHolder: View = itemView.findViewById(R.id.home_watch_holder)
|
||||
private val resumeRecyclerView: RecyclerView =
|
||||
itemView.findViewById(R.id.home_watch_child_recyclerview)
|
||||
private var bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder)
|
||||
private var bookmarkRecyclerView: RecyclerView =
|
||||
private val bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder)
|
||||
private val bookmarkRecyclerView: RecyclerView =
|
||||
itemView.findViewById(R.id.home_bookmarked_child_recyclerview)
|
||||
|
||||
private var homeAccount: View? =
|
||||
itemView.findViewById(R.id.home_preview_switch_account)
|
||||
private var alternativeHomeAccount: View? =
|
||||
private val homeAccount: View? = itemView.findViewById(R.id.home_preview_switch_account)
|
||||
private val alternativeHomeAccount: View? =
|
||||
itemView.findViewById(R.id.alternative_switch_account)
|
||||
|
||||
private var topPadding: View? = itemView.findViewById(R.id.home_padding)
|
||||
private val topPadding: View? = itemView.findViewById(R.id.home_padding)
|
||||
|
||||
private var alternativeAccountPadding: View? = itemView.findViewById(R.id.alternative_account_padding)
|
||||
private val alternativeAccountPadding: View? =
|
||||
itemView.findViewById(R.id.alternative_account_padding)
|
||||
|
||||
private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding)
|
||||
|
||||
private val previewCallback: ViewPager2.OnPageChangeCallback =
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
previewAdapter.apply {
|
||||
if (position >= itemCount - 1 && hasMoreItems) {
|
||||
hasMoreItems = false // don't make two requests
|
||||
viewModel.loadMoreHomeScrollResponses()
|
||||
}
|
||||
}
|
||||
val item = previewAdapter.getItem(position) ?: return
|
||||
onSelect(item, position)
|
||||
}
|
||||
}
|
||||
|
||||
fun onSelect(item: LoadResponse, position: Int) {
|
||||
(binding as? FragmentHomeHeadTvBinding)?.apply {
|
||||
homePreviewDescription.isGone =
|
||||
|
@ -379,14 +347,14 @@ class HomeParentItemAdapterPreview(
|
|||
|
||||
homePreviewBookmark.setOnClickListener { fab ->
|
||||
fab.context.getActivity()?.showBottomDialog(
|
||||
WatchType.values()
|
||||
WatchType.entries
|
||||
.map { fab.context.getString(it.stringRes) }
|
||||
.toList(),
|
||||
DataStoreHelper.getResultWatchState(id).ordinal,
|
||||
fab.context.getString(R.string.action_add_to_bookmarks),
|
||||
showApply = false,
|
||||
{}) {
|
||||
val newValue = WatchType.values()[it]
|
||||
val newValue = WatchType.entries[it]
|
||||
|
||||
ResultViewModel2().updateWatchStatus(
|
||||
newValue,
|
||||
|
@ -411,38 +379,22 @@ class HomeParentItemAdapterPreview(
|
|||
}
|
||||
}
|
||||
|
||||
fun onViewDetachedFromWindow() {
|
||||
previewViewpager.unregisterOnPageChangeCallback(previewCallback)
|
||||
}
|
||||
|
||||
fun onViewAttachedToWindow() {
|
||||
previewViewpager.registerOnPageChangeCallback(previewCallback)
|
||||
|
||||
binding.root.findViewTreeLifecycleOwner()?.apply {
|
||||
observe(viewModel.preview) {
|
||||
updatePreview(it)
|
||||
}
|
||||
if (binding is FragmentHomeHeadTvBinding) {
|
||||
observe(viewModel.apiName) { name ->
|
||||
binding.homePreviewChangeApi.text = name
|
||||
}
|
||||
}
|
||||
observe(viewModel.resumeWatching) {
|
||||
updateResume(it)
|
||||
}
|
||||
observe(viewModel.bookmarks) {
|
||||
updateBookmarks(it)
|
||||
}
|
||||
observe(viewModel.availableWatchStatusTypes) { (checked, visible) ->
|
||||
for ((chip, watch) in toggleList) {
|
||||
chip.apply {
|
||||
isVisible = visible.contains(watch)
|
||||
isChecked = checked.contains(watch)
|
||||
private val previewCallback: ViewPager2.OnPageChangeCallback =
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
previewAdapter.apply {
|
||||
if (position >= itemCount - 1 && hasMoreItems) {
|
||||
hasMoreItems = false // don't make two requests
|
||||
viewModel.loadMoreHomeScrollResponses()
|
||||
}
|
||||
}
|
||||
toggleListHolder?.isGone = visible.isEmpty()
|
||||
val item = previewAdapter.getItemOrNull(position) ?: return
|
||||
onSelect(item, position)
|
||||
}
|
||||
} ?: debugException { "Expected findViewTreeLifecycleOwner" }
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow() {
|
||||
previewViewpager.unregisterOnPageChangeCallback(previewCallback)
|
||||
}
|
||||
|
||||
private val toggleList = listOf<Pair<Chip, WatchType>>(
|
||||
|
@ -455,6 +407,8 @@ class HomeParentItemAdapterPreview(
|
|||
|
||||
private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder)
|
||||
|
||||
fun bind() = Unit
|
||||
|
||||
init {
|
||||
previewViewpager.setPageTransformer(HomeScrollTransformer())
|
||||
|
||||
|
@ -561,7 +515,9 @@ class HomeParentItemAdapterPreview(
|
|||
|
||||
when (preview) {
|
||||
is Resource.Success -> {
|
||||
if (!previewAdapter.setItems(
|
||||
previewAdapter.submitList(preview.value.second)
|
||||
previewAdapter.hasMoreItems = preview.value.first
|
||||
/*if (!.setItems(
|
||||
preview.value.second,
|
||||
preview.value.first
|
||||
)
|
||||
|
@ -573,15 +529,16 @@ class HomeParentItemAdapterPreview(
|
|||
previewViewpager.fakeDragBy(1f)
|
||||
previewViewpager.endFakeDrag()
|
||||
previewCallback.onPageSelected(0)
|
||||
previewViewpager.isVisible = true
|
||||
previewViewpagerText.isVisible = true
|
||||
alternativeAccountPadding?.isVisible = false
|
||||
//previewHeader.isVisible = true
|
||||
}
|
||||
}*/
|
||||
|
||||
previewViewpager.isVisible = true
|
||||
previewViewpagerText.isVisible = true
|
||||
alternativeAccountPadding?.isVisible = false
|
||||
}
|
||||
|
||||
else -> {
|
||||
previewAdapter.setItems(listOf(), false)
|
||||
previewAdapter.submitList(listOf())
|
||||
previewViewpager.setCurrentItem(0, false)
|
||||
previewViewpager.isVisible = false
|
||||
previewViewpagerText.isVisible = false
|
||||
|
@ -593,12 +550,12 @@ class HomeParentItemAdapterPreview(
|
|||
|
||||
private fun updateResume(resumeWatching: List<SearchResponse>) {
|
||||
resumeHolder.isVisible = resumeWatching.isNotEmpty()
|
||||
resumeAdapter.updateList(resumeWatching)
|
||||
resumeAdapter.submitList(resumeWatching)
|
||||
|
||||
if (
|
||||
binding is FragmentHomeHeadBinding ||
|
||||
binding is FragmentHomeHeadTvBinding &&
|
||||
binding.root.context.isEmulatorSettings()
|
||||
isLayout(EMULATOR)
|
||||
) {
|
||||
val title = (binding as? FragmentHomeHeadBinding)?.homeWatchParentItemTitle
|
||||
?: (binding as? FragmentHomeHeadTvBinding)?.homeWatchParentItemTitle
|
||||
|
@ -623,12 +580,12 @@ class HomeParentItemAdapterPreview(
|
|||
private fun updateBookmarks(data: Pair<Boolean, List<SearchResponse>>) {
|
||||
val (visible, list) = data
|
||||
bookmarkHolder.isVisible = visible
|
||||
bookmarkAdapter.updateList(list)
|
||||
bookmarkAdapter.submitList(list)
|
||||
|
||||
if (
|
||||
binding is FragmentHomeHeadBinding ||
|
||||
binding is FragmentHomeHeadTvBinding &&
|
||||
binding.root.context.isEmulatorSettings()
|
||||
isLayout(EMULATOR)
|
||||
) {
|
||||
val title = (binding as? FragmentHomeHeadBinding)?.homeBookmarkParentItemTitle
|
||||
?: (binding as? FragmentHomeHeadTvBinding)?.homeBookmarkParentItemTitle
|
||||
|
@ -653,5 +610,35 @@ class HomeParentItemAdapterPreview(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow() {
|
||||
previewViewpager.registerOnPageChangeCallback(previewCallback)
|
||||
|
||||
binding.root.findViewTreeLifecycleOwner()?.apply {
|
||||
observe(viewModel.preview) {
|
||||
updatePreview(it)
|
||||
}
|
||||
if (binding is FragmentHomeHeadTvBinding) {
|
||||
observe(viewModel.apiName) { name ->
|
||||
binding.homePreviewChangeApi.text = name
|
||||
}
|
||||
}
|
||||
observe(viewModel.resumeWatching) {
|
||||
updateResume(it)
|
||||
}
|
||||
observe(viewModel.bookmarks) {
|
||||
updateBookmarks(it)
|
||||
}
|
||||
observe(viewModel.availableWatchStatusTypes) { (checked, visible) ->
|
||||
for ((chip, watch) in toggleList) {
|
||||
chip.apply {
|
||||
isVisible = visible.contains(watch)
|
||||
isChecked = checked.contains(watch)
|
||||
}
|
||||
}
|
||||
toggleListHolder?.isGone = visible.isEmpty()
|
||||
}
|
||||
} ?: debugException { "Expected findViewTreeLifecycleOwner" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,112 +4,61 @@ import android.content.res.Configuration
|
|||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.NoStateAdapter
|
||||
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
|
||||
class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private var items: MutableList<LoadResponse> = mutableListOf()
|
||||
class HomeScrollAdapter(
|
||||
fragment: Fragment
|
||||
) : NoStateAdapter<LoadResponse>(fragment) {
|
||||
var hasMoreItems: Boolean = false
|
||||
|
||||
fun getItem(position: Int): LoadResponse? {
|
||||
return items.getOrNull(position)
|
||||
}
|
||||
|
||||
fun setItems(newItems: List<LoadResponse>, hasNext: Boolean): Boolean {
|
||||
val isSame = newItems.firstOrNull()?.url == items.firstOrNull()?.url
|
||||
hasMoreItems = hasNext
|
||||
|
||||
val diffResult = DiffUtil.calculateDiff(
|
||||
HomeScrollDiffCallback(this.items, newItems)
|
||||
)
|
||||
|
||||
items.clear()
|
||||
items.addAll(newItems)
|
||||
|
||||
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
|
||||
return isSame
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Any> {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val binding = if (isTvSettings()) {
|
||||
val binding = if (isLayout(TV or EMULATOR)) {
|
||||
HomeScrollViewTvBinding.inflate(inflater, parent, false)
|
||||
} else {
|
||||
HomeScrollViewBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
return CardViewHolder(
|
||||
binding,
|
||||
//forceHorizontalPosters
|
||||
)
|
||||
return ViewHolderState(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is CardViewHolder -> {
|
||||
holder.bind(items[position])
|
||||
override fun onBindContent(
|
||||
holder: ViewHolderState<Any>,
|
||||
item: LoadResponse,
|
||||
position: Int,
|
||||
) {
|
||||
val binding = holder.view
|
||||
val itemView = holder.itemView
|
||||
val isHorizontal =
|
||||
binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
|
||||
val posterUrl =
|
||||
if (isHorizontal) item.backgroundPosterUrl ?: item.posterUrl else item.posterUrl
|
||||
?: item.backgroundPosterUrl
|
||||
|
||||
when (binding) {
|
||||
is HomeScrollViewBinding -> {
|
||||
binding.homeScrollPreview.setImage(posterUrl)
|
||||
binding.homeScrollPreviewTags.apply {
|
||||
text = item.tags?.joinToString(" • ") ?: ""
|
||||
isGone = item.tags.isNullOrEmpty()
|
||||
maxLines = 2
|
||||
}
|
||||
binding.homeScrollPreviewTitle.text = item.name
|
||||
}
|
||||
|
||||
is HomeScrollViewTvBinding -> {
|
||||
binding.homeScrollPreview.setImage(posterUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CardViewHolder
|
||||
constructor(
|
||||
val binding: ViewBinding,
|
||||
//private val forceHorizontalPosters: Boolean? = null
|
||||
) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(card: LoadResponse) {
|
||||
val isHorizontal =
|
||||
binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
|
||||
val posterUrl =
|
||||
if (isHorizontal) card.backgroundPosterUrl ?: card.posterUrl else card.posterUrl
|
||||
?: card.backgroundPosterUrl
|
||||
|
||||
when (binding) {
|
||||
is HomeScrollViewBinding -> {
|
||||
binding.homeScrollPreview.setImage(posterUrl)
|
||||
binding.homeScrollPreviewTags.apply {
|
||||
text = card.tags?.joinToString(" • ") ?: ""
|
||||
isGone = card.tags.isNullOrEmpty()
|
||||
maxLines = 2
|
||||
}
|
||||
binding.homeScrollPreviewTitle.text = card.name
|
||||
}
|
||||
|
||||
is HomeScrollViewTvBinding -> {
|
||||
binding.homeScrollPreview.setImage(posterUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HomeScrollDiffCallback(
|
||||
private val oldList: List<LoadResponse>,
|
||||
private val newList: List<LoadResponse>
|
||||
) :
|
||||
DiffUtil.Callback() {
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
oldList[oldItemPosition].url == newList[newItemPosition].url
|
||||
|
||||
override fun getOldListSize() = oldList.size
|
||||
|
||||
override fun getNewListSize() = newList.size
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
oldList[oldItemPosition] == newList[newItemPosition]
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
}
|
|
@ -34,7 +34,8 @@ import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
|
|||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
|
@ -52,6 +53,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.EnumSet
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import kotlin.collections.set
|
||||
|
||||
class HomeViewModel : ViewModel() {
|
||||
|
@ -124,7 +126,7 @@ class HomeViewModel : ViewModel() {
|
|||
|
||||
private val _resumeWatching = MutableLiveData<List<SearchResponse>>()
|
||||
private val _preview = MutableLiveData<Resource<Pair<Boolean, List<LoadResponse>>>>()
|
||||
private val previewResponses = mutableListOf<LoadResponse>()
|
||||
private val previewResponses = CopyOnWriteArrayList<LoadResponse>()
|
||||
private val previewResponsesAdded = mutableSetOf<String>()
|
||||
|
||||
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
|
||||
|
@ -132,7 +134,7 @@ class HomeViewModel : ViewModel() {
|
|||
|
||||
private fun loadResumeWatching() = viewModelScope.launchSafe {
|
||||
val resumeWatchingResult = getResumeWatching()
|
||||
if (isTrueTvSettings() && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (isLayout(TV) && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ioSafe {
|
||||
// this WILL crash on non tvs, so keep this inside a try catch
|
||||
activity?.addProgramsToContinueWatching(resumeWatchingResult)
|
||||
|
@ -326,7 +328,13 @@ class HomeViewModel : ViewModel() {
|
|||
val filteredList =
|
||||
context?.filterHomePageListByFilmQuality(list) ?: list
|
||||
expandable[list.name] =
|
||||
ExpandableHomepageList(filteredList, 1, home.hasNext)
|
||||
ExpandableHomepageList(
|
||||
filteredList.copy(
|
||||
list = CopyOnWriteArrayList(
|
||||
filteredList.list
|
||||
)
|
||||
), 1, home.hasNext
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -341,8 +349,7 @@ class HomeViewModel : ViewModel() {
|
|||
val currentList =
|
||||
items.shuffled().filter { it.list.isNotEmpty() }
|
||||
.flatMap { it.list }
|
||||
.distinctBy { it.url }
|
||||
.toList()
|
||||
.distinctBy { it.url }.toList()
|
||||
|
||||
if (currentList.isNotEmpty()) {
|
||||
val randomItems =
|
||||
|
|
|
@ -51,7 +51,10 @@ import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
|
|||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity
|
||||
|
@ -60,6 +63,7 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
||||
import org.checkerframework.framework.qual.Unused
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import kotlin.math.abs
|
||||
|
||||
const val LIBRARY_FOLDER = "library_folder"
|
||||
|
@ -104,7 +108,7 @@ class LibraryFragment : Fragment() {
|
|||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View {
|
||||
val layout =
|
||||
if (SettingsFragment.isTvSettings()) R.layout.fragment_library_tv else R.layout.fragment_library
|
||||
if (isLayout(TV or EMULATOR)) R.layout.fragment_library_tv else R.layout.fragment_library
|
||||
val root = inflater.inflate(layout, container, false)
|
||||
binding = try {
|
||||
FragmentLibraryBinding.bind(root)
|
||||
|
@ -224,7 +228,7 @@ class LibraryFragment : Fragment() {
|
|||
settingsManager.getBoolean(
|
||||
getString(R.string.random_button_key),
|
||||
false
|
||||
) && !SettingsFragment.isTvSettings()
|
||||
) && isLayout(PHONE)
|
||||
binding?.libraryRandom?.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
@ -232,7 +236,7 @@ class LibraryFragment : Fragment() {
|
|||
if (listLibraryItems.isNotEmpty()) {
|
||||
val listLibraryItem = listLibraryItems.random()
|
||||
libraryViewModel.currentSyncApi?.syncIdName?.let {
|
||||
loadLibraryItem(it, listLibraryItem.syncId,listLibraryItem)
|
||||
loadLibraryItem(it, listLibraryItem.syncId, listLibraryItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -311,44 +315,46 @@ class LibraryFragment : Fragment() {
|
|||
|
||||
binding?.viewpager?.setPageTransformer(LibraryScrollTransformer())
|
||||
|
||||
binding?.viewpager?.adapter =
|
||||
binding?.viewpager?.adapter ?: ViewpagerAdapter(
|
||||
mutableListOf(),
|
||||
{ isScrollingDown: Boolean ->
|
||||
if (isScrollingDown) {
|
||||
binding?.sortFab?.shrink()
|
||||
binding?.libraryRandom?.shrink()
|
||||
} else {
|
||||
binding?.sortFab?.extend()
|
||||
binding?.libraryRandom?.extend()
|
||||
}
|
||||
}) callback@{ searchClickCallback ->
|
||||
// To prevent future accidents
|
||||
debugAssert({
|
||||
searchClickCallback.card !is SyncAPI.LibraryItem
|
||||
}, {
|
||||
"searchClickCallback ${searchClickCallback.card} is not a LibraryItem"
|
||||
})
|
||||
binding?.viewpager?.adapter = ViewpagerAdapter(
|
||||
fragment = this,
|
||||
{ isScrollingDown: Boolean ->
|
||||
if (isScrollingDown) {
|
||||
binding?.sortFab?.shrink()
|
||||
binding?.libraryRandom?.shrink()
|
||||
} else {
|
||||
binding?.sortFab?.extend()
|
||||
binding?.libraryRandom?.extend()
|
||||
}
|
||||
}) callback@{ searchClickCallback ->
|
||||
// To prevent future accidents
|
||||
debugAssert({
|
||||
searchClickCallback.card !is SyncAPI.LibraryItem
|
||||
}, {
|
||||
"searchClickCallback ${searchClickCallback.card} is not a LibraryItem"
|
||||
})
|
||||
|
||||
val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId
|
||||
val syncName =
|
||||
libraryViewModel.currentSyncApi?.syncIdName ?: return@callback
|
||||
val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId
|
||||
val syncName =
|
||||
libraryViewModel.currentSyncApi?.syncIdName ?: return@callback
|
||||
|
||||
when (searchClickCallback.action) {
|
||||
SEARCH_ACTION_SHOW_METADATA -> {
|
||||
(activity as? MainActivity)?.loadPopup(searchClickCallback.card, load = false)
|
||||
when (searchClickCallback.action) {
|
||||
SEARCH_ACTION_SHOW_METADATA -> {
|
||||
(activity as? MainActivity)?.loadPopup(
|
||||
searchClickCallback.card,
|
||||
load = false
|
||||
)
|
||||
/*activity?.showPluginSelectionDialog(
|
||||
syncId,
|
||||
syncName,
|
||||
searchClickCallback.card.apiName
|
||||
)*/
|
||||
}
|
||||
}
|
||||
|
||||
SEARCH_ACTION_LOAD -> {
|
||||
loadLibraryItem(syncName, syncId, searchClickCallback.card)
|
||||
}
|
||||
SEARCH_ACTION_LOAD -> {
|
||||
loadLibraryItem(syncName, syncId, searchClickCallback.card)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding?.apply {
|
||||
viewpager.offscreenPageLimit = 2
|
||||
|
@ -394,7 +400,11 @@ class LibraryFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages
|
||||
(viewpager.adapter as? ViewpagerAdapter)?.submitList(pages.map {
|
||||
it.copy(
|
||||
items = CopyOnWriteArrayList(it.items)
|
||||
)
|
||||
})
|
||||
//fix focus on the viewpager itself
|
||||
(viewpager.getChildAt(0) as RecyclerView).apply {
|
||||
tag = "tv_no_focus_tag"
|
||||
|
@ -402,10 +412,10 @@ class LibraryFragment : Fragment() {
|
|||
}
|
||||
|
||||
// Using notifyItemRangeChanged keeps the animations when sorting
|
||||
viewpager.adapter?.notifyItemRangeChanged(
|
||||
/*viewpager.adapter?.notifyItemRangeChanged(
|
||||
0,
|
||||
viewpager.adapter?.itemCount ?: 0
|
||||
)
|
||||
)*/
|
||||
|
||||
libraryViewModel.currentPage.value?.let { page ->
|
||||
binding?.viewpager?.setCurrentItem(page, false)
|
||||
|
@ -463,12 +473,14 @@ class LibraryFragment : Fragment() {
|
|||
}
|
||||
}.attach()
|
||||
|
||||
binding?.libraryTabLayout?.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener {
|
||||
binding?.libraryTabLayout?.addOnTabSelectedListener(object :
|
||||
TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
binding?.libraryTabLayout?.selectedTabPosition?.let { page ->
|
||||
libraryViewModel.switchPage(page)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
|
||||
})
|
||||
|
@ -568,8 +580,9 @@ class LibraryFragment : Fragment() {
|
|||
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
(binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind()
|
||||
binding?.viewpager?.adapter?.notifyDataSetChanged()
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
|
||||
|
|
|
@ -113,7 +113,7 @@ class LibraryViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
val desiredSortingMethod =
|
||||
ListSorting.values().getOrNull(DataStoreHelper.librarySortingMode)
|
||||
ListSorting.entries.getOrNull(DataStoreHelper.librarySortingMode)
|
||||
if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) {
|
||||
sort(desiredSortingMethod, null, pages)
|
||||
} else {
|
||||
|
|
|
@ -1,104 +1,123 @@
|
|||
package com.lagradost.cloudstream3.ui.library
|
||||
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.RecyclerView.OnFlingListener
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.ui.BaseAdapter
|
||||
import com.lagradost.cloudstream3.ui.BaseDiffCallback
|
||||
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
||||
|
||||
class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding) :
|
||||
ViewHolderState<Bundle>(binding) {
|
||||
override fun save(): Bundle =
|
||||
Bundle().apply {
|
||||
putParcelable(
|
||||
"pageRecyclerview",
|
||||
binding.pageRecyclerview.layoutManager?.onSaveInstanceState()
|
||||
)
|
||||
}
|
||||
|
||||
override fun restore(state: Bundle) {
|
||||
state.getParcelable<Parcelable>("pageRecyclerview")?.let { recycle ->
|
||||
binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewpagerAdapter(
|
||||
var pages: List<SyncAPI.Page>,
|
||||
fragment: Fragment,
|
||||
val scrollCallback: (isScrollingDown: Boolean) -> Unit,
|
||||
val clickCallback: (SearchClickCallback) -> Unit
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return PageViewHolder(
|
||||
) : BaseAdapter<SyncAPI.Page, Bundle>(fragment,
|
||||
id = "ViewpagerAdapter".hashCode(),
|
||||
diffCallback = BaseDiffCallback(
|
||||
itemSame = { a, b ->
|
||||
a.title == b.title
|
||||
},
|
||||
contentSame = { a, b ->
|
||||
a.items == b.items && a.title == b.title
|
||||
}
|
||||
)) {
|
||||
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Bundle> {
|
||||
return ViewpagerAdapterViewHolderState(
|
||||
LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is PageViewHolder -> {
|
||||
holder.bind(pages[position], position, unbound.remove(position))
|
||||
}
|
||||
}
|
||||
override fun onUpdateContent(
|
||||
holder: ViewHolderState<Bundle>,
|
||||
item: SyncAPI.Page,
|
||||
position: Int
|
||||
) {
|
||||
val binding = holder.view
|
||||
if (binding !is LibraryViewpagerPageBinding) return
|
||||
(binding.pageRecyclerview.adapter as? PageAdapter)?.updateList(item.items)
|
||||
}
|
||||
|
||||
private val unbound = mutableSetOf<Int>()
|
||||
override fun onBindContent(holder: ViewHolderState<Bundle>, item: SyncAPI.Page, position: Int) {
|
||||
val binding = holder.view
|
||||
if (binding !is LibraryViewpagerPageBinding) return
|
||||
|
||||
/**
|
||||
* Used to mark all pages for re-binding and forces all items to be refreshed
|
||||
* Without this the pages will still use the same adapters
|
||||
**/
|
||||
fun rebind() {
|
||||
unbound.addAll(0..pages.size)
|
||||
this.notifyItemRangeChanged(0, pages.size)
|
||||
}
|
||||
|
||||
inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) {
|
||||
binding.pageRecyclerview.tag = position
|
||||
binding.pageRecyclerview.apply {
|
||||
spanCount =
|
||||
this@PageViewHolder.itemView.context.getSpanCount() ?: 3
|
||||
if (adapter == null || rebind) {
|
||||
// Only add the items after it has been attached since the items rely on ItemWidth
|
||||
// Which is only determined after the recyclerview is attached.
|
||||
// If this fails then item height becomes 0 when there is only one item
|
||||
doOnAttach {
|
||||
adapter = PageAdapter(
|
||||
page.items.toMutableList(),
|
||||
this,
|
||||
clickCallback
|
||||
)
|
||||
}
|
||||
} else {
|
||||
(adapter as? PageAdapter)?.updateList(page.items)
|
||||
scrollToPosition(0)
|
||||
binding.pageRecyclerview.tag = position
|
||||
binding.pageRecyclerview.apply {
|
||||
spanCount =
|
||||
binding.root.context.getSpanCount() ?: 3
|
||||
if (adapter == null) { // || rebind
|
||||
// Only add the items after it has been attached since the items rely on ItemWidth
|
||||
// Which is only determined after the recyclerview is attached.
|
||||
// If this fails then item height becomes 0 when there is only one item
|
||||
doOnAttach {
|
||||
adapter = PageAdapter(
|
||||
item.items.toMutableList(),
|
||||
this,
|
||||
clickCallback
|
||||
)
|
||||
}
|
||||
} else {
|
||||
(adapter as? PageAdapter)?.updateList(item.items)
|
||||
// scrollToPosition(0)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||
val diff = scrollY - oldScrollY
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||
val diff = scrollY - oldScrollY
|
||||
|
||||
//Expand the top Appbar based on scroll direction up/down, simulate phone behavior
|
||||
if (SettingsFragment.isTvSettings()) {
|
||||
binding.root.rootView.findViewById<AppBarLayout>(R.id.search_bar)
|
||||
.apply {
|
||||
if (diff <= 0)
|
||||
setExpanded(true)
|
||||
else
|
||||
setExpanded(false)
|
||||
}
|
||||
}
|
||||
if (diff == 0) return@setOnScrollChangeListener
|
||||
|
||||
scrollCallback.invoke(diff > 0)
|
||||
//Expand the top Appbar based on scroll direction up/down, simulate phone behavior
|
||||
if (isLayout(TV or EMULATOR)) {
|
||||
binding.root.rootView.findViewById<AppBarLayout>(R.id.search_bar)
|
||||
.apply {
|
||||
if (diff <= 0)
|
||||
setExpanded(true)
|
||||
else
|
||||
setExpanded(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onFlingListener = object : OnFlingListener() {
|
||||
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
|
||||
scrollCallback.invoke(velocityY > 0)
|
||||
return false
|
||||
}
|
||||
if (diff == 0) return@setOnScrollChangeListener
|
||||
|
||||
scrollCallback.invoke(diff > 0)
|
||||
}
|
||||
} else {
|
||||
onFlingListener = object : OnFlingListener() {
|
||||
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
|
||||
scrollCallback.invoke(velocityY > 0)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return pages.size
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.*
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.drawable.AnimatedImageDrawable
|
||||
import android.graphics.drawable.AnimatedVectorDrawable
|
||||
import android.media.metrics.PlaybackErrorEvent
|
||||
|
@ -24,11 +27,7 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.DefaultTimeBar
|
||||
import androidx.media3.ui.PlayerView
|
||||
import androidx.media3.ui.SubtitleView
|
||||
import androidx.media3.ui.TimeBar
|
||||
import androidx.media3.ui.*
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.github.rubensousa.previewseekbar.PreviewBar
|
||||
|
@ -442,6 +441,9 @@ abstract class AbstractPlayerFragment(
|
|||
|
||||
is VideoEndedEvent -> {
|
||||
context?.let { ctx ->
|
||||
// Resets subtitle delay on ended video
|
||||
player.setSubtitleOffset(0)
|
||||
|
||||
// Only play next episode if autoplay is on (default)
|
||||
if (PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
?.getBoolean(
|
||||
|
|
|
@ -1118,6 +1118,9 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
|
||||
Player.STATE_ENDED -> {
|
||||
// Resets subtitle delay on ended video
|
||||
setSubtitleOffset(0)
|
||||
|
||||
// Only play next episode if autoplay is on (default)
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context)
|
||||
?.getBoolean(
|
||||
|
|
|
@ -17,7 +17,7 @@ import com.lagradost.safefile.SafeFile
|
|||
const val DTAG = "PlayerActivity"
|
||||
|
||||
class DownloadedPlayerActivity : AppCompatActivity() {
|
||||
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
CommonActivity.dispatchKeyEvent(this, event)?.let {
|
||||
return it
|
||||
}
|
||||
|
|
|
@ -14,13 +14,7 @@ import android.os.Bundle
|
|||
import android.provider.Settings
|
||||
import android.text.Editable
|
||||
import android.text.format.DateUtils
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.Surface
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.*
|
||||
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
|
@ -38,6 +32,7 @@ import com.lagradost.cloudstream3.CommonActivity.keyEventListener
|
|||
import com.lagradost.cloudstream3.CommonActivity.playerEventListener
|
||||
import com.lagradost.cloudstream3.CommonActivity.screenHeight
|
||||
import com.lagradost.cloudstream3.CommonActivity.screenWidth
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
||||
import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding
|
||||
|
@ -46,7 +41,10 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvid
|
|||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
||||
import com.lagradost.cloudstream3.ui.result.setText
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
|
@ -77,7 +75,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
private var isVerticalOrientation: Boolean = false
|
||||
protected open var lockRotation = true
|
||||
protected open var isFullScreenPlayer = true
|
||||
protected open var isTv = false
|
||||
protected var playerBinding: PlayerCustomLayoutBinding? = null
|
||||
|
||||
private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false)
|
||||
|
@ -181,7 +178,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
|
||||
open fun openOnlineSubPicker(
|
||||
context: Context,
|
||||
imdbId: Long?,
|
||||
loadResponse: LoadResponse?,
|
||||
dismissCallback: (() -> Unit)
|
||||
) {
|
||||
throw NotImplementedError()
|
||||
|
@ -495,6 +492,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
dialog.dismissSafe(activity)
|
||||
player.seekTime(1L)
|
||||
}
|
||||
resetBtt.setOnClickListener {
|
||||
subtitleDelay = 0
|
||||
dialog.dismissSafe(activity)
|
||||
player.seekTime(1L)
|
||||
}
|
||||
cancelBtt.setOnClickListener {
|
||||
subtitleDelay = beforeOffset
|
||||
dialog.dismissSafe(activity)
|
||||
|
@ -1156,6 +1158,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||
if (!isShowing) {
|
||||
onClickChange()
|
||||
|
@ -1204,7 +1207,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
|
||||
// netflix capture back and hide ~monke
|
||||
KeyEvent.KEYCODE_BACK -> {
|
||||
if (isShowing && isTv) {
|
||||
if (isShowing && isLayout(TV or EMULATOR)) {
|
||||
onClickChange()
|
||||
return true
|
||||
}
|
||||
|
@ -1514,7 +1517,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
}
|
||||
}
|
||||
// cs3 is peak media center
|
||||
setRemainingTimeCounter(durationMode || SettingsFragment.isTrueTvSettings())
|
||||
setRemainingTimeCounter(durationMode || Globals.isLayout(Globals.TV))
|
||||
playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ ->
|
||||
updateRemainingTime()
|
||||
}
|
||||
|
|
|
@ -25,6 +25,10 @@ import com.lagradost.cloudstream3.*
|
|||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.getTMDbId
|
||||
import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
|
||||
|
@ -40,7 +44,9 @@ import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSub
|
|||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog
|
||||
import com.lagradost.cloudstream3.ui.result.*
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
|
@ -257,6 +263,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
var episode: Int? = null,
|
||||
var season: Int? = null,
|
||||
var name: String? = null,
|
||||
var imdbId: String? = null,
|
||||
)
|
||||
|
||||
private fun getMetaData(): TempMetaData {
|
||||
|
@ -283,7 +290,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
override fun openOnlineSubPicker(
|
||||
context: Context, imdbId: Long?, dismissCallback: (() -> Unit)
|
||||
context: Context, loadResponse: LoadResponse?, dismissCallback: (() -> Unit)
|
||||
) {
|
||||
val providers = subsProviders
|
||||
val isSingleProvider = subsProviders.size == 1
|
||||
|
@ -376,6 +383,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
val currentTempMeta = getMetaData()
|
||||
|
||||
// bruh idk why it is not correct
|
||||
val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent))
|
||||
binding.searchLoadingBar.progressTintList = color
|
||||
|
@ -423,7 +431,10 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val search =
|
||||
AbstractSubtitleEntities.SubtitleSearch(
|
||||
query = query ?: return@ioSafe,
|
||||
imdb = imdbId,
|
||||
imdbId = loadResponse?.getImdbId(),
|
||||
tmdbId = loadResponse?.getTMDbId()?.toInt(),
|
||||
malId = loadResponse?.getMalId()?.toInt(),
|
||||
aniListId = loadResponse?.getAniListId()?.toInt(),
|
||||
epNumber = currentTempMeta.episode,
|
||||
seasonNumber = currentTempMeta.season,
|
||||
lang = currentLanguageTwoLetters.ifBlank { null },
|
||||
|
@ -632,6 +643,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
if (subsProvidersIsActive) {
|
||||
val currentLoadResponse = viewModel.getLoadResponse()
|
||||
|
||||
val loadFromOpenSubsFooter: TextView = layoutInflater.inflate(
|
||||
R.layout.sort_bottom_footer_add_choice, null
|
||||
) as TextView
|
||||
|
@ -642,7 +655,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
loadFromOpenSubsFooter.setOnClickListener {
|
||||
shouldDismiss = false
|
||||
sourceDialog.dismissSafe(activity)
|
||||
openOnlineSubPicker(it.context, null) {
|
||||
openOnlineSubPicker(it.context, currentLoadResponse) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
@ -1278,8 +1291,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View? {
|
||||
// this is used instead of layout-television to follow the settings and some TV devices are not classified as TV for some reason
|
||||
isTv = isTvSettings()
|
||||
layout = if (isTv) R.layout.fragment_player_tv else R.layout.fragment_player
|
||||
layout = if (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player
|
||||
|
||||
viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java]
|
||||
sync = ViewModelProvider(this)[SyncViewModel::class.java]
|
||||
|
|
|
@ -10,7 +10,8 @@ enum class LoadType {
|
|||
InAppDownload,
|
||||
ExternalApp,
|
||||
Browser,
|
||||
Chromecast
|
||||
Chromecast,
|
||||
Fcast
|
||||
}
|
||||
|
||||
fun LoadType.toSet() : Set<ExtractorLinkType> {
|
||||
|
@ -29,12 +30,17 @@ fun LoadType.toSet() : Set<ExtractorLinkType> {
|
|||
ExtractorLinkType.VIDEO,
|
||||
ExtractorLinkType.M3U8
|
||||
)
|
||||
LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet()
|
||||
LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.entries.toSet()
|
||||
LoadType.Chromecast -> setOf(
|
||||
ExtractorLinkType.VIDEO,
|
||||
ExtractorLinkType.DASH,
|
||||
ExtractorLinkType.M3U8
|
||||
)
|
||||
LoadType.Fcast -> setOf(
|
||||
ExtractorLinkType.VIDEO,
|
||||
ExtractorLinkType.DASH,
|
||||
ExtractorLinkType.M3U8
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
|
@ -111,6 +112,9 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
}
|
||||
fun getLoadResponse(): LoadResponse? {
|
||||
return normalSafeApiCall { (generator as? RepoLinkGenerator?)?.page }
|
||||
}
|
||||
|
||||
fun getMeta(): Any? {
|
||||
return normalSafeApiCall { generator?.getCurrent() }
|
||||
|
|
|
@ -9,6 +9,9 @@ import android.util.Log
|
|||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.graphics.scale
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
|
@ -63,7 +66,7 @@ interface IPreviewGenerator {
|
|||
companion object {
|
||||
fun new(): IPreviewGenerator {
|
||||
/** because TV has low ram + not show we disable this for now */
|
||||
return if (SettingsFragment.isTrueTvSettings()) {
|
||||
return if (isLayout(TV)) {
|
||||
empty()
|
||||
} else {
|
||||
PreviewGenerator()
|
||||
|
|
|
@ -34,7 +34,11 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
|||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||
import com.lagradost.cloudstream3.ui.search.SearchViewModel
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
|
||||
import com.lagradost.cloudstream3.utils.UIHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
|
@ -173,7 +177,7 @@ class QuickSearchFragment : Fragment() {
|
|||
}
|
||||
} else {
|
||||
binding?.quickSearchMasterRecycler?.adapter =
|
||||
ParentItemAdapter(mutableListOf(), { callback ->
|
||||
ParentItemAdapter(fragment = this, id = "quickSearchMasterRecycler".hashCode(), { callback ->
|
||||
SearchHelper.handleSearchClickCallback(callback)
|
||||
//when (callback.action) {
|
||||
//SEARCH_ACTION_LOAD -> {
|
||||
|
@ -273,11 +277,16 @@ class QuickSearchFragment : Fragment() {
|
|||
// UIHelper.showInputMethod(view.findFocus())
|
||||
// }
|
||||
//}
|
||||
binding?.quickSearchBack?.setOnClickListener {
|
||||
activity?.popCurrentPage()
|
||||
if (isLayout(PHONE or EMULATOR)) {
|
||||
binding?.quickSearchBack?.apply {
|
||||
isVisible = true
|
||||
setOnClickListener {
|
||||
activity?.popCurrentPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isTrueTvSettings()) {
|
||||
if (isLayout(TV)) {
|
||||
binding?.quickSearch?.requestFocus()
|
||||
}
|
||||
|
||||
|
|
|
@ -9,18 +9,24 @@ import androidx.core.view.isVisible
|
|||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding
|
||||
import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK
|
||||
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.html
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
const val ACTION_PLAY_EPISODE_IN_PLAYER = 1
|
||||
|
@ -49,6 +55,8 @@ const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16
|
|||
const val ACTION_PLAY_EPISODE_IN_MPV = 17
|
||||
|
||||
const val ACTION_MARK_AS_WATCHED = 18
|
||||
const val ACTION_FCAST = 19
|
||||
|
||||
const val TV_EP_SIZE_LARGE = 400
|
||||
const val TV_EP_SIZE_SMALL = 300
|
||||
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
|
||||
|
@ -102,7 +110,7 @@ class EpisodeAdapter(
|
|||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val item = getItem(position)
|
||||
return if (item.poster.isNullOrBlank()) 0 else 1
|
||||
return if (item.poster.isNullOrBlank() && item.description.isNullOrBlank()) 0 else 1
|
||||
}
|
||||
|
||||
|
||||
|
@ -172,15 +180,13 @@ class EpisodeAdapter(
|
|||
@SuppressLint("SetTextI18n")
|
||||
fun bind(card: ResultEpisode) {
|
||||
localCard = card
|
||||
|
||||
val setWidth =
|
||||
if (isTvSettings()) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT
|
||||
if (isLayout(TV or EMULATOR)) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT
|
||||
|
||||
binding.episodeLinHolder.layoutParams.width = setWidth
|
||||
binding.episodeHolderLarge.layoutParams.width = setWidth
|
||||
binding.episodeHolder.layoutParams.width = setWidth
|
||||
|
||||
val isTrueTv = isTrueTvSettings()
|
||||
|
||||
binding.apply {
|
||||
downloadButton.isVisible = hasDownloadSupport
|
||||
|
@ -249,7 +255,7 @@ class EpisodeAdapter(
|
|||
|
||||
var isExpanded = false
|
||||
setOnClickListener {
|
||||
if (isTrueTv) {
|
||||
if (isLayout(TV)) {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card))
|
||||
} else {
|
||||
isExpanded = !isExpanded
|
||||
|
@ -260,7 +266,34 @@ class EpisodeAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
if (!isTrueTv) {
|
||||
if (card.airDate != null) {
|
||||
val isUpcoming = unixTimeMS < card.airDate
|
||||
|
||||
if (isUpcoming) {
|
||||
episodePlayIcon.isVisible = false
|
||||
episodeUpcomingIcon.isVisible = !episodePoster.isVisible
|
||||
episodeDate.setText(
|
||||
txt(
|
||||
R.string.episode_upcoming_format,
|
||||
secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "")
|
||||
)
|
||||
)
|
||||
} else {
|
||||
episodeUpcomingIcon.isVisible = false
|
||||
|
||||
val formattedAirDate = SimpleDateFormat.getDateInstance(
|
||||
DateFormat.LONG,
|
||||
Locale.getDefault()
|
||||
).apply {
|
||||
}.format(Date(card.airDate))
|
||||
|
||||
episodeDate.setText(txt(formattedAirDate))
|
||||
}
|
||||
} else {
|
||||
episodeDate.isVisible = false
|
||||
}
|
||||
|
||||
if (isLayout(EMULATOR or PHONE)) {
|
||||
episodePoster.setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||
}
|
||||
|
@ -271,11 +304,12 @@ class EpisodeAdapter(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||
}
|
||||
|
||||
if (isTrueTv) {
|
||||
if (isLayout(TV)) {
|
||||
itemView.isFocusable = true
|
||||
itemView.isFocusableInTouchMode = true
|
||||
//itemView.touchscreenBlocksFocus = false
|
||||
|
@ -300,11 +334,9 @@ class EpisodeAdapter(
|
|||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(card: ResultEpisode) {
|
||||
val isTrueTv = isTrueTvSettings()
|
||||
|
||||
binding.episodeHolder.layoutParams.apply {
|
||||
width =
|
||||
if (isTvSettings()) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT
|
||||
if (isLayout(TV or EMULATOR)) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
|
||||
binding.apply {
|
||||
|
@ -361,7 +393,7 @@ class EpisodeAdapter(
|
|||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||
}
|
||||
|
||||
if (isTrueTv) {
|
||||
if (isLayout(TV)) {
|
||||
itemView.isFocusable = true
|
||||
itemView.isFocusableInTouchMode = true
|
||||
//itemView.touchscreenBlocksFocus = false
|
||||
|
|
|
@ -5,7 +5,8 @@ import android.view.ViewGroup
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
|
||||
/*
|
||||
class ImageAdapter(context: Context, val resource: Int) : ArrayAdapter<Int>(context, resource) {
|
||||
|
@ -83,7 +84,7 @@ class ImageAdapter(
|
|||
this.nextFocusUpId = nextFocusUp
|
||||
}
|
||||
if (clickCallback != null) {
|
||||
if (isTrueTvSettings()) {
|
||||
if (isLayout(TV)) {
|
||||
isClickable = true
|
||||
isLongClickable = true
|
||||
isFocusable = true
|
||||
|
|
|
@ -50,6 +50,7 @@ data class ResultEpisode(
|
|||
val videoWatchState: VideoWatchState,
|
||||
/** Sum of all previous season episode counts + episode */
|
||||
val totalEpisodeIndex: Int? = null,
|
||||
val airDate: Long? = null,
|
||||
)
|
||||
|
||||
fun ResultEpisode.getRealPosition(): Long {
|
||||
|
@ -85,6 +86,7 @@ fun buildResultEpisode(
|
|||
tvType: TvType,
|
||||
parentId: Int,
|
||||
totalEpisodeIndex: Int? = null,
|
||||
airDate: Long? = null,
|
||||
): ResultEpisode {
|
||||
val posDur = getViewPos(id)
|
||||
val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None
|
||||
|
@ -107,7 +109,8 @@ fun buildResultEpisode(
|
|||
tvType,
|
||||
parentId,
|
||||
videoWatchState,
|
||||
totalEpisodeIndex
|
||||
totalEpisodeIndex,
|
||||
airDate,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,6 @@ package com.lagradost.cloudstream3.ui.result
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Rect
|
||||
|
@ -33,7 +30,6 @@ import com.google.android.gms.cast.framework.CastState
|
|||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.lagradost.cloudstream3.APIHolder
|
||||
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
||||
import com.lagradost.cloudstream3.CommonActivity
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.DubStatus
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
|
@ -65,11 +61,13 @@ import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
|
|||
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
||||
import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.openBatteryOptimizationSettings
|
||||
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.SingleSelectionHelper.showDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
|
@ -445,8 +443,9 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
|||
|
||||
val name = (viewModel.page.value as? Resource.Success)?.value?.title
|
||||
?: txt(R.string.no_data).asStringNull(context) ?: ""
|
||||
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
|
||||
showToast(txt(message, name), Toast.LENGTH_SHORT)
|
||||
}
|
||||
context?.let { openBatteryOptimizationSettings(it) }
|
||||
}
|
||||
resultFavorite.setOnClickListener {
|
||||
viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? ->
|
||||
|
@ -460,7 +459,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
|||
|
||||
val name = (viewModel.page.value as? Resource.Success)?.value?.title
|
||||
?: txt(R.string.no_data).asStringNull(context) ?: ""
|
||||
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
|
||||
showToast(txt(message, name), Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
mediaRouteButton.apply {
|
||||
|
@ -468,7 +467,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
|||
alpha = if (chromecastSupport) 1f else 0.3f
|
||||
if (!chromecastSupport) {
|
||||
setOnClickListener {
|
||||
CommonActivity.showToast(
|
||||
showToast(
|
||||
R.string.no_chromecast_support_toast,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
|
@ -643,6 +642,8 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
|||
),
|
||||
null
|
||||
) { click ->
|
||||
context?.let { openBatteryOptimizationSettings(it) }
|
||||
|
||||
when (click.action) {
|
||||
DOWNLOAD_ACTION_DOWNLOAD -> {
|
||||
viewModel.handleAction(
|
||||
|
@ -757,14 +758,8 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
|||
resultReloadConnectionOpenInBrowser.isVisible = data is Resource.Failure
|
||||
|
||||
resultTitle.setOnLongClickListener {
|
||||
val titleToCopy = resultTitle.text
|
||||
val clipboardManager =
|
||||
activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?
|
||||
clipboardManager?.setPrimaryClip(ClipData.newPlainText("Title", titleToCopy))
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
showToast(R.string.copyTitle, Toast.LENGTH_SHORT)
|
||||
}
|
||||
return@setOnLongClickListener true
|
||||
clipboardHelper(txt(R.string.title), resultTitle.text)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,14 +33,17 @@ import com.lagradost.cloudstream3.ui.WatchType
|
|||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup
|
||||
import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator
|
||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
||||
import com.lagradost.cloudstream3.ui.player.NEXT_WATCH_EPISODE_PERCENTAGE
|
||||
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
|
||||
import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.html
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
|
||||
|
@ -309,9 +312,10 @@ class ResultFragmentTv : Fragment() {
|
|||
resultEpisodesShowButton to resultEpisodesShowText
|
||||
).forEach { (button , text) ->
|
||||
|
||||
button.setOnFocusChangeListener { _, hasFocus ->
|
||||
button.setOnFocusChangeListener { view, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
text.isSelected = false
|
||||
if (view.id == R.id.result_episodes_show_button) toggleEpisodes(false)
|
||||
return@setOnFocusChangeListener
|
||||
}
|
||||
|
||||
|
@ -377,10 +381,6 @@ class ResultFragmentTv : Fragment() {
|
|||
|
||||
resultMetaSite.isFocusable = false
|
||||
|
||||
//resultReloadConnectionOpenInBrowser.setOnClickListener {view ->
|
||||
// view.context?.openBrowser(storedData?.url ?: return@setOnClickListener, fallbackWebview = true)
|
||||
//}
|
||||
|
||||
resultSeasonSelection.setAdapter()
|
||||
resultRangeSelection.setAdapter()
|
||||
resultDubSelection.setAdapter()
|
||||
|
@ -458,11 +458,12 @@ class ResultFragmentTv : Fragment() {
|
|||
observeNullable(viewModel.resumeWatching) { resume ->
|
||||
binding?.apply {
|
||||
|
||||
// > resultResumeSeries is visible when not null
|
||||
if (resume == null) {
|
||||
resultResumeSeries.isVisible = false
|
||||
return@observeNullable
|
||||
}
|
||||
resultResumeSeries.isVisible = true
|
||||
resultPlayMovie.isVisible = false
|
||||
resultPlaySeries.isVisible = false
|
||||
|
||||
// show progress no matter if series or movie
|
||||
resume.progress?.let { progress ->
|
||||
|
@ -477,10 +478,6 @@ class ResultFragmentTv : Fragment() {
|
|||
resultResumeProgressHolder.isVisible = false
|
||||
}
|
||||
|
||||
resultPlayMovie.isVisible = false
|
||||
resultPlaySeries.isVisible = false
|
||||
resultResumeSeries.isVisible = true
|
||||
|
||||
focusPlayButton()
|
||||
// Stops last button right focus if it is a movie
|
||||
if (resume.isMovie)
|
||||
|
@ -491,7 +488,7 @@ class ResultFragmentTv : Fragment() {
|
|||
resume.isMovie -> context?.getString(R.string.resume)
|
||||
resume.result.season != null ->
|
||||
"${getString(R.string.season_short)}${resume.result.season}:${getString(R.string.episode_short)}${resume.result.episode}"
|
||||
else -> "${getString(R.string.episode)}${resume.result.episode}"
|
||||
else -> "${getString(R.string.episode)} ${resume.result.episode}"
|
||||
}
|
||||
|
||||
resultResumeSeriesButton.setOnClickListener {
|
||||
|
@ -604,7 +601,7 @@ class ResultFragmentTv : Fragment() {
|
|||
}
|
||||
|
||||
observeNullable(viewModel.subscribeStatus) { isSubscribed ->
|
||||
binding?.resultSubscribe?.isVisible = isSubscribed != null && requireContext().isEmulatorSettings()
|
||||
binding?.resultSubscribe?.isVisible = isSubscribed != null && isLayout(EMULATOR)
|
||||
binding?.resultSubscribeButton?.apply {
|
||||
|
||||
if (isSubscribed == null) return@observeNullable
|
||||
|
@ -647,15 +644,14 @@ class ResultFragmentTv : Fragment() {
|
|||
}
|
||||
|
||||
observeNullable(viewModel.movie) { data ->
|
||||
if (data == null) return@observeNullable
|
||||
if (data == null ) {
|
||||
return@observeNullable
|
||||
}
|
||||
|
||||
binding?.apply {
|
||||
resultPlayMovie.isVisible = (data is Resource.Success) && !comingSoon
|
||||
resultPlaySeries.isVisible = false
|
||||
resultEpisodesShow.isVisible = false
|
||||
|
||||
(data as? Resource.Success)?.value?.let { (text, ep) ->
|
||||
//resultPlayMovieText.setText(text)
|
||||
|
||||
resultPlayMovieButton.setOnClickListener {
|
||||
viewModel.handleAction(
|
||||
EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep)
|
||||
|
@ -667,14 +663,17 @@ class ResultFragmentTv : Fragment() {
|
|||
)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
//focusPlayButton()
|
||||
resultPlayMovieButton.requestFocus()
|
||||
|
||||
resultPlayMovie.isVisible = !comingSoon && resultResumeSeries.isGone
|
||||
if (comingSoon)
|
||||
resultBookmarkButton.requestFocus()
|
||||
else
|
||||
resultPlayMovieButton.requestFocus()
|
||||
|
||||
// Stops last button right focus
|
||||
resultSearchButton.nextFocusRightId = R.id.result_search_Button
|
||||
}
|
||||
}
|
||||
//focusPlayButton()
|
||||
}
|
||||
|
||||
observeNullable(viewModel.selectPopup) { popup ->
|
||||
|
@ -756,7 +755,7 @@ class ResultFragmentTv : Fragment() {
|
|||
setRecommendations(recommendations, null)
|
||||
}
|
||||
|
||||
if (isTrueTvSettings()) {
|
||||
if (isLayout(TV)) {
|
||||
observe(viewModel.episodeSynopsis) { description ->
|
||||
view.context?.let { ctx ->
|
||||
val builder: AlertDialog.Builder =
|
||||
|
@ -778,39 +777,44 @@ class ResultFragmentTv : Fragment() {
|
|||
|
||||
binding?.apply {
|
||||
|
||||
resultPlayMovie.isVisible = false
|
||||
resultPlaySeries.isVisible = true && !comingSoon
|
||||
resultEpisodes.isVisible = true && !comingSoon
|
||||
resultEpisodesShow.isVisible = true && !comingSoon
|
||||
if (comingSoon)
|
||||
resultBookmarkButton.requestFocus()
|
||||
|
||||
// resultEpisodeLoading.isVisible = episodes is Resource.Loading
|
||||
if (episodes is Resource.Success) {
|
||||
val first = episodes.value.firstOrNull()
|
||||
if (first != null) {
|
||||
resultPlaySeriesText.text = //"${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}"
|
||||
|
||||
val lastWatchedIndex = episodes.value.indexOfLast { ep ->
|
||||
ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f || ep.videoWatchState == VideoWatchState.Watched
|
||||
}
|
||||
|
||||
val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() }
|
||||
|
||||
if (firstUnwatched != null) {
|
||||
resultPlaySeriesText.text =
|
||||
when {
|
||||
first.season != null ->
|
||||
"${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}"
|
||||
else -> "${getString(R.string.episode)} ${first.episode}"
|
||||
firstUnwatched.season != null ->
|
||||
"${getString(R.string.season_short)}${firstUnwatched.season}:${getString(R.string.episode_short)}${firstUnwatched.episode}"
|
||||
else -> "${getString(R.string.episode)} ${firstUnwatched.episode}"
|
||||
}
|
||||
resultPlaySeriesButton.setOnClickListener {
|
||||
viewModel.handleAction(
|
||||
EpisodeClickEvent(
|
||||
ACTION_CLICK_DEFAULT,
|
||||
first
|
||||
firstUnwatched
|
||||
)
|
||||
)
|
||||
}
|
||||
resultPlaySeriesButton.setOnLongClickListener {
|
||||
viewModel.handleAction(
|
||||
EpisodeClickEvent(ACTION_SHOW_OPTIONS, first)
|
||||
EpisodeClickEvent(ACTION_SHOW_OPTIONS, firstUnwatched)
|
||||
)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
if (!hasLoadedEpisodesOnce) {
|
||||
hasLoadedEpisodesOnce = true
|
||||
focusPlayButton()
|
||||
resultPlaySeries.requestFocus()
|
||||
resultPlaySeries.isVisible = resultResumeSeries.isGone && !comingSoon
|
||||
resultEpisodesShow.isVisible = true && !comingSoon
|
||||
resultPlaySeriesButton.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -883,7 +887,7 @@ class ResultFragmentTv : Fragment() {
|
|||
resultDescription.apply {
|
||||
setTextHtml(d.plotText)
|
||||
setOnClickListener {
|
||||
if (context.isEmulatorSettings()) {
|
||||
if (isLayout(EMULATOR)) {
|
||||
isExpanded = !isExpanded
|
||||
maxLines = if (isExpanded) {
|
||||
Integer.MAX_VALUE
|
||||
|
@ -919,9 +923,6 @@ class ResultFragmentTv : Fragment() {
|
|||
)
|
||||
comingSoon = d.comingSoon
|
||||
resultTvComingSoon.isVisible = d.comingSoon
|
||||
resultPlayMovie.isGone = d.comingSoon
|
||||
resultPlaySeries.isGone = d.comingSoon
|
||||
resultDataHolder.isGone = d.comingSoon
|
||||
|
||||
UIHelper.populateChips(resultTag, d.tags)
|
||||
resultCastItems.isGone = d.actors.isNullOrEmpty()
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.core.view.isGone
|
|||
import androidx.core.view.isVisible
|
||||
import com.lagradost.cloudstream3.CommonActivity.screenHeight
|
||||
import com.lagradost.cloudstream3.CommonActivity.screenWidth
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerEventSource
|
||||
|
@ -110,7 +111,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone() {
|
|||
|
||||
override fun openOnlineSubPicker(
|
||||
context: Context,
|
||||
imdbId: Long?,
|
||||
loadResponse: LoadResponse?,
|
||||
dismissCallback: () -> Unit
|
||||
) {
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.*
|
|||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.format.Formatter.formatFileSize
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
|
@ -20,15 +21,23 @@ import com.lagradost.cloudstream3.APIHolder.apis
|
|||
import com.lagradost.cloudstream3.APIHolder.getId
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||
import com.lagradost.cloudstream3.CommonActivity.getCastSession
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.MPV
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.MPV_COMPONENT
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.MPV_PACKAGE
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.VLC
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.VLC_COMPONENT
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.VLC_PACKAGE
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO_CAST_PACKAGE
|
||||
import com.lagradost.cloudstream3.metaproviders.SyncRedirector
|
||||
import com.lagradost.cloudstream3.mvvm.*
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
|
@ -81,12 +90,16 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
|
|||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setSubscribedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.fcast.FcastManager
|
||||
import com.lagradost.cloudstream3.utils.fcast.FcastSession
|
||||
import com.lagradost.cloudstream3.utils.fcast.Opcode
|
||||
import com.lagradost.cloudstream3.utils.fcast.PlayMessage
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
/** This starts at 1 */
|
||||
data class EpisodeRange(
|
||||
// used to index data
|
||||
|
@ -197,7 +210,11 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
|
|||
|
||||
else -> null
|
||||
}?.also {
|
||||
nextAiringEpisode = txt(R.string.next_episode_format, airing.episode)
|
||||
nextAiringEpisode = when (airing.season) {
|
||||
|
||||
null -> txt(R.string.next_episode_format, airing.episode)
|
||||
else -> txt(R.string.next_season_episode_format, airing.season, airing.episode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -246,6 +263,9 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
|
|||
TvType.Live -> R.string.live_singular
|
||||
TvType.Others -> R.string.other_singular
|
||||
TvType.NSFW -> R.string.nsfw_singular
|
||||
TvType.Music -> R.string.music_singlar
|
||||
TvType.AudioBook -> R.string.audio_book_singular
|
||||
TvType.CustomMedia -> R.string.custom_media_singluar
|
||||
}
|
||||
),
|
||||
yearText = txt(year?.toString()),
|
||||
|
@ -627,6 +647,9 @@ class ResultViewModel2 : ViewModel() {
|
|||
TvType.Live -> "LiveStreams"
|
||||
TvType.NSFW -> "NSFW"
|
||||
TvType.Others -> "Others"
|
||||
TvType.Music -> "Music"
|
||||
TvType.AudioBook -> "AudioBooks"
|
||||
TvType.CustomMedia -> "Media"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -928,15 +951,20 @@ class ResultViewModel2 : ViewModel() {
|
|||
) {
|
||||
val isSubscribed = _subscribeStatus.value ?: return
|
||||
val response = currentResponse ?: return
|
||||
if (response !is EpisodeResponse) return
|
||||
|
||||
val currentId = currentId ?: return
|
||||
|
||||
// This might be a bit confusing, but even if the loadresponse is not a EpisodeResponse
|
||||
// _subscribeStatus might be true.
|
||||
|
||||
if (isSubscribed) {
|
||||
removeSubscribedData(currentId)
|
||||
statusChangedCallback?.invoke(false)
|
||||
_subscribeStatus.postValue(false)
|
||||
_subscribeStatus.postValue(if (response is EpisodeResponse) false else null)
|
||||
MainActivity.reloadLibraryEvent(true)
|
||||
} else {
|
||||
if (response !is EpisodeResponse) {
|
||||
return
|
||||
}
|
||||
checkAndWarnDuplicates(
|
||||
context,
|
||||
LibraryListType.SUBSCRIPTIONS,
|
||||
|
@ -981,8 +1009,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
|
||||
_subscribeStatus.postValue(true)
|
||||
|
||||
statusChangedCallback?.invoke(true)
|
||||
MainActivity.reloadLibraryEvent(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1088,13 +1116,14 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse ->
|
||||
val librarySyncData = it.syncData
|
||||
val yearCheck = year == it.year || year == null || it.year == null
|
||||
|
||||
val checks = listOf(
|
||||
{ imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId },
|
||||
{ tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId },
|
||||
{ malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId },
|
||||
{ aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId },
|
||||
{ normalizedName == normalizeString(it.name) && year == it.year }
|
||||
{ normalizedName == normalizeString(it.name) && yearCheck }
|
||||
)
|
||||
|
||||
checks.any { it() }
|
||||
|
@ -1269,9 +1298,14 @@ class ResultViewModel2 : ViewModel() {
|
|||
callback: (Pair<LinkLoadingResult, Int>) -> Unit,
|
||||
) {
|
||||
loadLinks(result, isVisible = true, type) { links ->
|
||||
// Could not find a better way to do this
|
||||
val context = AcraApplication.context
|
||||
postPopup(
|
||||
text,
|
||||
links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) {
|
||||
links.links.apmap {
|
||||
val size = it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: ""
|
||||
txt("${it.name} ${Qualities.getStringByInt(it.quality)}$size")
|
||||
}) {
|
||||
callback.invoke(links to (it ?: return@postPopup))
|
||||
}
|
||||
}
|
||||
|
@ -1329,7 +1363,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
private fun launchActivity(
|
||||
activity: Activity?,
|
||||
resumeApp: ResultResume,
|
||||
resumeApp: MainActivity.Companion.ResultResume,
|
||||
id: Int? = null,
|
||||
work: suspend (Intent.(Activity) -> Unit)
|
||||
): Job? {
|
||||
|
@ -1498,6 +1532,13 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (FcastManager.currentDevices.isNotEmpty()) {
|
||||
options.add(
|
||||
txt(R.string.player_settings_play_in_fcast) to ACTION_FCAST
|
||||
)
|
||||
}
|
||||
|
||||
options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER)
|
||||
|
||||
for (app in apps) {
|
||||
|
@ -1673,6 +1714,39 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
ACTION_FCAST -> {
|
||||
val devices = FcastManager.currentDevices.toList()
|
||||
postPopup(
|
||||
txt(R.string.player_settings_select_cast_device),
|
||||
devices.map { txt(it.name) }) { index ->
|
||||
if (index == null) return@postPopup
|
||||
val device = devices.getOrNull(index)
|
||||
|
||||
acquireSingleLink(
|
||||
click.data,
|
||||
LoadType.Fcast,
|
||||
txt(R.string.episode_action_cast_mirror)
|
||||
) { (result, index) ->
|
||||
val host = device?.host ?: return@acquireSingleLink
|
||||
val link = result.links.getOrNull(index) ?: return@acquireSingleLink
|
||||
|
||||
FcastSession(host).use { session ->
|
||||
session.sendMessage(
|
||||
Opcode.Play,
|
||||
PlayMessage(
|
||||
link.type.getMimeType(),
|
||||
link.url,
|
||||
headers = mapOf(
|
||||
"referer" to link.referer,
|
||||
"user-agent" to USER_AGENT
|
||||
) + link.headers
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink(
|
||||
click.data,
|
||||
LoadType.Browser,
|
||||
|
@ -1693,14 +1767,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
LoadType.ExternalApp,
|
||||
txt(R.string.episode_action_copy_link)
|
||||
) { (result, index) ->
|
||||
val act = activity ?: return@acquireSingleLink
|
||||
val serviceClipboard =
|
||||
(act.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)
|
||||
?: return@acquireSingleLink
|
||||
val link = result.links[index]
|
||||
val clip = ClipData.newPlainText(link.name, link.url)
|
||||
serviceClipboard.setPrimaryClip(clip)
|
||||
showToast(R.string.copy_link_toast, Toast.LENGTH_SHORT)
|
||||
clipboardHelper(txt(link.name), link.url)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1760,20 +1828,28 @@ class ResultViewModel2 : ViewModel() {
|
|||
val data = currentResponse?.syncData?.toList() ?: emptyList()
|
||||
val list =
|
||||
HashMap<String, String>().apply { putAll(data) }
|
||||
|
||||
activity?.navigate(
|
||||
R.id.global_to_navigation_player,
|
||||
GeneratorPlayer.newInstance(
|
||||
generator?.also {
|
||||
it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work
|
||||
?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id }
|
||||
?.let { index ->
|
||||
if (index >= 0)
|
||||
it.goto(index)
|
||||
}
|
||||
} ?: return, list
|
||||
generator?.also {
|
||||
it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work
|
||||
?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id }
|
||||
?.let { index ->
|
||||
if (index >= 0)
|
||||
it.goto(index)
|
||||
}
|
||||
}
|
||||
if (currentResponse?.type == TvType.CustomMedia) {
|
||||
generator?.generateLinks(
|
||||
clearCache = true,
|
||||
LoadType.Unknown,
|
||||
callback = {},
|
||||
subtitleCallback = {})
|
||||
} else {
|
||||
activity?.navigate(
|
||||
R.id.global_to_navigation_player,
|
||||
GeneratorPlayer.newInstance(
|
||||
generator ?: return, list
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_MARK_AS_WATCHED -> {
|
||||
|
@ -2052,12 +2128,15 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
|
||||
private fun postSubscription(loadResponse: LoadResponse) {
|
||||
val id = loadResponse.getId()
|
||||
val data = getSubscribedData(id)
|
||||
if (loadResponse.isEpisodeBased()) {
|
||||
val id = loadResponse.getId()
|
||||
val data = getSubscribedData(id)
|
||||
updateSubscribedData(id, data, loadResponse as? EpisodeResponse)
|
||||
val isSubscribed = data != null
|
||||
_subscribeStatus.postValue(isSubscribed)
|
||||
_subscribeStatus.postValue(data != null)
|
||||
}
|
||||
// lets say that we have subscribed, then we must be able to unsubscribe no matter what
|
||||
else if (data != null) {
|
||||
_subscribeStatus.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2256,7 +2335,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
fillers.getOrDefault(episode, false),
|
||||
loadResponse.type,
|
||||
mainId,
|
||||
totalIndex
|
||||
totalIndex,
|
||||
airDate = i.date
|
||||
)
|
||||
|
||||
val season = eps.seasonIndex ?: 0
|
||||
|
@ -2305,7 +2385,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
null,
|
||||
loadResponse.type,
|
||||
mainId,
|
||||
totalIndex
|
||||
totalIndex,
|
||||
airDate = episode.date
|
||||
)
|
||||
|
||||
val season = ep.seasonIndex ?: 0
|
||||
|
@ -2337,7 +2418,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
null,
|
||||
loadResponse.type,
|
||||
mainId,
|
||||
null
|
||||
null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -2591,6 +2672,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
override var posterHeaders: Map<String, String>? = null,
|
||||
override var backgroundPosterUrl: String? = null,
|
||||
override var contentRating: String? = null,
|
||||
val id : Int?,
|
||||
) : LoadResponse
|
||||
|
||||
fun loadSmall(activity: Activity?, searchResponse : SearchResponse) = ioSafe {
|
||||
|
@ -2600,7 +2682,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
val api = APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull(searchResponse.url) ?: APIRepository.noneApi
|
||||
val repo = APIRepository(api)
|
||||
val response = LoadResponseFromSearch(name = searchResponse.name, url = searchResponse.url, apiName = api.name, type = searchResponse.type ?: TvType.Others,
|
||||
posterUrl = searchResponse.posterUrl).apply {
|
||||
posterUrl = searchResponse.posterUrl, id = searchResponse.id).apply {
|
||||
if (searchResponse is SyncAPI.LibraryItem) {
|
||||
this.plot = searchResponse.plot
|
||||
this.rating = searchResponse.personalRating?.times(100) ?: searchResponse.rating
|
||||
|
@ -2612,12 +2694,14 @@ class ResultViewModel2 : ViewModel() {
|
|||
this.tags = searchResponse.tags
|
||||
}
|
||||
}
|
||||
val mainId = searchResponse.id ?: response.getId()
|
||||
val mainId = response.getId()
|
||||
|
||||
postSuccessful(
|
||||
loadResponse = response,
|
||||
mainId = mainId,
|
||||
apiRepository = repo, updateEpisodes = false, updateFillers = false)
|
||||
apiRepository = repo,
|
||||
updateEpisodes = false,
|
||||
updateFillers = false)
|
||||
}
|
||||
|
||||
fun load(
|
||||
|
|
|
@ -6,7 +6,8 @@ import androidx.recyclerview.widget.DiffUtil
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.lagradost.cloudstream3.databinding.ResultSelectionBinding
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
|
||||
typealias SelectData = Pair<UiText?, Any>
|
||||
|
||||
|
@ -72,8 +73,7 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter<Recycler
|
|||
fun bind(
|
||||
data: SelectData, isSelected: Boolean, callback: (Any) -> Unit
|
||||
) {
|
||||
val isTrueTv = isTrueTvSettings()
|
||||
if (isTrueTv) {
|
||||
if (isLayout(TV)) {
|
||||
item.isFocusable = true
|
||||
item.isFocusableInTouchMode = true
|
||||
}
|
||||
|
|
|
@ -19,6 +19,13 @@ sealed class UiText {
|
|||
|
||||
data class DynamicString(val value: String) : UiText() {
|
||||
override fun toString(): String = value
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is DynamicString) return false
|
||||
return this.value == other.value
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = value.hashCode()
|
||||
}
|
||||
|
||||
class StringResource(
|
||||
|
@ -27,6 +34,16 @@ sealed class UiText {
|
|||
) : UiText() {
|
||||
override fun toString(): String =
|
||||
"resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}"
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is StringResource) return false
|
||||
return this.resId == other.resId && this.args == other.args
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = resId
|
||||
result = 31 * result + args.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
fun asStringNull(context: Context?): String? {
|
||||
|
|
|
@ -46,6 +46,7 @@ import com.lagradost.cloudstream3.mvvm.Resource
|
|||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.BaseAdapter
|
||||
import com.lagradost.cloudstream3.ui.home.HomeFragment
|
||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
|
||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan
|
||||
|
@ -54,8 +55,9 @@ import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.updateChips
|
|||
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
|
||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.ownHide
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||
|
@ -107,13 +109,16 @@ class SearchFragment : Fragment() {
|
|||
)
|
||||
bottomSheetDialog?.ownShow()
|
||||
|
||||
val layout = if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search
|
||||
|
||||
val root = inflater.inflate(layout, container, false)
|
||||
// TODO TRYCATCH
|
||||
binding = FragmentSearchBinding.bind(root)
|
||||
binding = try {
|
||||
val layout = if (isLayout(TV or EMULATOR)) R.layout.fragment_search_tv else R.layout.fragment_search
|
||||
val root = inflater.inflate(layout, container, false)
|
||||
FragmentSearchBinding.bind(root)
|
||||
} catch (t : Throwable) {
|
||||
FragmentSearchBinding.inflate(inflater)
|
||||
}
|
||||
|
||||
return root
|
||||
return binding?.root
|
||||
}
|
||||
|
||||
private fun fixGrid() {
|
||||
|
@ -157,7 +162,8 @@ class SearchFragment : Fragment() {
|
|||
**/
|
||||
fun search(query: String?) {
|
||||
if (query == null) return
|
||||
|
||||
// don't resume state from prev search
|
||||
(binding?.searchMasterRecycler?.adapter as? BaseAdapter<*,*>)?.clear()
|
||||
context?.let { ctx ->
|
||||
val default = enumValues<TvType>().sorted().filter { it != TvType.NSFW }
|
||||
.map { it.ordinal.toString() }.toSet()
|
||||
|
@ -369,7 +375,7 @@ class SearchFragment : Fragment() {
|
|||
|
||||
selectedSearchTypes = DataStoreHelper.searchPreferenceTags.toMutableList()
|
||||
|
||||
if (isTrueTvSettings()) {
|
||||
if (isLayout(TV)) {
|
||||
binding?.searchFilter?.isFocusable = true
|
||||
binding?.searchFilter?.isFocusableInTouchMode = true
|
||||
}
|
||||
|
@ -502,8 +508,8 @@ class SearchFragment : Fragment() {
|
|||
}*/
|
||||
//main_search.onActionViewExpanded()*/
|
||||
|
||||
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
||||
ParentItemAdapter(mutableListOf(), { callback ->
|
||||
val masterAdapter =
|
||||
ParentItemAdapter(fragment = this, id = "masterAdapter".hashCode(), { callback ->
|
||||
SearchHelper.handleSearchClickCallback(callback)
|
||||
}, { item ->
|
||||
bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.lagradost.cloudstream3.ui.search
|
||||
|
||||
import android.app.Activity
|
||||
import android.widget.Toast
|
||||
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
|
@ -10,7 +9,8 @@ import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_PLAY_FILE
|
|||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
||||
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
|
||||
import com.lagradost.cloudstream3.ui.result.START_ACTION_LOAD_EP
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
|
@ -56,7 +56,7 @@ object SearchHelper {
|
|||
}
|
||||
}
|
||||
SEARCH_ACTION_SHOW_METADATA -> {
|
||||
if(!isTvSettings()) { // we only want this on phone as UI is not done yet on tv
|
||||
if(isLayout(PHONE)) { // we only want this on phone as UI is not done yet on tv
|
||||
(activity as? MainActivity?)?.apply {
|
||||
loadPopup(callback.card)
|
||||
} ?: kotlin.run {
|
||||
|
|
|
@ -17,7 +17,8 @@ import com.lagradost.cloudstream3.SearchQuality
|
|||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.isMovieType
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
|
||||
|
@ -164,7 +165,7 @@ object SearchResultBuilder {
|
|||
|
||||
bg.isFocusable = false
|
||||
bg.isFocusableInTouchMode = false
|
||||
if(!isTrueTvSettings()) {
|
||||
if(!isLayout(TV)) {
|
||||
bg.setOnClickListener {
|
||||
click(it)
|
||||
}
|
||||
|
@ -207,7 +208,7 @@ object SearchResultBuilder {
|
|||
|
||||
*/
|
||||
|
||||
if (isTrueTvSettings()) {
|
||||
if (isLayout(TV)) {
|
||||
// bg.isFocusable = true
|
||||
// bg.isFocusableInTouchMode = true
|
||||
// bg.touchscreenBlocksFocus = false
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package com.lagradost.cloudstream3.ui.settings
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.R
|
||||
|
||||
object Globals {
|
||||
var beneneCount = 0
|
||||
|
||||
const val PHONE : Int = 0b001
|
||||
const val TV : Int = 0b010
|
||||
const val EMULATOR : Int = 0b100
|
||||
private const val INVALID = -1
|
||||
private var layoutId = INVALID
|
||||
|
||||
private fun Context.getLayoutInt(): Int {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
return settingsManager.getInt(this.getString(R.string.app_layout_key), -1)
|
||||
}
|
||||
|
||||
private fun Context.isAutoTv(): Boolean {
|
||||
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
|
||||
// AFT = Fire TV
|
||||
val model = Build.MODEL.lowercase()
|
||||
return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains(
|
||||
"AFT"
|
||||
) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast")
|
||||
}
|
||||
|
||||
private fun Context.layoutIntCorrected(): Int {
|
||||
return when(getLayoutInt()) {
|
||||
-1 -> if (isAutoTv()) TV else PHONE
|
||||
0 -> PHONE
|
||||
1 -> TV
|
||||
2 -> EMULATOR
|
||||
else -> PHONE
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.updateTv() {
|
||||
layoutId = layoutIntCorrected()
|
||||
}
|
||||
|
||||
/** Returns true if the layout is any of the flags,
|
||||
* so isLayout(TV or EMULATOR) is a valid statement for checking if the layout is in the emulator
|
||||
* or tv. Auto will become the "TV" or the "PHONE" layout.
|
||||
*
|
||||
* Valid flags are: PHONE, TV, EMULATOR
|
||||
* */
|
||||
fun isLayout(flags: Int) : Boolean {
|
||||
return (layoutId and flags) != 0
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import androidx.core.view.isVisible
|
|||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
||||
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
||||
|
@ -25,12 +26,17 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
|||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.pcloudApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.InAppOAuth2API
|
||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
|
@ -38,13 +44,20 @@ import com.lagradost.cloudstream3.ui.settings.helpers.settings.account.InAppAuth
|
|||
import com.lagradost.cloudstream3.ui.settings.helpers.settings.account.InAppOAuth2DialogBuilder
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.html
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo
|
||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
|
||||
class SettingsAccount : PreferenceFragmentCompat() {
|
||||
class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback {
|
||||
companion object {
|
||||
/** Used by nginx plugin too */
|
||||
fun showLoginInfo(
|
||||
|
@ -78,7 +91,7 @@ class SettingsAccount : PreferenceFragmentCompat() {
|
|||
showAccountSwitch(activity, api)
|
||||
}
|
||||
|
||||
if (isTvSettings()) {
|
||||
if (isLayout(TV or EMULATOR)) {
|
||||
binding.accountSwitchAccount.requestFocus()
|
||||
}
|
||||
}
|
||||
|
@ -135,6 +148,31 @@ class SettingsAccount : PreferenceFragmentCompat() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateAuthPreference(enabled: Boolean) {
|
||||
val biometricKey = getString(R.string.biometric_key)
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(context ?: return).edit()
|
||||
.putBoolean(biometricKey, enabled).apply()
|
||||
findPreference<SwitchPreferenceCompat>(biometricKey)?.isChecked = enabled
|
||||
}
|
||||
|
||||
override fun onAuthenticationError() {
|
||||
updateAuthPreference(!isAuthEnabled(context ?: return))
|
||||
}
|
||||
|
||||
override fun onAuthenticationSuccess() {
|
||||
if (isAuthEnabled(context?: return)) {
|
||||
updateAuthPreference(true)
|
||||
BackupUtils.backup(activity)
|
||||
activity?.showBottomDialogText(
|
||||
getString(R.string.biometric_setting),
|
||||
getString(R.string.biometric_warning).html()
|
||||
) { onDialogDismissedEvent }
|
||||
} else {
|
||||
updateAuthPreference(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setUpToolbar(R.string.category_account)
|
||||
|
@ -146,22 +184,22 @@ class SettingsAccount : PreferenceFragmentCompat() {
|
|||
hideKeyboard()
|
||||
setPreferencesFromResource(R.xml.settings_account, rootKey)
|
||||
|
||||
getPref(R.string.biometric_key)?.setOnPreferenceClickListener {
|
||||
val authEnabled = PreferenceManager.getDefaultSharedPreferences(
|
||||
context ?: return@setOnPreferenceClickListener false
|
||||
)
|
||||
.getBoolean(getString(R.string.biometric_key), false)
|
||||
getPref(R.string.biometric_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener {
|
||||
val ctx = context ?: return@setOnPreferenceClickListener false
|
||||
|
||||
if (authEnabled) {
|
||||
BackupUtils.backup(activity)
|
||||
val title = activity?.getString(R.string.biometric_setting)
|
||||
val warning = activity?.getString(R.string.biometric_warning)
|
||||
activity?.showBottomDialogText(
|
||||
title as String,
|
||||
warning.html()
|
||||
) { onDialogDismissedEvent }
|
||||
if (deviceHasPasswordPinLock(ctx)) {
|
||||
startBiometricAuthentication(
|
||||
activity?: return@setOnPreferenceClickListener false,
|
||||
R.string.biometric_authentication_title,
|
||||
false
|
||||
)
|
||||
promptInfo?.let {
|
||||
authCallback = this
|
||||
biometricPrompt?.authenticate(it)
|
||||
}
|
||||
}
|
||||
true
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
val syncApis =
|
||||
|
@ -170,8 +208,9 @@ class SettingsAccount : PreferenceFragmentCompat() {
|
|||
R.string.anilist_key to aniListApi,
|
||||
R.string.simkl_key to simklApi,
|
||||
R.string.opensubtitles_key to openSubtitlesApi,
|
||||
R.string.subdl_key to subDlApi,
|
||||
R.string.gdrive_key to googleDriveApi,
|
||||
R.string.pcloud_key to pcloudApi
|
||||
R.string.pcloud_key to pcloudApi,
|
||||
)
|
||||
|
||||
for ((key, api) in syncApis) {
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package com.lagradost.cloudstream3.ui.settings
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -12,36 +9,43 @@ import android.widget.ImageView
|
|||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.lagradost.cloudstream3.BuildConfig
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.MainSettingsBinding
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.BackupApis
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
|
||||
import com.lagradost.cloudstream3.ui.home.HomeFragment
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
class SettingsFragment : Fragment() {
|
||||
companion object {
|
||||
var beneneCount = 0
|
||||
|
||||
private var isTv: Boolean = false
|
||||
private var isTrueTv: Boolean = false
|
||||
|
||||
fun PreferenceFragmentCompat?.getPref(id: Int): Preference? {
|
||||
if (this == null) return null
|
||||
|
@ -54,16 +58,40 @@ class SettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide many Preferences on selected layouts.
|
||||
**/
|
||||
fun PreferenceFragmentCompat?.hidePrefs(ids: List<Int>, layoutFlags: Int) {
|
||||
if (this == null) return
|
||||
|
||||
try {
|
||||
ids.forEach {
|
||||
getPref(it)?.isVisible = !isLayout(layoutFlags)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the Preference on selected layouts.
|
||||
**/
|
||||
fun Preference?.hideOn(layoutFlags: Int): Preference? {
|
||||
if (this == null) return null
|
||||
this.isVisible = !isLayout(layoutFlags)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* On TV you cannot properly scroll to the bottom of settings, this fixes that.
|
||||
* */
|
||||
fun PreferenceFragmentCompat.setPaddingBottom() {
|
||||
if (isTvSettings()) {
|
||||
if (isLayout(TV or EMULATOR)) {
|
||||
listView?.setPadding(0, 0, 0, 100.toPx)
|
||||
}
|
||||
}
|
||||
fun PreferenceFragmentCompat.setToolBarScrollFlags() {
|
||||
if (isTvSettings()) {
|
||||
if (isLayout(TV or EMULATOR)) {
|
||||
val settingsAppbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar)
|
||||
|
||||
settingsAppbar?.updateLayoutParams<AppBarLayout.LayoutParams> {
|
||||
|
@ -72,7 +100,7 @@ class SettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
fun Fragment?.setToolBarScrollFlags() {
|
||||
if (isTvSettings()) {
|
||||
if (isLayout(TV or EMULATOR)) {
|
||||
val settingsAppbar = this?.view?.findViewById<MaterialToolbar>(R.id.settings_toolbar)
|
||||
|
||||
settingsAppbar?.updateLayoutParams<AppBarLayout.LayoutParams> {
|
||||
|
@ -87,12 +115,14 @@ class SettingsFragment : Fragment() {
|
|||
|
||||
settingsToolbar.apply {
|
||||
setTitle(title)
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
if (isLayout(PHONE or EMULATOR)) {
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
fixPaddingStatusbar(settingsToolbar)
|
||||
UIHelper.fixPaddingStatusbar(settingsToolbar)
|
||||
}
|
||||
|
||||
fun Fragment?.setUpToolbar(@StringRes title: Int) {
|
||||
|
@ -102,13 +132,15 @@ class SettingsFragment : Fragment() {
|
|||
|
||||
settingsToolbar.apply {
|
||||
setTitle(title)
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
if (isLayout(PHONE or EMULATOR)) {
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
fixPaddingStatusbar(settingsToolbar)
|
||||
UIHelper.fixPaddingStatusbar(settingsToolbar)
|
||||
}
|
||||
|
||||
fun getFolderSize(dir: File): Long {
|
||||
|
@ -124,60 +156,7 @@ class SettingsFragment : Fragment() {
|
|||
|
||||
return size
|
||||
}
|
||||
|
||||
private fun Context.getLayoutInt(): Int {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
return settingsManager.getInt(this.getString(R.string.app_layout_key), -1)
|
||||
}
|
||||
|
||||
private fun Context.isTvSettings(): Boolean {
|
||||
var value = getLayoutInt()
|
||||
if (value == -1) {
|
||||
value = if (isAutoTv()) 1 else 0
|
||||
}
|
||||
return value == 1 || value == 2
|
||||
}
|
||||
|
||||
private fun Context.isTrueTvSettings(): Boolean {
|
||||
var value = getLayoutInt()
|
||||
if (value == -1) {
|
||||
value = if (isAutoTv()) 1 else 0
|
||||
}
|
||||
return value == 1
|
||||
}
|
||||
|
||||
fun Context.updateTv() {
|
||||
isTrueTv = isTrueTvSettings()
|
||||
isTv = isTvSettings()
|
||||
}
|
||||
|
||||
fun isTrueTvSettings(): Boolean {
|
||||
return isTrueTv
|
||||
}
|
||||
|
||||
fun isTvSettings(): Boolean {
|
||||
return isTv
|
||||
}
|
||||
|
||||
fun Context.isEmulatorSettings(): Boolean {
|
||||
return getLayoutInt() == 2
|
||||
}
|
||||
|
||||
// phone exclusive
|
||||
fun isTruePhone(): Boolean {
|
||||
return !isTrueTvSettings() && !isTvSettings() && context?.isEmulatorSettings() != true
|
||||
}
|
||||
|
||||
private fun Context.isAutoTv(): Boolean {
|
||||
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
|
||||
// AFT = Fire TV
|
||||
val model = Build.MODEL.lowercase()
|
||||
return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains(
|
||||
"AFT"
|
||||
) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
|
@ -192,7 +171,6 @@ class SettingsFragment : Fragment() {
|
|||
val localBinding = MainSettingsBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root
|
||||
//return inflater.inflate(R.layout.main_settings, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -200,23 +178,44 @@ class SettingsFragment : Fragment() {
|
|||
activity?.navigate(id, Bundle())
|
||||
}
|
||||
|
||||
// used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}")
|
||||
/** used to debug leaks
|
||||
showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} :
|
||||
${VideoDownloadManager.downloadProgressEvent.size}") **/
|
||||
|
||||
val isTrueTv = isTrueTvSettings()
|
||||
fun hasProfilePictureFromAccountManagers(accountManagers: List<AccountManager>): Boolean {
|
||||
for (syncApi in accountManagers) {
|
||||
val login = syncApi.loginInfo()
|
||||
val pic = login?.profilePicture ?: continue
|
||||
|
||||
for (syncApi in accountManagers) {
|
||||
val login = syncApi.loginInfo()
|
||||
val pic = login?.profilePicture ?: continue
|
||||
if (binding?.settingsProfilePic?.setImage(
|
||||
pic,
|
||||
errorImageDrawable = HomeFragment.errorProfilePic
|
||||
) == true
|
||||
) {
|
||||
binding?.settingsProfileText?.text = login.name
|
||||
binding?.settingsProfile?.isVisible = true
|
||||
break
|
||||
if (binding?.settingsProfilePic?.setImage(
|
||||
pic,
|
||||
errorImageDrawable = HomeFragment.errorProfilePic
|
||||
) == true
|
||||
) {
|
||||
binding?.settingsProfileText?.text = login.name
|
||||
return true // sync profile exists
|
||||
}
|
||||
}
|
||||
return false // not syncing
|
||||
}
|
||||
|
||||
// display local account information if not syncing
|
||||
if (!hasProfilePictureFromAccountManagers(accountManagers)) {
|
||||
val activity = activity ?: return
|
||||
val currentAccount = try {
|
||||
DataStoreHelper.accounts.firstOrNull {
|
||||
it.keyIndex == DataStoreHelper.selectedKeyIndex
|
||||
} ?: activity.let { DataStoreHelper.getDefaultAccount(activity) }
|
||||
|
||||
} catch (t: IllegalStateException) {
|
||||
Log.e("AccountManager", "Activity not found", t)
|
||||
null
|
||||
}
|
||||
|
||||
binding?.settingsProfilePic?.setImage(currentAccount?.image)
|
||||
binding?.settingsProfileText?.text = currentAccount?.name
|
||||
}
|
||||
|
||||
binding?.apply {
|
||||
listOf(
|
||||
settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general,
|
||||
|
@ -231,7 +230,7 @@ class SettingsFragment : Fragment() {
|
|||
setOnClickListener {
|
||||
navigate(navigationId)
|
||||
}
|
||||
if (isTrueTv) {
|
||||
if (isLayout(TV)) {
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
}
|
||||
|
@ -255,9 +254,22 @@ class SettingsFragment : Fragment() {
|
|||
}
|
||||
|
||||
// Default focus on TV
|
||||
if (isTrueTv) {
|
||||
if (isLayout(TV)) {
|
||||
settingsGeneral.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
val appVersion = getString(R.string.app_version)
|
||||
val commitInfo = getString(R.string.commit_hash)
|
||||
val buildTimestamp = SimpleDateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG,
|
||||
Locale.getDefault()
|
||||
).apply { timeZone = TimeZone.getTimeZone("UTC")
|
||||
}.format(Date(BuildConfig.BUILD_DATE)).replace("UTC", "")
|
||||
|
||||
binding?.buildDate?.text = buildTimestamp
|
||||
binding?.appVersionInfo?.setOnLongClickListener {
|
||||
clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo $buildTimestamp")
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,10 +28,18 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
|||
import com.lagradost.cloudstream3.network.initClient
|
||||
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
|
||||
import com.lagradost.cloudstream3.ui.EasterEggMonke
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.isAppRestricted
|
||||
import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.showBatteryOptimizationDialog
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
|
@ -46,7 +54,6 @@ import com.lagradost.safefile.SafeFile
|
|||
|
||||
// Change local language settings in the app.
|
||||
fun getCurrentLocale(context: Context): String {
|
||||
// val dm = res.displayMetrics
|
||||
val res = context.resources
|
||||
val conf = res.configuration
|
||||
|
||||
|
@ -96,6 +103,7 @@ val appLanguages = arrayListOf(
|
|||
Triple("", "македонски", "mk"),
|
||||
Triple("", "മലയാളം", "ml"),
|
||||
Triple("", "bahasa Melayu", "ms"),
|
||||
Triple("", "Malti", "mt"),
|
||||
Triple("", "ဗမာစာ", "my"),
|
||||
Triple("", "नेपाली", "ne"),
|
||||
Triple("", "Nederlands", "nl"),
|
||||
|
@ -212,6 +220,18 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
getPref(R.string.battery_optimisation_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener {
|
||||
val ctx = context ?: return@setOnPreferenceClickListener false
|
||||
|
||||
if (isAppRestricted(ctx)) {
|
||||
showBatteryOptimizationDialog(ctx)
|
||||
} else {
|
||||
showToast(R.string.app_unrestricted_toast)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fun showAdd() {
|
||||
val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } }
|
||||
activity?.showDialog(
|
||||
|
@ -387,30 +407,30 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
}
|
||||
|
||||
try {
|
||||
SettingsFragment.beneneCount =
|
||||
beneneCount =
|
||||
settingsManager.getInt(getString(R.string.benene_count), 0)
|
||||
getPref(R.string.benene_count)?.let { pref ->
|
||||
pref.summary =
|
||||
if (SettingsFragment.beneneCount <= 0) getString(R.string.benene_count_text_none) else getString(
|
||||
if (beneneCount <= 0) getString(R.string.benene_count_text_none) else getString(
|
||||
R.string.benene_count_text
|
||||
).format(
|
||||
SettingsFragment.beneneCount
|
||||
beneneCount
|
||||
)
|
||||
|
||||
pref.setOnPreferenceClickListener {
|
||||
try {
|
||||
SettingsFragment.beneneCount++
|
||||
if (SettingsFragment.beneneCount%20 == 0) {
|
||||
beneneCount++
|
||||
if (beneneCount%20 == 0) {
|
||||
val intent = Intent(context, EasterEggMonke::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
settingsManager.edit().putInt(
|
||||
getString(R.string.benene_count),
|
||||
SettingsFragment.beneneCount
|
||||
beneneCount
|
||||
)
|
||||
.apply()
|
||||
it.summary =
|
||||
getString(R.string.benene_count_text).format(SettingsFragment.beneneCount)
|
||||
getString(R.string.benene_count_text).format(beneneCount)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
|
|
@ -8,8 +8,14 @@ import androidx.preference.PreferenceManager
|
|||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hidePrefs
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
|
@ -34,6 +40,18 @@ class SettingsPlayer : PreferenceFragmentCompat() {
|
|||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.attachBackupListener(requireContext().getSyncPrefs()).self
|
||||
|
||||
//Hide specific prefs on TV/EMULATOR
|
||||
hidePrefs(
|
||||
listOf(
|
||||
R.string.pref_category_gestures_key,
|
||||
R.string.rotate_video_key,
|
||||
R.string.auto_rotate_video_key
|
||||
),
|
||||
TV or EMULATOR
|
||||
)
|
||||
|
||||
getPref(R.string.pref_category_android_tv_key)?.hideOn(PHONE)
|
||||
|
||||
getPref(R.string.video_buffer_length_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.video_buffer_length_names)
|
||||
val prefValues = resources.getIntArray(R.array.video_buffer_length_values)
|
||||
|
@ -230,6 +248,5 @@ class SettingsPlayer : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -10,11 +10,11 @@ import com.lagradost.cloudstream3.SearchQuality
|
|||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
|
||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
package com.lagradost.cloudstream3.ui.settings
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.TransactionTooLargeException
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
|
@ -21,6 +17,7 @@ import com.lagradost.cloudstream3.mvvm.logError
|
|||
import com.lagradost.cloudstream3.network.initClient
|
||||
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
|
||||
import com.lagradost.cloudstream3.services.BackupWorkManager
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
|
||||
|
@ -32,6 +29,7 @@ import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
|||
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
|
@ -39,6 +37,9 @@ import okhttp3.internal.closeQuietly
|
|||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStream
|
||||
import java.lang.System.currentTimeMillis
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class SettingsUpdates : PreferenceFragmentCompat() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -119,29 +120,22 @@ class SettingsUpdates : PreferenceFragmentCompat() {
|
|||
binding.text1.text = text
|
||||
|
||||
binding.copyBtt.setOnClickListener {
|
||||
// Can crash on too much text
|
||||
try {
|
||||
val serviceClipboard =
|
||||
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?)
|
||||
?: return@setOnClickListener
|
||||
val clip = ClipData.newPlainText("logcat", text)
|
||||
serviceClipboard.setPrimaryClip(clip)
|
||||
dialog.dismissSafe(activity)
|
||||
} catch (e: TransactionTooLargeException) {
|
||||
showToast(R.string.clipboard_too_large)
|
||||
}
|
||||
clipboardHelper(txt("Logcat"), text)
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
|
||||
binding.clearBtt.setOnClickListener {
|
||||
Runtime.getRuntime().exec("logcat -c")
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
|
||||
binding.saveBtt.setOnClickListener {
|
||||
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
|
||||
var fileStream: OutputStream? = null
|
||||
try {
|
||||
fileStream =
|
||||
VideoDownloadManager.setupStream(
|
||||
fileStream = VideoDownloadManager.setupStream(
|
||||
it.context,
|
||||
"logcat",
|
||||
"logcat_${date}",
|
||||
null,
|
||||
"txt",
|
||||
false
|
||||
|
@ -155,9 +149,11 @@ class SettingsUpdates : PreferenceFragmentCompat() {
|
|||
fileStream?.closeQuietly()
|
||||
}
|
||||
}
|
||||
|
||||
binding.closeBtt.setOnClickListener {
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager
|
|||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||
import com.lagradost.cloudstream3.ui.result.setText
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.downloadAllPluginsDialog
|
||||
|
@ -97,7 +98,7 @@ class ExtensionsFragment : Fragment() {
|
|||
nextLeft = R.id.nav_rail_view
|
||||
)
|
||||
|
||||
if (!isTrueTvSettings())
|
||||
if (!isLayout(TV))
|
||||
binding?.addRepoButton?.let { button ->
|
||||
button.post {
|
||||
setPadding(
|
||||
|
@ -286,7 +287,7 @@ class ExtensionsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
val isTv = isTrueTvSettings()
|
||||
val isTv = isLayout(TV)
|
||||
binding?.apply {
|
||||
addRepoButton.isGone = isTv
|
||||
addRepoButtonImageviewHolder.isVisible = isTv
|
||||
|
|
|
@ -17,7 +17,8 @@ import com.lagradost.cloudstream3.plugins.PluginManager
|
|||
import com.lagradost.cloudstream3.plugins.VotingApi.getVotes
|
||||
import com.lagradost.cloudstream3.ui.result.setText
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.html
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
|
@ -44,7 +45,7 @@ class PluginAdapter(
|
|||
private val plugins: MutableList<PluginViewData> = mutableListOf()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val layout = if(isTrueTvSettings()) R.layout.repository_item_tv else R.layout.repository_item
|
||||
val layout = if(isLayout(TV)) R.layout.repository_item_tv else R.layout.repository_item
|
||||
val inflated = LayoutInflater.from(parent.context).inflate(layout, parent, false)
|
||||
|
||||
return PluginViewHolder(
|
||||
|
|
|
@ -17,7 +17,9 @@ import com.lagradost.cloudstream3.mvvm.observe
|
|||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
|
||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import com.lagradost.cloudstream3.ui.settings.appLanguages
|
||||
|
@ -155,7 +157,7 @@ class PluginsFragment : Fragment() {
|
|||
pluginViewModel.handlePluginAction(activity, url, it, isLocal)
|
||||
}
|
||||
|
||||
if (isTvSettings()) {
|
||||
if (isLayout(TV or EMULATOR)) {
|
||||
// Scrolling down does not reveal the whole RecyclerView on TV, add to bypass that.
|
||||
binding?.pluginRecyclerView?.setPadding(0, 0, 0, 200.toPx)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.lifecycle.MutableLiveData
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
|
||||
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.amap
|
||||
|
@ -181,8 +182,11 @@ class PluginsViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) {
|
||||
val isAdult = settingsForProvider.enableAdult
|
||||
val plugins = getPlugins(repositoryUrl)
|
||||
val list = plugins.map { plugin ->
|
||||
val list = plugins.filter {
|
||||
return@filter !(it.second.tvTypes?.contains("NSFW") == true && !isAdult)
|
||||
}.map { plugin ->
|
||||
PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
package com.lagradost.cloudstream3.ui.settings.extensions
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
|
||||
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
|
||||
|
||||
class RepoAdapter(
|
||||
val isSetup: Boolean,
|
||||
|
@ -28,7 +24,7 @@ class RepoAdapter(
|
|||
private val repositories: MutableList<RepositoryData> = mutableListOf()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val layout = if (isTrueTvSettings()) RepositoryItemTvBinding.inflate(
|
||||
val layout = if (isLayout(TV)) RepositoryItemTvBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
|
@ -121,13 +117,9 @@ class RepoAdapter(
|
|||
}
|
||||
|
||||
repositoryItemRoot.setOnLongClickListener {
|
||||
val clipboardManager =
|
||||
activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?
|
||||
clipboardManager?.setPrimaryClip(ClipData.newPlainText("RepoUrl", repositoryData.url))
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
showToast(R.string.copyRepoUrl, Toast.LENGTH_SHORT)
|
||||
}
|
||||
return@setOnLongClickListener true
|
||||
val shareableRepoData = "${repositoryData.name} : \n ${repositoryData.url}"
|
||||
clipboardHelper(txt(R.string.repo_copy_label), shareableRepoData)
|
||||
true
|
||||
}
|
||||
|
||||
mainText.text = repositoryData.name
|
||||
|
|
|
@ -9,7 +9,9 @@ import androidx.fragment.app.FragmentActivity
|
|||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
|
||||
abstract class DialogBuilder<T : ViewBinding>(
|
||||
|
@ -82,7 +84,7 @@ abstract class DialogBuilder<T : ViewBinding>(
|
|||
private fun setItemVisibility(dialog: AlertDialog) {
|
||||
val visibilityMap = getVisibilityMap(dialog)
|
||||
|
||||
if (SettingsFragment.isTvSettings()) {
|
||||
if (isLayout(TV or EMULATOR)) {
|
||||
visibilityMap.forEach { (input, isVisible) ->
|
||||
input.isVisible = isVisible
|
||||
|
||||
|
@ -134,4 +136,4 @@ abstract class DialogBuilder<T : ViewBinding>(
|
|||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@ import com.lagradost.cloudstream3.databinding.FragmentTestingBinding
|
|||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.mvvm.observeNullable
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
|
||||
|
@ -62,7 +63,7 @@ class TestFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
if (isTrueTvSettings()) {
|
||||
if (isLayout(TV)) {
|
||||
providerTest.playPauseButton?.isFocusableInTouchMode = true
|
||||
providerTest.playPauseButton?.requestFocus()
|
||||
}
|
||||
|
@ -75,7 +76,7 @@ class TestFragment : Fragment() {
|
|||
|
||||
fun focusRecyclerView() {
|
||||
// Hack to make it possible to focus the recyclerview.
|
||||
if (isTrueTvSettings()) {
|
||||
if (isLayout(TV)) {
|
||||
providerTestRecyclerView.requestFocus()
|
||||
providerTestAppbar.setExpanded(false, true)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.widget.ArrayAdapter
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.FragmentSetupLayoutBinding
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
|
@ -89,6 +90,7 @@ class SetupFragmentLayout : Fragment() {
|
|||
|
||||
|
||||
nextBtt.setOnClickListener {
|
||||
setKey(HAS_DONE_SETUP_KEY, true)
|
||||
findNavController().navigate(R.id.navigation_home)
|
||||
}
|
||||
|
||||
|
|
|
@ -13,8 +13,8 @@ import android.view.ViewGroup
|
|||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import androidx.media3.common.text.Cue
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.google.android.gms.cast.TextTrackStyle
|
||||
import com.google.android.gms.cast.TextTrackStyle.*
|
||||
import com.jaredrummler.android.colorpicker.ColorPickerDialog
|
||||
|
@ -24,7 +24,9 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
|||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.ChromecastSubtitleSettingsBinding
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
|
@ -173,7 +175,7 @@ class ChromecastSubtitlesFragment : Fragment() {
|
|||
state = getCurrentSavedStyle()
|
||||
context?.updateState()
|
||||
|
||||
val isTvSettings = isTvSettings()
|
||||
val isTvSettings = isLayout(TV or EMULATOR)
|
||||
|
||||
fun View.setFocusableInTv() {
|
||||
this.isFocusableInTouchMode = isTvSettings
|
||||
|
|
|
@ -28,8 +28,10 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
|||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
|
@ -254,7 +256,7 @@ class SubtitlesFragment : Fragment() {
|
|||
state = getCurrentSavedStyle()
|
||||
context?.updateState()
|
||||
|
||||
val isTvTrueSettings = isTrueTvSettings()
|
||||
val isTvTrueSettings = isLayout(TV)
|
||||
|
||||
fun View.setFocusableInTv() {
|
||||
this.isFocusableInTouchMode = isTvTrueSettings
|
||||
|
|
|
@ -61,8 +61,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStri
|
|||
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
|
||||
import com.lagradost.cloudstream3.ui.WebviewFragment
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals
|
||||
import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel.Companion.downloadAll
|
||||
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
|
@ -78,7 +77,6 @@ import okhttp3.Cache
|
|||
import java.io.*
|
||||
import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
object AppUtils {
|
||||
fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) {
|
||||
|
@ -583,7 +581,7 @@ object AppUtils {
|
|||
//private val viewModel: ResultViewModel by activityViewModels()
|
||||
|
||||
private fun getResultsId(): Int {
|
||||
return if (isTvSettings()) {
|
||||
return if (Globals.isLayout(Globals.TV or Globals.EMULATOR)) {
|
||||
R.id.global_to_navigation_results_tv
|
||||
} else {
|
||||
R.id.global_to_navigation_results_phone
|
||||
|
@ -707,7 +705,7 @@ object AppUtils {
|
|||
* Sets the focus to the negative button when in TV and Emulator layout.
|
||||
**/
|
||||
fun AlertDialog.setDefaultFocus(buttonFocus: Int = DialogInterface.BUTTON_NEGATIVE) {
|
||||
if (!isTvSettings()) return
|
||||
if (!Globals.isLayout(Globals.TV or Globals.EMULATOR)) return
|
||||
this.getButton(buttonFocus).run {
|
||||
isFocusableInTouchMode = true
|
||||
requestFocus()
|
||||
|
|
|
@ -31,6 +31,7 @@ import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_T
|
|||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi.Companion.OPEN_SUBTITLES_USER_KEY
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi.Companion.SUBDL_SUBTITLES_USER_KEY
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
|
@ -48,7 +49,7 @@ import java.io.OutputStream
|
|||
import java.io.PrintWriter
|
||||
import java.lang.System.currentTimeMillis
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
object BackupUtils {
|
||||
enum class RestoreSource {
|
||||
|
@ -80,10 +81,13 @@ object BackupUtils {
|
|||
PLUGINS_KEY_LOCAL,
|
||||
|
||||
OPEN_SUBTITLES_USER_KEY,
|
||||
SUBDL_SUBTITLES_USER_KEY,
|
||||
|
||||
DOWNLOAD_EPISODE_CACHE,
|
||||
|
||||
"biometric_key", // can lock down users if backup is shared on a incompatible device
|
||||
"nginx_user" // Nginx user key
|
||||
"nginx_user", // Nginx user key
|
||||
"download_path_key" // No access rights after restore data from backup
|
||||
)
|
||||
|
||||
/** false if key should not be contained in backup */
|
||||
|
@ -378,4 +382,4 @@ object BackupUtils {
|
|||
|
||||
private fun String.withoutPrefix(restoreSource: BackupUtils.RestoreSource) =
|
||||
// will not remove sync prefix because it wont match (its not a bug its a feature ¯\_(ツ)_/¯ )
|
||||
removePrefix(restoreSource.prefix)
|
||||
removePrefix(restoreSource.prefix)
|
||||
|
|
|
@ -12,20 +12,20 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
|||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getString
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
|
||||
object BiometricAuthenticator {
|
||||
|
||||
const val TAG = "cs3Auth"
|
||||
private const val MAX_FAILED_ATTEMPTS = 3
|
||||
private var failedAttempts = 0
|
||||
const val TAG = "cs3Auth"
|
||||
|
||||
private var biometricManager: BiometricManager? = null
|
||||
var biometricPrompt: BiometricPrompt? = null
|
||||
var promptInfo: BiometricPrompt.PromptInfo? = null
|
||||
|
||||
var authCallback: BiometricAuthCallback? = null // listen to authentication success
|
||||
|
||||
private fun initializeBiometrics(activity: Activity) {
|
||||
|
@ -37,20 +37,12 @@ object BiometricAuthenticator {
|
|||
activity as FragmentActivity,
|
||||
executor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
showToast("$errString")
|
||||
Log.e(TAG, "$errorCode")
|
||||
failedAttempts++
|
||||
|
||||
if (failedAttempts >= MAX_FAILED_ATTEMPTS) {
|
||||
failedAttempts = 0
|
||||
activity.finish()
|
||||
} else {
|
||||
failedAttempts = 0
|
||||
activity.finish()
|
||||
}
|
||||
authCallback?.onAuthenticationError()
|
||||
//activity.finish()
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
|
@ -89,7 +81,6 @@ object BiometricAuthenticator {
|
|||
.setDescription(description)
|
||||
.setAllowedAuthenticators(authFlag)
|
||||
.build()
|
||||
|
||||
} else {
|
||||
// for apis < 30
|
||||
promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
|
@ -98,7 +89,6 @@ object BiometricAuthenticator {
|
|||
.setDeviceCredentialAllowed(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
} else {
|
||||
// fallback for A12+ when both fingerprint & Face unlock is absent but PIN is set
|
||||
promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
|
@ -114,7 +104,6 @@ object BiometricAuthenticator {
|
|||
var result = false
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
|
||||
when (biometricManager?.canAuthenticate(
|
||||
DEVICE_CREDENTIAL or BIOMETRIC_STRONG or BIOMETRIC_WEAK
|
||||
)) {
|
||||
|
@ -126,7 +115,6 @@ object BiometricAuthenticator {
|
|||
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> result = true
|
||||
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> result = false
|
||||
}
|
||||
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
when (biometricManager?.canAuthenticate()) {
|
||||
|
@ -153,12 +141,11 @@ object BiometricAuthenticator {
|
|||
// function to start authentication in any fragment or activity
|
||||
fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) {
|
||||
initializeBiometrics(activity)
|
||||
|
||||
authCallback = activity as? BiometricAuthCallback
|
||||
if (isBiometricHardWareAvailable()) {
|
||||
authCallback = activity as? BiometricAuthCallback
|
||||
authenticationDialog(activity, title, setDeviceCred)
|
||||
promptInfo?.let { biometricPrompt?.authenticate(it) }
|
||||
|
||||
} else {
|
||||
if (deviceHasPasswordPinLock(activity)) {
|
||||
authCallback = activity as? BiometricAuthCallback
|
||||
|
@ -171,7 +158,15 @@ object BiometricAuthenticator {
|
|||
}
|
||||
}
|
||||
|
||||
fun isAuthEnabled(ctx: Context):Boolean {
|
||||
return ctx.let {
|
||||
PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
.getBoolean(getString(ctx, R.string.biometric_key), false)
|
||||
}
|
||||
}
|
||||
|
||||
interface BiometricAuthCallback {
|
||||
fun onAuthenticationSuccess()
|
||||
fun onAuthenticationError()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ import com.lagradost.cloudstream3.extractors.FileMoonIn
|
|||
import com.lagradost.cloudstream3.extractors.FileMoonSx
|
||||
import com.lagradost.cloudstream3.extractors.Filesim
|
||||
import com.lagradost.cloudstream3.extractors.Fplayer
|
||||
import com.lagradost.cloudstream3.extractors.Geodailymotion
|
||||
import com.lagradost.cloudstream3.extractors.GMPlayer
|
||||
import com.lagradost.cloudstream3.extractors.Gdriveplayer
|
||||
import com.lagradost.cloudstream3.extractors.Gdriveplayerapi
|
||||
|
@ -83,6 +84,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream
|
|||
import com.lagradost.cloudstream3.extractors.Mcloud
|
||||
import com.lagradost.cloudstream3.extractors.Megacloud
|
||||
import com.lagradost.cloudstream3.extractors.Meownime
|
||||
import com.lagradost.cloudstream3.extractors.MetaGnathTuggers
|
||||
import com.lagradost.cloudstream3.extractors.Minoplres
|
||||
import com.lagradost.cloudstream3.extractors.MixDrop
|
||||
import com.lagradost.cloudstream3.extractors.MixDropBz
|
||||
|
@ -108,6 +110,7 @@ import com.lagradost.cloudstream3.extractors.Hotlinger
|
|||
import com.lagradost.cloudstream3.extractors.FourCX
|
||||
import com.lagradost.cloudstream3.extractors.PlayRu
|
||||
import com.lagradost.cloudstream3.extractors.FourPlayRu
|
||||
import com.lagradost.cloudstream3.extractors.FourPichive
|
||||
import com.lagradost.cloudstream3.extractors.HDMomPlayer
|
||||
import com.lagradost.cloudstream3.extractors.HDPlayerSystem
|
||||
import com.lagradost.cloudstream3.extractors.VideoSeyred
|
||||
|
@ -139,6 +142,7 @@ import com.lagradost.cloudstream3.extractors.Sbspeed
|
|||
import com.lagradost.cloudstream3.extractors.Sbthe
|
||||
import com.lagradost.cloudstream3.extractors.Sendvid
|
||||
import com.lagradost.cloudstream3.extractors.ShaveTape
|
||||
import com.lagradost.cloudstream3.extractors.Simpulumlamerop
|
||||
import com.lagradost.cloudstream3.extractors.Solidfiles
|
||||
import com.lagradost.cloudstream3.extractors.Ssbstream
|
||||
import com.lagradost.cloudstream3.extractors.StreamM4u
|
||||
|
@ -175,6 +179,7 @@ import com.lagradost.cloudstream3.extractors.UpstreamExtractor
|
|||
import com.lagradost.cloudstream3.extractors.Uqload
|
||||
import com.lagradost.cloudstream3.extractors.Uqload1
|
||||
import com.lagradost.cloudstream3.extractors.Uqload2
|
||||
import com.lagradost.cloudstream3.extractors.Urochsunloath
|
||||
import com.lagradost.cloudstream3.extractors.Userload
|
||||
import com.lagradost.cloudstream3.extractors.Userscloud
|
||||
import com.lagradost.cloudstream3.extractors.Uservideo
|
||||
|
@ -182,10 +187,12 @@ import com.lagradost.cloudstream3.extractors.Vanfem
|
|||
import com.lagradost.cloudstream3.extractors.Vicloud
|
||||
import com.lagradost.cloudstream3.extractors.VidSrcExtractor
|
||||
import com.lagradost.cloudstream3.extractors.VidSrcExtractor2
|
||||
import com.lagradost.cloudstream3.extractors.VidSrcTo
|
||||
import com.lagradost.cloudstream3.extractors.VideoVard
|
||||
import com.lagradost.cloudstream3.extractors.VideovardSX
|
||||
import com.lagradost.cloudstream3.extractors.Vidgomunime
|
||||
import com.lagradost.cloudstream3.extractors.Vidgomunimesb
|
||||
import com.lagradost.cloudstream3.extractors.Vidguardto
|
||||
import com.lagradost.cloudstream3.extractors.VidhideExtractor
|
||||
import com.lagradost.cloudstream3.extractors.Vidmoly
|
||||
import com.lagradost.cloudstream3.extractors.Vidmolyme
|
||||
|
@ -207,6 +214,7 @@ import com.lagradost.cloudstream3.extractors.Watchx
|
|||
import com.lagradost.cloudstream3.extractors.WcoStream
|
||||
import com.lagradost.cloudstream3.extractors.Wibufile
|
||||
import com.lagradost.cloudstream3.extractors.XStreamCdn
|
||||
import com.lagradost.cloudstream3.extractors.Yipsu
|
||||
import com.lagradost.cloudstream3.extractors.YourUpload
|
||||
import com.lagradost.cloudstream3.extractors.YoutubeExtractor
|
||||
import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor
|
||||
|
@ -217,6 +225,8 @@ import com.lagradost.cloudstream3.extractors.Zorofile
|
|||
import com.lagradost.cloudstream3.extractors.Zplayer
|
||||
import com.lagradost.cloudstream3.extractors.ZplayerV2
|
||||
import com.lagradost.cloudstream3.extractors.Ztreamhub
|
||||
import com.lagradost.cloudstream3.extractors.EPlayExtractor
|
||||
import com.lagradost.cloudstream3.extractors.Vtbe
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -299,7 +309,18 @@ enum class ExtractorLinkType {
|
|||
/** No support at the moment */
|
||||
TORRENT,
|
||||
/** No support at the moment */
|
||||
MAGNET,
|
||||
MAGNET;
|
||||
|
||||
// See https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
fun getMimeType(): String {
|
||||
return when (this) {
|
||||
VIDEO -> "video/mp4"
|
||||
M3U8 -> "application/x-mpegURL"
|
||||
DASH -> "application/dash+xml"
|
||||
TORRENT -> "application/x-bittorrent"
|
||||
MAGNET -> "application/x-bittorrent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun inferTypeFromUrl(url: String): ExtractorLinkType {
|
||||
|
@ -402,9 +423,29 @@ open class ExtractorLink constructor(
|
|||
open val extractorData: String? = null,
|
||||
open val type: ExtractorLinkType,
|
||||
) : VideoDownloadManager.IDownloadableMinimum {
|
||||
val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8
|
||||
val isDash : Boolean get() = type == ExtractorLinkType.DASH
|
||||
|
||||
val isM3u8: Boolean get() = type == ExtractorLinkType.M3U8
|
||||
val isDash: Boolean get() = type == ExtractorLinkType.DASH
|
||||
|
||||
// Cached video size
|
||||
private var videoSize: Long? = null
|
||||
|
||||
/**
|
||||
* Get video size in bytes with one head request. Only available for ExtractorLinkType.Video
|
||||
* @param timeoutSeconds timeout of the head request.
|
||||
*/
|
||||
suspend fun getVideoSize(timeoutSeconds: Long = 3L): Long? {
|
||||
// Content-Length is not applicable to other types of formats
|
||||
if (this.type != ExtractorLinkType.VIDEO) return null
|
||||
|
||||
videoSize = videoSize ?: runCatching {
|
||||
val response =
|
||||
app.head(this.url, headers = headers, referer = referer, timeout = timeoutSeconds)
|
||||
response.headers["Content-Length"]?.toLong()
|
||||
}.getOrNull()
|
||||
|
||||
return videoSize
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getAllHeaders() : Map<String, String> {
|
||||
if (referer.isBlank()) {
|
||||
|
@ -708,6 +749,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
FourCX(),
|
||||
PlayRu(),
|
||||
FourPlayRu(),
|
||||
FourPichive(),
|
||||
HDMomPlayer(),
|
||||
HDPlayerSystem(),
|
||||
VideoSeyred(),
|
||||
|
@ -849,6 +891,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
Streamlare(),
|
||||
VidSrcExtractor(),
|
||||
VidSrcExtractor2(),
|
||||
VidSrcTo(),
|
||||
PlayLtXyz(),
|
||||
AStreamHub(),
|
||||
Vidplay(),
|
||||
|
@ -864,7 +907,16 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
Megacloud(),
|
||||
VidhideExtractor(),
|
||||
StreamWishExtractor(),
|
||||
EmturbovidExtractor()
|
||||
EmturbovidExtractor(),
|
||||
Vtbe(),
|
||||
EPlayExtractor(),
|
||||
Vidguardto(),
|
||||
Simpulumlamerop(),
|
||||
Urochsunloath(),
|
||||
Yipsu(),
|
||||
MetaGnathTuggers(),
|
||||
Geodailymotion(),
|
||||
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.BuildConfig
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
|
||||
const val packageName = BuildConfig.APPLICATION_ID
|
||||
const val TAG = "PowerManagerAPI"
|
||||
|
||||
object BatteryOptimizationChecker {
|
||||
|
||||
fun isAppRestricted(context: Context?): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context != null) {
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return !powerManager.isIgnoringBatteryOptimizations(context.packageName)
|
||||
}
|
||||
|
||||
return false // below Marshmallow, it's always unrestricted when app is in background
|
||||
}
|
||||
|
||||
fun openBatteryOptimizationSettings(context: Context) {
|
||||
if (shouldShowBatteryOptimizationDialog(context)) {
|
||||
showBatteryOptimizationDialog(context)
|
||||
}
|
||||
}
|
||||
|
||||
fun showBatteryOptimizationDialog(context: Context) {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
try {
|
||||
context.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setTitle(R.string.battery_dialog_title)
|
||||
.setIcon(R.drawable.ic_battery)
|
||||
.setMessage(R.string.battery_dialog_message)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
intentOpenAppInfo(it)
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
settingsManager.edit()
|
||||
.putBoolean(context.getString(R.string.battery_optimisation_key), false)
|
||||
.apply()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "Error showing battery optimization dialog", t)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldShowBatteryOptimizationDialog(context: Context): Boolean {
|
||||
val isRestricted = isAppRestricted(context)
|
||||
val isOptimizedNotShown = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.battery_optimisation_key), true)
|
||||
return isRestricted && isOptimizedNotShown && isLayout(PHONE)
|
||||
}
|
||||
|
||||
private fun intentOpenAppInfo(context: Context) {
|
||||
val intent = Intent()
|
||||
try {
|
||||
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
.setData(Uri.fromParts("package", packageName, null))
|
||||
context.startActivity(intent, Bundle())
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "Unable to invoke any intent", t)
|
||||
if (t is ActivityNotFoundException) {
|
||||
showToast("Exception: Activity Not Found")
|
||||
} else {
|
||||
showToast(R.string.app_info_intent_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,9 @@ import com.lagradost.cloudstream3.databinding.BottomInputDialogBinding
|
|||
import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding
|
||||
import com.lagradost.cloudstream3.databinding.BottomTextDialogBinding
|
||||
import com.lagradost.cloudstream3.databinding.OptionsPopupTvBinding
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
|
@ -54,7 +56,7 @@ object SingleSelectionHelper {
|
|||
) {
|
||||
if (this == null) return
|
||||
|
||||
if (isTvSettings()) {
|
||||
if (isLayout(TV or EMULATOR)) {
|
||||
val binding = OptionsPopupTvBinding.inflate(layoutInflater)
|
||||
val dialog = AlertDialog.Builder(this, R.style.AlertDialogCustom)
|
||||
.setView(binding.root)
|
||||
|
|
|
@ -5,6 +5,8 @@ import android.annotation.SuppressLint
|
|||
import android.app.Activity
|
||||
import android.app.AppOpsManager
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
|
@ -14,12 +16,15 @@ import android.graphics.Color
|
|||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.TransactionTooLargeException
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.ListAdapter
|
||||
import android.widget.ListView
|
||||
import android.widget.Toast.LENGTH_LONG
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
|
@ -30,18 +35,17 @@ import androidx.appcompat.view.menu.MenuBuilder
|
|||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.alpha
|
||||
import androidx.core.graphics.blue
|
||||
import androidx.core.graphics.drawable.toBitmapOrNull
|
||||
import androidx.core.graphics.green
|
||||
import androidx.core.graphics.red
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.core.view.marginLeft
|
||||
import androidx.core.view.marginRight
|
||||
import androidx.core.view.marginTop
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
|
@ -55,20 +59,24 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
|||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions.bitmapTransform
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipDrawable
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.result.UiImage
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.result.UiText
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
object UIHelper {
|
||||
val Int.toPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt()
|
||||
val Float.toPx: Float get() = (this * Resources.getSystem().displayMetrics.density)
|
||||
|
@ -123,6 +131,35 @@ object UIHelper {
|
|||
)
|
||||
}
|
||||
|
||||
fun clipboardHelper(label: UiText, text: CharSequence) {
|
||||
val ctx = context ?: return
|
||||
try {
|
||||
ctx.let {
|
||||
val clip = ClipData.newPlainText(label.asString(ctx), text)
|
||||
val labelSuffix = txt(R.string.toast_copied).asString(ctx)
|
||||
ctx.getSystemService<ClipboardManager>()?.setPrimaryClip(clip)
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
showToast("${label.asString(ctx)} $labelSuffix")
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.e("ClipboardService", "$t")
|
||||
when (t) {
|
||||
is SecurityException -> {
|
||||
showToast(R.string.clipboard_permission_error)
|
||||
}
|
||||
|
||||
is TransactionTooLargeException -> {
|
||||
showToast(R.string.clipboard_too_large)
|
||||
}
|
||||
|
||||
else -> {
|
||||
showToast(R.string.clipboard_unknown_error, LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets ListView height dynamically based on the height of the items.
|
||||
|
@ -173,6 +210,14 @@ object UIHelper {
|
|||
}
|
||||
}
|
||||
|
||||
fun View?.setAppBarNoScrollFlagsOnTV() {
|
||||
if (isLayout(Globals.TV or EMULATOR)) {
|
||||
this?.updateLayoutParams<AppBarLayout.LayoutParams> {
|
||||
scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.hideKeyboard() {
|
||||
window?.decorView?.clearFocus()
|
||||
this.findViewById<View>(android.R.id.content)?.rootView?.let {
|
||||
|
@ -434,7 +479,7 @@ object UIHelper {
|
|||
}
|
||||
|
||||
fun Context.getStatusBarHeight(): Int {
|
||||
if (isTvSettings()) {
|
||||
if (isLayout(Globals.TV or EMULATOR)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
|
@ -536,7 +581,7 @@ object UIHelper {
|
|||
(View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||
//}
|
||||
|
||||
changeStatusBarState(isEmulatorSettings())
|
||||
changeStatusBarState(isLayout(EMULATOR))
|
||||
}
|
||||
|
||||
fun Context.shouldShowPIPMode(isInPlayer: Boolean): Boolean {
|
||||
|
|
|
@ -187,7 +187,7 @@ object VideoDownloadManager {
|
|||
private val DOWNLOAD_BAD_CONFIG =
|
||||
DownloadStatus(retrySame = false, tryNext = false, success = false)
|
||||
|
||||
private const val KEY_RESUME_PACKAGES = "download_resume"
|
||||
const val KEY_RESUME_PACKAGES = "download_resume"
|
||||
const val KEY_DOWNLOAD_INFO = "download_info"
|
||||
private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume"
|
||||
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
package com.lagradost.cloudstream3.utils.fcast
|
||||
|
||||
import android.content.Context
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdManager.ResolveListener
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
|
||||
class FcastManager {
|
||||
private var nsdManager: NsdManager? = null
|
||||
|
||||
// Used for receiver
|
||||
private val registrationListenerTcp = DefaultRegistrationListener()
|
||||
private fun getDeviceName(): String {
|
||||
return "${Build.MANUFACTURER}-${Build.MODEL}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the fcast service
|
||||
* @param registerReceiver If true will register the app as a compatible fcast receiver for discovery in other app
|
||||
*/
|
||||
fun init(context: Context, registerReceiver: Boolean) = ioSafe {
|
||||
nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||
val serviceType = "_fcast._tcp"
|
||||
|
||||
if (registerReceiver) {
|
||||
val serviceName = "$APP_PREFIX-${getDeviceName()}"
|
||||
|
||||
val serviceInfo = NsdServiceInfo().apply {
|
||||
this.serviceName = serviceName
|
||||
this.serviceType = serviceType
|
||||
this.port = TCP_PORT
|
||||
}
|
||||
|
||||
nsdManager?.registerService(
|
||||
serviceInfo,
|
||||
NsdManager.PROTOCOL_DNS_SD,
|
||||
registrationListenerTcp
|
||||
)
|
||||
}
|
||||
|
||||
nsdManager?.discoverServices(
|
||||
serviceType,
|
||||
NsdManager.PROTOCOL_DNS_SD,
|
||||
DefaultDiscoveryListener()
|
||||
)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
nsdManager?.unregisterService(registrationListenerTcp)
|
||||
}
|
||||
|
||||
inner class DefaultDiscoveryListener : NsdManager.DiscoveryListener {
|
||||
val tag = "DiscoveryListener"
|
||||
override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {
|
||||
Log.d(tag, "Discovery failed: $serviceType, error code: $errorCode")
|
||||
}
|
||||
|
||||
override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) {
|
||||
Log.d(tag, "Stop discovery failed: $serviceType, error code: $errorCode")
|
||||
}
|
||||
|
||||
override fun onDiscoveryStarted(serviceType: String?) {
|
||||
Log.d(tag, "Discovery started: $serviceType")
|
||||
}
|
||||
|
||||
override fun onDiscoveryStopped(serviceType: String?) {
|
||||
Log.d(tag, "Discovery stopped: $serviceType")
|
||||
}
|
||||
|
||||
override fun onServiceFound(serviceInfo: NsdServiceInfo?) {
|
||||
if (serviceInfo == null) return
|
||||
nsdManager?.resolveService(serviceInfo, object : ResolveListener {
|
||||
override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {
|
||||
}
|
||||
|
||||
override fun onServiceResolved(serviceInfo: NsdServiceInfo?) {
|
||||
if (serviceInfo == null) return
|
||||
|
||||
currentDevices.add(PublicDeviceInfo(serviceInfo))
|
||||
|
||||
Log.d(
|
||||
tag,
|
||||
"Service found: ${serviceInfo.serviceName}, Net: ${serviceInfo.host.hostAddress}"
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {
|
||||
if (serviceInfo == null) return
|
||||
|
||||
// May remove duplicates, but net and port is null here, preventing device specific identification
|
||||
currentDevices.removeAll {
|
||||
it.rawName == serviceInfo.serviceName
|
||||
}
|
||||
|
||||
Log.d(tag, "Service lost: ${serviceInfo.serviceName}")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val APP_PREFIX = "CloudStream"
|
||||
val currentDevices: MutableList<PublicDeviceInfo> = mutableListOf()
|
||||
|
||||
class DefaultRegistrationListener : NsdManager.RegistrationListener {
|
||||
val tag = "DiscoveryService"
|
||||
override fun onServiceRegistered(serviceInfo: NsdServiceInfo) {
|
||||
Log.d(tag, "Service registered: ${serviceInfo.serviceName}")
|
||||
}
|
||||
|
||||
override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||
Log.e(tag, "Service registration failed: errorCode=$errorCode")
|
||||
}
|
||||
|
||||
override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) {
|
||||
Log.d(tag, "Service unregistered: ${serviceInfo.serviceName}")
|
||||
}
|
||||
|
||||
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||
Log.e(tag, "Service unregistration failed: errorCode=$errorCode")
|
||||
}
|
||||
}
|
||||
|
||||
const val TCP_PORT = 46899
|
||||
}
|
||||
}
|
||||
|
||||
class PublicDeviceInfo(serviceInfo: NsdServiceInfo) {
|
||||
val rawName: String = serviceInfo.serviceName
|
||||
val host: String? = serviceInfo.host.hostAddress
|
||||
val name = rawName.replace("-", " ") + host?.let { " $it" }
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package com.lagradost.cloudstream3.utils.fcast
|
||||
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.safefile.closeQuietly
|
||||
import java.io.DataOutputStream
|
||||
import java.net.Socket
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
class FcastSession(private val hostAddress: String): AutoCloseable {
|
||||
val tag = "FcastSession"
|
||||
|
||||
private var socket: Socket? = null
|
||||
@Throws
|
||||
@WorkerThread
|
||||
fun open(): Socket {
|
||||
val socket = Socket(hostAddress, FcastManager.TCP_PORT)
|
||||
this.socket = socket
|
||||
return socket
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
socket?.closeQuietly()
|
||||
socket = null
|
||||
}
|
||||
|
||||
@Throws
|
||||
private fun acquireSocket(): Socket {
|
||||
return socket ?: open()
|
||||
}
|
||||
|
||||
fun ping() {
|
||||
sendMessage(Opcode.Ping, null)
|
||||
}
|
||||
|
||||
fun <T> sendMessage(opcode: Opcode, message: T) {
|
||||
ioSafe {
|
||||
val socket = acquireSocket()
|
||||
val outputStream = DataOutputStream(socket.getOutputStream())
|
||||
|
||||
val json = message?.toJson()
|
||||
val content = json?.toByteArray() ?: ByteArray(0)
|
||||
|
||||
// Little endian starting from 1
|
||||
// https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1
|
||||
val size = content.size + 1
|
||||
|
||||
val sizeArray = ByteArray(4) { num ->
|
||||
(size shr 8 * num and 0xff).toByte()
|
||||
}
|
||||
|
||||
Log.d(tag, "Sending message with size: $size, opcode: $opcode")
|
||||
outputStream.write(sizeArray)
|
||||
outputStream.write(ByteArray(1) { opcode.value })
|
||||
outputStream.write(content)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package com.lagradost.cloudstream3.utils.fcast
|
||||
|
||||
// See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1
|
||||
enum class Opcode(val value: Byte) {
|
||||
None(0),
|
||||
Play(1),
|
||||
Pause(2),
|
||||
Resume(3),
|
||||
Stop(4),
|
||||
Seek(5),
|
||||
PlaybackUpdate(6),
|
||||
VolumeUpdate(7),
|
||||
SetVolume(8),
|
||||
PlaybackError(9),
|
||||
SetSpeed(10),
|
||||
Version(11),
|
||||
Ping(12),
|
||||
Pong(13);
|
||||
}
|
||||
|
||||
|
||||
data class PlayMessage(
|
||||
val container: String,
|
||||
val url: String? = null,
|
||||
val content: String? = null,
|
||||
val time: Double? = null,
|
||||
val speed: Double? = null,
|
||||
val headers: Map<String, String>? = null
|
||||
)
|
||||
|
||||
data class SeekMessage(
|
||||
val time: Double
|
||||
)
|
||||
|
||||
data class PlaybackUpdateMessage(
|
||||
val generationTime: Long,
|
||||
val time: Double,
|
||||
val duration: Double,
|
||||
val state: Int,
|
||||
val speed: Double
|
||||
)
|
||||
|
||||
data class VolumeUpdateMessage(
|
||||
val generationTime: Long,
|
||||
val volume: Double
|
||||
)
|
||||
|
||||
data class PlaybackErrorMessage(
|
||||
val message: String
|
||||
)
|
||||
|
||||
data class SetSpeedMessage(
|
||||
val speed: Double
|
||||
)
|
||||
|
||||
data class SetVolumeMessage(
|
||||
val volume: Double
|
||||
)
|
||||
|
||||
data class VersionMessage(
|
||||
val version: Long
|
||||
)
|
9
app/src/main/res/drawable/hourglass_24.xml
Normal file
9
app/src/main/res/drawable/hourglass_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#9BA0A4"
|
||||
android:pathData="M320,800h320v-120q0,-66 -47,-113t-113,-47q-66,0 -113,47t-47,113v120ZM480,440q66,0 113,-47t47,-113v-120L320,160v120q0,66 47,113t113,47ZM160,880v-80h80v-120q0,-61 28.5,-114.5T348,480q-51,-32 -79.5,-85.5T240,280v-120h-80v-80h640v80h-80v120q0,61 -28.5,114.5T612,480q51,32 79.5,85.5T720,680v120h80v80L160,880ZM480,800ZM480,160Z"/>
|
||||
</vector>
|
12
app/src/main/res/drawable/ic_battery.xml
Normal file
12
app/src/main/res/drawable/ic_battery.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/white"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15.67,4L14,4L14,3c0,-0.55 -0.45,-1 -1,-1h-2c-0.55,0 -1,0.45 -1,1v1L8.33,4C7.6,4 7,4.6 7,5.33v15.33C7,21.4 7.6,22 8.34,22h7.32c0.74,0 1.34,-0.6 1.34,-1.33L17,5.33C17,4.6 16.4,4 15.67,4zM13,18h-2v-2h2v2zM13,13c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1v-3c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v3z" />
|
||||
|
||||
</vector>
|
13
app/src/main/res/drawable/rounded_outline.xml
Normal file
13
app/src/main/res/drawable/rounded_outline.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@android:color/white">
|
||||
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="?attr/white" />
|
||||
<corners android:radius="10dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
10
app/src/main/res/drawable/subdl_logo_big.xml
Normal file
10
app/src/main/res/drawable/subdl_logo_big.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="20dp"
|
||||
android:viewportHeight="320"
|
||||
android:viewportWidth="320"
|
||||
android:width="20dp">
|
||||
|
||||
<path android:fillColor="@color/white"
|
||||
android:pathData="m107.87,39.3l-8.44,8.59l0,35.68l0,35.83l18.95,22.5c10.36,12.44 30.35,36.27 44.41,53l25.46,30.5l0,12.14l0,12.29l-24.43,-0l-24.43,-0l0,-11.84l0,-11.84l-19.99,-0l-19.99,-0l0,23.24l0,23.24l8.44,8.59l8.44,8.59l48.26,-0l48.26,-0l7.7,-7.85l7.7,-7.85l0,-36.86l-0.15,-37.01l-23.98,-28.28c-13.18,-15.54 -33.16,-39.23 -44.26,-52.55l-20.43,-24.13l0,-12.29l0,-12.29l24.43,-0l24.43,-0l0,12.58l0,12.58l19.99,-0l19.99,-0l0,-24.87l0,-24.87l-7.85,-7.7l-7.85,-7.7l-48.11,-0l-48.11,-0l-8.44,8.59z"/>
|
||||
|
||||
</vector>
|
|
@ -62,14 +62,16 @@
|
|||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_switch_account"
|
||||
android:text="@string/switch_account"
|
||||
style="@style/SettingsItem" />
|
||||
android:id="@+id/account_switch_account"
|
||||
android:text="@string/switch_account"
|
||||
style="@style/SettingsItem"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_logout"
|
||||
android:text="@string/logout"
|
||||
style="@style/SettingsItem">
|
||||
android:id="@+id/account_logout"
|
||||
android:text="@string/logout"
|
||||
style="@style/SettingsItem"
|
||||
android:focusable="true">
|
||||
|
||||
<requestFocus />
|
||||
</TextView>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:orientation="horizontal"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:orientation="horizontal"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:focusable="true">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/account_profile_picture_holder"
|
||||
|
@ -15,16 +16,16 @@
|
|||
android:layout_height="30dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/account_profile_picture"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="ContentDescription" />
|
||||
android:id="@+id/account_profile_picture"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="ContentDescription" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<TextView
|
||||
android:foreground="@null"
|
||||
android:id="@+id/account_name"
|
||||
tools:text="Account 1"
|
||||
style="@style/SettingsItem" />
|
||||
android:foreground="@null"
|
||||
android:id="@+id/account_name"
|
||||
tools:text="Account 1"
|
||||
style="@style/SettingsItem" />
|
||||
</LinearLayout>
|
||||
|
||||
|
|
|
@ -7,18 +7,20 @@
|
|||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/account_list"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
tools:listitem="@layout/account_single"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_height="wrap_content" />
|
||||
android:id="@+id/account_list"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
tools:listitem="@layout/account_single"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_add"
|
||||
android:text="@string/add_account"
|
||||
style="@style/SettingsItem">
|
||||
android:id="@+id/account_add"
|
||||
android:text="@string/add_account"
|
||||
style="@style/SettingsItem"
|
||||
android:focusable="true">
|
||||
|
||||
<requestFocus />
|
||||
</TextView>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue