diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index c5c0ff3b..d7c08c9c 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,17 +4,16 @@
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 51cb26e3..79df7221 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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("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("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("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 {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt
index c93f0f9b..1680d698 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt
@@ -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()
)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
index 80de223e..82e985db 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
@@ -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) -> 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(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)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
index 7a25b738..07a82583 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
@@ -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? = 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..? = 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..? = 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,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
index 0cfe1b5f..8ff710fe 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -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? = 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 parse(text: String, kClass: KClass): T {
- return mapper.readValue(text, kClass.java)
- }
-
- override fun parseSafe(text: String, kClass: KClass): 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? = 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()
+
/**
* Used by DataStoreHelper to fully reload home when switching accounts
*/
val reloadHomeEvent = Event()
+
/**
* 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?) {
+ fun setUserData(status: Resource?) {
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() {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt
index f03a5525..26567c7a 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt
@@ -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()?.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
)
+
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt
index 0df93dc5..2343a92e 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt
@@ -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() ?: 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
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt
new file mode 100644
index 00000000..2cb12e16
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt
@@ -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? {
+ 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
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt
index b557a53e..db721108 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt
@@ -20,4 +20,9 @@ class PlayRu : ContentX() {
class FourPlayRu : ContentX() {
override var name = "FourPlayRu"
override var mainUrl = "https://four.playru.net"
-}
\ No newline at end of file
+}
+
+class FourPichive : ContentX() {
+ override var name = "FourPichive"
+ override var mainUrl = "https://four.pichive.online"
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt
new file mode 100644
index 00000000..2655670d
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt
@@ -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() ?: return
+ if (res.status != 200) return
+ res.result?.amap { source ->
+ try {
+ val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: 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?
+ )
+
+ 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)
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt
new file mode 100644
index 00000000..230a9e1a
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt
@@ -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(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
+ )
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt
index 615cfd74..979fd8c5 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt
@@ -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,
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt
index b9a07a6d..c5e01552 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt
@@ -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) {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt
index 2c6998de..67fd7eea 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt
@@ -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()
+
+ 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(decoded)
+ videoLinkDTO.let { videoLinks.add(it.toString()) }
+ }
+
+ videoLinks.forEach { videoLink ->
+ M3u8Helper.generateM3u8(
+ name,
+ videoLink,
+ "$mainUrl/",
+ headers = mapOf("Origin" to "$mainUrl/")
+ ).forEach(callback)
+ }
}
-}
\ No newline at end of file
+
+ data class WcoSources(
+ @JsonProperty("VideoLinkDTO") val VideoLinkDTO: String,
+ )
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt
new file mode 100644
index 00000000..919a9cbd
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt
@@ -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? {
+ 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
+ }
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
index 50301e22..c5b4d453 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
@@ -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
)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
new file mode 100644
index 00000000..8d149888
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
@@ -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>(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? {
+ val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query")
+
+ val results = parseJson>(apiResponse).map { element ->
+ element.toSearchResponse()
+ }
+
+ return results
+ }
+ override suspend fun load(url: String): LoadResponse {
+
+ val data = parseJson(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(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>(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()
+ val seasons = parseJson>(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? = null,
+ @JsonProperty("available_translations") val availableTranslations: List? = null,
+ @JsonProperty("genres") val genres: List? = 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? = null,
+ @JsonProperty("poster") val poster: List? = null,
+ @JsonProperty("logo") val logo: List? = null,
+ @JsonProperty("clearart") val clearart: List? = null,
+ @JsonProperty("banner") val banner: List? = null,
+ @JsonProperty("thumb") val thumb: List? = null,
+ @JsonProperty("screenshot") val screenshot: List? = null,
+ @JsonProperty("headshot") val headshot: List? = null,
+ )
+
+ data class People(
+ @JsonProperty("cast") val cast: List? = null,
+ )
+
+ data class Cast(
+ @JsonProperty("character") val character: String? = null,
+ @JsonProperty("characters") val characters: List? = 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? = 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? = 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,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt
new file mode 100644
index 00000000..3df5197c
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt
@@ -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 LifecycleOwner.observe(liveData: LiveData, 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 LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) {
+ liveData.removeObservers(this)
+ liveData.observe(this) { action(it) }
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
index 025e6fb6..a30af11c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
@@ -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()
diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
index adf5abfa..e2bcd6e1 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
@@ -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()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt
index f6424c4c..ed4ccb74 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt
@@ -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
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
index bc4048e0..c1fbee69 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
@@ -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"
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt
deleted file mode 100644
index 1adecce9..00000000
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt
+++ /dev/null
@@ -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 {
- 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()
-
- fun cleanResources(
- results: MutableList,
- 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()
-
- 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
-
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt
index 99723e90..7552fe9d 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt
@@ -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()
} + mapOf(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt
index ceca952d..61ae5965 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt
@@ -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
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt
deleted file mode 100644
index fbe05026..00000000
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt
+++ /dev/null
@@ -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? {
- 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
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt
new file mode 100644
index 00000000..29544e65
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt
@@ -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? {
+
+ 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()?.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()
+
+ if (tokenResponse?.token == null) return false
+
+ val apiResponse = app.get(
+ url = "$APIURL/user/userApi",
+ headers = mapOf(
+ "Authorization" to "Bearer ${tokenResponse.token}"
+ )
+ ).parsedSafe()
+
+ 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? = null,
+ @JsonProperty("subtitles") val subtitles: List? = 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,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt
new file mode 100644
index 00000000..d90177f5
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt
@@ -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(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>()
+}
+
+abstract class NoStateAdapter(fragment: Fragment) : BaseAdapter(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 = BaseDiffCallback()
+) : RecyclerView.Adapter>() {
+ 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 = 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?) {
+ // 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, item: T, position: Int) =
+ onBindContent(holder, item, position)
+
+ open fun onBindContent(holder: ViewHolderState, item: T, position: Int) = Unit
+ open fun onBindFooter(holder: ViewHolderState) = Unit
+ open fun onBindHeader(holder: ViewHolderState) = Unit
+ open fun onCreateContent(parent: ViewGroup): ViewHolderState = throw NotImplementedError()
+ open fun onCreateFooter(parent: ViewGroup): ViewHolderState = throw NotImplementedError()
+ open fun onCreateHeader(parent: ViewGroup): ViewHolderState = throw NotImplementedError()
+
+ override fun onViewAttachedToWindow(holder: ViewHolderState) {
+ holder.onViewAttachedToWindow()
+ }
+
+ override fun onViewDetachedFromWindow(holder: ViewHolderState) {
+ holder.onViewDetachedFromWindow()
+ }
+
+ fun save(recyclerView: RecyclerView) {
+ for (child in recyclerView.children) {
+ val holder =
+ recyclerView.findContainingViewHolder(child) as? ViewHolderState ?: continue
+ setState(holder)
+ }
+ }
+
+ fun clear() {
+ stateViewModel.layoutManagerStates[id]?.clear()
+ }
+
+ private fun getState(holder: ViewHolderState): S? =
+ stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S
+
+ private fun setState(holder: ViewHolderState) {
+ 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) {
+ setState(holder)
+ holder.onViewRecycled()
+ super.onViewRecycled(holder)
+ }
+
+ final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderState {
+ 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,
+ position: Int,
+ payloads: MutableList
+ ) {
+ 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, 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(
+ val itemSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() },
+ val contentSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() }
+) : DiffUtil.ItemCallback() {
+ 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()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt
new file mode 100644
index 00000000..f721401e
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt
@@ -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)
+ }
+}
+
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt
index 60260edf..de0b5c05 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt
@@ -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
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt
index 1dae5a0f..0b0d83db 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt
@@ -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()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt
index c3ec2bbd..f54c8698 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt
@@ -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 =
DownloadChildAdapter(
ArrayList(),
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
index 27c2e1a3..31790b0f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
@@ -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)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt
index d20fcf93..f1031c24 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt
@@ -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
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt
index f84966eb..ebed901f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt
@@ -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,
+class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState(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() {
+ BaseAdapter(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 {
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,
+ 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) {
- 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,
- private val newList: List
-) :
- 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
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt
index cd843517..12185cbf 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt
@@ -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()
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
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt
index 443278a9..4b0360d7 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt
@@ -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,
- //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() {
- 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) {
- updateList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
- .toMutableList())
- }
-
- @JvmName("updateListExpandableHomepageList")
- fun updateList(
- newList: MutableList,
- 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(
+ 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(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?) {
+ super.submitList(list?.sortedBy { it.list.list.isEmpty() })
+ }
+
+ override fun onUpdateContent(
+ holder: ViewHolderState,
+ 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,
+ 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,
- private val newList: List
-) :
- 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) {
+ submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
+ .toMutableList())
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt
index 0e397f81..52ec06db 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt
@@ -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,
+ 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 {
+ 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) {
+ (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(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("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("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>(
@@ -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) {
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>) {
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" }
+ }
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt
index 666fbc24..29186e83 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt
@@ -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() {
- private var items: MutableList = mutableListOf()
+class HomeScrollAdapter(
+ fragment: Fragment
+) : NoStateAdapter(fragment) {
var hasMoreItems: Boolean = false
- fun getItem(position: Int): LoadResponse? {
- return items.getOrNull(position)
- }
-
- fun setItems(newItems: List, 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 {
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,
+ 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,
- private val newList: List
- ) :
- 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
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt
index f471fefd..a2c7583f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt
@@ -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>()
private val _preview = MutableLiveData>>>()
- private val previewResponses = mutableListOf()
+ private val previewResponses = CopyOnWriteArrayList()
private val previewResponsesAdded = mutableSetOf()
val resumeWatching: LiveData> = _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 =
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt
index 686156b4..6c9978bc 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt
@@ -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)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt
index c983ea2f..1bd01c86 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt
@@ -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 {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt
index 6731eae2..cfd22220 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt
@@ -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(binding) {
+ override fun save(): Bundle =
+ Bundle().apply {
+ putParcelable(
+ "pageRecyclerview",
+ binding.pageRecyclerview.layoutManager?.onSaveInstanceState()
+ )
+ }
+
+ override fun restore(state: Bundle) {
+ state.getParcelable("pageRecyclerview")?.let { recycle ->
+ binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle)
+ }
+ }
+}
+
class ViewpagerAdapter(
- var pages: List,
+ fragment: Fragment,
val scrollCallback: (isScrollingDown: Boolean) -> Unit,
val clickCallback: (SearchClickCallback) -> Unit
-) : RecyclerView.Adapter() {
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
- return PageViewHolder(
+) : BaseAdapter(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 {
+ 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,
+ 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()
+ override fun onBindContent(holder: ViewHolderState, 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(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(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
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt
index cfa6682d..0865b220 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt
@@ -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(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt
index 210bfdca..31adbc87 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt
@@ -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(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt
index 1e2ea540..4d8860f8 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt
@@ -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
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt
index e8d74752..aa25157b 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt
@@ -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()
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt
index e14514c1..b46b6aba 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt
@@ -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]
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt
index af74cb57..c5de1a1c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt
@@ -10,7 +10,8 @@ enum class LoadType {
InAppDownload,
ExternalApp,
Browser,
- Chromecast
+ Chromecast,
+ Fcast
}
fun LoadType.toSet() : Set {
@@ -29,12 +30,17 @@ fun LoadType.toSet() : Set {
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
+ )
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt
index 0d98f205..ee44567f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt
@@ -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() }
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt
index 6414374b..fb600ef1 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt
@@ -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()
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt
index 5b300c06..85e20d1c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt
@@ -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()
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt
index b12475bf..e4fd0559 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt
@@ -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
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt
index ca2934ef..7b7bae43 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt
@@ -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(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
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt
index a1574eec..1d3f5a08 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt
@@ -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,
)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt
index 6105e8c9..fb5160a7 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt
@@ -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
}
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt
index bd7105ee..13621cda 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt
@@ -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()
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt
index ef3db0b4..135dc530 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt
@@ -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
) {
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt
index a05b4059..4285feb1 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt
@@ -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) -> 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().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? = 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(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt
index 6fe45730..5a23bfc1 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt
@@ -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
@@ -72,8 +73,7 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter Unit
) {
- val isTrueTv = isTrueTvSettings()
- if (isTrueTv) {
+ if (isLayout(TV)) {
item.isFocusable = true
item.isFocusableInTouchMode = true
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt
index 24d56897..0e8160db 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt
@@ -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? {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt
index 243d9f4e..24e87d30 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt
@@ -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().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 =
- ParentItemAdapter(mutableListOf(), { callback ->
+ val masterAdapter =
+ ParentItemAdapter(fragment = this, id = "masterAdapter".hashCode(), { callback ->
SearchHelper.handleSearchClickCallback(callback)
}, { item ->
bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt
index 3e33e01a..5b943105 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt
@@ -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 {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt
index e1b72b30..d18c0197 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt
@@ -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
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt
new file mode 100644
index 00000000..aa513d87
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt
@@ -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
+ }
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt
index 4bc239f6..ba0b806f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt
@@ -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(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) {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt
index ee66ac24..b4c0a195 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt
@@ -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, 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(R.id.settings_toolbar)
settingsAppbar?.updateLayoutParams {
@@ -72,7 +100,7 @@ class SettingsFragment : Fragment() {
}
}
fun Fragment?.setToolBarScrollFlags() {
- if (isTvSettings()) {
+ if (isLayout(TV or EMULATOR)) {
val settingsAppbar = this?.view?.findViewById(R.id.settings_toolbar)
settingsAppbar?.updateLayoutParams {
@@ -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): 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
+ }
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt
index 7e558db9..8eb95e7c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt
@@ -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)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt
index d8346ade..a8466c03 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt
@@ -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
}
}
-
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt
index 7058c5be..744544dc 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt
@@ -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
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt
index f05f06e1..2c609624 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt
@@ -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
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt
index f0b8a0bd..ebd3260f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt
@@ -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
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt
index c3fb4fc2..04da30c7 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt
@@ -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 = 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(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt
index c5256ffa..acfbc584 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt
@@ -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)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt
index 151c8d57..2b026e0d 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt
@@ -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))
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt
index 7ac7cbb2..faf6d38b 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt
@@ -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 = 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
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/helpers/settings/account/DialogBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/helpers/settings/account/DialogBuilder.kt
index 0686cdc3..747e579b 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/helpers/settings/account/DialogBuilder.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/helpers/settings/account/DialogBuilder.kt
@@ -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(
@@ -82,7 +84,7 @@ abstract class DialogBuilder(
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(
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt
index 3fbd1131..7878afaa 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt
@@ -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)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt
index e6039105..6f2e3c66 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt
@@ -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)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt
index 71fac2ed..bb9558b8 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt
@@ -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
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt
index fa9191da..5aeb849d 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt
@@ -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
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt
index 1be966b6..ff27b192 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt
@@ -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()
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt
index 15dab1cc..01b16f5f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt
@@ -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)
\ No newline at end of file
+ removePrefix(restoreSource.prefix)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt
index de9b9963..c57600ee 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt
@@ -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()
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
index 637f65b9..c6cad804 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
@@ -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 {
if (referer.isBlank()) {
@@ -708,6 +749,7 @@ val extractorApis: MutableList = arrayListOf(
FourCX(),
PlayRu(),
FourPlayRu(),
+ FourPichive(),
HDMomPlayer(),
HDPlayerSystem(),
VideoSeyred(),
@@ -849,6 +891,7 @@ val extractorApis: MutableList = arrayListOf(
Streamlare(),
VidSrcExtractor(),
VidSrcExtractor2(),
+ VidSrcTo(),
PlayLtXyz(),
AStreamHub(),
Vidplay(),
@@ -864,7 +907,16 @@ val extractorApis: MutableList = arrayListOf(
Megacloud(),
VidhideExtractor(),
StreamWishExtractor(),
- EmturbovidExtractor()
+ EmturbovidExtractor(),
+ Vtbe(),
+ EPlayExtractor(),
+ Vidguardto(),
+ Simpulumlamerop(),
+ Urochsunloath(),
+ Yipsu(),
+ MetaGnathTuggers(),
+ Geodailymotion(),
+
)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt
new file mode 100644
index 00000000..27609730
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt
@@ -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)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt
index f34e7238..70edf80c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt
@@ -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)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt
index 6e925d15..cb527020 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt
@@ -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()?.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 {
+ scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL
+ }
+ }
+ }
+
fun Activity.hideKeyboard() {
window?.decorView?.clearFocus()
this.findViewById(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 {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt
index 50a8df02..7d4d5d98 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt
@@ -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"
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt
new file mode 100644
index 00000000..9ff5cc08
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt
@@ -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 = 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" }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt
new file mode 100644
index 00000000..1f33bca4
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt
@@ -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 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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt
new file mode 100644
index 00000000..61c00d6e
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt
@@ -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? = 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
+)
diff --git a/app/src/main/res/drawable/hourglass_24.xml b/app/src/main/res/drawable/hourglass_24.xml
new file mode 100644
index 00000000..7bd1ebbd
--- /dev/null
+++ b/app/src/main/res/drawable/hourglass_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_battery.xml b/app/src/main/res/drawable/ic_battery.xml
new file mode 100644
index 00000000..24d0a77f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_battery.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_outline.xml b/app/src/main/res/drawable/rounded_outline.xml
new file mode 100644
index 00000000..b85ace8e
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_outline.xml
@@ -0,0 +1,13 @@
+
+
+
+ -
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/subdl_logo_big.xml b/app/src/main/res/drawable/subdl_logo_big.xml
new file mode 100644
index 00000000..a6cbb311
--- /dev/null
+++ b/app/src/main/res/drawable/subdl_logo_big.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/account_managment.xml b/app/src/main/res/layout/account_managment.xml
index 389a3406..e7afb382 100644
--- a/app/src/main/res/layout/account_managment.xml
+++ b/app/src/main/res/layout/account_managment.xml
@@ -62,14 +62,16 @@
+ android:id="@+id/account_switch_account"
+ android:text="@string/switch_account"
+ style="@style/SettingsItem"
+ android:focusable="true"/>
+ android:id="@+id/account_logout"
+ android:text="@string/logout"
+ style="@style/SettingsItem"
+ android:focusable="true">
diff --git a/app/src/main/res/layout/account_single.xml b/app/src/main/res/layout/account_single.xml
index cbfb9f18..c4f7fa39 100644
--- a/app/src/main/res/layout/account_single.xml
+++ b/app/src/main/res/layout/account_single.xml
@@ -1,10 +1,11 @@
+ 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">
+ android:id="@+id/account_profile_picture"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:ignore="ContentDescription" />
+ android:foreground="@null"
+ android:id="@+id/account_name"
+ tools:text="Account 1"
+ style="@style/SettingsItem" />
diff --git a/app/src/main/res/layout/account_switch.xml b/app/src/main/res/layout/account_switch.xml
index 659ad840..5153f0e3 100644
--- a/app/src/main/res/layout/account_switch.xml
+++ b/app/src/main/res/layout/account_switch.xml
@@ -7,18 +7,20 @@
android:layout_height="match_parent">
+ 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"/>
+ android:id="@+id/account_add"
+ android:text="@string/add_account"
+ style="@style/SettingsItem"
+ android:focusable="true">
diff --git a/app/src/main/res/layout/bottom_resultview_preview.xml b/app/src/main/res/layout/bottom_resultview_preview.xml
index 4a64114e..3372fe7b 100644
--- a/app/src/main/res/layout/bottom_resultview_preview.xml
+++ b/app/src/main/res/layout/bottom_resultview_preview.xml
@@ -41,22 +41,34 @@
android:layout_marginStart="10dp"
android:orientation="vertical">
-
+ tools:text="The Perfect Run" />
-
+
-
+
+
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:padding="7dp">
+ tools:visibility="visible" />
+ tools:visibility="visible" />
diff --git a/app/src/main/res/layout/dialog_online_subtitles.xml b/app/src/main/res/layout/dialog_online_subtitles.xml
index 7803e261..e0eac5e0 100644
--- a/app/src/main/res/layout/dialog_online_subtitles.xml
+++ b/app/src/main/res/layout/dialog_online_subtitles.xml
@@ -40,7 +40,7 @@
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_gravity="center_vertical"
- android:layout_marginEnd="30dp">
+ android:layout_marginEnd="40dp">
@@ -106,7 +107,8 @@
android:layout_margin="10dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des"
- android:nextFocusLeft="@id/main_search"
+ android:focusable="true"
+ android:nextFocusLeft="@id/year_btt"
android:nextFocusRight="@id/main_search"
android:nextFocusUp="@id/nav_rail_view"
android:nextFocusDown="@id/search_autofit_results"
diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml
index fd845ee8..4974a027 100644
--- a/app/src/main/res/layout/download_child_episode.xml
+++ b/app/src/main/res/layout/download_child_episode.xml
@@ -9,6 +9,7 @@
android:layout_height="50dp"
android:layout_marginBottom="5dp"
android:foreground="@drawable/outline_drawable"
+ android:focusable="true"
android:nextFocusLeft="@id/nav_rail_view"
android:nextFocusRight="@id/download_button"
app:cardBackgroundColor="@color/transparent"
@@ -84,7 +85,9 @@
android:layout_height="@dimen/download_size"
android:layout_gravity="center_vertical|end"
android:layout_marginStart="-50dp"
- android:background="?selectableItemBackgroundBorderless"
+ android:foreground="@drawable/outline_drawable"
+ android:focusable="true"
+ android:nextFocusLeft="@id/download_child_episode_holder"
android:padding="10dp" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml
index 226c1632..21f79ca6 100644
--- a/app/src/main/res/layout/download_header_episode.xml
+++ b/app/src/main/res/layout/download_header_episode.xml
@@ -9,6 +9,8 @@
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:foreground="@drawable/outline_drawable"
+ android:focusable="true"
+ android:nextFocusRight="@id/download_button"
app:cardBackgroundColor="?attr/boxItemBackground"
app:cardCornerRadius="@dimen/rounded_image_radius">
@@ -71,7 +73,9 @@
android:layout_height="@dimen/download_size"
android:layout_gravity="center_vertical|end"
android:layout_marginStart="-50dp"
- android:background="?selectableItemBackgroundBorderless"
+ android:foreground="@drawable/outline_drawable"
+ android:focusable="true"
+ android:nextFocusLeft="@id/episode_holder"
android:padding="10dp" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_plugin_details.xml b/app/src/main/res/layout/fragment_plugin_details.xml
index 7a8f85e4..79013d9f 100644
--- a/app/src/main/res/layout/fragment_plugin_details.xml
+++ b/app/src/main/res/layout/fragment_plugin_details.xml
@@ -52,6 +52,7 @@
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/title_settings"
android:visibility="gone"
+ android:focusable="true"
app:srcCompat="@drawable/ic_baseline_tune_24"
tools:visibility="visible" />
@@ -61,6 +62,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:layout_marginStart="16dp"
+ android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_github_logo" />
@@ -81,6 +83,7 @@
style="@style/SmallBlackButton"
android:layout_gravity="center"
android:layout_marginStart="10dp"
+ android:focusable="false"
android:text="@string/extension_description" />
+
+
-
-
+ android:orientation="horizontal">
+ android:layout_marginEnd="5dp"
+ tools:text="Season 2 Episode 1022 will be released in" />
+ android:orientation="vertical"
+ android:visibility="gone"
+ tools:visibility="visible">
+ android:orientation="vertical"
+ android:visibility="gone"
+ tools:visibility="visible">
+ android:scaleType="centerCrop"
+ android:foreground="@drawable/rounded_outline"
+ tools:src="@drawable/profile_bg_orange"
+ android:contentDescription="@string/account"/>
+
+ tools:text="Quick Brown Fox" />
@@ -142,10 +144,24 @@
android:id="@+id/commit_hash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:gravity="center"
android:padding="10dp"
android:text="@string/commit_hash"
android:textColor="?attr/textColor" />
+
+
+
+
diff --git a/app/src/main/res/layout/player_select_tracks.xml b/app/src/main/res/layout/player_select_tracks.xml
index d32e1b4e..94e09d60 100644
--- a/app/src/main/res/layout/player_select_tracks.xml
+++ b/app/src/main/res/layout/player_select_tracks.xml
@@ -38,21 +38,20 @@
android:requiresFadingEdge="vertical"
android:id="@+id/video_tracks_list"
android:layout_width="match_parent"
-
android:layout_height="match_parent"
android:layout_rowWeight="1"
android:background="?attr/primaryBlackBackground"
- android:nextFocusLeft="@id/sort_subtitles"
- android:nextFocusRight="@id/apply_btt"
+ android:nextFocusRight="@id/audio_tracks_holder"
+
tools:listitem="@layout/sort_bottom_single_choice" />
+ android:id="@+id/audio_tracks_holder"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="50"
+ android:orientation="vertical">
@@ -107,17 +106,16 @@
+ android:requiresFadingEdge="vertical"
+ android:id="@+id/auto_tracks_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_rowWeight="1"
+ android:background="?attr/primaryBlackBackground"
+ android:nextFocusRight="@id/apply_btt"
+ android:nextFocusLeft="@id/video_tracks_list"
+ tools:listfooter="@layout/sort_bottom_footer_add_choice"
+ tools:listitem="@layout/sort_bottom_single_choice" />
@@ -132,11 +130,12 @@
+ style="@style/WhiteButton"
+ android:layout_gravity="center_vertical|end"
+ android:text="@string/sort_apply"
+ android:id="@+id/apply_btt"
+ android:nextFocusLeft="@id/auto_tracks_list"
+ android:layout_width="wrap_content" />
-
-
+ android:layout_height="wrap_content"
+ tools:visibility="visible">
@@ -130,7 +133,7 @@
android:clickable="true"
android:contentDescription="@string/download"
android:focusable="true"
- android:nextFocusLeft="@id/repository_item_root"
+ android:nextFocusLeft="@id/action_settings"
android:padding="12dp"
tools:src="@drawable/ic_baseline_add_24" />
diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml
index 76e8c434..e5a6881a 100644
--- a/app/src/main/res/layout/result_episode_large.xml
+++ b/app/src/main/res/layout/result_episode_large.xml
@@ -43,14 +43,26 @@
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:nextFocusRight="@id/download_button"
android:scaleType="centerCrop"
- tools:src="@drawable/example_poster" />
+ tools:src="@drawable/example_poster"
+ tools:visibility="invisible"/>
+ android:src="@drawable/play_button"
+ tools:visibility="invisible"/>
+
+
+
+
+
+
Laai tans…
Sub
Verwyder
- Stroom
+ Netwerk stroom
Op rus
CloudStream
Speel
@@ -105,4 +105,5 @@
Soek met behulp van tipes
Voer lettertipes in deur dit in %s te plaas
Rolverdeling: %s
+ Nuwe episode notifikasie
diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml
index 550d83a9..734d5644 100644
--- a/app/src/main/res/values-ajp/strings.xml
+++ b/app/src/main/res/values-ajp/strings.xml
@@ -68,8 +68,8 @@
هيدا المصدر مش عاطي \"ميتا داتا\". إذا مش موجودة بالمصدر، ما رح يمشي الڤيديو.
ما في أجزاء
مشّي الحلقة
- أكونتات
- سرعة \"إيگن گرايڤي\"
+ الحسابات والأمان
+ سرعة الڤيديو
كَمِّل
سِجِل
ما نلاقى الوصف
@@ -80,7 +80,6 @@
محي
بلش
في تِلِفونات ما فيا تعوز الطريقة الجديدة لتجديد الآپات. جربو \"الطريقة القديمة\" إذا ما عم تنزل التجديدات.
- بتزيد أوپسيونات لتحدد سرعة الڤيديو
بعد ما تسكر \"كلود ستريم\"، بكفي الڤيديو بشِباك زغير فوق غير آپ
هيدا المصدر ما بيدعم \"كروم كاست\"
تنبيش منظّم
@@ -124,7 +123,7 @@
%1$d–%2$d
عطي المطورين موزززة
عوز النسخة الإحتياطية
- نقى أكونت
+ نقي أكونت
سحابو ل فوق وتحت من اليمين أو الشمال ل تتحكمو بقوة الصوت أو قوة الضو
ننسخ الرابط
الوصف
@@ -144,7 +143,7 @@
لودينگ…
شيل
بَعِد مَعلومات
- التجديد والنسخات الاحتياطية
+ التجديدات والنسخات الاحتياطية
خبي هيدي الجودات من نتائج التنبيش
موقف موقتًا
كلود ستريم
@@ -199,7 +198,7 @@
شوف إذا في تجديد
في مشكلة بجهاز العرض (Renderer error)
العِنوان
- پروكسي raw.githubusercontent.com
+ پروكسي \"گِت هَب\"
جودة مشغل الڤيديو
ملصق الترجمة
أوڤا
@@ -252,7 +251,7 @@
الحد الأعلى للحروف بعنوان الڤيديو
المظاهر
تجديدات الآپ
- لغات المصادر
+ لغات الإضافات
عام
ممر التنزيل
إخلاء مسؤولية
@@ -265,7 +264,7 @@
فرجي أنمي المدبلج-المترجم
النسخ الإحتياطي
الإجراءات
- بتتجاوز منع \"گِت هَب\" بستعمال JSDelivr. معقول تقدي لتأخير بتجديدات الآپ بكم يوم.
+ تجاوز منع روابط \"گِت هَب\" الـ\"raw\" بستعمال JSDelivr. معقول تقدي لتأخير تجديدات الآپ بكم يوم.
ميزات مشغل الڤيديوات
شيل موقع
مظهر
@@ -289,7 +288,7 @@
الشكل
%1$d ساعة %2$d ديقة
فضّى
- إسم أكونتي الكول
+ إسم الأكونت
فَلتِر الإشارات المرجعية
بَلَش التجديد
نسخ
@@ -305,9 +304,9 @@
مشّي الحلقة
سرعة الڤيديو
تحكمو بالأكونتات
- رمز اللغة (apc/ar/en)
+ رمز اللغة (ar)
عمول أكونت
- فرجي المحتوى الـ18+ بالمصادر يلي بتحتوي
+ تمكين محتوى 18+ في الامتدادات الداعمة
المحتوى المفضل
غَيِر الأكونت
حط پوز على التنزيل
@@ -334,8 +333,8 @@
إي مايل (ع شكل: email@example.com)
%d ديقة
فحص المصادر
- example.com
- إسم الوبسيت الكول تبعي
+ https://example.com
+ إسم الوب سيت الجديدة
فتاح ومَشّي الملف
نوع الألون
رجاع
@@ -348,7 +347,7 @@
اللون الاساسي
فوت ع الأكونت
مترجم
- بِث
+ بِث من الإنترنت
مشي
التخزين الجُواني
زيد أكونت
@@ -382,11 +381,9 @@
حطو الأرقام السرية لـ\"%s\"
الطريقة القديمة
معلى
- \"كلود ستريم\" ما بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپوز.
+ \"كلود ستريم\" ما بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپويات.
\n
-\nفي معلومات على الـ\"ديسكورد\" تبعنا، أو فيكون تنبشو ع معلومات على الإنترنت.
-\n
-\nما فينا تحط الروابط تبع ريپوز المصادر هون من ورا \"سكاي يو كي المحدودة\" 🤮، يلي عازت تفكيرا المحدود لتجرب توقف هيدا الآپ بإستعمال \"قانون الألفية للملكية الرقميَّة\".
+\nفي معلومات على الـ\"ديسكورد\" تبعنا، أو فيكن تنبشو ع معلومات على الإنترنت.
زبد تتبع
3G/4G…
نفَتح %s
@@ -410,7 +407,7 @@
HDR
مش منزل: %d
ل بعدو
- رابط الڤيديو
+ https://example.com/example.mp4
نقي الرفّ
وقفتو الإشتراك لـ\"%s\"
WP
@@ -451,7 +448,7 @@
التلخيص
إِيه
الرئيسي
- طبّق وقتما سكّر الآپ
+ سكر الآپ حتى تطبق التغيرات
ساعدوني
عوز هيدا إذا عم بتبين الترجمة %d ميلي ثانية بعدما لازم
ما نلاقا الآپ
@@ -569,7 +566,7 @@
دي ڤي دي
الجودة
عين الافتراضي
- المرجع
+ المرجع (إختياري)
المشغل يلي بـ\"كلود ستريم\"
نزل لايحة المواقع يلي بدك تعوزن
حطو الأرقام السرية
@@ -595,5 +592,34 @@
برومو
غير إتجاه الشاشة أوتوماتيكيًا حسب شكل الڤيديو
رجع نعمل لاود لاللينك
- نعمل كَپي للعنوان!
+ نبش بغير مصادر
+ نوتيفيكايشن عن حلقات جديدة
+ فرجي الاقترحات
+ بتزيد خيار السرعة بالمشغل
+ فحاص كل المصادر
+ هيدا الفحص معمول للمطورين وما بأكد لحالو إزا المصدر عم يشتغل.
+ المفضلة
+ فتح قفل كلودستريم
+ قفل بواسطة المقاييس الحيوية
+ رمز/كلمة مرور للمصادقة
+ فتاح التطبيق باستعمال البصمة، آي دي الوج، پِن، النمط، إو الپاسورد.
+ تسَكرت هيدي الواجهة من ورا محاولات فاشلة عديدة. پليز، سكر الآپ ورجاع فتحه.
+ %s
+\nباقي
+ المصادقة البيومترية مش مدعومة ع هالجهاز
+ شيله من المفضل
+ اسم وعنوان الريپوزيتوري
+ نتسخ!
+ في ارور بالوصول ل الكليپبورد. پليز جرب مرة أخرى.
+ في ارور بالنسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ.
+ هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها.
+\nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج.
+ أوكي
+ وقف اپتميزايشن بطارية جهازك
+ بطارية الآپ اصلًا محطوطة ع «غير مقيد» \"Unrestricted\"
+ ما قدرنا نفتح معلومات الآپ تبع \"كلود ستريم\".
+ موسيقى
+ أوديو بوك
+ الميديا
+ لتضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يلي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي بـ الباكگروند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". ملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآن بـ الباكگروند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ بـ«الإعدادات العامة» \"General settings\"، إزا غيرت رأيك.
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index a8e79d22..8681398d 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -54,7 +54,7 @@
فشل التنزيل
تم إلغاء التنزيل
تم التنزيل
- بث
+ دفق الشبكة
خطأ في تحميل الرابط
التخزين الداخلي
مدبلج
@@ -114,8 +114,7 @@
إعدادات ترجمة المُشغل
ترجمة كروم كاست
إعدادات ترجمة كروم كاست
- وضع إيغنغرافي
- يضيف خيار السرعة في المُشغل
+ سرعة التشغيل
السحب لتقديم
اسحب من جانب إلى آخر للتحكم في موضعك في مقطع فيديو
السحب لتغيير الإعدادات
@@ -139,7 +138,7 @@
إذن الوصول الى ذاكرة التخزين مفقود, من فضلك حاول مجددا.
فشل إنشاء نسخة احتياطية %s
بحث
- الحسابات
+ الحسابات والأمن
التحديثات والنسخ الاحتياطية
معلومات
البحث المتقدم
@@ -284,10 +283,10 @@
عام
زر العشوائي
إظهار زر عشوائي على الصفحة الرئيسية والمكتبة
- لغات المزود
+ لغات الامتداد
واجهة التطبيق
المحتوى المفضل
- تفعيل محتوى خاص للبالغين داخل المزودين المدعومين
+ تفعيل محتوى خاص للبالغين داخل الإمتداد المدعوم
فك تشفير الترجمة
المصادر
الواجهة
@@ -308,8 +307,8 @@
إسم المستخدم
البريد الإلكتروني
127.0.0.1
- إسم الموقع
- رابط الموقع
+ إسم الموقع الجديد
+ رابط الموقع مثلا : https://example.com
اللغة (الإنجليزية)
Poster
Pôster
- Episode Poster
- Main Poster
- Next Random
- Go back
- Change Provider
- Preview Background
+ Pôster do episódio
+ Pôster Principal
+ Próximo Aleatório
+ Voltar
+ Alterar Provedor
+ Visualizar plano de fundo
Velocidade (%.2fx)
- Nota: %.1f
+ Avaliado: %.1f
Nova atualização encontrada!
\n%1$s -> %2$s
- Filler
+ Preenchimento
%d min
CloudStream
Início
- Procurar
+ Pesquisar
Downloads
Configurações
Procurar…
- Procurar no %s…
+ Pesquisar %s…
Sem dados
- Mais Opções
+ Mais opções
Próximo episódio
Gêneros
Compartilhar
- Abrir no Navegador
- Pular Carregamento
+ Abrir no navegador
+ Pular carregamento
Carregando…
Assistindo
Em espera
- Completado
- Deixado
+ Concluído
+ Desistido
Planejando assistir
Reassistindo
- Assistir Filme
+ Reproduzir filme
Transmitir Torrent
Fontes
Legendas
- Tentar reconectar…
- Voltar
- Assistir Episódio
+ Tentando conectar novamente…
+ Volte
+ Reproduzir episódio
- Baixar
- Baixado
+ Download
+ Download concluído
Baixando
- Download Pausado
- Download Iniciado
- Download Falhado
- Download Cancelado
- Download Finalizado
+ Download pausado
+ Download iniciado
+ Download falhou
+ Download cancelado
+ Download concluído
Transmitir
- Erro Carregando Links
- Armazenamento Interno
+ Erro ao carregar links
+ Armazenamento interno
Dub
Sub
- Deletar Arquivo
- Assistir Arquivo
- Retomar Download
- Pausar Download
- Desativar relatório automático de erros
- Mais info
+ Deletar arquivo
+ Reproduzir arquivo
+ Retomar download
+ Pausar download
+ Desative o relatório automático de erros
+ Mais informações
Esconder
- Assistir
- Info
- Filtrar Marcadores
+ Reproduzir
+ Informações
+ Filtrar marcadores
Marcadores
Remover
- Selecionar marcador
+ Definir como assistido/não assistido
Aplicar
Copiar
Fechar
Limpar
Salvar
- Velocidade do Reprodutor
- Configurar Legendas
- Cor do Texto
- Cor do Contorno
- Cor do Fundo
- Cor da Janela
- Tipo de Borda
- Elevação da Legenda
+ Velocidade de reprodução
+ Configurações de legendas
+ Cor do texto
+ Cor do contorno
+ Cor de fundo
+ Cor da janela
+ Tipo de borda
+ Elevação da legenda
Fonte
- Tamanho da Fonte
+ Tamanho da fonte
Pesquisar usando fornecedor
- Pesquisar usando genêros
+ Pesquisar usando tipos
%d Benenes doados aos desenvolvedores
Nenhuma Benenes doada
- Autosseleção de Lingua
- Baixar Linguas
- Lingua da legenda
- Segure para retornar a configuração padrão
- Importe fontes colocando elas em %s
- Continue Assistindo
+ Seleção automática de idioma
+ Baixar idiomas
+ Idioma da Legenda
+ Segure para redefinir para o padrão
+ Importe fontes colocando-as em %s
+ Continuar assistindo
Remover
Mais Info
@string/home_play
@@ -122,8 +122,7 @@
Configurações de legendas do Player
Legendas do Chromecast
Configurações de legendas do Chromecast
- Modo Eigengravy
- Adiciona um botão de velocidade no player
+ Velocidade de playback
Deslize para avançar o vídeo
Deslize de lado à lado para controlar a posição no vídeo
Deslize para mudar as configurações
@@ -145,8 +144,8 @@
Permissões de armazenamento faltando. Por favor tente novamente.
Erro no backup de %s
Procurar
- Contas
- Atualizações e backup
+ Contas e Segurança
+ Atualizações e Backup
Info
Procura Avançada
Mostrar resultados separados por fornecedor
@@ -167,7 +166,7 @@
Junte-se ao Discord
Dar um benene para os desenvolvedores
Benene dada
- Linguagem do App
+ Idioma do aplicativo
Esse fornecedor não possui suporte para Chromecast
Nenhum link encontrado
Link copiado para área de transferência
@@ -280,8 +279,8 @@
Any legal issues regarding the content on this application should be taken up with the actual file hosts and providers themselves as we are not affiliated with them. In case of copyright infringement, please directly contact the responsible parties or the streaming websites. The app is purely for educational and personal use. CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface. It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk.
Geral
Botão Aleatório
- Mostra o botão Aleatório na página inicial
- Linguagem dos fornecedores
+ Mostrar botão aleatório na página inicial e na biblioteca
+ Linguagem das extensões
Layout do App
Mídia preferida
Codificação das legendas
@@ -296,11 +295,11 @@
Coloca o título debaixo do poster
senha123
- MeuNomeLegal
+ Nome de usuário
oi@mundo.com
127.0.0.1
- MeuSiteLegal
- examplo.com
+ NovoNomedoSite
+ https://example.com
Codigo da Língua (bp)
- Já fiz vinho com toque de kiwi para belga sexy.
+ A rápida raposa marrom salta sobre o cachorro preguiçoso
Recomendada
%s carregada
Carregar de arquivo
@@ -419,8 +418,6 @@
Não transferido: %d
CloudStream não tem fontes instaladas por padrão. Você precisa instalar um site de repositórios.
\n
-\nPor causa das limitações do DMCA (Digital Millennium Copyright Act ) feito em nome de Sky UK Limited 🤮nós não podemos adicionar site de repositórios no app.
-\n
\nEntre no nosso Discord ou pesquise online.
Ver repositórios da comunidade
Lista pública
@@ -429,23 +426,23 @@
%s (Desativado)
Reproduzir automaticamente próximo episódio
Começa o próximo episódio quando o atual termina
- Ativar NSFW em fornecedores compatíveis
+ Ativar NSFW em extensões compatíveis
Fornecedores
Reverter
Ações
votou com sucesso
Baixando atualização do aplicativo…
- Referencias
+ Referenciador (opcional)
Atualizações do App
- Tocar com CloudStream
+ Assistir com o CloudStream
Automaticamente instale todos os plugins não instalados dos repositórios adicionados.
- Reproduzir Trailer
+ Reproduzir trailer
Navegador
Copia de Segurança
A Barra de Progresso pode ser usada quando o player estiver oculto
Inscrito
Essa lista está vazia. Tente mudar para outra.
- Reproduzir Livestream
+ Reproduzir transmissão ao vivo
Log do Teste
Baixar plugins automaticamente
Selecione o modo para filtrar os plugins baixados
@@ -476,7 +473,7 @@
Conteúdo +18
Ajuda
Processo de configuração de Redo
- Não pudemos instalar a nova versão do App
+ Não foi possível instalar a nova versão do aplicativo
instalador de pacotes
Organizar por
Votação (Alta para Baixa)
@@ -493,7 +490,7 @@
Arquivo de modo de segurança encontrado!
\nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido.
Inscrito em %s
- Episódio %d lançado
+ Episódio %d lançado!
Selecionar padrão
Inscrição cancelada de %s
Alguns aparelhos não possuem suporte para este pacote de instalação. Tente a opção legada se a atualização não instalar.
@@ -544,7 +541,7 @@
MPV
Abrindo mistura
VLC
- Aplicar quando reiniciar
+ Reinicie o aplicativo para ver as alterações.
Visualização info de crash
Faixas de áudio
Adicionado em (novo para antigo)
@@ -558,7 +555,7 @@
Aparência
Desativar
Usar
- Link da stream
+ https://example.com/example.mp4
Gestos
Plugin baixado
Não foi possível se conectar ao GitHub. Ativando proxy JsDelivr…
@@ -572,7 +569,74 @@
Provedor de teste
Layout
Padrões
- Proxy: raw.githubusercontent.com
- Contorna o bloqueio do GitHub usando jsDelivr. Pode atrasar as atualizações por alguns dias.
+ Proxy do GitHub
+ Contorne o bloqueio de URLs \"raw\" do GitHub usando jsDelivr. Pode atrasar as atualizações por alguns dias.
Rotas alternativas
+ Favoritos
+ %s adicionado aos favoritos
+ Duplicata em potencial encontrada
+ Adicionar
+ Substituir
+ Possíveis itens duplicados foram encontrados em sua biblioteca:
+\n
+\n %s
+\n
+\nGostaria de adicionar este item mesmo assim, substituir os existentes ou cancelar a ação?
+ Insira o PIN
+ Insira o PIN para %s
+ Insira o PIN atual
+ PIN incorreto. Por favor, tente novamente.
+ O PIN deve ter 4 caracteres
+ Selecione uma conta
+ Gerenciar contas
+ Ignorar a seleção da conta na inicialização
+ Exibir um botão para alternar a orientação da tela
+ %s removido dos favoritos
+ Adicionar aos favoritos
+ Remover dos favoritos
+ Girar
+ Bloquear perfil
+ PIN
+ Links recarregados
+ Frequência de backup
+ Substitua tudo
+ Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\'
+\n
+\nGostaria de adicionar este item mesmo assim, substituir o existente ou cancelar a ação?
+ Inscrever-se
+ Cancelar inscrição
+ Usar conta padrão
+ Editar conta
+ Conectado como %s
+ Habilite a troca automática de orientação da tela com base na orientação do vídeo
+ Rotação automática
+ Notificação de novo episódio
+ Pesquisar em outras extensões
+ Mostrar recomendações
+ Adiciona uma opção de velocidade no reprodutor
+ Testar todas as extensões
+ Esse teste é feito somente para desenvolvedores e não verifica ou nega o funcionamento de qualquer extensão.
+ Desbloquear CloudStream
+ O backup dos seus dados do CloudStream foi feito agora. Embora a possibilidade disso seja muito baixa, todos os dispositivos podem se comportar de maneira diferente. No caso raro de você ficar impedido de acessar o aplicativo, limpe os dados do aplicativo completamente e restaure a partir de um backup. Lamentamos muito qualquer inconveniente decorrente disso.
+ Bloquear com Biometria
+ Autenticação de Senha/PIN
+ A autenticação biométrica não é compatível com este dispositivo
+ Desbloquear o aplicativo com impressão digital, ID facial, PIN, padrão e senha.
+ Esta tela foi fechada devido a diversas tentativas malsucedidas. Por favor reinicie o aplicativo.
+ %s
+\nrestante(s)
+ Favorito
+ Não favorito
+ copiado!
+ Erro ao acessar a área de transferência. Tente novamente.
+ Nome e URL do repositório
+ Erro ao copiar. Copie o logcat e entre em contato com o suporte do aplicativo.
+ Para garantir downloads e notificações ininterruptos para programas de TV assinados, o CloudStream precisa de permissão para ser executado em segundo plano. Ao pressionar OK, você será direcionado para as informações do aplicativo. Lá, vá até Uso da bateria do aplicativo e defina o uso da bateria como Irrestrito. Observe que esta permissão não significa que o CS3 irá descarregar sua bateria. Ele só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se você optar por cancelar, poderá ajustar essa configuração posteriormente nas Configurações Gerais.
+ Ok
+ Desativar otimização de bateria
+ O uso da bateria do app já está definido como irrestrito
+ Não foi possível abrir as informações do aplicativo CloudStream.
+ Música
+ Áudio-livro
+ Mídia
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 00c968f2..0a8cf997 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -117,8 +117,7 @@
Nastavení titulků přehrávače
Titulky Chromecastu
Natavení titulků Chromecastu
- Rychlostní režim
- Přidá do přehrávače možnost rychlosti
+ Rychlost přehrávání
Přejet pro posun
Přejeďte prstem ze strany na stranu pro ovládání své pozice ve videu
Přejet pro změnu nastavení
@@ -140,7 +139,7 @@
Chybí oprávnění k úložišti. Zkuste to prosím znovu.
Chyba při zálohování %s
Search
- Účty
+ Účty a zabezpečení
Aktualizace a záloha
Informace
Pokročilé hledání
@@ -266,7 +265,7 @@
Obecné
Náhodné tlačítko
Zobrazit na domovské stránce a v knihovně náhodné tlačítko
- Jazyk poskytovatelů
+ Jazyky rozšíření
Rozložení aplikace
Preferovaná média
Kódování titulků
@@ -281,7 +280,7 @@
Umístit název pod plakát
heslo123
- MojeSuperJmeno
+ Uživatelské jméno
ahoj@svete.cz
127.0.0.1
lozinka123
- MojeCoolIme
+ Korisničko ime
bok@svijete.com
127.0.0.1
- MojaCoolStranica
- primjer.com
+ NovoImeStranice
+ https://primjer.com
Šifra jezika (en)
%1$s %2$s
račun
@@ -402,8 +401,8 @@
Filtriraj po željenom jeziku medija
Extras
Trailer
- Veza na stream
- Upućivač
+ https://primjer.com/primjer.mp4
+ Referent (nije obavezno)
Sljedeće
Gledaj videozapise na ovim jezicima
Prethodno
@@ -434,8 +433,6 @@
Nepreuzeto: %d
CloudStream nema instalirane web stranice prema zadanim postavkama. Morate instalirati stranice iz repozitorija.
\n
-\nZbog bezumnog uklanjanja DMCA od strane Sky UK Limited 🤮 ne možemo povezati web mjesto repozitorija u aplikaciji.
-\n
\nPridružite se našem Discordu ili tražite online.
Pregledajte repozitorije zajednice
Javni popis
@@ -549,9 +546,9 @@
Otkazana pretplata sa %s
Vraćanje
ISP zaobilaznice
- raw.githubusercontent.com Proxy
+ GitHub Proxy
Neuspješno dohvaćanje GitHuba. Uključuje se jsdelivr proxy …
- Zaobilazi blokiranje GitHuba koristeći jsdelivr. Može odgoditi ažuriranja za nekoliko dana.
+ Zaobilazi blokiranje neobrađenih GitHub URL-ova koristeći jsDelivr. Može uzrokovati kašnjenje ažuriranja nekoliko dana.
Preferirana kvaliteta gledanja (podatkovna mobilna mreža)
Profil %d
Wi-Fi
@@ -614,7 +611,22 @@
Prikaži gumb za prebacivanje orijentacije zaslona
Omogućuje automatsko mijenjanje orijentacije zaslona na temelju orijentacije videa
Automatsko rotiranje
- Naslov je kopiran!
rotiraj_video_tipka
automatski_rotiraj_video_tipka
+ Obavijest za novu epizodu
+ Pretraži u ostalim proširenjima
+ Dodaje opciju brzine u playeru
+ Testiraj sva proširenja
+ Ovaj je test namijenjen samo programerima i ne provjerava niti negira rad bilo kojeg proširenja.
+ Prikaži preporuke
+ Ime repozitorija i URL
+ kopirano!
+ Zaključaj s biometrijskim podatcima
+ %s
+\npreostalo
+ Greška u pristupanju međuspremnika. Pokušaj ponovo.
+ Otključaj CloudStream
+ Lozinka/PIN autentifikacija
+ Ovaj uređaj ne podržava biometrijsku autentifikaciju
+ Ovaj je ekran zatvoren zbog višestrukih neuspjelih pokušaja. Pokrenite aplikaciju ponovo.
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 5b1dbcf0..5533cdc0 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -37,7 +37,7 @@
Állapot
Forrás címe
Előzmények
- Eigengravy mód
+ Visszajátszás sebessége
Felirat magassága
Letöltés elkezdve
Nem található cselekmény
@@ -71,7 +71,7 @@
Feliratok
Vissza
Letöltés kész
- Stream
+ Hálózati stream
Hiba a linkek betöltésekor
Belső tárhely
Dub
@@ -124,7 +124,6 @@
Lejátszó feliratok beállításai
Chromecast Feliratok
Chromecast feliratok beállításai
- Sebességbeállítást ad hozzá a lejátszóhoz
Epizód lejátszása
Letöltve
Letöltés szüneteltetve
@@ -171,11 +170,11 @@
OVA
Egyebek
Sorozat
- @string/anime
+ Anime
Forráshiba
NSFW
Rajzfilm
- @string/ova
+ OVA
Élőadás
NSFW
Videó
@@ -184,7 +183,7 @@
Ázsiai dráma
Linkek újratöltése
Link másolás
- Link letöltés
+ Letöltés mirror
Automatikus letöltés
Adatok eltárolva
Hiba a biztonsági mentés során %s
@@ -223,9 +222,9 @@
Távoli hiba
Render hiba
Váratlan lejátszó hiba
- Letöltés hiba, ellenőrízze a tárolási engedélyeket
+ Letöltés hiba, ellenőrizze a tárolási engedélyeket
Chromecast epizód
- Chromecast link
+ Chromecast mirror
Lejátszás az alkalmazásban
Lejátszás %s
Lejátszás böngészőben
@@ -233,7 +232,7 @@
Újracsatlakozás…
Húzd balra vagy jobbra a videólejátszóban az idő vezérléséhez
Csúsztassa ujját a beállítások módosításához
- Csúsztassa felfelé vagy lefelé a bal vagy jobb oldalon a fényerő vagy a hangerő megváltoztatásához
+ Csúsztassa felf/le az ujját a bal/jobb oldalon a fényerő vagy a hangerő megváltoztatásához
Biztonsági mentés
0 Banán a fejlesztőknek
Húzd el, hogy beless
@@ -243,7 +242,7 @@
Dupla koppintás a szüneteltetéshez
Lejátszó keresési értéke (Másodpercben)
Koppintson kétszer a jobb vagy bal oldalra az előre vagy hátra ugráshoz
- Koppintson kétszer középen a szüneteltetéshez
+ Koppintson kétszer középre a szüneteltetéshez
Rendszer fényerejének használata
Rendszer fényerejének használata az appban a sötét átfedés helyett
Előrehaladás frissítése
@@ -286,7 +285,7 @@
Funkciók
Előnyben részesített videóminőség (mobilinternet)
Videolejátszó cím max karakterek
- Nem sikerült elérni a GitHubot, a jsdelivr proxy engedélyezése.
+ Nem sikerült elérni a GitHubot, a jsdelivr proxy bekapcsolva…
Bővítmények
Általános
Felirat kódolása
@@ -295,10 +294,10 @@
Szolgáltató teszt
Sikertelen
Problémákat okoz, ha túl magasra van állítva az alacsony tárhellyel rendelkező eszközökön, például az Android TV-n.
- Korhatáros tartalmak engedélyezése a támogatott szolgáltatóknál
+ Korhatáros tartalmak engedélyezése a támogatott kiegészítőknél
Elrendezés
- raw.githubusercontent.com Proxy
- A lejátszó funkciói
+ GitHub Proxy
+ Lejátszó funkciók
Előnyben részesített videóminőség (WiFi-n)
Hasznos az internetszolgáltató blokkjainak megkerüléséhez
Elrendezés
@@ -306,10 +305,10 @@
NGINX szerver URL-címe
Szinkronizált/feliratozott animék megjelenítése
Alapértelmezettek
- Megjelenít egy gombot a Kezdőlapon, amely egy véletlenszerű filmet vagy TV sorozatot választ a Kezdőlapról
+ Véletlenszerű gomb megjelenítése a Könyvtárban és Főoldalon
Letöltési útvonal
Gyorsítótár
- Szolgáltatók nyelvei
+ Kiegészítők nyelvei
Napló
Könyvtár
internetszolgáltató-kikerülések
@@ -322,7 +321,7 @@
Előnyben részesített média
Hivatkozások
Videó és kép gyorsítótár törlése
- A jsdelivr használatával a GitHub blokkolása megkerülhető. Néhány nappal késleltetheti a frissítéseket.
+ A jsDelivr használatával a tiszta GitHub blokkolása megkerülhető. Néhány nappal késleltetheti a frissítéseket.
Összeomlást okoz, ha túl magasra van állítva a kevés memóriával rendelkező eszközökön, például az Android TV-n.
Betöltés az internetről
Videósávok
@@ -333,13 +332,13 @@
Alkalmazásfrissítés letöltése…
Frissítve (újabbtól a régebbihez)
Úgy tűnik, a könyvtárad üres :(
-\nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz
- Úgy tűnik, ez a lista üres, próbálj meg egy másikra váltani
+\nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz.
+ Úgy tűnik, ez a lista üres, próbálj meg egy másikra váltani.
Max
4K
SDR
Fiók létrehozása
- pelda.com
+ https://példa.hu
Feliratok szinkronizálása
Alkalmazásfrissítés telepítése…
Túl sok szöveg. Nem lehet a vágólapra menteni.
@@ -349,7 +348,7 @@
Frissítés
/\?\?
Árnyék
- Filmelőzetes
+ Előzetes
Mit szeretnél látni
Minden %s már letöltött
Először telepítse a bővítményt
@@ -357,13 +356,13 @@
Kinézet
Alkalmazás elrendezés
Szinkronizálás
- Nem sikerült bejelentkezni a következőként: %s
+ Nem sikerült bejelentkezni a %s-nál
Min
1000 ms
Ajánlott
Fő
Érvénytelen adatok
- Link a streamhez
+ https://példa.hu/példa.mp4
Nem sikerült betölteni: %s
Elkezdődött a(z) %1$d %2$s letöltése…
Töltse le az összes bővítményt ebből a tárolóból\?
@@ -372,16 +371,16 @@
MPV
Alkalmazás nem található
PackageInstaller
- Rendezés e szerint:
+ Rendezés e szerint
Feliratkozott a következőre: %s
- MenőWeboldalam
+ ÚjOldalNév
DVD
%d plugin frissítve
Értékelés: %s
Előzmények törlése
Nem
Feliratkozva
- Használd ezt, ha a feliratok %d ms-sel korábban jelennek meg.
+ Használd ezt, ha a feliratok %d ms-sel korábban jelennek meg
Lejátszó
Felbontás és cím
Előnyben részesített videolejátszó
@@ -420,7 +419,7 @@
Minden felirat nagybetűs
Intro
Leiratkozott a következőről: %s
- Bloat eltávolítása a feliratokról
+ Szükségtelen elemek eltávolítása a feliratokról
Szűrés előnyben részesített médianyelv szerint
Biztos vagy benne, hogy ki akarsz lépni\?
Rendezés
@@ -431,9 +430,7 @@
Ez az összes tároló bővítményt is törli
A CloudStream alapértelmezés szerint nem telepített webhelyeket. A webhelyeket a tárolókból kell telepítenie.
\n
-\nA Sky UK Limited agyatlan DMCA letiltása miatt 🤮 nem tudjuk az alkalmazásban linkelni az adattár oldalát.
-\n
-\nCsatlakozz a Discordunkhoz vagy keress online.
+\nCsatlakozz a Discord-unkhoz vagy keress online.
Verzió
Megjelölés megtekintettként
Eltávolítás a megnézettek közül
@@ -466,7 +463,7 @@
Betűrendben (A-tól a Z-ig)
Frissítve (régebbitől az újabbig)
jelszó123
- AzÉnMenőFelhasználónevem
+ Felhasználónév
127.0.0.1
Fiókváltás
Fiók hozzáadása
@@ -481,14 +478,14 @@
Bővítmények
Tároló hozzáadása
Tároló neve
- Tárhely URL címe
+ Repó URL
Bővítmény betöltve
Bővítmény letöltve
Közreműködők
Betűrendben (Z-től az A-ig)
Könyvtár kiválasztása
- Biztonságos módú fájl található!
-\nNem tölt be semmilyen kiterjesztést indításkor, amíg a fájl el nem lesz távolítva.
+ Biztonságos módú fájlba ütköztünk!
+\nNem töltődik be semmilyen kiegészítő indításkor, amíg a fájl nem kerül törlésre.
Normál
%s betöltve
Beállítás kihagyása
@@ -503,7 +500,7 @@
Az átugrás mértéke, amikor a lejátszó el van rejtve
Jogi nyilatkozat
Lejátszó megjelenítve - Ugrási Érték
- Lejátszó elrejtve - Ugrási Érték
+ Lejátszó Elrejtve - Ugrási Érték
Klónozott oldal
Egy meglévő webhely klónjának hozzáadása, más URL-címmel
TV elrendezés
@@ -513,4 +510,86 @@
Mentési gyakoriság
Értékelt
Kikapcsolás
+ Kamera Rip
+ Nyitás
+ WP
+ Új epizód értesítés
+ Keresés más kiegészítőkben
+ Ajánlatok mutatása
+ Sebesség opció megjelenítése a lejátszóban
+ Minden kiegészítő tesztelése
+ Ez a teszt szigorúan fejlesztők számára készült, nem alkalmas egyes kiegészítők működésének visszaigazolására.
+ %1$s %2$s
+ TS
+ Feliratkozás
+ Leiratkozás
+ Linkek Újratöltve
+ Zárás
+ Hivatkozó (opcionális)
+ Nem találhatóak pluginek a repóban
+ Repó nem található, ellenőrizze a címet vagy próbálja VPN-el
+ Web Videó Cast
+ %s kihagyása
+ A kihagyási felugró ablakok mutatása nyitás/zárás esetén
+ Alapbeállítás
+ Használ
+ %d profil
+ Használja ezt ha a felirat %d ms-ot késik
+ Kamera HD
+ Poszter Kép
+ TC
+ Mobil adat
+ Wi-Fi
+ Szerkeszt
+ Kamera
+ Nyomott
+ Kevert zárás
+ Kevert nyitás
+ Segítség
+ Profilok
+ Eltávolítás kedvencekből
+ Adja meg a jelenlegi PIN-t
+ Itt beállíthatja hogyan rendezze el a forrásokat. Ha egy videónak nagyobb a prioritása, előbb fog megjelenni a listában. A forrás prioritás és a minőség prioritás együttes értéke adja ki a videó prioritást.
+\n
+\nForrás A: 3
+\nMinőség B: 7
+\nEzek összértéke egy 10-es videó prioritást eredményez.
+\n
+\nFIGYELEM: Ha az összeg több mint 10, a lejátszó nem tölt be mást ha már a link betöltésre került!
+ Potenciálisan dupla elemek a könyvtárjában:
+\n
+\n%s
+\n
+\nSzeretné hozzáadni ezt az elemet mindenképpen, ezzel felülírva a jelenlegit, vagy visszavonja a műveletet?
+ Fiók választás kihagyása belépéskor
+ Használjon alapértelmezett fiókot
+ Elforgatás
+ Profil háttér
+ Kedvencek
+ %s hozzáadva a kedvencekhez
+ %s eltávolítva a kedvencekből
+ Hozzáadás a kedvencekhez
+ Úgy tűnik egy potenciálisan dupla elem már létezik a könyvtárjában: \'%s.\'
+\n
+\nMindenképpen hozzá akarja adni ezt az elemet, ezzel felülírva a régit, vagy visszavonja a műveletet?
+ Adja meg a PIN-t
+ Profil Zárolása
+ Válasszon egy fiókot
+ Fiókok kezelése
+ Fiók módosítása
+ Belépve mint %s
+ Jelenítsen meg egy kapcsolót a képorientáció váltáshoz
+ Potenciális Dupla Találat
+ Adja meg a PIN-t a %s-hoz
+ PIN
+ Hibás PIN. Próbálja újra.
+ A UI hibásan jelenítődött meg, ez egy JELENTŐS BUG ezért kérjük jelentse be %s
+ Már korábban szavazott
+ Hozzáadás
+ Kicserélés
+ Mind Kicserélése
+ Minőségek
+ A PIN 4 karakter hosszú kell legyen
+ Auto elforgatás
+ Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 0bb2a24a..d537a1d5 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -115,8 +115,7 @@
Pengaturan subtitle pemutar
Subtitle Chromecast
Pengaturan subtitle Chromecast
- Mode Eigengravy
- Menambahkan opsi kecepatan di pemutar
+ Kecepatan pemutaran
Geser untuk mengubah waktu
Geser dari sisi ke sisi untuk mengontrol posisi dalam video
Geser untuk mengubah pengaturan
@@ -138,8 +137,8 @@
Izin penyimpanan tidak ditemukan, mohon coba lagi.
Error saat mencadang %s
Cari
- Kredit dan akun
- Update dan cadangan
+ Akun dan Keamanan
+ Update dan Cadangan
Info
Pencarian Lanjutan
Memberikan hasil pencarian yang dipisahkan berdasarkan provider
@@ -263,8 +262,8 @@
Any legal issues regarding the content on this application should be taken up with the actual file hosts and providers themselves as we are not affiliated with them. In case of copyright infringement, please directly contact the responsible parties or the streaming websites. The app is purely for educational and personal use. CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface. It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk.
Umum
Tombol Acak
- Tampilkan tombol acak di Beranda
- Bahasa provider
+ Tampilkan tombol acak di Beranda dan Pustaka
+ Bahasa ekstensi
Tata Letak Aplikasi
Media yang lebih diinginkan
Antarmuka pengguna
@@ -373,10 +372,10 @@
Bawaan
Tampilan
Fitur
- Tampilkan konten NSFW
+ Mengaktifkan NSFW pada Ekstensi yang didukung
Putar Cuplikan
Putar Siaran
- Siaran
+ Aliran jaringan
Bahasa Subtitel
Putar otomatis episode selanjutnya
Putar episode selanjutnya, setelah ini berakhir
@@ -398,22 +397,20 @@
%1$s %2$d%3$s
Siaran langsung
Hapus Website
- UsernameKeren
+ Username
contoh@email.com
127.0.0.1
- Websiteku
- contoh.com
+ NamaSitus Baru
+ https://contoh.com
Ekstra
Apa yang ingin anda lihat
Plugin terhapus
%d plugin diperbarui
Lihat Repositori dari Group
List Umum
- CloudStream tidak memiliki sumber video secara bawaan. Kamu harus menginstall dari repositori.
+ CloudStream tidak memiliki situs yang terinstal secara default. Anda perlu menginstal situs-situs dari repositori.
\n
-\nKarena banyak laporan dari banyak pihak berwajib, kami tidak dapat memberikannya secara langsung.
-\n
-\nGabung dengan group Discord atau cari di internet.
+\nBergabunglah dengan Discord kami atau cari secara online.
Alamat Repositori
Buat Akun
Error
@@ -435,7 +432,7 @@
Semua Umur
%s (Tidak aktif)
Trek
- Terapkan saat dimuat ulang
+ Terapkan saat dimuat ulang untuk melihat perubahan.
Keterangan
Versi
Status
@@ -457,7 +454,7 @@
Pilih ini untuk menghapus semua repositori plugin
Lewati pengaturan
Alamat salah
- Alamat streaming
+ https://contoh.com/contoh.mp4
Selanjutnya
Sebelumnya
Ubah tampilan aplikasi
@@ -482,7 +479,7 @@
Gerakan
Beberapa perangkat tidak mendukung penginstal paket mode baru. Coba mode lama jika pembaruan tidak dapat diinstal.
Aksi
- Referer
+ Memberi referensi (opsional)
Ya
Install ekstensi terlebih dahulu
Semua Bahasa
@@ -545,9 +542,9 @@
Berlangganan ke %s
Berhenti berlangganan di %s
Episode %d telah rilis!
- Proxy raw.githubusercontent.com
+ Proxy GitHub
Tidak dapat menjangkau GitHub. Mengaktifkan proxy jsDelivr…
- Melewati pemblokiran GitHub menggunakan jsDelivr. Dapat menyebabkan pembaruan tertunda beberapa hari.
+ Lewati pemblokiran raw URL github menggunakan jsDelivr. Dapat menyebabkan pembaruan tertunda selama beberapa hari.
Bypass ISP
Pulihkan
Kualitas nonton yang diinginkan (Data Seluler)
@@ -607,4 +604,38 @@
Lewati pemilihan akun saat startup
Kelola Akun
Edit akun
+ Putar
+ Menampilkan tombol sakelar untuk orientasi layar
+ Tautan Dimuat Ulang
+ Mengaktifkan peralihan otomatis orientasi layar berdasarkan orientasi video
+ Putar otomatis
+ Cari di ekstensi lainnya
+ Menambahkan opsi percepat di pemutar
+ Tes ini hanya ditujukan untuk pengembang dan tidak memverifikasi atau menolak kerja ekstensi apa pun.
+ Notifikasi episode baru
+ Tampilkan rekomendasi
+ Menguji semua Ekstensi
+ Data CloudStream Anda telah dicadangkan. Meskipun peluang terjadinya kasus ini sangat kecil dan jarang terjadi, tetapi semua perangkat berperilaku berbeda. Jika Anda ada dalam situasi terburuk, misalnya gagal untuk mengakses aplikasi, segera hapus data aplikasi sepenuhnya dan pulihkan data cadangan. Kami mohon maaf atas segala ketidaknyamanan yang mungkin ditimbulkan.
+ Otentikasi Kata Sandi/PIN
+ Otentikasi biometrik tidak didukung di perangkat ini
+ Buka kunci aplikasi dengan Sidik Jari, ID Wajah, PIN, Pola, dan Kata Sandi.
+ Layar ini ditutup setelah mengalami beberapa kali percobaan yang gagal. Anda harus memulai ulang aplikasi ini.
+ Batalkan favorit
+ Buka kunci CloudStream
+ %s
+\ntersisa
+ Favorit
+ Kunci dengan Biometrik
+ Nama dan URL repositori
+ Gagal mengakses Papan Klip, mohon coba lagi.
+ disalin!
+ Gagal menyalin, mohon salin logcat dan hubungi pengembang aplikasi.
+ Oke
+ Matikan pengoptimalan Baterai
+ Pemakaian baterai untuk aplikasi ini sudah diatur menjadi tidak dibatasi
+ Gagal membuka info aplikasi CloudStream.
+ Musik
+ Buku Audio
+ Media
+ Untuk memastikan unduhan dan pemberitahuan tanpa gangguan untuk acara TV berlangganan, CloudStream memerlukan izin untuk berjalan di latar belakang. Dengan menekan OK, Anda akan diarahkan ke Info aplikasi. Di sana, gulir ke Penggunaan baterai aplikasi dan atur penggunaan baterai ke Tidak Terbatas. Harap dicatat, izin ini tidak berarti CS3 akan menguras baterai Anda. Ini hanya akan beroperasi di latar belakang ketika diperlukan, seperti ketika menerima pemberitahuan atau mengunduh video dari ekstensi resmi. Jika Anda memilih untuk membatalkannya, Anda dapat menyesuaikan pengaturan ini nanti di Pengaturan Umum.
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 74839a47..040b0f31 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -61,7 +61,7 @@
Download fallito
Download cancellato
Download completato
- Stream
+ Flusso di rete
Errore durante il caricamento dei link
Archiviazione interna
Doppiato
@@ -122,8 +122,7 @@
Impostazioni sottotitoli lettore
Sottotitoli Chromecast
Impostazioni sottotitoli Chromecast
- Modalità Eigengravy
- Aggiungi opzione velocità nel player
+ Velocità di riproduzione
Scorri per mandare avanti/indietro
Scorri da un lato all\'altro per controllare la tua posizione in un video
Scorri per cambiare le impostazioni
@@ -147,7 +146,7 @@
Permessi di archiviazione mancanti. Per favore riprova.
Errore nel backup %s
Cerca
- Accounts
+ Account e sicurezza
Aggiornamenti e Backup
Info
Ricerca avanzata
@@ -287,11 +286,11 @@
Avvertenza
Generale
Random
- Mostra pulsante Random nella homepage
- Lingua provider
+ Mostra pulsante casuale nella home page e nella libreria
+ Lingue estensione
Layout app
Media preferito
- Abilita NSFW sui provider supportati
+ Abilita NSFW sulle estensioni supportate
Encoding Sottotitoli
Provider
Interfaccia utente
@@ -305,11 +304,11 @@
Titolo sotto il poster
password123
- IlMioUsername
+ Nome utente
hello@world.com
127.0.0.1
- IlMioSito
- example.com
+ NuovoNomeSito
+ https://example.com
Codice lingua (it)
%1$s %2$s
account
@@ -391,8 +390,8 @@
Filtra in base alla lingua preferita
Extra
Trailer
- Link allo stream
- Referer
+ https://example.com/example.mp4
+ Referente (facoltativo)
Prossimo
Guarda video in queste lingue
Precedente
@@ -424,9 +423,7 @@
Aggiornati %d plugin
CloudStream non ha siti installati per impostazione predefinita. È necessario installare i siti dai repository.
\n
-\nA causa di una rimozione DMCA senza cervello da Sky UK Limited 🤮 non possiamo collegare il sito repository nell\'app.
-\n
-\nUnisciti al nostro Discord o cerca online.
+\nJoin our Discord or search online.
Vedi le repository della community
Lista pubblica
Tutti i sottotitoli in maiuscolo
@@ -435,7 +432,7 @@
Tracce
Traccia audio
Traccia video
- Applica al riavvio
+ Riavvia app per visualizzare le modifiche.
Safe mode attiva
Tutte le estensioni sono state disabilitate a causa di un arresto anomalo per aiutarti a trovare l\'estensione che causa il problema.
Vedi informazioni del crash
@@ -539,12 +536,12 @@
Ferma
Superato
Fallito
- Proxy raw.githubusercontent.com
+ Proxy GitHub
Disiscritto da %s
Iscritto
Iscritto a %s
Impossibile raggiungere GitHub. Attivazione proxy jsDelivr…
- Aggira il blocco di GitHub usando jsDelivr. Potrebbe causare un ritardo degli aggiornamenti di alcuni giorni.
+ Evita il blocco degli URL github non elaborati utilizzando jsDelivr. Potrebbe causare un ritardo degli aggiornamenti di alcuni giorni.
Baypass ISP
Ripristina
Aggiornando shows a cui sei iscritto
@@ -602,10 +599,42 @@
Entrato come %s
Inserisci il PIN per %s
Blocca profilo
- Usa Account Default
+ Usa account predefinito
Salta la selezione dell\'account all\'avvio
Gestisci Accounts
Modifica account
Collegamenti ricaricati
Ruota
+ Visualizza un pulsante di commutazione per l\'orientamento dello schermo
+ Abilita la commutazione automatica dell\'orientamento dello schermo in base all\'orientamento del video
+ Rotazione automatica
+ Cerca in altre estensioni
+ Mostra consigli
+ Aggiunge un\'opzione di velocità nel lettore
+ Prova tutte le estensioni
+ Questo test è pensato solo per gli sviluppatori e non verifica o nega il funzionamento di alcuna estensione.
+ Notifica nuovo episodio
+ Sblocca CloudStream
+ Blocca con biometria
+ Autenticazione con password/PIN
+ L\'autenticazione biometrica non è supportata su questo dispositivo
+ Sblocca app con impronta digitale, Face ID, PIN, sequenza e password.
+ Questa schermata è stata chiusa a causa di più tentativi falliti. Riavvia l\'app.
+ È stato eseguito il backup dei tuoi dati CloudStream. Sebbene questa possibilità sia molto bassa, tutti i dispositivi possono comportarsi in modo diverso. Nel raro caso in cui ti venga bloccato l\'accesso all\'app, cancella completamente i dati dell\'app e ripristina da un backup. Siamo molto spiacenti per qualsiasi inconveniente derivanti da questo.
+ Non preferito
+ %s
+\nresiduo
+ Preferito
+ Nome e URL del repository
+ copiato!
+ Errore durante l\'accesso agli Appunti. Riprova.
+ Errore durante la copia. Copia logcat e contatta il supporto dell\'app.
+ OK
+ Disabilita ottimizzazione della batteria
+ Impossibile aprire le informazioni sull\'app CloudStream.
+ Media
+ Per garantire download e notifiche ininterrotti per i programmi TV sottoscritti, CloudStream necessita dell\'autorizzazione per l\'esecuzione in background. Premendo OK, verrai indirizzato alle informazioni sull\'app. Successivamente, scorri fino a \"Utilizzo della batteria\" e imposta l\'utilizzo della batteria su \"Senza restrizioni\". Tieni presente che questa autorizzazione non significa che CS3 scaricherà la batteria. Funzionerà in background solo quando necessario, ad esempio quando si ricevono notifiche o si scaricano video da estensioni ufficiali. Se scegli di annullare, puoi modificare questa impostazione più tardi in \"Impostazioni generali\".
+ L\'utilizzo della batteria dell\'app è già impostato su \"Senza restrizioni\"
+ Musica
+ Audiolibro
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index d5c2ad5e..da2952a0 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -271,7 +271,6 @@
דפדפן
צבע חלון
הצג לוג
- הוסף אפשרות מהירות בנגן
לחץ פעמיים כדי להציץ
לחץ פעמיים כדי לעצור
התשתמש בבהירות המערכת בנגן האפליקציה במקום שכבת-על כהה
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 04e27c85..acb2cfc3 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -237,9 +237,9 @@
プレーヤーの字幕設定
Chromecastの字幕
Chromecastの字幕設定
- プレーヤーに速度オプションを追加します
スワイプして探す
次のエピソードを自動再生する
現在のエピソードが終了したら次のエピソードを開始する
長押しするとデフォルトにリセットされます
+ ダウンロードを再開
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 0bf3bd9b..1a63050a 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -111,7 +111,6 @@
Chromecast 자막
Chromecast 자막 설정
배속 모드
- 플레이어에 속도 옵션을 추가합니다
스와이프하여 탐색
좌우로 스와이프하여 동영상 위치 제어하기
스와이프하여 설정 변경
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index 54f9be82..f61bcfc0 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -67,7 +67,6 @@
Ištrinti
Atšaukti
Pradėti
- Prideda greičio pasirinkti grotuve
Filmukas
Atsiuntimas atšauktas
Išplėstinė paieška
diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml
index ab8db2a9..49b333e3 100644
--- a/app/src/main/res/values-lv/strings.xml
+++ b/app/src/main/res/values-lv/strings.xml
@@ -124,7 +124,6 @@
Chromecast subtitri
Chromecast subtitru iestāfijumi
Eigengravy Mode
- Pievieno atskaņošanas ātrumu playerim
Novelc lai paradītu
Novelc no māla lidz malai lai pozicionētu video
Novēlu lai mainītu iestādījums
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index 956f18e5..fe82a90b 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -90,7 +90,6 @@
Преводи
Поставки на плеерот за преводи
Режим на Eigengravy
- Додава можност за брзина на снимка во плеерот
Повлечете за да барате
Повлечете од страна на страна за да ја контролирате вашата позиција во видеото
Повлечете за да ги промените поставките
@@ -336,7 +335,7 @@
Карактеристики
Азиска драма
Додатоци
- Се прикажува копче на почетната страница што може да избере случаен филм или ТВ серија од почетната страница
+ Прикажи случајно копче на почетната страница и библиотеката
Поддржано
Сметки
Вовед
@@ -531,4 +530,65 @@
Смени провајдер
Оди назад
Актери: %s
+ %s додадени на фаворити
+ %s избришено од фаворити
+ Одбери опција за филтрирање на превземени плагини
+ Складиштето не е пронајдено, проверете го URL-то и пробајте VPN
+ Фаворити
+ Додадено во фаворити
+ Избриши од фаворити
+ Замени ги сите
+ Се чини дека потенцијално дупликат ставка веќе постои во вашата библиотека: „%s“.
+\n
+\nДали сепак сакате да ја додадете оваа ставка, да ја замените постојната или да го откажете дејството?
+ Во вашата библиотека се пронајдени потенцијални дупликати ставки:
+\n
+\n%s
+\n
+\nДали сепак сакате да ја додадете оваа ставка, да ги замените постоечките или да го откажете дејството?
+ Внеси ПИН за %s
+ ПИН-от мора да биде 4 карактери
+ Менаџирај кориснички сметки
+ Измени корисничка сметка
+ Логиран како %s
+ Прескокнете го изборот на корисничка сметка при стартување
+ Користете ја стандардната сметка
+ Ротирај
+ Прикажете копче за префрлување за ориентација на екранот
+ Веќе гласаше
+ Овде можете да го промените начинот на кој се подредуваат изворите. Ако видеото има повисок приоритет, ќе се појави повисоко во изборот на изворот. Збирот на приоритетот на изворот и приоритетот на квалитетот е приоритет на видеото.
+\n
+\nИзвор А: 3
+\nКвалитет Б: 7
+\nЌе има комбиниран приоритет на видеото од 10.
+\n
+\nЗАБЕЛЕШКА: Ако сумата е 10 или повеќе, играчот автоматски ќе го прескокне вчитувањето кога ќе се вчита таа врска!
+ Одбери корисничка сметка
+ Повторно вчитани линкови
+ Оневозможи
+ Внеси ПИН
+ Внеси моментален ПИН
+ ПИН
+ Заклучи профил
+ Неточен ПИН. Пробај повторно.
+ Исклучи претплата
+ Профил %d
+ Претплати се
+ Позадина на профил
+ Мобилен интернет
+ Поставете стандардно
+ Квалитети
+ Пронајдени потенцијални дупликати
+ Додади
+ Замени
+ Wi-Fi
+ Не се најдени плагини во складиштето
+ Користи
+ Измени
+ Профили
+ Помош
+ UI-то не можеше да се креира правилно, ова е ГОЛЕМ БАГ и треба веднаш да се пријави %s
+ Зачестеност на зачувување на бекап
+ Овозможете автоматско префрлување на ориентацијата на екранот врз основа на видео ориентација
+ Автоматска ротација
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index 49c5b3ec..279f5511 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -3,14 +3,14 @@
വേഗം (%.2fx)
റേറ്റിംഗ്: %.1f
- പുതിയ അപ്ഡേറ്റ്
+ പുതിയ അപ്ഡേറ്റ്!
\n%1$s -> %2$s
- CloudStream
+ ക്ലൗഡ് സ്ട്രീം
ഹോം
തിരയുക
ഡൗൺലോഡ്സ്
സെറ്റിങ്സ്
- തിരയുക
+ തിരയുക…
ടാറ്റ ലഭ്യമല്ല
കൂടുതൽ ഓപ്ഷൻസ്
അടുത്ത എപ്പിസോഡ്
@@ -84,8 +84,6 @@
കറുത്ത അതിർത്തി നീക്കംചെയ്യുക
പ്ലേയർ സബ്ടൈറ്റിലുകളുടെ സെറ്റിങ്സ്
-
- വേഗം നിയന്ത്രിക്കാൻ ഓപ്ഷൻ ചേർക്കുക
വീഡിയോപ്ലേയറിൽ സമയം നിയന്ത്രിക്കാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ സ്വൈപ്പുചെയ്യുക
@@ -169,11 +167,11 @@
ഔചിത്യ വീഡിയോ ക്വാളിറ്റി
ചരിത്രം
കണ്ടതാണെന്ന് അടയാളപ്പെടുത്തുക
- %1$d%2$d
- yg5t4r%dujyhtg
- %d മണിക്കൂർ %d മിനിറ്റ്
- %1$sghj%2$d
- rtf:%
+ %d ദിവസങ്ങൾ %d മണിക്കൂർ %d മിനിറ്റ്
+ അധ്യായം%dൽ റിലീസ് ചെയ്യും
+ %1$d മണിക്കൂർ %2$d മിനിറ്റ്
+ %1$sഅധ്യാ%2$d
+ കാസ്റ്റ്:%s
അക്കൗണ്ട് ഉണ്ടാക്കുക
പുറത്ത്പോകുന്നതോടുകൂടി ആപ് അപ്ഡേറ്റ് ആവുന്നതാണ്
ലൈബ്രറി തിരഞ്ഞെടുക്കുക
@@ -181,8 +179,8 @@
ട്രെയിലർ പ്ലേ ചെയ്യുക
ലൈവ് സ്ട്രീം പ്ലേ ചെയ്യുക
ഫില്ലർ
- %d min
- ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് കളിക്കുക
+ %d മിനിറ്റ്
+ ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് പ്രവർത്തിപ്പിക്കുക
അടുത്ത ക്രമരഹിതമായ
എപ്പിസോഡ് പോസ്റ്റർ
അപ്ഡേറ്റ് ആരംഭിച്ചു
@@ -190,7 +188,7 @@
പോസ്റ്റർ
ലോഡിംഗ് ഒഴിവാക്കുക
തിരയുക %s…
- %dm
+ %dമിനിറ്റ്
മടങ്ങിപ്പോവുക
പശ്ചാത്തല പ്രിവ്യൂ
പോസ്റ്റർ
@@ -204,8 +202,6 @@
പൊതു പട്ടിക
CloudStream-ന് സ്ഥിരസ്ഥിതിയായി സൈറ്റുകളൊന്നും ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. നിങ്ങൾ റിപ്പോസിറ്ററികളിൽ നിന്ന് സൈറ്റുകൾ ഇൻസ്റ്റാൾ ചെയ്യേണ്ടതുണ്ട്.
\n
-\nസ്കൈ യുകെ ലിമിറ്റഡിലെ ഡോഗ്ഷിറ്റ് ആളുകളിൽ നിന്ന് DMCA നീക്കം ചെയ്തതിനാൽ 🤮 ഞങ്ങൾക്ക് ആപ്പിൽ റിപ്പോസിറ്ററി സൈറ്റ് ലിങ്ക് ചെയ്യാൻ കഴിയില്ല.
-\n
\nഞങ്ങളുടെ ഡിസ്കോർഡിൽ ചേരുക അല്ലെങ്കിൽ ഓൺലൈനിൽ തിരയുക.
പകർത്തുക
എല്ലാ സബ്ടൈറ്റിലുകളും വലിയക്ഷരമാക്കുക
@@ -216,7 +212,7 @@
അക്കൗണ്ടുകൾ കൈകാര്യം ചെയ്യുക
ഉടൻ വരുന്നു…
പുനരാരംഭിക്കുമ്പോൾ പ്രയോഗിക്കുക
- അക്കൗണ്ട് എഡിറ്റ് ചെയ്യുക
+ അക്കൗണ്ട് തിരുത്തുക
തെറ്റായ പിൻ. ദയവായി വീണ്ടും ശ്രമിക്കുക.
നിർത്തുക
ട്രാക്കുകൾ
@@ -247,4 +243,41 @@
ഉറവിട പിശക്
നിലവിലെ പിൻ നൽകുക
ഓഡിയോ ട്രാക്കുകൾ
+ ചിത്രം-ഇൻ-ചിത്രം
+ പുതുക്കിയത് (പഴയത് മുതൽ പുതിയത് വരെ)
+ റേറ്റിംഗ് (ഉയർന്നത് മുതൽ താഴ്ന്നത്)
+ പാരമ്പര്യം
+ വിൻഡോ നിറം
+ ക്ലിയർ
+ ലോഗ്
+ ശുപാർശകൾ കാണിക്കുക
+ %s ആയി ലോഗിൻ ചെയ്തു
+ ഇങ്ങനെ അടുക്കുക
+ അടുക്കുക
+ തിരുത്തുക
+ പുതുക്കിയത് (പുതിയത് മുതൽ പഴയത് വരെ)
+ NSFW
+ ആപ്പ് അപ്ഡേറ്റ് ഇൻസ്റ്റാൾ ചെയ്യുന്നു…
+ അപ്ഡേറ്റുകളും ഒപ്പം ബാക്കപ്പും
+ %s(അപ്രാപ്തമാക്കി)
+ റേറ്റിംഗ് (താഴ്ന്നത് മുതൽ ഉയർന്നത് വരെ)
+ വാചക നിറം
+ ആപ്പിൻ്റെ പുതിയ പതിപ്പ് ഇൻസ്റ്റാൾ ചെയ്യാനായില്ല
+ പാക്കേജ് ഇൻസ്റ്റാളർ
+ അക്ഷരമാലാക്രമം (A മുതൽ Z വരെ)
+ അക്ഷരമാലാക്രമം (Z മുതൽ A വരെ)
+ ഈ ലിസ്റ്റ് ശൂന്യമാണ്. മറ്റൊന്നിലേക്ക് മാറാൻ ശ്രമിക്കുക.
+ ചരിത്രം മായ്ക്കുക
+ ലോഗ്കാറ്റ് കാണിക്കുക 🐈
+ നിങ്ങളുടെ ലൈബ്രറി ശൂന്യമാണ് :(
+\nഒരു ലൈബ്രറി അക്കൗണ്ടിൽ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ നിങ്ങളുടെ പ്രാദേശിക ലൈബ്രറിയിലേക്ക് ഷോകൾ ചേർക്കുക.
+ വീഡിയോ
+ റിപ്പോസിറ്ററി നാമവും URL ഉം
+ പകർത്തി!
+ പുതിയ എപ്പിസോഡ് അറിയിപ്പ്
+ മറ്റ് വിപുലീകരണങ്ങളിൽ തിരയുക
+ ഉപശീർഷകം ക്രമീകരണങ്ങൾ
+ എഡ്ജ് തരം
+ ഔട്ട്ലൈൻ നിറം
+ പശ്ചാത്തല നിറം
diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml
new file mode 100644
index 00000000..b2c0356a
--- /dev/null
+++ b/app/src/main/res/values-mt/strings.xml
@@ -0,0 +1,126 @@
+
+
+ Preferenzi tas-sottotitli
+ Kulur tal-kitba
+ Kulur tat-Tieqa
+ Fittex bl-użu ta \'tipi
+ Importa fonts billi tpoġġihom ġo %s
+ Dan il-fornitur huwa torrent, VPN huwa rakkomandat
+ Atturi: %s
+ L-episodju %d ha johrog fil
+ %1$dh %2$dm
+ %dm
+ Kartellun
+ Kartellun
+ Kartellun tal-episodju
+ Kartellun Principali
+ Li jmiss bl\'addoċċ
+ Ibdel Il-fornitur
+ veloċità (%.2fx)
+ Klassifikazzjoni: %.1f
+ Aġġornament ġdid misjub!
+\n%1$s -> %2$s
+ %d min
+ CloudStream
+ Ara bil-CloudStream
+ Dar
+ Fittex
+ Imnizzel
+ Preferenzi
+ Fittex…
+ Fittex%s…
+ Bla dejta
+ Iktar Preferenzi
+ L-episodju li\'jmiss
+ Ġeneri
+ Aqsam
+ Iftah fil-brawser
+ Brawser
+ Aqbez it-tagħbija
+ Tagħbija…
+ Jaraw
+ Stenna ftit
+ Lest
+ Imwaqqa
+ Pjana biex tara
+ Terġa\' tara
+ Ibda t-trejler
+ Ibda l-livestream
+ Stream Torrent
+ Sorsi
+ Erġa\' pprova l-konnessjoni…
+ Mur lura
+ Ibda l-episodju
+ Tniżżila ppawzata
+ Qed jinżlu
+ Imniżżel
+ Tniżżil ikkanċellat
+ Lest it-tniżżil
+ Beda l-aġġornament
+ Network stream
+ Tagħbija tal-Links falliet
+ Links regaw gew mogħbija
+ Ħażna Interna
+ Dub
+ Ibda
+ Info
+ Issettja l-istatus ta-rajtux
+ Applika
+ Ikkopja
+ Għalaq
+ Neħħi
+ Issevja
+ Isem tar-repożitorju u URL
+ Ikkupjat!
+ Notifika ta\' episodju ġdid
+ Fittex f\'estensjonijiet oħra
+ Uri r-rakkomandazzjonijiet
+ Veloċità tal-Plejer
+ Kulur tal-Kontorn
+ Kulur tal-Isfond
+ Tip tat-tarf
+ Elevazzjoni tas-Sottotitolu
+ Font
+ Daqs tal-font
+ Fittex bl-użu ta\' fornituri
+ %d Benenes mogħtija lil devs
+ Ebda Benenes mogħtija
+ Agħżel il-Lingwa Awtomatikament
+ Niżżel Lingwi
+ Lingwa tas-sottotitolu
+ Żomm biex tirrisettja għal default
+ Kompli Ara
+ Neħħi
+ Iktar informazzjoni
+ @string/home_play
+ Jista\' jkun hemm bżonn ta\' VPN biex dan il-fornitur jaħdem b\'mod korrett
+ Il-metadata mhix ipprovduta mis-sit, it-tagħbija tal-vidjo se tfalli jekk ma teżistix fuq is-sit.
+ Deskrizzjoni
+ Lebda Plot misjub
+ Lebda Deskrizzjoni misjuba
+ Uri Logcat 🐈
+ ġurnal
+ Stampa f-istampa
+ Ikompli d-daqq fi player minjatura fuq apps oħra
+ %1$s Ep %2$d
+ %1$dd %2$dh %3$dm
+ Mur Lura
+ Ara l\'isfond
+ Mili
+ Ibda l-film
+ Sottotitli
+ Sut
+ Ibda l-fajl
+ Niżżel
+ Hassar il-fajl
+ Kompli Nizzel
+ Ieqaf Nizzel
+ Iddiżattiva r-rappurtar awtomatiku tal-bugs
+ Iktar Informazzjoni
+ Aħbi
+ Iffiltra l-Bookmarks
+ Beda t-tniżżil
+ Bookmarks
+ Neħħi
+ Falla t-tniżżil
+
diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml
index f60362ae..ef796f9f 100644
--- a/app/src/main/res/values-my/strings.xml
+++ b/app/src/main/res/values-my/strings.xml
@@ -263,7 +263,6 @@
အက်ပ်ထဲဝင်လိုက်သည့်နှင့်အက်ပ်အပ်ဒိတ်ကိုစစ်ဆေးပါ။
Chromecast စာတန်းထိုးများ
Chromecast စာတန်းထိုး ပြုပြင်ရန်
- ကြည့်ရှုမှုပုံစံထဲမှာအရိှန်ရွေးစရာတစ်ခုထည့်ရန်
အသံအတိုးအကျယ်နှင့်အလင်းအမှောင်များကိုချိန်ညိှရန် ဘယ် သို့ ညာ ဘက်တွင် အပေါ်အောက်ဆွဲပါ
ယခုကြည့်နေသောအပိုင်းပြီးပါကနောက်အပိုင်းကိုဖွင့်ပါ
သင့်၏အပိုင်းကြည်ရှုမှုရောက်ရှိနေရာကိုအလိုအလျောက်သိမ်းဆည်းပါ
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 5b594334..fc537837 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -124,7 +124,6 @@
Chromecast Ondertitels
Chromecast ondertitels instellingen
Eigengravy Modus
- Voegt een snelheidsoptie toe in de speler
Swipe to seek
Veeg naar links of rechts om de tijd in de videospeler te regelen
Veeg om instellingen te wijzigen
@@ -606,4 +605,7 @@
PIN invoeren
PIN
Huidige PIN invoeren
+ Link opnieuw geladen
+ Autoroteer
+ Roteer
diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml
index 49b559ed..724f4a63 100644
--- a/app/src/main/res/values-no/strings.xml
+++ b/app/src/main/res/values-no/strings.xml
@@ -98,7 +98,6 @@
Undertekster
Innstillinger for spillerens teksting
Eigengravy Modus
- Legger til hastighetsalternativ i spilleren
Sveip for å søke
Sveip til venstre eller høyre for å kontrollere tiden i videospilleren
Sveip for å endre innstillinger
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index cb7cf73d..c61f0104 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -52,7 +52,7 @@
Błąd przy pobieraniu
Anulowano pobieranie
Zakończono pobieranie
- Odtwórz
+ Strumień sieciowy
Błąd przy ładowaniu linków
Pamięć wewnętrzna
Dub
@@ -112,18 +112,17 @@
Ustawienia napisów
Napisy Chromecast
Ustawienia napisów Chromecast
- Tryb Eigengravy
- Ustawienia prędkości
- Przesuń aby przewinąć
+ Prędkość odtwarzania
+ Przesuń, aby przewinąć
Przesuwaj w lewo lub prawo, aby kontrolować czas filmu
- Przesuń aby zmienić ustawienia
- Przesuwaj góra-dół z lewej lub prawej strony ekranu aby zmienić jasność czy głośność
+ Przesuń, aby zmienić ustawienia
+ Przesuwaj góra-dół z lewej lub prawej strony ekranu, aby zmienić jasność czy głośność
Autoodtwarzanie następnego odcinka
Rozpocznij następny odcinek po skończeniu bieżącego
Czas przewinięcia przy podwójnym kliknięciu (w sekundach)
- Podwójne kliknięcie aby przewinąć
- Kliknij 2 razy z prawej lub lewej strony aby przewinąć
- Kliknij dwukrotnie aby wstrzymać
+ Podwójne kliknięcie, aby przewinąć
+ Kliknij dwa razy z prawej lub lewej strony, aby przewinąć
+ Kliknij dwukrotnie, aby wstrzymać
Kliknij dwukrotnie na środku, aby zatrzymać wideo
Użyj jasności systemowej
Użyj jasności systemowej w odtwarzaczu aplikacji zamiast ciemnej nakładki
@@ -137,8 +136,8 @@
Brak uprawnień do pamięci, spróbuj ponownie.
Błąd tworzenia kopii zapasowej %s
Szukaj
- Konta
- Aktualizacje i kopia zapasowa
+ Konta i zabezpieczenia
+ Aktualizacje i kopie zapasowe
Informacje
Zaawansowane wyszukiwanie
Szukaj z podziałem na źródła
@@ -210,7 +209,7 @@
Torrenty
Filmy dokumentalne
OVA
- Dramy azjatyckie
+ Dramaty azjatyckie
Transmisje na żywo
NSFW
Inne
@@ -220,7 +219,7 @@
Kreskówka
Torrent
Film dokumentalny
- Drama azjatycka
+ Dramat azjatycki
Transmisja na żywo
NSFW
Inne
@@ -229,8 +228,8 @@
Błąd renderowania
Nieoczekiwany błąd odtwarzacza
Błąd pobierania, sprawdź uprawnienia aplikacji
- odcinek Chromecast
- mirror dla Chromecast
+ Odcinek Chromecast
+ Mirror dla Chromecast
Odtwórz w aplikacji
Odtwórz w %s
Odtwórz w przeglądarce
@@ -254,7 +253,7 @@
Pomiń tę aktualizację
Aktualizacja
Domyślna jakość (WiFi)
- Maksymalna ilość znaków w tytule odtwarzacza
+ Maksymalna liczba znaków w tytule odtwarzacza
Rozdzielczość odtwarzacza wideo
Rozmiar bufora wideo
Długość bufora wideo
@@ -276,11 +275,11 @@
Zastrzeżenie
Ogólne
Przycisk do losowania
- Pokaż przycisk do losowania na stronie głównej
- Języki źródeł
+ Pokaż przycisk do losowania na stronie głównej i w bibliotece
+ Języki rozszerzeń
Układ aplikacji
- Preferowane media
- Włącz NSFW w obsługiwanych źródłach
+ Preferowane multimedia
+ Włącz NSFW w obsługiwanych rozszerzeniach
Kodowanie napisów
Źródła
Układ interfejsu
@@ -365,14 +364,14 @@
Filtrowanie wg preferowanego języka
Dodatki
Zwiastun
- Odsyłacz
+ Odsyłacz (opcjonalny)
Następny
Wyświetlaj filmy w tych językach
Poprzedni
- Pomiń setup
+ Pomiń konfigurację
Dostosuj wygląd aplikacji do urządzenia
Zgłaszanie błędów
- Co chciałbyś obejrzeć
+ Co chcesz obejrzeć
Gotowe
Rozszerzenia
Dodaj repozytorium
@@ -397,8 +396,6 @@
Zaaktualizowano %d rozszerzeń
CloudStream nie ma domyślnie zainstalowanych żadnych witryn. Musisz zainstalować witryny z repozytoriów.
\n
-\nZ powodu bezmyślnego usunięcia DMCA przez Sky UK Limited 🤮 nie możemy zamieścić linku do witryny z repozytoriami.
-\n
\nDołącz do naszego Discorda lub poszukaj online.
Zobacz repozytoria społeczności
Publiczna lista
@@ -408,7 +405,7 @@
Ścieżki
Ścieżki audio
Ścieżki wideo
- Zastosuj po ponownym uruchomieniu
+ Uruchom ponownie aplikację, aby zobaczyć zmiany.
Tryb bezpieczny włączony
Z powodu wystąpienia błędu wszystkie rozszerzenia zostały wyłączone, aby ułatwić wykrycie tego wadliwego.
Wyświetl informacje o błędzie
@@ -433,7 +430,7 @@
Wyczyść historię
Historia
Za dużo tekstu. Nie można skopiować do schowka.
- Link do odtwarzania
+ https://example.com/example.mp4
Odtwórz w CloudStream
Pomiń %s
%1$dh %2$dm
@@ -453,17 +450,17 @@
Podsumowanie
Instalator APK
Niektóre telefony nie obsługują nowego instalatora pakietów. Wypróbuj tryb legacy, jeśli aktualizacje nie zostaną zainstalowane.
- password123
+ hasło123
@string/ova
- MojaFajnaWitryna
- MyCoolUsername
+ NowaNazwaWitryny
+ Nazwa użytkownika
127.0.0.1
Tryb kompatybilności
- przyklad.pl
+ https://example.com
/\?\?
Instalator pakietów
@string/home_play
- hello@world.com
+ witaj@poczta.pl
@string/anime
Opening
Ending
@@ -518,7 +515,7 @@
Rozpocznij
Nie powiodło się
Ukończone powodzeniem
- Serwer pośredniczący raw.githubusercontent.com
+ Serwer proxy GitHuba
Obejścia ISP
Test dostawcy
Zatrzymaj
@@ -528,8 +525,8 @@
Zasubskrybowano %s
Anulowano subskrypcję %s
Został wydany odcinek %d!
- Obchodzi blokadę GitHuba za pomocą jsDelivr. może spowodować opóźnienie aktualizacji o kilka dni.
- Nie udało się połączyć z GitHub, włączono serwer pośredniczący jsDelivr…
+ Obchodzi blokadę surowych adresów URL GitHuba za pomocą jsDelivr. Może powodować opóźnienie aktualizacji o kilka dni.
+ Nie udało się połączyć z GitHubem. Włączono serwer pośredniczący jsDelivr…
Domyślna jakość (dane mobilne)
W tym miejscu można zmienić kolejność źródeł. Jeśli wideo ma wyższy priorytet, pojawi się wyżej w wyborze źródła. Priorytet wideo jest sumą priorytetu źródła i priorytetu jakości.
\n
@@ -562,7 +559,7 @@
\n%s
\n
\nCzy chcesz dodać ten element, zastąpić istniejące, czy anulować operację?
- Wprowadź pin dla %s
+ Wprowadź PIN dla %s
Częstotliwość tworzenia kopii zapasowych
Znaleziono potencjalny duplikat
Zablokuj profil
@@ -592,4 +589,33 @@
Automatyczny obrót
Obrót
Włącz automatyczne przełączanie orientacji ekranu na podstawie orientacji filmu
+ Dodaje opcję prędkości w odtwarzaczu
+ Powiadomienie o nowym odcinku
+ Szukaj w innych rozszerzeniach
+ Pokaż rekomendacje
+ Przetestuj wszystkie rozszerzenia
+ Ten test jest przeznaczony wyłącznie dla programistów i nie weryfikuje ani nie zaprzecza działaniu żadnego rozszerzenia.
+ Zablokuj za pomocą biometrii
+ Uwierzytelnianie hasłem/kodem PIN
+ Ten ekran został zamknięty z powodu wielu nieudanych prób. Uruchom ponownie aplikację.
+ Odblokuj CloudStream
+ To urządzenie nie obsługuje uwierzytelniania biometrycznego
+ Odblokuj aplikację za pomocą odcisku palca, identyfikatora twarzy, kodu PIN, wzoru i hasła.
+ Kopia zapasowa Twoich danych CloudStream została teraz utworzona. Chociaż prawdopodobieństwo tego jest bardzo niskie, wszystkie urządzenia mogą zachowywać się inaczej. W rzadkich przypadkach, gdy dostęp do aplikacji zostanie zablokowany, należy całkowicie wyczyścić dane aplikacji i przywrócić je z kopii zapasowej. Bardzo nam przykro z powodu wszelkich niedogodności z tym związanych.
+ Usuń z ulubionych
+ %s
+\npozostało
+ Dodaj do ulubionych
+ Nazwa repozytorium i adres URL
+ Błąd dostępu do schowka. Spróbuj ponownie.
+ skopiowano!
+ Błąd podczas kopiowania. Skopiuj logcat i skontaktuj się z pomocą techniczną aplikacji.
+ Wyłącz optymalizację akumulatora
+ Nie można otworzyć informacji o aplikacji CloudStream.
+ Muzyka
+ Audiobook
+ OK
+ Multimedia
+ Użycie akumulatora przez aplikację jest już ustawione na nieograniczone
+ Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach telewizyjnych, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do informacji o aplikacji. Tam przewiń do użycia akumulatora przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać akumulator. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach głównych.
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 398a1aa3..06e2352c 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -59,7 +59,7 @@
Falha no Download
Download cancelado
Download concluído
- Stream
+ Transmitir
Erro a Carregar Links
Armazenamento Interno
Dob
@@ -118,8 +118,7 @@
Configurações de legendas do player
Legendas do Chromecast
Configurações de legendas do Chromecast
- Modo Eigengravy
- Acrescenta uma opção de velocidade no player
+ Velocidade de reprodução
Deslize para andar
Deslize para os lados para controlar a posição em um vídeo
Deslize para mudar as configurações
@@ -143,8 +142,8 @@
Permissão de armazenamento não encontrada, por favor tente novamente.
Erro no backup de %s
Procurar
- Contas
- Atualizações e backup
+ Contas e segurança
+ Atualizações e cópias de segurança
Info
Procura Avançada
Mostra resultados separados por fornecedor
@@ -273,10 +272,10 @@
Geral
Botão Aleatório
Mostrar botão aleatório na página inicial
- Idioma dos fornecedores
+ Línguas de extensão
Layout da App
Mídia preferida
- Ativar NSFW em fornecedores compatíveis
+ Ativar NSFW nas Extensões suportadas
Codificação das legendas
Fornecedores
Layout
@@ -289,11 +288,11 @@
Local do título do poster
Coloca o título debaixo do poster
senha123
- MeuNomeFixe
+ Nome de utilizador
ola@mundo.com
127.0.0.1
- MeuSiteFixe
- examplo.com
+ NovoNomedoSite
+ https://example.com
Codigo da Língua (pt)
Conta
Sair
@@ -374,9 +373,7 @@
Transferido: %d
Desativado: %d
Não transferido: %d
- O CloudStream não possui sites instalados por padrão. Você precisa instalar os sites a partir de repositórios.
-\n
-\nDevido a uma restrição sem sentido de direitos autorais (DMCA) pela Sky UK Limited 🤮 não podemos vincular o site do repositório no aplicativo.
+ O CloudStream não tem sites instalados por padrão. É necessário instalar os sites a partir de repositórios.
\n
\nJunte-se ao nosso Discord ou pesquise online.
Ver repositórios da comunidade
@@ -443,7 +440,7 @@
Cam
Abertura
Selecionar Biblioteca
- Ignora o bloqueio do GitHub usando jsDelivr. Pode fazer com que as actualizações sejam atrasadas por alguns dias.
+ Contorna o bloqueio de URLs raw do GitHub usando jsDelivr. Pode atrasar as atualizações por uns dias.
VLC
Todas as linguagens
Atualizado (Novo para Antigo)
@@ -461,10 +458,10 @@
Inscrição cancelada em %s
Final misto
Avaliações (Decrescente)
- Aplicar ao reiniciar
- Referente
+ Reinicie a aplicação para ver as alterações.
+ Referenciador (opcional)
Player oculto - Quantidade de Busca
- raw.githubusercontent.com Proxy
+ Proxy do GitHub
Blu-ray
Aparência
1000 ms
@@ -476,7 +473,7 @@
Ver informações sobre falha
Aplicativo não encontrado
Reverter
- Link para transmitir
+ https://example.com/example.mp4
Plugins baixados
%d plugins atualizados
Pular %s
@@ -551,4 +548,71 @@
Não foram encontrados plugins no repositório
Repositório não encontrado, verifique o URL e tente a VPN
Você já votou
+ Cancelar Inscrição
+ Subscrever
+ Favoritos
+ A recarregar links
+ Frequência de Backup
+ %s removido dos favoritos
+ Adicionar aos favoritos
+ Possível duplicata encontrada
+ %s adicionado aos favoritos
+ Remover dos favoritos
+ Substituir
+ Substituir Tudo
+ Insira o PIN atual
+ PIN
+ PIN incorreto. Por favor, tente novamente.
+ O PIN deve ter 4 caracteres
+ Editar conta
+ Conectado como %s
+ Ignorar a seleção da conta na inicialização
+ Girar
+ Digite o PIN para %s
+ Bloquear Perfil
+ Selecione uma conta
+ Gerenciar contas
+ Usar conta padrão
+ Potenciais itens duplicados foram encontrados na sua biblioteca:
+\n
+\n%s
+\n
+\nDeseja adicionar esse item mesmo assim, subtituir os existentes, ou cancelar a ação?
+ Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\'
+\n
+\nDeseja adicionar esse item mesmo assim, subtituir o existente, ou cancelar a ação?
+ Mostrar recomendações
+ Adiciona uma opção de velocidade no leitor
+ Testar todas as extensões
+ Este teste destina-se apenas a programadores e não verifica ou nega o funcionamento de qualquer extensão.
+ Adicionar
+ Insira o PIN
+ Notificação de novo episódio
+ Procurar noutras extensões
+ Apresentar um botão de alternância para a orientação do ecrã
+ Ativar a mudança automática da orientação do ecrã com base na orientação do vídeo
+ Rotação automática
+ Nome do repositório e URL
+ Favorito
+ Desfavorito
+ Bloqueio com biometria
+ copiado!
+ %s
+\nrestante
+ Erro ao aceder à área de transferência, tente novamente.
+ Erro ao copiar, copie o logcat e contacte o suporte da aplicação.
+ Desbloquear o CloudStream
+ Autenticação por palavra-passe/PIN
+ A autenticação biométrica não é suportada neste dispositivo
+ Desbloqueie a aplicação com impressão digital, ID facial, PIN, padrão e palavra-passe.
+ Este ecrã foi encerrado devido a várias tentativas falhadas. Reinicie a aplicação.
+ Os dados do seu CloudStream já foram copiados. Embora a possibilidade de isto acontecer ser muito baixa, todos os dispositivos podem comportar-se de forma diferente. No caso raro de ficar impedido de aceder à aplicação, limpe completamente os dados da aplicação e restaure a partir de uma cópia de segurança. Lamentamos qualquer incómodo causado por esta situação.
+ OK
+ A utilização da bateria da aplicação já está definida como sem restrições
+ Não é possível abrir a informação da aplicação CloudStream.
+ Música
+ Livro Aúdio
+ Multimédia
+ Desativar a otimização da bateria
+ Para garantir descarregamentos ininterruptos e notificações de programas de TV subscritos, o CloudStream precisa de permissão para ser executado em segundo plano. Ao premir OK, será direcionado para informações da aplicação. Aí, desloque-se para utilização da bateria da aplicação e defina a utilização da bateria para sem restrições. Tenha em atenção que esta permissão não significa que o CS3 irá esgotar a sua bateria. Este só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se optar por cancelar, pode ajustar esta definição mais tarde em definições gerais.
diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml
index 72f16012..5de97c7d 100644
--- a/app/src/main/res/values-qt/strings.xml
+++ b/app/src/main/res/values-qt/strings.xml
@@ -82,7 +82,6 @@
ahhhaauugghh
oha ooh ouuhhh oooohhahhh ouuhhh
haaahhh ahoouuh
- haaoooohhaaahhuoha ouuhhh ah oouuh ohoohaaahhu
ohahaaaauugghh ahooo aaahhu
aaaghh aaaghhohahooooo ouuhhh oouuh ooo-ahahahooo-ahah ohaaaaaghh
aaaaaahhaaahhuoouuhaaaaa aahooo
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index 1a084e02..d7da44b4 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -120,8 +120,7 @@
Setări de subtitrare
Subtitrări Chromecast
Setări pentru subtitrare Chromecast
- Modul Eigengravy
- Adăugați opțiunea de viteză în player
+ viteza de redare
Derulați spre înainte/înapoi
Derulați dintr-o parte în alta pentru a controla timpul de difuzare a videoclipului
Derulați pentru a modifica setările
@@ -273,7 +272,7 @@
Orice probleme legale privind conținutul acestei aplicații ar trebui să fie rezolvate cu furnizorii și gazdele actuale de fișiere, întrucât noi nu suntem afiliați cu aceștia. În caz de încălcare a drepturilor de autor, vă rugăm să contactați direct părțile responsabile sau site-urile de streaming. Aplicația este destinată exclusiv utilizării educaționale și personale. CloudStream 3 nu găzduiește niciun fel de conținut în aplicație și nu are niciun control asupra conținutului media care este pus sau retras. CloudStream 3 funcționează ca orice alt motor de căutare, cum ar fi Google. CloudStream 3 nu găzduiește, nu încarcă și nu gestionează niciun videoclip, film sau conținut. Pur și simplu navighează, adună și afișează linkuri într-o interfață convenabilă și ușor de utilizat. Pur și simplu, acesta extrage paginile web ale unor terțe părți care sunt accesibile publicului prin intermediul oricărui browser web obișnuit. Este responsabilitatea utilizatorului de a evita orice acțiune care ar putea încălca legile care guvernează locația sa. Utilizați CloudStream 3 pe propria răspundere.
General
Aleatoriu
- Afișați butonul Aleatoriu pe pagina de start
+ Afișați butonul aleatoriu pe pagina de start și în bibliotecă
Limba furnizorului
Aplicație de prezentare
Media preferată
@@ -289,11 +288,11 @@
Locația titlului de pe poster
parola123
- David Popovici
+ Numele utilizatorului/utilizatoarei
davidpopovici@gmail.com
127.0.0.1
- David Popovici
- davidpopovici.com
+ NouNumeSite
+ https://exemplu.com
Cod limbă (RO)
%1$s %2$s
Cont
@@ -309,7 +308,7 @@
%d / 10
/\?\?
/%d
- %s autentificat
+ %s autentificat/ă
Nu s-a putut autentifica la %s
Nu există
@@ -396,11 +395,11 @@
NSFW
%1$d-%2$d
Player Afișat - Căutați Suma
- Player Ascuns - Căutați Suma
+ Player Ascuns/ă - Căutați Suma
Livestream-uri
NSFW
Eșuat
- Cantitatea de căutare utilizată atunci când playerul este vizibil
+ Suma căutată și utilizată atunci când player-ul este vizibil/ă
Livestream
Cantitatea de căutare utilizată atunci când playerul este ascuns
Calitatea preferată (Date Mobile)
@@ -484,11 +483,9 @@
Toate extensiile au fost dezactivate din cauza unei defecțiuni pentru a vă ajuta să o găsiți pe cea care cauzează probleme.
Se descarcă actualizarea aplicației…
Browser web
- CloudStream nu are niciun site instalat în mod implicit. Trebuie să instalați site-urile din depozite.
+ CloudStream nu are niciun site instalat din start. Trebuie să instalați site-urile din depozite.
\n
-\nDin cauza unui DMCA takedown fără creier de către Sky UK Limited 🤮 nu putem lega site-ul de depozit în aplicație.
-\n
-\nAlăturați-vă Discordului nostru sau căutați online.
+\nAlăturați-vă Discord-ului nostru sau căutați online.
A început să descarce %1$d %2$s…
Mod sigur pornit
Fișier Mod Sigur găsit!
@@ -513,8 +510,8 @@
Repornește
Activează NSFW la furnizori suportate
Nu s-a putut ajunge la GitHub. Se activează proxy-ul jsDelivr…
- Proxy raw.githubusercontent.com
- Depășește blocarea GitHub folosind jsdelivr, poate cauza întârzieri de câteva zile la actualizări.
+ Proxy GitHub
+ Depășește blocarea GitHub folosind jsDelivr. Poate cauza întârzieri de câteva zile la actualizări.
Următorul
Toate %s deja descărcate
S-a descărcat: %d
@@ -528,7 +525,7 @@
Moştenit
Test de furnizor
Furnizori
- Link către stream
+ https://example.com/example.mp4
Acest lucru va șterge, de asemenea, toate plugin-urile din depozit
Se instalează actualizarea aplicației…
S-a descărcat %1$d %2$s
@@ -569,4 +566,31 @@
UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s
Selectați modul de filtrare a descărcării plugin-urilor
Ați votat deja
+ Elemente potențial duplicate au fost găsite în biblioteca ta:
+\n
+\n%s
+\n
+\nÎn ciuda acestui fapt, ai dori să adaugi acest alement, să le înlocuiești pe cele existente, sau să anulezi acțiunea?
+ %s a fost adăugat la favoriți/te
+ %s a fost eliminat din favoriți/te
+ Adaugă la favoriți/te
+ Elimină din favoriți/te
+ Se pare că un element potențial duplicat deja există în biblioteca ta: \'%s.\'
+\n
+\nÎn ciuda aceasta, ai dori să adaugi acest element, să îl înlocuiești pe cel existent, sau să anulezi acțiunea?
+ Introduce PIN-ul pentru %s
+ Introduce PIN-ul actual
+ Introduce PIN-ul
+ Blochează Profilul
+ Dezabonează-te
+ Abonează-te
+ Adaugă
+ Înlocuiește
+ Înlocuiește tot
+ Posibil Duplicat Găsit
+ Notificare episod nou
+ Arată sugestii
+ Adaugă o opțiune de viteză la player
+ Favoriți/te
+ Frecvența de backup
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 82bd3581..cf456f56 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -15,7 +15,7 @@
Отмена
Все
Пауза
- Актёрский состав: %s
+ В ролях: %s
Название источника
Войти
Нет
@@ -39,7 +39,7 @@
Заполнитель
CloudStream
Убирать
- %1$s Серия %2$d
+ %1$s Ep %2$d
Смотреть с CloudStream
Главная
Поиск
@@ -71,8 +71,8 @@
Скачано
Скачивание
Скачать остановлена
- Скачать начатый
- Скачать отменённый
+ Загрузка началась
+ Загрузка отменена
Скачать выполнено
Инфо
Обновление началось
@@ -99,17 +99,17 @@
Цвет фона
Цвет окна
Тип края
- Субтитр подъём
- Поиск с использованием поставщиков
+ Подъем субтитров
+ Поиск с использованием провайдеров
Поиск с использованием типов
- %d Бенены данность на разрабы
- Бенены не дают
+ %d бенен(а/ов) выдано разрабам
+ Бенены не выданы
Автовыбор языка
Скачать языки
Язык субтитров
Удерживайте, чтобы сбросить по умолчанию
Ошибка загрузки ссылок
- Поток
+ Сетевой Поток
Шрифт
Размер шрифта
Удалить файл
@@ -132,13 +132,12 @@
Картинка в картинке
Продолжение воспроизведения в миниатюрном проигрывателе поверх других приложений
Кнопка изменения размера проигрывателя
- Удалите черные границы
+ Убрать черные границы
Субтитры
Настройки субтитров проигрывателя
Субтитры Chromecast
Настройки субтитров Chromecast
- Режим Eigengravy
- Добавляет опцию скорости в проигрывателе
+ Скорость воспроизведения
Проведите пальцем для поиска
Проведите пальцем для изменения настроек
Проведите вверх или вниз по левой или правой стороне, чтобы изменить яркость или громкость
@@ -149,14 +148,14 @@
Загружена резервная копия
Не удалось восстановить данные из %s
Отсутствует разрешение на хранение. Пожалуйста попробуйте снова.
- Аккаунты
- Обновления и резервное
+ Аккаунты и Безопасность
+ Обновления и Резервное копирование
Информация
Расширенный поиск
Показывать трейлеры
- Скрыть выбранное качество видео в результатах поиска
- Автоматическое обновление плагинов
- Автоматическая загрузка плагинов
+ Скрыть выбранные форматы видео в результатах поиска
+ Автообновление плагинов
+ Автозагрузка плагинов
Показать обновления приложения
Автоматически проверять обновления при старте приложения.
Обновится до пре-релиза
@@ -197,10 +196,10 @@
Сезон
Аниме приложение от тех же разработчиков
Автоматически загружать еще не установленные плагины из добавленных репозиториев.
- Присоединится в Discord
- Бесплатно
+ Присоединиться к Discord-серверу
+ Свободно
%dm
- %1$d ч. %2$d мин.
+ %1$dч %2$dм
Фильмы
Мультфильм
Сериалы
@@ -218,7 +217,7 @@
Азиатская драма
Общие
Провайдеры
- Макет
+ Расстановка
Расширения
Плеер
Резервное копирование данных
@@ -246,25 +245,25 @@
Оговорка
Синхронизация субтитров
Добавить клон существующего сайта с другим URL-адресом
- Используется для обхода блокировок интернет провайдера
+ Используется для обхода блокировок интернет-провайдера
Путь скачивания
Давал бенен
Обновить
Основной цвет
- Языки поставщиков
+ Языки провайдеров
Название репозитория
Очистить историю
- Referer
+ Реферер (необязательно)
Дайте бенен разрабам
Ссылки
- Макет
- Макет приложения
+ Расстановка
+ Расстановка приложения
Тема приложения
Добавить репозиторий
Убрать отметку
Вы уверены, что хотите выйти\?
Плагин скачан
- Плагин удалён
+ Плагин удален
Описание
Версия
Статус
@@ -276,7 +275,7 @@
Пропустить %s
Концовка
Используйте яркость системы в проигрывателе приложения вместо темного наложения
- Обновить состояние хода просмотра
+ Обновлять прогресс просмотра
Данные сохранены
Показывает результаты поиска, разделенные по провайдеру
Поиск предварительных обновлений вместо полных выпусков
@@ -302,7 +301,7 @@
Больше не показывать
Пропустить это обновление
URL сервера NGINX
- Создать учётную запись
+ Создать аккаунт
Добавить слежение
Добавлено %s
Синхронизировать
@@ -325,7 +324,7 @@
Отчистить кеш видео и изображений
Вызывает сбои, если установлено слишком высокое значение на устройствах с небольшим объемом памяти, таких как Android TV.
Вызывает проблемы, если установлено слишком высокое значение на устройствах с небольшим объемом памяти, таких как Android TV.
- Легкая новелла от тех же разработчиков
+ Легкое приложение для новелл от тех же разработчиков
Язык
Плейлист HLS
Сначала установить расширение
@@ -344,13 +343,13 @@
Эмулятор
Под плакатом
parol123
- МоёИмяПользователя
- Сменить учётную запись
- Добавить учётную запись
- МойКрутойСайт
- example.com
+ Юзернейм
+ Сменить аккаунт
+ Добавить аккаунт
+ ИмяСайта
+ https://example.com
Код языка (ru)
- учётная запись
+ аккаунт
Автоматически
127.0.0.1
Обновления приложения
@@ -423,9 +422,6 @@
%s (отключено)
Далее
В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев.
-\n
-\nИз-за безмозглой жалобы DMCA от Sky UK Limited 🤮 мы не можем привязать сайт репозитория в приложении.
-\n
\nПрисоединяйтесь к нашему Discord-серверу или найдите в интернете.
Недопустимые данные
Разрешение и название
@@ -461,7 +457,7 @@
Загрузить из интернета
Загрузка обновления приложения…
Недопустимый URL
- Применить при перезапуске
+ Перезапустите приложение, чтобы увидеть изменения.
Отчеты ошибках
Что вы хотите увидеть
Смотрите видео на этих языках
@@ -469,8 +465,8 @@
Изображение постера
Пакетная загрузка
Скачайте список сайтов, который вы хотите использовать
- Отображать Аниме с Дубляжом/Субтитрами
- Включить NSFW на поддерживаемых провайдерах
+ Отображать аниме с дубляжом/субтитрами
+ Включить NSFW у поддерживаемых расширений (провайдеров)
Удалять скрытые субтитры из субтитров
Дополнительно
Изменить вид интерфейса, чтобы соответствовать устройству
@@ -491,11 +487,11 @@
Показывать всплывающие окна для пропуска вступления/заключения
Фильтровать по предпочитаемому языку медиа
Неверный ID
- Ссылка на стрим
- Отображать рандомную кнопку на Главной странице
+ https://example.com/example.mp4
+ Отображать рандомную кнопку в библиотеке и главной странице
Рандомная кнопка
Legacy (старый)
- Веб видеокаст
+ Web Video Cast
Не отправляет данные
Перезагрузить ссылки
Предпочтительные медиа
@@ -520,12 +516,12 @@
Вернуться
Подписался на %s
Предпочтительное качество видео (Мобильный интернет)
- raw.githubusercontent.com Прокси-сервер
- Не удалось подключиться к GitHub. Включаем проксирование через jsdelivr…
+ GitHub прокси
+ Не удалось подключиться к GitHub. Включаем проксирование через jsDelivr…
Эпизод %d выпущен!
Обходы провайдера
Обновление подписки на фильмы и сериалы
- Обход ограничения доступа к GitHub с помощью jsDelivr может задержать обновления на несколько дней.
+ Обход ограничения доступа к raw github URLs с помощью jsDelivr. Обновления могут задержаться на несколько дней.
Подписные
Отказались от подписки на %s
Мобильный интернет
@@ -552,4 +548,72 @@
\n
\nПРИМЕЧАНИЕ. Если сумма равна 10 или более, плеер автоматически пропустит загрузку при загрузке этой ссылки!
Ссылки перезагружены
+ Выбрать аккаунт
+ %s убран из избранных
+ Введите пин-код
+ Защитить профиль
+ Пин-код
+ Найден возможный дубликат
+ Добавить
+ Неверный пин-код. Попробуйте снова.
+ Пин-код должен состоять из 4 цифр
+ Изменить аккаунт
+ Управление аккаунтами
+ Вы вошли как %s
+ Пропускать выбор аккаунта при запуске
+ Повернуть
+ Показывать переключатель ориентации экрана
+ Заменить все
+ Потенциальные дубликаты элементов были найдены в вашей библиотеке:
+\n
+\n\'%s.\'
+\n
+\nВы хотите добавить этот элемент, заменить существующие или отменить это действие?
+ Убрать из избранных
+ Добавить в избранное
+ Включить автоматическую смену ориентации экрана на основе ориентации видео
+ Автоповорот
+ rotate_video_key
+ Использовать аккаунт по умолчанию
+ Отписаться
+ Заменить
+ Введите текущий пин-код
+ auto_rotate_video_key
+ Избранное
+ %s добавлен в избранное
+ Введите пин-код от %s
+ Подписаться
+ Частота резервного копирования
+ Похоже, что потенциальный дубликат элемента уже существует в вашей библиотеке: \'%s.\'
+\n
+\nВы хотите добавить этот элемент, заменить существующий или отменить это действие?
+ Оповещение о выходе нового эпизода
+ Искать в других расширениях
+ Показать рекомендации
+ Этот тест предназначен только для разработчиков и не подтверждает или не опровергает работоспособность провайдеров.
+ Добавление настроек скорости в плеер
+ Протестировать всех провайдеров
+ скопировано!
+ ОК
+ Имя репозитория и URL адрес
+ Ошибка доступа к буферу обмена, пожалуйста, попробуйте ещё раз
+ Ошибка при копировании, пожалуйста, скопируйте лог и свяжитесь с технической поддержкой.
+ Нелюбимое
+ Разблокировать CloudStream
+ Любимое
+ Использование батареи приложением уже настроено на неограниченное
+ Не удается открыть информацию о приложении CloudStream.
+ Заблокировать биометрией
+ Музыка
+ Аудиокнига
+ Медиа
+ Разблокируйте приложение с помощью отпечатка пальца, Face ID, PIN-кода, шаблона и пароля.
+ %s
+\nосталось
+ Отключить оптимизацию батареи
+ Аутентификация по паролю/PIN-коду
+ Биометрическая аутентификация на этом устройстве не поддерживается
+ Этот экран был закрыт из-за нескольких неудачных попыток. Пожалуйста, перезапустите приложение.
+ Ваши данные в CloudStream были скопированы. Хотя вероятность этого очень мала, все устройства могут вести себя по-разному. В редких случаях, когда доступ к приложению заблокирован, полностью удалите данные приложения и восстановите их из резервной копии. Мы приносим свои извинения за любые неудобства, связанные с этим.
+ Чтобы обеспечить бесперебойную загрузку и получение уведомлений о телепередачах, на которые вы подписаны, CloudStream необходимо разрешение на запуск в фоновом режиме. Нажав OK, вы перейдете к информации о приложении. Там перейдите к разделу 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и установите значение \"Использование батареи\" 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Пожалуйста, обратите внимание, что это разрешение не означает, что CS3 разрядит вашу батарею. Он будет работать в фоновом режиме только при необходимости, например, при получении уведомлений или загрузке видео с официальных расширений. Если вы решите отменить, вы можете изменить эту настройку позже в 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨.
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 490f74ac..ebaaa2ae 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -149,7 +149,6 @@
Použiť systémový jas
Obnoviť dáta zo zálohy
Dvojitým ťuknutím pretočiť
- Pridá možnosť rýchlosti do prehrávača
Automaticky sťahovať doplnky
Pripojte sa na Discord
Neodosiela žiadne dáta
diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml
index a63d44fe..7b0d2870 100644
--- a/app/src/main/res/values-so/strings.xml
+++ b/app/src/main/res/values-so/strings.xml
@@ -250,7 +250,6 @@
Qoraal-hoosaadka
Habaynta Qrl-hoosaadka Koromakaastiga
Habka Xawaare-kordhinta
- Waxaad kordhin karta xawaaraha aad ku daawanayso
Midig iyo bidix u jiid si aad marba dhinac ugu dhaafiso muqaalka
Laba jeer taabo midig ama bidix si aad u dhaafiso ama ku ceshato
Laba jeer taabo si aad u dhaafiso
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 0f6f37cd..76508c43 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -83,8 +83,7 @@
Ta bort de svarta kanterna
Undertexter
Inställningar för undertexter
- Eigengrau Läge
- Lägger till hastighetsalternativet i spelaren
+ Uppspelningshastighet
Svep för att strya tiden
Svep åt vänster eller höger för att styra tiden i videospelaren
Svep för att ändra inställningar
@@ -138,7 +137,7 @@
Inga undertexter
Standard
Tillgängligt
- Använtt
+ Använt
App
Filmer
Tv Serier
@@ -180,9 +179,9 @@
Använd system ljusstyrka
Använder systemets ljusstyrka instället för en svart överlaga
Visa filler avsnitt för anime
- Visa Dubbad/Subbad anime
- Anpassad till skärmstorlek
- Leverantörspråk
+ Visa dubbad/undertextad anime
+ Anpassa till skärmstorlek
+ Tilläggsspråk
Utsträckt
Inzoomad
Oförväntat uppspelingsfel
@@ -205,7 +204,7 @@
Logga ut
konto
Nerladdningsplats
- Kollar om
+ Tittar på nytt
Automatisk
DNS över HTTPS
" "
@@ -217,7 +216,7 @@
Chromecast-undertexter
Dubbeltryck i mitten för att pausa
Återställ data från backup
- Konton
+ Konton och säkerhet
Uppdateringar och backup
Automatiska pluginuppdateringar
%1$dd %2$dh %3$dm
@@ -294,7 +293,7 @@
UHD
Upplösning och titel
Fel
- Referer
+ Referent (valfritt)
Nästa
1000 ms
Lägga till en klon av en befintlig webbplats med en annan webbadress
@@ -327,7 +326,7 @@
Trailer
Applayout
Funktioner
- exempel.se
+ https://example.com
Språkkod (en)
Videocache på disken
Rensa video- och bildcache
@@ -348,7 +347,7 @@
Cache
Säkerhetskopiering
Undertexter
- Aktivera NSFW på sidor som stöds
+ Aktivera NSFW på tillägg som stöds
Undertextkodning
lösenord123
Fördröjning av undertexter
@@ -367,7 +366,7 @@
Titta på videor på dessa språk
Föregående
Spår
- Uppdaterar
+ Uppdatering påbörjad
Logg
Videospelarens hoppsträcka (Sekunder)
Ändra status
@@ -379,12 +378,12 @@
Slumpknapp
Visa sub
Kunde inte öppna appen
- raw.githubusercontent.com Proxy
+ GitHub Proxy
Webbläsarens videospelare
Installerar uppdatering till appen…
Kunde inte nå GitHub, sätter på jsDelivr proxy…
Leverantörer
- MinTrevligaWebbsida
+ Nytt webbplatsnamn
Ta bort reklam från undertexter
VLC
Alla språk
@@ -432,7 +431,7 @@
Profiler
Hjälp
Kvalitet
- Ström
+ Nätverks Stream
Databasens namn
All %s har redan laddats ner
Ladda ner alla tillägg från den här databasen?
@@ -480,7 +479,7 @@
Loggat in som %s
Hoppa över val av konto vid start
auto_rotera_video_nyckel
- Förbi passera blockering av GitHub genom att använda jsDelivr. Kan göra att uppdateringar försenas med några dagar.
+ Gå förbi blockering av rå GitHub-URL:er med jsDelivr. Kan göra att uppdateringar försenas med några dagar.
Funktion
Önskad media
Lägg titeln under affischen
@@ -499,9 +498,7 @@
Visa community databaser
Blandad inledning
Skippa %s
- CloudStream har inga webbplatser installerade som standard. Du måste installera webbplatserna från databaser.
-\n
-\nPå grund av en hjärnlös DMCA-borttagning av Sky UK Limited kan vi inte länka databaser på denna applikation.
+ CloudStream har inga webbplatser installerade som standard. Du måste installera webbplatser från arkiv.
\n
\nGå med i vår Discord eller sök online.
Välj bibliotek
@@ -551,7 +548,7 @@
Gester
%s autentiserad
Statister
- Länka till strömmen
+ https://example.com/example.mp4
Radera databasen
Profil bakgrund
WP
@@ -591,5 +588,20 @@
\nKommer att ha en kombinerad videoprioritet på 10.
\n
\nOBS: Om summan är 10 eller mer kommer spelaren automatiskt att hoppa över laddningen när den länken laddas!
- Titel kopierad!
+ Meddelande om nytt avsnitt
+ Sök i andra tillägg
+ Visa rekommendationer
+ Lägger till ett hastighetsalternativ i spelaren
+ Testa alla tillägg
+ Detta test är endast avsett för utvecklare och verifierar eller förnekar inte att någon tillägg fungerar.
+ Lås med biometrik
+ Lösenord/PIN autentisering
+ Lås upp appen med Fingerprint, Face ID, PIN, mönster eller lösenord.
+ Lås upp CloudStream
+ Biometrisk autentisering stöds inte på den här enheten
+ Detta fönster stängs efter några misslyckade försök. Du måste starta om appen.
+ Favorit
+ Ta bort från favoriter
+ %s
+\nkvarstår
diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml
index 234207b9..e981d05a 100644
--- a/app/src/main/res/values-ta/strings.xml
+++ b/app/src/main/res/values-ta/strings.xml
@@ -99,7 +99,6 @@
Logcat 🐈 காட்டு
பிற பயன்பாடுகளுக்கு மேல் மினியேச்சர் பிளேயரில் பிளேபேக் தொடர்கிறது
வசன வரிகள்
- பிளேயரில் வேக விருப்பத்தை சேர்க்க
வீடியோ பிளேயரில் நேரத்தைக் கட்டுப்படுத்த இடது அல்லது வலதுபுறம் ஸ்வைப் செய்யவும்
பிரகாசம் அல்லது ஒலியளவை மாற்ற இடது அல்லது வலது பக்கத்தில் ஸ்வைப் செய்யவும்
இடைநிறுத்துவதற்கு இருமுறை தட்டவும்
diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml
index fc3946bb..b4308eb7 100644
--- a/app/src/main/res/values-tl/strings.xml
+++ b/app/src/main/res/values-tl/strings.xml
@@ -101,7 +101,6 @@
Subtitles
Player subtitles settings
Eigengravy Mode
- Magdagdag ng \'speed option\' sa \'player\'
Swipe to seek
Swipe pakanan o pakaliwa upang makontrol ang oras ng pinapanood
Swipe to change settings
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index e8b7881a..c3e5959a 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -76,7 +76,7 @@
İndirme iptal edildi
İndirme bitti
%s - %s
- Yayınla
+ Ağ akışı
Bağlantılar yüklenirken hata oluştu
Dahili depolama
Dublajlı
@@ -137,8 +137,7 @@
Oynatıcı alt yazı ayarları
Chromecast alt yazıları
Chromecast alt yazı ayarları
- Eigengrau modu
- Oynatıcıya hız seçeneği ekler
+ Oynatma hızı
Atlamak için kaydır
Zamanı ayarlamak için yanlardan kaydır
Ayarları değiştirmek için kaydır
@@ -162,8 +161,8 @@
Depolama izinleri eksik. Lütfen tekrar deneyin.
%s yedeklenirken hata
Ara
- Hesaplar
- Güncellemeler ve yedekleme
+ Hesaplar ve Güvenlik
+ Güncellemeler ve Yedekleme
Bilgi
Gelişmiş arama
Arama sonuçlarını sağlayıcıya göre ayırır
@@ -309,10 +308,10 @@
Genel
Rastgele İçerik
Anasayfada ve Kütüphanede rastgele düğmesini göster
- Sağlayıcı dilleri
+ Uzantı dilleri
Uygulama düzeni
Tercih edilen medya
- Desteklenen sağlayıcılarda +18 içeriği etkinleştir
+ Desteklenen Uzantılarda NSFW\'yi etkinleştirin
Alt yazı kodlaması
Sağlayıcılar
Düzen
@@ -330,11 +329,11 @@
opensubtitles_key
nginx_key
şifre123
- HavalıKullanıcıAdı
+ Kullanıcı Adı
hello@world.com
127.0.0.1
- MyCoolSite
- ornek.com
+ YeniSiteAdı
+ https://ornek.com
Dil kodu (tr)
- ?attr/colorPrimary
- - @android:dimen/dialog_min_width_major
- - @android:dimen/dialog_min_width_minor
+ - @dimen/abc_dialog_min_width_major
+ - @dimen/abc_dialog_min_width_minor
- @drawable/dialog__window_background
diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml
index b0821494..88dc82fe 100644
--- a/app/src/main/res/xml/settings_account.xml
+++ b/app/src/main/res/xml/settings_account.xml
@@ -9,6 +9,9 @@
+
diff --git a/app/src/main/res/xml/settings_general.xml b/app/src/main/res/xml/settings_general.xml
index c4900bca..853bbda1 100644
--- a/app/src/main/res/xml/settings_general.xml
+++ b/app/src/main/res/xml/settings_general.xml
@@ -6,10 +6,7 @@
android:title="@string/app_language"
android:icon="@drawable/ic_baseline_language_24" />
-
+
+ android:title="@string/title_downloads">
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml
index 82505511..5d5b11d0 100644
--- a/app/src/main/res/xml/settings_player.xml
+++ b/app/src/main/res/xml/settings_player.xml
@@ -101,7 +101,8 @@
+ android:title="@string/pref_category_gestures"
+ app:key="@string/pref_category_gestures_key">
+ android:title="@string/pref_category_android_tv"
+ android:key="@string/pref_category_android_tv_key" >
("clean") {
- delete(rootProject.layout.buildDirectory)
-}
+//tasks.register("clean") {
+// delete(rootProject.layout.buildDirectory)
+//}
diff --git a/fastlane/metadata/android/af/short_description.txt b/fastlane/metadata/android/af/short_description.txt
index 59cfb7e5..aaba87f3 100644
--- a/fastlane/metadata/android/af/short_description.txt
+++ b/fastlane/metadata/android/af/short_description.txt
@@ -1 +1 @@
-Laai af of stroom flieks, TV-reekse en anime.
+Laai af en stroom flieks, TV-reekse en anime.
diff --git a/fastlane/metadata/android/hi-IN/full_description.txt b/fastlane/metadata/android/hi-IN/full_description.txt
index 89927810..465db20e 100644
--- a/fastlane/metadata/android/hi-IN/full_description.txt
+++ b/fastlane/metadata/android/hi-IN/full_description.txt
@@ -1,10 +1,10 @@
-क्लाउडस्ट्रीम-3 आपको मूवी, टीवी-सीरीज़ और एनीमे स्ट्रीम और डाउनलोड करने की सुविधा देता है।
+क्लाउडस्ट्रीम-3 आपको फ़िल्में, टीवी शृंखलाएँ और एनिमे स्ट्रीम एवं डाउनलोड करने की सुविधा देता है।
-ऐप बिना किसी विज्ञापन और एनालिटिक्स के आता है और
-कई ट्रेलर और मूवी साइटों और अन्य का समर्थन करता है जैसे कि-
+ऐप विज्ञापन और विश्लेषिकी से मुक्त है एवं
+अनेकों ट्रेलर और मूवी साइटों के समर्थन जैसी सुविधाएँ देता है, जैसे कि –
-बुकमार्क
+पृष्ठचिह्न
उपशीर्षक डाउनलोड
-क्रोमकास्ट का समर्थन
+क्रोमकास्ट समर्थन
diff --git a/fastlane/metadata/android/hi-IN/short_description.txt b/fastlane/metadata/android/hi-IN/short_description.txt
index a8a1eb4d..5a0f2d12 100644
--- a/fastlane/metadata/android/hi-IN/short_description.txt
+++ b/fastlane/metadata/android/hi-IN/short_description.txt
@@ -1 +1 @@
-फिल्में, टीवी-सीरीज़ और एनीमे स्ट्रीम करें और डाउनलोड करें।
+फ़िल्में, टीवी धारावाहिक और एनिमे स्ट्रीम और डाउनलोड करें।
diff --git a/fastlane/metadata/android/hu-HU/changelogs/2.txt b/fastlane/metadata/android/hu-HU/changelogs/2.txt
new file mode 100644
index 00000000..012882d2
--- /dev/null
+++ b/fastlane/metadata/android/hu-HU/changelogs/2.txt
@@ -0,0 +1 @@
+- Változáslista hozzáadva!
diff --git a/fastlane/metadata/android/hu-HU/full_description.txt b/fastlane/metadata/android/hu-HU/full_description.txt
new file mode 100644
index 00000000..24deb97a
--- /dev/null
+++ b/fastlane/metadata/android/hu-HU/full_description.txt
@@ -0,0 +1,10 @@
+A CloudStream-3 segítségével streamelhet vagy letölthet filmeket, TV sorozatokat vagy animéket..
+
+Az app nem tartalmaz semmilyen reklámot vagy követést,
+és támogat többféle film és előzetes oldalt, és sok minden mást, pl.
+
+Könyvjelzőket
+
+Felirat Letöltést
+
+Chromecast támogatást
diff --git a/fastlane/metadata/android/hu-HU/short_description.txt b/fastlane/metadata/android/hu-HU/short_description.txt
new file mode 100644
index 00000000..2e7e6127
--- /dev/null
+++ b/fastlane/metadata/android/hu-HU/short_description.txt
@@ -0,0 +1 @@
+Streameljen vagy töltsön le filmeket, TV sorozatokat vagy animét.
diff --git a/fastlane/metadata/android/hu-HU/title.txt b/fastlane/metadata/android/hu-HU/title.txt
new file mode 100644
index 00000000..dde89d58
--- /dev/null
+++ b/fastlane/metadata/android/hu-HU/title.txt
@@ -0,0 +1 @@
+CloudStream
diff --git a/fastlane/metadata/android/id/changelogs/2.txt b/fastlane/metadata/android/id/changelogs/2.txt
new file mode 100644
index 00000000..677af86c
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/2.txt
@@ -0,0 +1 @@
+- Log perubahan ditambahkan!
diff --git a/fastlane/metadata/android/id/full_description.txt b/fastlane/metadata/android/id/full_description.txt
new file mode 100644
index 00000000..fde69775
--- /dev/null
+++ b/fastlane/metadata/android/id/full_description.txt
@@ -0,0 +1,12 @@
+CloudStream-3 memudahkan Anda streaming dan mengunduh Film, Seri TV, dan Anime.
+
+
+Aplikasi ini hadir tanpa iklan dan pelacak analitik dan
+mendukung beberapa situs trailer & film, dan
+masih banyak lagi, misalnya
+
+Bookmark
+
+Mengunduh subtitle
+
+Dukungan Chromecast
diff --git a/fastlane/metadata/android/id/short_description.txt b/fastlane/metadata/android/id/short_description.txt
new file mode 100644
index 00000000..d7a9ebe4
--- /dev/null
+++ b/fastlane/metadata/android/id/short_description.txt
@@ -0,0 +1 @@
+Stream dan unduh film, seri TV, dan anime.
diff --git a/fastlane/metadata/android/id/title.txt b/fastlane/metadata/android/id/title.txt
new file mode 100644
index 00000000..dde89d58
--- /dev/null
+++ b/fastlane/metadata/android/id/title.txt
@@ -0,0 +1 @@
+CloudStream
diff --git a/fastlane/metadata/android/ko-KR/changelogs/2.txt b/fastlane/metadata/android/ko-KR/changelogs/2.txt
new file mode 100644
index 00000000..f4c05b14
--- /dev/null
+++ b/fastlane/metadata/android/ko-KR/changelogs/2.txt
@@ -0,0 +1 @@
+- 변경기록이 추가됨!
diff --git a/fastlane/metadata/android/ko-KR/full_description.txt b/fastlane/metadata/android/ko-KR/full_description.txt
new file mode 100644
index 00000000..542c1ff7
--- /dev/null
+++ b/fastlane/metadata/android/ko-KR/full_description.txt
@@ -0,0 +1,10 @@
+클라우트스트림-3는 영화, TV-연속극 및 애니메이션 스트리밍을 할 수 있고 내려받을 수 있습니다.
+
+이 앱은 광고나 분석 없이 제공되고
+여러 예고편 & 영화 사이트 등을 지원합니다.
+
+북마크
+
+자막 내려받기
+
+크롬캐스트 지원
diff --git a/fastlane/metadata/android/ko-KR/short_description.txt b/fastlane/metadata/android/ko-KR/short_description.txt
new file mode 100644
index 00000000..e85980b1
--- /dev/null
+++ b/fastlane/metadata/android/ko-KR/short_description.txt
@@ -0,0 +1 @@
+영화, TV 시리즈 및 애니메이션 스트림과 내려받기.
diff --git a/fastlane/metadata/android/ko-KR/title.txt b/fastlane/metadata/android/ko-KR/title.txt
new file mode 100644
index 00000000..a199a665
--- /dev/null
+++ b/fastlane/metadata/android/ko-KR/title.txt
@@ -0,0 +1 @@
+클라우드스티림
diff --git a/fastlane/metadata/android/mk-MK/changelogs/2.txt b/fastlane/metadata/android/mk-MK/changelogs/2.txt
index 949f6579..ee4f49cc 100644
--- a/fastlane/metadata/android/mk-MK/changelogs/2.txt
+++ b/fastlane/metadata/android/mk-MK/changelogs/2.txt
@@ -1 +1 @@
-- Дневник на промени е додаден!
+- Дневникот на промени е додаден!
diff --git a/fastlane/metadata/android/mk-MK/full_description.txt b/fastlane/metadata/android/mk-MK/full_description.txt
index cb06980e..b49c1683 100644
--- a/fastlane/metadata/android/mk-MK/full_description.txt
+++ b/fastlane/metadata/android/mk-MK/full_description.txt
@@ -1,8 +1,8 @@
CloudStream-3 ви дозволува да гледате и превземате филмови, телевизиски серии и аниме. Апликацијата нема реклами и аналитика. Таа поддржува повеќе страници за трејлери, филмови и многу повеќе.
Апликацијата вклучува:
-
+
Обележувачи (Bookmarks)
-
+
Превземање на преводи
-
+
Поддршка за Chromecast
diff --git a/fastlane/metadata/android/ml-IN/changelogs/2.txt b/fastlane/metadata/android/ml-IN/changelogs/2.txt
new file mode 100644
index 00000000..f7523831
--- /dev/null
+++ b/fastlane/metadata/android/ml-IN/changelogs/2.txt
@@ -0,0 +1 @@
+-ചേഞ്ച്ലോഗ് ചേർത്തു!
diff --git a/fastlane/metadata/android/ml-IN/full_description.txt b/fastlane/metadata/android/ml-IN/full_description.txt
new file mode 100644
index 00000000..218f9f98
--- /dev/null
+++ b/fastlane/metadata/android/ml-IN/full_description.txt
@@ -0,0 +1,10 @@
+ക്ലൗഡ് സ്ട്രീം-3 സിനിമകൾ, ടിവി സീരീസ്, ആനിമേഷൻ എന്നിവ സ്ട്രീം ചെയ്യാനും ഡൗൺലോഡ് ചെയ്യാനും നിങ്ങളെ അനുവദിക്കുന്നു.
+
+പരസ്യങ്ങളും അനലിറ്റിക്സും കൂടാതെ ആപ്പ് വരുന്നു ഒപ്പം
+ഒന്നിലധികം ട്രെയിലർ, മൂവി സൈറ്റുകൾ എന്നിവയും മറ്റും പിന്തുണയ്ക്കുന്നു, ഉദാഹരണം
+
+ബുക്ക്മാർക്കുകൾ
+
+ഉപശീർഷകം ഡൗൺലോഡുകൾ
+
+ക്രോംകാസ്റ്റ് പിന്തുണ
diff --git a/fastlane/metadata/android/ml-IN/short_description.txt b/fastlane/metadata/android/ml-IN/short_description.txt
new file mode 100644
index 00000000..f12fe5b5
--- /dev/null
+++ b/fastlane/metadata/android/ml-IN/short_description.txt
@@ -0,0 +1 @@
+സ്ട്രീം ഒപ്പം ഡൗൺലോഡ് സിനിമകളും, ടിവി സീരീസുകളും, ആനിമേഷനും .
diff --git a/fastlane/metadata/android/ml-IN/title.txt b/fastlane/metadata/android/ml-IN/title.txt
new file mode 100644
index 00000000..8e89348a
--- /dev/null
+++ b/fastlane/metadata/android/ml-IN/title.txt
@@ -0,0 +1 @@
+ക്ലൗഡ് സ്ട്രീം
diff --git a/fastlane/metadata/android/mt/changelogs/2.txt b/fastlane/metadata/android/mt/changelogs/2.txt
new file mode 100644
index 00000000..66bbca8f
--- /dev/null
+++ b/fastlane/metadata/android/mt/changelogs/2.txt
@@ -0,0 +1 @@
+- Changelog miżjud!
diff --git a/fastlane/metadata/android/mt/full_description.txt b/fastlane/metadata/android/mt/full_description.txt
new file mode 100644
index 00000000..da507aae
--- /dev/null
+++ b/fastlane/metadata/android/mt/full_description.txt
@@ -0,0 +1,10 @@
+CloudStream-3 iħallik tistrimja u tniżżel Films, Serje TV u Anime.
+
+L-app tiġi mingħajr reklami u analytics u
+jappoġġja siti multipli ta' trejlers u films, u aktar, eż.
+
+Bookmarks
+
+Downloads tas-sottotitli
+
+Appoġġ tal-Chromecast
diff --git a/fastlane/metadata/android/mt/short_description.txt b/fastlane/metadata/android/mt/short_description.txt
new file mode 100644
index 00000000..542b8614
--- /dev/null
+++ b/fastlane/metadata/android/mt/short_description.txt
@@ -0,0 +1 @@
+Tistrimja u tniżżel films, serje tat-TV u Anime.
diff --git a/fastlane/metadata/android/mt/title.txt b/fastlane/metadata/android/mt/title.txt
new file mode 100644
index 00000000..dde89d58
--- /dev/null
+++ b/fastlane/metadata/android/mt/title.txt
@@ -0,0 +1 @@
+CloudStream
diff --git a/fastlane/metadata/android/pl-PL/changelogs/2.txt b/fastlane/metadata/android/pl-PL/changelogs/2.txt
new file mode 100644
index 00000000..e558535d
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/changelogs/2.txt
@@ -0,0 +1 @@
+- Dodano dziennik zmian!
diff --git a/fastlane/metadata/android/pl-PL/full_description.txt b/fastlane/metadata/android/pl-PL/full_description.txt
new file mode 100644
index 00000000..11f71ff7
--- /dev/null
+++ b/fastlane/metadata/android/pl-PL/full_description.txt
@@ -0,0 +1,10 @@
+CloudStream-3 umożliwia strumieniowe przesyłanie i pobieranie filmów, seriali telewizyjnych i anime.
+
+Aplikacja jest dostarczana bez reklam i analityki, obsługuje
+wiele witryn ze zwiastunami, filmami i nie tylko, np.
+
+Zakładki
+
+Pobieranie napisów
+
+Obsługa Chromecasta
diff --git a/fastlane/metadata/android/pt-BR/changelogs/2.txt b/fastlane/metadata/android/pt-BR/changelogs/2.txt
new file mode 100644
index 00000000..c094fe97
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/2.txt
@@ -0,0 +1 @@
+- Histórico de mudanças adicionado!
diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt
new file mode 100644
index 00000000..1406838e
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/full_description.txt
@@ -0,0 +1,10 @@
+O CloudStream-3 permite que você faça transmissões, download de filmes, séries, e anime.
+
+O aplicativo não contém anúncios ou ferramentas de análise,
+e suporta múltiplos sites de filmes e trailers, e muito mais, como:
+
+Favoritos
+
+Download de legendas
+
+Suporte à Chromecast
diff --git a/fastlane/metadata/android/pt-BR/short_description.txt b/fastlane/metadata/android/pt-BR/short_description.txt
new file mode 100644
index 00000000..46635de9
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/short_description.txt
@@ -0,0 +1 @@
+Faça transmissões, download de filmes, séries, e anime.
diff --git a/fastlane/metadata/android/pt-BR/title.txt b/fastlane/metadata/android/pt-BR/title.txt
new file mode 100644
index 00000000..dde89d58
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/title.txt
@@ -0,0 +1 @@
+CloudStream
diff --git a/fastlane/metadata/android/ru-RU/changelogs/2.txt b/fastlane/metadata/android/ru-RU/changelogs/2.txt
new file mode 100644
index 00000000..4b9464b6
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/changelogs/2.txt
@@ -0,0 +1 @@
+- Добавлен список изменений!
diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt
new file mode 100644
index 00000000..1790888e
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/full_description.txt
@@ -0,0 +1,10 @@
+CloudStream-3 позволяет транслировать и скачивать фильмы, сериалы и аниме.
+
+Приложение поставляется без рекламы и аналитики и
+поддерживает множество сайтов с трейлерами и фильмами, а также многое другое, например
+
+Книжные закладки
+
+Загрузка субтитров
+
+Поддержка Chromecast
diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt
new file mode 100644
index 00000000..a43bc8a1
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/short_description.txt
@@ -0,0 +1 @@
+Транслируйте и скачивайте фильмы, сериалы и аниме.
diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt
new file mode 100644
index 00000000..3c0406a6
--- /dev/null
+++ b/fastlane/metadata/android/ru-RU/title.txt
@@ -0,0 +1 @@
+Облачный поток
diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt
index dde89d58..0afff90c 100644
--- a/fastlane/metadata/android/vi/title.txt
+++ b/fastlane/metadata/android/vi/title.txt
@@ -1 +1 @@
-CloudStream
+double_tap_seek_time_key2
diff --git a/fastlane/metadata/android/zh-CN/changelogs/2.txt b/fastlane/metadata/android/zh-CN/changelogs/2.txt
index 5512f16c..c8c4624d 100644
--- a/fastlane/metadata/android/zh-CN/changelogs/2.txt
+++ b/fastlane/metadata/android/zh-CN/changelogs/2.txt
@@ -1 +1 @@
-- 添加了更新日志!
+- 新增更新日志!
diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt
index 56519df6..b2dcf1de 100644
--- a/fastlane/metadata/android/zh-CN/full_description.txt
+++ b/fastlane/metadata/android/zh-CN/full_description.txt
@@ -1,9 +1,10 @@
-CloudStream-3可以让你串流和下载电影、剧集和动漫。这款应用没有任何广告和分析。它支持多个预告片和电影网站等。特点包括:
+CloudStream-3可以让你串流和下载电影、剧集和动漫。
+
+这款应用没有任何广告和隐私分析并且
+它支持多个预告片和电影网站等。特点包括:
书签
-下载和串流电影、电视节目和动漫
-
下载字幕
支持投屏
diff --git a/library/build.gradle.kts b/library/build.gradle.kts
new file mode 100644
index 00000000..42a8c943
--- /dev/null
+++ b/library/build.gradle.kts
@@ -0,0 +1,68 @@
+import com.codingfeline.buildkonfig.compiler.FieldSpec
+
+plugins {
+ kotlin("multiplatform")
+ id("maven-publish")
+ id("com.android.library")
+ id("com.codingfeline.buildkonfig")
+}
+
+kotlin {
+ version = "1.0.0"
+ androidTarget()
+ jvm()
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser
+ ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API
+ Level 25 or Less. */
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
+ }
+ }
+}
+
+repositories {
+ mavenLocal()
+ maven("https://jitpack.io")
+}
+
+tasks.withType {
+ kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
+}
+
+buildkonfig {
+ packageName = "com.lagradost.api"
+ exposeObjectWithName = "BuildConfig"
+
+ defaultConfigs {
+ val isDebug = kotlin.runCatching { extra.get("isDebug") }.getOrNull() == true
+ buildConfigField(FieldSpec.Type.BOOLEAN, "DEBUG", isDebug.toString())
+ }
+}
+
+android {
+ compileSdk = 34
+ sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
+
+ defaultConfig {
+ minSdk = 21
+ targetSdk = 33
+ }
+
+ // If this is the same com.lagradost.cloudstream3.R stops working
+ namespace = "com.lagradost.api"
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+}
+publishing {
+ publications {
+ withType {
+ groupId = "com.lagradost.api"
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/src/androidMain/AndroidManifest.xml b/library/src/androidMain/AndroidManifest.xml
new file mode 100644
index 00000000..568741e5
--- /dev/null
+++ b/library/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/library/src/androidMain/kotlin/com/lagradost/api/Log.kt b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt
new file mode 100644
index 00000000..12524411
--- /dev/null
+++ b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt
@@ -0,0 +1,21 @@
+package com.lagradost.api
+
+import android.util.Log
+
+actual object Log {
+ actual fun d(tag: String, message: String) {
+ Log.d(tag, message)
+ }
+
+ actual fun i(tag: String, message: String) {
+ Log.i(tag, message)
+ }
+
+ actual fun w(tag: String, message: String) {
+ Log.w(tag, message)
+ }
+
+ actual fun e(tag: String, message: String) {
+ Log.e(tag, message)
+ }
+}
\ No newline at end of file
diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt
new file mode 100644
index 00000000..48a709eb
--- /dev/null
+++ b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt
@@ -0,0 +1,11 @@
+package com.lagradost.cloudstream3.utils
+
+import android.os.Handler
+import android.os.Looper
+
+actual fun runOnMainThreadNative(work: () -> Unit) {
+ val mainHandler = Handler(Looper.getMainLooper())
+ mainHandler.post {
+ work()
+ }
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/api/Log.kt b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt
new file mode 100644
index 00000000..4b8e6329
--- /dev/null
+++ b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt
@@ -0,0 +1,8 @@
+package com.lagradost.api
+
+expect object Log {
+ fun d(tag: String, message: String)
+ fun i(tag: String, message: String)
+ fun w(tag: String, message: String)
+ fun e(tag: String, message: String)
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt
new file mode 100644
index 00000000..6502cc83
--- /dev/null
+++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt
@@ -0,0 +1,35 @@
+package com.lagradost.cloudstream3
+
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.lagradost.nicehttp.Requests
+import com.lagradost.nicehttp.ResponseParser
+import kotlin.reflect.KClass
+
+// 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 parse(text: String, kClass: KClass): T {
+ return mapper.readValue(text, kClass.java)
+ }
+
+ override fun parseSafe(text: String, kClass: KClass): 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)
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt
new file mode 100644
index 00000000..160ff098
--- /dev/null
+++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt
@@ -0,0 +1,6 @@
+package com.lagradost.cloudstream3
+
+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"
+
+class ErrorLoadingException(message: String? = null) : Exception(message)
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt
similarity index 100%
rename from app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt
rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt
diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
similarity index 86%
rename from app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
index 817d7db3..d3b4999a 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
+++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
@@ -1,10 +1,7 @@
package com.lagradost.cloudstream3.mvvm
-import android.util.Log
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.LiveData
-import com.bumptech.glide.load.HttpException
-import com.lagradost.cloudstream3.BuildConfig
+import com.lagradost.api.BuildConfig
+import com.lagradost.api.Log
import com.lagradost.cloudstream3.ErrorLoadingException
import kotlinx.coroutines.*
import java.io.InterruptedIOException
@@ -49,18 +46,6 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) {
}
}
-/** NOTE: Only one observer at a time per value */
-fun LifecycleOwner.observe(liveData: LiveData, 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 LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) {
- liveData.removeObservers(this)
- liveData.observe(this) { action(it) }
-}
-
sealed class Resource {
data class Success(val value: T) : Resource()
data class Failure(
@@ -158,14 +143,14 @@ fun throwAbleToResource(
"Connection Timeout\nPlease try again later."
)
}
- is HttpException -> {
- Resource.Failure(
- false,
- throwable.statusCode,
- null,
- throwable.message ?: "HttpException"
- )
- }
+// is HttpException -> {
+// Resource.Failure(
+// false,
+// throwable.statusCode,
+// null,
+// throwable.message ?: "HttpException"
+// )
+// }
is UnknownHostException -> {
Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}")
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt
similarity index 90%
rename from app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt
rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt
index c3b244c2..f87ddc6a 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt
+++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt
@@ -1,12 +1,11 @@
package com.lagradost.cloudstream3.utils
-import android.os.Handler
-import android.os.Looper
import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.mvvm.logError
import kotlinx.coroutines.*
import java.util.Collections.synchronizedList
+expect fun runOnMainThreadNative(work: (() -> Unit))
object Coroutines {
fun T.main(work: suspend ((T) -> Unit)): Job {
val value = this
@@ -50,10 +49,7 @@ object Coroutines {
}
fun runOnMainThread(work: (() -> Unit)) {
- val mainHandler = Handler(Looper.getMainLooper())
- mainHandler.post {
- work()
- }
+ runOnMainThreadNative(work)
}
/**
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsHunter.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt
similarity index 100%
rename from app/src/main/java/com/lagradost/cloudstream3/utils/JsHunter.kt
rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt
similarity index 99%
rename from app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt
rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt
index 153dbd3e..d9f0b382 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt
+++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt
@@ -50,7 +50,7 @@ class JsUnpacker(packedJS: String?) {
throw Exception("Unknown p.a.c.k.e.r. encoding")
}
val unbase = Unbase(radix)
- p = Pattern.compile("\\b\\w+\\b")
+ p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""")
m = p.matcher(payload)
val decoded = StringBuilder(payload)
var replaceOffset = 0
diff --git a/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt
new file mode 100644
index 00000000..e9a0e6b4
--- /dev/null
+++ b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt
@@ -0,0 +1,19 @@
+package com.lagradost.api
+
+actual object Log {
+ actual fun d(tag: String, message: String) {
+ println("DEBUG $tag: $message")
+ }
+
+ actual fun i(tag: String, message: String) {
+ println("INFO $tag: $message")
+ }
+
+ actual fun w(tag: String, message: String) {
+ println("WARNING $tag: $message")
+ }
+
+ actual fun e(tag: String, message: String) {
+ println("ERROR $tag: $message")
+ }
+}
\ No newline at end of file
diff --git a/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt
new file mode 100644
index 00000000..0a9667cb
--- /dev/null
+++ b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt
@@ -0,0 +1,5 @@
+package com.lagradost.cloudstream3.utils
+
+actual fun runOnMainThreadNative(work: () -> Unit) {
+ work.invoke()
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 17070047..eabd9f0e 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,3 +1,4 @@
rootProject.name = "CloudStream"
-include(":app")
\ No newline at end of file
+include(":app")
+include(":library")
\ No newline at end of file