Merge branch 'master' into feature/remote-sync

This commit is contained in:
CranberrySoup 2023-11-17 18:29:00 +00:00 committed by GitHub
commit 744d1ebd21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 1583 additions and 1045 deletions

View file

@ -43,7 +43,8 @@ jobs:
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
- name: Run Gradle
run: |
./gradlew assemblePrerelease makeJar androidSourcesJar
./gradlew assemblePrerelease build androidSourcesJar
./gradlew makeJar # for classes.jar, has to be done after assemblePrerelease
env:
SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}

1
.idea/gradle.xml generated
View file

@ -8,6 +8,7 @@
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

View file

@ -1,5 +1,6 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.ByteArrayOutputStream
import java.net.URL
import java.util.Properties
@ -7,8 +8,8 @@ import java.io.FileInputStream
plugins {
id("com.android.application")
id("com.google.devtools.ksp")
id("kotlin-android")
id("kotlin-kapt")
id("org.jetbrains.dokka")
}
@ -43,12 +44,12 @@ android {
enable = true
}
// disable this for now
//externalNativeBuild {
// cmake {
// path("CMakeLists.txt")
// }
//}
/* disable this for now
externalNativeBuild {
cmake {
path("CMakeLists.txt")
}
}*/
signingConfigs {
create("prerelease") {
@ -67,10 +68,8 @@ android {
defaultConfig {
applicationId = "com.lagradost.cloudstream3"
minSdk = 21
// https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading
targetSdk = 33 // android 14 is fucked
targetSdk = 33 /* Android 14 is Fu*ked
^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
versionCode = 62
versionName = "4.2.1"
@ -98,8 +97,9 @@ android {
)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
kapt {
includeCompileClasspath = true
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("exportSchema", "true")
}
}
@ -132,6 +132,7 @@ android {
)
}
}
flavorDimensions.add("state")
productFlavors {
create("stable") {
@ -148,25 +149,18 @@ android {
versionCode = (System.currentTimeMillis() / 60000).toInt()
}
}
//toolchain {
// languageVersion.set(JavaLanguageVersion.of(17))
// }
// jvmToolchain(17)
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf("-Xjvm-default=compatibility")
}
lint {
abortOnError = false
checkReleaseBuilds = false
}
namespace = "com.lagradost.cloudstream3"
}
@ -175,111 +169,80 @@ repositories {
}
dependencies {
implementation("com.google.android.mediahome:video:1.0.0")
implementation("androidx.test.ext:junit-ktx:1.1.5")
testImplementation("org.json:json:20230618")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.4")
implementation("androidx.navigation:navigation-ui-ktx:2.7.4")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
// Testing
testImplementation("junit:junit:4.13.2")
testImplementation("org.json:json:20230618")
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")
androidTestImplementation("androidx.test:core")
// implementation("io.karn:khttp-android:0.1.2") //okhttp instead
// implementation("org.jsoup:jsoup:1.13.1")
// DONT UPDATE, WILL CRASH ANDROID TV ????
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("com.github.bumptech.glide:glide:4.13.1")
kapt("com.github.bumptech.glide:compiler:4.13.1")
implementation("com.github.bumptech.glide:okhttp3-integration:4.13.0")
// Android Core & Lifecycle
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.navigation:navigation-ui-ktx:2.7.5")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.5")
// 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("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
// implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
// Glide Module
ksp("com.github.bumptech.glide:ksp:4.15.1")
implementation("com.github.bumptech.glide:glide:4.15.1")
implementation("com.github.bumptech.glide:okhttp3-integration:4.15.1")
// Media 3
implementation("androidx.media3:media3-common:1.1.1")
implementation("androidx.media3:media3-exoplayer:1.1.1")
implementation("androidx.media3:media3-datasource-okhttp:1.1.1")
// 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.2-android")
implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
// Media 3 (ExoPlayer)
implementation("androidx.media3:media3-ui:1.1.1")
implementation("androidx.media3:media3-session:1.1.1")
implementation("androidx.media3:media3-cast:1.1.1")
implementation("androidx.media3:media3-common:1.1.1")
implementation("androidx.media3:media3-session:1.1.1")
implementation("androidx.media3:media3-exoplayer:1.1.1")
implementation("com.google.android.mediahome:video:1.0.0")
implementation("androidx.media3:media3-exoplayer-hls:1.1.1")
implementation("androidx.media3:media3-exoplayer-dash:1.1.1")
// Custom ffmpeg extension for audio codecs
implementation("com.github.recloudstream:media-ffmpeg:1.1.0")
implementation("androidx.media3:media3-datasource-okhttp:1.1.1")
// Bug reports
// 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:eac850") /* 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
// Crash Reports (AcraApplication.kt)
implementation("ch.acra:acra-core:5.11.2")
implementation("ch.acra:acra-toast:5.11.2")
compileOnly("com.google.auto.service:auto-service-annotations:1.1.1")
//either for java sources:
annotationProcessor("com.google.auto.service:auto-service:1.1.1")
//or for kotlin sources (requires kapt gradle plugin):
kapt("com.google.auto.service:auto-service:1.1.1")
// subtitle color picker
implementation("com.jaredrummler:colorpicker:1.1.0")
// run JS
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
implementation("org.mozilla:rhino:1.7.13")
// TorrentStream
// implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
// Downloading
implementation("androidx.work:work-runtime:2.8.1")
implementation("androidx.work:work-runtime-ktx:2.8.1")
// Networking
// implementation("com.squareup.okhttp3:okhttp:4.9.2")
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
implementation("com.github.Blatzar:NiceHttp:0.4.4") // http library
// To fix SSL fuckery on android 9
implementation("org.conscrypt:conscrypt-android:2.5.2")
// Util to skip the URI file fuckery 🙏
implementation("com.github.LagradOst:SafeFile:0.0.5")
// API because cba maintaining it myself
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0")
implementation("com.github.discord:OverlappingPanels:0.1.5")
// debugImplementation because LeakCanary should only run in debug builds.
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
// for shimmer when loading
implementation("com.facebook.shimmer:shimmer:0.5.0")
// UI Stuff
implementation("com.facebook.shimmer:shimmer:0.5.0") // Shimmering Effect (Loading Skeleton)
implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors
implementation("androidx.tvprovider:tvprovider:1.0.0")
implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
implementation("com.github.albfernandez:juniversalchardet:2.4.0")
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev
// this should be updated frequently to avoid trailer fu*kery
implementation("com.github.teamnewpipe:NewPipeExtractor:917554a")
// Extensionns & Other Libs
implementation("org.mozilla:rhino:1.7.13") /* run JS
^ Don't Bump RhinoJS to 1.7.14, since in 1.7.14 Rhino Uses the `SourceVersion` Class, Which is NOT
Available on Android (even with Desugaring) & `NoClassDefFoundError` Occurs. */
implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance
implementation("com.github.LagradOst:SafeFile:0.0.5") // To Prevent the URI File Fu*kery
implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0") // TMDB API v3 Wrapper Made with RetroFit
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
// Library/extensions searching with Levenshtein distance
implementation("me.xdrop:fuzzywuzzy:1.4.0")
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. */
// color palette for images -> colors
implementation("androidx.palette:palette-ktx:1.0.0")
@ -300,22 +263,31 @@ dependencies {
group = "org.apache.httpcomponents",
)
}
// seekbar https://github.com/rubensousa/PreviewSeekBar
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0")
// Downloading & Networking
implementation("androidx.work:work-runtime:2.8.1")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("com.github.Blatzar:NiceHttp:0.4.4") // HTTP Lib
}
tasks.register("androidSourcesJar", Jar::class) {
archiveClassifier.set("sources")
from(android.sourceSets.getByName("main").java.srcDirs) //full sources
from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources
}
// this is used by the gradlew plugin
// For GradLew Plugin
tasks.register("makeJar", Copy::class) {
from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
into("build")
include("classes.jar")
dependsOn("build")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
}
}
tasks.withType<DokkaTask>().configureEach {
@ -328,6 +300,7 @@ tasks.withType<DokkaTask>().configureEach {
// URL showing where the source code can be accessed through the web browser
remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
// Suffix which is used to append the line number to the URL. Use #L for GitHub
remoteLineSuffix.set("#L")
}

View file

@ -6,7 +6,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Downloads -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Downloads on low api devices -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <!-- Plugin API -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <!-- Plugin API -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
@ -17,7 +17,11 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!-- Required for getting arbitrary Aniyomi packages -->
<!-- Required for getting arbitrary Aniyomi packages -->
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<!-- Fixes android tv fuckery -->
<uses-feature
android:name="android.hardware.touchscreen"
@ -37,9 +41,11 @@
<application
android:name=".AcraApplication"
android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:appCategory="video"
android:banner="@mipmap/ic_banner"
android:fullBackupContent="@xml/backup_descriptor"
android:dataExtractionRules="@xml/data_extraction_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
@ -47,7 +53,7 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:targetApi="o">
tools:targetApi="tiramisu">
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
@ -161,9 +167,8 @@
<activity
android:name=".ui.account.AccountSelectActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
android:exported="true"
android:resizeableActivity="true">
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
android:exported="true">
<intent-filter android:exported="true">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -182,8 +187,8 @@
<receiver
android:name=".receivers.VideoDownloadRestartReceiver"
android:enabled="false"
android:exported="true">
<intent-filter android:exported="true">
android:exported="false">
<intent-filter android:exported="false">
<action android:name="restart_service" />
</intent-filter>
</receiver>

View file

@ -8,7 +8,6 @@ import android.content.Intent
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.google.auto.service.AutoService
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager
@ -37,7 +36,6 @@ import java.lang.ref.WeakReference
import kotlin.concurrent.thread
import kotlin.system.exitProcess
class CustomReportSender : ReportSender {
// Sends all your crashes to google forms
override fun send(context: Context, errorContent: CrashReportData) {
@ -65,7 +63,6 @@ class CustomReportSender : ReportSender {
}
}
@AutoService(ReportSenderFactory::class)
class CustomSenderFactory : ReportSenderFactory {
override fun create(context: Context, config: CoreConfiguration): ReportSender {
return CustomReportSender()

View file

@ -19,6 +19,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes
import androidx.annotation.MainThread
@ -133,7 +134,6 @@ import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.IOnBackPressed
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
@ -311,9 +311,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// kinda shitty solution, but cant com main->home otherwise for popups
val bookmarksUpdatedEvent = Event<Boolean>()
/**
* Used by data store helper to fully reload home when switching accounts
* Used by DataStoreHelper to fully reload home when switching accounts
*/
val reloadHomeEvent = Event<Boolean>()
/**
* Used by DataStoreHelper to fully reload library when switching accounts
*/
val reloadLibraryEvent = Event<Boolean>()
/**
@ -652,34 +656,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
builder.show().setDefaultFocus()
}
private fun backPressed() {
this.window?.navigationBarColor =
this.colorFromAttribute(R.attr.primaryGrayBackground)
this.updateLocale()
this.updateLocale()
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
val navController = navHostFragment?.navController
val isAtHome =
navController?.currentDestination?.matchDestination(R.id.navigation_home) == true
if (isAtHome && isTvSettings()) {
showConfirmExitDialog()
} else {
super.onBackPressed()
}
}
override fun onBackPressed() {
((supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?)?.childFragmentManager?.primaryNavigationFragment as? IOnBackPressed)?.onBackPressed()
?.let { runNormal ->
if (runNormal) backPressed()
} ?: run {
backPressed()
}
}
override fun onDestroy() {
val broadcastIntent = Intent()
broadcastIntent.action = "restart_service"
@ -1091,6 +1067,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} catch (_: Throwable) {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
@ -1388,6 +1365,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery)
}
}
if (isTvSettings()) {
if (navDestination.matchDestination(R.id.navigation_home)) {
attachBackPressedCallback()
} else detachBackPressedCallback()
}
}
//val navController = findNavController(R.id.nav_host_fragment)
@ -1601,6 +1584,45 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// showToast(this, currentFocus.toString(), Toast.LENGTH_LONG)
// }
// }
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
window?.navigationBarColor = colorFromAttribute(R.attr.primaryGrayBackground)
updateLocale()
// If we don't disable we end up in a loop with default behavior calling
// this callback as well, so we disable it, run default behavior,
// then re-enable this callback so it can be used for next back press.
isEnabled = false
onBackPressedDispatcher.onBackPressed()
isEnabled = true
}
}
)
}
private var backPressedCallback: OnBackPressedCallback? = null
private fun attachBackPressedCallback() {
if (backPressedCallback == null) {
backPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
showConfirmExitDialog()
window?.navigationBarColor =
colorFromAttribute(R.attr.primaryGrayBackground)
updateLocale()
}
}
}
backPressedCallback?.isEnabled = true
onBackPressedDispatcher.addCallback(this, backPressedCallback ?: return)
}
private fun detachBackPressedCallback() {
backPressedCallback?.isEnabled = false
}
suspend fun checkGithubConnectivity(): Boolean {

View file

@ -16,13 +16,13 @@ import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
// No License found in https://github.com/enimax-anime/key
// special credits to @enimax for providing key
// Code found in https://github.com/theonlymo/keys
// special credits to @theonlymo for providing key
class Megacloud : Rabbitstream() {
override val name = "Megacloud"
override val mainUrl = "https://megacloud.tv"
override val embed = "embed-2/ajax/e-1"
override val key = "https://raw.githubusercontent.com/enimax-anime/key/e6/key.txt"
override val key = "https://raw.githubusercontent.com/theonlymo/keys/e1/key"
}
class Dokicloud : Rabbitstream() {
@ -35,7 +35,7 @@ open class Rabbitstream : ExtractorApi() {
override val mainUrl = "https://rabbitstream.net"
override val requiresReferer = false
open val embed = "ajax/embed-4"
open val key = "https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt"
open val key = "https://raw.githubusercontent.com/theonlymo/keys/e4/key"
override suspend fun getUrl(
url: String,
@ -86,21 +86,23 @@ open class Rabbitstream : ExtractorApi() {
private suspend fun getRawKey(): String = app.get(key).text
private fun extractRealKey(originalString: String?, stops: String): Pair<String, String> {
val table = parseJson<List<List<Int>>>(stops)
val decryptedKey = StringBuilder()
var offset = 0
var encryptedString = originalString
private fun extractRealKey(sources: String, stops: String): Pair<String, String> {
val decryptKey = parseJson<List<List<Int>>>(stops)
val sourcesArray = sources.toCharArray()
table.forEach { (start, end) ->
decryptedKey.append(encryptedString?.substring(start - offset, end - offset))
encryptedString = encryptedString?.substring(
0,
start - offset
) + encryptedString?.substring(end - offset)
offset += end - start
var extractedKey = ""
var currentIndex = 0
for (index in decryptKey) {
val start = index[0] + currentIndex
val end = start + index[1]
for (i in start until end) {
extractedKey += sourcesArray[i].toString()
sourcesArray[i] = ' '
}
currentIndex += index[1]
}
return decryptedKey.toString() to encryptedString.toString()
return extractedKey to sourcesArray.joinToString("")
}
private inline fun <reified T> decryptMapped(input: String, key: String): T? {

View file

@ -1,110 +0,0 @@
package com.lagradost.cloudstream3.ui
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.databinding.WhoIsWatchingAccountAddBinding
import com.lagradost.cloudstream3.databinding.WhoIsWatchingAccountBinding
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.utils.DataStoreHelper
class WhoIsWatchingAdapter(
private val selectCallBack: (DataStoreHelper.Account) -> Unit = { },
private val editCallBack: (DataStoreHelper.Account) -> Unit = { },
private val addAccountCallback: () -> Unit = {}
) :
ListAdapter<DataStoreHelper.Account, WhoIsWatchingAdapter.WhoIsWatchingHolder>(DiffCallback()) {
companion object {
const val FOOTER = 1
const val NORMAL = 0
}
override fun getItemCount(): Int {
return currentList.size + 1
}
override fun getItemViewType(position: Int): Int = when (position) {
currentList.size -> FOOTER
else -> NORMAL
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WhoIsWatchingHolder =
WhoIsWatchingHolder(
binding = when (viewType) {
NORMAL -> WhoIsWatchingAccountBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
FOOTER -> WhoIsWatchingAccountAddBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
else -> throw NotImplementedError()
},
selectCallBack = selectCallBack,
addAccountCallback = addAccountCallback,
editCallBack = editCallBack,
)
override fun onBindViewHolder(holder: WhoIsWatchingHolder, position: Int) =
holder.bind(currentList.getOrNull(position))
class WhoIsWatchingHolder(
val binding: ViewBinding,
val selectCallBack: (DataStoreHelper.Account) -> Unit,
val addAccountCallback: () -> Unit,
val editCallBack: (DataStoreHelper.Account) -> Unit
) :
RecyclerView.ViewHolder(binding.root) {
fun bind(card: DataStoreHelper.Account?) {
when (binding) {
is WhoIsWatchingAccountBinding -> binding.apply {
if (card == null) return@apply
outline.isVisible = card.keyIndex == DataStoreHelper.selectedKeyIndex
profileText.text = card.name
profileImageBackground.setImage(card.image)
// Handle the lock indicator
val isLocked = card.lockPin != null
lockIcon.isVisible = isLocked
root.setOnClickListener {
selectCallBack(card)
}
root.setOnLongClickListener {
editCallBack(card)
return@setOnLongClickListener true
}
}
is WhoIsWatchingAccountAddBinding -> binding.apply {
root.setOnClickListener {
addAccountCallback()
}
}
}
}
}
class DiffCallback : DiffUtil.ItemCallback<DataStoreHelper.Account>() {
override fun areItemsTheSame(
oldItem: DataStoreHelper.Account,
newItem: DataStoreHelper.Account
): Boolean = oldItem.keyIndex == newItem.keyIndex
override fun areContentsTheSame(
oldItem: DataStoreHelper.Account,
newItem: DataStoreHelper.Account
): Boolean = oldItem == newItem
}
}

View file

@ -2,63 +2,197 @@ package com.lagradost.cloudstream3.ui.account
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.AccountListItemAddBinding
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.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.UIHelper.setImage
class AccountAdapter(
private val accounts: List<DataStoreHelper.Account>,
private val onItemClick: (DataStoreHelper.Account) -> Unit
private val accountSelectCallback: (DataStoreHelper.Account) -> Unit,
private val accountCreateCallback: (DataStoreHelper.Account) -> Unit,
private val accountEditCallback: (DataStoreHelper.Account) -> Unit,
private val accountDeleteCallback: (DataStoreHelper.Account) -> Unit
) : RecyclerView.Adapter<AccountAdapter.AccountViewHolder>() {
inner class AccountViewHolder(private val binding: AccountListItemBinding) :
companion object {
const val VIEW_TYPE_SELECT_ACCOUNT = 0
const val VIEW_TYPE_ADD_ACCOUNT = 1
const val VIEW_TYPE_EDIT_ACCOUNT = 2
}
inner class AccountViewHolder(private val binding: ViewBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(account: DataStoreHelper.Account) {
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
fun bind(account: DataStoreHelper.Account?) {
when (binding) {
is AccountListItemBinding -> binding.apply {
if (account == null) return@apply
binding.accountName.text = account.name
binding.accountImage.setImage(account.image)
binding.lockIcon.isVisible = account.lockPin != null
binding.outline.isVisible = isLastUsedAccount
val isTv = isTvSettings() || !root.isInTouchMode
if (isTvSettings()) {
binding.root.isFocusableInTouchMode = true
if (isLastUsedAccount) {
binding.root.requestFocus()
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
accountName.text = account.name
accountImage.setImage(account.image)
lockIcon.isVisible = account.lockPin != null
outline.isVisible = !isTv && isLastUsedAccount
if (isTv) {
// For emulator but this is fine on TV also
root.isFocusableInTouchMode = true
if (isLastUsedAccount) {
root.requestFocus()
}
root.foreground = ContextCompat.getDrawable(
root.context,
R.drawable.outline_drawable
)
} else {
root.setOnLongClickListener {
showAccountEditDialog(
context = root.context,
account = account,
isNewAccount = false,
accountEditCallback = { account -> accountEditCallback.invoke(account) },
accountDeleteCallback = { account -> accountDeleteCallback.invoke(account) }
)
true
}
}
root.setOnClickListener {
accountSelectCallback.invoke(account)
}
}
is AccountListItemEditBinding -> binding.apply {
if (account == null) return@apply
val isTv = isTvSettings() || !root.isInTouchMode
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
accountName.text = account.name
accountImage.setImage(
account.image,
fadeIn = false,
radius = 10
)
lockIcon.isVisible = account.lockPin != null
outline.isVisible = !isTv && isLastUsedAccount
if (isTv) {
// For emulator but this is fine on TV also
root.isFocusableInTouchMode = true
if (isLastUsedAccount) {
root.requestFocus()
}
root.foreground = ContextCompat.getDrawable(
root.context,
R.drawable.outline_drawable
)
}
root.setOnClickListener {
showAccountEditDialog(
context = root.context,
account = account,
isNewAccount = false,
accountEditCallback = { account -> accountEditCallback.invoke(account) },
accountDeleteCallback = { account -> accountDeleteCallback.invoke(account) }
)
}
}
is AccountListItemAddBinding -> binding.apply {
root.setOnClickListener {
val remainingImages =
DataStoreHelper.profileImages.toSet() - accounts.filter { it.customImage == null }
.mapNotNull { DataStoreHelper.profileImages.getOrNull(it.defaultImageIndex) }.toSet()
val image =
DataStoreHelper.profileImages.indexOf(remainingImages.randomOrNull() ?: DataStoreHelper.profileImages.random())
val keyIndex = (accounts.maxOfOrNull { it.keyIndex } ?: 0) + 1
val accountName = root.context.getString(R.string.account)
showAccountEditDialog(
root.context,
DataStoreHelper.Account(
keyIndex = keyIndex,
name = "$accountName $keyIndex",
customImage = null,
defaultImageIndex = image
),
isNewAccount = true,
accountEditCallback = { account -> accountCreateCallback.invoke(account) },
accountDeleteCallback = {}
)
}
}
}
binding.root.setOnClickListener {
onItemClick(account)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder {
val binding = AccountListItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder =
AccountViewHolder(
binding = when (viewType) {
VIEW_TYPE_SELECT_ACCOUNT -> {
AccountListItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
}
VIEW_TYPE_ADD_ACCOUNT -> {
AccountListItemAddBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
}
VIEW_TYPE_EDIT_ACCOUNT -> {
AccountListItemEditBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
}
else -> throw IllegalArgumentException("Invalid view type")
}
)
if (isTvSettings()) {
val layoutParams = binding.root.layoutParams as RecyclerView.LayoutParams
val marginInDp = 5 // Set the margin to 5dp
val marginInPixels = (marginInDp * parent.resources.displayMetrics.density).toInt()
layoutParams.setMargins(marginInPixels, marginInPixels, marginInPixels, marginInPixels)
binding.root.layoutParams = layoutParams
}
return AccountViewHolder(binding)
override fun onBindViewHolder(holder: AccountViewHolder, position: Int) {
holder.bind(accounts.getOrNull(position))
}
override fun onBindViewHolder(holder: AccountViewHolder, position: Int) {
holder.bind(accounts[position])
var viewType = 0
override fun getItemViewType(position: Int): Int {
if (viewType != 0 && position != accounts.count()) {
return viewType
}
return when (position) {
accounts.count() -> VIEW_TYPE_ADD_ACCOUNT
else -> VIEW_TYPE_SELECT_ACCOUNT
}
}
override fun getItemCount(): Int {
return accounts.size
return accounts.count() + 1
}
}

View file

@ -1,115 +0,0 @@
package com.lagradost.cloudstream3.ui.account
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.LockPinDialogBinding
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
object AccountDialog {
// TODO add account creation dialog to allow creating accounts directly from AccountSelectActivity
fun showPinInputDialog(
context: Context,
currentPin: String?,
editAccount: Boolean,
callback: (String?) -> Unit
) {
fun TextView.visibleWithText(@StringRes textRes: Int) {
visibility = View.VISIBLE
setText(textRes)
}
fun View.isVisible() = visibility == View.VISIBLE
val binding = LockPinDialogBinding.inflate(LayoutInflater.from(context))
val isPinSet = currentPin != null
val isNewPin = editAccount && !isPinSet
val isEditPin = editAccount && isPinSet
val titleRes = if (isEditPin) R.string.enter_current_pin else R.string.enter_pin
val dialog = AlertDialog.Builder(context, R.style.AlertDialogCustom)
.setView(binding.root)
.setTitle(titleRes)
.setNegativeButton(R.string.cancel) { _, _ ->
callback.invoke(null)
}
.setOnCancelListener {
callback.invoke(null)
}
.setOnDismissListener {
if (binding.pinEditTextError.isVisible()) {
callback.invoke(null)
}
}
.create()
var isPinValid = false
binding.pinEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val enteredPin = s.toString()
val isEnteredPinValid = enteredPin.length == 4
if (isEnteredPinValid) {
if (isPinSet) {
if (enteredPin != currentPin) {
binding.pinEditTextError.visibleWithText(R.string.pin_error_incorrect)
binding.pinEditText.text = null
isPinValid = false
} else {
binding.pinEditTextError.visibility = View.GONE
isPinValid = true
callback.invoke(enteredPin)
dialog.dismissSafe()
}
} else {
binding.pinEditTextError.visibility = View.GONE
isPinValid = true
}
} else if (isNewPin) {
binding.pinEditTextError.visibleWithText(R.string.pin_error_length)
isPinValid = false
}
}
override fun afterTextChanged(s: Editable?) {}
})
// Detect IME_ACTION_DONE
binding.pinEditText.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE && isPinValid) {
val enteredPin = binding.pinEditText.text.toString()
callback.invoke(enteredPin)
dialog.dismissSafe()
}
true
}
// We don't want to accidentally have the dialog dismiss when clicking outside of it.
// That is what the cancel button is for.
dialog.setCanceledOnTouchOutside(false)
dialog.show()
// Auto focus on PIN input and show keyboard
binding.pinEditText.requestFocus()
binding.pinEditText.postDelayed({
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.pinEditText, InputMethodManager.SHOW_IMPLICIT)
}, 200)
}
}

View file

@ -0,0 +1,356 @@
package com.lagradost.cloudstream3.ui.account
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.text.Editable
import android.view.LayoutInflater
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.AccountEditDialogBinding
import com.lagradost.cloudstream3.databinding.AccountSelectLinearBinding
import com.lagradost.cloudstream3.databinding.LockPinDialogBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod
object AccountHelper {
fun showAccountEditDialog(
context: Context,
account: DataStoreHelper.Account,
isNewAccount: Boolean,
accountEditCallback: (DataStoreHelper.Account) -> Unit,
accountDeleteCallback: (DataStoreHelper.Account) -> Unit
) {
val binding = AccountEditDialogBinding.inflate(LayoutInflater.from(context), null, false)
val builder = AlertDialog.Builder(context, R.style.AlertDialogCustom)
.setView(binding.root)
var currentEditAccount = account
val dialog = builder.show()
if (!isNewAccount) binding.title.setText(R.string.edit_account)
// Set up the dialog content
binding.accountName.text = Editable.Factory.getInstance()?.newEditable(account.name)
binding.accountName.doOnTextChanged { text, _, _, _ ->
currentEditAccount = currentEditAccount.copy(name = text?.toString() ?: "")
}
binding.deleteBtt.isGone = isNewAccount
binding.deleteBtt.setOnClickListener {
val dialogClickListener = DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
accountDeleteCallback.invoke(account)
dialog?.dismissSafe()
}
DialogInterface.BUTTON_NEGATIVE -> {
dialog?.dismissSafe()
}
}
}
try {
AlertDialog.Builder(context).setTitle(R.string.delete).setMessage(
context.getString(R.string.delete_message).format(
currentEditAccount.name
)
)
.setPositiveButton(R.string.delete, dialogClickListener)
.setNegativeButton(R.string.cancel, dialogClickListener)
.show().setDefaultFocus()
} catch (t: Throwable) {
logError(t)
}
}
binding.cancelBtt.setOnClickListener {
dialog?.dismissSafe()
}
// Handle the profile picture and its interactions
binding.accountImage.setImage(account.image)
binding.accountImage.setOnClickListener {
// Roll the image forwards once
currentEditAccount =
currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % DataStoreHelper.profileImages.size)
binding.accountImage.setImage(currentEditAccount.image)
}
// Handle applying changes
binding.applyBtt.setOnClickListener {
if (currentEditAccount.lockPin != null) {
// Ask for the current PIN
showPinInputDialog(context, currentEditAccount.lockPin, false) { pin ->
if (pin == null) return@showPinInputDialog
// PIN is correct, proceed to update the account
accountEditCallback.invoke(currentEditAccount)
dialog.dismissSafe()
}
} else {
// No lock PIN set, proceed to update the account
accountEditCallback.invoke(currentEditAccount)
dialog.dismissSafe()
}
}
// Handle setting or changing the PIN
if (currentEditAccount.keyIndex == getDefaultAccount(context).keyIndex) {
binding.lockProfileCheckbox.isVisible = false
if (currentEditAccount.lockPin != null) {
currentEditAccount = currentEditAccount.copy(lockPin = null)
}
}
var canSetPin = true
binding.lockProfileCheckbox.isChecked = currentEditAccount.lockPin != null
binding.lockProfileCheckbox.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
if (canSetPin) {
showPinInputDialog(context, null, true) { pin ->
if (pin == null) {
binding.lockProfileCheckbox.isChecked = false
return@showPinInputDialog
}
currentEditAccount = currentEditAccount.copy(lockPin = pin)
}
}
} else {
if (currentEditAccount.lockPin != null) {
// Ask for the current PIN
showPinInputDialog(context, currentEditAccount.lockPin, true) { pin ->
if (pin == null || pin != currentEditAccount.lockPin) {
canSetPin = false
binding.lockProfileCheckbox.isChecked = true
} else {
currentEditAccount = currentEditAccount.copy(lockPin = null)
}
}
}
}
}
canSetPin = true
}
fun showPinInputDialog(
context: Context,
currentPin: String?,
editAccount: Boolean,
forStartup: Boolean = false,
errorText: String? = null,
callback: (String?) -> Unit
) {
fun TextView.visibleWithText(@StringRes textRes: Int) {
isVisible = true
setText(textRes)
}
fun TextView.visibleWithText(text: String?) {
isVisible = true
setText(text)
}
val binding = LockPinDialogBinding.inflate(LayoutInflater.from(context))
val isPinSet = currentPin != null
val isNewPin = editAccount && !isPinSet
val isEditPin = editAccount && isPinSet
val titleRes = if (isEditPin) R.string.enter_current_pin else R.string.enter_pin
var isPinValid = false
val builder = AlertDialog.Builder(context, R.style.AlertDialogCustom)
.setView(binding.root)
.setTitle(titleRes)
.setNegativeButton(R.string.cancel) { _, _ ->
callback.invoke(null)
}
.setOnCancelListener {
callback.invoke(null)
}
.setOnDismissListener {
if (!isPinValid) {
callback.invoke(null)
}
}
if (forStartup) {
val currentAccount = DataStoreHelper.accounts.firstOrNull {
it.keyIndex == DataStoreHelper.selectedKeyIndex
}
builder.setTitle(context.getString(R.string.enter_pin_with_name, currentAccount?.name))
builder.setOnDismissListener {
if (!isPinValid) {
context.getActivity()?.finish()
}
}
// So that if they don't know the PIN for the current account,
// they don't get completely locked out
builder.setNeutralButton(R.string.use_default_account) { _, _ ->
val activity = context.getActivity()
if (activity is AccountSelectActivity) {
isPinValid = true
activity.viewModel.handleAccountSelect(getDefaultAccount(context), activity)
}
}
}
if (isNewPin) {
if (errorText != null) binding.pinEditTextError.visibleWithText(errorText)
builder.setPositiveButton(R.string.setup_done) { _, _ ->
if (!isPinValid) {
// If the done button is pressed and there is an error,
// ask again, and mention the error that caused this.
showPinInputDialog(
context = binding.root.context,
currentPin = null,
editAccount = true,
errorText = binding.pinEditTextError.text.toString(),
callback = callback
)
} else {
val enteredPin = binding.pinEditText.text.toString()
callback.invoke(enteredPin)
}
}
}
val dialog = builder.create()
binding.pinEditText.doOnTextChanged { text, _, _, _ ->
val enteredPin = text.toString()
val isEnteredPinValid = enteredPin.length == 4
if (isEnteredPinValid) {
if (isPinSet) {
if (enteredPin != currentPin) {
binding.pinEditTextError.visibleWithText(R.string.pin_error_incorrect)
binding.pinEditText.text = null
isPinValid = false
} else {
binding.pinEditTextError.isVisible = false
isPinValid = true
callback.invoke(enteredPin)
dialog.dismissSafe()
}
} else {
binding.pinEditTextError.isVisible = false
isPinValid = true
}
} else if (isNewPin) {
binding.pinEditTextError.visibleWithText(R.string.pin_error_length)
isPinValid = false
}
}
// Detect IME_ACTION_DONE
binding.pinEditText.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE && isPinValid) {
val enteredPin = binding.pinEditText.text.toString()
callback.invoke(enteredPin)
dialog.dismissSafe()
}
true
}
// We don't want to accidentally have the dialog dismiss when clicking outside of it.
// That is what the cancel button is for.
dialog.setCanceledOnTouchOutside(false)
dialog.show()
// Auto focus on PIN input and show keyboard
binding.pinEditText.requestFocus()
binding.pinEditText.postDelayed({
showInputMethod(binding.pinEditText)
}, 200)
}
fun Activity?.showAccountSelectLinear() {
val activity = this as? MainActivity ?: return
val viewModel = ViewModelProvider(activity)[AccountViewModel::class.java]
val binding: AccountSelectLinearBinding = AccountSelectLinearBinding.inflate(
LayoutInflater.from(activity)
)
val builder = BottomSheetDialog(activity)
builder.setContentView(binding.root)
builder.show()
binding.manageAccountsButton.setOnClickListener {
val accountSelectIntent = Intent(activity, AccountSelectActivity::class.java)
accountSelectIntent.putExtra("isEditingFromMainActivity", true)
activity.startActivity(accountSelectIntent)
builder.dismissSafe()
}
val recyclerView: RecyclerView = binding.accountRecyclerView
val itemSize = recyclerView.resources.getDimensionPixelSize(
R.dimen.account_select_linear_item_size
)
recyclerView.addItemDecoration(AccountSelectLinearItemDecoration(itemSize))
recyclerView.setLinearListLayout(isHorizontal = true)
val currentAccount = DataStoreHelper.accounts.firstOrNull {
it.keyIndex == DataStoreHelper.selectedKeyIndex
} ?: getDefaultAccount(activity)
// We want to make sure the accounts are up-to-date
viewModel.handleAccountSelect(
currentAccount,
activity,
reloadForActivity = true
)
activity.observe(viewModel.accounts) { liveAccounts ->
recyclerView.adapter = AccountAdapter(
liveAccounts,
accountSelectCallback = { account ->
viewModel.handleAccountSelect(account, activity)
builder.dismissSafe()
},
accountCreateCallback = { viewModel.handleAccountUpdate(it, activity) },
accountEditCallback = { viewModel.handleAccountUpdate(it, activity) },
accountDeleteCallback = { viewModel.handleAccountDelete(it, activity) }
)
activity.observe(viewModel.selectedKeyIndex) { selectedKeyIndex ->
// Scroll to current account (which is focused by default)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
layoutManager.scrollToPositionWithOffset(selectedKeyIndex, 0)
}
}
}
}

View file

@ -1,87 +1,163 @@
package com.lagradost.cloudstream3.ui.account
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.CommonActivity.loadThemes
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.ActivityAccountSelectBinding
import com.lagradost.cloudstream3.databinding.ActivityAccountSelectTvBinding
import com.lagradost.cloudstream3.ui.account.AccountDialog.showPinInputDialog
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.isTvSettings
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAccounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex
import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
class AccountSelectActivity : AppCompatActivity() {
lateinit var viewModel: AccountViewModel
@SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val accounts = getAccounts(this@AccountSelectActivity)
// Don't show account selection if there is only
// one account that exists
if (accounts.count() <= 1) {
navigateToMainActivity()
return
}
CommonActivity.init(this)
loadThemes(this)
window.navigationBarColor = colorFromAttribute(R.attr.primaryBlackBackground)
val binding = if (isTvSettings()) {
ActivityAccountSelectTvBinding.inflate(layoutInflater)
} else ActivityAccountSelectBinding.inflate(layoutInflater)
// Are we editing and coming from MainActivity?
val isEditingFromMainActivity = intent.getBooleanExtra(
"isEditingFromMainActivity",
false
)
setContentView(binding.root)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val skipStartup = settingsManager.getBoolean(
getString(R.string.skip_startup_account_select_key),
false
) || accounts.count() <= 1
val recyclerView: RecyclerView = binding.root.findViewById(R.id.account_recycler_view)
viewModel = ViewModelProvider(this)[AccountViewModel::class.java]
// Don't show account selection if there is only
// one account that exists
if (!isEditingFromMainActivity && skipStartup) {
val currentAccount = accounts.firstOrNull { it.keyIndex == selectedKeyIndex }
if (currentAccount?.lockPin != null) {
CommonActivity.init(this)
viewModel.handleAccountSelect(currentAccount, this, true)
observe(viewModel.isAllowedLogin) { isAllowedLogin ->
if (isAllowedLogin) {
// We are allowed to continue to MainActivity
navigateToMainActivity()
}
}
} else {
if (accounts.count() > 1) {
showToast(this, getString(
R.string.logged_account,
currentAccount?.name
))
}
val adapter = AccountAdapter(accounts) { selectedAccount ->
// Handle the selected account
onAccountSelected(selectedAccount)
}
recyclerView.adapter = adapter
recyclerView.layoutManager = if (isTvSettings()) {
LinearLayoutManager(this)
} else GridLayoutManager(this, 2)
}
private fun onAccountSelected(selectedAccount: DataStoreHelper.Account) {
if (selectedAccount.lockPin != null) {
// The selected account has a PIN set, prompt the user to enter the PIN
showPinInputDialog(this@AccountSelectActivity, selectedAccount.lockPin, false) { pin ->
if (pin == null) return@showPinInputDialog
// Pin is correct, proceed to main activity
setAccount(selectedAccount)
navigateToMainActivity()
}
} else {
// No PIN set for the selected account, proceed to main activity
setAccount(selectedAccount)
navigateToMainActivity()
}
}
private fun setAccount(account: DataStoreHelper.Account) {
// Don't reload if it is the same account
if (DataStoreHelper.selectedKeyIndex == account.keyIndex) {
return
}
DataStoreHelper.selectedKeyIndex = account.keyIndex
CommonActivity.init(this)
MainActivity.bookmarksUpdatedEvent(true)
MainActivity.reloadHomeEvent(true)
val binding = ActivityAccountSelectBinding.inflate(layoutInflater)
setContentView(binding.root)
val recyclerView: AutofitRecyclerView = binding.accountRecyclerView
observe(viewModel.accounts) { liveAccounts ->
val adapter = AccountAdapter(
liveAccounts,
// Handle the selected account
accountSelectCallback = {
viewModel.handleAccountSelect(it, this)
observe(viewModel.isAllowedLogin) { isAllowedLogin ->
if (isAllowedLogin) {
// We are allowed to continue to MainActivity
navigateToMainActivity()
}
}
},
accountCreateCallback = { viewModel.handleAccountUpdate(it, this) },
accountEditCallback = {
viewModel.handleAccountUpdate(it, this)
// We came from MainActivity, return there
// and switch to the edited account
if (isEditingFromMainActivity) {
setAccount(it)
navigateToMainActivity()
}
},
accountDeleteCallback = { viewModel.handleAccountDelete(it,this) }
)
recyclerView.adapter = adapter
if (isTvSettings()) {
binding.editAccountButton.setBackgroundResource(
R.drawable.player_button_tv_attr_no_bg
)
}
observe(viewModel.selectedKeyIndex) { selectedKeyIndex ->
// Scroll to current account (which is focused by default)
val layoutManager = recyclerView.layoutManager as GridLayoutManager
layoutManager.scrollToPositionWithOffset(selectedKeyIndex, 0)
}
observe(viewModel.isEditing) { isEditing ->
if (isEditing) {
binding.editAccountButton.setImageResource(R.drawable.ic_baseline_close_24)
binding.title.setText(R.string.manage_accounts)
adapter.viewType = VIEW_TYPE_EDIT_ACCOUNT
} else {
binding.editAccountButton.setImageResource(R.drawable.ic_baseline_edit_24)
binding.title.setText(R.string.select_an_account)
adapter.viewType = VIEW_TYPE_SELECT_ACCOUNT
}
adapter.notifyDataSetChanged()
}
if (isEditingFromMainActivity) {
viewModel.setIsEditing(true)
}
binding.editAccountButton.setOnClickListener {
// We came from MainActivity, return there
// and resume its state
if (isEditingFromMainActivity) {
navigateToMainActivity()
return@setOnClickListener
}
viewModel.toggleIsEditing()
}
if (isTvSettings()) {
recyclerView.spanCount = if (liveAccounts.count() + 1 <= 6) {
liveAccounts.count() + 1
} else 6
}
}
}
private fun navigateToMainActivity() {

View file

@ -0,0 +1,14 @@
package com.lagradost.cloudstream3.ui.account
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class AccountSelectLinearItemDecoration(private val size: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val layoutParams = view.layoutParams as RecyclerView.LayoutParams
layoutParams.width = size
layoutParams.height = size
view.layoutParams = layoutParams
}
}

View file

@ -0,0 +1,123 @@
package com.lagradost.cloudstream3.ui.account
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
import com.lagradost.cloudstream3.ui.account.AccountHelper.showPinInputDialog
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAccounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount
import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount
class AccountViewModel : ViewModel() {
private fun getAllAccounts(): List<DataStoreHelper.Account> {
return context?.let { getAccounts(it) } ?: DataStoreHelper.accounts.toList()
}
private val _accounts: MutableLiveData<List<DataStoreHelper.Account>> = MutableLiveData(getAllAccounts())
val accounts: LiveData<List<DataStoreHelper.Account>> = _accounts
private val _isEditing = MutableLiveData(false)
val isEditing: LiveData<Boolean> = _isEditing
private val _isAllowedLogin = MutableLiveData(false)
val isAllowedLogin: LiveData<Boolean> = _isAllowedLogin
private val _selectedKeyIndex = MutableLiveData(
getAllAccounts().indexOfFirst {
it.keyIndex == DataStoreHelper.selectedKeyIndex
}
)
val selectedKeyIndex: LiveData<Int> = _selectedKeyIndex
fun setIsEditing(value: Boolean) {
_isEditing.postValue(value)
}
fun toggleIsEditing() {
_isEditing.postValue(!(_isEditing.value ?: false))
}
fun handleAccountUpdate(
account: DataStoreHelper.Account,
context: Context
) {
val currentAccounts = getAccounts(context).toMutableList()
val overrideIndex = currentAccounts.indexOfFirst { it.keyIndex == account.keyIndex }
if (overrideIndex != -1) {
currentAccounts[overrideIndex] = account
} else currentAccounts.add(account)
val currentHomePage = DataStoreHelper.currentHomePage
setAccount(account)
DataStoreHelper.currentHomePage = currentHomePage
DataStoreHelper.accounts = currentAccounts.toTypedArray()
_accounts.postValue(getAccounts(context))
_selectedKeyIndex.postValue(getAccounts(context).indexOf(account))
}
fun handleAccountDelete(
account: DataStoreHelper.Account,
context: Context
) {
removeKeys(account.keyIndex.toString())
val currentAccounts = getAccounts(context).toMutableList()
currentAccounts.removeIf { it.keyIndex == account.keyIndex }
DataStoreHelper.accounts = currentAccounts.toTypedArray()
if (account.keyIndex == DataStoreHelper.selectedKeyIndex) {
setAccount(getDefaultAccount(context))
}
_accounts.postValue(getAccounts(context))
_selectedKeyIndex.postValue(getAllAccounts().indexOfFirst {
it.keyIndex == DataStoreHelper.selectedKeyIndex
})
}
fun handleAccountSelect(
account: DataStoreHelper.Account,
context: Context,
forStartup: Boolean = false,
reloadForActivity: Boolean = false
) {
if (reloadForActivity) {
_accounts.postValue(getAccounts(context))
_selectedKeyIndex.postValue(getAccounts(context).indexOf(account))
return
}
// Check if the selected account has a lock PIN set
if (account.lockPin != null) {
// The selected account has a PIN set, prompt the user to enter the PIN
showPinInputDialog(
context,
account.lockPin,
false,
forStartup
) { pin ->
if (pin == null) return@showPinInputDialog
// Pin is correct, proceed
_isAllowedLogin.postValue(true)
_selectedKeyIndex.postValue(getAccounts(context).indexOf(account))
setAccount(account)
}
} else {
// No PIN set for the selected account, proceed
_isAllowedLogin.postValue(true)
_selectedKeyIndex.postValue(getAccounts(context).indexOf(account))
setAccount(account)
}
}
}

View file

@ -60,7 +60,7 @@ class DownloadChildFragment : Fragment() {
}
}.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 }
if (eps.isEmpty()) {
activity?.onBackPressed()
activity?.onBackPressedDispatcher?.onBackPressed()
return@main
}
@ -78,7 +78,7 @@ class DownloadChildFragment : Fragment() {
val folder = arguments?.getString("folder")
val name = arguments?.getString("name")
if (folder == null) {
activity?.onBackPressed() // TODO FIX
activity?.onBackPressedDispatcher?.onBackPressed() // TODO FIX
return
}
fixPaddingStatusbar(binding?.downloadChildRoot)
@ -87,7 +87,7 @@ class DownloadChildFragment : Fragment() {
title = name
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
setNavigationOnClickListener {
activity?.onBackPressed()
activity?.onBackPressedDispatcher?.onBackPressed()
}
}

View file

@ -38,6 +38,7 @@ import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.*
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
@ -495,9 +496,10 @@ class HomeFragment : Fragment() {
//homeChangeApiLoading.setOnClickListener(apiChangeClickListener)
homeApiFab.setOnClickListener(apiChangeClickListener)
homeChangeApi.setOnClickListener(apiChangeClickListener)
homeSwitchAccount.setOnClickListener { v ->
DataStoreHelper.showWhoIsWatching(v?.context ?: return@setOnClickListener)
homeSwitchAccount.setOnClickListener {
activity?.showAccountSelectLinear()
}
homeRandom.setOnClickListener {
if (listHomepageItems.isNotEmpty()) {
activity.loadSearchResult(listHomepageItems.random())

View file

@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugException
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
@ -477,8 +478,8 @@ class HomeParentItemAdapterPreview(
}
}
homeAccount?.setOnClickListener { v ->
DataStoreHelper.showWhoIsWatching(v?.context ?: return@setOnClickListener)
homeAccount?.setOnClickListener {
activity?.showAccountSelectLinear()
}
(binding as? FragmentHomeHeadTvBinding)?.apply {

View file

@ -27,6 +27,7 @@ import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.viewpager2.widget.ViewPager2
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.lagradost.cloudstream3.APIHolder
@ -140,6 +141,10 @@ class LibraryFragment : Fragment() {
binding?.libraryRoot?.findViewById<TextView>(R.id.search_src_text)?.apply {
tag = "tv_no_focus_tag"
//Expand the Appbar when search bar is focused, fixing scroll up issue
setOnFocusChangeListener { _, _ ->
binding?.searchBar?.setExpanded(true)
}
}
// Set the color for the search exit icon to the correct theme text color
@ -346,6 +351,7 @@ class LibraryFragment : Fragment() {
binding?.apply {
viewpager.offscreenPageLimit = 2
viewpager.reduceDragSensitivity()
searchBar.setExpanded(true)
}
val startLoading = Runnable {
@ -445,6 +451,10 @@ class LibraryFragment : Fragment() {
val distance = abs(position - currentItem)
hideViewpager(distance)
}
//Expand the appBar on tab focus
tab.view.setOnFocusChangeListener { view, b ->
binding?.searchBar?.setExpanded(true)
}
}.attach()
}
}

View file

@ -120,11 +120,11 @@ class LibraryViewModel : ViewModel() {
}
init {
MainActivity.reloadHomeEvent += ::reloadPages
MainActivity.reloadLibraryEvent += ::reloadPages
}
override fun onCleared() {
MainActivity.reloadHomeEvent -= ::reloadPages
MainActivity.reloadLibraryEvent -= ::reloadPages
super.onCleared()
}
}

View file

@ -1,14 +1,18 @@
package com.lagradost.cloudstream3.ui.library
import android.os.Build
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.doOnAttach
import androidx.recyclerview.widget.RecyclerView
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.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
class ViewpagerAdapter(
@ -67,6 +71,17 @@ class ViewpagerAdapter(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
val diff = scrollY - oldScrollY
//Expand the top Appbar based on scroll direction up/down, simulate phone behavior
if (SettingsFragment.isTvSettings()) {
binding.root.rootView.findViewById<AppBarLayout>(R.id.search_bar)
.apply {
if (diff <= 0)
setExpanded(true)
else
setExpanded(false)
}
}
if (diff == 0) return@setOnScrollChangeListener
scrollCallback.invoke(diff > 0)
@ -80,8 +95,6 @@ class ViewpagerAdapter(
}
}
}
}
}

View file

@ -1014,7 +1014,8 @@ class CS3IPlayer : IPlayer {
format.id!!,
SubtitleOrigin.EMBEDDED_IN_VIDEO,
format.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP,
emptyMap()
emptyMap(),
format.language
)
}
@ -1254,7 +1255,7 @@ class CS3IPlayer : IPlayer {
.setMimeType(sub.mimeType)
.setLanguage("_${sub.name}")
.setId(sub.getId())
.setSelectionFlags(SELECTION_FLAG_DEFAULT)
.setSelectionFlags(0)
.build()
when (sub.origin) {
SubtitleOrigin.DOWNLOADED_FILE -> {

View file

@ -100,7 +100,8 @@ class DownloadFileGenerator(
uri.toString(),
SubtitleOrigin.DOWNLOADED_FILE,
name.toSubtitleMimeType(),
emptyMap()
emptyMap(),
null
)
)
}

View file

@ -6,6 +6,7 @@ import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.R
@ -34,10 +35,6 @@ class DownloadedPlayerActivity : AppCompatActivity() {
CommonActivity.onUserLeaveHint(this)
}
override fun onBackPressed() {
finish()
}
private fun playLink(url: String) {
this.navigate(
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
@ -109,6 +106,15 @@ class DownloadedPlayerActivity : AppCompatActivity() {
finish()
return
}
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
finish()
}
}
)
}
override fun onResume() {

View file

@ -23,6 +23,7 @@ import androidx.media3.common.Format.NO_VALUE
import androidx.media3.common.MimeTypes
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.databinding.DialogOnlineSubtitlesBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
@ -39,6 +40,7 @@ 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.subtitles.SUBTITLE_AUTO_SELECT_KEY
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@ -102,10 +104,33 @@ class GeneratorPlayer : FullScreenPlayer() {
binding?.playerLoadingOverlay?.isVisible = true
}
private fun setSubtitles(sub: SubtitleData?): Boolean {
currentSelectedSubtitles = sub
//Log.i(TAG, "setSubtitles = $sub")
return player.setPreferredSubtitles(sub)
private fun setSubtitles(subtitle: SubtitleData?): Boolean {
// If subtitle is changed -> Save the language
if (subtitle != currentSelectedSubtitles) {
val subtitleLanguage639 = if (subtitle == null) {
// "" is No Subtitles
""
} else if (subtitle.languageCode != null) {
// Could be "English 4" which is why it is trimmed.
val trimmedLanguage = subtitle.languageCode.replace(Regex("\\d"), "").trim()
languages.firstOrNull { language ->
language.languageName.equals(trimmedLanguage, ignoreCase = true) ||
language.ISO_639_1 == subtitle.languageCode
}?.ISO_639_1
} else {
null
}
if (subtitleLanguage639 != null) {
setKey(SUBTITLE_AUTO_SELECT_KEY, subtitleLanguage639)
preferredAutoSelectSubtitles = subtitleLanguage639
}
}
currentSelectedSubtitles = subtitle
//Log.i(TAG, "setSubtitles = $subtitle")
return player.setPreferredSubtitles(subtitle)
}
override fun embeddedSubtitlesFetched(subtitles: List<SubtitleData>) {
@ -450,7 +475,8 @@ class GeneratorPlayer : FullScreenPlayer() {
url = url,
origin = SubtitleOrigin.URL,
mimeType = url.toSubtitleMimeType(),
headers = currentSubtitle.headers
headers = currentSubtitle.headers,
currentSubtitle.lang
)
runOnMainThread {
addAndSelectSubtitles(subtitle)
@ -538,7 +564,8 @@ class GeneratorPlayer : FullScreenPlayer() {
uri.toString(),
SubtitleOrigin.DOWNLOADED_FILE,
name.toSubtitleMimeType(),
emptyMap()
emptyMap(),
null
)
addAndSelectSubtitles(subtitleData)
@ -949,7 +976,7 @@ class GeneratorPlayer : FullScreenPlayer() {
var maxEpisodeSet: Int? = null
var hasRequestedStamps: Boolean = false
override fun playerPositionChanged(position: Long, duration : Long) {
override fun playerPositionChanged(position: Long, duration: Long) {
// Don't save livestream data
if ((currentMeta as? ResultEpisode)?.tvType?.isLiveStream() == true) return
@ -1212,7 +1239,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}
override fun playerDimensionsLoaded(width: Int, height : Int) {
override fun playerDimensionsLoaded(width: Int, height: Int) {
setPlayerDimen(width to height)
}

View file

@ -30,13 +30,15 @@ enum class SubtitleOrigin {
* @param name To be displayed in the player
* @param url Url for the subtitle, when EMBEDDED_IN_VIDEO this variable is used as the real backend id
* @param headers if empty it will use the base onlineDataSource headers else only the specified headers
* @param languageCode Not guaranteed to follow any standard. Could be something like "English 4" or "en".
* */
data class SubtitleData(
val name: String,
val url: String,
val origin: SubtitleOrigin,
val mimeType: String,
val headers: Map<String, String>
val headers: Map<String, String>,
val languageCode: String?
) {
/** Internal ID for exoplayer, unique for each link*/
fun getId(): String {
@ -80,7 +82,8 @@ class PlayerSubtitleHelper {
url = subtitleFile.url,
origin = SubtitleOrigin.URL,
mimeType = subtitleFile.url.toSubtitleMimeType(),
headers = emptyMap()
headers = emptyMap(),
languageCode = subtitleFile.lang
)
}
}

View file

@ -7,6 +7,7 @@ import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.activity.OnBackPressedCallback
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.lagradost.cloudstream3.CommonActivity.screenHeight
@ -15,10 +16,8 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
import com.lagradost.cloudstream3.ui.player.PlayerEventSource
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.IOnBackPressed
open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed {
open class ResultTrailerPlayer : ResultFragmentPhone() {
override var lockRotation = false
override var isFullScreenPlayer = false
@ -28,7 +27,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed {
const val TAG = "RESULT_TRAILER"
}
var playerWidthHeight: Pair<Int, Int>? = null
private var playerWidthHeight: Pair<Int, Int>? = null
override fun nextEpisode() {}
@ -154,6 +153,10 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed {
}
fixPlayerSize()
uiReset()
if (isFullScreenPlayer) {
attachBackPressedCallback()
} else detachBackPressedCallback()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -172,12 +175,26 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed {
}
}
override fun onBackPressed(): Boolean {
return if (isFullScreenPlayer) {
updateFullscreen(false)
false
} else {
true
private var backPressedCallback: OnBackPressedCallback? = null
private fun attachBackPressedCallback() {
if (backPressedCallback == null) {
backPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
updateFullscreen(false)
}
}
}
backPressedCallback?.isEnabled = true
activity?.onBackPressedDispatcher?.addCallback(
activity ?: return,
backPressedCallback ?: return
)
}
private fun detachBackPressedCallback() {
backPressedCallback?.isEnabled = false
}
}

View file

@ -28,6 +28,7 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API
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.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.helpers.settings.account.InAppAuthDialogBuilder
import com.lagradost.cloudstream3.ui.settings.helpers.settings.account.InAppOAuth2DialogBuilder
@ -130,6 +131,7 @@ class SettingsAccount : PreferenceFragmentCompat() {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_account)
setPaddingBottom()
setToolBarScrollFlags()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {

View file

@ -13,10 +13,12 @@ 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.CommonActivity.showToast
import com.lagradost.cloudstream3.R
@ -59,7 +61,24 @@ class SettingsFragment : Fragment() {
listView?.setPadding(0, 0, 0, 100.toPx)
}
}
fun PreferenceFragmentCompat.setToolBarScrollFlags() {
if (isTvSettings()) {
val settingsAppbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar)
settingsAppbar?.updateLayoutParams<AppBarLayout.LayoutParams> {
scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL
}
}
}
fun Fragment?.setToolBarScrollFlags() {
if (isTvSettings()) {
val settingsAppbar = this?.view?.findViewById<MaterialToolbar>(R.id.settings_toolbar)
settingsAppbar?.updateLayoutParams<AppBarLayout.LayoutParams> {
scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL
}
}
}
fun Fragment?.setUpToolbar(title: String) {
if (this == null) return
val settingsToolbar =
@ -69,7 +88,7 @@ class SettingsFragment : Fragment() {
setTitle(title)
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
setNavigationOnClickListener {
activity?.onBackPressed()
activity?.onBackPressedDispatcher?.onBackPressed()
}
}
fixPaddingStatusbar(settingsToolbar)
@ -85,7 +104,7 @@ class SettingsFragment : Fragment() {
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
setNavigationOnClickListener {
activity?.onBackPressed()
activity?.onBackPressedDispatcher?.onBackPressed()
}
}
fixPaddingStatusbar(settingsToolbar)

View file

@ -29,6 +29,7 @@ import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.ui.EasterEggMonke
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.utils.DataStore.getSyncPrefs
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
@ -56,10 +57,10 @@ fun getCurrentLocale(context: Context): String {
// https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes leave blank for auto
val appLanguages = arrayListOf(
/* begin language list */
Triple("", "ajp", "ajp"),
Triple("", "عربي شامي", "ajp"),
Triple("", "አማርኛ", "am"),
Triple("", "العربية", "ar"),
Triple("", "ars", "ars"),
Triple("", "اللهجة النجدية", "ars"),
Triple("", "български", "bg"),
Triple("", "বাংলা", "bn"),
Triple("\uD83C\uDDE7\uD83C\uDDF7", "português brasileiro", "bp"),
@ -117,6 +118,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_general)
setPaddingBottom()
setToolBarScrollFlags()
}
data class CustomSite(
@ -201,7 +203,6 @@ class SettingsGeneral : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true
}
fun showAdd() {
val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } }
activity?.showDialog(

View file

@ -11,6 +11,7 @@ import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
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.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
@ -25,6 +26,7 @@ class SettingsPlayer : PreferenceFragmentCompat() {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_player)
setPaddingBottom()
setToolBarScrollFlags()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()

View file

@ -18,6 +18,7 @@ import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.ui.APIRepository
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.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
@ -31,6 +32,7 @@ class SettingsProviders : PreferenceFragmentCompat() {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_providers)
setPaddingBottom()
setToolBarScrollFlags()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {

View file

@ -12,6 +12,7 @@ import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
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
@ -25,6 +26,7 @@ class SettingsUI : PreferenceFragmentCompat() {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_ui)
setPaddingBottom()
setToolBarScrollFlags()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {

View file

@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.services.BackupWorkManager
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.utils.BackupUtils
import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt
@ -44,6 +45,7 @@ class SettingsUpdates : PreferenceFragmentCompat() {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_updates)
setPaddingBottom()
setToolBarScrollFlags()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {

View file

@ -30,6 +30,7 @@ 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.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.AppUtils.downloadAllPluginsDialog
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
@ -85,7 +86,7 @@ class ExtensionsFragment : Fragment() {
//context?.fixPaddingStatusbar(extensions_root)
setUpToolbar(R.string.extensions)
setToolBarScrollFlags()
binding?.repoRecyclerView?.apply {
setLinearListLayout(

View file

@ -21,7 +21,6 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueT
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.GlideApp
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -87,7 +86,7 @@ class PluginAdapter(
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is PluginViewHolder) {
holder.binding.entryIcon.let { pluginIcon ->
GlideApp.with(pluginIcon).clear(pluginIcon)
com.bumptech.glide.Glide.with(pluginIcon).clear(pluginIcon)
}
}
super.onViewRecycled(holder)

View file

@ -18,6 +18,7 @@ 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.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.ui.settings.appLanguages
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
@ -69,10 +70,11 @@ class PluginsFragment : Fragment() {
val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true
if (url == null || name == null) {
activity?.onBackPressed()
activity?.onBackPressedDispatcher?.onBackPressed()
return
}
setToolBarScrollFlags()
setUpToolbar(name)
binding?.settingsToolbar?.apply {
setOnMenuItemClickListener { menuItem ->
@ -117,7 +119,7 @@ class PluginsFragment : Fragment() {
if (searchView?.isIconified == false) {
searchView.isIconified = true
} else {
activity?.onBackPressed()
activity?.onBackPressedDispatcher?.onBackPressed()
}
}
searchView?.setOnQueryTextFocusChangeListener { _, hasFocus ->

View file

@ -12,6 +12,7 @@ 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.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
@ -27,6 +28,7 @@ class TestFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setUpToolbar(R.string.category_provider_test)
setToolBarScrollFlags()
super.onViewCreated(view, savedInstanceState)
binding?.apply {

View file

@ -1,15 +1,7 @@
package com.lagradost.cloudstream3.utils
import android.content.Context
import android.content.DialogInterface
import android.text.Editable
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import com.fasterxml.jackson.annotation.JsonProperty
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
@ -20,21 +12,12 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.databinding.WhoIsWatchingAccountEditBinding
import com.lagradost.cloudstream3.databinding.WhoIsWatchingBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.WhoIsWatchingAdapter
import com.lagradost.cloudstream3.ui.account.AccountDialog.showPinInputDialog
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.UiImage
import com.lagradost.cloudstream3.ui.result.VideoWatchState
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
@ -75,7 +58,7 @@ class UserPreferenceDelegate<T : Any>(
object DataStoreHelper {
// be aware, don't change the index of these as Account uses the index for the art
private val profileImages = arrayOf(
val profileImages = arrayOf(
R.drawable.profile_bg_dark_blue,
R.drawable.profile_bg_blue,
R.drawable.profile_bg_orange,
@ -147,7 +130,7 @@ object DataStoreHelper {
}
const val TAG = "data_store_helper"
private var accounts by PreferenceDelegate("$TAG/account", arrayOf<Account>())
var accounts by PreferenceDelegate("$TAG/account", arrayOf<Account>())
var selectedKeyIndex by PreferenceDelegate("$TAG/account_key_index", 0)
val currentAccount: String get() = selectedKeyIndex.toString()
@ -166,156 +149,21 @@ object DataStoreHelper {
}
}
private fun setAccount(account: Account, refreshHomePage: Boolean) {
fun setAccount(account: Account) {
val homepage = currentHomePage
selectedKeyIndex = account.keyIndex
showToast(account.name)
showToast(context?.getString(R.string.logged_account, account.name) ?: account.name)
MainActivity.bookmarksUpdatedEvent(true)
if (refreshHomePage) {
MainActivity.reloadLibraryEvent(true)
val oldAccount = accounts.find { it.keyIndex == account.keyIndex }
if (oldAccount != null && currentHomePage != homepage) {
// This is not a new account, and the homepage has changed, reload it
MainActivity.reloadHomeEvent(true)
}
}
private fun editAccount(context: Context, account: Account, isNewAccount: Boolean) {
val binding =
WhoIsWatchingAccountEditBinding.inflate(LayoutInflater.from(context), null, false)
val builder =
AlertDialog.Builder(context, R.style.AlertDialogCustom)
.setView(binding.root)
var currentEditAccount = account
val dialog = builder.show()
binding.accountName.text = Editable.Factory.getInstance()?.newEditable(account.name)
binding.accountName.doOnTextChanged { text, _, _, _ ->
currentEditAccount = currentEditAccount.copy(name = text?.toString() ?: "")
}
binding.deleteBtt.isGone = isNewAccount
binding.deleteBtt.setOnClickListener {
val dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
// remove all keys as well as the account, note that default wont get
// deleted from currentAccounts, as it is not part of "accounts",
// but the watch keys will
removeKeys(account.keyIndex.toString())
val currentAccounts = accounts.toMutableList()
currentAccounts.removeIf { it.keyIndex == account.keyIndex }
accounts = currentAccounts.toTypedArray()
// update UI
setAccount(getDefaultAccount(context), true)
dialog?.dismissSafe()
}
DialogInterface.BUTTON_NEGATIVE -> {}
}
}
try {
AlertDialog.Builder(context).setTitle(R.string.delete).setMessage(
context.getString(R.string.delete_message).format(
currentEditAccount.name
)
)
.setPositiveButton(R.string.delete, dialogClickListener)
.setNegativeButton(R.string.cancel, dialogClickListener)
.show().setDefaultFocus()
} catch (t: Throwable) {
logError(t)
// ye you somehow fucked up formatting did you?
}
}
binding.cancelBtt.setOnClickListener {
dialog?.dismissSafe()
}
binding.profilePic.setImage(account.image)
binding.profilePic.setOnClickListener {
// Roll the image forwards once
currentEditAccount =
currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % profileImages.size)
binding.profilePic.setImage(currentEditAccount.image)
}
binding.applyBtt.setOnClickListener {
if (currentEditAccount.lockPin != null) {
// Ask for the current PIN
showPinInputDialog(context, currentEditAccount.lockPin, false) { pin ->
if (pin == null) return@showPinInputDialog
// PIN is correct, proceed to update the account
performAccountUpdate(currentEditAccount)
dialog.dismissSafe()
}
} else {
// No lock PIN set, proceed to update the account
performAccountUpdate(currentEditAccount)
dialog.dismissSafe()
}
}
// Handle setting or changing the PIN
if (currentEditAccount.keyIndex == getDefaultAccount(context).keyIndex) {
binding.lockProfileCheckbox.isVisible = false
if (currentEditAccount.lockPin != null) {
currentEditAccount = currentEditAccount.copy(lockPin = null)
}
}
var canSetPin = true
binding.lockProfileCheckbox.isChecked = currentEditAccount.lockPin != null
binding.lockProfileCheckbox.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
if (canSetPin) {
showPinInputDialog(context, null, true) { pin ->
if (pin == null) {
binding.lockProfileCheckbox.isChecked = false
return@showPinInputDialog
}
currentEditAccount = currentEditAccount.copy(lockPin = pin)
}
}
} else {
if (currentEditAccount.lockPin != null) {
// Ask for the current PIN
showPinInputDialog(context, currentEditAccount.lockPin, true) { pin ->
if (pin == null || pin != currentEditAccount.lockPin) {
canSetPin = false
binding.lockProfileCheckbox.isChecked = true
} else {
currentEditAccount = currentEditAccount.copy(lockPin = null)
}
}
}
}
}
canSetPin = true
}
private fun performAccountUpdate(account: Account) {
val currentAccounts = accounts.toMutableList()
val overrideIndex = currentAccounts.indexOfFirst { it.keyIndex == account.keyIndex }
if (overrideIndex != -1) {
currentAccounts[overrideIndex] = account
} else {
currentAccounts.add(account)
}
val currentHomePage = this.currentHomePage
setAccount(account, false)
this.currentHomePage = currentHomePage
accounts = currentAccounts.toTypedArray()
}
private fun getDefaultAccount(context: Context): Account {
fun getDefaultAccount(context: Context): Account {
return accounts.let { currentAccounts ->
currentAccounts.getOrNull(currentAccounts.indexOfFirst { it.keyIndex == 0 }) ?: Account(
keyIndex = 0,
@ -333,71 +181,6 @@ object DataStoreHelper {
}
}
fun showWhoIsWatching(context: Context) {
val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate(LayoutInflater.from(context))
val builder = BottomSheetDialog(context)
builder.setContentView(binding.root)
val showAccount = accounts.toMutableList().apply {
val item = getDefaultAccount(context)
remove(item)
add(0, item)
}
val accountName = context.getString(R.string.account)
binding.profilesRecyclerview.setLinearListLayout(isHorizontal = true)
binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter(
selectCallBack = { account ->
// Check if the selected account has a lock PIN set
if (account.lockPin != null) {
// Prompt for the lock pin
showPinInputDialog(context, account.lockPin, false) { pin ->
if (pin == null) return@showPinInputDialog
// Pin is correct, unlock the profile
setAccount(account, true)
builder.dismissSafe()
}
} else {
// No lock PIN set, directly set the account
setAccount(account, true)
builder.dismissSafe()
}
},
addAccountCallback = {
val currentAccounts = accounts
val remainingImages =
profileImages.toSet() - currentAccounts.filter { it.customImage == null }
.mapNotNull { profileImages.getOrNull(it.defaultImageIndex) }.toSet()
val image =
profileImages.indexOf(remainingImages.randomOrNull() ?: profileImages.random())
val keyIndex = (currentAccounts.maxOfOrNull { it.keyIndex } ?: 0) + 1
// create a new dummy account
editAccount(
context,
Account(
keyIndex = keyIndex,
name = "$accountName $keyIndex",
customImage = null,
defaultImageIndex = image
), isNewAccount = true
)
builder.dismissSafe()
},
editCallBack = { account ->
editAccount(
context, account, isNewAccount = false
)
builder.dismissSafe()
}
).apply {
submitList(showAccount)
}
builder.show()
}
data class PosDur(
@JsonProperty("position") val position: Long,
@JsonProperty("duration") val duration: Long

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.utils
import android.net.Uri
import com.fasterxml.jackson.annotation.JsonIgnore
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.USER_AGENT
@ -377,7 +378,8 @@ open class ExtractorLink constructor(
) : VideoDownloadManager.IDownloadableMinimum {
val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8
val isDash : Boolean get() = type == ExtractorLinkType.DASH
@JsonIgnore
fun getAllHeaders() : Map<String, String> {
if (referer.isBlank()) {
return headers
@ -920,4 +922,4 @@ abstract class ExtractorApi {
open fun getExtractorUrl(id: String): String {
return id
}
}
}

View file

@ -1,5 +0,0 @@
package com.lagradost.cloudstream3.utils
interface IOnBackPressed {
fun onBackPressed(): Boolean
}

View file

@ -153,9 +153,11 @@ object SubtitleHelper {
private val flags = mapOf(
"af" to "ZA",
"agq" to "CM",
"ajp" to "SY",
"ak" to "GH",
"am" to "ET",
"ar" to "AE",
"ars" to "SA",
"as" to "IN",
"asa" to "TZ",
"az" to "AZ",
@ -515,4 +517,4 @@ object SubtitleHelper {
Language639("Zhuang", "Saɯ cueŋƅ, Saw cuengh", "za", "zha", "zha", "zha", ""),
Language639("Zulu", "isiZulu", "zu", "zul", "zul", "zul", ""),
)
}
}

View file

@ -301,7 +301,7 @@ object UIHelper {
} ?: return false
return try {
var builder = GlideApp.with(this)
var builder = com.bumptech.glide.Glide.with(this)
.load(glideImage)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.ALL).let { req ->
@ -368,7 +368,7 @@ object UIHelper {
) {
if (this == null || url.isNullOrBlank()) return
try {
val res = GlideApp.with(this)
val res = com.bumptech.glide.Glide.with(this)
.load(GlideUrl(url) { headers ?: emptyMap() })
.apply(bitmapTransform(BlurTransformation(radius, sample)))
.transition(
@ -418,7 +418,7 @@ object UIHelper {
}
fun FragmentActivity.popCurrentPage() {
this.onBackPressed()
this.onBackPressedDispatcher.onBackPressed()
/*val currentFragment = supportFragmentManager.fragments.lastOrNull {
it.isVisible
} ?: return
@ -438,7 +438,7 @@ object UIHelper {
val currentFragment = supportFragmentManager.fragments.lastOrNull {
it.isVisible
}
?: //this.onBackPressed()
?: //this.onBackPressedDispatcher.onBackPressed()
return
/*

View file

@ -234,7 +234,7 @@ object VideoDownloadManager {
return cachedBitmaps[url]
}
val bitmap = GlideApp.with(this)
val bitmap = com.bumptech.glide.Glide.with(this)
.asBitmap()
.load(GlideUrl(url) { headers ?: emptyMap() })
.into(720, 720)

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
@ -14,13 +15,12 @@
android:orientation="horizontal">
<TextView
android:id="@+id/text1"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_rowWeight="1"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:text="@string/create_account"
@ -28,34 +28,8 @@
android:textSize="20sp"
android:textStyle="bold" />
<!-- <com.google.android.material.button.MaterialButton-->
<!-- android:nextFocusDown="@id/repo_name_input"-->
<!-- android:id="@+id/list_repositories"-->
<!-- android:nextFocusLeft="@id/apply_btt"-->
<!-- android:nextFocusRight="@id/cancel_btt"-->
<!-- style="@style/WhiteButton"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_gravity="center_vertical"-->
<!-- android:text="@string/view_public_repositories_button_short" />-->
</LinearLayout>
<TextView
android:id="@+id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_rowWeight="1"
android:layout_gravity="center_vertical"
android:layout_marginBottom="10dp"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:textColor="?attr/grayTextColor"
android:textSize="15sp"
android:visibility="gone"
tools:text="Gogoanime" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -89,55 +63,55 @@
app:cardCornerRadius="@dimen/rounded_image_radius">
<ImageView
android:foreground="@drawable/outline_drawable_forced_round"
android:id="@+id/profile_pic"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_gravity="center"
android:id="@+id/account_image"
android:src="@drawable/profile_bg_blue"
android:focusable="true"
android:contentDescription="@string/preview_background_img_des"
android:scaleType="centerCrop"
android:src="@drawable/profile_bg_blue" />
android:foreground="@drawable/outline_drawable_forced_round"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_gravity="center" />
</androidx.cardview.widget.CardView>
</LinearLayout>
<RelativeLayout
android:id="@+id/apply_btt_holder"
android:orientation="horizontal"
android:padding="10dp"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_gravity="bottom"
android:layout_marginTop="-60dp"
android:orientation="horizontal"
android:padding="10dp">
android:layout_marginTop="-60dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/delete_btt"
style="@style/BlackButton"
android:text="@string/delete"
android:layout_width="wrap_content"
android:layout_alignParentStart="true"
android:layout_gravity="center_vertical"
android:nextFocusRight="@id/apply_btt"
android:text="@string/delete" />
style="@style/BlackButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/apply_btt"
style="@style/WhiteButton"
android:text="@string/sort_apply"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical|end"
android:layout_toStartOf="@+id/cancel_btt"
android:nextFocusLeft="@id/delete_btt"
android:nextFocusRight="@id/cancel_btt"
android:text="@string/sort_apply" />
style="@style/WhiteButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_btt"
style="@style/BlackButton"
android:text="@string/sort_cancel"
android:layout_width="wrap_content"
android:layout_alignParentEnd="true"
android:layout_gravity="center_vertical|end"
android:nextFocusLeft="@id/apply_btt"
android:text="@string/sort_cancel" />
style="@style/BlackButton" />
</RelativeLayout>
</LinearLayout>

View file

@ -9,9 +9,9 @@
android:animateLayoutChanges="true"
android:backgroundTint="?attr/primaryGrayBackground"
android:foreground="?attr/selectableItemBackground"
app:cardCornerRadius="@dimen/rounded_image_radius"
android:layout_margin="10dp"
android:focusable="true"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent"
@ -19,38 +19,38 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/account_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.4"
android:contentDescription="@string/profile_background_des"
android:scaleType="centerCrop" />
<ImageView
android:id="@+id/account_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.4"
android:scaleType="centerCrop" />
<View
android:id="@+id/outline"
tools:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/outline_card"
android:visibility="gone" />
<View
android:id="@+id/outline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/outline_card"
android:visibility="gone"
tools:visibility="visible" />
<ImageView
android:id="@+id/lock_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="top|end"
android:layout_margin="4dp"
android:src="@drawable/video_locked"
android:visibility="gone" />
<ImageView
android:id="@+id/lock_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="top|end"
android:layout_margin="4dp"
android:src="@drawable/video_locked"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:padding="10dp"
android:textSize="16sp" />
<TextView
android:id="@+id/account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:padding="10dp"
android:textSize="16sp" />
</androidx.cardview.widget.CardView>
</androidx.cardview.widget.CardView>

View file

@ -2,16 +2,15 @@
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card_view"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_width="110dp"
android:layout_height="110dp"
android:animateLayoutChanges="true"
android:backgroundTint="?attr/primaryGrayBackground"
android:foreground="?attr/selectableItemBackgroundBorderless"
app:cardCornerRadius="@dimen/rounded_image_radius"
android:layout_margin="5dp"
android:foreground="?attr/selectableItemBackground"
android:layout_margin="10dp"
android:focusable="true"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -4,14 +4,14 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card_view"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_width="110dp"
android:layout_height="110dp"
android:animateLayoutChanges="true"
android:backgroundTint="?attr/primaryGrayBackground"
android:foreground="?attr/selectableItemBackgroundBorderless"
app:cardCornerRadius="@dimen/rounded_image_radius"
android:layout_margin="5dp"
android:foreground="?attr/selectableItemBackground"
android:layout_margin="10dp"
android:focusable="true"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent"
@ -20,20 +20,19 @@
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/profile_image_background"
android:id="@+id/account_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.4"
android:contentDescription="@string/profile_background_des"
android:alpha="0.1"
android:scaleType="centerCrop" />
<View
android:id="@+id/outline"
tools:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/outline_card"
android:visibility="gone" />
android:visibility="gone"
tools:visibility="visible" />
<ImageView
android:id="@+id/lock_icon"
@ -42,11 +41,18 @@
android:layout_gravity="top|end"
android:layout_margin="4dp"
android:src="@drawable/video_locked"
android:visibility="gone" />
android:visibility="gone"
tools:visibility="visible" />
<ImageView
android:id="@+id/pencil_icon"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="top|start"
android:src="@drawable/ic_baseline_edit_24" />
<TextView
android:id="@+id/profile_text"
tools:text="@string/mobile_data"
android:id="@+id/account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:textStyle="bold"
android:text="@string/switch_account"
android:textSize="20sp"
android:textColor="?attr/textColor"
android:layout_width="match_parent"
android:layout_rowWeight="1"
android:layout_height="wrap_content"
android:layout_marginTop="20dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/account_recycler_view"
android:orientation="horizontal"
android:descendantFocusability="afterDescendants"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="4"
tools:listitem="@layout/account_list_item" />
<com.google.android.material.button.MaterialButton
android:id="@+id/manage_accounts_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="10dp"
android:text="@string/manage_accounts"
android:textSize="16sp"
app:icon="@drawable/ic_baseline_edit_24"
style="@style/BlackButton" />
</LinearLayout>

View file

@ -1,28 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="?attr/primaryBlackBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="36dp"
android:gravity="center">
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/select_an_account"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" />
<ImageView
android:id="@+id/edit_account_button"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="end|top"
android:layout_marginTop="40dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_baseline_edit_24"
android:focusable="true"
android:nextFocusDown="@id/account_recycler_view"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/manage_accounts"
app:tint="@color/player_on_button_tv_attr" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/account_recycler_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="36dp"
android:gravity="center">
</LinearLayout>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/select_an_account"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" />
<com.lagradost.cloudstream3.ui.AutofitRecyclerView
android:id="@+id/account_recycler_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp" />
</LinearLayout>
</FrameLayout>

View file

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:background="?attr/primaryBlackBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="36dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textAlignment="center"
android:text="@string/select_an_account"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/account_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp" />
</LinearLayout>

View file

@ -27,7 +27,8 @@
android:id="@+id/search_status_bar_padding"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:orientation="horizontal"
app:layout_scrollFlags="scroll|enterAlways">
<ImageView
android:id="@+id/provider_selector"
@ -108,6 +109,7 @@
</androidx.appcompat.widget.SearchView>
</FrameLayout>
</LinearLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/library_tab_layout"
style="@style/Theme.Widget.Tabs"
@ -117,7 +119,7 @@
android:nextFocusDown="@id/search_result_root"
android:background="?attr/primaryGrayBackground"
android:paddingHorizontal="5dp"
app:layout_scrollFlags="noScroll"
android:focusable="true"
app:tabGravity="center"
app:tabIndicator="@drawable/indicator_background"
app:tabIndicatorColor="?attr/white"
@ -134,15 +136,15 @@
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="40dp"
android:focusable="true"
android:tag="@string/tv_no_focus_tag"
tools:listitem="@layout/library_viewpager_page" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="40dp"
android:focusable="true"
android:tag="@string/tv_no_focus_tag"
tools:listitem="@layout/library_viewpager_page" />
<LinearLayout
android:id="@+id/library_loading_overlay"
@ -182,7 +184,6 @@
tools:listitem="@layout/loading_poster_dynamic" />
</com.facebook.shimmer.ShimmerFrameLayout>
</LinearLayout>
</FrameLayout>
<FrameLayout

View file

@ -1,55 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:layout_marginTop="20dp"
android:textStyle="bold"
android:text="@string/switch_account"
android:textSize="20sp"
android:textColor="?attr/textColor"
android:layout_width="match_parent"
android:layout_rowWeight="1"
android:layout_height="wrap_content" />
<TextView
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:text="@string/history"
android:textSize="15sp"
android:textColor="?attr/grayTextColor"
android:layout_width="match_parent"
android:layout_rowWeight="1"
android:layout_height="wrap_content" />
<TextView
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:text="@string/error_bookmarks_text"
android:textSize="15sp"
android:textColor="?attr/grayTextColor"
android:layout_width="match_parent"
android:layout_rowWeight="1"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:layout_marginTop="10dp"
android:descendantFocusability="afterDescendants"
android:id="@+id/profiles_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="4"
tools:listitem="@layout/who_is_watching_account">
<requestFocus />
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>

View file

@ -12,8 +12,8 @@
<!-- TRANSLATE, BUT DON'T FORGET FORMAT -->
<string name="player_speed_text_format" formatted="true">سرعة (%.2fx)</string>
<string name="rated_format" formatted="true">تقييم: %.1f</string>
<string name="new_update_format" formatted="true">!تم العثور على تحديث جديد
\n%s -&gt; %s</string>
<string name="new_update_format" formatted="true">يوجد تحديث جديد!
\n%1$s -&gt; %2$s</string>
<string name="duration_format" formatted="true">%d دقيقة</string>
<string name="app_name">CloudStream</string>
<string name="play_with_app_name">تشغيل بواسطة CloudStream</string>
@ -319,7 +319,7 @@
<string name="kitsu_account_settings" translatable="false">Kitsu</string>
<string name="trakt_account_settings" translatable="false">Trakt</string>
-->
<string name="login_format" formatted="true">%s %s</string>
<string name="login_format" formatted="true">%1$s %2$s</string>
<string name="account">حساب</string>
<string name="logout">تسجيل الخروج</string>
<string name="login">تسجيل الدخول</string>
@ -418,8 +418,8 @@
<string name="plugin_deleted">تم إزالة الإضافة</string>
<string name="plugin_load_fail" formatted="true">تعذر التحميل %s</string>
<string name="is_adult">18+</string>
<string name="batch_download_start_format" formatted="true">بدأ تنزيل %d %s </string>
<string name="batch_download_finish_format" formatted="true">تم التنزيل %d %s</string>
<string name="batch_download_start_format" formatted="true">بدأ تنزيل %1$d %2$s…</string>
<string name="batch_download_finish_format" formatted="true">تم تنزيل %1$d %2$s</string>
<string name="batch_download_nothing_to_download_format" formatted="true">جميع %s محملة بالفعل</string>
<string name="batch_download">تحميل مكثف</string>
<string name="plugin_singular">إضافة</string>
@ -461,11 +461,11 @@
<string name="history">السجل</string>
<string name="enable_skip_op_from_database_des">عرض زر تخطي المقدمة/الخاتمة</string>
<string name="cast_format" formatted="true">طاقم العمل: %s</string>
<string name="next_episode_time_day_format" formatted="true">%d يوم %d ساعة %d دقيقة</string>
<string name="next_episode_time_hour_format" formatted="true">%d ساعة %d دقيقة</string>
<string name="next_episode_time_day_format" formatted="true">%1$d يوم %2$d ساعة %3$d دقيقة</string>
<string name="next_episode_time_hour_format" formatted="true">%1$d ساعة %2$d دقيقة</string>
<string name="filler" formatted="true">الفيلير</string>
<string name="action_open_play">فتح(تشغيل)</string>
<string name="season_format">%s %d%s</string>
<string name="season_format">%1$s %2$d%3$s</string>
<string name="plugins_updated" formatted="true">المكونات الإضافية المحدثة %d</string>
<string name="player_settings_play_in_vlc">VLC</string>
<string name="player_settings_play_in_mpv">MPV</string>
@ -482,7 +482,7 @@
<string name="action_mark_as_watched">علّمه كفيديو تمت مشاهدته</string>
<string name="yes">نعم</string>
<string name="no"></string>
<string name="app_dub_sub_episode_text_format" formatted="true">%s الحلقة %d</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%1$s الحلقة %2$d</string>
<string name="next_episode_format" formatted="true">سيتم إصدار الحلقة %d في</string>
<string name="update_notification_failed">تعذر تثبيت الإصدار الجديد من التطبيق</string>
<string name="extension_install_first">تثبيت الإضافة أولا</string>
@ -493,8 +493,8 @@
<string name="update_notification_downloading">‌تنزيل تحديث التطبيق…</string>
<string name="update_notification_installing">‏تثبيت تحديث التطبيق…</string>
<string name="next_episode_time_min_format" formatted="true">%d دقيقة</string>
<string name="episodes_range">%d-%d</string>
<string name="episode_format" formatted="true">%d %s</string>
<string name="episodes_range">%1$d-%2$d</string>
<string name="episode_format" formatted="true">%1$d %2$s</string>
<string name="confirm_exit_dialog">هل أنت متأكد أنك تريد الخروج؟</string>
<string name="automatic_plugin_download_summary">قم بتثبيت جميع المكونات الإضافية التي لم يتم تثبيتها بعد تلقائيا من المستودعات المضافة.</string>
<string name="apk_installer_settings">مثبت الحزم</string>
@ -584,4 +584,30 @@
<string name="no_repository_found_error">المستودع لم يتم العثور عليه، تحقق من العنوان اوجرب شبكة افتراضية خاصة(vpn)</string>
<string name="already_voted">لقد صوتت بالفعل</string>
<string name="backup_frequency">معدل النسخ الإحتياطي</string>
<string name="favorite_removed">تمت إزالة %s من المفضلة</string>
<string name="favorites_list_name">المفضلة</string>
<string name="favorite_added">تمت إضافة %s إلى المفضلة</string>
<string name="duplicate_message_multiple" formatted="true">احتمال وجود تكرارات في مكتبتك.
\n
\n%s
\n
\nهل تريد الاضافة على اي حال مستبدلاً النسخة الموجودة بالفعل, أم تفضل إلغاء العملية؟</string>
<string name="duplicate_title">احتمال أن يكون موجود بالفعل</string>
<string name="lock_profile">قفل الحساب</string>
<string name="action_add_to_favorites">اضافة الى المفضلة</string>
<string name="duplicate_replace_all">تبديل الكل</string>
<string name="pin_error_incorrect">رقم PIN غير صحيح. برجاء المحاولة مرة اخرى.</string>
<string name="action_unsubscribe">إلغاء الاشتراك</string>
<string name="pin_error_length">رقم ال PIN يجب ان يكون 4 ارقام</string>
<string name="duplicate_replace">استبدال</string>
<string name="duplicate_add">اضافة</string>
<string name="action_subscribe">إشترك</string>
<string name="action_remove_from_favorites">إزالة من المفضلة</string>
<string name="select_an_account">اختار حساب</string>
<string name="duplicate_message_single">من الظاهر أن \"%1$s\" موجود بالفعل في مكتبتك.
\n
\nهل تريد الاضافة على أي حال مستبدلاً القديم أو إلغاء العملية؟</string>
<string name="enter_pin">ادخال ال PIN</string>
<string name="pin">PIN</string>
<string name="enter_current_pin">أدخل ال PIN الحالي</string>
</resources>

View file

@ -2,7 +2,7 @@
<resources>
<!-- KEYS DON'T TRANSLATE -->
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%1$s Ep %2$d</string>
<string name="cast_format" formatted="true">Hrají: %s</string>
<!-- IS NOT NEEDED TO TRANSLATE AS THEY ARE ONLY USED FOR SCREEN READERS AND WONT SHOW UP TO NORMAL USERS -->
<string name="result_poster_img_des">Plakát</string>
@ -17,7 +17,7 @@
<string name="player_speed_text_format" formatted="true">Rychlost (%.2fx)</string>
<string name="rated_format" formatted="true">Hodnocení: %.1f</string>
<string name="new_update_format" formatted="true">Nalezena nová aktualizace!
\n%s -&gt; %s</string>
\n%1$s -&gt; %2$s</string>
<string name="filler" formatted="true">Výplň</string>
<string name="duration_format" formatted="true">%d min</string>
<string name="app_name">CloudStream</string>
@ -292,7 +292,7 @@
<string name="kitsu_account_settings" translatable="false">Kitsu</string>
<string name="trakt_account_settings" translatable="false">Trakt</string>
-->
<string name="login_format" formatted="true">%s %s</string>
<string name="login_format" formatted="true">%1$s %2$s</string>
<string name="account">účet</string>
<string name="logout">Odhlásit se</string>
<string name="login">Přihlásit se</string>
@ -410,17 +410,17 @@
<string name="clipboard_too_large">Příliš mnoho textu. Nepodařilo se uložit do schránky.</string>
<string name="yes">Ano</string>
<string name="browser">Prohlížeč</string>
<string name="episodes_range">%d-%d</string>
<string name="episodes_range">%1$d-%2$d</string>
<string name="library">Knihovna</string>
<string name="kitsu_settings">Zobrazit plakáty z Kitsu</string>
<string name="automatic_plugin_download">Automaticky stahovat doplňky</string>
<string name="redo_setup_process">Znovu provést proces nastavení</string>
<string name="apk_installer_settings">Instalátor APK</string>
<string name="episode_format" formatted="true">%d %s</string>
<string name="episode_format" formatted="true">%1$d %2$s</string>
<string name="apk_installer_settings_des">Některé telefony nepodporují nový instalátor balíčků. Pokud se aktualizace nenainstalují, zkuste použít starší možnost.</string>
<string name="pref_category_cache">Mezipaměť</string>
<string name="next_episode_format" formatted="true">Epizoda %d bude vydána za</string>
<string name="next_episode_time_hour_format" formatted="true">%dh %dm</string>
<string name="next_episode_time_hour_format" formatted="true">%1$dh %2$dm</string>
<string name="play_livestream_button">Přehrát přímý přenos</string>
<string name="pref_category_extensions">Rozšíření</string>
<string name="pref_category_actions">Akce</string>
@ -436,7 +436,7 @@
<string name="preferred_media_subtext">Co chcete vidět</string>
<string name="plugin_downloaded">Doplněk stažen</string>
<string name="is_adult">18+</string>
<string name="batch_download_start_format" formatted="true">Spuštěno stahování %d %s…</string>
<string name="batch_download_start_format" formatted="true">Spuštěno stahování %1$d %2$s…</string>
<string name="blank_repo_message">CloudStream nemá ve výchozím nastavení nainstalované žádné weby. Stránky je třeba nainstalovat z úložišť.
\n
\nKvůli nesmyslnému podání stížnosti DMCA společností Sky UK Limited 🤮 nemůžeme v aplikaci propojit stránky repozitářů.
@ -505,12 +505,12 @@
<string name="pref_category_app_updates">Aktualizace aplikace</string>
<string name="setup_done">Hotovo</string>
<string name="extension_types">Podporováno</string>
<string name="season_format">%s %d%s</string>
<string name="season_format">%1$s %2$d%3$s</string>
<string name="live_singular">Živý přenos</string>
<string name="nsfw_singular">NSFW</string>
<string name="extensions">Rozšíření</string>
<string name="play_trailer_button">Přehrát trailer</string>
<string name="next_episode_time_day_format" formatted="true">%dd %dh %dm</string>
<string name="next_episode_time_day_format" formatted="true">%1$dd %2$dh %3$dm</string>
<string name="view_public_repositories_button">Zobrazit komunitní repozitáře</string>
<string name="update_started">Aktualizace zahájena</string>
<string name="stream">Stream</string>
@ -520,7 +520,7 @@
<string name="referer">Referent</string>
<string name="next">Další</string>
<string name="provider_languages_tip">Sledovat videa v těchto jazycích</string>
<string name="batch_download_finish_format" formatted="true">Staženo %d %s</string>
<string name="batch_download_finish_format" formatted="true">Staženo %1$d %2$s</string>
<string name="batch_download_nothing_to_download_format" formatted="true">Všechny %s jsou již staženy</string>
<string name="batch_download">Hromadné stahování</string>
<string name="plugin_singular">doplněk</string>
@ -575,4 +575,31 @@
<string name="no_plugins_found_error">V repozitáři nebyly nalezeny žádné doplňky</string>
<string name="no_repository_found_error">Repozitář nenalezen, zkontrolujte adresu URL a zkuste použít VPN</string>
<string name="already_voted">Již jste hlasovali</string>
<string name="favorite_removed">%s odebráno z oblíbených</string>
<string name="favorites_list_name">Oblíbené</string>
<string name="favorite_added">%s přidáno do oblíbených</string>
<string name="duplicate_message_multiple" formatted="true">Ve vaší knihovně byl nalezen potenciální duplikát:
\n
\n%s
\n
\nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci\?</string>
<string name="backup_frequency">Frekvence záloh</string>
<string name="duplicate_title">Nalezena potenciální duplicita</string>
<string name="lock_profile">Zamknout profil</string>
<string name="action_add_to_favorites">Přidat do oblíbených</string>
<string name="duplicate_replace_all">Nahradit vše</string>
<string name="pin_error_incorrect">Nesprávný PIN. Zkuste to prosím znovu.</string>
<string name="action_unsubscribe">Zrušit odběr</string>
<string name="pin_error_length">PIN musí obsahovat 4 znaky</string>
<string name="duplicate_replace">Nahradit</string>
<string name="duplicate_add">Přidat</string>
<string name="action_subscribe">Odebírat</string>
<string name="action_remove_from_favorites">Odebrat z oblíbených</string>
<string name="select_an_account">Vyberte účet</string>
<string name="duplicate_message_single">Vypadá to, že ve vaší knihovně již existuje potenciální duplikát: „%1$s“.
\n
\nChcete přesto přidat tuto položku, nahradit existující nebo zrušit akci\?</string>
<string name="enter_pin">Zadejte PIN</string>
<string name="pin">PIN</string>
<string name="enter_current_pin">Zadejte současný PIN</string>
</resources>

View file

@ -4,10 +4,10 @@
<string name="setup_extensions_subtext">Descargue la lista de sitios que quiera utilizar</string>
<string name="plugins_downloaded" formatted="true">Descargado:%d</string>
<string name="downloaded">Descargado</string>
<string name="batch_download_finish_format" formatted="true">Descargado %d %s</string>
<string name="batch_download_finish_format" formatted="true">Descargado %1$d %2$s</string>
<string name="delete_repository">Borrar repositorio</string>
<string name="next_episode_format" formatted="true">El episodio %d se lanzará en</string>
<string name="next_episode_time_hour_format" formatted="true">%dh %dm</string>
<string name="next_episode_time_hour_format" formatted="true">%1$dh %2$dm</string>
<string name="next_episode_time_min_format" formatted="true">%dm</string>
<string name="result_poster_img_des">Poster</string>
<string name="extensions">Extensiones</string>
@ -97,12 +97,12 @@
<string name="search_poster_img_des">Poster</string>
<string name="home_next_random_img_des">Siguiente al azar</string>
<string name="all_languages_preference">Todos los Idiomas</string>
<string name="go_back_img_des">Volver</string>
<string name="go_back_img_des">Regresar</string>
<string name="home_change_provider_img_des">Cambiar proveedor</string>
<string name="preview_background_img_des">Vista previa del fondo</string>
<string name="rated_format" formatted="true">Nota:%.1f</string>
<string name="new_update_format" formatted="true">Nueva actualización encontrada!
\n%s -&gt; %s</string>
<string name="new_update_format" formatted="true">¡Nueva actualización encontrada!
\n%1$s -&gt; %2$s</string>
<string name="download">Descargar</string>
<string name="popup_pause_download">Pausar Descarga</string>
<string name="subs_font">Formato de fuente</string>
@ -110,8 +110,8 @@
<string name="subs_font_size">Tamaño de Fuente</string>
<string name="player_speed_text_format" formatted="true">Velocidad (%.2fx)</string>
<string name="skip_loading">Omitir carga</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>
<string name="next_episode_time_day_format" formatted="true">%dd %dh %dm</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%1$s Ep. %2$d</string>
<string name="next_episode_time_day_format" formatted="true">%1$dd %2$dh %3$dm</string>
<string name="cast_format" formatted="true">Elenco %s</string>
<string name="filler" formatted="true">Relleno</string>
<string name="duration_format" formatted="true">%d min</string>
@ -145,7 +145,7 @@
<string name="play_torrent_button">Transmitir Torrent</string>
<string name="pick_source">Fuentes</string>
<string name="reload_error">Reintentar conexión…</string>
<string name="go_back">Volver</string>
<string name="go_back">Regresar</string>
<string name="downloading">Descargando</string>
<string name="download_paused">Descarga pausada</string>
<string name="download_started">Descarga iniciada</string>
@ -218,8 +218,8 @@
<string name="play_episode_toast">Reproducir Episodio</string>
<string name="episode">Episodio</string>
<string name="episodes">Episodios</string>
<string name="episodes_range">%d-%d</string>
<string name="episode_format" formatted="true">%d %s</string>
<string name="episodes_range">%1$d-%2$d</string>
<string name="episode_format" formatted="true">%1$d %2$s</string>
<string name="episode_short">E</string>
<string name="restore_failed_format" formatted="true">Falló la restauración de los datos desde el archivo %s</string>
<string name="backup_success">Datos guardados</string>
@ -233,7 +233,7 @@
<string name="advanced_search_des">Mostrar los resultados de la búsqueda por proveedor</string>
<string name="bug_report_settings_off">Solo envíar los datos si la App se cierra / falla inesperadamente</string>
<string name="bug_report_settings_on">No enviar datos</string>
<string name="show_trailers_settings">Mostrar Trailers (avances)</string>
<string name="show_trailers_settings">Mostrar los trailers</string>
<string name="kitsu_settings">Mostrar pósters de Kitsu</string>
<string name="uprereleases_settings">Actualizar a las versiones preliminares</string>
<string name="uprereleases_settings_des">Buscar actualizaciones preliminares (beta) en lugar de solo versiones completas (stable releases)</string>
@ -249,7 +249,7 @@
<string name="subs_default_reset_toast">Reiniciar a valores predefinidos</string>
<string name="acra_report_toast">Lo sentimos, la aplicación se bloqueó. Se enviará un informe de error anónimo a los desarrolladores</string>
<string name="season">Temporada</string>
<string name="season_format">%s %d%s</string>
<string name="season_format">%1$s %2$d%3$s</string>
<string name="no_season">Ninguna Temporada</string>
<string name="season_short">T</string>
<string name="delete_file">Borrar Archivo</string>
@ -311,7 +311,7 @@
<string name="switch_account">Cambiar cuenta</string>
<string name="add_account">Añadir cuenta</string>
<string name="upload_sync">Sincronizar</string>
<string name="sync_score">Calificación</string>
<string name="sync_score">Clasificado</string>
<string name="authenticated_user" formatted="true">%s autenticado</string>
<string name="authenticated_user_fail" formatted="true">No se pudo autenticar a %s</string>
<string name="recommended">Recomendado</string>
@ -345,7 +345,7 @@
<string name="primary_color_settings">Color primario</string>
<string name="app_theme_settings">Tema de la aplicación</string>
<string name="example_email">hola@mundo.com</string>
<string name="login_format" formatted="true">%s %s</string>
<string name="login_format" formatted="true">%1$s %2$s</string>
<string name="all">Todo</string>
<string name="subtitle_offset_hint">1000ms</string>
<string name="subtitle_offset_extra_hint_none_format">Sin retraso de subtítulos</string>
@ -448,7 +448,7 @@
<string name="setup_done">Hecho</string>
<string name="plugin_loaded">Plugin Cargado</string>
<string name="is_adult">18+</string>
<string name="batch_download_start_format" formatted="true">Iniciada la descarga %d %s…</string>
<string name="batch_download_start_format" formatted="true">Comenzó la descarga de %1$d %2$s…</string>
<string name="batch_download">Descarga por lotes</string>
<string name="plugin_singular">plugin</string>
<string name="plugin">plugins</string>
@ -552,4 +552,30 @@
<string name="no_repository_found_error">Repositorio no encontrado, comprueba la URL y prueba la VPN</string>
<string name="already_voted">Ya has votado</string>
<string name="backup_frequency">Frecuencia de la copia de seguridad</string>
<string name="favorite_removed">%s eliminado de favoritos</string>
<string name="favorites_list_name">Favoritos</string>
<string name="favorite_added">%s añadido a favoritos</string>
<string name="duplicate_message_multiple" formatted="true">Se han encontrado posibles elementos duplicados en su biblioteca:
\n
\n%s
\n
\n¿Desea añadir este elemento de todos modos, sustituir los existentes o cancelar la acción\?</string>
<string name="duplicate_title">Posible duplicado encontrado</string>
<string name="lock_profile">Perfil de bloqueo</string>
<string name="action_add_to_favorites">Añadido a favoritos</string>
<string name="duplicate_replace_all">Sustituir todo</string>
<string name="pin_error_incorrect">PIN incorrecto. Por favor, inténtelo de nuevo.</string>
<string name="action_unsubscribe">Cancelar la suscripción</string>
<string name="pin_error_length">El PIN debe tener 4 caracteres</string>
<string name="duplicate_replace">Sustituir</string>
<string name="duplicate_add">Añadir</string>
<string name="action_subscribe">Suscríbase</string>
<string name="action_remove_from_favorites">Eliminar de favoritos</string>
<string name="select_an_account">Seleccione una cuenta</string>
<string name="duplicate_message_single">Parece que ya existe un elemento potencialmente duplicado en su biblioteca: \'%s.\'
\n
\n¿Desea añadir este elemento de todos modos, sustituir el existente o cancelar la acción\?</string>
<string name="enter_pin">Introducir el PIN</string>
<string name="pin">PIN</string>
<string name="enter_current_pin">Introduzca el PIN actual</string>
</resources>

View file

@ -20,7 +20,7 @@
<string name="player_speed_text_format" formatted="true">Velocità (%.2fx)</string>
<string name="rated_format" formatted="true">Valutato: %.1f</string>
<string name="new_update_format" formatted="true">Nuovo aggiornamento trovato!
\n%s -&gt; %s</string>
\n%1$s -&gt; %2$s</string>
<string name="filler" formatted="true">Filler</string>
<string name="duration_format" formatted="true">%d min</string>
<!-- <string name="app_name">CloudStream</string> -->
@ -409,8 +409,8 @@
<string name="plugin_deleted">Plugin eliminato</string>
<string name="plugin_load_fail" formatted="true">Impossibile caricare %s</string>
<string name="is_adult">18+</string>
<string name="batch_download_start_format" formatted="true">Download iniziato %d %s…</string>
<string name="batch_download_finish_format" formatted="true">Scaricato %d %s</string>
<string name="batch_download_start_format" formatted="true">Download iniziato %1$d %2$s…</string>
<string name="batch_download_finish_format" formatted="true">Scaricato %1$d %2$s</string>
<string name="batch_download_nothing_to_download_format" formatted="true">Tutti %s già scaricati</string>
<string name="batch_download">Download in blocco</string>
<string name="plugin_singular">plugin</string>
@ -573,4 +573,30 @@
<string name="automatic_plugin_download_mode_title">Seleziona la modalità per filtrare il download dei plugin</string>
<string name="disable">Disabilita</string>
<string name="already_voted">Hai già votato</string>
<string name="favorite_removed">%s rimosso dai preferiti</string>
<string name="favorites_list_name">Preferiti</string>
<string name="favorite_added">%s aggiunto ai preferiti</string>
<string name="duplicate_message_multiple" formatted="true">Dei possibili duplicati sono stati trovati nella tua libreria:
\n
\n%s
\n
\nVorresti aggiungere l\'oggetto alla libreria comunque, rimpiazzare l\'esistente, o cancellare l\'azione\?</string>
<string name="backup_frequency">Frequenza di backup</string>
<string name="duplicate_title">Trovato Possibile Duplicato</string>
<string name="action_add_to_favorites">Aggiungi ai preferiti</string>
<string name="duplicate_replace_all">Rimpiazza tutti</string>
<string name="pin_error_incorrect">PIN non corretto. Riprova.</string>
<string name="action_unsubscribe">Disiscriviti</string>
<string name="pin_error_length">Il PIN deve essere almeno di 4 caratteri</string>
<string name="duplicate_replace">Rimpiazza</string>
<string name="duplicate_add">Aggiungi</string>
<string name="action_subscribe">Iscriviti</string>
<string name="action_remove_from_favorites">Rimuovi dai preferiti</string>
<string name="select_an_account">Seleziona un Account</string>
<string name="duplicate_message_single">Sembra che un oggetto potenziale duplicato sia già presente nella tua libreria: \'%1$s.\'
\n
\nVorresti aggiungere l\'oggetto lo stesso, rimpiazzare l\'esistente, o cancellare l\'azione\?</string>
<string name="enter_pin">Inserisci PIN</string>
<string name="pin">PIN</string>
<string name="enter_current_pin">Inserisci PIN Corrente</string>
</resources>

View file

@ -9,16 +9,16 @@
<string name="cast_format" formatted="true">Актори: %s</string>
<string name="next_episode_format" formatted="true">Епізод %d вийде через</string>
<string name="result_poster_img_des">Poster</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%s Еп. %d</string>
<string name="next_episode_time_day_format" formatted="true">%dд %dгод %dхв</string>
<string name="next_episode_time_hour_format" formatted="true">%dгод %dхв</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%1$s Еп. %2$d</string>
<string name="next_episode_time_day_format" formatted="true">%1$dд %2$dгод %3$dхв</string>
<string name="next_episode_time_hour_format" formatted="true">%1$dгод %2$dхв</string>
<string name="next_episode_time_min_format" formatted="true">%dхв</string>
<string name="home_main_poster_img_des">Головний постер</string>
<string name="home_next_random_img_des">Наступний випадковий</string>
<string name="preview_background_img_des">Попередній перегляд фону</string>
<string name="player_speed_text_format" formatted="true">Швидкість (%.2fx)</string>
<string name="new_update_format" formatted="true">Знайдено нове оновлення!
\n%s &gt; %s</string>
\n%1$s &gt; %2$s</string>
<string name="title_search">Пошук</string>
<string name="title_downloads">Завантаження</string>
<string name="duration_format" formatted="true">%d хв</string>
@ -112,7 +112,7 @@
<string name="popup_play_file">Переглянути файл</string>
<string name="home_more_info">Детальніше</string>
<string name="filter_bookmarks">Фільтр закладок</string>
<string name="sort_clear">Очистити</string>
<string name="sort_clear">Очистити</string>
<string name="subtitles_settings">Налаштування субтитрів</string>
<string name="subs_background_color">Колір фону</string>
<string name="subs_subtitle_elevation">Висота субтитрів</string>
@ -167,7 +167,7 @@
<string name="subs_default_reset_toast">Скинути до значення за замовчуванням</string>
<string name="no_season">Немає сезону</string>
<string name="episodes">епізодів</string>
<string name="episode_format" formatted="true">%d %s</string>
<string name="episode_format" formatted="true">%1$d %2$s</string>
<string name="season_short">С</string>
<string name="episode_short">Е</string>
<string name="delete_file">Видалити файл</string>
@ -238,9 +238,9 @@
<string name="year">Рік</string>
<string name="go_forward_30">+30</string>
<string name="acra_report_toast">Вибачте, у застосунку стався збій. Анонімне повідомлення про помилку буде відправлено розробникам</string>
<string name="season_format">%s %d%s</string>
<string name="season_format">%1$s %2$d%3$s</string>
<string name="episode">Епізод</string>
<string name="episodes_range">%d-%d</string>
<string name="episodes_range">%1$d-%2$d</string>
<string name="no_episodes_found">Епізодів не знайдено</string>
<string name="pause">Пауза</string>
<string name="season">Сезон</string>
@ -364,7 +364,7 @@
<string name="category_ui">Макет</string>
<string name="category_providers">Постачальники</string>
<string name="example_site_url">example.com</string>
<string name="login_format" formatted="true">%s %s</string>
<string name="login_format" formatted="true">%2$s %1$s</string>
<string name="subtitles_depressed">Депресивний</string>
<string name="account">обліковий запис</string>
<string name="create_account">Створити</string>
@ -414,8 +414,8 @@
<string name="plugin_loaded">Плагін завантажено</string>
<string name="plugin_downloaded">Плагін завантажено</string>
<string name="plugin_load_fail" formatted="true">Не вдалося завантажити %s</string>
<string name="batch_download_start_format" formatted="true">Почалося завантаження %d %s…</string>
<string name="batch_download_finish_format" formatted="true">Завантажено %d %s</string>
<string name="batch_download_start_format" formatted="true">Почалося завантаження %1$d %2$s…</string>
<string name="batch_download_finish_format" formatted="true">Завантажено %1$d %2$s</string>
<string name="batch_download_nothing_to_download_format" formatted="true">Всі %s вже завантажено</string>
<string name="batch_download">Завантажити пакети</string>
<string name="plugin_singular">плагін</string>
@ -499,7 +499,7 @@
<string name="empty_library_no_accounts_message">Ваша бібліотека порожня :(
\nУвійдіть в обліковий запис бібліотеки або додайте фільми до вашої локальної бібліотеки.</string>
<string name="sort_alphabetical_z">Алфавітом (від Я до А)</string>
<string name="select_library">Виберіть бібліотеку</string>
<string name="select_library">Оберіть бібліотеку</string>
<string name="open_with">Відкрити</string>
<string name="browser">Браузер</string>
<string name="empty_library_logged_in_message">Цей список порожній. Спробуйте перейти до іншого.</string>
@ -546,10 +546,36 @@
<string name="qualities">Якості</string>
<string name="profile_background_des">Фон профілю</string>
<string name="unable_to_inflate">Не вдалося створити UI коректно, це ВАЖЛИВА ПОМИЛКА, про яку слід негайно повідомити %s</string>
<string name="automatic_plugin_download_mode_title">Виберіть режим для фільтрації завантаження плагінів</string>
<string name="automatic_plugin_download_mode_title">Оберіть режим для фільтрації завантаження плагінів</string>
<string name="disable">Вимкнути</string>
<string name="no_repository_found_error">Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN</string>
<string name="no_plugins_found_error">Не знайдено жодних плагінів у репозиторії</string>
<string name="already_voted">Ви вже проголосували</string>
<string name="backup_frequency">Частота резервного копіювання</string>
<string name="favorite_removed">%s вилучено з обраного</string>
<string name="favorites_list_name">Обране</string>
<string name="favorite_added">%s додано до обраного</string>
<string name="duplicate_message_multiple" formatted="true">У вашій бібліотеці знайдено потенційні дублікати:
\n
\n%s
\n
\nВсе одно хочете додати цей елемент, замінити наявні чи скасувати дію\?</string>
<string name="duplicate_title">Знайдено потенційний дублікат</string>
<string name="lock_profile">Розблокувати профіль</string>
<string name="action_add_to_favorites">Додати до обраного</string>
<string name="duplicate_replace_all">Замінити усе</string>
<string name="pin_error_incorrect">Неправильний PIN-код. Спробуйте ще раз.</string>
<string name="action_unsubscribe">Відписатись</string>
<string name="pin_error_length">PIN-код повинен складатися з 4 символів</string>
<string name="duplicate_replace">Замінити</string>
<string name="duplicate_add">Додати</string>
<string name="action_subscribe">Підписатись</string>
<string name="action_remove_from_favorites">Вилучити з обраного</string>
<string name="select_an_account">Оберіть обліковий запис</string>
<string name="duplicate_message_single">Виявилося, що у вашій бібліотеці вже є потенційно повторюваний елемент: \'%1$s.\'
\n
\nВсе одно хочете додати цей елемент, замінити наявний чи скасувати дію\?</string>
<string name="enter_pin">Введіть PIN-код</string>
<string name="pin">PIN-код</string>
<string name="enter_current_pin">Введіть поточний PIN-код</string>
</resources>

View file

@ -409,8 +409,8 @@
<string name="plugin_deleted">Plugin đã xoá</string>
<string name="plugin_load_fail" formatted="true">Không tải được %s</string>
<string name="is_adult">18+</string>
<string name="batch_download_start_format" formatted="true">Bắt đầu tải %d %s…</string>
<string name="batch_download_finish_format" formatted="true">Tải xuống %d %s thành công</string>
<string name="batch_download_start_format" formatted="true">Đã bắt đầu tải xuống %1$d %2$s…</string>
<string name="batch_download_finish_format" formatted="true">Đã tải xuống %1$d %2$s</string>
<string name="batch_download_nothing_to_download_format" formatted="true">Toàn bộ %s đã được tải xuống</string>
<string name="batch_download">Tải hàng loạt</string>
<string name="plugin_singular">plugin</string>
@ -566,4 +566,31 @@
<string name="no_plugins_found_error">Không tìm thấy plugin</string>
<string name="unable_to_inflate">Không thể khởi tạo UI, đây là một LỖI LỚN và cần được báo cáo ngay lập tức tới %s</string>
<string name="automatic_plugin_download_mode_title">Chọn chế độ để lọc plugin tải xuống</string>
<string name="favorite_removed">%s đã loại bỏ khỏi mục yêu thích</string>
<string name="favorites_list_name">Yêu thích</string>
<string name="favorite_added">%s đã thêm vào mục yêu thích</string>
<string name="duplicate_message_multiple" formatted="true">Các mục có thể trùng lặp đã được tìm thấy trong thư viện của bạn:
\n
\n%s
\n
\nBạn vẫn muốn thêm mục này, thay thế những mục hiện có hay hủy hành động\?</string>
<string name="backup_frequency">Tần suất sao lưu</string>
<string name="duplicate_title">Đã tìm thấy bản sao tiềm năng</string>
<string name="lock_profile">Khóa hồ sơ</string>
<string name="action_add_to_favorites">Thêm vào mục yêu thích</string>
<string name="duplicate_replace_all">Thay thế tất cả</string>
<string name="pin_error_incorrect">Mã PIN không chính xác. Vui lòng thử lại.</string>
<string name="action_unsubscribe">Hủy đăng ký</string>
<string name="pin_error_length">Mã PIN phải có 4 ký tự</string>
<string name="duplicate_replace">Thay thế</string>
<string name="duplicate_add">Thêm vào</string>
<string name="action_subscribe">Đăng ký</string>
<string name="action_remove_from_favorites">Loại bỏ khỏi mục yêu thích</string>
<string name="select_an_account">Chọn một tài khoản</string>
<string name="duplicate_message_single">Có vẻ như một mục có khả năng trùng lặp đã tồn tại trong thư viện của bạn: \'%1$s.\'
\n
\nBạn vẫn muốn thêm mục này, thay thế mục hiện có hay hủy hành động\?</string>
<string name="enter_pin">Nhập PIN</string>
<string name="pin">PIN</string>
<string name="enter_current_pin">Nhập mã PIN hiện tại</string>
</resources>

View file

@ -20,4 +20,6 @@
<dimen name="download_size">50dp</dimen>
<dimen name="video_frame_width">1dp</dimen>
<dimen name="account_select_linear_item_size">100dp</dimen>
</resources>

View file

@ -65,6 +65,7 @@
<string name="filter_sub_lang_key" translatable="false">filter_sub_lang_key</string>
<string name="pref_filter_search_quality_key" translatable="false">pref_filter_search_quality_key</string>
<string name="enable_nsfw_on_providers_key" translatable="false">enable_nsfw_on_providers_key</string>
<string name="skip_startup_account_select_key" translatable="false">skip_startup_account_select_key</string>
<string name="enable_skip_op_from_database" translatable="false">enable_skip_op_from_database</string>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string name="extra_info_format" formatted="true" translatable="false">%d %s | %s</string>
@ -714,9 +715,9 @@
<string name="duplicate_add">Add</string>
<string name="duplicate_replace">Replace</string>
<string name="duplicate_replace_all">Replace All</string>
<string name="duplicate_cancel" translatable="false">@string/sort_cancel</string>
<string name="duplicate_message_single">
It appears that a potentially duplicate item already exists in your library: \'%1$s.\'
<string name="duplicate_cancel" translatable="false">@string/cancel</string>
<string name="duplicate_message_single" formatted="true">
It appears that a potentially duplicate item already exists in your library: \'%s.\'
\n\nWould you like to add this item anyway, replace the existing one, or cancel the action?
</string>
@ -731,10 +732,16 @@
<string name="tv_no_focus_tag" translatable="false">tv_no_focus_tag</string>
<string name="enter_pin">Enter PIN</string>
<string name="enter_pin_with_name" formatted="true">Enter PIN for %s</string>
<string name="enter_current_pin">Enter Current PIN</string>
<string name="lock_profile">Lock Profile</string>
<string name="pin">PIN</string>
<string name="pin_error_incorrect">Incorrect PIN. Please try again.</string>
<string name="pin_error_length">PIN must be 4 characters</string>
<string name="select_an_account">Select an Account</string>
<string name="manage_accounts">Manage Accounts</string>
<string name="edit_account">Edit account</string>
<string name="logged_account" formatted="true">Logged in as %s</string>
<string name="skip_startup_account_select_pref">Skip account selection at startup</string>
<string name="use_default_account">Use Default Account</string>
</resources>

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
</data-extraction-rules>

View file

@ -2,9 +2,11 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="@string/settings_category_plugins">
<Preference
android:key="@string/mal_key"
android:icon="@drawable/mal_logo" />
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_outline_account_circle_24"
android:key="@string/skip_startup_account_select_key"
android:title="@string/skip_startup_account_select_pref" />
<Preference
android:key="@string/anilist_key"

View file

@ -5,10 +5,9 @@ buildscript {
mavenCentral()
}
dependencies {
// we stay on low ver because prerelease build gradle is fucked
classpath("com.android.tools.build:gradle:7.3.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20")
classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.5.0")
classpath("com.android.tools.build:gradle:8.1.3")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle.kts files
@ -22,6 +21,10 @@ allprojects {
}
}
tasks.register("clean", Delete::class) {
delete(rootProject.buildDir)
plugins {
id("com.google.devtools.ksp") version "1.9.20-1.0.14" apply false
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View file

@ -0,0 +1 @@
- ¡Cambios añadidos!

View file

@ -0,0 +1,10 @@
CloudStream-3 te permite ver y descargar películas, series de TV y anime.
La aplicación viene sin ningún tipo de anuncios y análisis y
soporta múltiples tráilers y páginas de películas, y más, por ejemplo
Marcadores
Descargas de subtítulos
Compatible con Chromecast

View file

@ -0,0 +1 @@
Vea y descargue películas, series de televisión y anime.

View file

@ -0,0 +1 @@
CloudStream

View file

@ -1,6 +1,6 @@
#Fri Apr 30 17:11:15 CEST 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME