Merge branch 'recloudstream:master' into master

This commit is contained in:
CranberrySoup 2023-11-15 01:15:54 +00:00 committed by GitHub
commit 04ecd15870
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
138 changed files with 3069 additions and 1232 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,10 +1,12 @@
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
plugins {
id("com.android.application")
id("com.google.devtools.ksp")
id("kotlin-android")
id("kotlin-kapt")
id("org.jetbrains.dokka")
@ -50,14 +52,15 @@ android {
}
}
// https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading
compileSdk = 33 // android 14 is fucked
compileSdk = 34
buildToolsVersion = "34.0.0"
defaultConfig {
applicationId = "com.lagradost.cloudstream3"
minSdk = 21
targetSdk = 33
// https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading
targetSdk = 33 // android 14 is fucked
versionCode = 62
versionName = "4.2.1"
@ -86,6 +89,11 @@ android {
)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("exportSchema", "true")
}
kapt {
includeCompileClasspath = true
}
@ -126,10 +134,6 @@ android {
versionCode = (System.currentTimeMillis() / 60000).toInt()
}
}
//toolchain {
// languageVersion.set(JavaLanguageVersion.of(17))
// }
// jvmToolchain(17)
compileOptions {
isCoreLibraryDesugaringEnabled = true
@ -137,10 +141,6 @@ android {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf("-Xjvm-default=compatibility")
}
lint {
abortOnError = false
checkReleaseBuilds = false
@ -157,18 +157,16 @@ dependencies {
implementation("androidx.test.ext:junit-ktx:1.1.5")
testImplementation("org.json:json:20230618")
implementation("androidx.core:core-ktx:1.10.1") // need 34 for higher
implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
// dont change this to 1.6.0 it looks ugly af
implementation("com.google.android.material:material:1.5.0")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
// need 34 for higher
implementation("androidx.navigation:navigation-fragment-ktx:2.6.0")
implementation("androidx.navigation:navigation-ui-ktx:2.6.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.5")
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")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
@ -180,11 +178,15 @@ dependencies {
// DONT UPDATE, WILL CRASH ANDROID TV ????
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
implementation("androidx.preference:preference-ktx:1.2.0")
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")
implementation("com.github.bumptech.glide:glide:4.15.1")
ksp("com.github.bumptech.glide:ksp:4.15.1")
implementation("com.github.bumptech.glide:okhttp3-integration:4.15.1")
// for ksp
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
implementation("com.google.guava:guava:32.1.2-android")
implementation("jp.wasabeef:glide-transformations:4.3.0")
@ -208,9 +210,6 @@ dependencies {
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")
@ -232,7 +231,7 @@ dependencies {
// 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.3")
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 🙏
@ -277,7 +276,13 @@ 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 {

View file

@ -19,6 +19,7 @@ import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
import com.lagradost.cloudstream3.databinding.HomepageParentEmulatorBinding
import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding
@ -119,8 +120,9 @@ class ExampleInstrumentedTest {
// testAllLayouts<HomeScrollViewBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
// testAllLayouts<HomeScrollViewTvBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
testAllLayouts<HomepageParentEmulatorBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
testAllLayouts<FragmentLibraryTvBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
testAllLayouts<FragmentLibraryBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)

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"
@ -96,12 +102,6 @@
android:launchMode="singleTask"
android:resizeableActivity="true"
android:supportsPictureInPicture="true">
<intent-filter android:exported="true">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
<intent-filter>
@ -165,6 +165,21 @@
</intent-filter>
</activity>
<activity
android:name=".ui.account.AccountSelectActivity"
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" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ui.EasterEggMonke"
android:exported="true" />
@ -172,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

@ -1246,6 +1246,18 @@ interface LoadResponse {
return this.syncData[aniListIdPrefix]
}
fun LoadResponse.getImdbId(): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb)
}
}
fun LoadResponse.getTMDbId(): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb)
}
}
fun LoadResponse.addMalId(id: Int?) {
this.syncData[malIdPrefix] = (id ?: return).toString()
this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString())
@ -1453,6 +1465,15 @@ interface EpisodeResponse {
var nextAiring: NextAiring?
var seasonNames: List<SeasonData>?
fun getLatestEpisodes(): Map<DubStatus, Int?>
/** Count all episodes in all previous seasons up until this episode to get a total count.
* Example:
* Season 1: 10 episodes.
* Season 2: 6 episodes.
*
* getTotalEpisodeIndex(episode = 3, season = 2) -> 10 + 3 = 13
* */
fun getTotalEpisodeIndex(episode: Int, season: Int): Int
}
@JvmName("addSeasonNamesString")
@ -1532,6 +1553,12 @@ data class AnimeLoadResponse(
.takeUnless { it == Int.MIN_VALUE }
}.toMap()
}
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
return this.episodes.maxOf { (_, episodes) ->
episodes.count { ((it.season ?: Int.MIN_VALUE) < season) && it.season != 0 }
} + episode
}
}
/**
@ -1740,6 +1767,12 @@ data class TvSeriesLoadResponse(
.takeUnless { it == Int.MIN_VALUE }
return mapOf(DubStatus.None to max)
}
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
return episodes.count {
(it.season ?: Int.MIN_VALUE) < season && it.season != 0
} + episode
}
}
suspend fun MainAPI.newTvSeriesLoadResponse(

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
@ -132,7 +133,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
@ -309,9 +309,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>()
/**
@ -650,34 +654,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 && isTrueTvSettings()) {
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"
@ -1087,6 +1063,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} catch (_: Throwable) {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
@ -1306,7 +1283,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this@MainActivity.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
viewModel.updateWatchStatus(WatchType.values()[it])
viewModel.updateWatchStatus(WatchType.values()[it], this@MainActivity)
}
}
@ -1384,6 +1361,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)
@ -1598,6 +1581,44 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// }
// }
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,73 +0,0 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import com.lagradost.cloudstream3.utils.SyncUtil
// wont be implemented
class MultiAnimeProvider : MainAPI() {
override var name = "MultiAnime"
override var lang = "en"
override val usesWebView = true
override val supportedTypes = setOf(TvType.Anime)
private val syncApi: SyncAPI = aniListApi
private val syncUtilType by lazy {
when (syncApi) {
is AniListApi -> "anilist"
is MALApi -> "myanimelist"
else -> throw ErrorLoadingException("Invalid Api")
}
}
private val validApis
get() =
synchronized(APIHolder.apis) {
APIHolder.apis.filter {
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
TvType.Anime
)
}
}
private fun filterName(name: String): String {
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
}
override suspend fun search(query: String): List<SearchResponse>? {
return syncApi.search(query)?.map {
AnimeSearchResponse(it.name, it.url, this.name, TvType.Anime, it.posterUrl)
}
}
override suspend fun load(url: String): LoadResponse? {
return syncApi.getResult(url)?.let { res ->
val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).amap { url ->
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
}.filterNotNull()
val type =
if (data.any { it.type == TvType.AnimeMovie }) TvType.AnimeMovie else TvType.Anime
newAnimeLoadResponse(
res.title ?: throw ErrorLoadingException("No Title found"),
url,
type
) {
posterUrl = res.posterUrl
plot = res.synopsis
tags = res.genres
rating = res.publicScore
addTrailer(res.trailers)
addAniListId(res.id.toIntOrNull())
recommendations = res.recommendations
}
}
}
}

View file

@ -8,7 +8,9 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
@ -69,29 +71,52 @@ class LocalList : SyncAPI {
}?.distinctBy { it.first } ?: return null
val list = ioWork {
watchStatusIds.groupBy {
it.second.stringRes
}.mapValues { group ->
val isTrueTv = isTrueTvSettings()
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
// None is not something to display
it.stringRes to emptyList<SyncAPI.LibraryItem>()
} + mapOf(
R.string.favorites_list_name to emptyList()
) + if (!isTrueTv) {
mapOf(
R.string.subscription_list_name to emptyList()
)
} else {
emptyMap()
}
val watchStatusMap = watchStatusIds.groupBy { it.second.stringRes }.mapValues { group ->
group.value.mapNotNull {
getBookmarkedData(it.first)?.toLibraryItem(it.first.toString())
}
} + mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
}
val favoritesMap = mapOf(R.string.favorites_list_name to getAllFavorites().mapNotNull {
it.toLibraryItem()
})
// Don't show subscriptions on TV
val result = if (isTrueTv) {
baseMap + watchStatusMap + favoritesMap
} else {
val subscriptionsMap = mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
it.toLibraryItem()
})
baseMap + watchStatusMap + subscriptionsMap + favoritesMap
}
result
}
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
// None is not something to display
it.stringRes to emptyList<SyncAPI.LibraryItem>()
} + mapOf(R.string.subscription_list_name to emptyList())
return SyncAPI.LibraryMetadata(
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
list.map { SyncAPI.LibraryList(txt(it.key), it.value) },
setOf(
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
// ListSorting.UpdatedNew,
// ListSorting.UpdatedOld,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
// ListSorting.RatingHigh,
// ListSorting.RatingLow,
)

View file

@ -203,7 +203,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
}
/** Read the id string to get all other ids */
private fun readIdFromString(idString: String?): Map<SyncServices, String> {
fun readIdFromString(idString: String?): Map<SyncServices, String> {
return tryParseJson(idString) ?: return emptyMap()
}
@ -376,6 +376,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
private var status: Int? = null,
private var addEpisodes: Pair<List<MediaObject.Season>?, List<MediaObject.Season.Episode>?>? = null,
private var removeEpisodes: Pair<List<MediaObject.Season>?, List<MediaObject.Season.Episode>?>? = null,
// Required for knowing if the status should be overwritten
private var onList: Boolean = false
) {
fun interceptor(interceptor: Interceptor) = apply { this.interceptor = interceptor }
fun apiUrl(url: String) = apply { this.url = url }
@ -387,6 +389,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
}
fun status(newStatus: Int?, oldStatus: Int?) = apply {
onList = oldStatus != null
// Only set status if its new
if (newStatus != oldStatus) {
this.status = newStatus
@ -412,6 +415,11 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
// Do not add episodes if there is no change
if (newEpisodes > (oldEpisodes ?: 0)) {
this.addEpisodes = getEpisodes(allEpisodes.take(newEpisodes))
// Set to watching if episodes are added and there is no current status
if (!onList) {
status = SimklListStatusType.Watching.value
}
}
if ((oldEpisodes ?: 0) > newEpisodes) {
this.removeEpisodes = getEpisodes(allEpisodes.drop(newEpisodes))
@ -431,6 +439,28 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
interceptor = interceptor
).isSuccessful
} else {
val statusResponse = status?.let { setStatus ->
val newStatus =
SimklListStatusType.values()
.firstOrNull { it.value == setStatus }?.originalName
?: SimklListStatusType.Watching.originalName!!
app.post(
"${this.url}/sync/add-to-list",
json = StatusRequest(
shows = listOf(
StatusMediaObject(
null,
null,
ids,
newStatus,
)
), movies = emptyList()
),
interceptor = interceptor
).isSuccessful
} ?: true
val episodeRemovalResponse = removeEpisodes?.let { (seasons, episodes) ->
app.post(
"${this.url}/sync/history/remove",
@ -472,28 +502,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
true
}
val statusResponse = status?.let { setStatus ->
val newStatus =
SimklListStatusType.values()
.firstOrNull { it.value == setStatus }?.originalName
?: SimklListStatusType.Watching.originalName!!
app.post(
"${this.url}/sync/add-to-list",
json = StatusRequest(
shows = listOf(
StatusMediaObject(
null,
null,
ids,
newStatus,
)
), movies = emptyList()
),
interceptor = interceptor
).isSuccessful
} ?: true
statusResponse && episodeRemovalResponse && historyResponse
}
}

View file

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

@ -0,0 +1,198 @@
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 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>() {
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?) {
when (binding) {
is AccountListItemBinding -> 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)
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 = {}
)
}
}
}
}
}
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")
}
)
override fun onBindViewHolder(holder: AccountViewHolder, position: Int) {
holder.bind(accounts.getOrNull(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.count() + 1
}
}

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

@ -0,0 +1,168 @@
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 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.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.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)
loadThemes(this)
window.navigationBarColor = colorFromAttribute(R.attr.primaryBlackBackground)
// Are we editing and coming from MainActivity?
val isEditingFromMainActivity = intent.getBooleanExtra(
"isEditingFromMainActivity",
false
)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val skipStartup = settingsManager.getBoolean(
getString(R.string.skip_startup_account_select_key),
false
) || accounts.count() <= 1
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
))
}
navigateToMainActivity()
}
return
}
CommonActivity.init(this)
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() {
val mainIntent = Intent(this, MainActivity::class.java)
startActivity(mainIntent)
finish() // Finish the account selection activity
}
}

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

@ -15,7 +15,8 @@ import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
class LoadClickCallback(
@ -34,11 +35,13 @@ open class ParentItemAdapter(
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val root = LayoutInflater.from(parent.context).inflate(
if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent,
parent,
false
)
val layoutResId = when {
isTrueTvSettings() -> R.layout.homepage_parent_tv
parent.context.isEmulatorSettings() -> R.layout.homepage_parent_emulator
else -> R.layout.homepage_parent
}
val root = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
val binding = HomepageParentBinding.bind(root)
@ -234,7 +237,7 @@ open class ParentItemAdapter(
})
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
if (!isTvSettings()) {
if (!isTrueTvSettings()) {
title.setOnClickListener {
moreInfoClickCallback.invoke(expand)
}

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
@ -35,6 +36,7 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
@ -81,6 +83,28 @@ class HomeParentItemAdapterPreview(
parent,
false
) else FragmentHomeHeadBinding.inflate(inflater, parent, false)
if (binding is FragmentHomeHeadTvBinding && parent.context.isEmulatorSettings()) {
binding.homeBookmarkParentItemMoreInfo.isVisible = true
val marginInDp = 50
val density = binding.horizontalScrollChips.context.resources.displayMetrics.density
val marginInPixels = (marginInDp * density).toInt()
val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams
params.marginEnd = marginInPixels
binding.horizontalScrollChips.layoutParams = params
binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds(
null,
null,
ContextCompat.getDrawable(
parent.context,
R.drawable.ic_baseline_arrow_forward_24
),
null
)
}
HeaderViewHolder(
binding,
viewModel,
@ -355,21 +379,25 @@ class HomeParentItemAdapterPreview(
showApply = false,
{}) {
val newValue = WatchType.values()[it]
homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds(
null,
ContextCompat.getDrawable(
homePreviewBookmark.context,
newValue.iconRes
),
null,
null
)
homePreviewBookmark.setText(newValue.stringRes)
ResultViewModel2.updateWatchStatus(
item,
newValue
)
ResultViewModel2().updateWatchStatus(
newValue,
fab.context,
item
) { statusChanged: Boolean ->
if (!statusChanged) return@updateWatchStatus
homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds(
null,
ContextCompat.getDrawable(
homePreviewBookmark.context,
newValue.iconRes
),
null,
null
)
homePreviewBookmark.setText(newValue.stringRes)
}
}
}
}
@ -450,8 +478,8 @@ class HomeParentItemAdapterPreview(
}
}
homeAccount?.setOnClickListener { v ->
DataStoreHelper.showWhoIsWatching(v?.context ?: return@setOnClickListener)
homeAccount?.setOnClickListener {
activity?.showAccountSelectLinear()
}
(binding as? FragmentHomeHeadTvBinding)?.apply {
@ -553,12 +581,19 @@ class HomeParentItemAdapterPreview(
resumeHolder.isVisible = resumeWatching.isNotEmpty()
resumeAdapter.updateList(resumeWatching)
if (binding is FragmentHomeHeadBinding) {
binding.homeWatchParentItemTitle.setOnClickListener {
if (
binding is FragmentHomeHeadBinding ||
binding is FragmentHomeHeadTvBinding &&
binding.root.context.isEmulatorSettings()
) {
val title = (binding as? FragmentHomeHeadBinding)?.homeWatchParentItemTitle
?: (binding as? FragmentHomeHeadTvBinding)?.homeWatchParentItemTitle
title?.setOnClickListener {
viewModel.popup(
HomeViewModel.ExpandableHomepageList(
HomePageList(
binding.homeWatchParentItemTitle.text.toString(),
title.text.toString(),
resumeWatching,
false
), 1, false
@ -576,8 +611,15 @@ class HomeParentItemAdapterPreview(
bookmarkHolder.isVisible = visible
bookmarkAdapter.updateList(list)
if (binding is FragmentHomeHeadBinding) {
binding.homeBookmarkParentItemTitle.setOnClickListener {
if (
binding is FragmentHomeHeadBinding ||
binding is FragmentHomeHeadTvBinding &&
binding.root.context.isEmulatorSettings()
) {
val title = (binding as? FragmentHomeHeadBinding)?.homeBookmarkParentItemTitle
?: (binding as? FragmentHomeHeadTvBinding)?.homeBookmarkParentItemTitle
title?.setOnClickListener {
val items = toggleList.map { it.first }.filter { it.isChecked }
if (items.isEmpty()) return@setOnClickListener // we don't want to show an empty dialog
val textSum = items

View file

@ -40,7 +40,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
@ -102,11 +101,6 @@ class HomeViewModel : ViewModel() {
loadStoredData()
}
fun deleteBookmarks() {
deleteAllBookmarkedData()
loadStoredData()
}
var repo: APIRepository? = null
private val _apiName = MutableLiveData<String>()

View file

@ -8,12 +8,14 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.FOCUS_AFTER_DESCENDANTS
import android.view.ViewGroup.FOCUS_BLOCK_DESCENDANTS
import android.view.animation.AlphaAnimation
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
@ -25,6 +27,7 @@ import androidx.fragment.app.Fragment
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
@ -60,7 +63,7 @@ const val LIBRARY_FOLDER = "library_folder"
enum class LibraryOpenerType(@StringRes val stringRes: Int) {
Default(R.string.default_subtitles), // TODO FIX AFTER MERGE
Default(R.string.action_default),
Provider(R.string.none),
Browser(R.string.browser),
Search(R.string.search),
@ -135,8 +138,19 @@ 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
val searchExitIcon = binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
val searchExitIconColor = TypedValue()
activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
searchExitIcon?.setColorFilter(searchExitIconColor.data)
binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
libraryViewModel.sort(ListSorting.Query, query)
@ -333,6 +347,7 @@ class LibraryFragment : Fragment() {
binding?.apply {
viewpager.offscreenPageLimit = 2
viewpager.reduceDragSensitivity()
searchBar.setExpanded(true)
}
val startLoading = Runnable {
@ -432,6 +447,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
@ -38,6 +39,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
@ -100,10 +102,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>) {
@ -448,7 +473,8 @@ class GeneratorPlayer : FullScreenPlayer() {
url = url,
origin = SubtitleOrigin.URL,
mimeType = url.toSubtitleMimeType(),
headers = currentSubtitle.headers
headers = currentSubtitle.headers,
currentSubtitle.lang
)
runOnMainThread {
addAndSelectSubtitles(subtitle)
@ -536,7 +562,8 @@ class GeneratorPlayer : FullScreenPlayer() {
uri.toString(),
SubtitleOrigin.DOWNLOADED_FILE,
name.toSubtitleMimeType(),
emptyMap()
emptyMap(),
null
)
addAndSelectSubtitles(subtitleData)
@ -946,7 +973,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
@ -1023,7 +1050,7 @@ class GeneratorPlayer : FullScreenPlayer() {
ctx.getString(R.string.episode_sync_enabled_key), true
)
) maxEpisodeSet = meta.episode
sync.modifyMaxEpisode(meta.episode)
sync.modifyMaxEpisode(meta.totalEpisodeIndex ?: meta.episode)
}
}
@ -1209,7 +1236,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

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -215,10 +216,16 @@ class QuickSearchFragment : Fragment() {
binding?.quickSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
//val searchMagIcon =
// binding.quickSearch.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
// binding?.quickSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
//searchMagIcon?.scaleX = 0.65f
//searchMagIcon?.scaleY = 0.65f
// searchMagIcon?.scaleX = 0.65f
// searchMagIcon?.scaleY = 0.65f
// Set the color for the search exit icon to the correct theme text color
val searchExitIconColor = TypedValue()
activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
searchExitIcon?.setColorFilter(searchExitIconColor.data)
binding?.quickSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {

View file

@ -47,7 +47,9 @@ data class ResultEpisode(
/**
* Conveys if the episode itself is marked as watched
**/
val videoWatchState: VideoWatchState
val videoWatchState: VideoWatchState,
/** Sum of all previous season episode counts + episode */
val totalEpisodeIndex: Int? = null,
)
fun ResultEpisode.getRealPosition(): Long {
@ -82,6 +84,7 @@ fun buildResultEpisode(
isFiller: Boolean? = null,
tvType: TvType,
parentId: Int,
totalEpisodeIndex: Int? = null,
): ResultEpisode {
val posDur = getViewPos(id)
val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None
@ -103,7 +106,8 @@ fun buildResultEpisode(
isFiller,
tvType,
parentId,
videoWatchState
videoWatchState,
totalEpisodeIndex
)
}

View file

@ -17,7 +17,6 @@ import android.view.animation.DecelerateInterpolator
import android.widget.AbsListView
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
@ -66,6 +65,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
@ -430,20 +430,36 @@ open class ResultFragmentPhone : FullScreenPlayer() {
}
})
resultSubscribe.setOnClickListener {
val isSubscribed =
viewModel.toggleSubscriptionStatus() ?: return@setOnClickListener
viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleSubscriptionStatus
val message = if (isSubscribed) {
// Kinda icky to have this here, but it works.
SubscriptionWorkManager.enqueuePeriodicWork(context)
R.string.subscription_new
} else {
R.string.subscription_deleted
val message = if (newStatus) {
// Kinda icky to have this here, but it works.
SubscriptionWorkManager.enqueuePeriodicWork(context)
R.string.subscription_new
} else {
R.string.subscription_deleted
}
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: txt(R.string.no_data).asStringNull(context) ?: ""
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
}
}
resultFavorite.setOnClickListener {
viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleFavoriteStatus
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: txt(R.string.no_data).asStringNull(context) ?: ""
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
val message = if (newStatus) {
R.string.favorite_added
} else {
R.string.favorite_removed
}
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: txt(R.string.no_data).asStringNull(context) ?: ""
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
}
}
mediaRouteButton.apply {
val chromecastSupport = api?.hasChromecastSupport == true
@ -564,6 +580,19 @@ open class ResultFragmentPhone : FullScreenPlayer() {
binding?.resultSubscribe?.setImageResource(drawable)
}
observeNullable(viewModel.favoriteStatus) { isFavorite ->
binding?.resultFavorite?.isVisible = isFavorite != null
if (isFavorite == null) return@observeNullable
val drawable = if (isFavorite) {
R.drawable.ic_baseline_favorite_24
} else {
R.drawable.ic_baseline_favorite_border_24
}
binding?.resultFavorite?.setImageResource(drawable)
}
observe(viewModel.trailers) { trailers ->
setTrailers(trailers.flatMap { it.mirros }) // I dont care about subtitles yet!
}
@ -654,14 +683,13 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultPoster.setImage(d.posterImage)
resultPosterBackground.setImage(d.posterBackgroundImage)
resultDescription.setTextHtml(d.plotText)
resultDescription.setOnClickListener { view ->
// todo bottom view?
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
builder.setMessage(d.plotText.asString(ctx).html())
.setTitle(d.plotHeaderText.asString(ctx))
.show()
resultDescription.setOnClickListener {
activity?.let { activity ->
activity.showBottomDialogText(
d.titleText.asString(activity),
d.plotText.asString(activity).html(),
{}
)
}
}
@ -852,16 +880,11 @@ open class ResultFragmentPhone : FullScreenPlayer() {
setRecommendations(recommendations, null)
}
observe(viewModel.episodeSynopsis) { description ->
// TODO bottom dialog
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
builder.setMessage(description.html())
.setTitle(R.string.synopsis)
.setOnDismissListener {
viewModel.releaseEpisodeSynopsis()
}
.show()
activity?.let { activity ->
activity.showBottomDialogText(
activity.getString(R.string.synopsis),
description.html()
) { viewModel.releaseEpisodeSynopsis() }
}
}
context?.let { ctx ->
@ -939,7 +962,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
viewModel.updateWatchStatus(WatchType.values()[it])
viewModel.updateWatchStatus(WatchType.values()[it], context)
}
}
}

View file

@ -8,6 +8,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.core.view.isVisible
@ -17,6 +18,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
@ -26,6 +28,7 @@ import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup
import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator
@ -35,6 +38,7 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
@ -265,6 +269,7 @@ class ResultFragmentTv : Fragment() {
resultEpisodesShow.onFocusChangeListener = rightListener
resultDescription.onFocusChangeListener = leftListener
resultBookmarkButton.onFocusChangeListener = leftListener
resultFavoriteButton.onFocusChangeListener = leftListener
resultEpisodesShow.setOnClickListener {
// toggle, to make it more touch accessable just in case someone thinks that a
// tv layout is better but is using a touch device
@ -283,7 +288,9 @@ class ResultFragmentTv : Fragment() {
resultPlaySeries,
resultResumeSeries,
resultPlayTrailer,
resultBookmarkButton
resultBookmarkButton,
resultFavoriteButton,
resultSubscribeButton
)
for (requestView in views) {
if (!requestView.isVisible) continue
@ -424,6 +431,8 @@ class ResultFragmentTv : Fragment() {
val aboveCast = listOf(
binding?.resultEpisodesShow,
binding?.resultBookmarkButton,
binding?.resultFavoriteButton,
binding?.resultSubscribeButton,
).firstOrNull {
it?.isVisible == true
}
@ -526,7 +535,83 @@ class ResultFragmentTv : Fragment() {
view.context.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
viewModel.updateWatchStatus(WatchType.values()[it])
viewModel.updateWatchStatus(WatchType.values()[it], context)
}
}
}
}
observeNullable(viewModel.favoriteStatus) { isFavorite ->
binding?.resultFavoriteButton?.apply {
isVisible = isFavorite != null
if (isFavorite == null) return@observeNullable
val drawable = if (isFavorite) {
R.drawable.ic_baseline_favorite_24
} else {
R.drawable.ic_baseline_favorite_border_24
}
val text = if (isFavorite) {
R.string.action_remove_from_favorites
} else {
R.string.action_add_to_favorites
}
setIconResource(drawable)
setText(text)
setOnClickListener {
viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleFavoriteStatus
val message = if (newStatus) {
R.string.favorite_added
} else {
R.string.favorite_removed
}
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: txt(R.string.no_data).asStringNull(context) ?: ""
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
}
}
}
}
observeNullable(viewModel.subscribeStatus) { isSubscribed ->
binding?.resultSubscribeButton?.apply {
isVisible = isSubscribed != null && context.isEmulatorSettings()
if (isSubscribed == null) return@observeNullable
val drawable = if (isSubscribed) {
R.drawable.ic_baseline_notifications_active_24
} else {
R.drawable.baseline_notifications_none_24
}
val text = if (isSubscribed) {
R.string.action_unsubscribe
} else {
R.string.action_subscribe
}
setIconResource(drawable)
setText(text)
setOnClickListener {
viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleSubscriptionStatus
val message = if (newStatus) {
// Kinda icky to have this here, but it works.
SubscriptionWorkManager.enqueuePeriodicWork(context)
R.string.subscription_new
} else {
R.string.subscription_deleted
}
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: txt(R.string.no_data).asStringNull(context) ?: ""
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
}
}
}
@ -535,6 +620,7 @@ class ResultFragmentTv : Fragment() {
observeNullable(viewModel.movie) { data ->
binding?.apply {
resultPlayMovie.isVisible = data is Resource.Success
resultPlaySeries.isVisible = data == null
seriesHolder.isVisible = data == null
resultEpisodesShow.isVisible = data == null
@ -764,12 +850,14 @@ class ResultFragmentTv : Fragment() {
R.drawable.profile_bg_red,
R.drawable.profile_bg_teal
).random()
//Change poster crop area to 20% from Top
backgroundPoster.cropYCenterOffsetPct = 0.20F
backgroundPoster.setImage(
d.posterBackgroundImage ?: UiImage.Drawable(error),
radius = 0,
errorImageDrawable = error
)
resultComingSoon.isVisible = d.comingSoon
resultDataHolder.isGone = d.comingSoon
UIHelper.populateChips(resultTag, d.tags)

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

@ -7,6 +7,8 @@ import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
@ -31,6 +33,7 @@ import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
@ -45,19 +48,37 @@ import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.CastHelper.startCast
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.getFavoritesData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeFavoritesData
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.setFavoritesData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.setSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import kotlinx.coroutines.*
import java.io.File
@ -110,6 +131,18 @@ data class ResultData(
val plotHeaderText: UiText,
)
data class CheckDuplicateData(
val name: String,
val year: Int?,
val syncData: Map<String, String>?
)
enum class LibraryListType {
BOOKMARKS,
FAVORITES,
SUBSCRIPTIONS
}
fun txt(status: DubStatus?): UiText? {
return txt(
when (status) {
@ -425,6 +458,9 @@ class ResultViewModel2 : ViewModel() {
private val _subscribeStatus: MutableLiveData<Boolean?> = MutableLiveData(null)
val subscribeStatus: LiveData<Boolean?> = _subscribeStatus
private val _favoriteStatus: MutableLiveData<Boolean?> = MutableLiveData(null)
val favoriteStatus: LiveData<Boolean?> = _favoriteStatus
companion object {
const val TAG = "RVM2"
//private const val EPISODE_RANGE_SIZE = 20
@ -435,33 +471,6 @@ class ResultViewModel2 : ViewModel() {
return this?.firstOrNull { it.season == season }
}
fun updateWatchStatus(currentResponse: LoadResponse, status: WatchType) {
val currentId = currentResponse.getId()
val currentWatchType = getResultWatchState(currentId)
DataStoreHelper.setResultWatchState(currentId, status.internalId)
val current = DataStoreHelper.getBookmarkedData(currentId)
val currentTime = System.currentTimeMillis()
DataStoreHelper.setBookmarkedData(
currentId,
DataStoreHelper.BookmarkedData(
currentId,
current?.bookmarkedTime ?: currentTime,
currentTime,
currentResponse.name,
currentResponse.url,
currentResponse.apiName,
currentResponse.type,
currentResponse.posterUrl,
currentResponse.year
)
)
if (currentWatchType != status) {
MainActivity.bookmarksUpdatedEvent(true)
}
}
private fun filterName(name: String?): String? {
if (name == null) return null
Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let {
@ -816,9 +825,77 @@ class ResultViewModel2 : ViewModel() {
val selectPopup: LiveData<SelectPopup?> = _selectPopup
fun updateWatchStatus(status: WatchType) {
updateWatchStatus(currentResponse ?: return, status)
_watchStatus.postValue(status)
fun updateWatchStatus(
status: WatchType,
context: Context?,
loadResponse: LoadResponse? = null,
statusChangedCallback: ((statusChanged: Boolean) -> Unit)? = null
) {
val response = loadResponse ?: currentResponse ?: return
val currentId = response.getId()
val currentStatus = getResultWatchState(currentId)
// If the current status is "NONE" and the new status is not "NONE",
// fetch the bookmarked data to check for duplicates, otherwise set this
// to an empty list, so that we don't show the duplicate warning dialog,
// but we still want to update the current bookmark and refresh the data anyway.
val bookmarkedData = if (currentStatus == WatchType.NONE && status != WatchType.NONE) {
getAllBookmarkedData()
} else emptyList()
checkAndWarnDuplicates(
context,
LibraryListType.BOOKMARKS,
CheckDuplicateData(
name = response.name,
year = response.year,
syncData = response.syncData,
),
bookmarkedData
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
if (!shouldContinue) return@checkAndWarnDuplicates
if (duplicateIds.isNotEmpty()) {
duplicateIds.forEach { duplicateId ->
deleteBookmarkedData(duplicateId)
}
}
setResultWatchState(currentId, status.internalId)
// We don't need to store if WatchType.NONE.
// The key is removed in setResultWatchState, we don't want to
// re-add it again here if it was just removed.
if (status != WatchType.NONE) {
val current = getBookmarkedData(currentId)
setBookmarkedData(
currentId,
DataStoreHelper.BookmarkedData(
current?.bookmarkedTime ?: unixTimeMS,
currentId,
unixTimeMS,
response.name,
response.url,
response.apiName,
response.type,
response.posterUrl,
response.year,
response.syncData
)
)
}
if (currentStatus != status) {
MainActivity.bookmarksUpdatedEvent(true)
}
_watchStatus.postValue(status)
statusChangedCallback?.invoke(true)
}
}
private fun startChromecast(
@ -833,39 +910,255 @@ class ResultViewModel2 : ViewModel() {
}
/**
* @return true if the new status is Subscribed, false if not. Null if not possible to subscribe.
**/
fun toggleSubscriptionStatus(): Boolean? {
val isSubscribed = _subscribeStatus.value ?: return null
val response = currentResponse ?: return null
if (response !is EpisodeResponse) return null
* Toggles the subscription status of an item.
*
* @param context The context to use for operations.
* @param statusChangedCallback A callback that is invoked when the subscription status changes.
* It provides the new subscription status (true if subscribed, false if unsubscribed, null if action was canceled).
*/
fun toggleSubscriptionStatus(
context: Context?,
statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null
) {
val isSubscribed = _subscribeStatus.value ?: return
val response = currentResponse ?: return
if (response !is EpisodeResponse) return
val currentId = response.getId()
if (isSubscribed) {
DataStoreHelper.removeSubscribedData(currentId)
removeSubscribedData(currentId)
statusChangedCallback?.invoke(false)
_subscribeStatus.postValue(false)
} else {
val current = DataStoreHelper.getSubscribedData(currentId)
checkAndWarnDuplicates(
context,
LibraryListType.SUBSCRIPTIONS,
CheckDuplicateData(
name = response.name,
year = response.year,
syncData = response.syncData,
),
getAllSubscriptions(),
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
if (!shouldContinue) {
statusChangedCallback?.invoke(null)
return@checkAndWarnDuplicates
}
DataStoreHelper.setSubscribedData(
currentId,
DataStoreHelper.SubscribedData(
if (duplicateIds.isNotEmpty()) {
duplicateIds.forEach { duplicateId ->
removeSubscribedData(duplicateId)
}
}
val current = getSubscribedData(currentId)
setSubscribedData(
currentId,
current?.bookmarkedTime ?: unixTimeMS,
unixTimeMS,
response.getLatestEpisodes(),
response.name,
response.url,
response.apiName,
response.type,
response.posterUrl,
response.year
DataStoreHelper.SubscribedData(
current?.subscribedTime ?: unixTimeMS,
response.getLatestEpisodes(),
currentId,
unixTimeMS,
response.name,
response.url,
response.apiName,
response.type,
response.posterUrl,
response.year,
response.syncData
)
)
)
_subscribeStatus.postValue(true)
statusChangedCallback?.invoke(true)
}
}
}
/**
* Toggles the favorite status of an item.
*
* @param context The context to use.
* @param statusChangedCallback A callback that is invoked when the favorite status changes.
* It provides the new favorite status (true if added to favorites, false if removed, null if action was canceled).
*/
fun toggleFavoriteStatus(
context: Context?,
statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null
) {
val isFavorite = _favoriteStatus.value ?: return
val response = currentResponse ?: return
val currentId = response.getId()
if (isFavorite) {
removeFavoritesData(currentId)
statusChangedCallback?.invoke(false)
_favoriteStatus.postValue(false)
} else {
checkAndWarnDuplicates(
context,
LibraryListType.FAVORITES,
CheckDuplicateData(
name = response.name,
year = response.year,
syncData = response.syncData,
),
getAllFavorites(),
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
if (!shouldContinue) {
statusChangedCallback?.invoke(null)
return@checkAndWarnDuplicates
}
if (duplicateIds.isNotEmpty()) {
duplicateIds.forEach { duplicateId ->
removeFavoritesData(duplicateId)
}
}
val current = getFavoritesData(currentId)
setFavoritesData(
currentId,
DataStoreHelper.FavoritesData(
current?.favoritesTime ?: unixTimeMS,
currentId,
unixTimeMS,
response.name,
response.url,
response.apiName,
response.type,
response.posterUrl,
response.year,
response.syncData
)
)
_favoriteStatus.postValue(true)
statusChangedCallback?.invoke(true)
}
}
}
@MainThread
private fun checkAndWarnDuplicates(
context: Context?,
listType: LibraryListType,
checkDuplicateData: CheckDuplicateData,
data: List<DataStoreHelper.LibrarySearchResponse>,
checkDuplicatesCallback: (shouldContinue: Boolean, duplicateIds: List<Int?>) -> Unit
) {
val whitespaceRegex = "\\s+".toRegex()
fun normalizeString(input: String): String {
/**
* Trim the input string and replace consecutive spaces with a single space.
* This covers some edge-cases where the title does not match exactly across providers,
* and one provider has the title with an extra whitespace. This is minor enough that
* it should still match in this case.
*/
return input.trim().replace(whitespaceRegex, " ")
}
_subscribeStatus.postValue(!isSubscribed)
return !isSubscribed
val syncData = checkDuplicateData.syncData
val imdbId = getImdbIdFromSyncData(syncData)
val tmdbId = getTMDbIdFromSyncData(syncData)
val malId = syncData?.get(AccountManager.malApi.idPrefix)
val aniListId = syncData?.get(AccountManager.aniListApi.idPrefix)
val normalizedName = normalizeString(checkDuplicateData.name)
val year = checkDuplicateData.year
val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse ->
val librarySyncData = it.syncData
val checks = listOf(
{ imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId },
{ tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId },
{ malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId },
{ aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId },
{ normalizedName == normalizeString(it.name) && year == it.year }
)
checks.any { it() }
}
if (duplicateEntries.isEmpty() || context == null) {
checkDuplicatesCallback.invoke(true, emptyList())
return
}
val replaceMessage = if (duplicateEntries.size > 1) {
R.string.duplicate_replace_all
} else R.string.duplicate_replace
val message = if (duplicateEntries.size == 1) {
val list = when (listType) {
LibraryListType.BOOKMARKS -> getResultWatchState(duplicateEntries[0].id ?: 0).stringRes
LibraryListType.FAVORITES -> R.string.favorites_list_name
LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name
}
context.getString(R.string.duplicate_message_single,
"${normalizeString(duplicateEntries[0].name)} (${context.getString(list)}) — ${duplicateEntries[0].apiName}"
)
} else {
val bulletPoints = duplicateEntries.joinToString("\n") {
val list = when (listType) {
LibraryListType.BOOKMARKS -> getResultWatchState(it.id ?: 0).stringRes
LibraryListType.FAVORITES -> R.string.favorites_list_name
LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name
}
"${it.apiName}: ${normalizeString(it.name)} (${context.getString(list)})"
}
context.getString(R.string.duplicate_message_multiple, bulletPoints)
}
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
val dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
checkDuplicatesCallback.invoke(true, emptyList())
}
DialogInterface.BUTTON_NEGATIVE -> {
checkDuplicatesCallback.invoke(false, emptyList())
}
DialogInterface.BUTTON_NEUTRAL -> {
checkDuplicatesCallback.invoke(true, duplicateEntries.map { it.id })
}
}
}
builder.setTitle(R.string.duplicate_title)
.setMessage(message)
.setPositiveButton(R.string.duplicate_add, dialogClickListener)
.setNegativeButton(R.string.duplicate_cancel, dialogClickListener)
.setNeutralButton(replaceMessage, dialogClickListener)
.show().setDefaultFocus()
}
private fun getImdbIdFromSyncData(syncData: Map<String, String>?): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(
syncData?.get(AccountManager.simklApi.idPrefix)
)[SimklApi.Companion.SyncServices.Imdb]
}
}
private fun getTMDbIdFromSyncData(syncData: Map<String, String>?): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(
syncData?.get(AccountManager.simklApi.idPrefix)
)[SimklApi.Companion.SyncServices.Tmdb]
}
}
private fun startChromecast(
@ -1219,7 +1512,7 @@ class ResultViewModel2 : ViewModel() {
// Do not add mark as watched on movies
if (!listOf(TvType.Movie, TvType.AnimeMovie).contains(click.data.tvType)) {
val isWatched =
DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched
getVideoWatchState(click.data.id) == VideoWatchState.Watched
val watchedText = if (isWatched) R.string.action_remove_from_watched
else R.string.action_mark_as_watched
@ -1468,12 +1761,12 @@ class ResultViewModel2 : ViewModel() {
ACTION_MARK_AS_WATCHED -> {
val isWatched =
DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched
getVideoWatchState(click.data.id) == VideoWatchState.Watched
if (isWatched) {
DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.None)
setVideoWatchState(click.data.id, VideoWatchState.None)
} else {
DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.Watched)
setVideoWatchState(click.data.id, VideoWatchState.Watched)
}
// Kinda dirty to reload all episodes :(
@ -1682,7 +1975,7 @@ class ResultViewModel2 : ViewModel() {
list.subList(start, end).map {
val posDur = getViewPos(it.id)
val watchState =
DataStoreHelper.getVideoWatchState(it.id) ?: VideoWatchState.None
getVideoWatchState(it.id) ?: VideoWatchState.None
it.copy(
position = posDur?.position ?: 0,
duration = posDur?.duration ?: 0,
@ -1743,13 +2036,19 @@ class ResultViewModel2 : ViewModel() {
private fun postSubscription(loadResponse: LoadResponse) {
if (loadResponse.isEpisodeBased()) {
val id = loadResponse.getId()
val data = DataStoreHelper.getSubscribedData(id)
DataStoreHelper.updateSubscribedData(id, data, loadResponse as? EpisodeResponse)
val data = getSubscribedData(id)
updateSubscribedData(id, data, loadResponse as? EpisodeResponse)
val isSubscribed = data != null
_subscribeStatus.postValue(isSubscribed)
}
}
private fun postFavorites(loadResponse: LoadResponse) {
val id = loadResponse.getId()
val isFavorite = getFavoritesData(id) != null
_favoriteStatus.postValue(isFavorite)
}
private fun postEpisodeRange(indexer: EpisodeIndexer?, range: EpisodeRange?) {
if (range == null || indexer == null) {
return
@ -1887,6 +2186,7 @@ class ResultViewModel2 : ViewModel() {
currentResponse = loadResponse
postPage(loadResponse, apiRepository)
postSubscription(loadResponse)
postFavorites(loadResponse)
if (updateEpisodes)
postEpisodes(loadResponse, updateFillers)
}
@ -1915,6 +2215,10 @@ class ResultViewModel2 : ViewModel() {
val id =
mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000)
?: 0)
val totalIndex =
i.season?.let { season -> loadResponse.getTotalEpisodeIndex(episode, season) }
if (!existingEpisodes.contains(id)) {
existingEpisodes.add(id)
val seasonData = loadResponse.seasonNames.getSeason(i.season)
@ -1934,7 +2238,8 @@ class ResultViewModel2 : ViewModel() {
i.description,
fillers.getOrDefault(episode, false),
loadResponse.type,
mainId
mainId,
totalIndex
)
val season = eps.seasonIndex ?: 0
@ -1963,6 +2268,9 @@ class ResultViewModel2 : ViewModel() {
val seasonData =
loadResponse.seasonNames.getSeason(episode.season)
val totalIndex =
episode.season?.let { season -> loadResponse.getTotalEpisodeIndex(episodeIndex, season) }
val ep =
buildResultEpisode(
loadResponse.name,
@ -1979,7 +2287,8 @@ class ResultViewModel2 : ViewModel() {
episode.description,
null,
loadResponse.type,
mainId
mainId,
totalIndex
)
val season = ep.seasonIndex ?: 0
@ -2010,7 +2319,8 @@ class ResultViewModel2 : ViewModel() {
null,
null,
loadResponse.type,
mainId
mainId,
null
)
)
}
@ -2032,7 +2342,8 @@ class ResultViewModel2 : ViewModel() {
null,
null,
loadResponse.type,
mainId
mainId,
null
)
)
}
@ -2054,7 +2365,8 @@ class ResultViewModel2 : ViewModel() {
null,
null,
loadResponse.type,
mainId
mainId,
null
)
)
}
@ -2115,13 +2427,13 @@ class ResultViewModel2 : ViewModel() {
postResume()
}
fun postResume() {
private fun postResume() {
_resumeWatching.postValue(resume())
}
private fun resume(): ResumeWatchingStatus? {
val correctId = currentId ?: return null
val resume = DataStoreHelper.getLastWatched(correctId)
val resume = getLastWatched(correctId)
val resumeParentId = resume?.parentId
if (resumeParentId != correctId) return null // is null or smth went wrong with getLastWatched
val resumeId = resume.episodeId ?: return null// invalid episode id

View file

@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.search
import android.content.DialogInterface
import android.content.res.Configuration
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -231,9 +232,15 @@ class SearchFragment : Fragment() {
val searchExitIcon =
binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
// val searchMagIcon =
// main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
//searchMagIcon.scaleX = 0.65f
//searchMagIcon.scaleY = 0.65f
// binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
// searchMagIcon.scaleX = 0.65f
// searchMagIcon.scaleY = 0.65f
// Set the color for the search exit icon to the correct theme text color
val searchExitIconColor = TypedValue()
activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
searchExitIcon?.setColorFilter(searchExitIconColor.data)
selectedApis = DataStoreHelper.searchPreferenceProviders.toMutableSet()

View file

@ -30,6 +30,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.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
@ -248,6 +249,7 @@ class SettingsAccount : PreferenceFragmentCompat() {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_account)
setPaddingBottom()
setToolBarScrollFlags()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {

View file

@ -12,10 +12,12 @@ import android.widget.ImageView
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.R
import com.lagradost.cloudstream3.databinding.MainSettingsBinding
@ -54,7 +56,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 = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar) ?: return
@ -63,7 +82,7 @@ class SettingsFragment : Fragment() {
setTitle(title)
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
setNavigationOnClickListener {
activity?.onBackPressed()
activity?.onBackPressedDispatcher?.onBackPressed()
}
}
fixPaddingStatusbar(settingsToolbar)
@ -78,7 +97,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)
@ -187,13 +206,13 @@ class SettingsFragment : Fragment() {
}
binding?.apply {
listOf(
settingsGeneral to R.id.action_navigation_settings_to_navigation_settings_general,
settingsPlayer to R.id.action_navigation_settings_to_navigation_settings_player,
settingsCredits to R.id.action_navigation_settings_to_navigation_settings_account,
settingsUi to R.id.action_navigation_settings_to_navigation_settings_ui,
settingsProviders to R.id.action_navigation_settings_to_navigation_settings_providers,
settingsUpdates to R.id.action_navigation_settings_to_navigation_settings_updates,
settingsExtensions to R.id.action_navigation_settings_to_navigation_settings_extensions,
settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general,
settingsPlayer to R.id.action_navigation_global_to_navigation_settings_player,
settingsCredits to R.id.action_navigation_global_to_navigation_settings_account,
settingsUi to R.id.action_navigation_global_to_navigation_settings_ui,
settingsProviders to R.id.action_navigation_global_to_navigation_settings_providers,
settingsUpdates to R.id.action_navigation_global_to_navigation_settings_updates,
settingsExtensions to R.id.action_navigation_global_to_navigation_settings_extensions,
).forEach { (view, navigationId) ->
view.apply {
setOnClickListener {

View file

@ -28,6 +28,7 @@ import com.lagradost.cloudstream3.network.initClient
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.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
@ -115,6 +116,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_general)
setPaddingBottom()
setToolBarScrollFlags()
}
data class CustomSite(
@ -192,7 +194,6 @@ class SettingsGeneral : PreferenceFragmentCompat() {
return@setOnPreferenceClickListener true
}
fun showAdd() {
val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } }
activity?.showDialog(

View file

@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.mvvm.logError
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
@ -23,6 +24,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

@ -13,6 +13,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
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.SingleSelectionHelper.showMultiDialog
@ -25,6 +26,7 @@ class SettingsProviders : PreferenceFragmentCompat() {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_providers)
setPaddingBottom()
setToolBarScrollFlags()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {

View file

@ -11,6 +11,7 @@ import com.lagradost.cloudstream3.mvvm.logError
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.SingleSelectionHelper.showBottomDialog
@ -23,6 +24,7 @@ class SettingsUI : PreferenceFragmentCompat() {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_ui)
setPaddingBottom()
setToolBarScrollFlags()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {

View file

@ -22,6 +22,7 @@ import com.lagradost.cloudstream3.network.initClient
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
@ -42,6 +43,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

@ -583,7 +583,7 @@ object AppUtils {
//private val viewModel: ResultViewModel by activityViewModels()
private fun getResultsId(): Int {
return if (isTrueTvSettings()) {
return if (isTvSettings()) {
R.id.global_to_navigation_results_tv
} else {
R.id.global_to_navigation_results_phone

View file

@ -1,14 +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.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
@ -19,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.library.ListSorting
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
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
@ -42,6 +26,7 @@ const val VIDEO_WATCH_STATE = "video_watch_state"
const val RESULT_WATCH_STATE = "result_watch_state"
const val RESULT_WATCH_STATE_DATA = "result_watch_state_data"
const val RESULT_SUBSCRIBED_STATE_DATA = "result_subscribed_state_data"
const val RESULT_FAVORITES_STATE_DATA = "result_favorites_state_data"
const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes
const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching"
const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated"
@ -73,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,
@ -135,6 +120,8 @@ object DataStoreHelper {
val customImage: String? = null,
@JsonProperty("defaultImageIndex")
val defaultImageIndex: Int,
@JsonProperty("lockPin")
val lockPin: String? = null,
) {
val image: UiImage
get() = customImage?.let { UiImage.Image(it) } ?: UiImage.Drawable(
@ -143,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()
@ -162,106 +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 {
// rolls the image forwards once
currentEditAccount =
currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % profileImages.size)
binding.profilePic.setImage(currentEditAccount.image)
}
binding.applyBtt.setOnClickListener {
val currentAccounts = accounts.toMutableList()
val overrideIndex =
currentAccounts.indexOfFirst { it.keyIndex == currentEditAccount.keyIndex }
// if an account is found that has the same keyIndex then override that one, if not then append it
if (overrideIndex != -1) {
currentAccounts[overrideIndex] = currentEditAccount
} else {
currentAccounts.add(currentEditAccount)
}
// Save the current homepage for new accounts
val currentHomePage = DataStoreHelper.currentHomePage
// set the new default account as well as add the key for the new account
setAccount(currentEditAccount, false)
DataStoreHelper.currentHomePage = currentHomePage
accounts = currentAccounts.toTypedArray()
dialog.dismissSafe()
}
}
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,
@ -271,69 +173,14 @@ object DataStoreHelper {
}
}
fun showWhoIsWatching(context: Context) {
val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate(
LayoutInflater.from(context)
)
val showAccount = accounts.toMutableList().apply {
fun getAccounts(context: Context): List<Account> {
return accounts.toMutableList().apply {
val item = getDefaultAccount(context)
remove(item)
add(0, item)
}
val builder =
BottomSheetDialog(context)
builder.setContentView(binding.root)
val accountName = context.getString(R.string.account)
binding.profilesRecyclerview.setLinearListLayout(
isHorizontal = true,
nextUp = FOCUS_SELF,
nextDown = FOCUS_SELF,
nextLeft = FOCUS_SELF,
nextRight = FOCUS_SELF
)
binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter(
selectCallBack = { 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
@ -351,20 +198,35 @@ object DataStoreHelper {
/**
* Used to display notifications on new episodes and posters in library.
**/
data class SubscribedData(
abstract class LibrarySearchResponse(
@JsonProperty("id") override var id: Int?,
@JsonProperty("subscribedTime") val bookmarkedTime: Long,
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long,
@JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map<DubStatus, Int?>,
@JsonProperty("latestUpdatedTime") open val latestUpdatedTime: Long,
@JsonProperty("name") override val name: String,
@JsonProperty("url") override val url: String,
@JsonProperty("apiName") override val apiName: String,
@JsonProperty("type") override var type: TvType? = null,
@JsonProperty("type") override var type: TvType?,
@JsonProperty("posterUrl") override var posterUrl: String?,
@JsonProperty("year") val year: Int?,
@JsonProperty("quality") override var quality: SearchQuality? = null,
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
) : SearchResponse {
@JsonProperty("year") open val year: Int?,
@JsonProperty("syncData") open val syncData: Map<String, String>?,
@JsonProperty("quality") override var quality: SearchQuality?,
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>?
) : SearchResponse
data class SubscribedData(
@JsonProperty("subscribedTime") val subscribedTime: Long,
@JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map<DubStatus, Int?>,
override var id: Int?,
override val latestUpdatedTime: Long,
override val name: String,
override val url: String,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override val year: Int?,
override val syncData: Map<String, String>? = null,
override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null
) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) {
fun toLibraryItem(): SyncAPI.LibraryItem? {
return SyncAPI.LibraryItem(
name,
@ -380,18 +242,19 @@ object DataStoreHelper {
}
data class BookmarkedData(
@JsonProperty("id") override var id: Int?,
@JsonProperty("bookmarkedTime") val bookmarkedTime: Long,
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long,
@JsonProperty("name") override val name: String,
@JsonProperty("url") override val url: String,
@JsonProperty("apiName") override val apiName: String,
@JsonProperty("type") override var type: TvType? = null,
@JsonProperty("posterUrl") override var posterUrl: String?,
@JsonProperty("year") val year: Int?,
@JsonProperty("quality") override var quality: SearchQuality? = null,
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
) : SearchResponse {
override var id: Int?,
override val latestUpdatedTime: Long,
override val name: String,
override val url: String,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override val year: Int?,
override val syncData: Map<String, String>? = null,
override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null
) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) {
fun toLibraryItem(id: String): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem(
name,
@ -406,6 +269,34 @@ object DataStoreHelper {
}
}
data class FavoritesData(
@JsonProperty("favoritesTime") val favoritesTime: Long,
override var id: Int?,
override val latestUpdatedTime: Long,
override val name: String,
override val url: String,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override val year: Int?,
override val syncData: Map<String, String>? = null,
override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null
) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) {
fun toLibraryItem(): SyncAPI.LibraryItem? {
return SyncAPI.LibraryItem(
name,
url,
id?.toString() ?: return null,
null,
null,
null,
latestUpdatedTime,
apiName, type, posterUrl, posterHeaders, quality, this.id
)
}
}
data class ResumeWatchingResult(
@JsonProperty("name") override val name: String,
@JsonProperty("url") override val url: String,
@ -438,15 +329,9 @@ object DataStoreHelper {
removeKeys(folder)
}
fun deleteAllBookmarkedData() {
val folder1 = "$currentAccount/$RESULT_WATCH_STATE"
val folder2 = "$currentAccount/$RESULT_WATCH_STATE_DATA"
removeKeys(folder1)
removeKeys(folder2)
}
fun deleteBookmarkedData(id: Int?) {
if (id == null) return
AccountManager.localListApi.requireLibraryRefresh = true
removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString())
removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
}
@ -544,6 +429,12 @@ object DataStoreHelper {
return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
}
fun getAllBookmarkedData(): List<BookmarkedData> {
return getKeys("$currentAccount/$RESULT_WATCH_STATE_DATA")?.mapNotNull {
getKey(it)
} ?: emptyList()
}
fun getAllSubscriptions(): List<SubscribedData> {
return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull {
getKey(it)
@ -579,6 +470,29 @@ object DataStoreHelper {
return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString())
}
fun getAllFavorites(): List<FavoritesData> {
return getKeys("$currentAccount/$RESULT_FAVORITES_STATE_DATA")?.mapNotNull {
getKey(it)
} ?: emptyList()
}
fun removeFavoritesData(id: Int?) {
if (id == null) return
AccountManager.localListApi.requireLibraryRefresh = true
removeKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString())
}
fun setFavoritesData(id: Int?, data: FavoritesData) {
if (id == null) return
setKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString(), data)
AccountManager.localListApi.requireLibraryRefresh = true
}
fun getFavoritesData(id: Int?): FavoritesData? {
if (id == null) return null
return getKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString())
}
fun setViewPos(id: Int?, pos: Long, dur: Long) {
if (id == null) return
if (dur < 30_000) return // too short

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
@ -378,6 +379,7 @@ open class ExtractorLink constructor(
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

View file

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

View file

@ -0,0 +1,94 @@
package com.lagradost.cloudstream3.utils
//Reference: https://stackoverflow.com/a/29055283
import android.content.Context
import android.graphics.Matrix
import android.graphics.drawable.Drawable
import android.util.AttributeSet
class PercentageCropImageView : androidx.appcompat.widget.AppCompatImageView {
private var mCropYCenterOffsetPct: Float? = null
private var mCropXCenterOffsetPct: Float? = null
constructor(context: Context?) : super(context!!)
constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs)
constructor(
context: Context?, attrs: AttributeSet?,
defStyle: Int
) : super(context!!, attrs, defStyle)
var cropYCenterOffsetPct: Float
get() = mCropYCenterOffsetPct!!
set(cropYCenterOffsetPct) {
require(cropYCenterOffsetPct <= 1.0) { "Value too large: Must be <= 1.0" }
mCropYCenterOffsetPct = cropYCenterOffsetPct
}
var cropXCenterOffsetPct: Float
get() = mCropXCenterOffsetPct!!
set(cropXCenterOffsetPct) {
require(cropXCenterOffsetPct <= 1.0) { "Value too large: Must be <= 1.0" }
mCropXCenterOffsetPct = cropXCenterOffsetPct
}
private fun myConfigureBounds() {
if (this.scaleType == ScaleType.MATRIX) {
val d = this.drawable
if (d != null) {
val dWidth = d.intrinsicWidth
val dHeight = d.intrinsicHeight
val m = Matrix()
val vWidth = width - this.paddingLeft - this.paddingRight
val vHeight = height - this.paddingTop - this.paddingBottom
val scale: Float
var dx = 0f
var dy = 0f
if (dWidth * vHeight > vWidth * dHeight) {
val cropXCenterOffsetPct =
if (mCropXCenterOffsetPct != null) mCropXCenterOffsetPct!!.toFloat() else 0.5f
scale = vHeight.toFloat() / dHeight.toFloat()
dx = (vWidth - dWidth * scale) * cropXCenterOffsetPct
} else {
val cropYCenterOffsetPct =
if (mCropYCenterOffsetPct != null) mCropYCenterOffsetPct!!.toFloat() else 0f
scale = vWidth.toFloat() / dWidth.toFloat()
dy = (vHeight - dHeight * scale) * cropYCenterOffsetPct
}
m.setScale(scale, scale)
m.postTranslate((dx + 0.5f).toInt().toFloat(), (dy + 0.5f).toInt().toFloat())
this.imageMatrix = m
}
}
}
// These 3 methods call configureBounds in ImageView.java class, which
// adjusts the matrix in a call to center_crop (android's built-in
// scaling and centering crop method). We also want to trigger
// in the same place, but using our own matrix, which is then set
// directly at line 588 of ImageView.java and then copied over
// as the draw matrix at line 942 of ImageView.java
override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
val changed = super.setFrame(l, t, r, b)
myConfigureBounds()
return changed
}
override fun setImageDrawable(d: Drawable?) {
super.setImageDrawable(d)
myConfigureBounds()
}
override fun setImageResource(resId: Int) {
super.setImageResource(resId)
myConfigureBounds()
}
// In case you can change the ScaleType in code you have to call redraw()
//fullsizeImageView.setScaleType(ScaleType.FIT_CENTER);
//fullsizeImageView.redraw();
fun redraw() {
val d = this.drawable
if (d != null) {
// Force toggle to recalculate our bounds
setImageDrawable(null)
setImageDrawable(d)
}
}
}

View file

@ -2,14 +2,12 @@ package com.lagradost.cloudstream3.utils
import android.app.Activity
import android.app.Dialog
import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.widget.AbsListView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
@ -19,7 +17,10 @@ import androidx.core.view.marginRight
import androidx.core.view.marginTop
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.BottomInputDialogBinding
import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding
import com.lagradost.cloudstream3.databinding.BottomTextDialogBinding
import com.lagradost.cloudstream3.databinding.OptionsPopupTvBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
@ -54,14 +55,14 @@ object SingleSelectionHelper {
if (this == null) return
if (isTvSettings()) {
val builder =
AlertDialog.Builder(this, R.style.AlertDialogCustom)
.setView(R.layout.options_popup_tv)
val binding = OptionsPopupTvBinding.inflate(layoutInflater)
val dialog = AlertDialog.Builder(this, R.style.AlertDialogCustom)
.setView(binding.root)
.create()
val dialog = builder.create()
dialog.show()
dialog.findViewById<ListView>(R.id.listview1)?.let { listView ->
binding.listview1.let { listView ->
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
listView.adapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice_color).apply {
@ -74,7 +75,7 @@ object SingleSelectionHelper {
}
}
dialog.findViewById<ImageView>(R.id.imageView)?.apply {
binding.imageView.apply {
isGone = poster.isNullOrEmpty()
setImage(poster)
}
@ -105,12 +106,12 @@ object SingleSelectionHelper {
if (this == null) return
val realShowApply = showApply || isMultiSelect
val listView = binding.listview1//.findViewById<ListView>(R.id.listview1)!!
val textView = binding.text1//.findViewById<TextView>(R.id.text1)!!
val applyButton = binding.applyBtt//.findViewById<TextView>(R.id.apply_btt)
val cancelButton = binding.cancelBtt//findViewById<TextView>(R.id.cancel_btt)
val listView = binding.listview1
val textView = binding.text1
val applyButton = binding.applyBtt
val cancelButton = binding.cancelBtt
val applyHolder =
binding.applyBttHolder//.findViewById<LinearLayout>(R.id.apply_btt_holder)
binding.applyBttHolder
applyHolder.isVisible = realShowApply
if (!realShowApply) {
@ -173,8 +174,8 @@ object SingleSelectionHelper {
}
}
private fun Activity?.showInputDialog(
binding: BottomInputDialogBinding,
dialog: Dialog,
value: String,
name: String,
@ -184,11 +185,11 @@ object SingleSelectionHelper {
) {
if (this == null) return
val inputView = dialog.findViewById<EditText>(R.id.nginx_text_input)!!
val textView = dialog.findViewById<TextView>(R.id.text1)!!
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!!
val inputView = binding.nginxTextInput
val textView = binding.text1
val applyButton = binding.applyBtt
val cancelButton = binding.cancelBtt
val applyHolder = binding.applyBttHolder
applyHolder.isVisible = true
textView.text = name
@ -350,11 +351,17 @@ object SingleSelectionHelper {
dismissCallback: () -> Unit,
callback: (String) -> Unit,
) {
val builder = BottomSheetDialog(this) // probably the stuff at the bottom
builder.setContentView(R.layout.bottom_input_dialog) // input layout
val builder = BottomSheetDialog(this)
val binding: BottomInputDialogBinding = BottomInputDialogBinding.inflate(
LayoutInflater.from(this)
)
builder.setContentView(binding.root)
builder.show()
showInputDialog(
binding,
builder,
value,
name,
@ -363,4 +370,24 @@ object SingleSelectionHelper {
dismissCallback
)
}
fun Activity.showBottomDialogText(
title: String,
text: Spanned,
dismissCallback: () -> Unit
) {
val binding = BottomTextDialogBinding.inflate(layoutInflater)
val dialog = BottomSheetDialog(this)
dialog.setContentView(binding.root)
binding.dialogTitle.text = title
binding.dialogText.text = text
dialog.setOnDismissListener {
dismissCallback.invoke()
}
dialog.show()
}
}

View file

@ -178,9 +178,10 @@ object UIHelper {
fun Activity?.navigate(@IdRes navigation: Int, arguments: Bundle? = null) {
try {
if (this is FragmentActivity) {
(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?)?.navController?.navigate(
navigation, arguments
)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?
navHostFragment?.navController?.let {
it.navigate(navigation, arguments)
}
}
} catch (t: Throwable) {
logError(t)
@ -300,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 ->
@ -367,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(
@ -417,7 +418,7 @@ object UIHelper {
}
fun FragmentActivity.popCurrentPage() {
this.onBackPressed()
this.onBackPressedDispatcher.onBackPressed()
/*val currentFragment = supportFragmentManager.fragments.lastOrNull {
it.isVisible
} ?: return
@ -437,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"
@ -77,61 +51,67 @@
android:textColorHint="?attr/grayTextColor"
tools:ignore="LabelFor" />
<CheckBox
android:id="@+id/lockProfileCheckbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/lock_profile" />
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
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

@ -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,24 +20,32 @@
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: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"
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/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

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

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<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="110dp"
android:layout_height="110dp"
android:animateLayoutChanges="true"
android:backgroundTint="?attr/primaryGrayBackground"
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"
app:layout_constraintHeight_percent="0.4"
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.1"
android:scaleType="centerCrop" />
<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"
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/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>

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

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="?attr/primaryBlackBackground"
android:layout_width="match_parent"
android:layout_height="match_parent">
<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" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="36dp"
android:gravity="center">
<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

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/dialog_title"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:textStyle="bold"
android:textSize="20sp"
android:textColor="?attr/textColor"
android:layout_width="match_parent"
android:layout_rowWeight="1"
tools:text="Test"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/dialog_text"
android:textAppearance="?android:attr/textAppearanceListItem"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingTop="10dp"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_rowWeight="1" />
</LinearLayout>

View file

@ -9,7 +9,7 @@
android:layout_margin="5dp"
android:foreground="@drawable/outline_drawable"
app:cardBackgroundColor="@color/transparent"
android:focusable="true"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:cardElevation="0dp">

View file

@ -92,15 +92,7 @@
android:layout_height="100dp"
android:layout_gravity="bottom"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal"
android:padding="20dp">
android:orientation="horizontal">
<TextView
android:id="@+id/home_preview_bookmark"
@ -139,7 +131,7 @@
app:drawableTint="?attr/white"
app:drawableTopCompat="@drawable/ic_outline_info_24"
app:tint="?attr/white" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View file

@ -232,7 +232,9 @@
android:layout_marginStart="@dimen/navbar_width"
android:layout_marginEnd="0dp"
android:padding="12dp"
android:text="@string/continue_watching" />
android:text="@string/continue_watching"
android:background="?android:attr/selectableItemBackground"
app:drawableTint="?attr/white" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/home_watch_child_recyclerview"
@ -258,7 +260,15 @@
android:visibility="gone"
tools:visibility="visible">
<FrameLayout
android:id="@+id/home_bookmark_parent_item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground">
<HorizontalScrollView
android:id="@+id/horizontal_scroll_chips"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fadingEdge="horizontal"
@ -344,6 +354,18 @@
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<ImageView
android:id="@+id/home_bookmark_parent_item_more_info"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:layout_marginEnd="12dp"
android:contentDescription="@string/home_more_info"
android:src="@drawable/ic_baseline_arrow_forward_24"
android:visibility="gone"
app:drawableTint="?attr/white" />
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/home_bookmarked_child_recyclerview"
android:layout_width="wrap_content"

View file

@ -140,6 +140,7 @@
android:layout_gravity="end"
android:background="@drawable/player_button_tv_attr_no_bg"
android:contentDescription="@string/account"
android:focusable="true"
android:nextFocusLeft="@id/home_preview_search_button"
android:nextFocusRight="@id/home_switch_account"
android:nextFocusDown="@id/home_change_api"

View file

@ -68,6 +68,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginEnd="25dp"
android:iconifiedByDefault="false"
android:imeOptions="actionSearch"
@ -81,6 +82,7 @@
app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon"
app:closeIcon="@drawable/ic_baseline_close_24"
tools:ignore="RtlSymmetry">
</androidx.appcompat.widget.SearchView>

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"
@ -102,11 +103,13 @@
app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon"
app:closeIcon="@drawable/ic_baseline_close_24"
tools:ignore="RtlSymmetry">
</androidx.appcompat.widget.SearchView>
</FrameLayout>
</LinearLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/library_tab_layout"
style="@style/Theme.Widget.Tabs"
@ -116,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"
@ -133,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"
@ -181,7 +184,6 @@
tools:listitem="@layout/loading_poster_dynamic" />
</com.facebook.shimmer.ShimmerFrameLayout>
</LinearLayout>
</FrameLayout>
<FrameLayout

View file

@ -74,7 +74,7 @@
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_description"
android:nextFocusLeft="@id/result_add_sync"
android:nextFocusRight="@id/result_share"
android:nextFocusRight="@id/result_favorite"
tools:visibility="visible"
@ -89,10 +89,27 @@
android:layout_gravity="end|center_vertical"
app:tint="?attr/textColor" />
<ImageView
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_description"
android:nextFocusLeft="@id/result_subscribe"
android:nextFocusRight="@id/result_share"
android:id="@+id/result_favorite"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_margin="5dp"
android:elevation="10dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_favorite_border_24"
android:layout_gravity="end|center_vertical"
app:tint="?attr/textColor" />
<ImageView
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_description"
android:nextFocusLeft="@id/result_subscribe"
android:nextFocusLeft="@id/result_favorite"
android:nextFocusRight="@id/result_open_in_browser"
android:id="@+id/result_share"

View file

@ -124,28 +124,27 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:textColor="?attr/textColor" />
</LinearLayout>
<FrameLayout
android:id="@+id/background_poster_holder"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_height="250dp"
android:visibility="visible">
<ImageView
<com.lagradost.cloudstream3.utils.PercentageCropImageView
android:id="@+id/background_poster"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_height="275dp"
android:layout_gravity="center"
android:alpha="0.8"
android:scaleType="centerCrop"
tools:src="@drawable/profile_bg_dark_blue" />
android:scaleType="matrix"
tools:src="@drawable/profile_bg_dark_blue" >
</com.lagradost.cloudstream3.utils.PercentageCropImageView>
<ImageView
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_height="120dp"
android:layout_gravity="bottom"
android:src="@drawable/background_shadow">
</ImageView>
</FrameLayout>
@ -155,7 +154,6 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -166,8 +164,70 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:orientation="horizontal">
android:orientation="vertical"
android:layout_marginTop="175dp">
<TextView
android:id="@+id/result_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
tools:text="The Perfect Run The Perfect Run" />
<LinearLayout
android:id="@+id/result_next_airing_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:orientation="vertical">
<TextView
android:id="@+id/result_episodes_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="20dp"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
android:visibility="gone"
tools:text="8 Episodes" />
<TextView
android:id="@+id/result_next_airing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center"
android:textColor="?attr/grayTextColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="Episode 1022 will be released in" />
<TextView
android:id="@+id/result_next_airing_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="5d 3h 30m" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp">
<LinearLayout
android:layout_width="250dp"
@ -176,67 +236,11 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_weight="0"
android:orientation="vertical">
<TextView
android:id="@+id/result_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
tools:text="The Perfect Run The Perfect Run" />
<LinearLayout
android:id="@+id/result_next_airing_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:orientation="vertical">
<TextView
android:id="@+id/result_episodes_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="20dp"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
android:visibility="gone"
tools:text="8 Episodes" />
<TextView
android:id="@+id/result_next_airing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center"
android:textColor="?attr/grayTextColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="Episode 1022 will be released in" />
<TextView
android:id="@+id/result_next_airing_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="5d 3h 30m" />
</LinearLayout>
<LinearLayout
android:id="@+id/result_movie_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="10dp"
android:animateLayoutChanges="true"
android:orientation="vertical"
tools:visibility="visible">
@ -310,19 +314,41 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_play_trailer"
android:nextFocusDown="@id/result_episodes_show"
android:nextFocusDown="@id/result_favorite_button"
android:text="@string/type_none"
android:visibility="visible"
app:icon="@drawable/ic_baseline_bookmark_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_favorite_button"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusDown="@id/result_subscribe_button"
android:text="@string/action_add_to_favorites"
android:visibility="visible"
app:icon="@drawable/ic_baseline_favorite_border_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_subscribe_button"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_favorite_button"
android:nextFocusDown="@id/result_episodes_show"
android:text="@string/action_subscribe"
android:visibility="visible"
app:icon="@drawable/ic_baseline_favorite_border_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_episodes_show"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/redirect_to_episodes"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusUp="@id/result_subscribe_button"
android:nextFocusDown="@id/result_cast_items"
android:text="@string/episodes"
@ -404,6 +430,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:fadingEdgeLength="30dp"
android:foreground="@drawable/outline_drawable"
android:maxLines="7"
android:focusable="true"
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_bookmark_button"
android:padding="5dp"

View file

@ -49,6 +49,7 @@
app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon"
app:closeIcon="@drawable/ic_baseline_close_24"
tools:ignore="RtlSymmetry">
<requestFocus />

View file

@ -50,6 +50,7 @@
app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon"
app:closeIcon="@drawable/ic_baseline_close_24"
tools:ignore="RtlSymmetry">
<requestFocus />

View file

@ -0,0 +1,35 @@
<?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:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/home_child_more_info"
style="@style/WatchHeaderText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/navbar_width"
android:layout_marginEnd="0dp"
android:padding="12dp"
tools:text="@string/continue_watching"
app:drawableRightCompat="@drawable/ic_baseline_arrow_forward_24"
app:drawableTint="?attr/white"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/home_more_info"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/home_child_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:descendantFocusability="afterDescendants"
android:nextFocusUp="@id/home_child_more_info"
android:orientation="horizontal"
android:paddingStart="@dimen/navbar_width"
android:paddingEnd="5dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/home_result_grid" />
</LinearLayout>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/pinEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/pin"
android:inputType="numberPassword"
android:maxLength="4" />
<TextView
android:id="@+id/pinEditTextError"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:visibility="gone" />
</LinearLayout>

View file

@ -49,6 +49,8 @@
app:queryBackground="@color/transparent"
app:searchIcon="@drawable/search_icon"
app:closeIcon="@drawable/ic_baseline_close_24"
android:paddingStart="-10dp"
android:iconifiedByDefault="false"
app:queryHint="@string/search_hint"

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

@ -331,57 +331,56 @@
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim"
tools:layout="@layout/main_settings">
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_ui"
app:destination="@id/navigation_settings_ui"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_providers"
app:destination="@id/navigation_settings_providers"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_player"
app:destination="@id/navigation_settings_player"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_updates"
app:destination="@id/navigation_settings_updates"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_account"
app:destination="@id/navigation_settings_account"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_general"
app:destination="@id/navigation_settings_general"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_extensions"
app:destination="@id/navigation_settings_extensions"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
</fragment>
tools:layout="@layout/main_settings" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_ui"
app:destination="@id/navigation_settings_ui"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_providers"
app:destination="@id/navigation_settings_providers"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_player"
app:destination="@id/navigation_settings_player"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_updates"
app:destination="@id/navigation_settings_updates"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_account"
app:destination="@id/navigation_settings_account"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_general"
app:destination="@id/navigation_settings_general"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_extensions"
app:destination="@id/navigation_settings_extensions"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<fragment
android:id="@+id/navigation_subtitles"

View file

@ -74,12 +74,10 @@
<string name="popup_resume_download">ማውረድ ቀጥል</string>
<string name="subs_text_color">የጽሑፍ ቀለም</string>
<string name="type_completed">የተጠናቀቀ</string>
<string name="type_none">ምንም</string>
<string name="play_trailer_button">የፊልም ማስታወቂያ አጫውት</string>
<string name="play_livestream_button">የቀጥታ ስርጭት አጫውት</string>
<string name="popup_play_file">ፋይል አጫውት</string>
<string name="type_re_watching">እንደገና በማየት ላይ</string>
<string name="sort_cancel">ሰርዝ</string>
<string name="go_back">ወደ ኋላ መመለሻ</string>
<string name="home_info">መረጃ</string>
<string name="sort_save">ያስቀምጡ</string>

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>
@ -36,7 +36,6 @@
<string name="type_completed">مكتمل</string>
<string name="type_dropped">مهمل</string>
<string name="type_plan_to_watch">أخطط لمشاهدته</string>
<string name="type_none">لا شيء</string>
<string name="type_re_watching">إعادة المشاهدة</string>
<string name="play_movie_button">مشاهدة الفيلم</string>
<string name="play_livestream_button">تشغيل بث حي</string>
@ -74,7 +73,6 @@
<string name="action_remove_from_bookmarks">ازالة</string>
<string name="action_add_to_bookmarks">إعداد حالة المشاهدة</string>
<string name="sort_apply">تطبيق</string>
<string name="sort_cancel">إلغاء</string>
<string name="sort_copy">نسخ</string>
<string name="sort_close">إغلاق</string>
<string name="sort_clear">مسح</string>
@ -180,6 +178,7 @@
<string name="no_episodes_found">لم يتم العثور على أي حلقات</string>
<string name="delete_file">حذف الملف</string>
<string name="delete">حذف</string>
<string name="cancel">إلغاء</string>
<string name="pause">إيقاف مؤقت</string>
<string name="resume">إستئناف</string>
<string name="go_back_30">-٣٠</string>
@ -198,7 +197,7 @@
<string name="synopsis">القصة</string>
<string name="queued">في قائمة الانتظار</string>
<string name="no_subtitles">الترجمة ليست موجودة</string>
<string name="default_subtitles">الإفتراضي</string>
<string name="action_default">الإفتراضي</string>
<string name="free_storage">فارغ</string>
<string name="used_storage">مستخدم</string>
<string name="app_storage">التطبيق</string>
@ -320,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>
@ -419,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>
@ -462,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>
@ -483,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>
@ -494,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>
@ -581,9 +580,34 @@
<string name="unable_to_inflate">تعذر إنشاء واجهة المستخدم بشكل صحيح ، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور %s</string>
<string name="automatic_plugin_download_mode_title">حدد الوضع لتصفية تنزيل المكونات الإضافية</string>
<string name="disable">تعطيل</string>
<string name="default_account">@string/default_subtitles</string>
<string name="no_plugins_found_error">لا توجد اضافة في المستودع</string>
<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

@ -34,7 +34,6 @@
<string name="result_tags">الأنواع</string>
<string name="download_paused">توقف التنزيل</string>
<string name="type_plan_to_watch">خطط للمشاهدة</string>
<string name="type_none">لا يوجد</string>
<string name="type_re_watching">إعادة المشاهدة</string>
<string name="new_update_format" formatted="true">!تم العثور على تحديث جديد
\n%s-&gt;%s</string>
@ -125,6 +124,7 @@
<string name="season">موسم</string>
<string name="copy_link_toast">تم نسخ الرابط إلى الحافظة</string>
<string name="delete">مسح</string>
<string name="cancel">الغي</string>
<string name="pause">وقف</string>
<string name="update_notification_downloading">جارٍ تنزيل تحديث التطبيق…</string>
<string name="subs_default_reset_toast">إعادة التعيين إلى القيمة العادية</string>
@ -208,7 +208,6 @@
<string name="popup_resume_download">استئناف تحميل</string>
<string name="home_info">معلومات</string>
<string name="popup_pause_download">وقفة التحميل</string>
<string name="sort_cancel">الغي</string>
<string name="sort_save">احفظ</string>
<string name="subtitles_settings">إعدادات الترجمة</string>
<string name="subs_text_color">لون الخط</string>

View file

@ -41,7 +41,6 @@
<string name="type_completed">Завършено</string>
<string name="type_dropped">Изпуснат</string>
<string name="type_plan_to_watch">План за гледане</string>
<string name="type_none">Нито един</string>
<string name="type_re_watching">Повторно гледане</string>
<string name="play_movie_button">Пускане на филм</string>
<string name="play_livestream_button">Възпроизвеждане на живо</string>
@ -78,7 +77,6 @@
<string name="action_remove_from_bookmarks">Премахване</string>
<string name="action_add_to_bookmarks">Задайте статус на гледане</string>
<string name="sort_apply">Приложи</string>
<string name="sort_cancel">Отказ</string>
<string name="sort_copy">Копирай</string>
<string name="sort_close">Затвори</string>
<string name="sort_clear">Изчисти</string>
@ -187,6 +185,7 @@
<string name="no_episodes_found">Няма намерени епизоди</string>
<string name="delete_file">Изтрий файла</string>
<string name="delete">Изтрий</string>
<string name="cancel">Отказ</string>
<string name="pause">Пауза</string>
<string name="resume">Продължи</string>
<string name="go_back_30">-30</string>
@ -205,7 +204,7 @@
<string name="synopsis">Синопсис</string>
<string name="queued">На опашката</string>
<string name="no_subtitles">Без субтитри</string>
<string name="default_subtitles">По подразбиране</string>
<string name="action_default">По подразбиране</string>
<string name="free_storage">Безплатно</string>
<string name="used_storage">Използвано</string>
<string name="app_storage">Приложения</string>

View file

@ -43,7 +43,6 @@
<string name="type_completed">শেষ</string>
<string name="type_dropped">বাদ</string>
<string name="type_plan_to_watch">দেখার ইচ্ছায়</string>
<string name="type_none">কোন কিছুই না</string>
<string name="type_re_watching">পুনরায় দেখা হচ্ছে</string>
<string name="play_torrent_button">টরেন্ট স্ট্রিম করুন</string>
<string name="pick_source">উৎসসমূহ</string>
@ -72,7 +71,6 @@
<string name="action_remove_from_bookmarks">বাদ দিন</string>
<string name="action_add_to_bookmarks">বুকমার্ক করুন</string>
<string name="sort_apply">প্রয়োগ করুন</string>
<string name="sort_cancel">বাদ দিন</string>
<string name="sort_copy">কপি করুন</string>
<string name="sort_close">বন্ধ করুন</string>
<string name="sort_clear">মুছুন</string>

View file

@ -44,7 +44,6 @@
<string name="type_completed">Completado</string>
<string name="type_dropped">Deixado</string>
<string name="type_plan_to_watch">Planejando assistir</string>
<string name="type_none">Nenhum</string>
<string name="type_re_watching">Reassistindo</string>
<string name="play_movie_button">Assistir Filme</string>
<string name="play_torrent_button">Transmitir Torrent</string>
@ -81,7 +80,6 @@
<string name="action_remove_from_bookmarks">Remover</string>
<string name="action_add_to_bookmarks">Selecionar marcador</string>
<string name="sort_apply">Aplicar</string>
<string name="sort_cancel">Cancelar</string>
<string name="sort_copy">Copiar</string>
<string name="sort_close">Fechar</string>
<string name="sort_clear">Limpar</string>
@ -185,6 +183,7 @@
<string name="no_episodes_found">Nenhum Episódio encontrado</string>
<string name="delete_file">Apagar Arquivo</string>
<string name="delete">Deletar</string>
<string name="cancel">Cancelar</string>
<string name="pause">Pausar</string>
<string name="resume">Retomar</string>
<string name="go_back_30">-30</string>
@ -203,7 +202,7 @@
<string name="synopsis">Sinopse</string>
<string name="queued">Na fila</string>
<string name="no_subtitles">Sem Legendas</string>
<string name="default_subtitles">Padrão</string>
<string name="action_default">Padrão</string>
<string name="free_storage">Livre</string>
<string name="used_storage">Usado</string>
<string name="app_storage">App</string>
@ -569,7 +568,6 @@
<string name="wifi">Wi-Fi</string>
<string name="player_settings_play_in_web">Lista de videos da web</string>
<string name="unable_to_inflate">A interface de usuário não foi gerada corretamente. Isto se trata de um bug importante e deve ser reportado imediatamente %s</string>
<string name="default_account">Legendas padrão da conta</string>
<string name="pref_category_ui_features">Características da interface de usuário</string>
<string name="category_provider_test">Provedor de teste</string>
<string name="pref_category_player_layout">Layout</string>

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>
@ -40,7 +40,6 @@
<string name="type_completed">Dokončeno</string>
<string name="type_dropped">Zahozeno</string>
<string name="type_plan_to_watch">Plánuji sledovat</string>
<string name="type_none">Žádné</string>
<string name="type_re_watching">Opětovné sledování</string>
<string name="play_movie_button">Přehrát film</string>
<string name="play_torrent_button">Streamovat torrent</string>
@ -76,7 +75,6 @@
<string name="action_remove_from_bookmarks">Odebrat</string>
<string name="action_add_to_bookmarks">Nastavit stav sledování</string>
<string name="sort_apply">Použít</string>
<string name="sort_cancel">Zrušit</string>
<string name="sort_copy">Kopírovat</string>
<string name="sort_close">Zavřít</string>
<string name="sort_clear">Vymazat</string>
@ -176,6 +174,7 @@
<string name="no_episodes_found">Nenalezeny žádné epizody</string>
<string name="delete_file">Smazat soubor</string>
<string name="delete">Smazat</string>
<string name="cancel">Zrušit</string>
<string name="pause">Pozastavit</string>
<string name="resume">Pokračovat</string>
<string name="go_back_30">-30</string>
@ -194,7 +193,7 @@
<string name="synopsis">Synopse</string>
<string name="queued">ve frontě</string>
<string name="no_subtitles">Žádné titulky</string>
<string name="default_subtitles">Výchozí</string>
<string name="action_default">Výchozí</string>
<string name="free_storage">Volné</string>
<string name="used_storage">Použito</string>
<string name="app_storage">Aplikace</string>
@ -293,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>
@ -411,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>
@ -437,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ářů.
@ -506,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>
@ -521,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,6 +574,32 @@
<string name="automatic_plugin_download_mode_title">Výběr režimu pro filtrování stahování doplňků</string>
<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="default_account">@string/default_subtitles</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

@ -52,7 +52,6 @@
<string name="type_completed">Abgeschlossen</string>
<string name="type_dropped">Abgebrochen</string>
<string name="type_plan_to_watch">Geplant</string>
<string name="type_none">Nichts</string>
<string name="type_re_watching">Erneut schauen</string>
<string name="play_movie_button">Film abspielen</string>
<string name="play_livestream_button">Livestream abspielen</string>
@ -89,7 +88,6 @@
<string name="action_remove_from_bookmarks">Entfernen</string>
<string name="action_add_to_bookmarks">Status setzen</string>
<string name="sort_apply">Anwenden</string>
<string name="sort_cancel">Abbrechen</string>
<string name="sort_copy">Kopieren</string>
<string name="sort_close">Schließen</string>
<string name="sort_clear">Leeren</string>
@ -192,6 +190,7 @@
<string name="episode_short">E</string>
<string name="no_episodes_found">Keine Episoden gefunden</string>
<string name="delete">Löschen</string>
<string name="cancel">Abbrechen</string>
<string name="pause">Pause</string>
<string name="resume">Fortsetzen</string>
<string name="go_back_30">-30</string>
@ -210,7 +209,7 @@
<string name="synopsis">Zusammenfassung</string>
<string name="queued">In Warteschlange eingereiht</string>
<string name="no_subtitles">Keine Untertitel</string>
<string name="default_subtitles">Standard</string>
<string name="action_default">Standard</string>
<string name="free_storage">Frei</string>
<string name="used_storage">Belegt</string>
<string name="app_storage">App</string>
@ -552,5 +551,4 @@
<string name="no_repository_found_error">Repository nicht gefunden, überprüf die URL und versuch ein VPN</string>
<string name="unable_to_inflate">Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein SCHWERWIEGENDER FEHLER und sollte sofort gemeldet werden. %s</string>
<string name="disable">Deaktivieren</string>
<string name="default_account">@string/default_subtitles</string>
</resources>

View file

@ -23,7 +23,6 @@
<string name="type_completed">Ολοκληρώθηκε</string>
<string name="type_dropped">Διακόπηκε</string>
<string name="type_plan_to_watch">Για παρακολούθηση</string>
<string name="type_none">Τίποτα</string>
<string name="play_movie_button">Αναπαραγωγή ταινίας</string>
<string name="play_torrent_button">Μετάδοση Torrent</string>
<string name="pick_source">Πηγές</string>
@ -58,7 +57,6 @@
<string name="action_remove_from_bookmarks">Αφαίρεση</string>
<string name="play_episode_toast">Αναπαραγωγή επεισοδίου</string>
<string name="sort_apply">Υποβολή</string>
<string name="sort_cancel">Ακύρωση</string>
<string name="player_speed">Ταχύτητα αναπαραγωγής</string>
<string name="subtitles_settings">Ρυθμίσεις υπότιτλων</string>
<string name="subs_text_color">Χρώμα κειμένου</string>
@ -155,6 +153,7 @@
<string name="no_episodes_found">Δεν βρέθηκαν επεισόδια</string>
<string name="delete_file">Διαγραφή αρχείου</string>
<string name="delete">Διαγραφή</string>
<string name="cancel">Ακύρωση</string>
<string name="pause">Παύση</string>
<string name="resume">Συνέχιση</string>
<string name="delete_message" formatted="true">Αυτό θα διαγράψει μόνιμα το %s
@ -169,7 +168,7 @@
<string name="synopsis">Περίληψη</string>
<string name="queued">προστέθηκε στην ουρά</string>
<string name="no_subtitles">Δεν υπάρχουν διαθέσιμοι υπότιτλοι</string>
<string name="default_subtitles">Προεπιλεγμένοι υπότιτλοι</string>
<string name="action_default">Προεπιλεγμένοι υπότιτλοι</string>
<string name="free_storage">Ελεύθερος</string>
<string name="used_storage">Σε χρήση</string>
<string name="app_storage">Εφαρμογή</string>
@ -547,6 +546,5 @@
<string name="unable_to_inflate">Το UI δεν ήταν σε θέση να δημιουργηθεί σωστά, είναι ένα σφάλμα και θα πρέπει να αναφερθεί αμέσως %s</string>
<string name="automatic_plugin_download_mode_title">Επιλέξτε κατάσταση για φιλτράρισμα επεκτάσεων για λήψη</string>
<string name="disable">Απενεργοποιημένο</string>
<string name="default_account">@string/default_subtitles</string>
<string name="stop">Τέλος</string>
</resources>

View file

@ -22,7 +22,6 @@
<string name="extension_description">Priskribo</string>
<string name="extension_version">Versio</string>
<string name="extension_status">Stato</string>
<string name="sort_cancel">Nuligi</string>
<string name="sort_clear">Forviŝi</string>
<string name="yes">Jes</string>
<string name="no">Ne</string>

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>
@ -45,7 +45,7 @@
<string name="player_load_subtitles_online">Cargar desde Internet</string>
<string name="autoplay_next_settings">Reproducir automáticamente episodio siguiente</string>
<string name="chromecast_subtitles_settings_des">Configuración de subtítulos de Chromecast</string>
<string name="default_subtitles">Predeterminado</string>
<string name="action_default">Predeterminado</string>
<string name="subtitles_outline">Contorno</string>
<string name="no_subtitles">Sin Subtítulos</string>
<string name="subtitles_raised">Elevado</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>
@ -138,7 +138,6 @@
<string name="type_completed">Completado</string>
<string name="type_dropped">Descartado</string>
<string name="type_plan_to_watch">Planeando ver</string>
<string name="type_none">Ninguno</string>
<string name="type_re_watching">Volviendo a mirar</string>
<string name="play_movie_button">Reproducir película</string>
<string name="play_trailer_button">Reproducir Trailer</string>
@ -146,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>
@ -167,7 +166,6 @@
<string name="error_bookmarks_text">Marcadores</string>
<string name="action_remove_from_bookmarks">Remover</string>
<string name="action_add_to_bookmarks">Seleccionar estado de visualización</string>
<string name="sort_cancel">Cancelar</string>
<string name="sort_copy">Copiar</string>
<string name="sort_close">Cerrar</string>
<string name="sort_clear">Limpiar</string>
@ -220,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>
@ -235,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>
@ -251,11 +249,12 @@
<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>
<string name="delete">Borrar</string>
<string name="cancel">Cancelar</string>
<string name="unexpected_error">Error inesperado del reproductor</string>
<string name="episode_action_chromecast_episode">Episodio en Chromecast</string>
<string name="episode_action_play_in_app">Reproducir en la aplicación</string>
@ -312,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>
@ -346,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>
@ -449,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>
@ -549,9 +548,34 @@
<string name="unable_to_inflate">La interfaz de usuario no se ha podido crear correctamente, se trata de un GRAN BUG y debe ser reportado inmediatamente %s</string>
<string name="automatic_plugin_download_mode_title">Seleccionar modo para filtrar los plugins descargados</string>
<string name="disable">Deshabilitar</string>
<string name="default_account">@string/default_subtitles</string>
<string name="no_plugins_found_error">No se encontraron complementos en el repositorio</string>
<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

@ -22,7 +22,6 @@
<string name="type_completed">Terminé</string>
<string name="type_dropped">Abandonné</string>
<string name="type_plan_to_watch">À regarder</string>
<string name="type_none">Aucun</string>
<string name="play_movie_button">Lire</string>
<string name="play_torrent_button">Streamer le Torrent</string>
<string name="pick_source">Sources</string>
@ -60,7 +59,6 @@
<string name="error_bookmarks_text">Marque-pages</string>
<string name="action_remove_from_bookmarks">Supprimer</string>
<string name="sort_apply">Appliquer</string>
<string name="sort_cancel">Annuler</string>
<string name="player_speed">Vitesse de lecture</string>
<string name="preview_background_img_des">Aperçu de l\'arrière-plan</string>
<string name="benene">Donner une benene aux devs</string>
@ -80,6 +78,7 @@
<string name="episode_short">E</string>
<string name="delete_file">Supprimer le Fichier</string>
<string name="delete">Supprimer</string>
<string name="cancel">Annuler</string>
<string name="pause">Pause</string>
<string name="resume">Reprendre</string>
<string name="delete_message">Cela va supprimer définitivement %s
@ -94,7 +93,7 @@
<string name="synopsis">Synopsis</string>
<string name="queued">Liste d\'attente</string>
<string name="no_subtitles">Pas de sous-titres</string>
<string name="default_subtitles">Défault</string>
<string name="action_default">Défault</string>
<string name="free_storage">Libre</string>
<string name="used_storage">Utilisé</string>
<string name="app_storage">Application</string>
@ -553,5 +552,4 @@
<string name="unable_to_inflate">L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s</string>
<string name="automatic_plugin_download_mode_title">Sélectionnez le mode pour filtrer le téléchargement des plugins</string>
<string name="profile_background_des">Fond de profil</string>
<string name="default_account">@string/default_subtitles</string>
</resources>

View file

@ -29,13 +29,11 @@
<string name="type_completed">Completado</string>
<string name="type_dropped">Descartado</string>
<string name="type_plan_to_watch">Planeando ver</string>
<string name="type_none">Ningún</string>
<string name="type_re_watching">Remirando</string>
<string name="error_bookmarks_text">Marcadores</string>
<string name="action_remove_from_bookmarks">Borrar</string>
<string name="action_add_to_bookmarks">Seleccionar estado de visualización</string>
<string name="sort_apply">Aplicar</string>
<string name="sort_cancel">Cancelar</string>
<string name="sort_copy">Copiar</string>
<string name="sort_close">Cerrar</string>
<string name="sort_clear">Limpar</string>

View file

@ -49,7 +49,6 @@
<string name="error_bookmarks_text">बुकमार्क्स</string>
<string name="action_remove_from_bookmarks">हटाएँ</string>
<string name="sort_apply">लागू करें</string>
<string name="sort_cancel">रद्द करें</string>
<string name="player_speed">प्लेयर स्पीड</string>
<string name="search_provider_text_providers">प्रोवाइडरों का उपयोग कर खोजें</string>
<string name="search_provider_text_types">प्रकार का उपयोग करके खोजें</string>
@ -91,6 +90,7 @@
<string name="acra_report_toast">क्षमा करें, एप्प क्रैश हो गया है । निर्माताओं को एक अनाम बग रिपोर्ट भेजी जाएगी</string>
<string name="delete_file">फ़ाइल डिलीट करें</string>
<string name="delete">डिलीट</string>
<string name="cancel">रद्द करें</string>
<string name="pause">रोकें</string>
<string name="resume">फिर से चलाएं</string>
<string name="delete_message">इससे %s स्थायी रूप से हट जाएगा

View file

@ -54,7 +54,6 @@
<string name="type_completed">Dovršeno</string>
<string name="type_dropped">Ispušteno</string>
<string name="type_plan_to_watch">Planiram pogledati</string>
<string name="type_none">Ništa</string>
<string name="type_re_watching">Ponovno gledam</string>
<string name="play_movie_button">Pokreni Film</string>
<string name="play_livestream_button">Pokreni LiveStream</string>
@ -92,7 +91,6 @@
<string name="action_remove_from_bookmarks">Ukloni</string>
<string name="action_add_to_bookmarks">Postavi status gledanja</string>
<string name="sort_apply">Primijeni</string>
<string name="sort_cancel">Poništi</string>
<string name="sort_copy">Kopiraj</string>
<string name="sort_close">Zatvori</string>
<string name="sort_clear">Očisti</string>
@ -200,6 +198,7 @@
<string name="no_episodes_found">Nisu pronađene epizode</string>
<string name="delete_file">Izbriši datoteku</string>
<string name="delete">Izbriši</string>
<string name="cancel">Poništi</string>
<string name="pause">Pauziraj</string>
<string name="resume">Nastavi</string>
<string name="go_back_30">-30</string>
@ -218,7 +217,7 @@
<string name="synopsis">Sinopsis</string>
<string name="queued">u redu čekanja</string>
<string name="no_subtitles">Bez titlova</string>
<string name="default_subtitles">Zadano</string>
<string name="action_default">Zadano</string>
<string name="free_storage">Slobodno</string>
<string name="used_storage">Iskorišteno</string>
<string name="app_storage">Aplikacija</string>
@ -567,7 +566,6 @@
<string name="unable_to_inflate">Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s</string>
<string name="automatic_plugin_download_mode_title">Odaberi modus za filtriranje preuzimanja dodataka</string>
<string name="disable">Onemogući</string>
<string name="default_account">@string/default_subtitles</string>
<string name="no_plugins_found_error">U repozitoriju nisu pronađeni dodaci</string>
<string name="no_repository_found_error">Repozitorij nije pronađen, provjerite URL i pokušajte koristiti VPN</string>
<string name="quality_profile_help">Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je video prioritet.

View file

@ -20,6 +20,7 @@
<string name="download">Letöltés</string>
<string name="search">Keresés</string>
<string name="delete">Törlés</string>
<string name="cancel">Mégse</string>
<string name="pause">Szüneteltetés</string>
<string name="queued">sorba állítva</string>
<string name="resize_fit">Igazítás</string>
@ -61,7 +62,6 @@
<string name="type_watching">Nézés</string>
<string name="type_completed">Befejezve</string>
<string name="type_plan_to_watch">Később megnézés</string>
<string name="type_none">Nincs</string>
<string name="type_re_watching">Újranézés</string>
<string name="play_movie_button">Film lejátszása</string>
<string name="play_trailer_button">Előzetes lejátszása</string>
@ -90,7 +90,6 @@
<string name="action_remove_from_bookmarks">Eltávolítás</string>
<string name="action_add_to_bookmarks">Megtekintés állapotának beállítása</string>
<string name="sort_apply">Alkalmazás</string>
<string name="sort_cancel">Mégse</string>
<string name="sort_copy">Másolás</string>
<string name="sort_close">Bezárás</string>
<string name="sort_clear">Törlés</string>
@ -162,7 +161,7 @@
<string name="rating">Értékelés</string>
<string name="cartoons">Rajzfilmek</string>
<string name="livestreams">Élőadások</string>
<string name="default_subtitles">Alapértelmezett</string>
<string name="action_default">Alapértelmezett</string>
<string name="movies">Filmek</string>
<string name="tv_series">TV sorozat</string>
<string name="anime">Anime</string>

View file

@ -39,7 +39,6 @@
<string name="type_completed">Selesai</string>
<string name="type_dropped">Dihentikan</string>
<string name="type_plan_to_watch">Rencana untuk Menonton</string>
<string name="type_none">Tidak Ada</string>
<string name="type_re_watching">Menonton Ulang</string>
<string name="play_movie_button">Putar Movie</string>
<string name="play_torrent_button">Streaming Torrent</string>
@ -75,7 +74,6 @@
<string name="action_remove_from_bookmarks">Hapus</string>
<string name="action_add_to_bookmarks">Atur status tontonan</string>
<string name="sort_apply">Terapkan</string>
<string name="sort_cancel">Batalkan</string>
<string name="sort_copy">Salin</string>
<string name="sort_close">Tutup</string>
<string name="sort_clear">Bersihkan</string>
@ -174,6 +172,7 @@
<string name="no_episodes_found">Episode Tidak Ditemukan</string>
<string name="delete_file">Hapus File</string>
<string name="delete">Hapus</string>
<string name="cancel">Batalkan</string>
<string name="pause">Jeda</string>
<string name="resume">Lanjutkan</string>
<string name="go_back_30">-30</string>
@ -192,7 +191,7 @@
<string name="synopsis">Sinopsis</string>
<string name="queued">antri</string>
<string name="no_subtitles">Tidak Ada Subtitle</string>
<string name="default_subtitles">Default</string>
<string name="action_default">Default</string>
<string name="free_storage">Tersedia</string>
<string name="used_storage">Terpakai</string>
<string name="app_storage">Aplikasi</string>
@ -575,5 +574,4 @@
<string name="no_plugins_found_error">Tidak ada plugin yang ditemukan di repositori</string>
<string name="no_repository_found_error">Repositori tidak ditemukan, periksa URL dan coba VPN</string>
<string name="already_voted">Kamu sudah voting</string>
<string name="default_account">@string/default_subtitles</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> -->
@ -44,7 +44,6 @@
<string name="type_completed">Completato</string>
<string name="type_dropped">Abbandonato</string>
<string name="type_plan_to_watch">Da guardare</string>
<string name="type_none">Nessuno</string>
<string name="type_re_watching">Riguardando</string>
<string name="play_movie_button">Riproduci film</string>
<string name="play_livestream_button">Riproduci Livestream</string>
@ -81,7 +80,6 @@
<string name="action_remove_from_bookmarks">Rimuovi</string>
<string name="action_add_to_bookmarks">Imposta stato riproduzione</string>
<string name="sort_apply">Applica</string>
<string name="sort_cancel">Cancella</string>
<string name="sort_copy">Copia</string>
<string name="sort_close">Chiudi</string>
<string name="sort_clear">Cancella</string>
@ -190,6 +188,7 @@
<string name="no_episodes_found">Nessun episodio trovato</string>
<string name="delete_file">Elimina file</string>
<string name="delete">Elimina</string>
<string name="cancel">Cancella</string>
<string name="pause">Pausa</string>
<string name="resume">Riprendi</string>
<string name="go_back_30">-30</string>
@ -208,7 +207,7 @@
<string name="synopsis">Sinossi</string>
<string name="queued">In coda</string>
<string name="no_subtitles">Nessun sottotiolo</string>
<string name="default_subtitles">Default</string>
<string name="action_default">Default</string>
<string name="free_storage">Libero</string>
<string name="used_storage">Usato</string>
<string name="app_storage">App</string>
@ -410,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>
@ -572,7 +571,32 @@
<string name="no_repository_found_error">Repository non trovato, controlla l\'URL e prova la VPN</string>
<string name="unable_to_inflate">Non è stato possibile creare correttamente l\'interfaccia utente, questo è un GRANDE BUG e dovrebbe essere segnalato immediatamente %s</string>
<string name="automatic_plugin_download_mode_title">Seleziona la modalità per filtrare il download dei plugin</string>
<string name="default_account">@string/default_subtitles</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

@ -66,7 +66,6 @@
<string name="action_remove_from_bookmarks">הסר</string>
<string name="action_add_to_bookmarks">הגדר מצב צפייה</string>
<string name="sort_apply">ליישם</string>
<string name="sort_cancel">בטל</string>
<string name="sort_copy">העתק</string>
<string name="sort_close">לסגור</string>
<string name="sort_clear">נקה</string>
@ -78,7 +77,6 @@
<string name="type_watching">צופה</string>
<string name="pick_subtitle">כתוביות</string>
<string name="type_on_hold">בהמתנה</string>
<string name="type_none">ללא</string>
<string name="download">להוריד</string>
<string name="app_dubbed_text">מדובב</string>
<string name="home_more_info">יותר מידע</string>
@ -146,6 +144,7 @@
<string name="no_episodes_found">לא נמצאו פרקים</string>
<string name="delete_file">מחק קובץ</string>
<string name="delete">מחק</string>
<string name="cancel">בטל</string>
<string name="pause">השהה</string>
<string name="resume">המשך</string>
<string name="go_back_30">-30</string>
@ -159,7 +158,7 @@
<string name="rating">דירוג</string>
<string name="year">שנה</string>
<string name="no_subtitles">ללא כתוביות</string>
<string name="default_subtitles">ברירת מחדל</string>
<string name="action_default">ברירת מחדל</string>
<string name="free_storage">חינם</string>
<string name="used_storage">משומש</string>
<string name="tv_series">סדרת טלוויזיה</string>
@ -519,7 +518,6 @@
<string name="edit">עריכה</string>
<string name="wifi">Wi-Fi</string>
<string name="profile_background_des">רקע הפרופיל</string>
<string name="default_account">@string/default_subtitles</string>
<string name="test_log">רשומה</string>
<string name="help">עזרה</string>
<string name="start">התחלה</string>

View file

@ -37,7 +37,6 @@
<string name="asian_drama_singular">アジアドラマ</string>
<string name="live_singular">ライブ配信</string>
<string name="nsfw_singular">NSFW</string>
<string name="sort_cancel">キャンセル</string>
<string name="anime">アニメ</string>
<string name="video_lock">ロック</string>
<string name="video_source">ソース</string>
@ -71,7 +70,6 @@
<string name="home_source">ソース</string>
<string name="history">履歴</string>
<string name="result_poster_img_des">ポスター</string>
<string name="type_none">なし</string>
<string name="sort_copy">コピー</string>
<string name="sort_close">閉じる</string>
<string name="sort_save">保存</string>
@ -135,6 +133,7 @@
<string name="pause">一時停止</string>
<string name="play_episode_toast">再生エピソード</string>
<string name="delete">削除</string>
<string name="cancel">キャンセル</string>
<string name="start">開始</string>
<string name="status">状態</string>
<string name="year"></string>
@ -155,7 +154,7 @@
<string name="extension_version">バージョン</string>
<string name="extension_rating" formatted="true">視聴率 %s</string>
<string name="rating">視聴率</string>
<string name="default_subtitles">デフォルト</string>
<string name="action_default">デフォルト</string>
<string name="download_failed">ダウンロード失敗</string>
<string name="download_started">ダウンロード開始</string>
<string name="download_done">ダウンロード完了</string>

Some files were not shown because too many files have changed in this diff Show more