diff --git a/.idea/gradle.xml b/.idea/gradle.xml index c5c0ff3b..d7c08c9c 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,17 +4,16 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 51cb26e3..79df7221 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import org.jetbrains.dokka.gradle.DokkaTask +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.ByteArrayOutputStream import java.net.URL @@ -15,6 +16,7 @@ plugins { val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/" val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first() +var isLibraryDebug = false fun String.execute() = ByteArrayOutputStream().use { baot -> if (project.exec { @@ -71,7 +73,7 @@ android { targetSdk = 33 /* Android 14 is Fu*ked ^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/ versionCode = 63 - versionName = "4.3.1" + versionName = "4.3.2" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") @@ -81,9 +83,9 @@ android { val localProperties = gradleLocalProperties(rootDir) buildConfigField( - "String", - "BUILDDATE", - "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));" + "long", + "BUILD_DATE", + "${System.currentTimeMillis()}" ) buildConfigField( "String", @@ -114,6 +116,7 @@ android { ) } debug { + isLibraryDebug = true isDebuggable = true applicationIdSuffix = ".debug" proguardFiles( @@ -175,24 +178,24 @@ repositories { dependencies { // Testing testImplementation("junit:junit:4.13.2") - testImplementation("org.json:json:20231013") + testImplementation("org.json:json:20240303") androidTestImplementation("androidx.test:core") implementation("androidx.test.ext:junit-ktx:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") // Android Core & Lifecycle - implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.6.1") - implementation("androidx.navigation:navigation-ui-ktx:2.7.6") + implementation("androidx.navigation:navigation-ui-ktx:2.7.7") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") - implementation("androidx.navigation:navigation-fragment-ktx:2.7.6") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") // Design & UI implementation("jp.wasabeef:glide-transformations:4.3.0") implementation("androidx.preference:preference-ktx:1.2.1") - implementation("com.google.android.material:material:1.10.0") + implementation("com.google.android.material:material:1.12.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") @@ -203,7 +206,7 @@ dependencies { // For KSP -> Official Annotation Processors are Not Yet Supported for KSP ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") - implementation("com.google.guava:guava:32.1.3-android") + implementation("com.google.guava:guava:33.2.0-android") implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") // Media 3 (ExoPlayer) @@ -220,7 +223,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:6dc25f7") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding @@ -237,9 +240,7 @@ dependencies { implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview // Extensions & Other Libs - implementation("org.mozilla:rhino:1.7.13") /* run JavaScript - ^ Don't Bump RhinoJS to 1.7.14,`NoClassDefFoundError` Occurs and Trailers won't play (even with Desugaring) - NewPipeExtractor Issue */ + implementation("org.mozilla:rhino:1.7.15") // run JavaScript implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9 @@ -273,19 +274,37 @@ dependencies { implementation("androidx.work:work-runtime:2.9.0") implementation("androidx.work:work-runtime-ktx:2.9.0") implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib + + implementation(project(":library") { + this.extra.set("isDebug", isLibraryDebug) + }) } - -tasks.register("androidSourcesJar", Jar::class) { +tasks.register("androidSourcesJar") { archiveClassifier.set("sources") from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources } -// For GradLew Plugin -tasks.register("makeJar", Copy::class) { - from("build/intermediates/compile_app_classes_jar/prereleaseDebug") - into("build") - include("classes.jar") +tasks.register("copyJar") { + from( + "build/intermediates/compile_app_classes_jar/prereleaseDebug", + "../library/build/libs" + ) + into("build/app-classes") + include("classes.jar", "library-jvm*.jar") + // Remove the version + rename("library-jvm.*.jar", "library-jvm.jar") +} + +// Merge the app classes and the library classes into classes.jar +tasks.register("makeJar") { + dependsOn(tasks.getByName("copyJar")) + from( + zipTree("build/app-classes/classes.jar"), + zipTree("build/app-classes/library-jvm.jar") + ) + destinationDirectory.set(layout.buildDirectory) + archivesName = "classes" } tasks.withType { diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index c93f0f9b..1680d698 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -11,7 +11,9 @@ import androidx.fragment.app.FragmentActivity import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.openBrowser import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStore.getKey @@ -31,7 +33,6 @@ import org.acra.sender.ReportSenderFactory import java.io.File import java.io.FileNotFoundException import java.io.PrintStream -import java.lang.Exception import java.lang.ref.WeakReference import kotlin.concurrent.thread import kotlin.system.exitProcess @@ -211,7 +212,7 @@ class AcraApplication : Application() { fun openBrowser(url: String, activity: FragmentActivity?) { openBrowser( url, - isTvSettings(), + isLayout(TV or EMULATOR), activity?.supportFragmentManager?.fragments?.lastOrNull() ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 80de223e..82e985db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -11,11 +11,9 @@ import android.util.DisplayMetrics import android.util.Log import android.view.Gravity import android.view.KeyEvent -import android.view.LayoutInflater import android.view.View import android.view.View.NO_ID import android.view.ViewGroup -import android.widget.TextView import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts @@ -31,11 +29,13 @@ import com.google.android.material.chip.ChipGroup import com.google.android.material.navigationrail.NavigationRailView import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps +import com.lagradost.cloudstream3.databinding.ToastBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.PlayerEventType import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.UiText -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv +import com.lagradost.cloudstream3.ui.settings.Globals.updateTv import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event @@ -99,8 +99,7 @@ object CommonActivity { var playerEventListener: ((PlayerEventType) -> Unit)? = null var keyEventListener: ((Pair) -> Boolean)? = null - - var currentToast: Toast? = null + private var currentToast: Toast? = null fun showToast(@StringRes message: Int, duration: Int? = null) { val act = activity ?: return @@ -156,25 +155,19 @@ object CommonActivity { } catch (e: Exception) { logError(e) } + try { - val inflater = - act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater - - val layout: View = inflater.inflate( - R.layout.toast, - act.findViewById(R.id.toast_layout_root) as ViewGroup? - ) - - val text = layout.findViewById(R.id.text) as TextView - text.text = message.trim() + val binding = ToastBinding.inflate(act.layoutInflater) + binding.text.text = message.trim() + // custom toasts are deprecated and won't appear when cs3 sets minSDK to api30 (A11) val toast = Toast(act) - toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) toast.duration = duration ?: Toast.LENGTH_SHORT - toast.view = layout - //https://github.com/PureWriter/ToastCompat - toast.show() + toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) + toast.view = binding.root currentToast = toast + toast.show() + } catch (e: Exception) { logError(e) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 7a25b738..07a82583 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -18,6 +18,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklAp import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.syncproviders.providers.SimklApi import com.lagradost.cloudstream3.ui.player.SubtitleData +import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.Coroutines.mainWork @@ -30,19 +31,16 @@ import java.text.SimpleDateFormat import java.util.* import kotlin.math.absoluteValue -const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" - -//val baseHeader = mapOf("User-Agent" to USER_AGENT) -val mapper = JsonMapper.builder().addModule(kotlinModule()) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! - /** * Defines the constant for the all languages preference, if this is set then it is * the equivalent of all languages being set **/ const val AllLanguagesName = "universal" +//val baseHeader = mapOf("User-Agent" to USER_AGENT) +val mapper = JsonMapper.builder().addModule(kotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! + object APIHolder { val unixTime: Long get() = System.currentTimeMillis() / 1000L @@ -119,7 +117,9 @@ object APIHolder { } fun LoadResponse.getId(): Int { - return getLoadResponseIdFromUrl(url, apiName) + // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked + return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) + ?: getLoadResponseIdFromUrl(url, apiName) } /** @@ -220,10 +220,15 @@ object APIHolder { } ?: false val matchingTypes = types?.any { it.name.equals(media.format, true) } == true - if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears + if (lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears } ?: return null - Tracker(res.idMal, res.id.toString(), res.coverImage?.extraLarge ?: res.coverImage?.large, res.bannerImage) + Tracker( + res.idMal, + res.id.toString(), + res.coverImage?.extraLarge ?: res.coverImage?.large, + res.bannerImage + ) } catch (t: Throwable) { logError(t) null @@ -741,8 +746,6 @@ fun base64Encode(array: ByteArray): String { } } -class ErrorLoadingException(message: String? = null) : Exception(message) - fun MainAPI.fixUrlNull(url: String?): String? { if (url.isNullOrEmpty()) { return null @@ -863,7 +866,12 @@ enum class TvType(value: Int?) { AsianDrama(9), Live(10), NSFW(11), - Others(12) + Others(12), + Music(13), + AudioBook(14), + + /** Wont load the built in player, make your own interaction */ + CustomMedia(15), } public enum class AutoDownloadMode(val value: Int) { @@ -1249,13 +1257,15 @@ interface LoadResponse { fun LoadResponse.getImdbId(): String? { return normalSafeApiCall { - SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb) + SimklApi.readIdFromString(this.syncData[simklIdPrefix]) + ?.get(SimklApi.Companion.SyncServices.Imdb) } } fun LoadResponse.getTMDbId(): String? { return normalSafeApiCall { - SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb) + SimklApi.readIdFromString(this.syncData[simklIdPrefix]) + ?.get(SimklApi.Companion.SyncServices.Tmdb) } } @@ -1444,11 +1454,24 @@ fun TvType?.isEpisodeBased(): Boolean { return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama) } - data class NextAiring( val episode: Int, val unixTime: Long, -) + val season: Int? = null, +) { + /** + * Secondary constructor for backwards compatibility without season. + * TODO Remove this constructor after there is a new stable release and extensions are updated to support season. + */ + constructor( + episode: Int, + unixTime: Long, + ) : this ( + episode, + unixTime, + null + ) +} /** * @param season To be mapped with episode season, not shown in UI if displaySeason is defined @@ -1539,8 +1562,26 @@ data class TorrentLoadResponse( posterHeaders: Map? = null, backgroundPosterUrl: String? = null, ) : this( - name, url, apiName, magnet, torrent, plot, type, posterUrl, year, rating, tags, duration, trailers, - recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null + name, + url, + apiName, + magnet, + torrent, + plot, + type, + posterUrl, + year, + rating, + tags, + duration, + trailers, + recommendations, + actors, + comingSoon, + syncData, + posterHeaders, + backgroundPosterUrl, + null ) } @@ -1592,7 +1633,8 @@ data class AnimeLoadResponse( return this.episodes.maxOf { (_, episodes) -> episodes.count { episodeData -> // Prioritize display season as actual season may be something random to fit multiple seasons into one. - val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE + val episodeSeason = + displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE // Count all episodes from season 1 to below the current season. episodeSeason in 1..? = null, backgroundPosterUrl: String? = null, ) : this( - engName, japName, name, url, apiName, type, posterUrl, year, episodes, showStatus, plot, tags, - synonyms, rating, duration, trailers, recommendations, actors, comingSoon, syncData, posterHeaders, - nextAiring, seasonNames, backgroundPosterUrl, null + engName, + japName, + name, + url, + apiName, + type, + posterUrl, + year, + episodes, + showStatus, + plot, + tags, + synonyms, + rating, + duration, + trailers, + recommendations, + actors, + comingSoon, + syncData, + posterHeaders, + nextAiring, + seasonNames, + backgroundPosterUrl, + null ) } @@ -1763,7 +1827,7 @@ data class MovieLoadResponse( backgroundPosterUrl: String? = null, ) : this( name, url, apiName, type, dataUrl, posterUrl, year, plot, rating, tags, duration, trailers, - recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl,null + recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null ) } @@ -1906,7 +1970,8 @@ data class TvSeriesLoadResponse( return episodes.count { episodeData -> // Prioritize display season as actual season may be something random to fit multiple seasons into one. - val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE + val episodeSeason = + displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE // Count all episodes from season 1 to below the current season. episodeSeason in 1..? = null, backgroundPosterUrl: String? = null, ) : this( - name, url, apiName, type, episodes, posterUrl, year, plot, showStatus, rating, tags, duration, - trailers, recommendations, actors, comingSoon, syncData, posterHeaders, nextAiring, seasonNames, - backgroundPosterUrl, null + name, + url, + apiName, + type, + episodes, + posterUrl, + year, + plot, + showStatus, + rating, + tags, + duration, + trailers, + recommendations, + actors, + comingSoon, + syncData, + posterHeaders, + nextAiring, + seasonNames, + backgroundPosterUrl, + null ) } @@ -2005,6 +2089,7 @@ data class AniSearch( @JsonProperty("extraLarge") var extraLarge: String? = null, @JsonProperty("large") var large: String? = null, ) + data class Title( @JsonProperty("romaji") var romaji: String? = null, @JsonProperty("english") var english: String? = null, diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 0cfe1b5f..8ff710fe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -86,6 +86,7 @@ import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver +import com.lagradost.cloudstream3.services.SubscriptionWorkManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.BackupApis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers @@ -113,11 +114,11 @@ import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SearchFragment import com.lagradost.cloudstream3.ui.search.SearchResultBuilder -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTruePhone -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import com.lagradost.cloudstream3.ui.settings.Globals.updateTv import com.lagradost.cloudstream3.ui.settings.SettingsGeneral import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions @@ -135,7 +136,10 @@ import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -158,6 +162,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.utils.fcast.FcastManager import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser import com.lagradost.safefile.SafeFile @@ -170,7 +175,6 @@ import java.net.URLDecoder import java.nio.charset.Charset import kotlin.math.abs import kotlin.math.absoluteValue -import kotlin.reflect.KClass import kotlin.system.exitProcess //https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898 @@ -183,116 +187,93 @@ import kotlin.system.exitProcess //https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225 -const val VLC_PACKAGE = "org.videolan.vlc" -const val MPV_PACKAGE = "is.xyz.mpv" -const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo" - -val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity") -val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") - -//TODO REFACTOR AF -open class ResultResume( - val packageString: String, - val action: String = Intent.ACTION_VIEW, - val position: String? = null, - val duration: String? = null, - var launcher: ActivityResultLauncher? = null, -) { - val defaultTime = -1L - - val lastId get() = "${packageString}_last_open_id" - suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { - val intent = Intent(action) - - if (id != null) - setKey(lastId, id) - else - removeKey(lastId) - - intent.setPackage(packageString) - callback.invoke(intent) - launcher?.launch(intent) - } - - open fun getPosition(intent: Intent?): Long { - return defaultTime - } - - open fun getDuration(intent: Intent?): Long { - return defaultTime - } -} - -val VLC = object : ResultResume( - VLC_PACKAGE, - // Android 13 intent restrictions fucks up specifically launching the VLC player - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - "org.videolan.vlc.player.result" - } else { - Intent.ACTION_VIEW - }, - "extra_position", - "extra_duration", -) { - override fun getPosition(intent: Intent?): Long { - return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime - } - - override fun getDuration(intent: Intent?): Long { - return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime - } -} - -val MPV = object : ResultResume( - MPV_PACKAGE, - //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: - position = "position", - duration = "duration", -) { - override fun getPosition(intent: Intent?): Long { - return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime - } - - override fun getDuration(intent: Intent?): Long { - return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime - } -} - -val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) - -val resumeApps = arrayOf( - VLC, MPV, WEB_VIDEO -) - -// Short name for requests client to make it nicer to use - -var app = Requests(responseParser = object : ResponseParser { - val mapper: ObjectMapper = jacksonObjectMapper().configure( - DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, - false - ) - - override fun parse(text: String, kClass: KClass): T { - return mapper.readValue(text, kClass.java) - } - - override fun parseSafe(text: String, kClass: KClass): T? { - return try { - mapper.readValue(text, kClass.java) - } catch (e: Exception) { - null - } - } - - override fun writeValueAsString(obj: Any): String { - return mapper.writeValueAsString(obj) - } -}).apply { - defaultHeaders = mapOf("user-agent" to USER_AGENT) -} - -class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAuthenticator.BiometricAuthCallback { +class MainActivity : AppCompatActivity(), ColorPickerDialogListener, + BiometricAuthenticator.BiometricAuthCallback { companion object { + const val VLC_PACKAGE = "org.videolan.vlc" + const val MPV_PACKAGE = "is.xyz.mpv" + const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo" + + val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity") + val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") + + //TODO REFACTOR AF + open class ResultResume( + val packageString: String, + val action: String = Intent.ACTION_VIEW, + val position: String? = null, + val duration: String? = null, + var launcher: ActivityResultLauncher? = null, + ) { + val defaultTime = -1L + + val lastId get() = "${packageString}_last_open_id" + suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { + val intent = Intent(action) + + if (id != null) + setKey(lastId, id) + else + removeKey(lastId) + + intent.setPackage(packageString) + callback.invoke(intent) + launcher?.launch(intent) + } + + open fun getPosition(intent: Intent?): Long { + return defaultTime + } + + open fun getDuration(intent: Intent?): Long { + return defaultTime + } + } + + val VLC = object : ResultResume( + VLC_PACKAGE, + // Android 13 intent restrictions fucks up specifically launching the VLC player + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + "org.videolan.vlc.player.result" + } else { + Intent.ACTION_VIEW + }, + "extra_position", + "extra_duration", + ) { + override fun getPosition(intent: Intent?): Long { + return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime + } + + override fun getDuration(intent: Intent?): Long { + return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime + } + } + + val MPV = object : ResultResume( + MPV_PACKAGE, + //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: + position = "position", + duration = "duration", + ) { + override fun getPosition(intent: Intent?): Long { + return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() + ?: defaultTime + } + + override fun getDuration(intent: Intent?): Long { + return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() + ?: defaultTime + } + } + + val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) + + val resumeApps = arrayOf( + VLC, MPV, WEB_VIDEO + ) + + const val TAG = "MAINACT" const val ANIMATED_OUTLINE: Boolean = false var lastError: String? = null @@ -338,10 +319,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu // kinda shitty solution, but cant com main->home otherwise for popups val bookmarksUpdatedEvent = Event() + /** * Used by DataStoreHelper to fully reload home when switching accounts */ val reloadHomeEvent = Event() + /** * Used by DataStoreHelper to fully reload library when switching accounts */ @@ -469,7 +452,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } var lastPopup: SearchResponse? = null - fun loadPopup(result: SearchResponse, load : Boolean = true) { + fun loadPopup(result: SearchResponse, load: Boolean = true) { lastPopup = result val syncName = syncViewModel.syncName(result.apiName) @@ -490,8 +473,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu .contains(DubStatus.Dubbed) ) DubStatus.Dubbed else DubStatus.Subbed, null ) - }else { - viewModel.loadSmall(this,result) + } else { + viewModel.loadSmall(this, result) } } @@ -556,7 +539,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu binding?.navHostFragment?.apply { val params = layoutParams as ConstraintLayout.LayoutParams val push = - if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0 + if (!dontPush && isLayout(TV or EMULATOR)) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0 if (!this.isLtr()) { params.setMargins( @@ -583,7 +566,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } Configuration.ORIENTATION_PORTRAIT -> { - isTvSettings() + isLayout(TV or EMULATOR) } else -> { @@ -671,7 +654,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } } - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + override fun dispatchKeyEvent(event: KeyEvent): Boolean { val response = CommonActivity.dispatchKeyEvent(this, event) if (response != null) return response @@ -791,9 +774,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } lateinit var viewModel: ResultViewModel2 - lateinit var syncViewModel : SyncViewModel + lateinit var syncViewModel: SyncViewModel + /** kinda dirty, however it signals that we should use the watch status as sync or not*/ - var isLocalList : Boolean = false + var isLocalList: Boolean = false override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { viewModel = ViewModelProvider(this)[ResultViewModel2::class.java] @@ -1109,8 +1093,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } } - private fun centerView(view : View?) { - if(view == null) return + private fun centerView(view: View?) { + if (view == null) return try { Log.v(TAG, "centerView: $view") val r = Rect(0, 0, 0, 0) @@ -1176,11 +1160,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH binding = try { - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) setContentView(newLocalBinding.root) - if (isTrueTvSettings() && ANIMATED_OUTLINE) { + if (isLayout(TV) && ANIMATED_OUTLINE) { TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) @@ -1192,7 +1176,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu newLocalBinding.focusOutline.isVisible = false } - if(isTrueTvSettings()) { + if (isLayout(TV)) { // Put here any button you don't want focusing it to center the view val exceptionButtons = listOf( R.id.home_preview_play_btt, @@ -1209,7 +1193,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu R.id.result_search_Button, R.id.result_episodes_show_button, ) - + newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> if (exceptionButtons.contains(newFocus?.id)) return@addOnGlobalFocusChangeListener centerView(newFocus) @@ -1227,18 +1211,20 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu null } - changeStatusBarState(isEmulatorSettings()) + changeStatusBarState(isLayout(EMULATOR)) /** Biometric stuff for users without accounts **/ - val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) - val noAccounts = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false) || accounts.count() <= 1 + val noAccounts = settingsManager.getBoolean( + getString(R.string.skip_startup_account_select_key), + false + ) || accounts.count() <= 1 - if (isTruePhone() && authEnabled && noAccounts) { + if (isLayout(PHONE) && isAuthEnabled(this) && noAccounts) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication(this, R.string.biometric_authentication_title, false) - BiometricAuthenticator.promptInfo?.let { - BiometricAuthenticator.biometricPrompt?.authenticate(it) + promptInfo?.let { prompt -> + biometricPrompt?.authenticate(prompt) } // hide background while authenticating, Sorry moms & dads 🙏 @@ -1330,7 +1316,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } - fun setUserData(status : Resource?) { + fun setUserData(status: Resource?) { if (isLocalList) return bottomPreviewBinding?.apply { when (status) { @@ -1355,7 +1341,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } } - fun setWatchStatus(state : WatchType?) { + fun setWatchStatus(state: WatchType?) { if (!isLocalList || state == null) return bottomPreviewBinding?.resultviewPreviewBookmark?.apply { @@ -1364,13 +1350,42 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } } - observe(viewModel.watchStatus) { state -> - setWatchStatus(state) - } - observe(syncViewModel.userData) { status -> - setUserData(status) + fun setSubscribeStatus(state: Boolean?) { + bottomPreviewBinding?.resultviewPreviewSubscribe?.apply { + if (state != null) { + val drawable = if (state) { + R.drawable.ic_baseline_notifications_active_24 + } else { + R.drawable.baseline_notifications_none_24 + } + setImageResource(drawable) + } + isVisible = state != null + + setOnClickListener { + viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? -> + if (newStatus == null) return@toggleSubscriptionStatus + + val message = if (newStatus) { + // Kinda icky to have this here, but it works. + SubscriptionWorkManager.enqueuePeriodicWork(context) + R.string.subscription_new + } else { + R.string.subscription_deleted + } + + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + showToast(txt(message, name), Toast.LENGTH_SHORT) + } + } + } } + observe(viewModel.watchStatus, ::setWatchStatus) + observe(syncViewModel.userData, ::setUserData) + observeNullable(viewModel.subscribeStatus, ::setSubscribeStatus) + observeNullable(viewModel.page) { resource -> if (resource == null) { hidePreviewPopupDialog() @@ -1412,6 +1427,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu setUserData(syncViewModel.userData.value) setWatchStatus(viewModel.watchStatus.value) + setSubscribeStatus(viewModel.subscribeStatus.value) resultviewPreviewBookmark.setOnClickListener { //viewModel.updateWatchStatus(WatchType.PLANTOWATCH) @@ -1430,7 +1446,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu ) } } else { - val value = (syncViewModel.userData.value as? Resource.Success)?.value?.status ?: SyncWatchType.NONE + val value = + (syncViewModel.userData.value as? Resource.Success)?.value?.status + ?: SyncWatchType.NONE this@MainActivity.showBottomDialog( SyncWatchType.values().map { getString(it.stringRes) }.toList(), @@ -1457,7 +1475,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu resultviewPreviewFavorite.setImageResource(drawable) } - resultviewPreviewFavorite.setOnClickListener{ + resultviewPreviewFavorite.setOnClickListener { viewModel.toggleFavoriteStatus(this@MainActivity) { newStatus: Boolean? -> if (newStatus == null) return@toggleFavoriteStatus @@ -1473,7 +1491,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } } - if (!isTvSettings()) // dont want this clickable on tv layout + if (isLayout(PHONE)) // dont want this clickable on tv layout resultviewPreviewDescription.setOnClickListener { view -> view.context?.let { ctx -> val builder: AlertDialog.Builder = @@ -1548,7 +1566,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } } - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { if (navDestination.matchDestination(R.id.navigation_home)) { attachBackPressedCallback() } else detachBackPressedCallback() @@ -1584,7 +1602,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu itemRippleColor = rippleColor itemActiveIndicatorColor = rippleColor setupWithNavController(navController) - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { background?.alpha = 200 } else { background?.alpha = 255 @@ -1718,6 +1736,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu runAutoUpdate() } + FcastManager().init(this, false) + APIRepository.dubStatusActive = getApiDubstatusSettings() try { @@ -1754,8 +1774,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } } catch (e: Exception) { logError(e) - } finally { - setKey(HAS_DONE_SETUP_KEY, true) } // Used to check current focus for TV @@ -1791,6 +1809,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu binding?.navHostFragment?.isInvisible = false } + override fun onAuthenticationError() { + finish() + } + private var backPressedCallback: OnBackPressedCallback? = null private fun attachBackPressedCallback() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index f03a5525..26567c7a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -2,9 +2,7 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.extractors.helper.* import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler -import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper @@ -28,30 +26,39 @@ open class Chillx : ExtractorApi() { override val name = "Chillx" override val mainUrl = "https://chillx.top" override val requiresReferer = true - private var key: String? = null + companion object { + private var key: String? = null + + suspend fun fetchKey(): String { + return if (key != null) { + key!! + } else { + val fetch = app.get("https://raw.githubusercontent.com/rushi-chavan/multi-keys/keys/keys.json").parsedSafe()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key") + key = fetch + key!! + } + } + } + + @Suppress("NAME_SHADOWING") override suspend fun getUrl( url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val master = Regex("\\s*=\\s*'([^']+)").find( + val master = Regex("""JScript[\w+]?\s*=\s*'([^']+)""").find( app.get( url, - referer = referer ?: "", - headers = mapOf( - "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", - "Accept-Language" to "en-US,en;q=0.5", - ) + referer = url, ).text )?.groupValues?.get(1) - val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") - + val key = fetchKey() + val decrypt = cryptoAESHandler(master ?: "", key.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex() + val subtitlePattern = """\[(.*?)](https?://[^\s,]+)""".toRegex() val matches = subtitlePattern.findAll(subtitles ?: "") val languageUrlPairs = matches.map { matchResult -> val (language, url) = matchResult.destructured @@ -83,23 +90,18 @@ open class Chillx : ExtractorApi() { headers = headers ).forEach(callback) } - + private fun decodeUnicodeEscape(input: String): String { val regex = Regex("u([0-9a-fA-F]{4})") return regex.replace(input) { it.groupValues[1].toInt(16).toChar().toString() } } - - suspend fun getKey() = key ?: fetchKey().also { key = it } - private suspend fun fetchKey(): String { - return app.get("https://raw.githubusercontent.com/Sofie99/Resources/main/chillix_key.json").parsed() - } - data class Tracks( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, - @JsonProperty("kind") val kind: String? = null, + + data class Keys( + @JsonProperty("chillx") val key: List ) + } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt index 0df93dc5..2343a92e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt @@ -9,10 +9,16 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import java.net.URL +class Geodailymotion : Dailymotion() { + override val name = "GeoDailymotion" + override val mainUrl = "https://geo.dailymotion.com" +} + open class Dailymotion : ExtractorApi() { override val mainUrl = "https://www.dailymotion.com" override val name = "Dailymotion" override val requiresReferer = false + private val baseUrl = "https://www.dailymotion.com" @Suppress("RegExpSimplifiable") private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex() @@ -34,7 +40,7 @@ open class Dailymotion : ExtractorApi() { val dmV1st = config.dmInternalData.v1st val dmTs = config.dmInternalData.ts val embedder = config.context.embedder - val metaDataUrl = "$mainUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" + val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies) .parsedSafe() ?: return metaData.qualities.forEach { (_, video) -> @@ -45,16 +51,19 @@ open class Dailymotion : ExtractorApi() { } private fun getEmbedUrl(url: String): String? { - if (url.contains("/embed/")) { - return url - } - val vid = getVideoId(url) ?: return null - return "$mainUrl/embed/video/$vid" + if (url.contains("/embed/") || url.contains("/video/")) { + return url } + if (url.contains("geo.dailymotion.com")) { + val videoId = url.substringAfter("video=") + return "$baseUrl/embed/video/$videoId" + } + return null + } private fun getVideoId(url: String): String? { val path = URL(url).path - val id = path.substringAfter("video/") + val id = path.substringAfter("/video/") if (id.matches(videoIdRegex)) { return id } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt new file mode 100644 index 00000000..2cb12e16 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt @@ -0,0 +1,27 @@ +package com.lagradost.cloudstream3.extractors + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson + +open class EPlayExtractor : ExtractorApi() { + override var name = "EPlay" + override var mainUrl = "https://eplayvid.net" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List? { + val response = app.get(url).document + val trueUrl = response.select("source").attr("src") + return listOf( + ExtractorLink( + this.name, + this.name, + trueUrl, + mainUrl, + getQualityFromName(""), // this needs to be auto + false + ) + ) + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt index b557a53e..db721108 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt @@ -20,4 +20,9 @@ class PlayRu : ContentX() { class FourPlayRu : ContentX() { override var name = "FourPlayRu" override var mainUrl = "https://four.playru.net" -} \ No newline at end of file +} + +class FourPichive : ContentX() { + override var name = "FourPichive" + override var mainUrl = "https://four.pichive.online" +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt new file mode 100644 index 00000000..2655670d --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -0,0 +1,70 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import java.net.URLDecoder +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec + +class VidSrcTo : ExtractorApi() { + override val name = "VidSrcTo" + override val mainUrl = "https://vidsrc.to" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return + val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return + if (res.status != 200) return + res.result?.amap { source -> + try { + val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap + val finalUrl = DecryptUrl(embedRes.result.encUrl) + if(finalUrl.equals(embedRes.result.encUrl)) return@amap + when (source.title) { + "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + } + } catch (e: Exception) { + logError(e) + } + } + } + + private fun DecryptUrl(encUrl: String): String { + var data = encUrl.toByteArray() + data = Base64.decode(data, Base64.URL_SAFE) + val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters) + data = cipher.doFinal(data) + return URLDecoder.decode(data.toString(Charsets.UTF_8), "utf-8") + } + + data class VidsrctoEpisodeSources( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: List? + ) + + data class VidsrctoResult( + @JsonProperty("id") val id: String, + @JsonProperty("title") val title: String + ) + + data class VidsrctoEmbedSource( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: VidsrctoUrl + ) + + data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt new file mode 100644 index 00000000..230a9e1a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt @@ -0,0 +1,101 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE +import com.lagradost.cloudstream3.utils.Qualities +import org.mozilla.javascript.Context +import org.mozilla.javascript.NativeJSON +import org.mozilla.javascript.NativeObject +import org.mozilla.javascript.Scriptable +import java.util.Base64 + +open class Vidguardto : ExtractorApi() { + override val name = "Vidguard" + override val mainUrl = "https://vidguard.to" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url) + val resc = res.document.select("script:containsData(eval)").firstOrNull()?.data() + resc?.let { + val jsonStr2 = AppUtils.parseJson(runJS2(it)) + val watchlink = sigDecode(jsonStr2.stream) + + callback.invoke( + ExtractorLink( + this.name, + name, + watchlink, + this.mainUrl, + Qualities.Unknown.value, + INFER_TYPE + ) + ) + } + } + + private fun sigDecode(url: String): String { + val sig = url.split("sig=")[1].split("&")[0] + var t = "" + for (v in sig.chunked(2)) { + val byteValue = Integer.parseInt(v, 16) xor 2 + t += byteValue.toChar() + } + val padding = when (t.length % 4) { + 2 -> "==" + 3 -> "=" + else -> "" + } + val decoded = Base64.getDecoder().decode((t + padding).toByteArray(Charsets.UTF_8)) + t = String(decoded).dropLast(5).reversed() + val charArray = t.toCharArray() + for (i in 0 until charArray.size - 1 step 2) { + val temp = charArray[i] + charArray[i] = charArray[i + 1] + charArray[i + 1] = temp + } + val modifiedSig = String(charArray).dropLast(5) + return url.replace(sig, modifiedSig) + } + + private fun runJS2(hideMyHtmlContent: String): String { + Log.d("runJS", "start") + val rhino = Context.enter() + rhino.initSafeStandardObjects() + rhino.optimizationLevel = -1 + val scope: Scriptable = rhino.initSafeStandardObjects() + scope.put("window", scope, scope) + var result = "" + try { + Log.d("runJS", "Executing JavaScript: $hideMyHtmlContent") + rhino.evaluateString(scope, hideMyHtmlContent, "JavaScript", 1, null) + val svgObject = scope.get("svg", scope) + result = if (svgObject is NativeObject) { + NativeJSON.stringify(Context.getCurrentContext(), scope, svgObject, null, null).toString() + } else { + Context.toString(svgObject) + } + Log.d("runJS", "Result: $result") + } catch (e: Exception) { + Log.e("runJS", "Error executing JavaScript", e) + } finally { + Context.exit() + } + return result + } + + data class SvgObject( + val stream: String, + val hash: String + ) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt index 615cfd74..979fd8c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt @@ -25,9 +25,13 @@ open class Vidmoly : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - + val headers = mapOf( + "User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + "Sec-Fetch-Dest" to "iframe" + ) val script = app.get( url, + headers = headers, referer = referer, ).document.select("script") .find { it.data().contains("sources:") }?.data() @@ -66,4 +70,4 @@ open class Vidmoly : ExtractorApi() { @JsonProperty("kind") val kind: String? = null, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt index b9a07a6d..c5e01552 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -13,6 +13,10 @@ import javax.crypto.spec.SecretKeySpec // Code found in https://github.com/KillerDogeEmpire/vidplay-keys // special credits to @KillerDogeEmpire for providing key +class AnyVidplay(hostUrl: String) : Vidplay() { + override val mainUrl = hostUrl +} + class MyCloud : Vidplay() { override val name = "MyCloud" override val mainUrl = "https://mcloud.bz" @@ -66,7 +70,7 @@ open class Vidplay : ExtractorApi() { } private suspend fun callFutoken(id: String, url: String): String? { - val script = app.get("$mainUrl/futoken").text + val script = app.get("$mainUrl/futoken", referer = url).text val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null val a = mutableListOf(k) for (i in id.indices) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt index 2c6998de..67fd7eea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt @@ -1,19 +1,46 @@ package com.lagradost.cloudstream3.extractors +import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper class Tubeless : Voe() { - override var mainUrl = "https://tubelessceliolymph.com" + override val name = "Tubeless" + override val mainUrl = "https://tubelessceliolymph.com" +} + +class Simpulumlamerop : Voe() { + override val name = "Simplum" + override var mainUrl = "https://simpulumlamerop.com" +} + +class Urochsunloath : Voe() { + override val name = "Uroch" + override var mainUrl = "https://urochsunloath.com" +} + +class Yipsu : Voe() { + override val name = "Yipsu" + override var mainUrl = "https://yip.su" +} + +class MetaGnathTuggers : Voe() { + override val name = "Metagnath" + override val mainUrl = "https://metagnathtuggers.com" } open class Voe : ExtractorApi() { override val name = "Voe" override val mainUrl = "https://voe.sx" override val requiresReferer = true + + private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex() + private val base64Regex = Regex("'.*'") override suspend fun getUrl( url: String, @@ -25,12 +52,33 @@ open class Voe : ExtractorApi() { val script = res.select("script").find { it.data().contains("sources =") }?.data() val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1) - M3u8Helper.generateM3u8( - name, - link ?: return, - "$mainUrl/", - headers = mapOf("Origin" to "$mainUrl/") - ).forEach(callback) - + val videoLinks = mutableListOf() + + if (!link.isNullOrBlank()) { + videoLinks.add( + when { + linkRegex.matches(link) -> link + else -> String(Base64.decode(link, Base64.DEFAULT)) + } + ) + } else { + val link2 = base64Regex.find(script)?.value ?: return + val decoded = Base64.decode(link2, Base64.DEFAULT).toString() + val videoLinkDTO = AppUtils.parseJson(decoded) + videoLinkDTO.let { videoLinks.add(it.toString()) } + } + + videoLinks.forEach { videoLink -> + M3u8Helper.generateM3u8( + name, + videoLink, + "$mainUrl/", + headers = mapOf("Origin" to "$mainUrl/") + ).forEach(callback) + } } -} \ No newline at end of file + + data class WcoSources( + @JsonProperty("VideoLinkDTO") val VideoLinkDTO: String, + ) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt new file mode 100644 index 00000000..919a9cbd --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt @@ -0,0 +1,39 @@ +package com.lagradost.cloudstream3.extractors + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.JsUnpacker +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.getQualityFromName +import java.net.URI + + +open class Vtbe : ExtractorApi() { + override var name = "Vtbe" + override var mainUrl = "https://vtbe.to" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List? { + val response = app.get(url,referer=mainUrl).document + val extractedpack =response.selectFirst("script:containsData(function(p,a,c,k,e,d))")?.data().toString() + JsUnpacker(extractedpack).unpack()?.let { unPacked -> + Regex("sources:\\[\\{file:\"(.*?)\"").find(unPacked)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + this.name, + this.name, + link, + referer ?: "", + Qualities.Unknown.value, + URI(link).path.endsWith(".m3u8") + ) + ) + } + } + return null + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt index 50301e22..c5b4d453 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt @@ -105,6 +105,7 @@ open class TmdbProvider : MainAPI() { this.id, episode.episode_number, episode.season_number, + this.name ?: this.original_name, ).toJson(), episode.name, episode.season_number, @@ -122,6 +123,7 @@ open class TmdbProvider : MainAPI() { this.id, episodeNum, season.season_number, + this.name ?: this.original_name, ).toJson(), season = season.season_number ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt new file mode 100644 index 00000000..8d149888 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -0,0 +1,441 @@ +package com.lagradost.cloudstream3.metaproviders + +import android.net.Uri +import com.lagradost.cloudstream3.* +import com.fasterxml.jackson.annotation.JsonAlias +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import java.util.Locale +import java.text.SimpleDateFormat +import kotlin.math.roundToInt + +open class TraktProvider : MainAPI() { + override var name = "Trakt" + override val hasMainPage = true + override val providerType = ProviderType.MetaProvider + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.Anime, + ) + + private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==") + private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2") + + override val mainPage = mainPageOf( + "$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now + "$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time + "$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now + "$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + + val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page") + + val results = parseJson>(apiResponse).map { element -> + element.toSearchResponse() + } + return newHomePageResponse(request.name, results) + } + + private fun MediaDetails.toSearchResponse(): SearchResponse { + + val media = this.media ?: this + val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries + val poster = media.images?.poster?.firstOrNull() + + if (mediaType == TvType.Movie) { + return newMovieSearchResponse( + name = media.title!!, + url = Data( + type = mediaType, + mediaDetails = media, + ).toJson(), + type = TvType.Movie, + ) { + posterUrl = fixPath(poster) + } + } else { + return newTvSeriesSearchResponse( + name = media.title!!, + url = Data( + type = mediaType, + mediaDetails = media, + ).toJson(), + type = TvType.TvSeries, + ) { + this.posterUrl = fixPath(poster) + } + } + } + + override suspend fun search(query: String): List? { + val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") + + val results = parseJson>(apiResponse).map { element -> + element.toSearchResponse() + } + + return results + } + override suspend fun load(url: String): LoadResponse { + + val data = parseJson(url) + val mediaDetails = data.mediaDetails + val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows" + + val posterUrl = mediaDetails?.images?.poster?.firstOrNull() + val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull() + + val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full") + + val actors = parseJson(resActor).cast?.map { + ActorData( + Actor( + name = it.person?.name!!, + image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500") + ), + roleString = it.character + ) + } + + val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20") + + val relatedMedia = parseJson>(resRelated).map { it.toSearchResponse() } + + val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true + val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja") + val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko") + val isBollywood = mediaDetails?.country == "in" + + if (data.type == TvType.Movie) { + + val linkData = LinkData( + id = mediaDetails?.ids?.tmdb, + traktId = mediaDetails?.ids?.trakt, + traktSlug = mediaDetails?.ids?.slug, + tmdbId = mediaDetails?.ids?.tmdb, + imdbId = mediaDetails?.ids?.imdb.toString(), + tvdbId = mediaDetails?.ids?.tvdb, + tvrageId = mediaDetails?.ids?.tvrage, + type = data.type.toString(), + title = mediaDetails?.title, + year = mediaDetails?.year, + orgTitle = mediaDetails?.title, + isAnime = isAnime, + //jpTitle = later if needed as it requires another network request, + airedDate = mediaDetails?.released + ?: mediaDetails?.firstAired, + isAsian = isAsian, + isBollywood = isBollywood, + ).toJson() + + return newMovieLoadResponse( + name = mediaDetails?.title!!, + url = data.toJson(), + dataUrl = linkData.toJson(), + type = if (isAnime) TvType.AnimeMovie else TvType.Movie, + ) { + this.name = mediaDetails.title + this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie + this.posterUrl = getOriginalWidthImageUrl(posterUrl) + this.year = mediaDetails.year + this.plot = mediaDetails.overview + this.rating = mediaDetails.rating?.times(1000)?.roundToInt() + this.tags = mediaDetails.genres + this.duration = mediaDetails.runtime + this.recommendations = relatedMedia + this.actors = actors + this.comingSoon = isUpcoming(mediaDetails.released) + //posterHeaders + this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) + this.contentRating = mediaDetails.certification + addTrailer(mediaDetails.trailer) + addImdbId(mediaDetails.ids?.imdb) + addTMDbId(mediaDetails.ids?.tmdb.toString()) + } + } else { + + val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") + val episodes = mutableListOf() + val seasons = parseJson>(resSeasons) + var nextAir: NextAiring? = null + + seasons.forEach { season -> + + season.episodes?.map { episode -> + + val linkData = LinkData( + id = mediaDetails?.ids?.tmdb, + traktId = mediaDetails?.ids?.trakt, + traktSlug = mediaDetails?.ids?.slug, + tmdbId = mediaDetails?.ids?.tmdb, + imdbId = mediaDetails?.ids?.imdb.toString(), + tvdbId = mediaDetails?.ids?.tvdb, + tvrageId = mediaDetails?.ids?.tvrage, + type = data.type.toString(), + season = episode.season, + episode = episode.number, + title = mediaDetails?.title, + year = mediaDetails?.year, + orgTitle = mediaDetails?.title, + isAnime = isAnime, + airedYear = mediaDetails?.year, + lastSeason = seasons.size, + epsTitle = episode.title, + //jpTitle = later if needed as it requires another network request, + date = episode.firstAired, + airedDate = episode.firstAired, + isAsian = isAsian, + isBollywood = isBollywood, + isCartoon = isCartoon + ).toJson() + + episodes.add( + Episode( + data = linkData.toJson(), + name = episode.title, + season = episode.season, + episode = episode.number, + posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()), + rating = episode.rating?.times(10)?.roundToInt(), + description = episode.overview, + ).apply { + this.addDate(episode.firstAired) + if (nextAir == null && this.date != null && this.date!! > unixTimeMS) { + nextAir = NextAiring( + episode = this.episode!!, + unixTime = this.date!!.div(1000L), + season = if (this.season == 1) null else this.season, + ) + } + } + ) + } + } + + return newTvSeriesLoadResponse( + name = mediaDetails?.title!!, + url = data.toJson(), + type = if (isAnime) TvType.Anime else TvType.TvSeries, + episodes = episodes + ) { + this.name = mediaDetails.title + this.type = if (isAnime) TvType.Anime else TvType.TvSeries + this.episodes = episodes + this.posterUrl = getOriginalWidthImageUrl(posterUrl) + this.year = mediaDetails.year + this.plot = mediaDetails.overview + this.showStatus = getStatus(mediaDetails.status) + this.rating = mediaDetails.rating?.times(1000)?.roundToInt() + this.tags = mediaDetails.genres + this.duration = mediaDetails.runtime + this.recommendations = relatedMedia + this.actors = actors + this.comingSoon = isUpcoming(mediaDetails.released) + //posterHeaders + this.nextAiring = nextAir + this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) + this.contentRating = mediaDetails.certification + addTrailer(mediaDetails.trailer) + addImdbId(mediaDetails.ids?.imdb) + addTMDbId(mediaDetails.ids?.tmdb.toString()) + } + } + } + + private suspend fun getApi(url: String) : String { + return app.get( + url = url, + headers = mapOf( + "Content-Type" to "application/json", + "trakt-api-version" to "2", + "trakt-api-key" to traktClientId, + ) + ).toString() + } + + private fun isUpcoming(dateString: String?): Boolean { + return try { + val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val dateTime = dateString?.let { format.parse(it)?.time } ?: return false + APIHolder.unixTimeMS < dateTime + } catch (t: Throwable) { + logError(t) + false + } + } + + private fun getStatus(t: String?): ShowStatus { + return when (t) { + "returning series" -> ShowStatus.Ongoing + "continuing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + + private fun fixPath(url: String?): String? { + url ?: return null + return "https://$url" + } + + private fun getWidthImageUrl(path: String?, width: String) : String? { + if (path == null) return null + if (!path.contains("image.tmdb.org")) return fixPath(path) + val fileName = Uri.parse(path).lastPathSegment ?: return null + return "https://image.tmdb.org/t/p/${width}/${fileName}" + } + + private fun getOriginalWidthImageUrl(path: String?) : String? { + if (path == null) return null + if (!path.contains("image.tmdb.org")) return fixPath(path) + return getWidthImageUrl(path, "original") + } + + data class Data( + val type: TvType? = null, + val mediaDetails: MediaDetails? = null, + ) + + data class MediaDetails( + @JsonProperty("title") val title: String? = null, + @JsonProperty("year") val year: Int? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("tagline") val tagline: String? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("released") val released: String? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("country") val country: String? = null, + @JsonProperty("updatedAt") val updatedAt: String? = null, + @JsonProperty("trailer") val trailer: String? = null, + @JsonProperty("homepage") val homepage: String? = null, + @JsonProperty("status") val status: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("votes") val votes: Long? = null, + @JsonProperty("comment_count") val commentCount: Long? = null, + @JsonProperty("language") val language: String? = null, + @JsonProperty("languages") val languages: List? = null, + @JsonProperty("available_translations") val availableTranslations: List? = null, + @JsonProperty("genres") val genres: List? = null, + @JsonProperty("certification") val certification: String? = null, + @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("airs") val airs: Airs? = null, + @JsonProperty("network") val network: String? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null + ) + + data class Airs( + @JsonProperty("day") val day: String? = null, + @JsonProperty("time") val time: String? = null, + @JsonProperty("timezone") val timezone: String? = null, + ) + + data class Ids( + @JsonProperty("trakt") val trakt: Int? = null, + @JsonProperty("slug") val slug: String? = null, + @JsonProperty("tvdb") val tvdb: Int? = null, + @JsonProperty("imdb") val imdb: String? = null, + @JsonProperty("tmdb") val tmdb: Int? = null, + @JsonProperty("tvrage") val tvrage: String? = null, + ) + + data class Images( + @JsonProperty("fanart") val fanart: List? = null, + @JsonProperty("poster") val poster: List? = null, + @JsonProperty("logo") val logo: List? = null, + @JsonProperty("clearart") val clearart: List? = null, + @JsonProperty("banner") val banner: List? = null, + @JsonProperty("thumb") val thumb: List? = null, + @JsonProperty("screenshot") val screenshot: List? = null, + @JsonProperty("headshot") val headshot: List? = null, + ) + + data class People( + @JsonProperty("cast") val cast: List? = null, + ) + + data class Cast( + @JsonProperty("character") val character: String? = null, + @JsonProperty("characters") val characters: List? = null, + @JsonProperty("episode_count") val episodeCount: Long? = null, + @JsonProperty("person") val person: Person? = null, + @JsonProperty("images") val images: Images? = null, + ) + + data class Person( + @JsonProperty("name") val name: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + ) + + data class Seasons( + @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("episode_count") val episodeCount: Int? = null, + @JsonProperty("episodes") val episodes: List? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("network") val network: String? = null, + @JsonProperty("number") val number: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") val votes: Int? = null, + ) + + data class TraktEpisode( + @JsonProperty("available_translations") val availableTranslations: List? = null, + @JsonProperty("comment_count") val commentCount: Int? = null, + @JsonProperty("episode_type") val episodeType: String? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("number") val number: Int? = null, + @JsonProperty("number_abs") val numberAbs: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") val votes: Int? = null, + ) + + data class LinkData( + val id: Int? = null, + val traktId: Int? = null, + val traktSlug: String? = null, + val tmdbId: Int? = null, + val imdbId: String? = null, + val tvdbId: Int? = null, + val tvrageId: String? = null, + val type: String? = null, + val season: Int? = null, + val episode: Int? = null, + val aniId: String? = null, + val animeId: String? = null, + val title: String? = null, + val year: Int? = null, + val orgTitle: String? = null, + val isAnime: Boolean = false, + val airedYear: Int? = null, + val lastSeason: Int? = null, + val epsTitle: String? = null, + val jpTitle: String? = null, + val date: String? = null, + val airedDate: String? = null, + val isAsian: Boolean = false, + val isBollywood: Boolean = false, + val isCartoon: Boolean = false, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt new file mode 100644 index 00000000..3df5197c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt @@ -0,0 +1,16 @@ +package com.lagradost.cloudstream3.mvvm + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData + +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { it?.let { t -> action(t) } } +} + +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { action(it) } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 025e6fb6..a30af11c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -429,7 +429,6 @@ object PluginManager { **/ fun loadAllLocalPlugins(context: Context, forceReload: Boolean) { val dir = File(LOCAL_PLUGINS_PATH) - removeKey(PLUGINS_KEY_LOCAL) if (!dir.exists()) { val res = dir.mkdirs() diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt index adf5abfa..e2bcd6e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.services +import android.annotation.SuppressLint import android.app.NotificationManager import android.app.PendingIntent import android.content.Context @@ -12,7 +13,7 @@ import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel @@ -97,128 +98,138 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete ) } + @SuppressLint("UnspecifiedImmutableFlag") override suspend fun doWork(): Result { + try { // println("Update subscriptions!") - context.createNotificationChannel( - SUBSCRIPTION_CHANNEL_ID, - SUBSCRIPTION_CHANNEL_NAME, - SUBSCRIPTION_CHANNEL_DESCRIPTION - ) - - setForeground( - ForegroundInfo( - SUBSCRIPTION_NOTIFICATION_ID, - progressNotificationBuilder.build() + context.createNotificationChannel( + SUBSCRIPTION_CHANNEL_ID, + SUBSCRIPTION_CHANNEL_NAME, + SUBSCRIPTION_CHANNEL_DESCRIPTION ) - ) - val subscriptions = getAllSubscriptions() + setForeground( + ForegroundInfo( + SUBSCRIPTION_NOTIFICATION_ID, + progressNotificationBuilder.build() + ) + ) - if (subscriptions.isEmpty()) { - WorkManager.getInstance(context).cancelWorkById(this.id) + val subscriptions = getAllSubscriptions() + + if (subscriptions.isEmpty()) { + WorkManager.getInstance(context).cancelWorkById(this.id) + return Result.success() + } + + val max = subscriptions.size + var progress = 0 + + updateProgress(max, progress, true) + + // We need all plugins loaded. + PluginManager.loadAllOnlinePlugins(context) + PluginManager.loadAllLocalPlugins(context, false) + + subscriptions.apmap { savedData -> + try { + val id = savedData.id ?: return@apmap null + val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null + + // Reasonable timeout to prevent having this worker run forever. + val response = withTimeoutOrNull(60_000) { + api.load(savedData.url) as? EpisodeResponse + } ?: return@apmap null + + val dubPreference = + getDub(id) ?: if ( + context.getApiDubstatusSettings().contains(DubStatus.Dubbed) + ) { + DubStatus.Dubbed + } else { + DubStatus.Subbed + } + + val latestEpisodes = response.getLatestEpisodes() + val latestPreferredEpisode = latestEpisodes[dubPreference] + + val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) { + val latestSeenEpisode = + savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE + val shouldUpdate = latestPreferredEpisode > latestSeenEpisode + shouldUpdate to latestPreferredEpisode + } else { + val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE + val latestSeenEpisode = + savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE + val shouldUpdate = latestEpisode > latestSeenEpisode + shouldUpdate to latestEpisode + } + + DataStoreHelper.updateSubscribedData( + id, + savedData, + response + ) + + if (shouldUpdate) { + val updateHeader = savedData.name + val updateDescription = txt( + R.string.subscription_episode_released, + latestEpisode, + savedData.name + ).asString(context) + + val intent = Intent(context, MainActivity::class.java).apply { + data = savedData.url.toUri() + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + + val pendingIntent = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + } else { + PendingIntent.getActivity(context, 0, intent, 0) + } + + val poster = ioWork { + savedData.posterUrl?.let { url -> + context.getImageBitmapFromUrl( + url, + savedData.posterHeaders + ) + } + } + + val updateNotification = + updateNotificationBuilder.setContentTitle(updateHeader) + .setContentText(updateDescription) + .setContentIntent(pendingIntent) + .setLargeIcon(poster) + .build() + + notificationManager.notify(id, updateNotification) + } + + // You can probably get some issues here since this is async but it does not matter much. + updateProgress(max, ++progress, false) + } catch (t: Throwable) { + logError(t) + } + } + + return Result.success() + } catch (t: Throwable) { + logError(t) + // ye, while this is not correct, but because gods know why android just crashes + // and this causes major battery usage as it retries it inf times. This is better, just + // in case android decides to be android and fuck us return Result.success() } - - val max = subscriptions.size - var progress = 0 - - updateProgress(max, progress, true) - - // We need all plugins loaded. - PluginManager.loadAllOnlinePlugins(context) - PluginManager.loadAllLocalPlugins(context, false) - - subscriptions.apmap { savedData -> - try { - val id = savedData.id ?: return@apmap null - val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null - - // Reasonable timeout to prevent having this worker run forever. - val response = withTimeoutOrNull(60_000) { - api.load(savedData.url) as? EpisodeResponse - } ?: return@apmap null - - val dubPreference = - getDub(id) ?: if ( - context.getApiDubstatusSettings().contains(DubStatus.Dubbed) - ) { - DubStatus.Dubbed - } else { - DubStatus.Subbed - } - - val latestEpisodes = response.getLatestEpisodes() - val latestPreferredEpisode = latestEpisodes[dubPreference] - - val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) { - val latestSeenEpisode = - savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE - val shouldUpdate = latestPreferredEpisode > latestSeenEpisode - shouldUpdate to latestPreferredEpisode - } else { - val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE - val latestSeenEpisode = - savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE - val shouldUpdate = latestEpisode > latestSeenEpisode - shouldUpdate to latestEpisode - } - - DataStoreHelper.updateSubscribedData( - id, - savedData, - response - ) - - if (shouldUpdate) { - val updateHeader = savedData.name - val updateDescription = txt( - R.string.subscription_episode_released, - latestEpisode, - savedData.name - ).asString(context) - - val intent = Intent(context, MainActivity::class.java).apply { - data = savedData.url.toUri() - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - - val pendingIntent = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.getActivity( - context, - 0, - intent, - PendingIntent.FLAG_IMMUTABLE - ) - } else { - PendingIntent.getActivity(context, 0, intent, 0) - } - - val poster = ioWork { - savedData.posterUrl?.let { url -> - context.getImageBitmapFromUrl( - url, - savedData.posterHeaders - ) - } - } - - val updateNotification = - updateNotificationBuilder.setContentTitle(updateHeader) - .setContentText(updateDescription) - .setContentIntent(pendingIntent) - .setLargeIcon(poster) - .build() - - notificationManager.notify(id, updateNotification) - } - - // You can probably get some issues here since this is async but it does not matter much. - updateProgress(max, ++progress, false) - } catch (_: Throwable) { - } - } - - return Result.success() } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt index f6424c4c..ed4ccb74 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.subtitles +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.TvType class AbstractSubtitleEntities { @@ -19,8 +20,11 @@ class AbstractSubtitleEntities { data class SubtitleSearch( var query: String = "", - var imdb: Long? = null, var lang: String? = null, + var imdbId: String? = null, + var tmdbId: Int? = null, + var malId: Int? = null, + var aniListId: Int? = null, var epNumber: Int? = null, var seasonNumber: Int? = null, var year: Int? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index bc4048e0..c1fbee69 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -4,7 +4,6 @@ import androidx.annotation.WorkerThread import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey -import com.lagradost.cloudstream3.syncproviders.providers.SubScene import com.lagradost.cloudstream3.syncproviders.providers.* import java.util.concurrent.TimeUnit @@ -14,11 +13,10 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val aniListApi = AniListApi(0) val openSubtitlesApi = OpenSubtitlesApi(0) val simklApi = SimklApi(0) + val addic7ed = Addic7ed() + val subDlApi = SubDlApi(0) val googleDriveApi = GoogleDriveApi(0) val pcloudApi = PcloudApi(0) - val indexSubtitlesApi = IndexSubtitleApi() - val addic7ed = Addic7ed() - val subScene = SubScene() val localListApi = LocalList() // used to login via app intent @@ -33,6 +31,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { malApi, aniListApi, openSubtitlesApi, + subDlApi, simklApi, googleDriveApi, pcloudApi //, nginxApi @@ -52,15 +51,17 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val inAppAuths get() = listOf( - openSubtitlesApi, googleDriveApi, pcloudApi//, nginxApi - ) + openSubtitlesApi, + subDlApi, + googleDriveApi, + pcloudApi + )//, nginxApi) val subtitleProviders get() = listOf( openSubtitlesApi, - indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, - subScene + subDlApi ) const val appString = "cloudstreamapp" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt deleted file mode 100644 index 1adecce9..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt +++ /dev/null @@ -1,265 +0,0 @@ -package com.lagradost.cloudstream3.syncproviders.providers - -import android.util.Log -import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.imdbUrlToIdNullable -import com.lagradost.cloudstream3.subtitles.AbstractSubApi -import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities -import com.lagradost.cloudstream3.utils.SubtitleHelper - -class IndexSubtitleApi : AbstractSubApi { - override val name = "IndexSubtitle" - override val idPrefix = "indexsubtitle" - override val requiresLogin = false - override val icon: Nothing? = null - override val createAccountUrl: Nothing? = null - - override fun loginInfo(): Nothing? = null - - override fun logOut() {} - - - companion object { - const val host = "https://indexsubtitle.com" - const val TAG = "INDEXSUBS" - - fun getOrdinal(num: Int?): String? { - return when (num) { - 1 -> "First" - 2 -> "Second" - 3 -> "Third" - 4 -> "Fourth" - 5 -> "Fifth" - 6 -> "Sixth" - 7 -> "Seventh" - 8 -> "Eighth" - 9 -> "Ninth" - 10 -> "Tenth" - 11 -> "Eleventh" - 12 -> "Twelfth" - 13 -> "Thirteenth" - 14 -> "Fourteenth" - 15 -> "Fifteenth" - 16 -> "Sixteenth" - 17 -> "Seventeenth" - 18 -> "Eighteenth" - 19 -> "Nineteenth" - 20 -> "Twentieth" - 21 -> "Twenty-First" - 22 -> "Twenty-Second" - 23 -> "Twenty-Third" - 24 -> "Twenty-Fourth" - 25 -> "Twenty-Fifth" - 26 -> "Twenty-Sixth" - 27 -> "Twenty-Seventh" - 28 -> "Twenty-Eighth" - 29 -> "Twenty-Ninth" - 30 -> "Thirtieth" - 31 -> "Thirty-First" - 32 -> "Thirty-Second" - 33 -> "Thirty-Third" - 34 -> "Thirty-Fourth" - 35 -> "Thirty-Fifth" - else -> null - } - } - } - - private fun fixUrl(url: String): String { - if (url.startsWith("http")) { - return url - } - if (url.isEmpty()) { - return "" - } - - val startsWithNoHttp = url.startsWith("//") - if (startsWithNoHttp) { - return "https:$url" - } else { - if (url.startsWith('/')) { - return host + url - } - return "$host/$url" - } - } - - private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean { - val FILTER_EPS_REGEX = - Regex("(?i)((Chapter\\s?0?${epNum})|((Season)?\\s?0?${seasonNum}?\\s?(Episode)\\s?0?${epNum}[^0-9]))|(?i)((S?0?${seasonNum}?E0?${epNum}[^0-9])|(0?${seasonNum}[a-z]0?${epNum}[^0-9]))") - return text.contains(FILTER_EPS_REGEX) - } - - private fun haveEps(text: String): Boolean { - val HAVE_EPS_REGEX = - Regex("(?i)((Chapter\\s?0?\\d)|((Season)?\\s?0?\\d?\\s?(Episode)\\s?0?\\d))|(?i)((S?0?\\d?E0?\\d)|(0?\\d[a-z]0?\\d))") - return text.contains(HAVE_EPS_REGEX) - } - - override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List { - val imdbId = query.imdb ?: 0 - val lang = query.lang - val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString()) - val queryText = query.query - val epNum = query.epNumber ?: 0 - val seasonNum = query.seasonNumber ?: 0 - val yearNum = query.year ?: 0 - - val urlItems = ArrayList() - - fun cleanResources( - results: MutableList, - name: String, - link: String - ) { - results.add( - AbstractSubtitleEntities.SubtitleEntity( - idPrefix = idPrefix, - name = name, - lang = queryLang.toString(), - data = link, - source = this.name, - type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie, - epNumber = epNum, - seasonNumber = seasonNum, - year = yearNum, - ) - ) - } - - val document = app.get("$host/?search=$queryText").document - - document.select("div.my-3.p-3 div.media").map { block -> - if (seasonNum > 0) { - val name = block.select("strong.text-primary, strong.text-info").text().trim() - val season = getOrdinal(seasonNum) - if ((block.selectFirst("a")?.attr("href") - ?.contains( - "$season", - ignoreCase = true - )!! || name.contains( - "$season", - ignoreCase = true - )) && name.contains(queryText, ignoreCase = true) - ) { - block.select("div.media").mapNotNull { - urlItems.add( - fixUrl( - it.selectFirst("a")!!.attr("href") - ) - ) - } - } - } else { - if (block.selectFirst("strong")!!.text().trim() - .matches(Regex("(?i)^$queryText\$")) - ) { - if (block.select("span[title=Release]").isNullOrEmpty()) { - block.select("div.media").mapNotNull { - val urlItem = fixUrl( - it.selectFirst("a")!!.attr("href") - ) - val itemDoc = app.get(urlItem).document - val id = imdbUrlToIdNullable( - itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent() - ?.attr("href") - )?.toLongOrNull() - val year = itemDoc.selectFirst("div.d-flex span.badge.badge-success") - ?.ownText() - ?.trim().toString() - Log.i(TAG, "id => $id \nyear => $year||$yearNum") - if (imdbId > 0) { - if (id == imdbId) { - urlItems.add(urlItem) - } - } else { - if (year.contains("$yearNum")) { - urlItems.add(urlItem) - } - } - } - } else { - if (block.select("span[title=Release]").text().trim() - .contains("$yearNum") - ) { - block.select("div.media").mapNotNull { - urlItems.add( - fixUrl( - it.selectFirst("a")!!.attr("href") - ) - ) - } - } - } - } - } - } - Log.i(TAG, "urlItems => $urlItems") - val results = mutableListOf() - - urlItems.forEach { url -> - val request = app.get(url) - if (request.isSuccessful) { - request.document.select("div.my-3.p-3 div.media").map { block -> - if (block.select("span.d-block span[data-original-title=Language]").text() - .trim() - .contains("$queryLang") - ) { - var name = block.select("strong.text-primary, strong.text-info").text().trim() - val link = fixUrl(block.selectFirst("a")!!.attr("href")) - if (seasonNum > 0) { - when { - isRightEps(name, seasonNum, epNum) -> { - cleanResources(results, name, link) - } - !(haveEps(name)) -> { - name = "$name (S${seasonNum}:E${epNum})" - cleanResources(results, name, link) - } - } - } else { - cleanResources(results, name, link) - } - } - } - } - } - return results - } - - override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? { - val seasonNum = data.seasonNumber - val epNum = data.epNumber - - val req = app.get(data.data) - - if (req.isSuccessful) { - val document = req.document - val link = if (document.select("div.my-3.p-3 div.media").size == 1) { - fixUrl( - document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href") - ) - } else { - document.select("div.my-3.p-3 div.media").firstNotNullOf { block -> - val name = - block.selectFirst("strong.d-block")?.text()?.trim().toString() - if (seasonNum!! > 0) { - if (isRightEps(name, seasonNum, epNum)) { - fixUrl(block.selectFirst("a")!!.attr("href")) - } else { - null - } - } else { - fixUrl(block.selectFirst("a")!!.attr("href")) - } - } - } - return link - } - - return null - - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt index 99723e90..7552fe9d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -8,7 +8,8 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions @@ -71,9 +72,9 @@ class LocalList : SyncAPI { }?.distinctBy { it.first } ?: return null val list = ioWork { - val isTrueTv = isTrueTvSettings() + val isTrueTv = isLayout(TV) - val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate { + val baseMap = WatchType.entries.filter { it != WatchType.NONE }.associate { // None is not something to display it.stringRes to emptyList() } + mapOf( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index ceca952d..61ae5965 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -185,7 +185,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi throwIfCantDoRequest() val fixedLang = fixLanguage(query.lang) - val imdbId = query.imdb ?: 0 + val imdbId = query.imdbId?.replace("tt", "")?.toInt() ?: 0 val queryText = query.query val epNum = query.epNumber ?: 0 val seasonNum = query.seasonNumber ?: 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt deleted file mode 100644 index fbe05026..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.lagradost.cloudstream3.syncproviders.providers - -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.debugPrint -import com.lagradost.cloudstream3.subtitles.AbstractSubProvider -import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities -import com.lagradost.cloudstream3.subtitles.SubtitleResource -import com.lagradost.cloudstream3.syncproviders.providers.IndexSubtitleApi.Companion.getOrdinal -import com.lagradost.cloudstream3.utils.SubtitleHelper - -class SubScene : AbstractSubProvider { - val mainUrl = "https://subscene.com" - val name = "Subscene" - override val idPrefix = "subscene" - - override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { - val seasonName = - query.seasonNumber?.let { number -> - // Need to translate "7" to "Seventh Season" - getOrdinal(number)?.let { words -> " - $words Season" } - } ?: "" - - val fullQuery = query.query + seasonName - - val doc = app.post( - "$mainUrl/subtitles/searchbytitle", - data = mapOf("query" to fullQuery, "l" to "") - ).document - - return doc.select("div.title a").map { element -> - val href = "$mainUrl${element.attr("href")}" - val title = element.text() - - AbstractSubtitleEntities.SubtitleEntity( - idPrefix = idPrefix, - name = title, - source = name, - data = href, - lang = query.lang ?: "en", - epNumber = query.epNumber - ) - }.distinctBy { it.data } - } - - override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { - val resultDoc = app.get(data.data).document - val queryLanguage = SubtitleHelper.fromTwoLettersToLanguage(data.lang) ?: "English" - - val results = resultDoc.select("table tbody tr").mapNotNull { element -> - val anchor = element.select("a") - val href = anchor.attr("href") ?: return@mapNotNull null - val fixedHref = "$mainUrl${href}" - val spans = anchor.select("span") - val language = spans.firstOrNull()?.text() - val title = spans.getOrNull(1)?.text() - val isPositive = anchor.select("span.positive-icon").isNotEmpty() - - TableElement(title, language, fixedHref, isPositive) - }.sortedBy { - it.getScore(queryLanguage, data.epNumber) - } - - debugPrint { "$name found subtitles: ${results.takeLast(3)}" } - // Last = highest score - val selectedResult = results.lastOrNull() ?: return - - val subtitleDocument = app.get(selectedResult.href).document - val subtitleDownloadUrl = - "$mainUrl${subtitleDocument.select("div.download a").attr("href")}" - - this.addZipUrl(subtitleDownloadUrl) { name, _ -> - name - } - } - - /** - * Class to manage the various different subtitle results and rank them. - */ - data class TableElement( - val title: String?, - val language: String?, - val href: String, - val isPositive: Boolean - ) { - private fun matchesLanguage(other: String): Boolean { - return language != null && (language.contains(other, ignoreCase = true) || - other.contains(language, ignoreCase = true)) - } - - /** - * Scores in this order: - * Preferred Language > Episode number > Positive rating > English Language - */ - fun getScore(queryLanguage: String, episodeNum: Int?): Int { - var score = 0 - if (this.matchesLanguage(queryLanguage)) { - score += 8 - } - // Matches Episode 7 using "E07" with any number of leading zeroes - if (episodeNum != null && title != null && title.contains( - Regex( - """E0*${episodeNum}""", - RegexOption.IGNORE_CASE - ) - ) - ) { - score += 4 - } - if (isPositive) { - score += 2 - } - if (this.matchesLanguage("English")) { - score += 1 - } - return score - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt new file mode 100644 index 00000000..29544e65 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt @@ -0,0 +1,247 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.subtitles.AbstractSubApi +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities +import com.lagradost.cloudstream3.subtitles.SubtitleResource +import com.lagradost.cloudstream3.syncproviders.AuthAPI.LoginInfo +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager + +class SubDlApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi { + override val idPrefix = "subdl" + override val name = "SubDL" + override val icon = R.drawable.subdl_logo_big + override val requiresPassword = true + override val requiresEmail = true + override val createAccountUrl = "https://subdl.com/login" + + companion object { + const val APIURL = "https://api.subdl.com" + const val APIENDPOINT = "$APIURL/api/v1/subtitles" + const val DOWNLOADENDPOINT = "https://dl.subdl.com" + const val SUBDL_SUBTITLES_USER_KEY: String = "subdl_user" + var currentSession: SubtitleOAuthEntity? = null + } + + override suspend fun initialize() { + currentSession = getAuthKey() + } + + override fun logOut() { + setAuthKey(null) + removeAccountKeys() + currentSession = getAuthKey() + } + override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { + val email = data.email ?: throw ErrorLoadingException("Requires Email") + val password = data.password ?: throw ErrorLoadingException("Requires Password") + switchToNewAccount() + try { + if (initLogin(email, password)) { + registerAccount() + return true + } + } catch (e: Exception) { + logError(e) + switchToOldAccount() + } + switchToOldAccount() + return false + } + + override fun getLatestLoginData(): InAppAuthAPI.LoginData? { + val current = getAuthKey() ?: return null + return InAppAuthAPI.LoginData( + email = current.userEmail, + password = current.pass + ) + } + + override fun loginInfo(): LoginInfo? { + getAuthKey()?.let { user -> + return LoginInfo( + profilePicture = null, + name = user.name ?: user.userEmail, + accountIndex = accountIndex + ) + } + return null + } + + override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { + + val queryText = query.query + val epNum = query.epNumber ?: 0 + val seasonNum = query.seasonNumber ?: 0 + val yearNum = query.year ?: 0 + + val idQuery = when { + query.imdbId != null -> "&imdb_id=${query.imdbId}" + query.tmdbId != null -> "&tmdb_id=${query.tmdbId}" + else -> null + } + + val epQuery = if (epNum > 0) "&episode_number=$epNum" else "" + val seasonQuery = if (seasonNum > 0) "&season_number=$seasonNum" else "" + val yearQuery = if (yearNum > 0) "&year=$yearNum" else "" + + val searchQueryUrl = when (idQuery) { + //Use imdb/tmdb id to search if its valid + null -> "$APIENDPOINT?api_key=${currentSession?.apiKey}&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + else -> "$APIENDPOINT?api_key=${currentSession?.apiKey}$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + } + + val req = app.get( + url = searchQueryUrl, + headers = mapOf( + "Accept" to "application/json" + ) + ) + + return req.parsedSafe()?.subtitles?.map { subtitle -> + + val lang = subtitle.lang.replaceFirstChar { it.uppercase() } + val resEpNum = subtitle.episode ?: query.epNumber + val resSeasonNum = subtitle.season ?: query.seasonNumber + val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie + + AbstractSubtitleEntities.SubtitleEntity( + idPrefix = this.idPrefix, + name = subtitle.releaseName, + lang = lang, + data = "${DOWNLOADENDPOINT}${subtitle.url}", + type = type, + source = this.name, + epNumber = resEpNum, + seasonNumber = resSeasonNum, + isHearingImpaired = subtitle.hearingImpaired ?: false, + ) + } + } + + override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { + this.addZipUrl(data.data) { name, _ -> + name + } + } + + private suspend fun initLogin(useremail: String, password: String): Boolean { + + val tokenResponse = app.post( + url = "$APIURL/login", + data = mapOf( + "email" to useremail, + "password" to password + ) + ).parsedSafe() + + if (tokenResponse?.token == null) return false + + val apiResponse = app.get( + url = "$APIURL/user/userApi", + headers = mapOf( + "Authorization" to "Bearer ${tokenResponse.token}" + ) + ).parsedSafe() + + if (apiResponse?.ok == false) return false + + setAuthKey( + SubtitleOAuthEntity( + userEmail = useremail, + pass = password, + name = tokenResponse.userData?.username ?: tokenResponse.userData?.name, + accessToken = tokenResponse.token, + apiKey = apiResponse?.apiKey + ) + ) + return true + } + + private fun getAuthKey(): SubtitleOAuthEntity? { + return getKey(accountId, SUBDL_SUBTITLES_USER_KEY) + } + + private fun setAuthKey(data: SubtitleOAuthEntity?) { + if (data == null) removeKey( + accountId, + SUBDL_SUBTITLES_USER_KEY + ) + currentSession = data + setKey(accountId, SUBDL_SUBTITLES_USER_KEY, data) + } + + data class SubtitleOAuthEntity( + @JsonProperty("userEmail") var userEmail: String, + @JsonProperty("pass") var pass: String, + @JsonProperty("name") var name: String? = null, + @JsonProperty("accessToken") var accessToken: String? = null, + @JsonProperty("apiKey") var apiKey: String? = null, + ) + + data class OAuthTokenResponse( + @JsonProperty("token") val token: String? = null, + @JsonProperty("userData") val userData: UserData? = null, + @JsonProperty("status") val status: Boolean? = null, + @JsonProperty("message") val message: String? = null, + ) + + data class UserData( + @JsonProperty("email") val email: String, + @JsonProperty("name") val name: String, + @JsonProperty("country") val country: String, + @JsonProperty("scStepCode") val scStepCode: String, + @JsonProperty("scVerified") val scVerified: Boolean, + @JsonProperty("username") val username: String? = null, + @JsonProperty("scUsername") val scUsername: String, + ) + + data class ApiKeyResponse( + @JsonProperty("ok") val ok: Boolean? = false, + @JsonProperty("api_key") val apiKey: String? = null, + @JsonProperty("usage") val usage: Usage? = null, + ) + + data class Usage( + @JsonProperty("total") val total: Long? = 0, + @JsonProperty("today") val today: Long? = 0, + ) + + data class ApiResponse( + @JsonProperty("status") val status: Boolean? = null, + @JsonProperty("results") val results: List? = null, + @JsonProperty("subtitles") val subtitles: List? = null, + ) + + data class Result( + @JsonProperty("sd_id") val sdId: Int? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("name") val name: String? = null, + @JsonProperty("imdb_id") val imdbId: String? = null, + @JsonProperty("tmdb_id") val tmdbId: Long? = null, + @JsonProperty("first_air_date") val firstAirDate: String? = null, + @JsonProperty("year") val year: Int? = null, + ) + + data class Subtitle( + @JsonProperty("release_name") val releaseName: String, + @JsonProperty("name") val name: String, + @JsonProperty("lang") val lang: String, + @JsonProperty("author") val author: String? = null, + @JsonProperty("url") val url: String? = null, + @JsonProperty("subtitlePage") val subtitlePage: String? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("episode") val episode: Int? = null, + @JsonProperty("language") val language: String? = null, + @JsonProperty("hi") val hearingImpaired: Boolean? = null, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt new file mode 100644 index 00000000..d90177f5 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt @@ -0,0 +1,250 @@ +package com.lagradost.cloudstream3.ui + +import android.view.View +import android.view.ViewGroup +import androidx.core.view.children +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModel +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import androidx.viewbinding.ViewBinding +import java.util.concurrent.CopyOnWriteArrayList + +open class ViewHolderState(val view: ViewBinding) : ViewHolder(view.root) { + open fun save(): T? = null + open fun restore(state: T) = Unit + open fun onViewAttachedToWindow() = Unit + open fun onViewDetachedFromWindow() = Unit + open fun onViewRecycled() = Unit +} + + +// Based of the concept https://github.com/brahmkshatriya/echo/blob/main/app%2Fsrc%2Fmain%2Fjava%2Fdev%2Fbrahmkshatriya%2Fecho%2Fui%2Fadapters%2FMediaItemsContainerAdapter.kt#L108-L154 +class StateViewModel : ViewModel() { + val layoutManagerStates = hashMapOf>() +} + +abstract class NoStateAdapter(fragment: Fragment) : BaseAdapter(fragment, 0) + +/** + * BaseAdapter is a persistent state stored adapter that supports headers and footers. + * This should be used for restoring eg scroll or focus related to a view when it is recreated. + * + * Id is a per fragment based unique id used to store the underlying data done in an internal ViewModel. + * + * diffCallback is how the view should be handled when updating, override onUpdateContent for updates + * + * NOTE: + * + * By default it should save automatically, but you can also call save(recycle) + * + * By default no state is stored, but doing an id != 0 will store + * + * By default no headers or footers exist, override footers and headers count + */ +abstract class BaseAdapter< + T : Any, + S : Any>( + fragment: Fragment, + val id: Int = 0, + diffCallback: DiffUtil.ItemCallback = BaseDiffCallback() +) : RecyclerView.Adapter>() { + open val footers: Int = 0 + open val headers: Int = 0 + + fun getItem(position: Int): T { + return mDiffer.currentList[position] + } + + fun getItemOrNull(position: Int): T? { + return mDiffer.currentList.getOrNull(position) + } + + private val mDiffer: AsyncListDiffer = AsyncListDiffer( + object : NonFinalAdapterListUpdateCallback(this) { + override fun onMoved(fromPosition: Int, toPosition: Int) { + super.onMoved(fromPosition + headers, toPosition + headers) + } + + override fun onRemoved(position: Int, count: Int) { + super.onRemoved(position + headers, count) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + super.onChanged(position + headers, count, payload) + } + + override fun onInserted(position: Int, count: Int) { + super.onInserted(position + headers, count) + } + }, + AsyncDifferConfig.Builder(diffCallback).build() + ) + + open fun submitList(list: List?) { + // deep copy at least the top list, because otherwise adapter can go crazy + mDiffer.submitList(list?.let { CopyOnWriteArrayList(it) }) + } + + override fun getItemCount(): Int { + return mDiffer.currentList.size + footers + headers + } + + open fun onUpdateContent(holder: ViewHolderState, item: T, position: Int) = + onBindContent(holder, item, position) + + open fun onBindContent(holder: ViewHolderState, item: T, position: Int) = Unit + open fun onBindFooter(holder: ViewHolderState) = Unit + open fun onBindHeader(holder: ViewHolderState) = Unit + open fun onCreateContent(parent: ViewGroup): ViewHolderState = throw NotImplementedError() + open fun onCreateFooter(parent: ViewGroup): ViewHolderState = throw NotImplementedError() + open fun onCreateHeader(parent: ViewGroup): ViewHolderState = throw NotImplementedError() + + override fun onViewAttachedToWindow(holder: ViewHolderState) { + holder.onViewAttachedToWindow() + } + + override fun onViewDetachedFromWindow(holder: ViewHolderState) { + holder.onViewDetachedFromWindow() + } + + fun save(recyclerView: RecyclerView) { + for (child in recyclerView.children) { + val holder = + recyclerView.findContainingViewHolder(child) as? ViewHolderState ?: continue + setState(holder) + } + } + + fun clear() { + stateViewModel.layoutManagerStates[id]?.clear() + } + + private fun getState(holder: ViewHolderState): S? = + stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S + + private fun setState(holder: ViewHolderState) { + if(id == 0) return + + if (!stateViewModel.layoutManagerStates.contains(id)) { + stateViewModel.layoutManagerStates[id] = HashMap() + } + stateViewModel.layoutManagerStates[id]?.let { map -> + map[holder.absoluteAdapterPosition] = holder.save() + } + } + + private val attachListener = object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) = Unit + override fun onViewDetachedFromWindow(v: View) { + if (v !is RecyclerView) return + save(v) + } + } + + final override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + recyclerView.addOnAttachStateChangeListener(attachListener) + super.onAttachedToRecyclerView(recyclerView) + } + + final override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + recyclerView.removeOnAttachStateChangeListener(attachListener) + super.onDetachedFromRecyclerView(recyclerView) + } + + final override fun getItemViewType(position: Int): Int { + if (position < headers) { + return HEADER + } + if (position - headers >= mDiffer.currentList.size) { + return FOOTER + } + + return CONTENT + } + + private val stateViewModel: StateViewModel by fragment.viewModels() + + final override fun onViewRecycled(holder: ViewHolderState) { + setState(holder) + holder.onViewRecycled() + super.onViewRecycled(holder) + } + + final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderState { + return when (viewType) { + CONTENT -> onCreateContent(parent) + HEADER -> onCreateHeader(parent) + FOOTER -> onCreateFooter(parent) + else -> throw NotImplementedError() + } + } + + // https://medium.com/@domen.lanisnik/efficiently-updating-recyclerview-items-using-payloads-1305f65f3068 + override fun onBindViewHolder( + holder: ViewHolderState, + position: Int, + payloads: MutableList + ) { + if (payloads.isEmpty()) { + super.onBindViewHolder(holder, position, payloads) + return + } + when (getItemViewType(position)) { + CONTENT -> { + val realPosition = position - headers + val item = getItem(realPosition) + onUpdateContent(holder, item, realPosition) + } + + FOOTER -> { + onBindFooter(holder) + } + + HEADER -> { + onBindHeader(holder) + } + } + } + + final override fun onBindViewHolder(holder: ViewHolderState, position: Int) { + when (getItemViewType(position)) { + CONTENT -> { + val realPosition = position - headers + val item = getItem(realPosition) + onBindContent(holder, item, realPosition) + } + + FOOTER -> { + onBindFooter(holder) + } + + HEADER -> { + onBindHeader(holder) + } + } + + getState(holder)?.let { state -> + holder.restore(state) + } + } + + companion object { + private const val HEADER: Int = 1 + private const val FOOTER: Int = 2 + private const val CONTENT: Int = 0 + } +} + +class BaseDiffCallback( + val itemSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() }, + val contentSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() } +) : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = itemSame(oldItem, newItem) + override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = contentSame(oldItem, newItem) + override fun getChangePayload(oldItem: T, newItem: T): Any = Any() +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt new file mode 100644 index 00000000..f721401e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt @@ -0,0 +1,39 @@ +package com.lagradost.cloudstream3.ui + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListUpdateCallback +import androidx.recyclerview.widget.RecyclerView + + +/** + * ListUpdateCallback that dispatches update events to the given adapter. + * + * @see DiffUtil.DiffResult.dispatchUpdatesTo + */ +open class NonFinalAdapterListUpdateCallback +/** + * Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter. + * + * @param adapter The Adapter to send updates to. + */(private var mAdapter: RecyclerView.Adapter<*>) : + ListUpdateCallback { + + override fun onInserted(position: Int, count: Int) { + mAdapter.notifyItemRangeInserted(position, count) + } + + override fun onRemoved(position: Int, count: Int) { + mAdapter.notifyItemRangeRemoved(position, count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + mAdapter.notifyItemMoved(fromPosition, toPosition) + } + + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + override fun onChanged(position: Int, count: Int, payload: Any?) { + mAdapter.notifyItemRangeChanged(position, count, payload) + } +} + diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt index 60260edf..de0b5c05 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt @@ -12,7 +12,9 @@ import com.lagradost.cloudstream3.databinding.AccountListItemBinding import com.lagradost.cloudstream3.databinding.AccountListItemEditBinding import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountEditDialog import com.lagradost.cloudstream3.ui.result.setImage -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -38,7 +40,7 @@ class AccountAdapter( is AccountListItemBinding -> binding.apply { if (account == null) return@apply - val isTv = isTvSettings() || !root.isInTouchMode + val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex @@ -80,7 +82,7 @@ class AccountAdapter( is AccountListItemEditBinding -> binding.apply { if (account == null) return@apply - val isTv = isTvSettings() || !root.isInTouchMode + val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt index 1dae5a0f..0b0d83db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -18,10 +18,15 @@ import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_EDIT_ACCOUNT import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_SELECT_ACCOUNT -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTruePhone -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex @@ -46,7 +51,6 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet ) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false ) || accounts.count() <= 1 @@ -54,7 +58,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet fun askBiometricAuth() { - if (isTruePhone() && authEnabled) { + if (isLayout(PHONE) && isAuthEnabled(this)) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication( this, @@ -62,8 +66,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet false ) - BiometricAuthenticator.promptInfo?.let { - BiometricAuthenticator.biometricPrompt?.authenticate(it) + promptInfo?.let { prompt -> + biometricPrompt?.authenticate(prompt) } } } @@ -127,7 +131,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet recyclerView.adapter = adapter - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { binding.editAccountButton.setBackgroundResource( R.drawable.player_button_tv_attr_no_bg ) @@ -168,7 +172,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet viewModel.toggleIsEditing() } - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { recyclerView.spanCount = if (liveAccounts.count() + 1 <= 6) { liveAccounts.count() + 1 } else 6 @@ -187,4 +191,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet override fun onAuthenticationSuccess() { Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity") } + + override fun onAuthenticationError() { + finish() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index c3ec2bbd..f54c8698 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -11,10 +11,14 @@ import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import kotlinx.coroutines.Dispatchers @@ -85,13 +89,15 @@ class DownloadChildFragment : Fragment() { binding?.downloadChildToolbar?.apply { title = name - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } + setAppBarNoScrollFlagsOnTV() } - val adapter: RecyclerView.Adapter = DownloadChildAdapter( ArrayList(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 27c2e1a3..31790b0f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -5,6 +5,7 @@ import android.content.ClipboardManager import android.content.Context import android.os.Build import android.os.Bundle +import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -13,17 +14,25 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding +import com.lagradost.cloudstream3.databinding.StreamInputBinding import com.lagradost.cloudstream3.isMovieType +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick +import com.lagradost.cloudstream3.ui.player.BasicLink import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.LinkGenerator +import com.lagradost.cloudstream3.ui.result.FOCUS_SELF +import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE @@ -32,17 +41,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager -import android.text.format.Formatter.formatShortFileSize -import androidx.core.widget.doOnTextChanged -import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding -import com.lagradost.cloudstream3.databinding.StreamInputBinding -import com.lagradost.cloudstream3.mvvm.normalSafeApiCall -import com.lagradost.cloudstream3.ui.player.BasicLink -import com.lagradost.cloudstream3.ui.result.FOCUS_SELF -import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import java.net.URI @@ -97,6 +98,8 @@ class DownloadFragment : Fragment() { super.onViewCreated(view, savedInstanceState) hideKeyboard() + binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() + observe(downloadsViewModel.noDownloadsText) { binding?.textNoDownloads?.text = it } @@ -200,7 +203,7 @@ class DownloadFragment : Fragment() { } // Should be visible in emulator layout - binding?.downloadStreamButton?.isGone = isTrueTvSettings() + binding?.downloadStreamButton?.isGone = isLayout(TV) binding?.downloadStreamButton?.setOnClickListener { val dialog = Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index d20fcf93..f1031c24 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -2,16 +2,21 @@ package com.lagradost.cloudstream3.ui.download.button import android.content.Context import android.graphics.drawable.Drawable +import android.os.Looper import android.util.AttributeSet import android.util.Log import android.view.View import android.view.animation.AnimationUtils import android.widget.ImageView import android.widget.TextView +import androidx.annotation.MainThread import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK @@ -22,6 +27,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager +import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES open class PieFetchButton(context: Context, attributeSet: AttributeSet) : @@ -164,6 +170,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : this.setPersistentId(card.id) view.setOnClickListener { if (isZeroBytes) { + removeKey(KEY_RESUME_PACKAGES, card.id.toString()) callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card)) //callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) } else { @@ -241,40 +248,54 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } }*/ + @MainThread + private fun setStatusInternal(status : DownloadStatusTell?) { + val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading + if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { + val animation = AnimationUtils.loadAnimation(context, waitingAnimation) + progressBarBackground.startAnimation(animation) + } else { + progressBarBackground.clearAnimation() + } + + val progressDrawable = + if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline + + progressBarBackground.background = + ContextCompat.getDrawable(context, progressDrawable) + + val drawable = getDrawableFromStatus(status) + statusView.setImageDrawable(drawable) + val isDrawable = drawable != null + + statusView.isVisible = isDrawable + val hide = hideWhenIcon && isDrawable + if (hide) { + progressBar.clearAnimation() + progressBarBackground.clearAnimation() + } + progressBarBackground.isGone = hide + progressBar.isGone = hide + } + /** Also sets currentStatus */ override fun setStatus(status: DownloadStatusTell?) { currentStatus = status - //progressBar.isVisible = - // status != null && status != DownloadStatusTell.Complete && status != DownloadStatusTell.Error - //progressBarBackground.isVisible = status != null && status != DownloadStatusTell.Complete - progressBarBackground.post { - val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading - if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { - val animation = AnimationUtils.loadAnimation(context, waitingAnimation) - progressBarBackground.startAnimation(animation) - } else { - progressBarBackground.clearAnimation() + // runs on the main thread, but also instant if it already is + if (Looper.myLooper() == Looper.getMainLooper()) { + try { + setStatusInternal(status) + } catch (t : Throwable) { + logError(t) // just in case setStatusInternal throws because thread + progressBarBackground.post { + setStatusInternal(status) + } } - - val progressDrawable = - if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline - - progressBarBackground.background = - ContextCompat.getDrawable(context, progressDrawable) - - val drawable = getDrawableFromStatus(status) - statusView.setImageDrawable(drawable) - val isDrawable = drawable != null - - statusView.isVisible = isDrawable - val hide = hideWhenIcon && isDrawable - if (hide) { - progressBar.clearAnimation() - progressBarBackground.clearAnimation() + } else { + progressBarBackground.post { + setStatusInternal(status) } - progressBarBackground.isGone = hide - progressBar.isGone = hide } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index f84966eb..ebed901f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -2,31 +2,58 @@ package com.lagradost.cloudstream3.ui.home import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView +import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.databinding.HomeResultGridBinding import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding +import com.lagradost.cloudstream3.ui.BaseAdapter +import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder -import com.lagradost.cloudstream3.utils.AppUtils.isRtl +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx -class HomeChildItemAdapter( - val cardList: MutableList, +class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState(view) { + /*private fun recursive(view : View) : Boolean { + if (view.isFocused) { + println("VIEW: $view | id=${view.id}") + } + return (view as? ViewGroup)?.children?.any { recursive(it) } ?: false + }*/ + // very shitty that we cant store the state when the view clears, + // but this is because the focus clears before the view is removed + // so we have to manually store it + var wasFocused: Boolean = false + override fun save(): Boolean = wasFocused + override fun restore(state: Boolean) { + if (state) { + wasFocused = false + // only refocus if tv + if(isLayout(TV)) { + itemView.requestFocus() + } + } + } +} + +class HomeChildItemAdapter( + fragment: Fragment, + id: Int, private val nextFocusUp: Int? = null, private val nextFocusDown: Int? = null, private val clickCallback: (SearchClickCallback) -> Unit, ) : - RecyclerView.Adapter() { + BaseAdapter(fragment, id) { var isHorizontal: Boolean = false var hasNext: Boolean = false - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + override fun onCreateContent(parent: ViewGroup): ViewHolderState { val expanded = parent.context.IsBottomLayout() /* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid @@ -39,164 +66,78 @@ class HomeChildItemAdapter( parent, false ) else HomeResultGridBinding.inflate(inflater, parent, false) + return HomeScrollViewHolderState(binding) + } + override fun onBindContent( + holder: ViewHolderState, + item: SearchResponse, + position: Int + ) { + when (val binding = holder.view) { + is HomeResultGridBinding -> { + binding.backgroundCard.apply { + val min = 114.toPx + val max = 180.toPx - return CardViewHolder( - binding, - clickCallback, - itemCount, + layoutParams = + layoutParams.apply { + width = if (!isHorizontal) { + min + } else { + max + } + height = if (!isHorizontal) { + max + } else { + min + } + } + } + } + + is HomeResultGridExpandedBinding -> { + binding.backgroundCard.apply { + val min = 114.toPx + val max = 180.toPx + + layoutParams = + layoutParams.apply { + width = if (!isHorizontal) { + min + } else { + max + } + height = if (!isHorizontal) { + max + } else { + min + } + } + } + + if (position == 0) { // to fix tv + binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view + } + } + } + + SearchResultBuilder.bind( + clickCallback = { click -> + // ok, so here we hijack the callback to fix the focus + when (click.action) { + SEARCH_ACTION_LOAD -> (holder as? HomeScrollViewHolderState)?.wasFocused = true + } + clickCallback(click) + }, + item, + position, + holder.itemView, + null, // nextFocusBehavior, nextFocusUp, - nextFocusDown, - isHorizontal, - parent.isRtl() - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is CardViewHolder -> { - holder.itemCount = itemCount // i know ugly af - holder.bind(cardList[position], position) - } - } - } - - override fun getItemCount(): Int { - return cardList.size - } - - override fun getItemId(position: Int): Long { - return (cardList[position].id ?: position).toLong() - } - - fun updateList(newList: List) { - val diffResult = DiffUtil.calculateDiff( - HomeChildDiffCallback(this.cardList, newList) + nextFocusDown ) - cardList.clear() - cardList.addAll(newList) - - diffResult.dispatchUpdatesTo(this) - } - - class CardViewHolder - constructor( - val binding: ViewBinding, - private val clickCallback: (SearchClickCallback) -> Unit, - var itemCount: Int, - private val nextFocusUp: Int? = null, - private val nextFocusDown: Int? = null, - private val isHorizontal: Boolean = false, - private val isRtl: Boolean - ) : - RecyclerView.ViewHolder(binding.root) { - - fun bind(card: SearchResponse, position: Int) { - - // TV focus fixing - /*val nextFocusBehavior = when (position) { - 0 -> true - itemCount - 1 -> false - else -> null - } - - if (position == 0) { // to fix tv - if (isRtl) { - itemView.nextFocusRightId = R.id.nav_rail_view - itemView.nextFocusLeftId = -1 - } - else { - itemView.nextFocusLeftId = R.id.nav_rail_view - itemView.nextFocusRightId = -1 - } - } else { - itemView.nextFocusRightId = -1 - itemView.nextFocusLeftId = -1 - }*/ - - - when (binding) { - is HomeResultGridBinding -> { - binding.backgroundCard.apply { - val min = 114.toPx - val max = 180.toPx - - layoutParams = - layoutParams.apply { - width = if (!isHorizontal) { - min - } else { - max - } - height = if (!isHorizontal) { - max - } else { - min - } - } - } - - - } - - is HomeResultGridExpandedBinding -> { - binding.backgroundCard.apply { - val min = 114.toPx - val max = 180.toPx - - layoutParams = - layoutParams.apply { - width = if (!isHorizontal) { - min - } else { - max - } - height = if (!isHorizontal) { - max - } else { - min - } - } - } - - if (position == 0) { // to fix tv - binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view - } - } - } - - SearchResultBuilder.bind( - clickCallback, - card, - position, - itemView, - null, // nextFocusBehavior, - nextFocusUp, - nextFocusDown - ) - itemView.tag = position - - //val ani = ScaleAnimation(0.9f, 1.0f, 0.9f, 1f) - //ani.fillAfter = true - //ani.duration = 200 - //itemView.startAnimation(ani) - } + holder.itemView.tag = position } } - -class HomeChildDiffCallback( - private val oldList: List, - private val newList: List -) : - DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition].name == newList[newItemPosition].name - - override fun getOldListSize() = oldList.size - - override fun getNewListSize() = newList.size - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition] == newList[newItemPosition] && oldItemPosition < oldList.size - 1 // always update the last item -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index cd843517..12185cbf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -42,8 +42,10 @@ import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLine import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.* import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.ownHide @@ -311,7 +313,7 @@ class HomeFragment : Fragment() { button?.isVisible = isValid button?.isChecked = isValid && selectedTypes.any { types.contains(it) } button?.isFocusable = true - if (isTrueTvSettings()) { + if (isLayout(TV)) { button?.isFocusableInTouchMode = true } @@ -435,7 +437,7 @@ class HomeFragment : Fragment() { bottomSheetDialog?.ownShow() val layout = - if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home + if (isLayout(TV or EMULATOR)) R.layout.fragment_home_tv else R.layout.fragment_home val root = inflater.inflate(layout, container, false) binding = try { FragmentHomeBinding.bind(root) @@ -449,6 +451,7 @@ class HomeFragment : Fragment() { } override fun onDestroyView() { + bottomSheetDialog?.ownHide() binding = null super.onDestroyView() @@ -485,6 +488,10 @@ class HomeFragment : Fragment() { private var bottomSheetDialog: BottomSheetDialog? = null + // https://github.com/vivchar/RendererRecyclerViewAdapter/blob/185251ee9d94fb6eb3e063b00d646b745186c365/example/src/main/java/com/github/vivchar/example/pages/github/GithubFragment.kt#L32 + // cry about it, but this is android we are talking about, we cant do the most simple shit without making a global variable + private var instanceState: Bundle = Bundle() + private var homeMasterAdapter: HomeParentItemAdapterPreview? = null @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -505,15 +512,14 @@ class HomeFragment : Fragment() { activity.loadSearchResult(listHomepageItems.random()) } } - - homeMasterRecycler.adapter = - HomeParentItemAdapterPreview( - mutableListOf(), - homeViewModel - ) + homeMasterAdapter = HomeParentItemAdapterPreview( + fragment = this@HomeFragment, + homeViewModel, + ) + homeMasterRecycler.adapter = homeMasterAdapter //fixPaddingStatusbar(homeLoadingStatusbar) - homeApiFab.isVisible = !isTvSettings() + homeApiFab.isVisible = isLayout(PHONE) homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -521,7 +527,7 @@ class HomeFragment : Fragment() { homeApiFab.shrink() // hide homeRandom.shrink() } else if (dy < -5) { - if (!isTvSettings()) { + if (isLayout(PHONE)) { homeApiFab.extend() // show homeRandom.extend() } @@ -540,7 +546,7 @@ class HomeFragment : Fragment() { settingsManager.getBoolean( getString(R.string.random_button_key), false - ) && !isTvSettings() + ) && isLayout(PHONE) binding?.homeRandom?.visibility = View.GONE } @@ -560,10 +566,11 @@ class HomeFragment : Fragment() { val mutableListOfResponse = mutableListOf() listHomepageItems.clear() - (homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList( - d.values.toMutableList(), - homeMasterRecycler - ) + (homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(d.values.map { + it.copy( + list = it.list.copy(list = it.list.list.toMutableList()) + ) + }.toMutableList()) homeLoading.isVisible = false homeLoadingError.isVisible = false @@ -612,7 +619,7 @@ class HomeFragment : Fragment() { } is Resource.Loading -> { - (homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(listOf()) + (homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(listOf()) homeLoadingShimmer.startShimmer() homeLoading.isVisible = true homeLoadingError.isVisible = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index 443278a9..4b0360d7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -1,22 +1,27 @@ package com.lagradost.cloudstream3.ui.home +import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListUpdateCallback +import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.HomepageParentBinding +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.BaseAdapter +import com.lagradost.cloudstream3.ui.BaseDiffCallback +import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SearchClickCallback -import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable class LoadClickCallback( @@ -27,193 +32,89 @@ class LoadClickCallback( ) open class ParentItemAdapter( - private var items: MutableList, - //private val viewModel: HomeViewModel, + open val fragment: Fragment, + id: Int, private val clickCallback: (SearchClickCallback) -> Unit, private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, private val expandCallback: ((String) -> Unit)? = null, -) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - - val layoutResId = when { - isTrueTvSettings() -> R.layout.homepage_parent_tv - parent.context.isEmulatorSettings() -> R.layout.homepage_parent_emulator - else -> R.layout.homepage_parent - } - - val root = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false) - - val binding = HomepageParentBinding.bind(root) - - return ParentViewHolder( - binding, - clickCallback, - moreInfoClickCallback, - expandCallback - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is ParentViewHolder -> { - holder.bind(items[position]) - } - } - } - - override fun getItemCount(): Int { - return items.size - } - - override fun getItemId(position: Int): Long { - return items[position].list.name.hashCode().toLong() - } - - @JvmName("updateListHomePageList") - fun updateList(newList: List) { - updateList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) } - .toMutableList()) - } - - @JvmName("updateListExpandableHomepageList") - fun updateList( - newList: MutableList, - recyclerView: RecyclerView? = null - ) { - // this - // 1. prevents deep copy that makes this.items == newList - // 2. filters out undesirable results - // 3. moves empty results to the bottom (sortedBy is a stable sort) - val new = - newList.map { it.copy(list = it.list.copy(list = it.list.list.filterSearchResponse())) } - .sortedBy { it.list.list.isEmpty() } - - val diffResult = DiffUtil.calculateDiff( - SearchDiffCallback(items, new) - ) - items.clear() - items.addAll(new) - - //val mAdapter = this - val delta = if (this@ParentItemAdapter is HomeParentItemAdapterPreview) { - headItems - } else { - 0 - } - - diffResult.dispatchUpdatesTo(object : ListUpdateCallback { - override fun onInserted(position: Int, count: Int) { - //notifyItemRangeChanged(position + delta, count) - notifyItemRangeInserted(position + delta, count) - } - - override fun onRemoved(position: Int, count: Int) { - notifyItemRangeRemoved(position + delta, count) - } - - override fun onMoved(fromPosition: Int, toPosition: Int) { - notifyItemMoved(fromPosition + delta, toPosition + delta) - } - - override fun onChanged(_position: Int, count: Int, payload: Any?) { - - val position = _position + delta - - // I know kinda messy, what this does is using the update or bind instead of onCreateViewHolder -> bind - recyclerView?.apply { - // this loops every viewHolder in the recycle view and checks the position to see if it is within the update range - val missingUpdates = (position until (position + count)).toMutableSet() - for (i in 0 until itemCount) { - val child = getChildAt(i) ?: continue - val viewHolder = getChildViewHolder(child) ?: continue - if (viewHolder !is ParentViewHolder) continue - - val absolutePosition = viewHolder.bindingAdapterPosition - if (absolutePosition >= position && absolutePosition < position + count) { - val expand = items.getOrNull(absolutePosition - delta) ?: continue - missingUpdates -= absolutePosition - //println("Updating ${viewHolder.title.text} ($absolutePosition $position) -> ${expand.list.name}") - if (viewHolder.title.text == expand.list.name) { - viewHolder.update(expand) - } else { - viewHolder.bind(expand) - } - } - } - - // just in case some item did not get updated - for (i in missingUpdates) { - notifyItemChanged(i, payload) - } - } ?: run { - // in case we don't have a nice - notifyItemRangeChanged(position, count, payload) - } - } +) : BaseAdapter( + fragment, + id, + diffCallback = BaseDiffCallback( + itemSame = { a, b -> a.list.name == b.list.name }, + contentSame = { a, b -> + a.list.list == b.list.list }) - - //diffResult.dispatchUpdatesTo(this) - } - - class ParentViewHolder - constructor( - val binding: HomepageParentBinding, - // val viewModel: HomeViewModel, - private val clickCallback: (SearchClickCallback) -> Unit, - private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, - private val expandCallback: ((String) -> Unit)? = null, - ) : - RecyclerView.ViewHolder(binding.root) { - val title: TextView = binding.homeChildMoreInfo - private val recyclerView: RecyclerView = binding.homeChildRecyclerview - private val startFocus = R.id.nav_rail_view - private val endFocus = FOCUS_SELF - fun update(expand: HomeViewModel.ExpandableHomepageList) { - val info = expand.list - (recyclerView.adapter as? HomeChildItemAdapter?)?.apply { - updateList(info.list.toMutableList()) - hasNext = expand.hasNext - } ?: run { - recyclerView.adapter = HomeChildItemAdapter( - info.list.toMutableList(), - clickCallback = clickCallback, - nextFocusUp = recyclerView.nextFocusUpId, - nextFocusDown = recyclerView.nextFocusDownId, - ).apply { - isHorizontal = info.isHorizontalImages - hasNext = expand.hasNext - } - recyclerView.setLinearListLayout( - isHorizontal = true, - nextLeft = startFocus, - nextRight = endFocus, - ) - } +) { + data class ParentItemHolder(val binding: ViewBinding) : ViewHolderState(binding) { + override fun save(): Bundle = Bundle().apply { + val recyclerView = (binding as? HomepageParentBinding)?.homeChildRecyclerview + putParcelable( + "value", + recyclerView?.layoutManager?.onSaveInstanceState() + ) + (recyclerView?.adapter as? BaseAdapter<*,*>)?.save(recyclerView) } - fun bind(expand: HomeViewModel.ExpandableHomepageList) { - val info = expand.list - recyclerView.adapter = HomeChildItemAdapter( - info.list.toMutableList(), + override fun restore(state: Bundle) { + (binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState( + state.getParcelable("value") + ) + } + } + + override fun submitList(list: List?) { + super.submitList(list?.sortedBy { it.list.list.isEmpty() }) + } + + override fun onUpdateContent( + holder: ViewHolderState, + item: HomeViewModel.ExpandableHomepageList, + position: Int + ) { + val binding = holder.view + if (binding !is HomepageParentBinding) return + (binding.homeChildRecyclerview.adapter as? HomeChildItemAdapter)?.submitList(item.list.list) + } + + override fun onBindContent( + holder: ViewHolderState, + item: HomeViewModel.ExpandableHomepageList, + position: Int + ) { + val startFocus = R.id.nav_rail_view + val endFocus = FOCUS_SELF + val binding = holder.view + if (binding !is HomepageParentBinding) return + val info = item.list + binding.apply { + homeChildRecyclerview.adapter = HomeChildItemAdapter( + fragment = fragment, + id = id + position + 100, clickCallback = clickCallback, - nextFocusUp = recyclerView.nextFocusUpId, - nextFocusDown = recyclerView.nextFocusDownId, + nextFocusUp = homeChildRecyclerview.nextFocusUpId, + nextFocusDown = homeChildRecyclerview.nextFocusDownId, ).apply { isHorizontal = info.isHorizontalImages - hasNext = expand.hasNext + hasNext = item.hasNext + submitList(item.list.list) } - recyclerView.setLinearListLayout( + homeChildRecyclerview.setLinearListLayout( isHorizontal = true, nextLeft = startFocus, nextRight = endFocus, ) - title.text = info.name + homeChildMoreInfo.text = info.name - recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + homeChildRecyclerview.addOnScrollListener(object : + RecyclerView.OnScrollListener() { var expandCount = 0 - val name = expand.list.name + val name = item.list.name - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + override fun onScrollStateChanged( + recyclerView: RecyclerView, + newState: Int + ) { super.onScrollStateChanged(recyclerView, newState) val adapter = recyclerView.adapter @@ -237,27 +138,35 @@ open class ParentItemAdapter( }) //(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged() - if (!isTrueTvSettings()) { - title.setOnClickListener { - moreInfoClickCallback.invoke(expand) + if (isLayout(PHONE)) { + homeChildMoreInfo.setOnClickListener { + moreInfoClickCallback.invoke(item) } } } } -} -class SearchDiffCallback( - private val oldList: List, - private val newList: List -) : - DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition].list.name == newList[newItemPosition].list.name + override fun onCreateContent(parent: ViewGroup): ParentItemHolder { + val layoutResId = when { + isLayout(TV) -> R.layout.homepage_parent_tv + isLayout(EMULATOR) -> R.layout.homepage_parent_emulator + else -> R.layout.homepage_parent + } - override fun getOldListSize() = oldList.size + val inflater = LayoutInflater.from(parent.context) + val binding = try { + HomepageParentBinding.bind(inflater.inflate(layoutResId, parent, false)) + } catch (t: Throwable) { + logError(t) + // just in case someone forgot we don't want to crash + HomepageParentBinding.inflate(inflater) + } - override fun getNewListSize() = newList.size + return ParentItemHolder(binding) + } - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - oldList[oldItemPosition] == newList[newItemPosition] + fun updateList(newList: List) { + submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) } + .toMutableList()) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 0e397f81..52ec06db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -1,5 +1,7 @@ package com.lagradost.cloudstream3.ui.home +import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -7,6 +9,7 @@ import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.fragment.app.Fragment import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding @@ -26,6 +29,7 @@ import com.lagradost.cloudstream3.databinding.FragmentHomeHeadTvBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage @@ -36,8 +40,9 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA import com.lagradost.cloudstream3.ui.search.SearchClickCallback -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes @@ -46,113 +51,87 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView import com.lagradost.cloudstream3.utils.UIHelper.populateChips class HomeParentItemAdapterPreview( - items: MutableList, + override val fragment: Fragment, private val viewModel: HomeViewModel, -) : ParentItemAdapter(items, clickCallback = { - viewModel.click(it) -}, moreInfoClickCallback = { - viewModel.popup(it) -}, expandCallback = { - viewModel.expand(it) -}) { - val headItems = 1 +) : ParentItemAdapter(fragment, id = "HomeParentItemAdapterPreview".hashCode(), + clickCallback = { + viewModel.click(it) + }, moreInfoClickCallback = { + viewModel.popup(it) + }, expandCallback = { + viewModel.expand(it) + }) { + override val headers = 1 + override fun onCreateHeader(parent: ViewGroup): ViewHolderState { + val inflater = LayoutInflater.from(parent.context) + val binding = if (isLayout(TV or EMULATOR)) FragmentHomeHeadTvBinding.inflate( + inflater, + parent, + false + ) else FragmentHomeHeadBinding.inflate(inflater, parent, false) - companion object { - private const val VIEW_TYPE_HEADER = 2 - private const val VIEW_TYPE_ITEM = 1 - } + if (binding is FragmentHomeHeadTvBinding && isLayout(EMULATOR)) { + binding.homeBookmarkParentItemMoreInfo.isVisible = true - override fun getItemViewType(position: Int) = when (position) { - 0 -> VIEW_TYPE_HEADER - else -> VIEW_TYPE_ITEM - } + val marginInDp = 50 + val density = binding.horizontalScrollChips.context.resources.displayMetrics.density + val marginInPixels = (marginInDp * density).toInt() - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is HeaderViewHolder -> {} - else -> super.onBindViewHolder(holder, position - headItems) + val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams + params.marginEnd = marginInPixels + binding.horizontalScrollChips.layoutParams = params + binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds( + null, + null, + ContextCompat.getDrawable( + parent.context, + R.drawable.ic_baseline_arrow_forward_24 + ), + null + ) } + + return HeaderViewHolder(binding, viewModel, fragment = fragment) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - VIEW_TYPE_HEADER -> { - val inflater = LayoutInflater.from(parent.context) - val binding = if (isTvSettings()) FragmentHomeHeadTvBinding.inflate( - inflater, - parent, - false - ) else FragmentHomeHeadBinding.inflate(inflater, parent, false) + override fun onBindHeader(holder: ViewHolderState) { + (holder as? HeaderViewHolder)?.bind() + } - if (binding is FragmentHomeHeadTvBinding && parent.context.isEmulatorSettings()) { - binding.homeBookmarkParentItemMoreInfo.isVisible = true + private class HeaderViewHolder( + val binding: ViewBinding, val viewModel: HomeViewModel, fragment: Fragment, + ) : + ViewHolderState(binding) { - val marginInDp = 50 - val density = binding.horizontalScrollChips.context.resources.displayMetrics.density - val marginInPixels = (marginInDp * density).toInt() - - val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams - params.marginEnd = marginInPixels - binding.horizontalScrollChips.layoutParams = params - binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds( - null, - null, - ContextCompat.getDrawable( - parent.context, - R.drawable.ic_baseline_arrow_forward_24 - ), - null - ) - } - - HeaderViewHolder( - binding, - viewModel, + override fun save(): Bundle = + Bundle().apply { + putParcelable( + "resumeRecyclerView", + resumeRecyclerView.layoutManager?.onSaveInstanceState() ) + putParcelable( + "bookmarkRecyclerView", + bookmarkRecyclerView.layoutManager?.onSaveInstanceState() + ) + //putInt("previewViewpager", previewViewpager.currentItem) } - VIEW_TYPE_ITEM -> super.onCreateViewHolder(parent, viewType) - else -> error("Unhandled viewType=$viewType") - } - } - - override fun getItemCount(): Int { - return super.getItemCount() + headItems - } - - override fun getItemId(position: Int): Long { - if (position == 0) return 0//previewData.hashCode().toLong() - return super.getItemId(position - headItems) - } - - override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { - when (holder) { - is HeaderViewHolder -> { - holder.onViewDetachedFromWindow() + override fun restore(state: Bundle) { + state.getParcelable("resumeRecyclerView")?.let { recycle -> + resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - - else -> super.onViewDetachedFromWindow(holder) - } - } - - override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { - when (holder) { - is HeaderViewHolder -> { - holder.onViewAttachedToWindow() + state.getParcelable("bookmarkRecyclerView")?.let { recycle -> + bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - - else -> super.onViewAttachedToWindow(holder) + //state.getInt("previewViewpager").let { recycle -> + // previewViewpager.setCurrentItem(recycle,true) + //} } - } - class HeaderViewHolder - constructor( - val binding: ViewBinding, - val viewModel: HomeViewModel, - ) : RecyclerView.ViewHolder(binding.root) { - private var previewAdapter: HomeScrollAdapter = HomeScrollAdapter() - private var resumeAdapter: HomeChildItemAdapter = HomeChildItemAdapter( - ArrayList(), + val previewAdapter = HomeScrollAdapter(fragment = fragment) + private val resumeAdapter = HomeChildItemAdapter( + fragment, + id = "resumeAdapter".hashCode(), nextFocusUp = itemView.nextFocusUpId, nextFocusDown = itemView.nextFocusDownId ) { callback -> @@ -207,8 +186,9 @@ class HomeParentItemAdapterPreview( } } } - private var bookmarkAdapter: HomeChildItemAdapter = HomeChildItemAdapter( - ArrayList(), + private val bookmarkAdapter = HomeChildItemAdapter( + fragment, + id = "bookmarkAdapter".hashCode(), nextFocusUp = itemView.nextFocusUpId, nextFocusDown = itemView.nextFocusDownId ) { callback -> @@ -217,7 +197,10 @@ class HomeParentItemAdapterPreview( return@HomeChildItemAdapter } - (callback.view.context?.getActivity() as? MainActivity)?.loadPopup(callback.card, load = false) + (callback.view.context?.getActivity() as? MainActivity)?.loadPopup( + callback.card, + load = false + ) /* callback.view.context?.getActivity()?.showOptionSelectStringRes( callback.view, @@ -267,7 +250,6 @@ class HomeParentItemAdapterPreview( */ } - private val previewViewpager: ViewPager2 = itemView.findViewById(R.id.home_preview_viewpager) @@ -275,38 +257,24 @@ class HomeParentItemAdapterPreview( itemView.findViewById(R.id.home_preview_viewpager_text) // private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) - private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder) - private var resumeRecyclerView: RecyclerView = + private val resumeHolder: View = itemView.findViewById(R.id.home_watch_holder) + private val resumeRecyclerView: RecyclerView = itemView.findViewById(R.id.home_watch_child_recyclerview) - private var bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder) - private var bookmarkRecyclerView: RecyclerView = + private val bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder) + private val bookmarkRecyclerView: RecyclerView = itemView.findViewById(R.id.home_bookmarked_child_recyclerview) - private var homeAccount: View? = - itemView.findViewById(R.id.home_preview_switch_account) - private var alternativeHomeAccount: View? = + private val homeAccount: View? = itemView.findViewById(R.id.home_preview_switch_account) + private val alternativeHomeAccount: View? = itemView.findViewById(R.id.alternative_switch_account) - private var topPadding: View? = itemView.findViewById(R.id.home_padding) + private val topPadding: View? = itemView.findViewById(R.id.home_padding) - private var alternativeAccountPadding: View? = itemView.findViewById(R.id.alternative_account_padding) + private val alternativeAccountPadding: View? = + itemView.findViewById(R.id.alternative_account_padding) private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding) - private val previewCallback: ViewPager2.OnPageChangeCallback = - object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - previewAdapter.apply { - if (position >= itemCount - 1 && hasMoreItems) { - hasMoreItems = false // don't make two requests - viewModel.loadMoreHomeScrollResponses() - } - } - val item = previewAdapter.getItem(position) ?: return - onSelect(item, position) - } - } - fun onSelect(item: LoadResponse, position: Int) { (binding as? FragmentHomeHeadTvBinding)?.apply { homePreviewDescription.isGone = @@ -379,14 +347,14 @@ class HomeParentItemAdapterPreview( homePreviewBookmark.setOnClickListener { fab -> fab.context.getActivity()?.showBottomDialog( - WatchType.values() + WatchType.entries .map { fab.context.getString(it.stringRes) } .toList(), DataStoreHelper.getResultWatchState(id).ordinal, fab.context.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - val newValue = WatchType.values()[it] + val newValue = WatchType.entries[it] ResultViewModel2().updateWatchStatus( newValue, @@ -411,38 +379,22 @@ class HomeParentItemAdapterPreview( } } - fun onViewDetachedFromWindow() { - previewViewpager.unregisterOnPageChangeCallback(previewCallback) - } - - fun onViewAttachedToWindow() { - previewViewpager.registerOnPageChangeCallback(previewCallback) - - binding.root.findViewTreeLifecycleOwner()?.apply { - observe(viewModel.preview) { - updatePreview(it) - } - if (binding is FragmentHomeHeadTvBinding) { - observe(viewModel.apiName) { name -> - binding.homePreviewChangeApi.text = name - } - } - observe(viewModel.resumeWatching) { - updateResume(it) - } - observe(viewModel.bookmarks) { - updateBookmarks(it) - } - observe(viewModel.availableWatchStatusTypes) { (checked, visible) -> - for ((chip, watch) in toggleList) { - chip.apply { - isVisible = visible.contains(watch) - isChecked = checked.contains(watch) + private val previewCallback: ViewPager2.OnPageChangeCallback = + object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + previewAdapter.apply { + if (position >= itemCount - 1 && hasMoreItems) { + hasMoreItems = false // don't make two requests + viewModel.loadMoreHomeScrollResponses() } } - toggleListHolder?.isGone = visible.isEmpty() + val item = previewAdapter.getItemOrNull(position) ?: return + onSelect(item, position) } - } ?: debugException { "Expected findViewTreeLifecycleOwner" } + } + + override fun onViewDetachedFromWindow() { + previewViewpager.unregisterOnPageChangeCallback(previewCallback) } private val toggleList = listOf>( @@ -455,6 +407,8 @@ class HomeParentItemAdapterPreview( private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder) + fun bind() = Unit + init { previewViewpager.setPageTransformer(HomeScrollTransformer()) @@ -561,7 +515,9 @@ class HomeParentItemAdapterPreview( when (preview) { is Resource.Success -> { - if (!previewAdapter.setItems( + previewAdapter.submitList(preview.value.second) + previewAdapter.hasMoreItems = preview.value.first + /*if (!.setItems( preview.value.second, preview.value.first ) @@ -573,15 +529,16 @@ class HomeParentItemAdapterPreview( previewViewpager.fakeDragBy(1f) previewViewpager.endFakeDrag() previewCallback.onPageSelected(0) - previewViewpager.isVisible = true - previewViewpagerText.isVisible = true - alternativeAccountPadding?.isVisible = false //previewHeader.isVisible = true - } + }*/ + + previewViewpager.isVisible = true + previewViewpagerText.isVisible = true + alternativeAccountPadding?.isVisible = false } else -> { - previewAdapter.setItems(listOf(), false) + previewAdapter.submitList(listOf()) previewViewpager.setCurrentItem(0, false) previewViewpager.isVisible = false previewViewpagerText.isVisible = false @@ -593,12 +550,12 @@ class HomeParentItemAdapterPreview( private fun updateResume(resumeWatching: List) { resumeHolder.isVisible = resumeWatching.isNotEmpty() - resumeAdapter.updateList(resumeWatching) + resumeAdapter.submitList(resumeWatching) if ( binding is FragmentHomeHeadBinding || binding is FragmentHomeHeadTvBinding && - binding.root.context.isEmulatorSettings() + isLayout(EMULATOR) ) { val title = (binding as? FragmentHomeHeadBinding)?.homeWatchParentItemTitle ?: (binding as? FragmentHomeHeadTvBinding)?.homeWatchParentItemTitle @@ -623,12 +580,12 @@ class HomeParentItemAdapterPreview( private fun updateBookmarks(data: Pair>) { val (visible, list) = data bookmarkHolder.isVisible = visible - bookmarkAdapter.updateList(list) + bookmarkAdapter.submitList(list) if ( binding is FragmentHomeHeadBinding || binding is FragmentHomeHeadTvBinding && - binding.root.context.isEmulatorSettings() + isLayout(EMULATOR) ) { val title = (binding as? FragmentHomeHeadBinding)?.homeBookmarkParentItemTitle ?: (binding as? FragmentHomeHeadTvBinding)?.homeBookmarkParentItemTitle @@ -653,5 +610,35 @@ class HomeParentItemAdapterPreview( } } } + + override fun onViewAttachedToWindow() { + previewViewpager.registerOnPageChangeCallback(previewCallback) + + binding.root.findViewTreeLifecycleOwner()?.apply { + observe(viewModel.preview) { + updatePreview(it) + } + if (binding is FragmentHomeHeadTvBinding) { + observe(viewModel.apiName) { name -> + binding.homePreviewChangeApi.text = name + } + } + observe(viewModel.resumeWatching) { + updateResume(it) + } + observe(viewModel.bookmarks) { + updateBookmarks(it) + } + observe(viewModel.availableWatchStatusTypes) { (checked, visible) -> + for ((chip, watch) in toggleList) { + chip.apply { + isVisible = visible.contains(watch) + isChecked = checked.contains(watch) + } + } + toggleListHolder?.isGone = visible.isEmpty() + } + } ?: debugException { "Expected findViewTreeLifecycleOwner" } + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt index 666fbc24..29186e83 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt @@ -4,112 +4,61 @@ import android.content.res.Configuration import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.isGone -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import androidx.viewbinding.ViewBinding +import androidx.fragment.app.Fragment import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.NoStateAdapter +import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.UIHelper.setImage -class HomeScrollAdapter : RecyclerView.Adapter() { - private var items: MutableList = mutableListOf() +class HomeScrollAdapter( + fragment: Fragment +) : NoStateAdapter(fragment) { var hasMoreItems: Boolean = false - fun getItem(position: Int): LoadResponse? { - return items.getOrNull(position) - } - - fun setItems(newItems: List, hasNext: Boolean): Boolean { - val isSame = newItems.firstOrNull()?.url == items.firstOrNull()?.url - hasMoreItems = hasNext - - val diffResult = DiffUtil.calculateDiff( - HomeScrollDiffCallback(this.items, newItems) - ) - - items.clear() - items.addAll(newItems) - - - diffResult.dispatchUpdatesTo(this) - - return isSame - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + override fun onCreateContent(parent: ViewGroup): ViewHolderState { val inflater = LayoutInflater.from(parent.context) - val binding = if (isTvSettings()) { + val binding = if (isLayout(TV or EMULATOR)) { HomeScrollViewTvBinding.inflate(inflater, parent, false) } else { HomeScrollViewBinding.inflate(inflater, parent, false) } - return CardViewHolder( - binding, - //forceHorizontalPosters - ) + return ViewHolderState(binding) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is CardViewHolder -> { - holder.bind(items[position]) + override fun onBindContent( + holder: ViewHolderState, + item: LoadResponse, + position: Int, + ) { + val binding = holder.view + val itemView = holder.itemView + val isHorizontal = + binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + + val posterUrl = + if (isHorizontal) item.backgroundPosterUrl ?: item.posterUrl else item.posterUrl + ?: item.backgroundPosterUrl + + when (binding) { + is HomeScrollViewBinding -> { + binding.homeScrollPreview.setImage(posterUrl) + binding.homeScrollPreviewTags.apply { + text = item.tags?.joinToString(" • ") ?: "" + isGone = item.tags.isNullOrEmpty() + maxLines = 2 + } + binding.homeScrollPreviewTitle.text = item.name + } + + is HomeScrollViewTvBinding -> { + binding.homeScrollPreview.setImage(posterUrl) } } } - - class CardViewHolder - constructor( - val binding: ViewBinding, - //private val forceHorizontalPosters: Boolean? = null - ) : - RecyclerView.ViewHolder(binding.root) { - - fun bind(card: LoadResponse) { - val isHorizontal = - binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - - val posterUrl = - if (isHorizontal) card.backgroundPosterUrl ?: card.posterUrl else card.posterUrl - ?: card.backgroundPosterUrl - - when (binding) { - is HomeScrollViewBinding -> { - binding.homeScrollPreview.setImage(posterUrl) - binding.homeScrollPreviewTags.apply { - text = card.tags?.joinToString(" • ") ?: "" - isGone = card.tags.isNullOrEmpty() - maxLines = 2 - } - binding.homeScrollPreviewTitle.text = card.name - } - - is HomeScrollViewTvBinding -> { - binding.homeScrollPreview.setImage(posterUrl) - } - } - } - } - - class HomeScrollDiffCallback( - private val oldList: List, - private val newList: List - ) : - DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition].url == newList[newItemPosition].url - - override fun getOldListSize() = oldList.size - - override fun getNewListSize() = newList.size - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition] == newList[newItemPosition] - } - - override fun getItemCount(): Int { - return items.size - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index f471fefd..a2c7583f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -34,7 +34,8 @@ import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -52,6 +53,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.withContext import java.util.EnumSet +import java.util.concurrent.CopyOnWriteArrayList import kotlin.collections.set class HomeViewModel : ViewModel() { @@ -124,7 +126,7 @@ class HomeViewModel : ViewModel() { private val _resumeWatching = MutableLiveData>() private val _preview = MutableLiveData>>>() - private val previewResponses = mutableListOf() + private val previewResponses = CopyOnWriteArrayList() private val previewResponsesAdded = mutableSetOf() val resumeWatching: LiveData> = _resumeWatching @@ -132,7 +134,7 @@ class HomeViewModel : ViewModel() { private fun loadResumeWatching() = viewModelScope.launchSafe { val resumeWatchingResult = getResumeWatching() - if (isTrueTvSettings() && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (isLayout(TV) && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ioSafe { // this WILL crash on non tvs, so keep this inside a try catch activity?.addProgramsToContinueWatching(resumeWatchingResult) @@ -326,7 +328,13 @@ class HomeViewModel : ViewModel() { val filteredList = context?.filterHomePageListByFilmQuality(list) ?: list expandable[list.name] = - ExpandableHomepageList(filteredList, 1, home.hasNext) + ExpandableHomepageList( + filteredList.copy( + list = CopyOnWriteArrayList( + filteredList.list + ) + ), 1, home.hasNext + ) } } @@ -341,8 +349,7 @@ class HomeViewModel : ViewModel() { val currentList = items.shuffled().filter { it.list.isNotEmpty() } .flatMap { it.list } - .distinctBy { it.url } - .toList() + .distinctBy { it.url }.toList() if (currentList.isNotEmpty()) { val randomItems = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index 686156b4..6c9978bc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -51,7 +51,10 @@ import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA -import com.lagradost.cloudstream3.ui.settings.SettingsFragment +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity @@ -60,6 +63,7 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import org.checkerframework.framework.qual.Unused +import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.abs const val LIBRARY_FOLDER = "library_folder" @@ -104,7 +108,7 @@ class LibraryFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { val layout = - if (SettingsFragment.isTvSettings()) R.layout.fragment_library_tv else R.layout.fragment_library + if (isLayout(TV or EMULATOR)) R.layout.fragment_library_tv else R.layout.fragment_library val root = inflater.inflate(layout, container, false) binding = try { FragmentLibraryBinding.bind(root) @@ -224,7 +228,7 @@ class LibraryFragment : Fragment() { settingsManager.getBoolean( getString(R.string.random_button_key), false - ) && !SettingsFragment.isTvSettings() + ) && isLayout(PHONE) binding?.libraryRandom?.visibility = View.GONE } @@ -232,7 +236,7 @@ class LibraryFragment : Fragment() { if (listLibraryItems.isNotEmpty()) { val listLibraryItem = listLibraryItems.random() libraryViewModel.currentSyncApi?.syncIdName?.let { - loadLibraryItem(it, listLibraryItem.syncId,listLibraryItem) + loadLibraryItem(it, listLibraryItem.syncId, listLibraryItem) } } } @@ -311,44 +315,46 @@ class LibraryFragment : Fragment() { binding?.viewpager?.setPageTransformer(LibraryScrollTransformer()) - binding?.viewpager?.adapter = - binding?.viewpager?.adapter ?: ViewpagerAdapter( - mutableListOf(), - { isScrollingDown: Boolean -> - if (isScrollingDown) { - binding?.sortFab?.shrink() - binding?.libraryRandom?.shrink() - } else { - binding?.sortFab?.extend() - binding?.libraryRandom?.extend() - } - }) callback@{ searchClickCallback -> - // To prevent future accidents - debugAssert({ - searchClickCallback.card !is SyncAPI.LibraryItem - }, { - "searchClickCallback ${searchClickCallback.card} is not a LibraryItem" - }) + binding?.viewpager?.adapter = ViewpagerAdapter( + fragment = this, + { isScrollingDown: Boolean -> + if (isScrollingDown) { + binding?.sortFab?.shrink() + binding?.libraryRandom?.shrink() + } else { + binding?.sortFab?.extend() + binding?.libraryRandom?.extend() + } + }) callback@{ searchClickCallback -> + // To prevent future accidents + debugAssert({ + searchClickCallback.card !is SyncAPI.LibraryItem + }, { + "searchClickCallback ${searchClickCallback.card} is not a LibraryItem" + }) - val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId - val syncName = - libraryViewModel.currentSyncApi?.syncIdName ?: return@callback + val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId + val syncName = + libraryViewModel.currentSyncApi?.syncIdName ?: return@callback - when (searchClickCallback.action) { - SEARCH_ACTION_SHOW_METADATA -> { - (activity as? MainActivity)?.loadPopup(searchClickCallback.card, load = false) + when (searchClickCallback.action) { + SEARCH_ACTION_SHOW_METADATA -> { + (activity as? MainActivity)?.loadPopup( + searchClickCallback.card, + load = false + ) /*activity?.showPluginSelectionDialog( syncId, syncName, searchClickCallback.card.apiName )*/ - } + } - SEARCH_ACTION_LOAD -> { - loadLibraryItem(syncName, syncId, searchClickCallback.card) - } + SEARCH_ACTION_LOAD -> { + loadLibraryItem(syncName, syncId, searchClickCallback.card) } } + } binding?.apply { viewpager.offscreenPageLimit = 2 @@ -394,7 +400,11 @@ class LibraryFragment : Fragment() { } } - (viewpager.adapter as? ViewpagerAdapter)?.pages = pages + (viewpager.adapter as? ViewpagerAdapter)?.submitList(pages.map { + it.copy( + items = CopyOnWriteArrayList(it.items) + ) + }) //fix focus on the viewpager itself (viewpager.getChildAt(0) as RecyclerView).apply { tag = "tv_no_focus_tag" @@ -402,10 +412,10 @@ class LibraryFragment : Fragment() { } // Using notifyItemRangeChanged keeps the animations when sorting - viewpager.adapter?.notifyItemRangeChanged( + /*viewpager.adapter?.notifyItemRangeChanged( 0, viewpager.adapter?.itemCount ?: 0 - ) + )*/ libraryViewModel.currentPage.value?.let { page -> binding?.viewpager?.setCurrentItem(page, false) @@ -463,12 +473,14 @@ class LibraryFragment : Fragment() { } }.attach() - binding?.libraryTabLayout?.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener { + binding?.libraryTabLayout?.addOnTabSelectedListener(object : + TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { binding?.libraryTabLayout?.selectedTabPosition?.let { page -> libraryViewModel.switchPage(page) } } + override fun onTabUnselected(tab: TabLayout.Tab?) = Unit override fun onTabReselected(tab: TabLayout.Tab?) = Unit }) @@ -568,8 +580,9 @@ class LibraryFragment : Fragment() { } + @SuppressLint("NotifyDataSetChanged") override fun onConfigurationChanged(newConfig: Configuration) { - (binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind() + binding?.viewpager?.adapter?.notifyDataSetChanged() super.onConfigurationChanged(newConfig) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index c983ea2f..1bd01c86 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -113,7 +113,7 @@ class LibraryViewModel : ViewModel() { } val desiredSortingMethod = - ListSorting.values().getOrNull(DataStoreHelper.librarySortingMode) + ListSorting.entries.getOrNull(DataStoreHelper.librarySortingMode) if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) { sort(desiredSortingMethod, null, pages) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt index 6731eae2..cfd22220 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -1,104 +1,123 @@ package com.lagradost.cloudstream3.ui.library import android.os.Build -import android.util.Log +import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.doOnAttach -import androidx.recyclerview.widget.RecyclerView +import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView.OnFlingListener import com.google.android.material.appbar.AppBarLayout import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.ui.BaseAdapter +import com.lagradost.cloudstream3.ui.BaseDiffCallback +import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.search.SearchClickCallback -import com.lagradost.cloudstream3.ui.settings.SettingsFragment +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount +class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding) : + ViewHolderState(binding) { + override fun save(): Bundle = + Bundle().apply { + putParcelable( + "pageRecyclerview", + binding.pageRecyclerview.layoutManager?.onSaveInstanceState() + ) + } + + override fun restore(state: Bundle) { + state.getParcelable("pageRecyclerview")?.let { recycle -> + binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle) + } + } +} + class ViewpagerAdapter( - var pages: List, + fragment: Fragment, val scrollCallback: (isScrollingDown: Boolean) -> Unit, val clickCallback: (SearchClickCallback) -> Unit -) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return PageViewHolder( +) : BaseAdapter(fragment, + id = "ViewpagerAdapter".hashCode(), + diffCallback = BaseDiffCallback( + itemSame = { a, b -> + a.title == b.title + }, + contentSame = { a, b -> + a.items == b.items && a.title == b.title + } +)) { + override fun onCreateContent(parent: ViewGroup): ViewHolderState { + return ViewpagerAdapterViewHolderState( LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is PageViewHolder -> { - holder.bind(pages[position], position, unbound.remove(position)) - } - } + override fun onUpdateContent( + holder: ViewHolderState, + item: SyncAPI.Page, + position: Int + ) { + val binding = holder.view + if (binding !is LibraryViewpagerPageBinding) return + (binding.pageRecyclerview.adapter as? PageAdapter)?.updateList(item.items) } - private val unbound = mutableSetOf() + override fun onBindContent(holder: ViewHolderState, item: SyncAPI.Page, position: Int) { + val binding = holder.view + if (binding !is LibraryViewpagerPageBinding) return - /** - * Used to mark all pages for re-binding and forces all items to be refreshed - * Without this the pages will still use the same adapters - **/ - fun rebind() { - unbound.addAll(0..pages.size) - this.notifyItemRangeChanged(0, pages.size) - } - - inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) { - binding.pageRecyclerview.tag = position - binding.pageRecyclerview.apply { - spanCount = - this@PageViewHolder.itemView.context.getSpanCount() ?: 3 - if (adapter == null || rebind) { - // Only add the items after it has been attached since the items rely on ItemWidth - // Which is only determined after the recyclerview is attached. - // If this fails then item height becomes 0 when there is only one item - doOnAttach { - adapter = PageAdapter( - page.items.toMutableList(), - this, - clickCallback - ) - } - } else { - (adapter as? PageAdapter)?.updateList(page.items) - scrollToPosition(0) + binding.pageRecyclerview.tag = position + binding.pageRecyclerview.apply { + spanCount = + binding.root.context.getSpanCount() ?: 3 + if (adapter == null) { // || rebind + // Only add the items after it has been attached since the items rely on ItemWidth + // Which is only determined after the recyclerview is attached. + // If this fails then item height becomes 0 when there is only one item + doOnAttach { + adapter = PageAdapter( + item.items.toMutableList(), + this, + clickCallback + ) } + } else { + (adapter as? PageAdapter)?.updateList(item.items) + // scrollToPosition(0) + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> - val diff = scrollY - oldScrollY + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> + val diff = scrollY - oldScrollY - //Expand the top Appbar based on scroll direction up/down, simulate phone behavior - if (SettingsFragment.isTvSettings()) { - binding.root.rootView.findViewById(R.id.search_bar) - .apply { - if (diff <= 0) - setExpanded(true) - else - setExpanded(false) - } - } - if (diff == 0) return@setOnScrollChangeListener - - scrollCallback.invoke(diff > 0) + //Expand the top Appbar based on scroll direction up/down, simulate phone behavior + if (isLayout(TV or EMULATOR)) { + binding.root.rootView.findViewById(R.id.search_bar) + .apply { + if (diff <= 0) + setExpanded(true) + else + setExpanded(false) + } } - } else { - onFlingListener = object : OnFlingListener() { - override fun onFling(velocityX: Int, velocityY: Int): Boolean { - scrollCallback.invoke(velocityY > 0) - return false - } + if (diff == 0) return@setOnScrollChangeListener + + scrollCallback.invoke(diff > 0) + } + } else { + onFlingListener = object : OnFlingListener() { + override fun onFling(velocityX: Int, velocityY: Int): Boolean { + scrollCallback.invoke(velocityY > 0) + return false } } } } } - - override fun getItemCount(): Int { - return pages.size - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index cfa6682d..0865b220 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -1,7 +1,10 @@ package com.lagradost.cloudstream3.ui.player import android.annotation.SuppressLint -import android.content.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.AnimatedVectorDrawable import android.media.metrics.PlaybackErrorEvent @@ -24,11 +27,7 @@ import androidx.fragment.app.Fragment import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession -import androidx.media3.ui.AspectRatioFrameLayout -import androidx.media3.ui.DefaultTimeBar -import androidx.media3.ui.PlayerView -import androidx.media3.ui.SubtitleView -import androidx.media3.ui.TimeBar +import androidx.media3.ui.* import androidx.preference.PreferenceManager import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.github.rubensousa.previewseekbar.PreviewBar @@ -442,6 +441,9 @@ abstract class AbstractPlayerFragment( is VideoEndedEvent -> { context?.let { ctx -> + // Resets subtitle delay on ended video + player.setSubtitleOffset(0) + // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(ctx) ?.getBoolean( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 210bfdca..31adbc87 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -1118,6 +1118,9 @@ class CS3IPlayer : IPlayer { } Player.STATE_ENDED -> { + // Resets subtitle delay on ended video + setSubtitleOffset(0) + // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(context) ?.getBoolean( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 1e2ea540..4d8860f8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -17,7 +17,7 @@ import com.lagradost.safefile.SafeFile const val DTAG = "PlayerActivity" class DownloadedPlayerActivity : AppCompatActivity() { - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + override fun dispatchKeyEvent(event: KeyEvent): Boolean { CommonActivity.dispatchKeyEvent(this, event)?.let { return it } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index e8d74752..aa25157b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -14,13 +14,7 @@ import android.os.Bundle import android.provider.Settings import android.text.Editable import android.text.format.DateUtils -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.Surface -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager +import android.view.* import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES import android.view.animation.AlphaAnimation import android.view.animation.Animation @@ -38,6 +32,7 @@ import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.screenWidth +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding @@ -46,7 +41,10 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvid import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.ui.settings.SettingsFragment +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog @@ -77,7 +75,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private var isVerticalOrientation: Boolean = false protected open var lockRotation = true protected open var isFullScreenPlayer = true - protected open var isTv = false protected var playerBinding: PlayerCustomLayoutBinding? = null private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false) @@ -181,7 +178,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { open fun openOnlineSubPicker( context: Context, - imdbId: Long?, + loadResponse: LoadResponse?, dismissCallback: (() -> Unit) ) { throw NotImplementedError() @@ -495,6 +492,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { dialog.dismissSafe(activity) player.seekTime(1L) } + resetBtt.setOnClickListener { + subtitleDelay = 0 + dialog.dismissSafe(activity) + player.seekTime(1L) + } cancelBtt.setOnClickListener { subtitleDelay = beforeOffset dialog.dismissSafe(activity) @@ -1156,6 +1158,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } + KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_UP -> { if (!isShowing) { onClickChange() @@ -1204,7 +1207,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { // netflix capture back and hide ~monke KeyEvent.KEYCODE_BACK -> { - if (isShowing && isTv) { + if (isShowing && isLayout(TV or EMULATOR)) { onClickChange() return true } @@ -1514,7 +1517,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } // cs3 is peak media center - setRemainingTimeCounter(durationMode || SettingsFragment.isTrueTvSettings()) + setRemainingTimeCounter(durationMode || Globals.isLayout(Globals.TV)) playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ -> updateRemainingTime() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index e14514c1..b46b6aba 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -25,6 +25,10 @@ import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.getTMDbId import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding @@ -40,7 +44,9 @@ import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSub import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog import com.lagradost.cloudstream3.ui.result.* -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1 import com.lagradost.cloudstream3.utils.* @@ -257,6 +263,7 @@ class GeneratorPlayer : FullScreenPlayer() { var episode: Int? = null, var season: Int? = null, var name: String? = null, + var imdbId: String? = null, ) private fun getMetaData(): TempMetaData { @@ -283,7 +290,7 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun openOnlineSubPicker( - context: Context, imdbId: Long?, dismissCallback: (() -> Unit) + context: Context, loadResponse: LoadResponse?, dismissCallback: (() -> Unit) ) { val providers = subsProviders val isSingleProvider = subsProviders.size == 1 @@ -376,6 +383,7 @@ class GeneratorPlayer : FullScreenPlayer() { } val currentTempMeta = getMetaData() + // bruh idk why it is not correct val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent)) binding.searchLoadingBar.progressTintList = color @@ -423,7 +431,10 @@ class GeneratorPlayer : FullScreenPlayer() { val search = AbstractSubtitleEntities.SubtitleSearch( query = query ?: return@ioSafe, - imdb = imdbId, + imdbId = loadResponse?.getImdbId(), + tmdbId = loadResponse?.getTMDbId()?.toInt(), + malId = loadResponse?.getMalId()?.toInt(), + aniListId = loadResponse?.getAniListId()?.toInt(), epNumber = currentTempMeta.episode, seasonNumber = currentTempMeta.season, lang = currentLanguageTwoLetters.ifBlank { null }, @@ -632,6 +643,8 @@ class GeneratorPlayer : FullScreenPlayer() { } if (subsProvidersIsActive) { + val currentLoadResponse = viewModel.getLoadResponse() + val loadFromOpenSubsFooter: TextView = layoutInflater.inflate( R.layout.sort_bottom_footer_add_choice, null ) as TextView @@ -642,7 +655,7 @@ class GeneratorPlayer : FullScreenPlayer() { loadFromOpenSubsFooter.setOnClickListener { shouldDismiss = false sourceDialog.dismissSafe(activity) - openOnlineSubPicker(it.context, null) { + openOnlineSubPicker(it.context, currentLoadResponse) { dismiss() } } @@ -1278,8 +1291,7 @@ class GeneratorPlayer : FullScreenPlayer() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // this is used instead of layout-television to follow the settings and some TV devices are not classified as TV for some reason - isTv = isTvSettings() - layout = if (isTv) R.layout.fragment_player_tv else R.layout.fragment_player + layout = if (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java] sync = ViewModelProvider(this)[SyncViewModel::class.java] diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index af74cb57..c5de1a1c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -10,7 +10,8 @@ enum class LoadType { InAppDownload, ExternalApp, Browser, - Chromecast + Chromecast, + Fcast } fun LoadType.toSet() : Set { @@ -29,12 +30,17 @@ fun LoadType.toSet() : Set { ExtractorLinkType.VIDEO, ExtractorLinkType.M3U8 ) - LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet() + LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.entries.toSet() LoadType.Chromecast -> setOf( ExtractorLinkType.VIDEO, ExtractorLinkType.DASH, ExtractorLinkType.M3U8 ) + LoadType.Fcast -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 0d98f205..ee44567f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError @@ -111,6 +112,9 @@ class PlayerGeneratorViewModel : ViewModel() { } } } + fun getLoadResponse(): LoadResponse? { + return normalSafeApiCall { (generator as? RepoLinkGenerator?)?.page } + } fun getMeta(): Any? { return normalSafeApiCall { generator?.getCurrent() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index 6414374b..fb600ef1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -9,6 +9,9 @@ import android.util.Log import androidx.annotation.WorkerThread import androidx.core.graphics.scale import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.ExtractorLink @@ -63,7 +66,7 @@ interface IPreviewGenerator { companion object { fun new(): IPreviewGenerator { /** because TV has low ram + not show we disable this for now */ - return if (SettingsFragment.isTrueTvSettings()) { + return if (isLayout(TV)) { empty() } else { PreviewGenerator() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index 5b300c06..85e20d1c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -34,7 +34,11 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar @@ -173,7 +177,7 @@ class QuickSearchFragment : Fragment() { } } else { binding?.quickSearchMasterRecycler?.adapter = - ParentItemAdapter(mutableListOf(), { callback -> + ParentItemAdapter(fragment = this, id = "quickSearchMasterRecycler".hashCode(), { callback -> SearchHelper.handleSearchClickCallback(callback) //when (callback.action) { //SEARCH_ACTION_LOAD -> { @@ -273,11 +277,16 @@ class QuickSearchFragment : Fragment() { // UIHelper.showInputMethod(view.findFocus()) // } //} - binding?.quickSearchBack?.setOnClickListener { - activity?.popCurrentPage() + if (isLayout(PHONE or EMULATOR)) { + binding?.quickSearchBack?.apply { + isVisible = true + setOnClickListener { + activity?.popCurrentPage() + } + } } - if (isTrueTvSettings()) { + if (isLayout(TV)) { binding?.quickSearch?.requestFocus() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index b12475bf..e4fd0559 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -9,18 +9,24 @@ import androidx.core.view.isVisible import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK import com.lagradost.cloudstream3.ui.download.DownloadClickEvent -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import java.text.DateFormat +import java.text.SimpleDateFormat import java.util.* const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 @@ -49,6 +55,8 @@ const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16 const val ACTION_PLAY_EPISODE_IN_MPV = 17 const val ACTION_MARK_AS_WATCHED = 18 +const val ACTION_FCAST = 19 + const val TV_EP_SIZE_LARGE = 400 const val TV_EP_SIZE_SMALL = 300 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) @@ -102,7 +110,7 @@ class EpisodeAdapter( override fun getItemViewType(position: Int): Int { val item = getItem(position) - return if (item.poster.isNullOrBlank()) 0 else 1 + return if (item.poster.isNullOrBlank() && item.description.isNullOrBlank()) 0 else 1 } @@ -172,15 +180,13 @@ class EpisodeAdapter( @SuppressLint("SetTextI18n") fun bind(card: ResultEpisode) { localCard = card - val setWidth = - if (isTvSettings()) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT + if (isLayout(TV or EMULATOR)) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT binding.episodeLinHolder.layoutParams.width = setWidth binding.episodeHolderLarge.layoutParams.width = setWidth binding.episodeHolder.layoutParams.width = setWidth - val isTrueTv = isTrueTvSettings() binding.apply { downloadButton.isVisible = hasDownloadSupport @@ -249,7 +255,7 @@ class EpisodeAdapter( var isExpanded = false setOnClickListener { - if (isTrueTv) { + if (isLayout(TV)) { clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card)) } else { isExpanded = !isExpanded @@ -260,7 +266,34 @@ class EpisodeAdapter( } } - if (!isTrueTv) { + if (card.airDate != null) { + val isUpcoming = unixTimeMS < card.airDate + + if (isUpcoming) { + episodePlayIcon.isVisible = false + episodeUpcomingIcon.isVisible = !episodePoster.isVisible + episodeDate.setText( + txt( + R.string.episode_upcoming_format, + secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "") + ) + ) + } else { + episodeUpcomingIcon.isVisible = false + + val formattedAirDate = SimpleDateFormat.getDateInstance( + DateFormat.LONG, + Locale.getDefault() + ).apply { + }.format(Date(card.airDate)) + + episodeDate.setText(txt(formattedAirDate)) + } + } else { + episodeDate.isVisible = false + } + + if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } @@ -271,11 +304,12 @@ class EpisodeAdapter( } } } + itemView.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } - if (isTrueTv) { + if (isLayout(TV)) { itemView.isFocusable = true itemView.isFocusableInTouchMode = true //itemView.touchscreenBlocksFocus = false @@ -300,11 +334,9 @@ class EpisodeAdapter( ) : RecyclerView.ViewHolder(binding.root) { @SuppressLint("SetTextI18n") fun bind(card: ResultEpisode) { - val isTrueTv = isTrueTvSettings() - binding.episodeHolder.layoutParams.apply { width = - if (isTvSettings()) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT + if (isLayout(TV or EMULATOR)) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT } binding.apply { @@ -361,7 +393,7 @@ class EpisodeAdapter( clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } - if (isTrueTv) { + if (isLayout(TV)) { itemView.isFocusable = true itemView.isFocusableInTouchMode = true //itemView.touchscreenBlocksFocus = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt index ca2934ef..7b7bae43 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt @@ -5,7 +5,8 @@ import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout /* class ImageAdapter(context: Context, val resource: Int) : ArrayAdapter(context, resource) { @@ -83,7 +84,7 @@ class ImageAdapter( this.nextFocusUpId = nextFocusUp } if (clickCallback != null) { - if (isTrueTvSettings()) { + if (isLayout(TV)) { isClickable = true isLongClickable = true isFocusable = true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index a1574eec..1d3f5a08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -50,6 +50,7 @@ data class ResultEpisode( val videoWatchState: VideoWatchState, /** Sum of all previous season episode counts + episode */ val totalEpisodeIndex: Int? = null, + val airDate: Long? = null, ) fun ResultEpisode.getRealPosition(): Long { @@ -85,6 +86,7 @@ fun buildResultEpisode( tvType: TvType, parentId: Int, totalEpisodeIndex: Int? = null, + airDate: Long? = null, ): ResultEpisode { val posDur = getViewPos(id) val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None @@ -107,7 +109,8 @@ fun buildResultEpisode( tvType, parentId, videoWatchState, - totalEpisodeIndex + totalEpisodeIndex, + airDate, ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index 6105e8c9..fb5160a7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -2,9 +2,6 @@ package com.lagradost.cloudstream3.ui.result import android.annotation.SuppressLint import android.app.Dialog -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.graphics.Rect @@ -33,7 +30,6 @@ import com.google.android.gms.cast.framework.CastState import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.updateHasTrailers -import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse @@ -65,11 +61,13 @@ import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.openBrowser +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.openBatteryOptimizationSettings import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper +import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard @@ -445,8 +443,9 @@ open class ResultFragmentPhone : FullScreenPlayer() { val name = (viewModel.page.value as? Resource.Success)?.value?.title ?: txt(R.string.no_data).asStringNull(context) ?: "" - CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + showToast(txt(message, name), Toast.LENGTH_SHORT) } + context?.let { openBatteryOptimizationSettings(it) } } resultFavorite.setOnClickListener { viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? -> @@ -460,7 +459,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { val name = (viewModel.page.value as? Resource.Success)?.value?.title ?: txt(R.string.no_data).asStringNull(context) ?: "" - CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + showToast(txt(message, name), Toast.LENGTH_SHORT) } } mediaRouteButton.apply { @@ -468,7 +467,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { alpha = if (chromecastSupport) 1f else 0.3f if (!chromecastSupport) { setOnClickListener { - CommonActivity.showToast( + showToast( R.string.no_chromecast_support_toast, Toast.LENGTH_LONG ) @@ -643,6 +642,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { ), null ) { click -> + context?.let { openBatteryOptimizationSettings(it) } + when (click.action) { DOWNLOAD_ACTION_DOWNLOAD -> { viewModel.handleAction( @@ -757,14 +758,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { resultReloadConnectionOpenInBrowser.isVisible = data is Resource.Failure resultTitle.setOnLongClickListener { - val titleToCopy = resultTitle.text - val clipboardManager = - activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager? - clipboardManager?.setPrimaryClip(ClipData.newPlainText("Title", titleToCopy)) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - showToast(R.string.copyTitle, Toast.LENGTH_SHORT) - } - return@setOnLongClickListener true + clipboardHelper(txt(R.string.title), resultTitle.text) + true } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index bd7105ee..13621cda 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -33,14 +33,17 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer +import com.lagradost.cloudstream3.ui.player.NEXT_WATCH_EPISODE_PERCENTAGE import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.AppUtils.loadCache @@ -309,9 +312,10 @@ class ResultFragmentTv : Fragment() { resultEpisodesShowButton to resultEpisodesShowText ).forEach { (button , text) -> - button.setOnFocusChangeListener { _, hasFocus -> + button.setOnFocusChangeListener { view, hasFocus -> if (!hasFocus) { text.isSelected = false + if (view.id == R.id.result_episodes_show_button) toggleEpisodes(false) return@setOnFocusChangeListener } @@ -377,10 +381,6 @@ class ResultFragmentTv : Fragment() { resultMetaSite.isFocusable = false - //resultReloadConnectionOpenInBrowser.setOnClickListener {view -> - // view.context?.openBrowser(storedData?.url ?: return@setOnClickListener, fallbackWebview = true) - //} - resultSeasonSelection.setAdapter() resultRangeSelection.setAdapter() resultDubSelection.setAdapter() @@ -458,11 +458,12 @@ class ResultFragmentTv : Fragment() { observeNullable(viewModel.resumeWatching) { resume -> binding?.apply { - // > resultResumeSeries is visible when not null if (resume == null) { - resultResumeSeries.isVisible = false return@observeNullable } + resultResumeSeries.isVisible = true + resultPlayMovie.isVisible = false + resultPlaySeries.isVisible = false // show progress no matter if series or movie resume.progress?.let { progress -> @@ -477,10 +478,6 @@ class ResultFragmentTv : Fragment() { resultResumeProgressHolder.isVisible = false } - resultPlayMovie.isVisible = false - resultPlaySeries.isVisible = false - resultResumeSeries.isVisible = true - focusPlayButton() // Stops last button right focus if it is a movie if (resume.isMovie) @@ -491,7 +488,7 @@ class ResultFragmentTv : Fragment() { resume.isMovie -> context?.getString(R.string.resume) resume.result.season != null -> "${getString(R.string.season_short)}${resume.result.season}:${getString(R.string.episode_short)}${resume.result.episode}" - else -> "${getString(R.string.episode)}${resume.result.episode}" + else -> "${getString(R.string.episode)} ${resume.result.episode}" } resultResumeSeriesButton.setOnClickListener { @@ -604,7 +601,7 @@ class ResultFragmentTv : Fragment() { } observeNullable(viewModel.subscribeStatus) { isSubscribed -> - binding?.resultSubscribe?.isVisible = isSubscribed != null && requireContext().isEmulatorSettings() + binding?.resultSubscribe?.isVisible = isSubscribed != null && isLayout(EMULATOR) binding?.resultSubscribeButton?.apply { if (isSubscribed == null) return@observeNullable @@ -647,15 +644,14 @@ class ResultFragmentTv : Fragment() { } observeNullable(viewModel.movie) { data -> - if (data == null) return@observeNullable + if (data == null ) { + return@observeNullable + } binding?.apply { - resultPlayMovie.isVisible = (data is Resource.Success) && !comingSoon - resultPlaySeries.isVisible = false - resultEpisodesShow.isVisible = false (data as? Resource.Success)?.value?.let { (text, ep) -> - //resultPlayMovieText.setText(text) + resultPlayMovieButton.setOnClickListener { viewModel.handleAction( EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep) @@ -667,14 +663,17 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } - //focusPlayButton() - resultPlayMovieButton.requestFocus() + + resultPlayMovie.isVisible = !comingSoon && resultResumeSeries.isGone + if (comingSoon) + resultBookmarkButton.requestFocus() + else + resultPlayMovieButton.requestFocus() // Stops last button right focus resultSearchButton.nextFocusRightId = R.id.result_search_Button } } - //focusPlayButton() } observeNullable(viewModel.selectPopup) { popup -> @@ -756,7 +755,7 @@ class ResultFragmentTv : Fragment() { setRecommendations(recommendations, null) } - if (isTrueTvSettings()) { + if (isLayout(TV)) { observe(viewModel.episodeSynopsis) { description -> view.context?.let { ctx -> val builder: AlertDialog.Builder = @@ -778,39 +777,44 @@ class ResultFragmentTv : Fragment() { binding?.apply { - resultPlayMovie.isVisible = false - resultPlaySeries.isVisible = true && !comingSoon - resultEpisodes.isVisible = true && !comingSoon - resultEpisodesShow.isVisible = true && !comingSoon + if (comingSoon) + resultBookmarkButton.requestFocus() // resultEpisodeLoading.isVisible = episodes is Resource.Loading if (episodes is Resource.Success) { - val first = episodes.value.firstOrNull() - if (first != null) { - resultPlaySeriesText.text = //"${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}" + + val lastWatchedIndex = episodes.value.indexOfLast { ep -> + ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f || ep.videoWatchState == VideoWatchState.Watched + } + + val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() } + + if (firstUnwatched != null) { + resultPlaySeriesText.text = when { - first.season != null -> - "${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}" - else -> "${getString(R.string.episode)} ${first.episode}" + firstUnwatched.season != null -> + "${getString(R.string.season_short)}${firstUnwatched.season}:${getString(R.string.episode_short)}${firstUnwatched.episode}" + else -> "${getString(R.string.episode)} ${firstUnwatched.episode}" } resultPlaySeriesButton.setOnClickListener { viewModel.handleAction( EpisodeClickEvent( ACTION_CLICK_DEFAULT, - first + firstUnwatched ) ) } resultPlaySeriesButton.setOnLongClickListener { viewModel.handleAction( - EpisodeClickEvent(ACTION_SHOW_OPTIONS, first) + EpisodeClickEvent(ACTION_SHOW_OPTIONS, firstUnwatched) ) return@setOnLongClickListener true } if (!hasLoadedEpisodesOnce) { hasLoadedEpisodesOnce = true - focusPlayButton() - resultPlaySeries.requestFocus() + resultPlaySeries.isVisible = resultResumeSeries.isGone && !comingSoon + resultEpisodesShow.isVisible = true && !comingSoon + resultPlaySeriesButton.requestFocus() } } @@ -883,7 +887,7 @@ class ResultFragmentTv : Fragment() { resultDescription.apply { setTextHtml(d.plotText) setOnClickListener { - if (context.isEmulatorSettings()) { + if (isLayout(EMULATOR)) { isExpanded = !isExpanded maxLines = if (isExpanded) { Integer.MAX_VALUE @@ -919,9 +923,6 @@ class ResultFragmentTv : Fragment() { ) comingSoon = d.comingSoon resultTvComingSoon.isVisible = d.comingSoon - resultPlayMovie.isGone = d.comingSoon - resultPlaySeries.isGone = d.comingSoon - resultDataHolder.isGone = d.comingSoon UIHelper.populateChips(resultTag, d.tags) resultCastItems.isGone = d.actors.isNullOrEmpty() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index ef3db0b4..135dc530 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -12,6 +12,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.screenWidth +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.PlayerEventSource @@ -110,7 +111,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { override fun openOnlineSubPicker( context: Context, - imdbId: Long?, + loadResponse: LoadResponse?, dismissCallback: () -> Unit ) { } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index a05b4059..4285feb1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -5,6 +5,7 @@ import android.content.* import android.net.Uri import android.os.Build import android.os.Bundle +import android.text.format.Formatter.formatFileSize import android.util.Log import android.widget.Toast import androidx.annotation.MainThread @@ -20,15 +21,23 @@ import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS -import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie +import com.lagradost.cloudstream3.MainActivity.Companion.MPV +import com.lagradost.cloudstream3.MainActivity.Companion.MPV_COMPONENT +import com.lagradost.cloudstream3.MainActivity.Companion.MPV_PACKAGE +import com.lagradost.cloudstream3.MainActivity.Companion.VLC +import com.lagradost.cloudstream3.MainActivity.Companion.VLC_COMPONENT +import com.lagradost.cloudstream3.MainActivity.Companion.VLC_PACKAGE +import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO +import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO_CAST_PACKAGE import com.lagradost.cloudstream3.metaproviders.SyncRedirector import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.syncproviders.AccountManager @@ -81,12 +90,16 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.setSubscribedData import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData +import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.fcast.FcastManager +import com.lagradost.cloudstream3.utils.fcast.FcastSession +import com.lagradost.cloudstream3.utils.fcast.Opcode +import com.lagradost.cloudstream3.utils.fcast.PlayMessage import kotlinx.coroutines.* import java.io.File import java.util.concurrent.TimeUnit - /** This starts at 1 */ data class EpisodeRange( // used to index data @@ -197,7 +210,11 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { else -> null }?.also { - nextAiringEpisode = txt(R.string.next_episode_format, airing.episode) + nextAiringEpisode = when (airing.season) { + + null -> txt(R.string.next_episode_format, airing.episode) + else -> txt(R.string.next_season_episode_format, airing.season, airing.episode) + } } } } @@ -246,6 +263,9 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { TvType.Live -> R.string.live_singular TvType.Others -> R.string.other_singular TvType.NSFW -> R.string.nsfw_singular + TvType.Music -> R.string.music_singlar + TvType.AudioBook -> R.string.audio_book_singular + TvType.CustomMedia -> R.string.custom_media_singluar } ), yearText = txt(year?.toString()), @@ -627,6 +647,9 @@ class ResultViewModel2 : ViewModel() { TvType.Live -> "LiveStreams" TvType.NSFW -> "NSFW" TvType.Others -> "Others" + TvType.Music -> "Music" + TvType.AudioBook -> "AudioBooks" + TvType.CustomMedia -> "Media" } } @@ -928,15 +951,20 @@ class ResultViewModel2 : ViewModel() { ) { val isSubscribed = _subscribeStatus.value ?: return val response = currentResponse ?: return - if (response !is EpisodeResponse) return - val currentId = currentId ?: return + // This might be a bit confusing, but even if the loadresponse is not a EpisodeResponse + // _subscribeStatus might be true. + if (isSubscribed) { removeSubscribedData(currentId) statusChangedCallback?.invoke(false) - _subscribeStatus.postValue(false) + _subscribeStatus.postValue(if (response is EpisodeResponse) false else null) + MainActivity.reloadLibraryEvent(true) } else { + if (response !is EpisodeResponse) { + return + } checkAndWarnDuplicates( context, LibraryListType.SUBSCRIPTIONS, @@ -981,8 +1009,8 @@ class ResultViewModel2 : ViewModel() { ) _subscribeStatus.postValue(true) - statusChangedCallback?.invoke(true) + MainActivity.reloadLibraryEvent(true) } } } @@ -1088,13 +1116,14 @@ class ResultViewModel2 : ViewModel() { val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse -> val librarySyncData = it.syncData + val yearCheck = year == it.year || year == null || it.year == null val checks = listOf( { imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId }, { tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId }, { malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId }, { aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId }, - { normalizedName == normalizeString(it.name) && year == it.year } + { normalizedName == normalizeString(it.name) && yearCheck } ) checks.any { it() } @@ -1269,9 +1298,14 @@ class ResultViewModel2 : ViewModel() { callback: (Pair) -> Unit, ) { loadLinks(result, isVisible = true, type) { links -> + // Could not find a better way to do this + val context = AcraApplication.context postPopup( text, - links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { + links.links.apmap { + val size = it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: "" + txt("${it.name} ${Qualities.getStringByInt(it.quality)}$size") + }) { callback.invoke(links to (it ?: return@postPopup)) } } @@ -1329,7 +1363,7 @@ class ResultViewModel2 : ViewModel() { private fun launchActivity( activity: Activity?, - resumeApp: ResultResume, + resumeApp: MainActivity.Companion.ResultResume, id: Int? = null, work: suspend (Intent.(Activity) -> Unit) ): Job? { @@ -1498,6 +1532,13 @@ class ResultViewModel2 : ViewModel() { ) ) } + + if (FcastManager.currentDevices.isNotEmpty()) { + options.add( + txt(R.string.player_settings_play_in_fcast) to ACTION_FCAST + ) + } + options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER) for (app in apps) { @@ -1673,6 +1714,39 @@ class ResultViewModel2 : ViewModel() { } } + ACTION_FCAST -> { + val devices = FcastManager.currentDevices.toList() + postPopup( + txt(R.string.player_settings_select_cast_device), + devices.map { txt(it.name) }) { index -> + if (index == null) return@postPopup + val device = devices.getOrNull(index) + + acquireSingleLink( + click.data, + LoadType.Fcast, + txt(R.string.episode_action_cast_mirror) + ) { (result, index) -> + val host = device?.host ?: return@acquireSingleLink + val link = result.links.getOrNull(index) ?: return@acquireSingleLink + + FcastSession(host).use { session -> + session.sendMessage( + Opcode.Play, + PlayMessage( + link.type.getMimeType(), + link.url, + headers = mapOf( + "referer" to link.referer, + "user-agent" to USER_AGENT + ) + link.headers + ) + ) + } + } + } + } + ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( click.data, LoadType.Browser, @@ -1693,14 +1767,8 @@ class ResultViewModel2 : ViewModel() { LoadType.ExternalApp, txt(R.string.episode_action_copy_link) ) { (result, index) -> - val act = activity ?: return@acquireSingleLink - val serviceClipboard = - (act.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?) - ?: return@acquireSingleLink val link = result.links[index] - val clip = ClipData.newPlainText(link.name, link.url) - serviceClipboard.setPrimaryClip(clip) - showToast(R.string.copy_link_toast, Toast.LENGTH_SHORT) + clipboardHelper(txt(link.name), link.url) } } @@ -1760,20 +1828,28 @@ class ResultViewModel2 : ViewModel() { val data = currentResponse?.syncData?.toList() ?: emptyList() val list = HashMap().apply { putAll(data) } - - activity?.navigate( - R.id.global_to_navigation_player, - GeneratorPlayer.newInstance( - generator?.also { - it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work - ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } - ?.let { index -> - if (index >= 0) - it.goto(index) - } - } ?: return, list + generator?.also { + it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work + ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } + ?.let { index -> + if (index >= 0) + it.goto(index) + } + } + if (currentResponse?.type == TvType.CustomMedia) { + generator?.generateLinks( + clearCache = true, + LoadType.Unknown, + callback = {}, + subtitleCallback = {}) + } else { + activity?.navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + generator ?: return, list + ) ) - ) + } } ACTION_MARK_AS_WATCHED -> { @@ -2052,12 +2128,15 @@ class ResultViewModel2 : ViewModel() { } private fun postSubscription(loadResponse: LoadResponse) { + val id = loadResponse.getId() + val data = getSubscribedData(id) if (loadResponse.isEpisodeBased()) { - val id = loadResponse.getId() - val data = getSubscribedData(id) updateSubscribedData(id, data, loadResponse as? EpisodeResponse) - val isSubscribed = data != null - _subscribeStatus.postValue(isSubscribed) + _subscribeStatus.postValue(data != null) + } + // lets say that we have subscribed, then we must be able to unsubscribe no matter what + else if (data != null) { + _subscribeStatus.postValue(true) } } @@ -2256,7 +2335,8 @@ class ResultViewModel2 : ViewModel() { fillers.getOrDefault(episode, false), loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = i.date ) val season = eps.seasonIndex ?: 0 @@ -2305,7 +2385,8 @@ class ResultViewModel2 : ViewModel() { null, loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = episode.date ) val season = ep.seasonIndex ?: 0 @@ -2337,7 +2418,7 @@ class ResultViewModel2 : ViewModel() { null, loadResponse.type, mainId, - null + null, ) ) } @@ -2591,6 +2672,7 @@ class ResultViewModel2 : ViewModel() { override var posterHeaders: Map? = null, override var backgroundPosterUrl: String? = null, override var contentRating: String? = null, + val id : Int?, ) : LoadResponse fun loadSmall(activity: Activity?, searchResponse : SearchResponse) = ioSafe { @@ -2600,7 +2682,7 @@ class ResultViewModel2 : ViewModel() { val api = APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull(searchResponse.url) ?: APIRepository.noneApi val repo = APIRepository(api) val response = LoadResponseFromSearch(name = searchResponse.name, url = searchResponse.url, apiName = api.name, type = searchResponse.type ?: TvType.Others, - posterUrl = searchResponse.posterUrl).apply { + posterUrl = searchResponse.posterUrl, id = searchResponse.id).apply { if (searchResponse is SyncAPI.LibraryItem) { this.plot = searchResponse.plot this.rating = searchResponse.personalRating?.times(100) ?: searchResponse.rating @@ -2612,12 +2694,14 @@ class ResultViewModel2 : ViewModel() { this.tags = searchResponse.tags } } - val mainId = searchResponse.id ?: response.getId() + val mainId = response.getId() postSuccessful( loadResponse = response, mainId = mainId, - apiRepository = repo, updateEpisodes = false, updateFillers = false) + apiRepository = repo, + updateEpisodes = false, + updateFillers = false) } fun load( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt index 6fe45730..5a23bfc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt @@ -6,7 +6,8 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.databinding.ResultSelectionBinding -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout typealias SelectData = Pair @@ -72,8 +73,7 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter Unit ) { - val isTrueTv = isTrueTvSettings() - if (isTrueTv) { + if (isLayout(TV)) { item.isFocusable = true item.isFocusableInTouchMode = true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index 24d56897..0e8160db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -19,6 +19,13 @@ sealed class UiText { data class DynamicString(val value: String) : UiText() { override fun toString(): String = value + + override fun equals(other: Any?): Boolean { + if (other !is DynamicString) return false + return this.value == other.value + } + + override fun hashCode(): Int = value.hashCode() } class StringResource( @@ -27,6 +34,16 @@ sealed class UiText { ) : UiText() { override fun toString(): String = "resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}" + override fun equals(other: Any?): Boolean { + if (other !is StringResource) return false + return this.resId == other.resId && this.args == other.args + } + + override fun hashCode(): Int { + var result = resId + result = 31 * result + args.hashCode() + return result + } } fun asStringNull(context: Context?): String? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 243d9f4e..24e87d30 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -46,6 +46,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.APIRepository +import com.lagradost.cloudstream3.ui.BaseAdapter import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan @@ -54,8 +55,9 @@ import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.updateChips import com.lagradost.cloudstream3.ui.home.ParentItemAdapter import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.ownHide import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus @@ -107,13 +109,16 @@ class SearchFragment : Fragment() { ) bottomSheetDialog?.ownShow() - val layout = if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search - val root = inflater.inflate(layout, container, false) - // TODO TRYCATCH - binding = FragmentSearchBinding.bind(root) + binding = try { + val layout = if (isLayout(TV or EMULATOR)) R.layout.fragment_search_tv else R.layout.fragment_search + val root = inflater.inflate(layout, container, false) + FragmentSearchBinding.bind(root) + } catch (t : Throwable) { + FragmentSearchBinding.inflate(inflater) + } - return root + return binding?.root } private fun fixGrid() { @@ -157,7 +162,8 @@ class SearchFragment : Fragment() { **/ fun search(query: String?) { if (query == null) return - + // don't resume state from prev search + (binding?.searchMasterRecycler?.adapter as? BaseAdapter<*,*>)?.clear() context?.let { ctx -> val default = enumValues().sorted().filter { it != TvType.NSFW } .map { it.ordinal.toString() }.toSet() @@ -369,7 +375,7 @@ class SearchFragment : Fragment() { selectedSearchTypes = DataStoreHelper.searchPreferenceTags.toMutableList() - if (isTrueTvSettings()) { + if (isLayout(TV)) { binding?.searchFilter?.isFocusable = true binding?.searchFilter?.isFocusableInTouchMode = true } @@ -502,8 +508,8 @@ class SearchFragment : Fragment() { }*/ //main_search.onActionViewExpanded()*/ - val masterAdapter: RecyclerView.Adapter = - ParentItemAdapter(mutableListOf(), { callback -> + val masterAdapter = + ParentItemAdapter(fragment = this, id = "masterAdapter".hashCode(), { callback -> SearchHelper.handleSearchClickCallback(callback) }, { item -> bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt index 3e33e01a..5b943105 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.search -import android.app.Activity import android.widget.Toast import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.showToast @@ -10,7 +9,8 @@ import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_PLAY_FILE import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.ui.result.START_ACTION_LOAD_EP -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper @@ -56,7 +56,7 @@ object SearchHelper { } } SEARCH_ACTION_SHOW_METADATA -> { - if(!isTvSettings()) { // we only want this on phone as UI is not done yet on tv + if(isLayout(PHONE)) { // we only want this on phone as UI is not done yet on tv (activity as? MainActivity?)?.apply { loadPopup(callback.card) } ?: kotlin.run { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt index e1b72b30..d18c0197 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -17,7 +17,8 @@ import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.isMovieType import com.lagradost.cloudstream3.syncproviders.SyncAPI -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual @@ -164,7 +165,7 @@ object SearchResultBuilder { bg.isFocusable = false bg.isFocusableInTouchMode = false - if(!isTrueTvSettings()) { + if(!isLayout(TV)) { bg.setOnClickListener { click(it) } @@ -207,7 +208,7 @@ object SearchResultBuilder { */ - if (isTrueTvSettings()) { + if (isLayout(TV)) { // bg.isFocusable = true // bg.isFocusableInTouchMode = true // bg.touchscreenBlocksFocus = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt new file mode 100644 index 00000000..aa513d87 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt @@ -0,0 +1,56 @@ +package com.lagradost.cloudstream3.ui.settings + +import android.app.UiModeManager +import android.content.Context +import android.content.res.Configuration +import android.os.Build +import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.R + +object Globals { + var beneneCount = 0 + + const val PHONE : Int = 0b001 + const val TV : Int = 0b010 + const val EMULATOR : Int = 0b100 + private const val INVALID = -1 + private var layoutId = INVALID + + private fun Context.getLayoutInt(): Int { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + return settingsManager.getInt(this.getString(R.string.app_layout_key), -1) + } + + private fun Context.isAutoTv(): Boolean { + val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? + // AFT = Fire TV + val model = Build.MODEL.lowercase() + return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains( + "AFT" + ) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast") + } + + private fun Context.layoutIntCorrected(): Int { + return when(getLayoutInt()) { + -1 -> if (isAutoTv()) TV else PHONE + 0 -> PHONE + 1 -> TV + 2 -> EMULATOR + else -> PHONE + } + } + + fun Context.updateTv() { + layoutId = layoutIntCorrected() + } + + /** Returns true if the layout is any of the flags, + * so isLayout(TV or EMULATOR) is a valid statement for checking if the layout is in the emulator + * or tv. Auto will become the "TV" or the "PHONE" layout. + * + * Valid flags are: PHONE, TV, EMULATOR + * */ + fun isLayout(flags: Int) : Boolean { + return (layoutId and flags) != 0 + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 4bc239f6..ba0b806f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -9,6 +9,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager +import androidx.preference.SwitchPreferenceCompat import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent @@ -25,12 +26,17 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.pcloudApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlApi import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppOAuth2API import com.lagradost.cloudstream3.syncproviders.OAuth2API +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -38,13 +44,20 @@ import com.lagradost.cloudstream3.ui.settings.helpers.settings.account.InAppAuth import com.lagradost.cloudstream3.ui.settings.helpers.settings.account.InAppOAuth2DialogBuilder import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.BackupUtils +import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage -class SettingsAccount : PreferenceFragmentCompat() { +class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback { companion object { /** Used by nginx plugin too */ fun showLoginInfo( @@ -78,7 +91,7 @@ class SettingsAccount : PreferenceFragmentCompat() { showAccountSwitch(activity, api) } - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { binding.accountSwitchAccount.requestFocus() } } @@ -135,6 +148,31 @@ class SettingsAccount : PreferenceFragmentCompat() { } } + private fun updateAuthPreference(enabled: Boolean) { + val biometricKey = getString(R.string.biometric_key) + + PreferenceManager.getDefaultSharedPreferences(context ?: return).edit() + .putBoolean(biometricKey, enabled).apply() + findPreference(biometricKey)?.isChecked = enabled + } + + override fun onAuthenticationError() { + updateAuthPreference(!isAuthEnabled(context ?: return)) + } + + override fun onAuthenticationSuccess() { + if (isAuthEnabled(context?: return)) { + updateAuthPreference(true) + BackupUtils.backup(activity) + activity?.showBottomDialogText( + getString(R.string.biometric_setting), + getString(R.string.biometric_warning).html() + ) { onDialogDismissedEvent } + } else { + updateAuthPreference(false) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setUpToolbar(R.string.category_account) @@ -146,22 +184,22 @@ class SettingsAccount : PreferenceFragmentCompat() { hideKeyboard() setPreferencesFromResource(R.xml.settings_account, rootKey) - getPref(R.string.biometric_key)?.setOnPreferenceClickListener { - val authEnabled = PreferenceManager.getDefaultSharedPreferences( - context ?: return@setOnPreferenceClickListener false - ) - .getBoolean(getString(R.string.biometric_key), false) + getPref(R.string.biometric_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener { + val ctx = context ?: return@setOnPreferenceClickListener false - if (authEnabled) { - BackupUtils.backup(activity) - val title = activity?.getString(R.string.biometric_setting) - val warning = activity?.getString(R.string.biometric_warning) - activity?.showBottomDialogText( - title as String, - warning.html() - ) { onDialogDismissedEvent } + if (deviceHasPasswordPinLock(ctx)) { + startBiometricAuthentication( + activity?: return@setOnPreferenceClickListener false, + R.string.biometric_authentication_title, + false + ) + promptInfo?.let { + authCallback = this + biometricPrompt?.authenticate(it) + } } - true + + false } val syncApis = @@ -170,8 +208,9 @@ class SettingsAccount : PreferenceFragmentCompat() { R.string.anilist_key to aniListApi, R.string.simkl_key to simklApi, R.string.opensubtitles_key to openSubtitlesApi, + R.string.subdl_key to subDlApi, R.string.gdrive_key to googleDriveApi, - R.string.pcloud_key to pcloudApi + R.string.pcloud_key to pcloudApi, ) for ((key, api) in syncApis) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index ee66ac24..b4c0a195 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -1,10 +1,7 @@ package com.lagradost.cloudstream3.ui.settings -import android.app.UiModeManager -import android.content.Context -import android.content.res.Configuration -import android.os.Build import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -12,36 +9,43 @@ import android.widget.ImageView import android.widget.Toast import androidx.annotation.StringRes import androidx.core.view.children -import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import androidx.preference.PreferenceManager import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar +import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.BackupApis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.UIHelper +import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import java.io.File +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone class SettingsFragment : Fragment() { companion object { - var beneneCount = 0 - - private var isTv: Boolean = false - private var isTrueTv: Boolean = false fun PreferenceFragmentCompat?.getPref(id: Int): Preference? { if (this == null) return null @@ -54,16 +58,40 @@ class SettingsFragment : Fragment() { } } + /** + * Hide many Preferences on selected layouts. + **/ + fun PreferenceFragmentCompat?.hidePrefs(ids: List, layoutFlags: Int) { + if (this == null) return + + try { + ids.forEach { + getPref(it)?.isVisible = !isLayout(layoutFlags) + } + } catch (e: Exception) { + logError(e) + } + } + + /** + * Hide the Preference on selected layouts. + **/ + fun Preference?.hideOn(layoutFlags: Int): Preference? { + if (this == null) return null + this.isVisible = !isLayout(layoutFlags) + return this + } + /** * On TV you cannot properly scroll to the bottom of settings, this fixes that. * */ fun PreferenceFragmentCompat.setPaddingBottom() { - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { listView?.setPadding(0, 0, 0, 100.toPx) } } fun PreferenceFragmentCompat.setToolBarScrollFlags() { - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { val settingsAppbar = view?.findViewById(R.id.settings_toolbar) settingsAppbar?.updateLayoutParams { @@ -72,7 +100,7 @@ class SettingsFragment : Fragment() { } } fun Fragment?.setToolBarScrollFlags() { - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { val settingsAppbar = this?.view?.findViewById(R.id.settings_toolbar) settingsAppbar?.updateLayoutParams { @@ -87,12 +115,14 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } } - fixPaddingStatusbar(settingsToolbar) + UIHelper.fixPaddingStatusbar(settingsToolbar) } fun Fragment?.setUpToolbar(@StringRes title: Int) { @@ -102,13 +132,15 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } } - fixPaddingStatusbar(settingsToolbar) + UIHelper.fixPaddingStatusbar(settingsToolbar) } fun getFolderSize(dir: File): Long { @@ -124,60 +156,7 @@ class SettingsFragment : Fragment() { return size } - - private fun Context.getLayoutInt(): Int { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - return settingsManager.getInt(this.getString(R.string.app_layout_key), -1) - } - - private fun Context.isTvSettings(): Boolean { - var value = getLayoutInt() - if (value == -1) { - value = if (isAutoTv()) 1 else 0 - } - return value == 1 || value == 2 - } - - private fun Context.isTrueTvSettings(): Boolean { - var value = getLayoutInt() - if (value == -1) { - value = if (isAutoTv()) 1 else 0 - } - return value == 1 - } - - fun Context.updateTv() { - isTrueTv = isTrueTvSettings() - isTv = isTvSettings() - } - - fun isTrueTvSettings(): Boolean { - return isTrueTv - } - - fun isTvSettings(): Boolean { - return isTv - } - - fun Context.isEmulatorSettings(): Boolean { - return getLayoutInt() == 2 - } - - // phone exclusive - fun isTruePhone(): Boolean { - return !isTrueTvSettings() && !isTvSettings() && context?.isEmulatorSettings() != true - } - - private fun Context.isAutoTv(): Boolean { - val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? - // AFT = Fire TV - val model = Build.MODEL.lowercase() - return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains( - "AFT" - ) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast") - } } - override fun onDestroyView() { binding = null super.onDestroyView() @@ -192,7 +171,6 @@ class SettingsFragment : Fragment() { val localBinding = MainSettingsBinding.inflate(inflater, container, false) binding = localBinding return localBinding.root - //return inflater.inflate(R.layout.main_settings, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -200,23 +178,44 @@ class SettingsFragment : Fragment() { activity?.navigate(id, Bundle()) } - // used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}") + /** used to debug leaks + showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : + ${VideoDownloadManager.downloadProgressEvent.size}") **/ - val isTrueTv = isTrueTvSettings() + fun hasProfilePictureFromAccountManagers(accountManagers: List): Boolean { + for (syncApi in accountManagers) { + val login = syncApi.loginInfo() + val pic = login?.profilePicture ?: continue - for (syncApi in accountManagers) { - val login = syncApi.loginInfo() - val pic = login?.profilePicture ?: continue - if (binding?.settingsProfilePic?.setImage( - pic, - errorImageDrawable = HomeFragment.errorProfilePic - ) == true - ) { - binding?.settingsProfileText?.text = login.name - binding?.settingsProfile?.isVisible = true - break + if (binding?.settingsProfilePic?.setImage( + pic, + errorImageDrawable = HomeFragment.errorProfilePic + ) == true + ) { + binding?.settingsProfileText?.text = login.name + return true // sync profile exists + } } + return false // not syncing } + + // display local account information if not syncing + if (!hasProfilePictureFromAccountManagers(accountManagers)) { + val activity = activity ?: return + val currentAccount = try { + DataStoreHelper.accounts.firstOrNull { + it.keyIndex == DataStoreHelper.selectedKeyIndex + } ?: activity.let { DataStoreHelper.getDefaultAccount(activity) } + + } catch (t: IllegalStateException) { + Log.e("AccountManager", "Activity not found", t) + null + } + + binding?.settingsProfilePic?.setImage(currentAccount?.image) + binding?.settingsProfileText?.text = currentAccount?.name + } + binding?.apply { listOf( settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general, @@ -231,7 +230,7 @@ class SettingsFragment : Fragment() { setOnClickListener { navigate(navigationId) } - if (isTrueTv) { + if (isLayout(TV)) { isFocusable = true isFocusableInTouchMode = true } @@ -255,9 +254,22 @@ class SettingsFragment : Fragment() { } // Default focus on TV - if (isTrueTv) { + if (isLayout(TV)) { settingsGeneral.requestFocus() } } + + val appVersion = getString(R.string.app_version) + val commitInfo = getString(R.string.commit_hash) + val buildTimestamp = SimpleDateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, + Locale.getDefault() + ).apply { timeZone = TimeZone.getTimeZone("UTC") + }.format(Date(BuildConfig.BUILD_DATE)).replace("UTC", "") + + binding?.buildDate?.text = buildTimestamp + binding?.appVersionInfo?.setOnLongClickListener { + clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo $buildTimestamp") + true + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index 7e558db9..8eb95e7c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -28,10 +28,18 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener import com.lagradost.cloudstream3.ui.EasterEggMonke +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.isAppRestricted +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.showBatteryOptimizationDialog import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog @@ -46,7 +54,6 @@ import com.lagradost.safefile.SafeFile // Change local language settings in the app. fun getCurrentLocale(context: Context): String { - // val dm = res.displayMetrics val res = context.resources val conf = res.configuration @@ -96,6 +103,7 @@ val appLanguages = arrayListOf( Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), Triple("", "bahasa Melayu", "ms"), + Triple("", "Malti", "mt"), Triple("", "ဗမာစာ", "my"), Triple("", "नेपाली", "ne"), Triple("", "Nederlands", "nl"), @@ -212,6 +220,18 @@ class SettingsGeneral : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + getPref(R.string.battery_optimisation_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener { + val ctx = context ?: return@setOnPreferenceClickListener false + + if (isAppRestricted(ctx)) { + showBatteryOptimizationDialog(ctx) + } else { + showToast(R.string.app_unrestricted_toast) + } + + true + } + fun showAdd() { val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } } activity?.showDialog( @@ -387,30 +407,30 @@ class SettingsGeneral : PreferenceFragmentCompat() { } try { - SettingsFragment.beneneCount = + beneneCount = settingsManager.getInt(getString(R.string.benene_count), 0) getPref(R.string.benene_count)?.let { pref -> pref.summary = - if (SettingsFragment.beneneCount <= 0) getString(R.string.benene_count_text_none) else getString( + if (beneneCount <= 0) getString(R.string.benene_count_text_none) else getString( R.string.benene_count_text ).format( - SettingsFragment.beneneCount + beneneCount ) pref.setOnPreferenceClickListener { try { - SettingsFragment.beneneCount++ - if (SettingsFragment.beneneCount%20 == 0) { + beneneCount++ + if (beneneCount%20 == 0) { val intent = Intent(context, EasterEggMonke::class.java) startActivity(intent) } settingsManager.edit().putInt( getString(R.string.benene_count), - SettingsFragment.beneneCount + beneneCount ) .apply() it.summary = - getString(R.string.benene_count_text).format(SettingsFragment.beneneCount) + getString(R.string.benene_count_text).format(beneneCount) } catch (e: Exception) { logError(e) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index d8346ade..a8466c03 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -8,8 +8,14 @@ import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hidePrefs import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -34,6 +40,18 @@ class SettingsPlayer : PreferenceFragmentCompat() { val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) .attachBackupListener(requireContext().getSyncPrefs()).self + //Hide specific prefs on TV/EMULATOR + hidePrefs( + listOf( + R.string.pref_category_gestures_key, + R.string.rotate_video_key, + R.string.auto_rotate_video_key + ), + TV or EMULATOR + ) + + getPref(R.string.pref_category_android_tv_key)?.hideOn(PHONE) + getPref(R.string.video_buffer_length_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.video_buffer_length_names) val prefValues = resources.getIntArray(R.array.video_buffer_length_values) @@ -230,6 +248,5 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } } - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt index 7058c5be..744544dc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt @@ -10,11 +10,11 @@ import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener import com.lagradost.cloudstream3.ui.search.SearchResultBuilder +import com.lagradost.cloudstream3.ui.settings.Globals.updateTv import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index f05f06e1..2c609624 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -1,10 +1,6 @@ package com.lagradost.cloudstream3.ui.settings -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context import android.os.Bundle -import android.os.TransactionTooLargeException import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -21,6 +17,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener import com.lagradost.cloudstream3.services.BackupWorkManager +import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags @@ -32,6 +29,7 @@ import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog +import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.VideoDownloadManager @@ -39,6 +37,9 @@ import okhttp3.internal.closeQuietly import java.io.BufferedReader import java.io.InputStreamReader import java.io.OutputStream +import java.lang.System.currentTimeMillis +import java.text.SimpleDateFormat +import java.util.* class SettingsUpdates : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -119,29 +120,22 @@ class SettingsUpdates : PreferenceFragmentCompat() { binding.text1.text = text binding.copyBtt.setOnClickListener { - // Can crash on too much text - try { - val serviceClipboard = - (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?) - ?: return@setOnClickListener - val clip = ClipData.newPlainText("logcat", text) - serviceClipboard.setPrimaryClip(clip) - dialog.dismissSafe(activity) - } catch (e: TransactionTooLargeException) { - showToast(R.string.clipboard_too_large) - } + clipboardHelper(txt("Logcat"), text) + dialog.dismissSafe(activity) } + binding.clearBtt.setOnClickListener { Runtime.getRuntime().exec("logcat -c") dialog.dismissSafe(activity) } + binding.saveBtt.setOnClickListener { + val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) var fileStream: OutputStream? = null try { - fileStream = - VideoDownloadManager.setupStream( + fileStream = VideoDownloadManager.setupStream( it.context, - "logcat", + "logcat_${date}", null, "txt", false @@ -155,9 +149,11 @@ class SettingsUpdates : PreferenceFragmentCompat() { fileStream?.closeQuietly() } } + binding.closeBtt.setOnClickListener { dialog.dismissSafe(activity) } + return@setOnPreferenceClickListener true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index f0b8a0bd..ebd3260f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -29,7 +29,8 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.result.setText -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.utils.AppUtils.downloadAllPluginsDialog @@ -97,7 +98,7 @@ class ExtensionsFragment : Fragment() { nextLeft = R.id.nav_rail_view ) - if (!isTrueTvSettings()) + if (!isLayout(TV)) binding?.addRepoButton?.let { button -> button.post { setPadding( @@ -286,7 +287,7 @@ class ExtensionsFragment : Fragment() { } } - val isTv = isTrueTvSettings() + val isTv = isLayout(TV) binding?.apply { addRepoButton.isGone = isTv addRepoButtonImageviewHolder.isVisible = isTv diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index c3fb4fc2..04da30c7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -17,7 +17,8 @@ import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.VotingApi.getVotes import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -44,7 +45,7 @@ class PluginAdapter( private val plugins: MutableList = mutableListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val layout = if(isTrueTvSettings()) R.layout.repository_item_tv else R.layout.repository_item + val layout = if(isLayout(TV)) R.layout.repository_item_tv else R.layout.repository_item val inflated = LayoutInflater.from(parent.context).inflate(layout, parent, false) return PluginViewHolder( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index c5256ffa..acfbc584 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -17,7 +17,9 @@ import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.ui.settings.appLanguages @@ -155,7 +157,7 @@ class PluginsFragment : Fragment() { pluginViewModel.handlePluginAction(activity, url, it, isLocal) } - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { // Scrolling down does not reveal the whole RecyclerView on TV, add to bypass that. binding?.pluginRecyclerView?.setPadding(0, 0, 0, 200.toPx) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt index 151c8d57..2b026e0d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.amap @@ -181,8 +182,11 @@ class PluginsViewModel : ViewModel() { } private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) { + val isAdult = settingsForProvider.enableAdult val plugins = getPlugins(repositoryUrl) - val list = plugins.map { plugin -> + val list = plugins.filter { + return@filter !(it.second.tvTypes?.contains("NSFW") == true && !isAdult) + }.map { plugin -> PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first)) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt index 7ac7cbb2..faf6d38b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt @@ -1,22 +1,18 @@ package com.lagradost.cloudstream3.ui.settings.extensions -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.os.Build import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.Toast import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding -import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper class RepoAdapter( val isSetup: Boolean, @@ -28,7 +24,7 @@ class RepoAdapter( private val repositories: MutableList = mutableListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val layout = if (isTrueTvSettings()) RepositoryItemTvBinding.inflate( + val layout = if (isLayout(TV)) RepositoryItemTvBinding.inflate( LayoutInflater.from(parent.context), parent, false @@ -121,13 +117,9 @@ class RepoAdapter( } repositoryItemRoot.setOnLongClickListener { - val clipboardManager = - activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager? - clipboardManager?.setPrimaryClip(ClipData.newPlainText("RepoUrl", repositoryData.url)) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - showToast(R.string.copyRepoUrl, Toast.LENGTH_SHORT) - } - return@setOnLongClickListener true + val shareableRepoData = "${repositoryData.name} : \n ${repositoryData.url}" + clipboardHelper(txt(R.string.repo_copy_label), shareableRepoData) + true } mainText.text = repositoryData.name diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/helpers/settings/account/DialogBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/helpers/settings/account/DialogBuilder.kt index 0686cdc3..747e579b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/helpers/settings/account/DialogBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/helpers/settings/account/DialogBuilder.kt @@ -9,7 +9,9 @@ import androidx.fragment.app.FragmentActivity import androidx.viewbinding.ViewBinding import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.syncproviders.AuthAPI -import com.lagradost.cloudstream3.ui.settings.SettingsFragment +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe abstract class DialogBuilder( @@ -82,7 +84,7 @@ abstract class DialogBuilder( private fun setItemVisibility(dialog: AlertDialog) { val visibilityMap = getVisibilityMap(dialog) - if (SettingsFragment.isTvSettings()) { + if (isLayout(TV or EMULATOR)) { visibilityMap.forEach { (input, isVisible) -> input.isVisible = isVisible @@ -134,4 +136,4 @@ abstract class DialogBuilder( } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt index 3fbd1131..7878afaa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt @@ -11,7 +11,8 @@ import com.lagradost.cloudstream3.databinding.FragmentTestingBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -62,7 +63,7 @@ class TestFragment : Fragment() { } } - if (isTrueTvSettings()) { + if (isLayout(TV)) { providerTest.playPauseButton?.isFocusableInTouchMode = true providerTest.playPauseButton?.requestFocus() } @@ -75,7 +76,7 @@ class TestFragment : Fragment() { fun focusRecyclerView() { // Hack to make it possible to focus the recyclerview. - if (isTrueTvSettings()) { + if (isLayout(TV)) { providerTestRecyclerView.requestFocus() providerTestAppbar.setExpanded(false, true) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt index e6039105..6f2e3c66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt @@ -9,6 +9,7 @@ import android.widget.ArrayAdapter import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentSetupLayoutBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall @@ -89,6 +90,7 @@ class SetupFragmentLayout : Fragment() { nextBtt.setOnClickListener { + setKey(HAS_DONE_SETUP_KEY, true) findNavController().navigate(R.id.navigation_home) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index 71fac2ed..bb9558b8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -13,8 +13,8 @@ import android.view.ViewGroup import android.widget.TextView import android.widget.Toast import androidx.fragment.app.Fragment -import com.fasterxml.jackson.annotation.JsonProperty import androidx.media3.common.text.Cue +import com.fasterxml.jackson.annotation.JsonProperty import com.google.android.gms.cast.TextTrackStyle import com.google.android.gms.cast.TextTrackStyle.* import com.jaredrummler.android.colorpicker.ColorPickerDialog @@ -24,7 +24,9 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ChromecastSubtitleSettingsBinding -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog @@ -173,7 +175,7 @@ class ChromecastSubtitlesFragment : Fragment() { state = getCurrentSavedStyle() context?.updateState() - val isTvSettings = isTvSettings() + val isTvSettings = isLayout(TV or EMULATOR) fun View.setFocusableInTv() { this.isFocusableInTouchMode = isTvSettings diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index fa9191da..5aeb849d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -28,8 +28,10 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.Event @@ -254,7 +256,7 @@ class SubtitlesFragment : Fragment() { state = getCurrentSavedStyle() context?.updateState() - val isTvTrueSettings = isTrueTvSettings() + val isTvTrueSettings = isLayout(TV) fun View.setFocusableInTv() { this.isFocusableInTouchMode = isTvTrueSettings diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index 1be966b6..ff27b192 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -61,8 +61,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStri import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WebviewFragment import com.lagradost.cloudstream3.ui.result.ResultFragment -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel.Companion.downloadAll import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -78,7 +77,6 @@ import okhttp3.Cache import java.io.* import java.net.URL import java.net.URLDecoder -import kotlin.system.measureTimeMillis object AppUtils { fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) { @@ -583,7 +581,7 @@ object AppUtils { //private val viewModel: ResultViewModel by activityViewModels() private fun getResultsId(): Int { - return if (isTvSettings()) { + return if (Globals.isLayout(Globals.TV or Globals.EMULATOR)) { R.id.global_to_navigation_results_tv } else { R.id.global_to_navigation_results_phone @@ -707,7 +705,7 @@ object AppUtils { * Sets the focus to the negative button when in TV and Emulator layout. **/ fun AlertDialog.setDefaultFocus(buttonFocus: Int = DialogInterface.BUTTON_NEGATIVE) { - if (!isTvSettings()) return + if (!Globals.isLayout(Globals.TV or Globals.EMULATOR)) return this.getButton(buttonFocus).run { isFocusableInTouchMode = true requestFocus() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 15dab1cc..01b16f5f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -31,6 +31,7 @@ import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_T import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi.Companion.OPEN_SUBTITLES_USER_KEY +import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi.Companion.SUBDL_SUBTITLES_USER_KEY import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -48,7 +49,7 @@ import java.io.OutputStream import java.io.PrintWriter import java.lang.System.currentTimeMillis import java.text.SimpleDateFormat -import java.util.* +import java.util.Date object BackupUtils { enum class RestoreSource { @@ -80,10 +81,13 @@ object BackupUtils { PLUGINS_KEY_LOCAL, OPEN_SUBTITLES_USER_KEY, + SUBDL_SUBTITLES_USER_KEY, + DOWNLOAD_EPISODE_CACHE, "biometric_key", // can lock down users if backup is shared on a incompatible device - "nginx_user" // Nginx user key + "nginx_user", // Nginx user key + "download_path_key" // No access rights after restore data from backup ) /** false if key should not be contained in backup */ @@ -378,4 +382,4 @@ object BackupUtils { private fun String.withoutPrefix(restoreSource: BackupUtils.RestoreSource) = // will not remove sync prefix because it wont match (its not a bug its a feature ¯\_(ツ)_/¯ ) - removePrefix(restoreSource.prefix) \ No newline at end of file + removePrefix(restoreSource.prefix) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt index de9b9963..c57600ee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt @@ -12,20 +12,20 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.getString import androidx.fragment.app.FragmentActivity +import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R object BiometricAuthenticator { + const val TAG = "cs3Auth" private const val MAX_FAILED_ATTEMPTS = 3 private var failedAttempts = 0 - const val TAG = "cs3Auth" - private var biometricManager: BiometricManager? = null var biometricPrompt: BiometricPrompt? = null var promptInfo: BiometricPrompt.PromptInfo? = null - var authCallback: BiometricAuthCallback? = null // listen to authentication success private fun initializeBiometrics(activity: Activity) { @@ -37,20 +37,12 @@ object BiometricAuthenticator { activity as FragmentActivity, executor, object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) showToast("$errString") Log.e(TAG, "$errorCode") - failedAttempts++ - - if (failedAttempts >= MAX_FAILED_ATTEMPTS) { - failedAttempts = 0 - activity.finish() - } else { - failedAttempts = 0 - activity.finish() - } + authCallback?.onAuthenticationError() + //activity.finish() } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { @@ -89,7 +81,6 @@ object BiometricAuthenticator { .setDescription(description) .setAllowedAuthenticators(authFlag) .build() - } else { // for apis < 30 promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -98,7 +89,6 @@ object BiometricAuthenticator { .setDeviceCredentialAllowed(true) .build() } - } else { // fallback for A12+ when both fingerprint & Face unlock is absent but PIN is set promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -114,7 +104,6 @@ object BiometricAuthenticator { var result = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - when (biometricManager?.canAuthenticate( DEVICE_CREDENTIAL or BIOMETRIC_STRONG or BIOMETRIC_WEAK )) { @@ -126,7 +115,6 @@ object BiometricAuthenticator { BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> result = true BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> result = false } - } else { @Suppress("DEPRECATION") when (biometricManager?.canAuthenticate()) { @@ -153,12 +141,11 @@ object BiometricAuthenticator { // function to start authentication in any fragment or activity fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) { initializeBiometrics(activity) - + authCallback = activity as? BiometricAuthCallback if (isBiometricHardWareAvailable()) { authCallback = activity as? BiometricAuthCallback authenticationDialog(activity, title, setDeviceCred) promptInfo?.let { biometricPrompt?.authenticate(it) } - } else { if (deviceHasPasswordPinLock(activity)) { authCallback = activity as? BiometricAuthCallback @@ -171,7 +158,15 @@ object BiometricAuthenticator { } } + fun isAuthEnabled(ctx: Context):Boolean { + return ctx.let { + PreferenceManager.getDefaultSharedPreferences(ctx) + .getBoolean(getString(ctx, R.string.biometric_key), false) + } + } + interface BiometricAuthCallback { fun onAuthenticationSuccess() + fun onAuthenticationError() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 637f65b9..c6cad804 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -53,6 +53,7 @@ import com.lagradost.cloudstream3.extractors.FileMoonIn import com.lagradost.cloudstream3.extractors.FileMoonSx import com.lagradost.cloudstream3.extractors.Filesim import com.lagradost.cloudstream3.extractors.Fplayer +import com.lagradost.cloudstream3.extractors.Geodailymotion import com.lagradost.cloudstream3.extractors.GMPlayer import com.lagradost.cloudstream3.extractors.Gdriveplayer import com.lagradost.cloudstream3.extractors.Gdriveplayerapi @@ -83,6 +84,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream import com.lagradost.cloudstream3.extractors.Mcloud import com.lagradost.cloudstream3.extractors.Megacloud import com.lagradost.cloudstream3.extractors.Meownime +import com.lagradost.cloudstream3.extractors.MetaGnathTuggers import com.lagradost.cloudstream3.extractors.Minoplres import com.lagradost.cloudstream3.extractors.MixDrop import com.lagradost.cloudstream3.extractors.MixDropBz @@ -108,6 +110,7 @@ import com.lagradost.cloudstream3.extractors.Hotlinger import com.lagradost.cloudstream3.extractors.FourCX import com.lagradost.cloudstream3.extractors.PlayRu import com.lagradost.cloudstream3.extractors.FourPlayRu +import com.lagradost.cloudstream3.extractors.FourPichive import com.lagradost.cloudstream3.extractors.HDMomPlayer import com.lagradost.cloudstream3.extractors.HDPlayerSystem import com.lagradost.cloudstream3.extractors.VideoSeyred @@ -139,6 +142,7 @@ import com.lagradost.cloudstream3.extractors.Sbspeed import com.lagradost.cloudstream3.extractors.Sbthe import com.lagradost.cloudstream3.extractors.Sendvid import com.lagradost.cloudstream3.extractors.ShaveTape +import com.lagradost.cloudstream3.extractors.Simpulumlamerop import com.lagradost.cloudstream3.extractors.Solidfiles import com.lagradost.cloudstream3.extractors.Ssbstream import com.lagradost.cloudstream3.extractors.StreamM4u @@ -175,6 +179,7 @@ import com.lagradost.cloudstream3.extractors.UpstreamExtractor import com.lagradost.cloudstream3.extractors.Uqload import com.lagradost.cloudstream3.extractors.Uqload1 import com.lagradost.cloudstream3.extractors.Uqload2 +import com.lagradost.cloudstream3.extractors.Urochsunloath import com.lagradost.cloudstream3.extractors.Userload import com.lagradost.cloudstream3.extractors.Userscloud import com.lagradost.cloudstream3.extractors.Uservideo @@ -182,10 +187,12 @@ import com.lagradost.cloudstream3.extractors.Vanfem import com.lagradost.cloudstream3.extractors.Vicloud import com.lagradost.cloudstream3.extractors.VidSrcExtractor import com.lagradost.cloudstream3.extractors.VidSrcExtractor2 +import com.lagradost.cloudstream3.extractors.VidSrcTo import com.lagradost.cloudstream3.extractors.VideoVard import com.lagradost.cloudstream3.extractors.VideovardSX import com.lagradost.cloudstream3.extractors.Vidgomunime import com.lagradost.cloudstream3.extractors.Vidgomunimesb +import com.lagradost.cloudstream3.extractors.Vidguardto import com.lagradost.cloudstream3.extractors.VidhideExtractor import com.lagradost.cloudstream3.extractors.Vidmoly import com.lagradost.cloudstream3.extractors.Vidmolyme @@ -207,6 +214,7 @@ import com.lagradost.cloudstream3.extractors.Watchx import com.lagradost.cloudstream3.extractors.WcoStream import com.lagradost.cloudstream3.extractors.Wibufile import com.lagradost.cloudstream3.extractors.XStreamCdn +import com.lagradost.cloudstream3.extractors.Yipsu import com.lagradost.cloudstream3.extractors.YourUpload import com.lagradost.cloudstream3.extractors.YoutubeExtractor import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor @@ -217,6 +225,8 @@ import com.lagradost.cloudstream3.extractors.Zorofile import com.lagradost.cloudstream3.extractors.Zplayer import com.lagradost.cloudstream3.extractors.ZplayerV2 import com.lagradost.cloudstream3.extractors.Ztreamhub +import com.lagradost.cloudstream3.extractors.EPlayExtractor +import com.lagradost.cloudstream3.extractors.Vtbe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay @@ -299,7 +309,18 @@ enum class ExtractorLinkType { /** No support at the moment */ TORRENT, /** No support at the moment */ - MAGNET, + MAGNET; + + // See https://www.iana.org/assignments/media-types/media-types.xhtml + fun getMimeType(): String { + return when (this) { + VIDEO -> "video/mp4" + M3U8 -> "application/x-mpegURL" + DASH -> "application/dash+xml" + TORRENT -> "application/x-bittorrent" + MAGNET -> "application/x-bittorrent" + } + } } private fun inferTypeFromUrl(url: String): ExtractorLinkType { @@ -402,9 +423,29 @@ open class ExtractorLink constructor( open val extractorData: String? = null, open val type: ExtractorLinkType, ) : VideoDownloadManager.IDownloadableMinimum { - val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 - val isDash : Boolean get() = type == ExtractorLinkType.DASH - + val isM3u8: Boolean get() = type == ExtractorLinkType.M3U8 + val isDash: Boolean get() = type == ExtractorLinkType.DASH + + // Cached video size + private var videoSize: Long? = null + + /** + * Get video size in bytes with one head request. Only available for ExtractorLinkType.Video + * @param timeoutSeconds timeout of the head request. + */ + suspend fun getVideoSize(timeoutSeconds: Long = 3L): Long? { + // Content-Length is not applicable to other types of formats + if (this.type != ExtractorLinkType.VIDEO) return null + + videoSize = videoSize ?: runCatching { + val response = + app.head(this.url, headers = headers, referer = referer, timeout = timeoutSeconds) + response.headers["Content-Length"]?.toLong() + }.getOrNull() + + return videoSize + } + @JsonIgnore fun getAllHeaders() : Map { if (referer.isBlank()) { @@ -708,6 +749,7 @@ val extractorApis: MutableList = arrayListOf( FourCX(), PlayRu(), FourPlayRu(), + FourPichive(), HDMomPlayer(), HDPlayerSystem(), VideoSeyred(), @@ -849,6 +891,7 @@ val extractorApis: MutableList = arrayListOf( Streamlare(), VidSrcExtractor(), VidSrcExtractor2(), + VidSrcTo(), PlayLtXyz(), AStreamHub(), Vidplay(), @@ -864,7 +907,16 @@ val extractorApis: MutableList = arrayListOf( Megacloud(), VidhideExtractor(), StreamWishExtractor(), - EmturbovidExtractor() + EmturbovidExtractor(), + Vtbe(), + EPlayExtractor(), + Vidguardto(), + Simpulumlamerop(), + Urochsunloath(), + Yipsu(), + MetaGnathTuggers(), + Geodailymotion(), + ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt new file mode 100644 index 00000000..27609730 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt @@ -0,0 +1,86 @@ +package com.lagradost.cloudstream3.utils + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.PowerManager +import android.provider.Settings +import android.util.Log +import androidx.appcompat.app.AlertDialog +import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout + +const val packageName = BuildConfig.APPLICATION_ID +const val TAG = "PowerManagerAPI" + +object BatteryOptimizationChecker { + + fun isAppRestricted(context: Context?): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context != null) { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + return !powerManager.isIgnoringBatteryOptimizations(context.packageName) + } + + return false // below Marshmallow, it's always unrestricted when app is in background + } + + fun openBatteryOptimizationSettings(context: Context) { + if (shouldShowBatteryOptimizationDialog(context)) { + showBatteryOptimizationDialog(context) + } + } + + fun showBatteryOptimizationDialog(context: Context) { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) + + try { + context.let { + AlertDialog.Builder(it) + .setTitle(R.string.battery_dialog_title) + .setIcon(R.drawable.ic_battery) + .setMessage(R.string.battery_dialog_message) + .setPositiveButton(R.string.ok) { _, _ -> + intentOpenAppInfo(it) + } + .setNegativeButton(R.string.cancel) { _, _ -> + settingsManager.edit() + .putBoolean(context.getString(R.string.battery_optimisation_key), false) + .apply() + } + .show() + } + } catch (t: Throwable) { + Log.e(TAG, "Error showing battery optimization dialog", t) + } + } + + private fun shouldShowBatteryOptimizationDialog(context: Context): Boolean { + val isRestricted = isAppRestricted(context) + val isOptimizedNotShown = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.battery_optimisation_key), true) + return isRestricted && isOptimizedNotShown && isLayout(PHONE) + } + + private fun intentOpenAppInfo(context: Context) { + val intent = Intent() + try { + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", packageName, null)) + context.startActivity(intent, Bundle()) + } catch (t: Throwable) { + Log.e(TAG, "Unable to invoke any intent", t) + if (t is ActivityNotFoundException) { + showToast("Exception: Activity Not Found") + } else { + showToast(R.string.app_info_intent_error) + } + } + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt index f34e7238..70edf80c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt @@ -21,7 +21,9 @@ import com.lagradost.cloudstream3.databinding.BottomInputDialogBinding import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding import com.lagradost.cloudstream3.databinding.BottomTextDialogBinding import com.lagradost.cloudstream3.databinding.OptionsPopupTvBinding -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -54,7 +56,7 @@ object SingleSelectionHelper { ) { if (this == null) return - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { val binding = OptionsPopupTvBinding.inflate(layoutInflater) val dialog = AlertDialog.Builder(this, R.style.AlertDialogCustom) .setView(binding.root) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index 6e925d15..cb527020 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -5,6 +5,8 @@ import android.annotation.SuppressLint import android.app.Activity import android.app.AppOpsManager import android.app.Dialog +import android.content.ClipData +import android.content.ClipboardManager import android.content.Context import android.content.pm.PackageManager import android.content.res.Configuration @@ -14,12 +16,15 @@ import android.graphics.Color import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle +import android.os.TransactionTooLargeException +import android.util.Log import android.view.* import android.view.ViewGroup.MarginLayoutParams import android.view.inputmethod.InputMethodManager import android.widget.ImageView import android.widget.ListAdapter import android.widget.ListView +import android.widget.Toast.LENGTH_LONG import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.DrawableRes @@ -30,18 +35,17 @@ import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.widget.PopupMenu import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import androidx.core.graphics.alpha import androidx.core.graphics.blue import androidx.core.graphics.drawable.toBitmapOrNull import androidx.core.graphics.green import androidx.core.graphics.red -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.marginBottom import androidx.core.view.marginLeft import androidx.core.view.marginRight import androidx.core.view.marginTop +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.NavHostFragment @@ -55,20 +59,24 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions.bitmapTransform import com.bumptech.glide.request.target.Target +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup +import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.result.UiImage -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.result.UiText +import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import jp.wasabeef.glide.transformations.BlurTransformation import kotlin.math.roundToInt - object UIHelper { val Int.toPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt() val Float.toPx: Float get() = (this * Resources.getSystem().displayMetrics.density) @@ -123,6 +131,35 @@ object UIHelper { ) } + fun clipboardHelper(label: UiText, text: CharSequence) { + val ctx = context ?: return + try { + ctx.let { + val clip = ClipData.newPlainText(label.asString(ctx), text) + val labelSuffix = txt(R.string.toast_copied).asString(ctx) + ctx.getSystemService()?.setPrimaryClip(clip) + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + showToast("${label.asString(ctx)} $labelSuffix") + } + } + } catch (t: Throwable) { + Log.e("ClipboardService", "$t") + when (t) { + is SecurityException -> { + showToast(R.string.clipboard_permission_error) + } + + is TransactionTooLargeException -> { + showToast(R.string.clipboard_too_large) + } + + else -> { + showToast(R.string.clipboard_unknown_error, LENGTH_LONG) + } + } + } + } /** * Sets ListView height dynamically based on the height of the items. @@ -173,6 +210,14 @@ object UIHelper { } } + fun View?.setAppBarNoScrollFlagsOnTV() { + if (isLayout(Globals.TV or EMULATOR)) { + this?.updateLayoutParams { + scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL + } + } + } + fun Activity.hideKeyboard() { window?.decorView?.clearFocus() this.findViewById(android.R.id.content)?.rootView?.let { @@ -434,7 +479,7 @@ object UIHelper { } fun Context.getStatusBarHeight(): Int { - if (isTvSettings()) { + if (isLayout(Globals.TV or EMULATOR)) { return 0 } @@ -536,7 +581,7 @@ object UIHelper { (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) //} - changeStatusBarState(isEmulatorSettings()) + changeStatusBarState(isLayout(EMULATOR)) } fun Context.shouldShowPIPMode(isInPlayer: Boolean): Boolean { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 50a8df02..7d4d5d98 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -187,7 +187,7 @@ object VideoDownloadManager { private val DOWNLOAD_BAD_CONFIG = DownloadStatus(retrySame = false, tryNext = false, success = false) - private const val KEY_RESUME_PACKAGES = "download_resume" + const val KEY_RESUME_PACKAGES = "download_resume" const val KEY_DOWNLOAD_INFO = "download_info" private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume" diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt new file mode 100644 index 00000000..9ff5cc08 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt @@ -0,0 +1,135 @@ +package com.lagradost.cloudstream3.utils.fcast + +import android.content.Context +import android.net.nsd.NsdManager +import android.net.nsd.NsdManager.ResolveListener +import android.net.nsd.NsdServiceInfo +import android.os.Build +import android.util.Log +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe + +class FcastManager { + private var nsdManager: NsdManager? = null + + // Used for receiver + private val registrationListenerTcp = DefaultRegistrationListener() + private fun getDeviceName(): String { + return "${Build.MANUFACTURER}-${Build.MODEL}" + } + + /** + * Start the fcast service + * @param registerReceiver If true will register the app as a compatible fcast receiver for discovery in other app + */ + fun init(context: Context, registerReceiver: Boolean) = ioSafe { + nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager + val serviceType = "_fcast._tcp" + + if (registerReceiver) { + val serviceName = "$APP_PREFIX-${getDeviceName()}" + + val serviceInfo = NsdServiceInfo().apply { + this.serviceName = serviceName + this.serviceType = serviceType + this.port = TCP_PORT + } + + nsdManager?.registerService( + serviceInfo, + NsdManager.PROTOCOL_DNS_SD, + registrationListenerTcp + ) + } + + nsdManager?.discoverServices( + serviceType, + NsdManager.PROTOCOL_DNS_SD, + DefaultDiscoveryListener() + ) + } + + fun stop() { + nsdManager?.unregisterService(registrationListenerTcp) + } + + inner class DefaultDiscoveryListener : NsdManager.DiscoveryListener { + val tag = "DiscoveryListener" + override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) { + Log.d(tag, "Discovery failed: $serviceType, error code: $errorCode") + } + + override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) { + Log.d(tag, "Stop discovery failed: $serviceType, error code: $errorCode") + } + + override fun onDiscoveryStarted(serviceType: String?) { + Log.d(tag, "Discovery started: $serviceType") + } + + override fun onDiscoveryStopped(serviceType: String?) { + Log.d(tag, "Discovery stopped: $serviceType") + } + + override fun onServiceFound(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + nsdManager?.resolveService(serviceInfo, object : ResolveListener { + override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) { + } + + override fun onServiceResolved(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + + currentDevices.add(PublicDeviceInfo(serviceInfo)) + + Log.d( + tag, + "Service found: ${serviceInfo.serviceName}, Net: ${serviceInfo.host.hostAddress}" + ) + } + }) + } + + override fun onServiceLost(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + + // May remove duplicates, but net and port is null here, preventing device specific identification + currentDevices.removeAll { + it.rawName == serviceInfo.serviceName + } + + Log.d(tag, "Service lost: ${serviceInfo.serviceName}") + } + } + + companion object { + const val APP_PREFIX = "CloudStream" + val currentDevices: MutableList = mutableListOf() + + class DefaultRegistrationListener : NsdManager.RegistrationListener { + val tag = "DiscoveryService" + override fun onServiceRegistered(serviceInfo: NsdServiceInfo) { + Log.d(tag, "Service registered: ${serviceInfo.serviceName}") + } + + override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.e(tag, "Service registration failed: errorCode=$errorCode") + } + + override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) { + Log.d(tag, "Service unregistered: ${serviceInfo.serviceName}") + } + + override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.e(tag, "Service unregistration failed: errorCode=$errorCode") + } + } + + const val TCP_PORT = 46899 + } +} + +class PublicDeviceInfo(serviceInfo: NsdServiceInfo) { + val rawName: String = serviceInfo.serviceName + val host: String? = serviceInfo.host.hostAddress + val name = rawName.replace("-", " ") + host?.let { " $it" } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt new file mode 100644 index 00000000..1f33bca4 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt @@ -0,0 +1,60 @@ +package com.lagradost.cloudstream3.utils.fcast + +import android.util.Log +import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.safefile.closeQuietly +import java.io.DataOutputStream +import java.net.Socket +import kotlin.jvm.Throws + +class FcastSession(private val hostAddress: String): AutoCloseable { + val tag = "FcastSession" + + private var socket: Socket? = null + @Throws + @WorkerThread + fun open(): Socket { + val socket = Socket(hostAddress, FcastManager.TCP_PORT) + this.socket = socket + return socket + } + + override fun close() { + socket?.closeQuietly() + socket = null + } + + @Throws + private fun acquireSocket(): Socket { + return socket ?: open() + } + + fun ping() { + sendMessage(Opcode.Ping, null) + } + + fun sendMessage(opcode: Opcode, message: T) { + ioSafe { + val socket = acquireSocket() + val outputStream = DataOutputStream(socket.getOutputStream()) + + val json = message?.toJson() + val content = json?.toByteArray() ?: ByteArray(0) + + // Little endian starting from 1 + // https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 + val size = content.size + 1 + + val sizeArray = ByteArray(4) { num -> + (size shr 8 * num and 0xff).toByte() + } + + Log.d(tag, "Sending message with size: $size, opcode: $opcode") + outputStream.write(sizeArray) + outputStream.write(ByteArray(1) { opcode.value }) + outputStream.write(content) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt new file mode 100644 index 00000000..61c00d6e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt @@ -0,0 +1,62 @@ +package com.lagradost.cloudstream3.utils.fcast + +// See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 +enum class Opcode(val value: Byte) { + None(0), + Play(1), + Pause(2), + Resume(3), + Stop(4), + Seek(5), + PlaybackUpdate(6), + VolumeUpdate(7), + SetVolume(8), + PlaybackError(9), + SetSpeed(10), + Version(11), + Ping(12), + Pong(13); +} + + +data class PlayMessage( + val container: String, + val url: String? = null, + val content: String? = null, + val time: Double? = null, + val speed: Double? = null, + val headers: Map? = null +) + +data class SeekMessage( + val time: Double +) + +data class PlaybackUpdateMessage( + val generationTime: Long, + val time: Double, + val duration: Double, + val state: Int, + val speed: Double +) + +data class VolumeUpdateMessage( + val generationTime: Long, + val volume: Double +) + +data class PlaybackErrorMessage( + val message: String +) + +data class SetSpeedMessage( + val speed: Double +) + +data class SetVolumeMessage( + val volume: Double +) + +data class VersionMessage( + val version: Long +) diff --git a/app/src/main/res/drawable/hourglass_24.xml b/app/src/main/res/drawable/hourglass_24.xml new file mode 100644 index 00000000..7bd1ebbd --- /dev/null +++ b/app/src/main/res/drawable/hourglass_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_battery.xml b/app/src/main/res/drawable/ic_battery.xml new file mode 100644 index 00000000..24d0a77f --- /dev/null +++ b/app/src/main/res/drawable/ic_battery.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_outline.xml b/app/src/main/res/drawable/rounded_outline.xml new file mode 100644 index 00000000..b85ace8e --- /dev/null +++ b/app/src/main/res/drawable/rounded_outline.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/subdl_logo_big.xml b/app/src/main/res/drawable/subdl_logo_big.xml new file mode 100644 index 00000000..a6cbb311 --- /dev/null +++ b/app/src/main/res/drawable/subdl_logo_big.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/layout/account_managment.xml b/app/src/main/res/layout/account_managment.xml index 389a3406..e7afb382 100644 --- a/app/src/main/res/layout/account_managment.xml +++ b/app/src/main/res/layout/account_managment.xml @@ -62,14 +62,16 @@ + android:id="@+id/account_switch_account" + android:text="@string/switch_account" + style="@style/SettingsItem" + android:focusable="true"/> + android:id="@+id/account_logout" + android:text="@string/logout" + style="@style/SettingsItem" + android:focusable="true"> diff --git a/app/src/main/res/layout/account_single.xml b/app/src/main/res/layout/account_single.xml index cbfb9f18..c4f7fa39 100644 --- a/app/src/main/res/layout/account_single.xml +++ b/app/src/main/res/layout/account_single.xml @@ -1,10 +1,11 @@ + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:foreground="?android:attr/selectableItemBackgroundBorderless" + android:orientation="horizontal" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:focusable="true"> + android:id="@+id/account_profile_picture" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="ContentDescription" /> + android:foreground="@null" + android:id="@+id/account_name" + tools:text="Account 1" + style="@style/SettingsItem" /> diff --git a/app/src/main/res/layout/account_switch.xml b/app/src/main/res/layout/account_switch.xml index 659ad840..5153f0e3 100644 --- a/app/src/main/res/layout/account_switch.xml +++ b/app/src/main/res/layout/account_switch.xml @@ -7,18 +7,20 @@ android:layout_height="match_parent"> + android:id="@+id/account_list" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + android:background="?attr/primaryBlackBackground" + tools:listitem="@layout/account_single" + android:layout_width="match_parent" + android:layout_rowWeight="1" + android:layout_height="wrap_content" + android:focusable="true"/> + android:id="@+id/account_add" + android:text="@string/add_account" + style="@style/SettingsItem" + android:focusable="true"> diff --git a/app/src/main/res/layout/bottom_resultview_preview.xml b/app/src/main/res/layout/bottom_resultview_preview.xml index 4a64114e..3372fe7b 100644 --- a/app/src/main/res/layout/bottom_resultview_preview.xml +++ b/app/src/main/res/layout/bottom_resultview_preview.xml @@ -41,22 +41,34 @@ android:layout_marginStart="10dp" android:orientation="vertical"> - + tools:text="The Perfect Run" /> - + - + + + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="7dp"> + tools:visibility="visible" /> + tools:visibility="visible" /> diff --git a/app/src/main/res/layout/dialog_online_subtitles.xml b/app/src/main/res/layout/dialog_online_subtitles.xml index 7803e261..e0eac5e0 100644 --- a/app/src/main/res/layout/dialog_online_subtitles.xml +++ b/app/src/main/res/layout/dialog_online_subtitles.xml @@ -40,7 +40,7 @@ android:layout_width="match_parent" android:layout_height="30dp" android:layout_gravity="center_vertical" - android:layout_marginEnd="30dp"> + android:layout_marginEnd="40dp"> @@ -106,7 +107,8 @@ android:layout_margin="10dp" android:background="?selectableItemBackgroundBorderless" android:contentDescription="@string/change_providers_img_des" - android:nextFocusLeft="@id/main_search" + android:focusable="true" + android:nextFocusLeft="@id/year_btt" android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" android:nextFocusDown="@id/search_autofit_results" diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml index fd845ee8..4974a027 100644 --- a/app/src/main/res/layout/download_child_episode.xml +++ b/app/src/main/res/layout/download_child_episode.xml @@ -9,6 +9,7 @@ android:layout_height="50dp" android:layout_marginBottom="5dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" android:nextFocusLeft="@id/nav_rail_view" android:nextFocusRight="@id/download_button" app:cardBackgroundColor="@color/transparent" @@ -84,7 +85,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/download_child_episode_holder" android:padding="10dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index 226c1632..21f79ca6 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -9,6 +9,8 @@ android:layout_marginTop="10dp" android:layout_marginEnd="10dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusRight="@id/download_button" app:cardBackgroundColor="?attr/boxItemBackground" app:cardCornerRadius="@dimen/rounded_image_radius"> @@ -71,7 +73,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/episode_holder" android:padding="10dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_plugin_details.xml b/app/src/main/res/layout/fragment_plugin_details.xml index 7a8f85e4..79013d9f 100644 --- a/app/src/main/res/layout/fragment_plugin_details.xml +++ b/app/src/main/res/layout/fragment_plugin_details.xml @@ -52,6 +52,7 @@ android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="@string/title_settings" android:visibility="gone" + android:focusable="true" app:srcCompat="@drawable/ic_baseline_tune_24" tools:visibility="visible" /> @@ -61,6 +62,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" android:layout_marginStart="16dp" + android:focusable="true" android:background="?attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_github_logo" /> @@ -81,6 +83,7 @@ style="@style/SmallBlackButton" android:layout_gravity="center" android:layout_marginStart="10dp" + android:focusable="false" android:text="@string/extension_description" /> + + - - + android:orientation="horizontal"> + android:layout_marginEnd="5dp" + tools:text="Season 2 Episode 1022 will be released in" /> + android:orientation="vertical" + android:visibility="gone" + tools:visibility="visible"> + android:orientation="vertical" + android:visibility="gone" + tools:visibility="visible"> + android:scaleType="centerCrop" + android:foreground="@drawable/rounded_outline" + tools:src="@drawable/profile_bg_orange" + android:contentDescription="@string/account"/> + + tools:text="Quick Brown Fox" /> @@ -142,10 +144,24 @@ android:id="@+id/commit_hash" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center" android:padding="10dp" android:text="@string/commit_hash" android:textColor="?attr/textColor" /> + + + + diff --git a/app/src/main/res/layout/player_select_tracks.xml b/app/src/main/res/layout/player_select_tracks.xml index d32e1b4e..94e09d60 100644 --- a/app/src/main/res/layout/player_select_tracks.xml +++ b/app/src/main/res/layout/player_select_tracks.xml @@ -38,21 +38,20 @@ android:requiresFadingEdge="vertical" android:id="@+id/video_tracks_list" android:layout_width="match_parent" - android:layout_height="match_parent" android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" - android:nextFocusLeft="@id/sort_subtitles" - android:nextFocusRight="@id/apply_btt" + android:nextFocusRight="@id/audio_tracks_holder" + tools:listitem="@layout/sort_bottom_single_choice" /> + android:id="@+id/audio_tracks_holder" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="50" + android:orientation="vertical"> @@ -107,17 +106,16 @@ + android:requiresFadingEdge="vertical" + android:id="@+id/auto_tracks_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_rowWeight="1" + android:background="?attr/primaryBlackBackground" + android:nextFocusRight="@id/apply_btt" + android:nextFocusLeft="@id/video_tracks_list" + tools:listfooter="@layout/sort_bottom_footer_add_choice" + tools:listitem="@layout/sort_bottom_single_choice" /> @@ -132,11 +130,12 @@ + style="@style/WhiteButton" + android:layout_gravity="center_vertical|end" + android:text="@string/sort_apply" + android:id="@+id/apply_btt" + android:nextFocusLeft="@id/auto_tracks_list" + android:layout_width="wrap_content" /> - - + android:layout_height="wrap_content" + tools:visibility="visible"> @@ -130,7 +133,7 @@ android:clickable="true" android:contentDescription="@string/download" android:focusable="true" - android:nextFocusLeft="@id/repository_item_root" + android:nextFocusLeft="@id/action_settings" android:padding="12dp" tools:src="@drawable/ic_baseline_add_24" /> diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index 76e8c434..e5a6881a 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -43,14 +43,26 @@ android:foreground="?android:attr/selectableItemBackgroundBorderless" android:nextFocusRight="@id/download_button" android:scaleType="centerCrop" - tools:src="@drawable/example_poster" /> + tools:src="@drawable/example_poster" + tools:visibility="invisible"/> + android:src="@drawable/play_button" + tools:visibility="invisible"/> + + + + + + Laai tans… Sub Verwyder - Stroom + Netwerk stroom Op rus CloudStream Speel @@ -105,4 +105,5 @@ Soek met behulp van tipes Voer lettertipes in deur dit in %s te plaas Rolverdeling: %s + Nuwe episode notifikasie diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index 550d83a9..734d5644 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -68,8 +68,8 @@ هيدا المصدر مش عاطي \"ميتا داتا\". إذا مش موجودة بالمصدر، ما رح يمشي الڤيديو. ما في أجزاء مشّي الحلقة - أكونتات - سرعة \"إيگن گرايڤي\" + الحسابات والأمان + سرعة الڤيديو كَمِّل سِجِل ما نلاقى الوصف @@ -80,7 +80,6 @@ محي بلش في تِلِفونات ما فيا تعوز الطريقة الجديدة لتجديد الآپات. جربو \"الطريقة القديمة\" إذا ما عم تنزل التجديدات. - بتزيد أوپسيونات لتحدد سرعة الڤيديو بعد ما تسكر \"كلود ستريم\"، بكفي الڤيديو بشِباك زغير فوق غير آپ هيدا المصدر ما بيدعم \"كروم كاست\" تنبيش منظّم @@ -124,7 +123,7 @@ %1$d–%2$d عطي المطورين موزززة عوز النسخة الإحتياطية - نقى أكونت + نقي أكونت سحابو ل فوق وتحت من اليمين أو الشمال ل تتحكمو بقوة الصوت أو قوة الضو ننسخ الرابط الوصف @@ -144,7 +143,7 @@ لودينگ… شيل بَعِد مَعلومات - التجديد والنسخات الاحتياطية + التجديدات والنسخات الاحتياطية خبي هيدي الجودات من نتائج التنبيش موقف موقتًا كلود ستريم @@ -199,7 +198,7 @@ شوف إذا في تجديد في مشكلة بجهاز العرض (Renderer error) العِنوان - پروكسي raw.githubusercontent.com + پروكسي \"گِت هَب\" جودة مشغل الڤيديو ملصق الترجمة أوڤا @@ -252,7 +251,7 @@ الحد الأعلى للحروف بعنوان الڤيديو المظاهر تجديدات الآپ - لغات المصادر + لغات الإضافات عام ممر التنزيل إخلاء مسؤولية @@ -265,7 +264,7 @@ فرجي أنمي المدبلج-المترجم النسخ الإحتياطي الإجراءات - بتتجاوز منع \"گِت هَب\" بستعمال JSDelivr. معقول تقدي لتأخير بتجديدات الآپ بكم يوم. + تجاوز منع روابط \"گِت هَب\" الـ\"raw\" بستعمال JSDelivr. معقول تقدي لتأخير تجديدات الآپ بكم يوم. ميزات مشغل الڤيديوات شيل موقع مظهر @@ -289,7 +288,7 @@ الشكل %1$d ساعة %2$d ديقة فضّى - إسم أكونتي الكول + إسم الأكونت فَلتِر الإشارات المرجعية بَلَش التجديد نسخ @@ -305,9 +304,9 @@ مشّي الحلقة سرعة الڤيديو تحكمو بالأكونتات - رمز اللغة (apc/ar/en) + رمز اللغة (ar) عمول أكونت - فرجي المحتوى الـ18+ بالمصادر يلي بتحتوي + تمكين محتوى 18+ في الامتدادات الداعمة المحتوى المفضل غَيِر الأكونت حط پوز على التنزيل @@ -334,8 +333,8 @@ إي مايل (ع شكل: email@example.com) %d ديقة فحص المصادر - example.com - إسم الوبسيت الكول تبعي + https://example.com + إسم الوب سيت الجديدة فتاح ومَشّي الملف نوع الألون رجاع @@ -348,7 +347,7 @@ اللون الاساسي فوت ع الأكونت مترجم - بِث + بِث من الإنترنت مشي التخزين الجُواني زيد أكونت @@ -382,11 +381,9 @@ حطو الأرقام السرية لـ\"%s\" الطريقة القديمة معلى - \"كلود ستريم\" ما بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپوز. + \"كلود ستريم\" ما بتجي مع مصادر ڤيديوات. لازم تنزلو المصادر من ريپويات. \n -\nفي معلومات على الـ\"ديسكورد\" تبعنا، أو فيكون تنبشو ع معلومات على الإنترنت. -\n -\nما فينا تحط الروابط تبع ريپوز المصادر هون من ورا \"سكاي يو كي المحدودة\" 🤮، يلي عازت تفكيرا المحدود لتجرب توقف هيدا الآپ بإستعمال \"قانون الألفية للملكية الرقميَّة\". +\nفي معلومات على الـ\"ديسكورد\" تبعنا، أو فيكن تنبشو ع معلومات على الإنترنت. زبد تتبع 3G/4G… نفَتح %s @@ -410,7 +407,7 @@ HDR مش منزل: %d ل بعدو - رابط الڤيديو + https://example.com/example.mp4 نقي الرفّ وقفتو الإشتراك لـ\"%s\" WP @@ -451,7 +448,7 @@ التلخيص إِيه الرئيسي - طبّق وقتما سكّر الآپ + سكر الآپ حتى تطبق التغيرات ساعدوني عوز هيدا إذا عم بتبين الترجمة %d ميلي ثانية بعدما لازم ما نلاقا الآپ @@ -569,7 +566,7 @@ دي ڤي دي الجودة عين الافتراضي - المرجع + المرجع (إختياري) المشغل يلي بـ\"كلود ستريم\" نزل لايحة المواقع يلي بدك تعوزن حطو الأرقام السرية @@ -595,5 +592,34 @@ برومو غير إتجاه الشاشة أوتوماتيكيًا حسب شكل الڤيديو رجع نعمل لاود لاللينك - نعمل كَپي للعنوان! + نبش بغير مصادر + نوتيفيكايشن عن حلقات جديدة + فرجي الاقترحات + بتزيد خيار السرعة بالمشغل + فحاص كل المصادر + هيدا الفحص معمول للمطورين وما بأكد لحالو إزا المصدر عم يشتغل. + المفضلة + فتح قفل كلودستريم + قفل بواسطة المقاييس الحيوية + رمز/كلمة مرور للمصادقة + فتاح التطبيق باستعمال البصمة، آي دي الوج، پِن، النمط، إو الپاسورد. + تسَكرت هيدي الواجهة من ورا محاولات فاشلة عديدة. پليز، سكر الآپ ورجاع فتحه. + %s +\nباقي + المصادقة البيومترية مش مدعومة ع هالجهاز + شيله من المفضل + اسم وعنوان الريپوزيتوري + نتسخ! + في ارور بالوصول ل الكليپبورد. پليز جرب مرة أخرى. + في ارور بالنسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ. + هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. +\nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. + أوكي + وقف اپتميزايشن بطارية جهازك + بطارية الآپ اصلًا محطوطة ع «غير مقيد» \"Unrestricted\" + ما قدرنا نفتح معلومات الآپ تبع \"كلود ستريم\". + موسيقى + أوديو بوك + الميديا + لتضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يلي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي بـ الباكگروند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". ملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآن بـ الباكگروند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ بـ«الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a8e79d22..8681398d 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -54,7 +54,7 @@ فشل التنزيل تم إلغاء التنزيل تم التنزيل - بث + دفق الشبكة خطأ في تحميل الرابط التخزين الداخلي مدبلج @@ -114,8 +114,7 @@ إعدادات ترجمة المُشغل ترجمة كروم كاست إعدادات ترجمة كروم كاست - وضع إيغنغرافي - يضيف خيار السرعة في المُشغل + سرعة التشغيل السحب لتقديم اسحب من جانب إلى آخر للتحكم في موضعك في مقطع فيديو السحب لتغيير الإعدادات @@ -139,7 +138,7 @@ إذن الوصول الى ذاكرة التخزين مفقود, من فضلك حاول مجددا. فشل إنشاء نسخة احتياطية %s بحث - الحسابات + الحسابات والأمن التحديثات والنسخ الاحتياطية معلومات البحث المتقدم @@ -284,10 +283,10 @@ عام زر العشوائي إظهار زر عشوائي على الصفحة الرئيسية والمكتبة - لغات المزود + لغات الامتداد واجهة التطبيق المحتوى المفضل - تفعيل محتوى خاص للبالغين داخل المزودين المدعومين + تفعيل محتوى خاص للبالغين داخل الإمتداد المدعوم فك تشفير الترجمة المصادر الواجهة @@ -308,8 +307,8 @@ إسم المستخدم البريد الإلكتروني 127.0.0.1 - إسم الموقع - رابط الموقع + إسم الموقع الجديد + رابط الموقع مثلا : https://example.com اللغة (الإنجليزية) Poster Pôster - Episode Poster - Main Poster - Next Random - Go back - Change Provider - Preview Background + Pôster do episódio + Pôster Principal + Próximo Aleatório + Voltar + Alterar Provedor + Visualizar plano de fundo Velocidade (%.2fx) - Nota: %.1f + Avaliado: %.1f Nova atualização encontrada! \n%1$s -> %2$s - Filler + Preenchimento %d min CloudStream Início - Procurar + Pesquisar Downloads Configurações Procurar… - Procurar no %s… + Pesquisar %s… Sem dados - Mais Opções + Mais opções Próximo episódio Gêneros Compartilhar - Abrir no Navegador - Pular Carregamento + Abrir no navegador + Pular carregamento Carregando… Assistindo Em espera - Completado - Deixado + Concluído + Desistido Planejando assistir Reassistindo - Assistir Filme + Reproduzir filme Transmitir Torrent Fontes Legendas - Tentar reconectar… - Voltar - Assistir Episódio + Tentando conectar novamente… + Volte + Reproduzir episódio - Baixar - Baixado + Download + Download concluído Baixando - Download Pausado - Download Iniciado - Download Falhado - Download Cancelado - Download Finalizado + Download pausado + Download iniciado + Download falhou + Download cancelado + Download concluído Transmitir - Erro Carregando Links - Armazenamento Interno + Erro ao carregar links + Armazenamento interno Dub Sub - Deletar Arquivo - Assistir Arquivo - Retomar Download - Pausar Download - Desativar relatório automático de erros - Mais info + Deletar arquivo + Reproduzir arquivo + Retomar download + Pausar download + Desative o relatório automático de erros + Mais informações Esconder - Assistir - Info - Filtrar Marcadores + Reproduzir + Informações + Filtrar marcadores Marcadores Remover - Selecionar marcador + Definir como assistido/não assistido Aplicar Copiar Fechar Limpar Salvar - Velocidade do Reprodutor - Configurar Legendas - Cor do Texto - Cor do Contorno - Cor do Fundo - Cor da Janela - Tipo de Borda - Elevação da Legenda + Velocidade de reprodução + Configurações de legendas + Cor do texto + Cor do contorno + Cor de fundo + Cor da janela + Tipo de borda + Elevação da legenda Fonte - Tamanho da Fonte + Tamanho da fonte Pesquisar usando fornecedor - Pesquisar usando genêros + Pesquisar usando tipos %d Benenes doados aos desenvolvedores Nenhuma Benenes doada - Autosseleção de Lingua - Baixar Linguas - Lingua da legenda - Segure para retornar a configuração padrão - Importe fontes colocando elas em %s - Continue Assistindo + Seleção automática de idioma + Baixar idiomas + Idioma da Legenda + Segure para redefinir para o padrão + Importe fontes colocando-as em %s + Continuar assistindo Remover Mais Info @string/home_play @@ -122,8 +122,7 @@ Configurações de legendas do Player Legendas do Chromecast Configurações de legendas do Chromecast - Modo Eigengravy - Adiciona um botão de velocidade no player + Velocidade de playback Deslize para avançar o vídeo Deslize de lado à lado para controlar a posição no vídeo Deslize para mudar as configurações @@ -145,8 +144,8 @@ Permissões de armazenamento faltando. Por favor tente novamente. Erro no backup de %s Procurar - Contas - Atualizações e backup + Contas e Segurança + Atualizações e Backup Info Procura Avançada Mostrar resultados separados por fornecedor @@ -167,7 +166,7 @@ Junte-se ao Discord Dar um benene para os desenvolvedores Benene dada - Linguagem do App + Idioma do aplicativo Esse fornecedor não possui suporte para Chromecast Nenhum link encontrado Link copiado para área de transferência @@ -280,8 +279,8 @@ Any legal issues regarding the content on this application should be taken up with the actual file hosts and providers themselves as we are not affiliated with them. In case of copyright infringement, please directly contact the responsible parties or the streaming websites. The app is purely for educational and personal use. CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface. It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk. Geral Botão Aleatório - Mostra o botão Aleatório na página inicial - Linguagem dos fornecedores + Mostrar botão aleatório na página inicial e na biblioteca + Linguagem das extensões Layout do App Mídia preferida Codificação das legendas @@ -296,11 +295,11 @@ Coloca o título debaixo do poster senha123 - MeuNomeLegal + Nome de usuário oi@mundo.com 127.0.0.1 - MeuSiteLegal - examplo.com + NovoNomedoSite + https://example.com Codigo da Língua (bp) - Já fiz vinho com toque de kiwi para belga sexy. + A rápida raposa marrom salta sobre o cachorro preguiçoso Recomendada %s carregada Carregar de arquivo @@ -419,8 +418,6 @@ Não transferido: %d CloudStream não tem fontes instaladas por padrão. Você precisa instalar um site de repositórios. \n -\nPor causa das limitações do DMCA (Digital Millennium Copyright Act ) feito em nome de Sky UK Limited 🤮nós não podemos adicionar site de repositórios no app. -\n \nEntre no nosso Discord ou pesquise online. Ver repositórios da comunidade Lista pública @@ -429,23 +426,23 @@ %s (Desativado) Reproduzir automaticamente próximo episódio Começa o próximo episódio quando o atual termina - Ativar NSFW em fornecedores compatíveis + Ativar NSFW em extensões compatíveis Fornecedores Reverter Ações votou com sucesso Baixando atualização do aplicativo… - Referencias + Referenciador (opcional) Atualizações do App - Tocar com CloudStream + Assistir com o CloudStream Automaticamente instale todos os plugins não instalados dos repositórios adicionados. - Reproduzir Trailer + Reproduzir trailer Navegador Copia de Segurança A Barra de Progresso pode ser usada quando o player estiver oculto Inscrito Essa lista está vazia. Tente mudar para outra. - Reproduzir Livestream + Reproduzir transmissão ao vivo Log do Teste Baixar plugins automaticamente Selecione o modo para filtrar os plugins baixados @@ -476,7 +473,7 @@ Conteúdo +18 Ajuda Processo de configuração de Redo - Não pudemos instalar a nova versão do App + Não foi possível instalar a nova versão do aplicativo instalador de pacotes Organizar por Votação (Alta para Baixa) @@ -493,7 +490,7 @@ Arquivo de modo de segurança encontrado! \nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. Inscrito em %s - Episódio %d lançado + Episódio %d lançado! Selecionar padrão Inscrição cancelada de %s Alguns aparelhos não possuem suporte para este pacote de instalação. Tente a opção legada se a atualização não instalar. @@ -544,7 +541,7 @@ MPV Abrindo mistura VLC - Aplicar quando reiniciar + Reinicie o aplicativo para ver as alterações. Visualização info de crash Faixas de áudio Adicionado em (novo para antigo) @@ -558,7 +555,7 @@ Aparência Desativar Usar - Link da stream + https://example.com/example.mp4 Gestos Plugin baixado Não foi possível se conectar ao GitHub. Ativando proxy JsDelivr… @@ -572,7 +569,74 @@ Provedor de teste Layout Padrões - Proxy: raw.githubusercontent.com - Contorna o bloqueio do GitHub usando jsDelivr. Pode atrasar as atualizações por alguns dias. + Proxy do GitHub + Contorne o bloqueio de URLs \"raw\" do GitHub usando jsDelivr. Pode atrasar as atualizações por alguns dias. Rotas alternativas + Favoritos + %s adicionado aos favoritos + Duplicata em potencial encontrada + Adicionar + Substituir + Possíveis itens duplicados foram encontrados em sua biblioteca: +\n +\n %s +\n +\nGostaria de adicionar este item mesmo assim, substituir os existentes ou cancelar a ação? + Insira o PIN + Insira o PIN para %s + Insira o PIN atual + PIN incorreto. Por favor, tente novamente. + O PIN deve ter 4 caracteres + Selecione uma conta + Gerenciar contas + Ignorar a seleção da conta na inicialização + Exibir um botão para alternar a orientação da tela + %s removido dos favoritos + Adicionar aos favoritos + Remover dos favoritos + Girar + Bloquear perfil + PIN + Links recarregados + Frequência de backup + Substitua tudo + Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' +\n +\nGostaria de adicionar este item mesmo assim, substituir o existente ou cancelar a ação? + Inscrever-se + Cancelar inscrição + Usar conta padrão + Editar conta + Conectado como %s + Habilite a troca automática de orientação da tela com base na orientação do vídeo + Rotação automática + Notificação de novo episódio + Pesquisar em outras extensões + Mostrar recomendações + Adiciona uma opção de velocidade no reprodutor + Testar todas as extensões + Esse teste é feito somente para desenvolvedores e não verifica ou nega o funcionamento de qualquer extensão. + Desbloquear CloudStream + O backup dos seus dados do CloudStream foi feito agora. Embora a possibilidade disso seja muito baixa, todos os dispositivos podem se comportar de maneira diferente. No caso raro de você ficar impedido de acessar o aplicativo, limpe os dados do aplicativo completamente e restaure a partir de um backup. Lamentamos muito qualquer inconveniente decorrente disso. + Bloquear com Biometria + Autenticação de Senha/PIN + A autenticação biométrica não é compatível com este dispositivo + Desbloquear o aplicativo com impressão digital, ID facial, PIN, padrão e senha. + Esta tela foi fechada devido a diversas tentativas malsucedidas. Por favor reinicie o aplicativo. + %s +\nrestante(s) + Favorito + Não favorito + copiado! + Erro ao acessar a área de transferência. Tente novamente. + Nome e URL do repositório + Erro ao copiar. Copie o logcat e entre em contato com o suporte do aplicativo. + Para garantir downloads e notificações ininterruptos para programas de TV assinados, o CloudStream precisa de permissão para ser executado em segundo plano. Ao pressionar OK, você será direcionado para as informações do aplicativo. Lá, vá até Uso da bateria do aplicativo e defina o uso da bateria como Irrestrito. Observe que esta permissão não significa que o CS3 irá descarregar sua bateria. Ele só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se você optar por cancelar, poderá ajustar essa configuração posteriormente nas Configurações Gerais. + Ok + Desativar otimização de bateria + O uso da bateria do app já está definido como irrestrito + Não foi possível abrir as informações do aplicativo CloudStream. + Música + Áudio-livro + Mídia diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 00c968f2..0a8cf997 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -117,8 +117,7 @@ Nastavení titulků přehrávače Titulky Chromecastu Natavení titulků Chromecastu - Rychlostní režim - Přidá do přehrávače možnost rychlosti + Rychlost přehrávání Přejet pro posun Přejeďte prstem ze strany na stranu pro ovládání své pozice ve videu Přejet pro změnu nastavení @@ -140,7 +139,7 @@ Chybí oprávnění k úložišti. Zkuste to prosím znovu. Chyba při zálohování %s Search - Účty + Účty a zabezpečení Aktualizace a záloha Informace Pokročilé hledání @@ -266,7 +265,7 @@ Obecné Náhodné tlačítko Zobrazit na domovské stránce a v knihovně náhodné tlačítko - Jazyk poskytovatelů + Jazyky rozšíření Rozložení aplikace Preferovaná média Kódování titulků @@ -281,7 +280,7 @@ Umístit název pod plakát heslo123 - MojeSuperJmeno + Uživatelské jméno ahoj@svete.cz 127.0.0.1 lozinka123 - MojeCoolIme + Korisničko ime bok@svijete.com 127.0.0.1 - MojaCoolStranica - primjer.com + NovoImeStranice + https://primjer.com Šifra jezika (en) %1$s %2$s račun @@ -402,8 +401,8 @@ Filtriraj po željenom jeziku medija Extras Trailer - Veza na stream - Upućivač + https://primjer.com/primjer.mp4 + Referent (nije obavezno) Sljedeće Gledaj videozapise na ovim jezicima Prethodno @@ -434,8 +433,6 @@ Nepreuzeto: %d CloudStream nema instalirane web stranice prema zadanim postavkama. Morate instalirati stranice iz repozitorija. \n -\nZbog bezumnog uklanjanja DMCA od strane Sky UK Limited 🤮 ne možemo povezati web mjesto repozitorija u aplikaciji. -\n \nPridružite se našem Discordu ili tražite online. Pregledajte repozitorije zajednice Javni popis @@ -549,9 +546,9 @@ Otkazana pretplata sa %s Vraćanje ISP zaobilaznice - raw.githubusercontent.com Proxy + GitHub Proxy Neuspješno dohvaćanje GitHuba. Uključuje se jsdelivr proxy … - Zaobilazi blokiranje GitHuba koristeći jsdelivr. Može odgoditi ažuriranja za nekoliko dana. + Zaobilazi blokiranje neobrađenih GitHub URL-ova koristeći jsDelivr. Može uzrokovati kašnjenje ažuriranja nekoliko dana. Preferirana kvaliteta gledanja (podatkovna mobilna mreža) Profil %d Wi-Fi @@ -614,7 +611,22 @@ Prikaži gumb za prebacivanje orijentacije zaslona Omogućuje automatsko mijenjanje orijentacije zaslona na temelju orijentacije videa Automatsko rotiranje - Naslov je kopiran! rotiraj_video_tipka automatski_rotiraj_video_tipka + Obavijest za novu epizodu + Pretraži u ostalim proširenjima + Dodaje opciju brzine u playeru + Testiraj sva proširenja + Ovaj je test namijenjen samo programerima i ne provjerava niti negira rad bilo kojeg proširenja. + Prikaži preporuke + Ime repozitorija i URL + kopirano! + Zaključaj s biometrijskim podatcima + %s +\npreostalo + Greška u pristupanju međuspremnika. Pokušaj ponovo. + Otključaj CloudStream + Lozinka/PIN autentifikacija + Ovaj uređaj ne podržava biometrijsku autentifikaciju + Ovaj je ekran zatvoren zbog višestrukih neuspjelih pokušaja. Pokrenite aplikaciju ponovo. diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 5b1dbcf0..5533cdc0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -37,7 +37,7 @@ Állapot Forrás címe Előzmények - Eigengravy mód + Visszajátszás sebessége Felirat magassága Letöltés elkezdve Nem található cselekmény @@ -71,7 +71,7 @@ Feliratok Vissza Letöltés kész - Stream + Hálózati stream Hiba a linkek betöltésekor Belső tárhely Dub @@ -124,7 +124,6 @@ Lejátszó feliratok beállításai Chromecast Feliratok Chromecast feliratok beállításai - Sebességbeállítást ad hozzá a lejátszóhoz Epizód lejátszása Letöltve Letöltés szüneteltetve @@ -171,11 +170,11 @@ OVA Egyebek Sorozat - @string/anime + Anime Forráshiba NSFW Rajzfilm - @string/ova + OVA Élőadás NSFW Videó @@ -184,7 +183,7 @@ Ázsiai dráma Linkek újratöltése Link másolás - Link letöltés + Letöltés mirror Automatikus letöltés Adatok eltárolva Hiba a biztonsági mentés során %s @@ -223,9 +222,9 @@ Távoli hiba Render hiba Váratlan lejátszó hiba - Letöltés hiba, ellenőrízze a tárolási engedélyeket + Letöltés hiba, ellenőrizze a tárolási engedélyeket Chromecast epizód - Chromecast link + Chromecast mirror Lejátszás az alkalmazásban Lejátszás %s Lejátszás böngészőben @@ -233,7 +232,7 @@ Újracsatlakozás… Húzd balra vagy jobbra a videólejátszóban az idő vezérléséhez Csúsztassa ujját a beállítások módosításához - Csúsztassa felfelé vagy lefelé a bal vagy jobb oldalon a fényerő vagy a hangerő megváltoztatásához + Csúsztassa felf/le az ujját a bal/jobb oldalon a fényerő vagy a hangerő megváltoztatásához Biztonsági mentés 0 Banán a fejlesztőknek Húzd el, hogy beless @@ -243,7 +242,7 @@ Dupla koppintás a szüneteltetéshez Lejátszó keresési értéke (Másodpercben) Koppintson kétszer a jobb vagy bal oldalra az előre vagy hátra ugráshoz - Koppintson kétszer középen a szüneteltetéshez + Koppintson kétszer középre a szüneteltetéshez Rendszer fényerejének használata Rendszer fényerejének használata az appban a sötét átfedés helyett Előrehaladás frissítése @@ -286,7 +285,7 @@ Funkciók Előnyben részesített videóminőség (mobilinternet) Videolejátszó cím max karakterek - Nem sikerült elérni a GitHubot, a jsdelivr proxy engedélyezése. + Nem sikerült elérni a GitHubot, a jsdelivr proxy bekapcsolva… Bővítmények Általános Felirat kódolása @@ -295,10 +294,10 @@ Szolgáltató teszt Sikertelen Problémákat okoz, ha túl magasra van állítva az alacsony tárhellyel rendelkező eszközökön, például az Android TV-n. - Korhatáros tartalmak engedélyezése a támogatott szolgáltatóknál + Korhatáros tartalmak engedélyezése a támogatott kiegészítőknél Elrendezés - raw.githubusercontent.com Proxy - A lejátszó funkciói + GitHub Proxy + Lejátszó funkciók Előnyben részesített videóminőség (WiFi-n) Hasznos az internetszolgáltató blokkjainak megkerüléséhez Elrendezés @@ -306,10 +305,10 @@ NGINX szerver URL-címe Szinkronizált/feliratozott animék megjelenítése Alapértelmezettek - Megjelenít egy gombot a Kezdőlapon, amely egy véletlenszerű filmet vagy TV sorozatot választ a Kezdőlapról + Véletlenszerű gomb megjelenítése a Könyvtárban és Főoldalon Letöltési útvonal Gyorsítótár - Szolgáltatók nyelvei + Kiegészítők nyelvei Napló Könyvtár internetszolgáltató-kikerülések @@ -322,7 +321,7 @@ Előnyben részesített média Hivatkozások Videó és kép gyorsítótár törlése - A jsdelivr használatával a GitHub blokkolása megkerülhető. Néhány nappal késleltetheti a frissítéseket. + A jsDelivr használatával a tiszta GitHub blokkolása megkerülhető. Néhány nappal késleltetheti a frissítéseket. Összeomlást okoz, ha túl magasra van állítva a kevés memóriával rendelkező eszközökön, például az Android TV-n. Betöltés az internetről Videósávok @@ -333,13 +332,13 @@ Alkalmazásfrissítés letöltése… Frissítve (újabbtól a régebbihez) Úgy tűnik, a könyvtárad üres :( -\nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz - Úgy tűnik, ez a lista üres, próbálj meg egy másikra váltani +\nJelentkezz be egy könyvtár fiókba, vagy adj hozzá műsorokat a helyi könyvtárodhoz. + Úgy tűnik, ez a lista üres, próbálj meg egy másikra váltani. Max 4K SDR Fiók létrehozása - pelda.com + https://példa.hu Feliratok szinkronizálása Alkalmazásfrissítés telepítése… Túl sok szöveg. Nem lehet a vágólapra menteni. @@ -349,7 +348,7 @@ Frissítés /\?\? Árnyék - Filmelőzetes + Előzetes Mit szeretnél látni Minden %s már letöltött Először telepítse a bővítményt @@ -357,13 +356,13 @@ Kinézet Alkalmazás elrendezés Szinkronizálás - Nem sikerült bejelentkezni a következőként: %s + Nem sikerült bejelentkezni a %s-nál Min 1000 ms Ajánlott Érvénytelen adatok - Link a streamhez + https://példa.hu/példa.mp4 Nem sikerült betölteni: %s Elkezdődött a(z) %1$d %2$s letöltése… Töltse le az összes bővítményt ebből a tárolóból\? @@ -372,16 +371,16 @@ MPV Alkalmazás nem található PackageInstaller - Rendezés e szerint: + Rendezés e szerint Feliratkozott a következőre: %s - MenőWeboldalam + ÚjOldalNév DVD %d plugin frissítve Értékelés: %s Előzmények törlése Nem Feliratkozva - Használd ezt, ha a feliratok %d ms-sel korábban jelennek meg. + Használd ezt, ha a feliratok %d ms-sel korábban jelennek meg Lejátszó Felbontás és cím Előnyben részesített videolejátszó @@ -420,7 +419,7 @@ Minden felirat nagybetűs Intro Leiratkozott a következőről: %s - Bloat eltávolítása a feliratokról + Szükségtelen elemek eltávolítása a feliratokról Szűrés előnyben részesített médianyelv szerint Biztos vagy benne, hogy ki akarsz lépni\? Rendezés @@ -431,9 +430,7 @@ Ez az összes tároló bővítményt is törli A CloudStream alapértelmezés szerint nem telepített webhelyeket. A webhelyeket a tárolókból kell telepítenie. \n -\nA Sky UK Limited agyatlan DMCA letiltása miatt 🤮 nem tudjuk az alkalmazásban linkelni az adattár oldalát. -\n -\nCsatlakozz a Discordunkhoz vagy keress online. +\nCsatlakozz a Discord-unkhoz vagy keress online. Verzió Megjelölés megtekintettként Eltávolítás a megnézettek közül @@ -466,7 +463,7 @@ Betűrendben (A-tól a Z-ig) Frissítve (régebbitől az újabbig) jelszó123 - AzÉnMenőFelhasználónevem + Felhasználónév 127.0.0.1 Fiókváltás Fiók hozzáadása @@ -481,14 +478,14 @@ Bővítmények Tároló hozzáadása Tároló neve - Tárhely URL címe + Repó URL Bővítmény betöltve Bővítmény letöltve Közreműködők Betűrendben (Z-től az A-ig) Könyvtár kiválasztása - Biztonságos módú fájl található! -\nNem tölt be semmilyen kiterjesztést indításkor, amíg a fájl el nem lesz távolítva. + Biztonságos módú fájlba ütköztünk! +\nNem töltődik be semmilyen kiegészítő indításkor, amíg a fájl nem kerül törlésre. Normál %s betöltve Beállítás kihagyása @@ -503,7 +500,7 @@ Az átugrás mértéke, amikor a lejátszó el van rejtve Jogi nyilatkozat Lejátszó megjelenítve - Ugrási Érték - Lejátszó elrejtve - Ugrási Érték + Lejátszó Elrejtve - Ugrási Érték Klónozott oldal Egy meglévő webhely klónjának hozzáadása, más URL-címmel TV elrendezés @@ -513,4 +510,86 @@ Mentési gyakoriság Értékelt Kikapcsolás + Kamera Rip + Nyitás + WP + Új epizód értesítés + Keresés más kiegészítőkben + Ajánlatok mutatása + Sebesség opció megjelenítése a lejátszóban + Minden kiegészítő tesztelése + Ez a teszt szigorúan fejlesztők számára készült, nem alkalmas egyes kiegészítők működésének visszaigazolására. + %1$s %2$s + TS + Feliratkozás + Leiratkozás + Linkek Újratöltve + Zárás + Hivatkozó (opcionális) + Nem találhatóak pluginek a repóban + Repó nem található, ellenőrizze a címet vagy próbálja VPN-el + Web Videó Cast + %s kihagyása + A kihagyási felugró ablakok mutatása nyitás/zárás esetén + Alapbeállítás + Használ + %d profil + Használja ezt ha a felirat %d ms-ot késik + Kamera HD + Poszter Kép + TC + Mobil adat + Wi-Fi + Szerkeszt + Kamera + Nyomott + Kevert zárás + Kevert nyitás + Segítség + Profilok + Eltávolítás kedvencekből + Adja meg a jelenlegi PIN-t + Itt beállíthatja hogyan rendezze el a forrásokat. Ha egy videónak nagyobb a prioritása, előbb fog megjelenni a listában. A forrás prioritás és a minőség prioritás együttes értéke adja ki a videó prioritást. +\n +\nForrás A: 3 +\nMinőség B: 7 +\nEzek összértéke egy 10-es videó prioritást eredményez. +\n +\nFIGYELEM: Ha az összeg több mint 10, a lejátszó nem tölt be mást ha már a link betöltésre került! + Potenciálisan dupla elemek a könyvtárjában: +\n +\n%s +\n +\nSzeretné hozzáadni ezt az elemet mindenképpen, ezzel felülírva a jelenlegit, vagy visszavonja a műveletet? + Fiók választás kihagyása belépéskor + Használjon alapértelmezett fiókot + Elforgatás + Profil háttér + Kedvencek + %s hozzáadva a kedvencekhez + %s eltávolítva a kedvencekből + Hozzáadás a kedvencekhez + Úgy tűnik egy potenciálisan dupla elem már létezik a könyvtárjában: \'%s.\' +\n +\nMindenképpen hozzá akarja adni ezt az elemet, ezzel felülírva a régit, vagy visszavonja a műveletet? + Adja meg a PIN-t + Profil Zárolása + Válasszon egy fiókot + Fiókok kezelése + Fiók módosítása + Belépve mint %s + Jelenítsen meg egy kapcsolót a képorientáció váltáshoz + Potenciális Dupla Találat + Adja meg a PIN-t a %s-hoz + PIN + Hibás PIN. Próbálja újra. + A UI hibásan jelenítődött meg, ez egy JELENTŐS BUG ezért kérjük jelentse be %s + Már korábban szavazott + Hozzáadás + Kicserélés + Mind Kicserélése + Minőségek + A PIN 4 karakter hosszú kell legyen + Auto elforgatás + Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 0bb2a24a..d537a1d5 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -115,8 +115,7 @@ Pengaturan subtitle pemutar Subtitle Chromecast Pengaturan subtitle Chromecast - Mode Eigengravy - Menambahkan opsi kecepatan di pemutar + Kecepatan pemutaran Geser untuk mengubah waktu Geser dari sisi ke sisi untuk mengontrol posisi dalam video Geser untuk mengubah pengaturan @@ -138,8 +137,8 @@ Izin penyimpanan tidak ditemukan, mohon coba lagi. Error saat mencadang %s Cari - Kredit dan akun - Update dan cadangan + Akun dan Keamanan + Update dan Cadangan Info Pencarian Lanjutan Memberikan hasil pencarian yang dipisahkan berdasarkan provider @@ -263,8 +262,8 @@ Any legal issues regarding the content on this application should be taken up with the actual file hosts and providers themselves as we are not affiliated with them. In case of copyright infringement, please directly contact the responsible parties or the streaming websites. The app is purely for educational and personal use. CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface. It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk. Umum Tombol Acak - Tampilkan tombol acak di Beranda - Bahasa provider + Tampilkan tombol acak di Beranda dan Pustaka + Bahasa ekstensi Tata Letak Aplikasi Media yang lebih diinginkan Antarmuka pengguna @@ -373,10 +372,10 @@ Bawaan Tampilan Fitur - Tampilkan konten NSFW + Mengaktifkan NSFW pada Ekstensi yang didukung Putar Cuplikan Putar Siaran - Siaran + Aliran jaringan Bahasa Subtitel Putar otomatis episode selanjutnya Putar episode selanjutnya, setelah ini berakhir @@ -398,22 +397,20 @@ %1$s %2$d%3$s Siaran langsung Hapus Website - UsernameKeren + Username contoh@email.com 127.0.0.1 - Websiteku - contoh.com + NamaSitus Baru + https://contoh.com Ekstra Apa yang ingin anda lihat Plugin terhapus %d plugin diperbarui Lihat Repositori dari Group List Umum - CloudStream tidak memiliki sumber video secara bawaan. Kamu harus menginstall dari repositori. + CloudStream tidak memiliki situs yang terinstal secara default. Anda perlu menginstal situs-situs dari repositori. \n -\nKarena banyak laporan dari banyak pihak berwajib, kami tidak dapat memberikannya secara langsung. -\n -\nGabung dengan group Discord atau cari di internet. +\nBergabunglah dengan Discord kami atau cari secara online. Alamat Repositori Buat Akun Error @@ -435,7 +432,7 @@ Semua Umur %s (Tidak aktif) Trek - Terapkan saat dimuat ulang + Terapkan saat dimuat ulang untuk melihat perubahan. Keterangan Versi Status @@ -457,7 +454,7 @@ Pilih ini untuk menghapus semua repositori plugin Lewati pengaturan Alamat salah - Alamat streaming + https://contoh.com/contoh.mp4 Selanjutnya Sebelumnya Ubah tampilan aplikasi @@ -482,7 +479,7 @@ Gerakan Beberapa perangkat tidak mendukung penginstal paket mode baru. Coba mode lama jika pembaruan tidak dapat diinstal. Aksi - Referer + Memberi referensi (opsional) Ya Install ekstensi terlebih dahulu Semua Bahasa @@ -545,9 +542,9 @@ Berlangganan ke %s Berhenti berlangganan di %s Episode %d telah rilis! - Proxy raw.githubusercontent.com + Proxy GitHub Tidak dapat menjangkau GitHub. Mengaktifkan proxy jsDelivr… - Melewati pemblokiran GitHub menggunakan jsDelivr. Dapat menyebabkan pembaruan tertunda beberapa hari. + Lewati pemblokiran raw URL github menggunakan jsDelivr. Dapat menyebabkan pembaruan tertunda selama beberapa hari. Bypass ISP Pulihkan Kualitas nonton yang diinginkan (Data Seluler) @@ -607,4 +604,38 @@ Lewati pemilihan akun saat startup Kelola Akun Edit akun + Putar + Menampilkan tombol sakelar untuk orientasi layar + Tautan Dimuat Ulang + Mengaktifkan peralihan otomatis orientasi layar berdasarkan orientasi video + Putar otomatis + Cari di ekstensi lainnya + Menambahkan opsi percepat di pemutar + Tes ini hanya ditujukan untuk pengembang dan tidak memverifikasi atau menolak kerja ekstensi apa pun. + Notifikasi episode baru + Tampilkan rekomendasi + Menguji semua Ekstensi + Data CloudStream Anda telah dicadangkan. Meskipun peluang terjadinya kasus ini sangat kecil dan jarang terjadi, tetapi semua perangkat berperilaku berbeda. Jika Anda ada dalam situasi terburuk, misalnya gagal untuk mengakses aplikasi, segera hapus data aplikasi sepenuhnya dan pulihkan data cadangan. Kami mohon maaf atas segala ketidaknyamanan yang mungkin ditimbulkan. + Otentikasi Kata Sandi/PIN + Otentikasi biometrik tidak didukung di perangkat ini + Buka kunci aplikasi dengan Sidik Jari, ID Wajah, PIN, Pola, dan Kata Sandi. + Layar ini ditutup setelah mengalami beberapa kali percobaan yang gagal. Anda harus memulai ulang aplikasi ini. + Batalkan favorit + Buka kunci CloudStream + %s +\ntersisa + Favorit + Kunci dengan Biometrik + Nama dan URL repositori + Gagal mengakses Papan Klip, mohon coba lagi. + disalin! + Gagal menyalin, mohon salin logcat dan hubungi pengembang aplikasi. + Oke + Matikan pengoptimalan Baterai + Pemakaian baterai untuk aplikasi ini sudah diatur menjadi tidak dibatasi + Gagal membuka info aplikasi CloudStream. + Musik + Buku Audio + Media + Untuk memastikan unduhan dan pemberitahuan tanpa gangguan untuk acara TV berlangganan, CloudStream memerlukan izin untuk berjalan di latar belakang. Dengan menekan OK, Anda akan diarahkan ke Info aplikasi. Di sana, gulir ke Penggunaan baterai aplikasi dan atur penggunaan baterai ke Tidak Terbatas. Harap dicatat, izin ini tidak berarti CS3 akan menguras baterai Anda. Ini hanya akan beroperasi di latar belakang ketika diperlukan, seperti ketika menerima pemberitahuan atau mengunduh video dari ekstensi resmi. Jika Anda memilih untuk membatalkannya, Anda dapat menyesuaikan pengaturan ini nanti di Pengaturan Umum. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 74839a47..040b0f31 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -61,7 +61,7 @@ Download fallito Download cancellato Download completato - Stream + Flusso di rete Errore durante il caricamento dei link Archiviazione interna Doppiato @@ -122,8 +122,7 @@ Impostazioni sottotitoli lettore Sottotitoli Chromecast Impostazioni sottotitoli Chromecast - Modalità Eigengravy - Aggiungi opzione velocità nel player + Velocità di riproduzione Scorri per mandare avanti/indietro Scorri da un lato all\'altro per controllare la tua posizione in un video Scorri per cambiare le impostazioni @@ -147,7 +146,7 @@ Permessi di archiviazione mancanti. Per favore riprova. Errore nel backup %s Cerca - Accounts + Account e sicurezza Aggiornamenti e Backup Info Ricerca avanzata @@ -287,11 +286,11 @@ Avvertenza Generale Random - Mostra pulsante Random nella homepage - Lingua provider + Mostra pulsante casuale nella home page e nella libreria + Lingue estensione Layout app Media preferito - Abilita NSFW sui provider supportati + Abilita NSFW sulle estensioni supportate Encoding Sottotitoli Provider Interfaccia utente @@ -305,11 +304,11 @@ Titolo sotto il poster password123 - IlMioUsername + Nome utente hello@world.com 127.0.0.1 - IlMioSito - example.com + NuovoNomeSito + https://example.com Codice lingua (it) %1$s %2$s account @@ -391,8 +390,8 @@ Filtra in base alla lingua preferita Extra Trailer - Link allo stream - Referer + https://example.com/example.mp4 + Referente (facoltativo) Prossimo Guarda video in queste lingue Precedente @@ -424,9 +423,7 @@ Aggiornati %d plugin CloudStream non ha siti installati per impostazione predefinita. È necessario installare i siti dai repository. \n -\nA causa di una rimozione DMCA senza cervello da Sky UK Limited 🤮 non possiamo collegare il sito repository nell\'app. -\n -\nUnisciti al nostro Discord o cerca online. +\nJoin our Discord or search online. Vedi le repository della community Lista pubblica Tutti i sottotitoli in maiuscolo @@ -435,7 +432,7 @@ Tracce Traccia audio Traccia video - Applica al riavvio + Riavvia app per visualizzare le modifiche. Safe mode attiva Tutte le estensioni sono state disabilitate a causa di un arresto anomalo per aiutarti a trovare l\'estensione che causa il problema. Vedi informazioni del crash @@ -539,12 +536,12 @@ Ferma Superato Fallito - Proxy raw.githubusercontent.com + Proxy GitHub Disiscritto da %s Iscritto Iscritto a %s Impossibile raggiungere GitHub. Attivazione proxy jsDelivr… - Aggira il blocco di GitHub usando jsDelivr. Potrebbe causare un ritardo degli aggiornamenti di alcuni giorni. + Evita il blocco degli URL github non elaborati utilizzando jsDelivr. Potrebbe causare un ritardo degli aggiornamenti di alcuni giorni. Baypass ISP Ripristina Aggiornando shows a cui sei iscritto @@ -602,10 +599,42 @@ Entrato come %s Inserisci il PIN per %s Blocca profilo - Usa Account Default + Usa account predefinito Salta la selezione dell\'account all\'avvio Gestisci Accounts Modifica account Collegamenti ricaricati Ruota + Visualizza un pulsante di commutazione per l\'orientamento dello schermo + Abilita la commutazione automatica dell\'orientamento dello schermo in base all\'orientamento del video + Rotazione automatica + Cerca in altre estensioni + Mostra consigli + Aggiunge un\'opzione di velocità nel lettore + Prova tutte le estensioni + Questo test è pensato solo per gli sviluppatori e non verifica o nega il funzionamento di alcuna estensione. + Notifica nuovo episodio + Sblocca CloudStream + Blocca con biometria + Autenticazione con password/PIN + L\'autenticazione biometrica non è supportata su questo dispositivo + Sblocca app con impronta digitale, Face ID, PIN, sequenza e password. + Questa schermata è stata chiusa a causa di più tentativi falliti. Riavvia l\'app. + È stato eseguito il backup dei tuoi dati CloudStream. Sebbene questa possibilità sia molto bassa, tutti i dispositivi possono comportarsi in modo diverso. Nel raro caso in cui ti venga bloccato l\'accesso all\'app, cancella completamente i dati dell\'app e ripristina da un backup. Siamo molto spiacenti per qualsiasi inconveniente derivanti da questo. + Non preferito + %s +\nresiduo + Preferito + Nome e URL del repository + copiato! + Errore durante l\'accesso agli Appunti. Riprova. + Errore durante la copia. Copia logcat e contatta il supporto dell\'app. + OK + Disabilita ottimizzazione della batteria + Impossibile aprire le informazioni sull\'app CloudStream. + Media + Per garantire download e notifiche ininterrotti per i programmi TV sottoscritti, CloudStream necessita dell\'autorizzazione per l\'esecuzione in background. Premendo OK, verrai indirizzato alle informazioni sull\'app. Successivamente, scorri fino a \"Utilizzo della batteria\" e imposta l\'utilizzo della batteria su \"Senza restrizioni\". Tieni presente che questa autorizzazione non significa che CS3 scaricherà la batteria. Funzionerà in background solo quando necessario, ad esempio quando si ricevono notifiche o si scaricano video da estensioni ufficiali. Se scegli di annullare, puoi modificare questa impostazione più tardi in \"Impostazioni generali\". + L\'utilizzo della batteria dell\'app è già impostato su \"Senza restrizioni\" + Musica + Audiolibro diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index d5c2ad5e..da2952a0 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -271,7 +271,6 @@ ‪דפדפן צבע חלון הצג לוג - הוסף אפשרות מהירות בנגן לחץ פעמיים כדי להציץ לחץ פעמיים כדי לעצור התשתמש בבהירות המערכת בנגן האפליקציה במקום שכבת-על כהה diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 04e27c85..acb2cfc3 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -237,9 +237,9 @@ プレーヤーの字幕設定 Chromecastの字幕 Chromecastの字幕設定 - プレーヤーに速度オプションを追加します スワイプして探す 次のエピソードを自動再生する 現在のエピソードが終了したら次のエピソードを開始する 長押しするとデフォルトにリセットされます + ダウンロードを再開 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 0bf3bd9b..1a63050a 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -111,7 +111,6 @@ Chromecast 자막 Chromecast 자막 설정 배속 모드 - 플레이어에 속도 옵션을 추가합니다 스와이프하여 탐색 좌우로 스와이프하여 동영상 위치 제어하기 스와이프하여 설정 변경 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 54f9be82..f61bcfc0 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -67,7 +67,6 @@ Ištrinti Atšaukti Pradėti - Prideda greičio pasirinkti grotuve Filmukas Atsiuntimas atšauktas Išplėstinė paieška diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index ab8db2a9..49b333e3 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -124,7 +124,6 @@ Chromecast subtitri Chromecast subtitru iestāfijumi Eigengravy Mode - Pievieno atskaņošanas ātrumu playerim Novelc lai paradītu Novelc no māla lidz malai lai pozicionētu video Novēlu lai mainītu iestādījums diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 956f18e5..fe82a90b 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -90,7 +90,6 @@ Преводи Поставки на плеерот за преводи Режим на Eigengravy - Додава можност за брзина на снимка во плеерот Повлечете за да барате Повлечете од страна на страна за да ја контролирате вашата позиција во видеото Повлечете за да ги промените поставките @@ -336,7 +335,7 @@ Карактеристики Азиска драма Додатоци - Се прикажува копче на почетната страница што може да избере случаен филм или ТВ серија од почетната страница + Прикажи случајно копче на почетната страница и библиотеката Поддржано Сметки Вовед @@ -531,4 +530,65 @@ Смени провајдер Оди назад Актери: %s + %s додадени на фаворити + %s избришено од фаворити + Одбери опција за филтрирање на превземени плагини + Складиштето не е пронајдено, проверете го URL-то и пробајте VPN + Фаворити + Додадено во фаворити + Избриши од фаворити + Замени ги сите + Се чини дека потенцијално дупликат ставка веќе постои во вашата библиотека: „%s“. +\n +\nДали сепак сакате да ја додадете оваа ставка, да ја замените постојната или да го откажете дејството? + Во вашата библиотека се пронајдени потенцијални дупликати ставки: +\n +\n%s +\n +\nДали сепак сакате да ја додадете оваа ставка, да ги замените постоечките или да го откажете дејството? + Внеси ПИН за %s + ПИН-от мора да биде 4 карактери + Менаџирај кориснички сметки + Измени корисничка сметка + Логиран како %s + Прескокнете го изборот на корисничка сметка при стартување + Користете ја стандардната сметка + Ротирај + Прикажете копче за префрлување за ориентација на екранот + Веќе гласаше + Овде можете да го промените начинот на кој се подредуваат изворите. Ако видеото има повисок приоритет, ќе се појави повисоко во изборот на изворот. Збирот на приоритетот на изворот и приоритетот на квалитетот е приоритет на видеото. +\n +\nИзвор А: 3 +\nКвалитет Б: 7 +\nЌе има комбиниран приоритет на видеото од 10. +\n +\nЗАБЕЛЕШКА: Ако сумата е 10 или повеќе, играчот автоматски ќе го прескокне вчитувањето кога ќе се вчита таа врска! + Одбери корисничка сметка + Повторно вчитани линкови + Оневозможи + Внеси ПИН + Внеси моментален ПИН + ПИН + Заклучи профил + Неточен ПИН. Пробај повторно. + Исклучи претплата + Профил %d + Претплати се + Позадина на профил + Мобилен интернет + Поставете стандардно + Квалитети + Пронајдени потенцијални дупликати + Додади + Замени + Wi-Fi + Не се најдени плагини во складиштето + Користи + Измени + Профили + Помош + UI-то не можеше да се креира правилно, ова е ГОЛЕМ БАГ и треба веднаш да се пријави %s + Зачестеност на зачувување на бекап + Овозможете автоматско префрлување на ориентацијата на екранот врз основа на видео ориентација + Автоматска ротација diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 49c5b3ec..279f5511 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -3,14 +3,14 @@ വേഗം (%.2fx) റേറ്റിംഗ്: %.1f - പുതിയ അപ്ഡേറ്റ് + പുതിയ അപ്ഡേറ്റ്! \n%1$s -> %2$s - CloudStream + ക്ലൗഡ് സ്ട്രീം ഹോം തിരയുക ഡൗൺലോഡ്സ് സെറ്റിങ്‌സ് - തിരയുക + തിരയുക… ടാറ്റ ലഭ്യമല്ല കൂടുതൽ ഓപ്ഷൻസ് അടുത്ത എപ്പിസോഡ് @@ -84,8 +84,6 @@ കറുത്ത അതിർത്തി നീക്കംചെയ്യുക പ്ലേയർ സബ്‌ടൈറ്റിലുകളുടെ സെറ്റിങ്‌സ് - - വേഗം നിയന്ത്രിക്കാൻ ഓപ്ഷൻ ചേർക്കുക വീഡിയോപ്ലേയറിൽ സമയം നിയന്ത്രിക്കാൻ ഇടത്തോട്ടോ വലത്തോട്ടോ സ്വൈപ്പുചെയ്യുക @@ -169,11 +167,11 @@ ഔചിത്യ വീഡിയോ ക്വാളിറ്റി ചരിത്രം കണ്ടതാണെന്ന് അടയാളപ്പെടുത്തുക - %1$d%2$d - yg5t4r%dujyhtg - %d മണിക്കൂർ %d മിനിറ്റ് - %1$sghj%2$d - rtf:% + %d ദിവസങ്ങൾ %d മണിക്കൂർ %d മിനിറ്റ് + അധ്യായം%dൽ റിലീസ് ചെയ്യും + %1$d മണിക്കൂർ %2$d മിനിറ്റ് + %1$sഅധ്യാ%2$d + കാസ്റ്റ്:%s അക്കൗണ്ട് ഉണ്ടാക്കുക പുറത്ത്പോകുന്നതോടുകൂടി ആപ് അപ്ഡേറ്റ് ആവുന്നതാണ് ലൈബ്രറി തിരഞ്ഞെടുക്കുക @@ -181,8 +179,8 @@ ട്രെയിലർ പ്ലേ ചെയ്യുക ലൈവ് സ്ട്രീം പ്ലേ ചെയ്യുക ഫില്ലർ - %d min - ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് കളിക്കുക + %d മിനിറ്റ് + ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് പ്രവർത്തിപ്പിക്കുക അടുത്ത ക്രമരഹിതമായ എപ്പിസോഡ് പോസ്റ്റർ അപ്ഡേറ്റ് ആരംഭിച്ചു @@ -190,7 +188,7 @@ പോസ്റ്റർ ലോഡിംഗ് ഒഴിവാക്കുക തിരയുക %s… - %dm + %dമിനിറ്റ് മടങ്ങിപ്പോവുക പശ്ചാത്തല പ്രിവ്യൂ പോസ്റ്റർ @@ -204,8 +202,6 @@ പൊതു പട്ടിക CloudStream-ന് സ്ഥിരസ്ഥിതിയായി സൈറ്റുകളൊന്നും ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. നിങ്ങൾ റിപ്പോസിറ്ററികളിൽ നിന്ന് സൈറ്റുകൾ ഇൻസ്റ്റാൾ ചെയ്യേണ്ടതുണ്ട്. \n -\nസ്‌കൈ യുകെ ലിമിറ്റഡിലെ ഡോഗ്‌ഷിറ്റ് ആളുകളിൽ നിന്ന് DMCA നീക്കം ചെയ്‌തതിനാൽ 🤮 ഞങ്ങൾക്ക് ആപ്പിൽ റിപ്പോസിറ്ററി സൈറ്റ് ലിങ്ക് ചെയ്യാൻ കഴിയില്ല. -\n \nഞങ്ങളുടെ ഡിസ്കോർഡിൽ ചേരുക അല്ലെങ്കിൽ ഓൺലൈനിൽ തിരയുക. പകർത്തുക എല്ലാ സബ്‌ടൈറ്റിലുകളും വലിയക്ഷരമാക്കുക @@ -216,7 +212,7 @@ അക്കൗണ്ടുകൾ കൈകാര്യം ചെയ്യുക ഉടൻ വരുന്നു… പുനരാരംഭിക്കുമ്പോൾ പ്രയോഗിക്കുക - അക്കൗണ്ട് എഡിറ്റ് ചെയ്യുക + അക്കൗണ്ട് തിരുത്തുക തെറ്റായ പിൻ. ദയവായി വീണ്ടും ശ്രമിക്കുക. നിർത്തുക ട്രാക്കുകൾ @@ -247,4 +243,41 @@ ഉറവിട പിശക് നിലവിലെ പിൻ നൽകുക ഓഡിയോ ട്രാക്കുകൾ + ചിത്രം-ഇൻ-ചിത്രം + പുതുക്കിയത് (പഴയത് മുതൽ പുതിയത് വരെ) + റേറ്റിംഗ് (ഉയർന്നത് മുതൽ താഴ്ന്നത്) + പാരമ്പര്യം + വിൻഡോ നിറം + ക്ലിയർ + ലോഗ് + ശുപാർശകൾ കാണിക്കുക + %s ആയി ലോഗിൻ ചെയ്തു + ഇങ്ങനെ അടുക്കുക + അടുക്കുക + തിരുത്തുക + പുതുക്കിയത് (പുതിയത് മുതൽ പഴയത് വരെ) + NSFW + ആപ്പ് അപ്ഡേറ്റ് ഇൻസ്റ്റാൾ ചെയ്യുന്നു… + അപ്ഡേറ്റുകളും ഒപ്പം ബാക്കപ്പും + %s(അപ്രാപ്തമാക്കി) + റേറ്റിംഗ് (താഴ്ന്നത് മുതൽ ഉയർന്നത് വരെ) + വാചക നിറം + ആപ്പിൻ്റെ പുതിയ പതിപ്പ് ഇൻസ്റ്റാൾ ചെയ്യാനായില്ല + പാക്കേജ് ഇൻസ്റ്റാളർ + അക്ഷരമാലാക്രമം (A മുതൽ Z വരെ) + അക്ഷരമാലാക്രമം (Z മുതൽ A വരെ) + ഈ ലിസ്റ്റ് ശൂന്യമാണ്. മറ്റൊന്നിലേക്ക് മാറാൻ ശ്രമിക്കുക. + ചരിത്രം മായ്ക്കുക + ലോഗ്കാറ്റ് കാണിക്കുക 🐈 + നിങ്ങളുടെ ലൈബ്രറി ശൂന്യമാണ് :( +\nഒരു ലൈബ്രറി അക്കൗണ്ടിൽ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ നിങ്ങളുടെ പ്രാദേശിക ലൈബ്രറിയിലേക്ക് ഷോകൾ ചേർക്കുക. + വീഡിയോ + റിപ്പോസിറ്ററി നാമവും URL ഉം + പകർത്തി! + പുതിയ എപ്പിസോഡ് അറിയിപ്പ് + മറ്റ് വിപുലീകരണങ്ങളിൽ തിരയുക + ഉപശീർഷകം ക്രമീകരണങ്ങൾ + എഡ്ജ് തരം + ഔട്ട്ലൈൻ നിറം + പശ്ചാത്തല നിറം diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml new file mode 100644 index 00000000..b2c0356a --- /dev/null +++ b/app/src/main/res/values-mt/strings.xml @@ -0,0 +1,126 @@ + + + Preferenzi tas-sottotitli + Kulur tal-kitba + Kulur tat-Tieqa + Fittex bl-użu ta \'tipi + Importa fonts billi tpoġġihom ġo %s + Dan il-fornitur huwa torrent, VPN huwa rakkomandat + Atturi: %s + L-episodju %d ha johrog fil + %1$dh %2$dm + %dm + Kartellun + Kartellun + Kartellun tal-episodju + Kartellun Principali + Li jmiss bl\'addoċċ + Ibdel Il-fornitur + veloċità (%.2fx) + Klassifikazzjoni: %.1f + Aġġornament ġdid misjub! +\n%1$s -> %2$s + %d min + CloudStream + Ara bil-CloudStream + Dar + Fittex + Imnizzel + Preferenzi + Fittex… + Fittex%s… + Bla dejta + Iktar Preferenzi + L-episodju li\'jmiss + Ġeneri + Aqsam + Iftah fil-brawser + Brawser + Aqbez it-tagħbija + Tagħbija… + Jaraw + Stenna ftit + Lest + Imwaqqa + Pjana biex tara + Terġa\' tara + Ibda t-trejler + Ibda l-livestream + Stream Torrent + Sorsi + Erġa\' pprova l-konnessjoni… + Mur lura + Ibda l-episodju + Tniżżila ppawzata + Qed jinżlu + Imniżżel + Tniżżil ikkanċellat + Lest it-tniżżil + Beda l-aġġornament + Network stream + Tagħbija tal-Links falliet + Links regaw gew mogħbija + Ħażna Interna + Dub + Ibda + Info + Issettja l-istatus ta-rajtux + Applika + Ikkopja + Għalaq + Neħħi + Issevja + Isem tar-repożitorju u URL + Ikkupjat! + Notifika ta\' episodju ġdid + Fittex f\'estensjonijiet oħra + Uri r-rakkomandazzjonijiet + Veloċità tal-Plejer + Kulur tal-Kontorn + Kulur tal-Isfond + Tip tat-tarf + Elevazzjoni tas-Sottotitolu + Font + Daqs tal-font + Fittex bl-użu ta\' fornituri + %d Benenes mogħtija lil devs + Ebda Benenes mogħtija + Agħżel il-Lingwa Awtomatikament + Niżżel Lingwi + Lingwa tas-sottotitolu + Żomm biex tirrisettja għal default + Kompli Ara + Neħħi + Iktar informazzjoni + @string/home_play + Jista\' jkun hemm bżonn ta\' VPN biex dan il-fornitur jaħdem b\'mod korrett + Il-metadata mhix ipprovduta mis-sit, it-tagħbija tal-vidjo se tfalli jekk ma teżistix fuq is-sit. + Deskrizzjoni + Lebda Plot misjub + Lebda Deskrizzjoni misjuba + Uri Logcat 🐈 + ġurnal + Stampa f-istampa + Ikompli d-daqq fi player minjatura fuq apps oħra + %1$s Ep %2$d + %1$dd %2$dh %3$dm + Mur Lura + Ara l\'isfond + Mili + Ibda l-film + Sottotitli + Sut + Ibda l-fajl + Niżżel + Hassar il-fajl + Kompli Nizzel + Ieqaf Nizzel + Iddiżattiva r-rappurtar awtomatiku tal-bugs + Iktar Informazzjoni + Aħbi + Iffiltra l-Bookmarks + Beda t-tniżżil + Bookmarks + Neħħi + Falla t-tniżżil + diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index f60362ae..ef796f9f 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -263,7 +263,6 @@ အက်ပ်ထဲဝင်လိုက်သည့်နှင့်အက်ပ်အပ်ဒိတ်ကိုစစ်ဆေးပါ။ Chromecast စာတန်းထိုးများ Chromecast စာတန်းထိုး ပြုပြင်ရန် - ကြည့်ရှုမှုပုံစံထဲမှာအရိှန်ရွေးစရာတစ်ခုထည့်ရန် အသံအတိုးအကျယ်နှင့်အလင်းအမှောင်များကိုချိန်ညိှရန် ဘယ် သို့ ညာ ဘက်တွင် အပေါ်အောက်ဆွဲပါ ယခုကြည့်နေသောအပိုင်းပြီးပါကနောက်အပိုင်းကိုဖွင့်ပါ သင့်၏အပိုင်းကြည်ရှုမှုရောက်ရှိနေရာကိုအလိုအလျောက်သိမ်းဆည်းပါ diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 5b594334..fc537837 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -124,7 +124,6 @@ Chromecast Ondertitels Chromecast ondertitels instellingen Eigengravy Modus - Voegt een snelheidsoptie toe in de speler Swipe to seek Veeg naar links of rechts om de tijd in de videospeler te regelen Veeg om instellingen te wijzigen @@ -606,4 +605,7 @@ PIN invoeren PIN Huidige PIN invoeren + Link opnieuw geladen + Autoroteer + Roteer diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 49b559ed..724f4a63 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -98,7 +98,6 @@ Undertekster Innstillinger for spillerens teksting Eigengravy Modus - Legger til hastighetsalternativ i spilleren Sveip for å søke Sveip til venstre eller høyre for å kontrollere tiden i videospilleren Sveip for å endre innstillinger diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index cb7cf73d..c61f0104 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -52,7 +52,7 @@ Błąd przy pobieraniu Anulowano pobieranie Zakończono pobieranie - Odtwórz + Strumień sieciowy Błąd przy ładowaniu linków Pamięć wewnętrzna Dub @@ -112,18 +112,17 @@ Ustawienia napisów Napisy Chromecast Ustawienia napisów Chromecast - Tryb Eigengravy - Ustawienia prędkości - Przesuń aby przewinąć + Prędkość odtwarzania + Przesuń, aby przewinąć Przesuwaj w lewo lub prawo, aby kontrolować czas filmu - Przesuń aby zmienić ustawienia - Przesuwaj góra-dół z lewej lub prawej strony ekranu aby zmienić jasność czy głośność + Przesuń, aby zmienić ustawienia + Przesuwaj góra-dół z lewej lub prawej strony ekranu, aby zmienić jasność czy głośność Autoodtwarzanie następnego odcinka Rozpocznij następny odcinek po skończeniu bieżącego Czas przewinięcia przy podwójnym kliknięciu (w sekundach) - Podwójne kliknięcie aby przewinąć - Kliknij 2 razy z prawej lub lewej strony aby przewinąć - Kliknij dwukrotnie aby wstrzymać + Podwójne kliknięcie, aby przewinąć + Kliknij dwa razy z prawej lub lewej strony, aby przewinąć + Kliknij dwukrotnie, aby wstrzymać Kliknij dwukrotnie na środku, aby zatrzymać wideo Użyj jasności systemowej Użyj jasności systemowej w odtwarzaczu aplikacji zamiast ciemnej nakładki @@ -137,8 +136,8 @@ Brak uprawnień do pamięci, spróbuj ponownie. Błąd tworzenia kopii zapasowej %s Szukaj - Konta - Aktualizacje i kopia zapasowa + Konta i zabezpieczenia + Aktualizacje i kopie zapasowe Informacje Zaawansowane wyszukiwanie Szukaj z podziałem na źródła @@ -210,7 +209,7 @@ Torrenty Filmy dokumentalne OVA - Dramy azjatyckie + Dramaty azjatyckie Transmisje na żywo NSFW Inne @@ -220,7 +219,7 @@ Kreskówka Torrent Film dokumentalny - Drama azjatycka + Dramat azjatycki Transmisja na żywo NSFW Inne @@ -229,8 +228,8 @@ Błąd renderowania Nieoczekiwany błąd odtwarzacza Błąd pobierania, sprawdź uprawnienia aplikacji - odcinek Chromecast - mirror dla Chromecast + Odcinek Chromecast + Mirror dla Chromecast Odtwórz w aplikacji Odtwórz w %s Odtwórz w przeglądarce @@ -254,7 +253,7 @@ Pomiń tę aktualizację Aktualizacja Domyślna jakość (WiFi) - Maksymalna ilość znaków w tytule odtwarzacza + Maksymalna liczba znaków w tytule odtwarzacza Rozdzielczość odtwarzacza wideo Rozmiar bufora wideo Długość bufora wideo @@ -276,11 +275,11 @@ Zastrzeżenie Ogólne Przycisk do losowania - Pokaż przycisk do losowania na stronie głównej - Języki źródeł + Pokaż przycisk do losowania na stronie głównej i w bibliotece + Języki rozszerzeń Układ aplikacji - Preferowane media - Włącz NSFW w obsługiwanych źródłach + Preferowane multimedia + Włącz NSFW w obsługiwanych rozszerzeniach Kodowanie napisów Źródła Układ interfejsu @@ -365,14 +364,14 @@ Filtrowanie wg preferowanego języka Dodatki Zwiastun - Odsyłacz + Odsyłacz (opcjonalny) Następny Wyświetlaj filmy w tych językach Poprzedni - Pomiń setup + Pomiń konfigurację Dostosuj wygląd aplikacji do urządzenia Zgłaszanie błędów - Co chciałbyś obejrzeć + Co chcesz obejrzeć Gotowe Rozszerzenia Dodaj repozytorium @@ -397,8 +396,6 @@ Zaaktualizowano %d rozszerzeń CloudStream nie ma domyślnie zainstalowanych żadnych witryn. Musisz zainstalować witryny z repozytoriów. \n -\nZ powodu bezmyślnego usunięcia DMCA przez Sky UK Limited 🤮 nie możemy zamieścić linku do witryny z repozytoriami. -\n \nDołącz do naszego Discorda lub poszukaj online. Zobacz repozytoria społeczności Publiczna lista @@ -408,7 +405,7 @@ Ścieżki Ścieżki audio Ścieżki wideo - Zastosuj po ponownym uruchomieniu + Uruchom ponownie aplikację, aby zobaczyć zmiany. Tryb bezpieczny włączony Z powodu wystąpienia błędu wszystkie rozszerzenia zostały wyłączone, aby ułatwić wykrycie tego wadliwego. Wyświetl informacje o błędzie @@ -433,7 +430,7 @@ Wyczyść historię Historia Za dużo tekstu. Nie można skopiować do schowka. - Link do odtwarzania + https://example.com/example.mp4 Odtwórz w CloudStream Pomiń %s %1$dh %2$dm @@ -453,17 +450,17 @@ Podsumowanie Instalator APK Niektóre telefony nie obsługują nowego instalatora pakietów. Wypróbuj tryb legacy, jeśli aktualizacje nie zostaną zainstalowane. - password123 + hasło123 @string/ova - MojaFajnaWitryna - MyCoolUsername + NowaNazwaWitryny + Nazwa użytkownika 127.0.0.1 Tryb kompatybilności - przyklad.pl + https://example.com /\?\? Instalator pakietów @string/home_play - hello@world.com + witaj@poczta.pl @string/anime Opening Ending @@ -518,7 +515,7 @@ Rozpocznij Nie powiodło się Ukończone powodzeniem - Serwer pośredniczący raw.githubusercontent.com + Serwer proxy GitHuba Obejścia ISP Test dostawcy Zatrzymaj @@ -528,8 +525,8 @@ Zasubskrybowano %s Anulowano subskrypcję %s Został wydany odcinek %d! - Obchodzi blokadę GitHuba za pomocą jsDelivr. może spowodować opóźnienie aktualizacji o kilka dni. - Nie udało się połączyć z GitHub, włączono serwer pośredniczący jsDelivr… + Obchodzi blokadę surowych adresów URL GitHuba za pomocą jsDelivr. Może powodować opóźnienie aktualizacji o kilka dni. + Nie udało się połączyć z GitHubem. Włączono serwer pośredniczący jsDelivr… Domyślna jakość (dane mobilne) W tym miejscu można zmienić kolejność źródeł. Jeśli wideo ma wyższy priorytet, pojawi się wyżej w wyborze źródła. Priorytet wideo jest sumą priorytetu źródła i priorytetu jakości. \n @@ -562,7 +559,7 @@ \n%s \n \nCzy chcesz dodać ten element, zastąpić istniejące, czy anulować operację? - Wprowadź pin dla %s + Wprowadź PIN dla %s Częstotliwość tworzenia kopii zapasowych Znaleziono potencjalny duplikat Zablokuj profil @@ -592,4 +589,33 @@ Automatyczny obrót Obrót Włącz automatyczne przełączanie orientacji ekranu na podstawie orientacji filmu + Dodaje opcję prędkości w odtwarzaczu + Powiadomienie o nowym odcinku + Szukaj w innych rozszerzeniach + Pokaż rekomendacje + Przetestuj wszystkie rozszerzenia + Ten test jest przeznaczony wyłącznie dla programistów i nie weryfikuje ani nie zaprzecza działaniu żadnego rozszerzenia. + Zablokuj za pomocą biometrii + Uwierzytelnianie hasłem/kodem PIN + Ten ekran został zamknięty z powodu wielu nieudanych prób. Uruchom ponownie aplikację. + Odblokuj CloudStream + To urządzenie nie obsługuje uwierzytelniania biometrycznego + Odblokuj aplikację za pomocą odcisku palca, identyfikatora twarzy, kodu PIN, wzoru i hasła. + Kopia zapasowa Twoich danych CloudStream została teraz utworzona. Chociaż prawdopodobieństwo tego jest bardzo niskie, wszystkie urządzenia mogą zachowywać się inaczej. W rzadkich przypadkach, gdy dostęp do aplikacji zostanie zablokowany, należy całkowicie wyczyścić dane aplikacji i przywrócić je z kopii zapasowej. Bardzo nam przykro z powodu wszelkich niedogodności z tym związanych. + Usuń z ulubionych + %s +\npozostało + Dodaj do ulubionych + Nazwa repozytorium i adres URL + Błąd dostępu do schowka. Spróbuj ponownie. + skopiowano! + Błąd podczas kopiowania. Skopiuj logcat i skontaktuj się z pomocą techniczną aplikacji. + Wyłącz optymalizację akumulatora + Nie można otworzyć informacji o aplikacji CloudStream. + Muzyka + Audiobook + OK + Multimedia + Użycie akumulatora przez aplikację jest już ustawione na nieograniczone + Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach telewizyjnych, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do informacji o aplikacji. Tam przewiń do użycia akumulatora przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać akumulator. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach głównych. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 398a1aa3..06e2352c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -59,7 +59,7 @@ Falha no Download Download cancelado Download concluído - Stream + Transmitir Erro a Carregar Links Armazenamento Interno Dob @@ -118,8 +118,7 @@ Configurações de legendas do player Legendas do Chromecast Configurações de legendas do Chromecast - Modo Eigengravy - Acrescenta uma opção de velocidade no player + Velocidade de reprodução Deslize para andar Deslize para os lados para controlar a posição em um vídeo Deslize para mudar as configurações @@ -143,8 +142,8 @@ Permissão de armazenamento não encontrada, por favor tente novamente. Erro no backup de %s Procurar - Contas - Atualizações e backup + Contas e segurança + Atualizações e cópias de segurança Info Procura Avançada Mostra resultados separados por fornecedor @@ -273,10 +272,10 @@ Geral Botão Aleatório Mostrar botão aleatório na página inicial - Idioma dos fornecedores + Línguas de extensão Layout da App Mídia preferida - Ativar NSFW em fornecedores compatíveis + Ativar NSFW nas Extensões suportadas Codificação das legendas Fornecedores Layout @@ -289,11 +288,11 @@ Local do título do poster Coloca o título debaixo do poster senha123 - MeuNomeFixe + Nome de utilizador ola@mundo.com 127.0.0.1 - MeuSiteFixe - examplo.com + NovoNomedoSite + https://example.com Codigo da Língua (pt) Conta Sair @@ -374,9 +373,7 @@ Transferido: %d Desativado: %d Não transferido: %d - O CloudStream não possui sites instalados por padrão. Você precisa instalar os sites a partir de repositórios. -\n -\nDevido a uma restrição sem sentido de direitos autorais (DMCA) pela Sky UK Limited 🤮 não podemos vincular o site do repositório no aplicativo. + O CloudStream não tem sites instalados por padrão. É necessário instalar os sites a partir de repositórios. \n \nJunte-se ao nosso Discord ou pesquise online. Ver repositórios da comunidade @@ -443,7 +440,7 @@ Cam Abertura Selecionar Biblioteca - Ignora o bloqueio do GitHub usando jsDelivr. Pode fazer com que as actualizações sejam atrasadas por alguns dias. + Contorna o bloqueio de URLs raw do GitHub usando jsDelivr. Pode atrasar as atualizações por uns dias. VLC Todas as linguagens Atualizado (Novo para Antigo) @@ -461,10 +458,10 @@ Inscrição cancelada em %s Final misto Avaliações (Decrescente) - Aplicar ao reiniciar - Referente + Reinicie a aplicação para ver as alterações. + Referenciador (opcional) Player oculto - Quantidade de Busca - raw.githubusercontent.com Proxy + Proxy do GitHub Blu-ray Aparência 1000 ms @@ -476,7 +473,7 @@ Ver informações sobre falha Aplicativo não encontrado Reverter - Link para transmitir + https://example.com/example.mp4 Plugins baixados %d plugins atualizados Pular %s @@ -551,4 +548,71 @@ Não foram encontrados plugins no repositório Repositório não encontrado, verifique o URL e tente a VPN Você já votou + Cancelar Inscrição + Subscrever + Favoritos + A recarregar links + Frequência de Backup + %s removido dos favoritos + Adicionar aos favoritos + Possível duplicata encontrada + %s adicionado aos favoritos + Remover dos favoritos + Substituir + Substituir Tudo + Insira o PIN atual + PIN + PIN incorreto. Por favor, tente novamente. + O PIN deve ter 4 caracteres + Editar conta + Conectado como %s + Ignorar a seleção da conta na inicialização + Girar + Digite o PIN para %s + Bloquear Perfil + Selecione uma conta + Gerenciar contas + Usar conta padrão + Potenciais itens duplicados foram encontrados na sua biblioteca: +\n +\n%s +\n +\nDeseja adicionar esse item mesmo assim, subtituir os existentes, ou cancelar a ação? + Parece que já existe um item potencialmente duplicado na sua biblioteca: \'%s.\' +\n +\nDeseja adicionar esse item mesmo assim, subtituir o existente, ou cancelar a ação? + Mostrar recomendações + Adiciona uma opção de velocidade no leitor + Testar todas as extensões + Este teste destina-se apenas a programadores e não verifica ou nega o funcionamento de qualquer extensão. + Adicionar + Insira o PIN + Notificação de novo episódio + Procurar noutras extensões + Apresentar um botão de alternância para a orientação do ecrã + Ativar a mudança automática da orientação do ecrã com base na orientação do vídeo + Rotação automática + Nome do repositório e URL + Favorito + Desfavorito + Bloqueio com biometria + copiado! + %s +\nrestante + Erro ao aceder à área de transferência, tente novamente. + Erro ao copiar, copie o logcat e contacte o suporte da aplicação. + Desbloquear o CloudStream + Autenticação por palavra-passe/PIN + A autenticação biométrica não é suportada neste dispositivo + Desbloqueie a aplicação com impressão digital, ID facial, PIN, padrão e palavra-passe. + Este ecrã foi encerrado devido a várias tentativas falhadas. Reinicie a aplicação. + Os dados do seu CloudStream já foram copiados. Embora a possibilidade de isto acontecer ser muito baixa, todos os dispositivos podem comportar-se de forma diferente. No caso raro de ficar impedido de aceder à aplicação, limpe completamente os dados da aplicação e restaure a partir de uma cópia de segurança. Lamentamos qualquer incómodo causado por esta situação. + OK + A utilização da bateria da aplicação já está definida como sem restrições + Não é possível abrir a informação da aplicação CloudStream. + Música + Livro Aúdio + Multimédia + Desativar a otimização da bateria + Para garantir descarregamentos ininterruptos e notificações de programas de TV subscritos, o CloudStream precisa de permissão para ser executado em segundo plano. Ao premir OK, será direcionado para informações da aplicação. Aí, desloque-se para utilização da bateria da aplicação e defina a utilização da bateria para sem restrições. Tenha em atenção que esta permissão não significa que o CS3 irá esgotar a sua bateria. Este só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se optar por cancelar, pode ajustar esta definição mais tarde em definições gerais. diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 72f16012..5de97c7d 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -82,7 +82,6 @@ ahhhaauugghh oha ooh ouuhhh oooohhahhh ouuhhh haaahhh ahoouuh - haaoooohhaaahhuoha ouuhhh ah oouuh ohoohaaahhu ohahaaaauugghh ahooo aaahhu aaaghh aaaghhohahooooo ouuhhh oouuh ooo-ahahahooo-ahah ohaaaaaghh aaaaaahhaaahhuoouuhaaaaa aahooo diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 1a084e02..d7da44b4 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -120,8 +120,7 @@ Setări de subtitrare Subtitrări Chromecast Setări pentru subtitrare Chromecast - Modul Eigengravy - Adăugați opțiunea de viteză în player + viteza de redare Derulați spre înainte/înapoi Derulați dintr-o parte în alta pentru a controla timpul de difuzare a videoclipului Derulați pentru a modifica setările @@ -273,7 +272,7 @@ Orice probleme legale privind conținutul acestei aplicații ar trebui să fie rezolvate cu furnizorii și gazdele actuale de fișiere, întrucât noi nu suntem afiliați cu aceștia. În caz de încălcare a drepturilor de autor, vă rugăm să contactați direct părțile responsabile sau site-urile de streaming. Aplicația este destinată exclusiv utilizării educaționale și personale. CloudStream 3 nu găzduiește niciun fel de conținut în aplicație și nu are niciun control asupra conținutului media care este pus sau retras. CloudStream 3 funcționează ca orice alt motor de căutare, cum ar fi Google. CloudStream 3 nu găzduiește, nu încarcă și nu gestionează niciun videoclip, film sau conținut. Pur și simplu navighează, adună și afișează linkuri într-o interfață convenabilă și ușor de utilizat. Pur și simplu, acesta extrage paginile web ale unor terțe părți care sunt accesibile publicului prin intermediul oricărui browser web obișnuit. Este responsabilitatea utilizatorului de a evita orice acțiune care ar putea încălca legile care guvernează locația sa. Utilizați CloudStream 3 pe propria răspundere. General Aleatoriu - Afișați butonul Aleatoriu pe pagina de start + Afișați butonul aleatoriu pe pagina de start și în bibliotecă Limba furnizorului Aplicație de prezentare Media preferată @@ -289,11 +288,11 @@ Locația titlului de pe poster parola123 - David Popovici + Numele utilizatorului/utilizatoarei davidpopovici@gmail.com 127.0.0.1 - David Popovici - davidpopovici.com + NouNumeSite + https://exemplu.com Cod limbă (RO) %1$s %2$s Cont @@ -309,7 +308,7 @@ %d / 10 /\?\? /%d - %s autentificat + %s autentificat/ă Nu s-a putut autentifica la %s Nu există @@ -396,11 +395,11 @@ NSFW %1$d-%2$d Player Afișat - Căutați Suma - Player Ascuns - Căutați Suma + Player Ascuns/ă - Căutați Suma Livestream-uri NSFW Eșuat - Cantitatea de căutare utilizată atunci când playerul este vizibil + Suma căutată și utilizată atunci când player-ul este vizibil/ă Livestream Cantitatea de căutare utilizată atunci când playerul este ascuns Calitatea preferată (Date Mobile) @@ -484,11 +483,9 @@ Toate extensiile au fost dezactivate din cauza unei defecțiuni pentru a vă ajuta să o găsiți pe cea care cauzează probleme. Se descarcă actualizarea aplicației… Browser web - CloudStream nu are niciun site instalat în mod implicit. Trebuie să instalați site-urile din depozite. + CloudStream nu are niciun site instalat din start. Trebuie să instalați site-urile din depozite. \n -\nDin cauza unui DMCA takedown fără creier de către Sky UK Limited 🤮 nu putem lega site-ul de depozit în aplicație. -\n -\nAlăturați-vă Discordului nostru sau căutați online. +\nAlăturați-vă Discord-ului nostru sau căutați online. A început să descarce %1$d %2$s… Mod sigur pornit Fișier Mod Sigur găsit! @@ -513,8 +510,8 @@ Repornește Activează NSFW la furnizori suportate Nu s-a putut ajunge la GitHub. Se activează proxy-ul jsDelivr… - Proxy raw.githubusercontent.com - Depășește blocarea GitHub folosind jsdelivr, poate cauza întârzieri de câteva zile la actualizări. + Proxy GitHub + Depășește blocarea GitHub folosind jsDelivr. Poate cauza întârzieri de câteva zile la actualizări. Următorul Toate %s deja descărcate S-a descărcat: %d @@ -528,7 +525,7 @@ Moştenit Test de furnizor Furnizori - Link către stream + https://example.com/example.mp4 Acest lucru va șterge, de asemenea, toate plugin-urile din depozit Se instalează actualizarea aplicației… S-a descărcat %1$d %2$s @@ -569,4 +566,31 @@ UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s Selectați modul de filtrare a descărcării plugin-urilor Ați votat deja + Elemente potențial duplicate au fost găsite în biblioteca ta: +\n +\n%s +\n +\nÎn ciuda acestui fapt, ai dori să adaugi acest alement, să le înlocuiești pe cele existente, sau să anulezi acțiunea? + %s a fost adăugat la favoriți/te + %s a fost eliminat din favoriți/te + Adaugă la favoriți/te + Elimină din favoriți/te + Se pare că un element potențial duplicat deja există în biblioteca ta: \'%s.\' +\n +\nÎn ciuda aceasta, ai dori să adaugi acest element, să îl înlocuiești pe cel existent, sau să anulezi acțiunea? + Introduce PIN-ul pentru %s + Introduce PIN-ul actual + Introduce PIN-ul + Blochează Profilul + Dezabonează-te + Abonează-te + Adaugă + Înlocuiește + Înlocuiește tot + Posibil Duplicat Găsit + Notificare episod nou + Arată sugestii + Adaugă o opțiune de viteză la player + Favoriți/te + Frecvența de backup diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 82bd3581..cf456f56 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -15,7 +15,7 @@ Отмена Все Пауза - Актёрский состав: %s + В ролях: %s Название источника Войти Нет @@ -39,7 +39,7 @@ Заполнитель CloudStream Убирать - %1$s Серия %2$d + %1$s Ep %2$d Смотреть с CloudStream Главная Поиск @@ -71,8 +71,8 @@ Скачано Скачивание Скачать остановлена - Скачать начатый - Скачать отменённый + Загрузка началась + Загрузка отменена Скачать выполнено Инфо Обновление началось @@ -99,17 +99,17 @@ Цвет фона Цвет окна Тип края - Субтитр подъём - Поиск с использованием поставщиков + Подъем субтитров + Поиск с использованием провайдеров Поиск с использованием типов - %d Бенены данность на разрабы - Бенены не дают + %d бенен(а/ов) выдано разрабам + Бенены не выданы Автовыбор языка Скачать языки Язык субтитров Удерживайте, чтобы сбросить по умолчанию Ошибка загрузки ссылок - Поток + Сетевой Поток Шрифт Размер шрифта Удалить файл @@ -132,13 +132,12 @@ Картинка в картинке Продолжение воспроизведения в миниатюрном проигрывателе поверх других приложений Кнопка изменения размера проигрывателя - Удалите черные границы + Убрать черные границы Субтитры Настройки субтитров проигрывателя Субтитры Chromecast Настройки субтитров Chromecast - Режим Eigengravy - Добавляет опцию скорости в проигрывателе + Скорость воспроизведения Проведите пальцем для поиска Проведите пальцем для изменения настроек Проведите вверх или вниз по левой или правой стороне, чтобы изменить яркость или громкость @@ -149,14 +148,14 @@ Загружена резервная копия Не удалось восстановить данные из %s Отсутствует разрешение на хранение. Пожалуйста попробуйте снова. - Аккаунты - Обновления и резервное + Аккаунты и Безопасность + Обновления и Резервное копирование Информация Расширенный поиск Показывать трейлеры - Скрыть выбранное качество видео в результатах поиска - Автоматическое обновление плагинов - Автоматическая загрузка плагинов + Скрыть выбранные форматы видео в результатах поиска + Автообновление плагинов + Автозагрузка плагинов Показать обновления приложения Автоматически проверять обновления при старте приложения. Обновится до пре-релиза @@ -197,10 +196,10 @@ Сезон Аниме приложение от тех же разработчиков Автоматически загружать еще не установленные плагины из добавленных репозиториев. - Присоединится в Discord - Бесплатно + Присоединиться к Discord-серверу + Свободно %dm - %1$d ч. %2$d мин. + %1$dч %2$dм Фильмы Мультфильм Сериалы @@ -218,7 +217,7 @@ Азиатская драма Общие Провайдеры - Макет + Расстановка Расширения Плеер Резервное копирование данных @@ -246,25 +245,25 @@ Оговорка Синхронизация субтитров Добавить клон существующего сайта с другим URL-адресом - Используется для обхода блокировок интернет провайдера + Используется для обхода блокировок интернет-провайдера Путь скачивания Давал бенен Обновить Основной цвет - Языки поставщиков + Языки провайдеров Название репозитория Очистить историю - Referer + Реферер (необязательно) Дайте бенен разрабам Ссылки - Макет - Макет приложения + Расстановка + Расстановка приложения Тема приложения Добавить репозиторий Убрать отметку Вы уверены, что хотите выйти\? Плагин скачан - Плагин удалён + Плагин удален Описание Версия Статус @@ -276,7 +275,7 @@ Пропустить %s Концовка Используйте яркость системы в проигрывателе приложения вместо темного наложения - Обновить состояние хода просмотра + Обновлять прогресс просмотра Данные сохранены Показывает результаты поиска, разделенные по провайдеру Поиск предварительных обновлений вместо полных выпусков @@ -302,7 +301,7 @@ Больше не показывать Пропустить это обновление URL сервера NGINX - Создать учётную запись + Создать аккаунт Добавить слежение Добавлено %s Синхронизировать @@ -325,7 +324,7 @@ Отчистить кеш видео и изображений Вызывает сбои, если установлено слишком высокое значение на устройствах с небольшим объемом памяти, таких как Android TV. Вызывает проблемы, если установлено слишком высокое значение на устройствах с небольшим объемом памяти, таких как Android TV. - Легкая новелла от тех же разработчиков + Легкое приложение для новелл от тех же разработчиков Язык Плейлист HLS Сначала установить расширение @@ -344,13 +343,13 @@ Эмулятор Под плакатом parol123 - МоёИмяПользователя - Сменить учётную запись - Добавить учётную запись - МойКрутойСайт - example.com + Юзернейм + Сменить аккаунт + Добавить аккаунт + ИмяСайта + https://example.com Код языка (ru) - учётная запись + аккаунт Автоматически 127.0.0.1 Обновления приложения @@ -423,9 +422,6 @@ %s (отключено) Далее В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев. -\n -\nИз-за безмозглой жалобы DMCA от Sky UK Limited 🤮 мы не можем привязать сайт репозитория в приложении. -\n \nПрисоединяйтесь к нашему Discord-серверу или найдите в интернете. Недопустимые данные Разрешение и название @@ -461,7 +457,7 @@ Загрузить из интернета Загрузка обновления приложения… Недопустимый URL - Применить при перезапуске + Перезапустите приложение, чтобы увидеть изменения. Отчеты ошибках Что вы хотите увидеть Смотрите видео на этих языках @@ -469,8 +465,8 @@ Изображение постера Пакетная загрузка Скачайте список сайтов, который вы хотите использовать - Отображать Аниме с Дубляжом/Субтитрами - Включить NSFW на поддерживаемых провайдерах + Отображать аниме с дубляжом/субтитрами + Включить NSFW у поддерживаемых расширений (провайдеров) Удалять скрытые субтитры из субтитров Дополнительно Изменить вид интерфейса, чтобы соответствовать устройству @@ -491,11 +487,11 @@ Показывать всплывающие окна для пропуска вступления/заключения Фильтровать по предпочитаемому языку медиа Неверный ID - Ссылка на стрим - Отображать рандомную кнопку на Главной странице + https://example.com/example.mp4 + Отображать рандомную кнопку в библиотеке и главной странице Рандомная кнопка Legacy (старый) - Веб видеокаст + Web Video Cast Не отправляет данные Перезагрузить ссылки Предпочтительные медиа @@ -520,12 +516,12 @@ Вернуться Подписался на %s Предпочтительное качество видео (Мобильный интернет) - raw.githubusercontent.com Прокси-сервер - Не удалось подключиться к GitHub. Включаем проксирование через jsdelivr… + GitHub прокси + Не удалось подключиться к GitHub. Включаем проксирование через jsDelivr… Эпизод %d выпущен! Обходы провайдера Обновление подписки на фильмы и сериалы - Обход ограничения доступа к GitHub с помощью jsDelivr может задержать обновления на несколько дней. + Обход ограничения доступа к raw github URLs с помощью jsDelivr. Обновления могут задержаться на несколько дней. Подписные Отказались от подписки на %s Мобильный интернет @@ -552,4 +548,72 @@ \n \nПРИМЕЧАНИЕ. Если сумма равна 10 или более, плеер автоматически пропустит загрузку при загрузке этой ссылки! Ссылки перезагружены + Выбрать аккаунт + %s убран из избранных + Введите пин-код + Защитить профиль + Пин-код + Найден возможный дубликат + Добавить + Неверный пин-код. Попробуйте снова. + Пин-код должен состоять из 4 цифр + Изменить аккаунт + Управление аккаунтами + Вы вошли как %s + Пропускать выбор аккаунта при запуске + Повернуть + Показывать переключатель ориентации экрана + Заменить все + Потенциальные дубликаты элементов были найдены в вашей библиотеке: +\n +\n\'%s.\' +\n +\nВы хотите добавить этот элемент, заменить существующие или отменить это действие? + Убрать из избранных + Добавить в избранное + Включить автоматическую смену ориентации экрана на основе ориентации видео + Автоповорот + rotate_video_key + Использовать аккаунт по умолчанию + Отписаться + Заменить + Введите текущий пин-код + auto_rotate_video_key + Избранное + %s добавлен в избранное + Введите пин-код от %s + Подписаться + Частота резервного копирования + Похоже, что потенциальный дубликат элемента уже существует в вашей библиотеке: \'%s.\' +\n +\nВы хотите добавить этот элемент, заменить существующий или отменить это действие? + Оповещение о выходе нового эпизода + Искать в других расширениях + Показать рекомендации + Этот тест предназначен только для разработчиков и не подтверждает или не опровергает работоспособность провайдеров. + Добавление настроек скорости в плеер + Протестировать всех провайдеров + скопировано! + ОК + Имя репозитория и URL адрес + Ошибка доступа к буферу обмена, пожалуйста, попробуйте ещё раз + Ошибка при копировании, пожалуйста, скопируйте лог и свяжитесь с технической поддержкой. + Нелюбимое + Разблокировать CloudStream + Любимое + Использование батареи приложением уже настроено на неограниченное + Не удается открыть информацию о приложении CloudStream. + Заблокировать биометрией + Музыка + Аудиокнига + Медиа + Разблокируйте приложение с помощью отпечатка пальца, Face ID, PIN-кода, шаблона и пароля. + %s +\nосталось + Отключить оптимизацию батареи + Аутентификация по паролю/PIN-коду + Биометрическая аутентификация на этом устройстве не поддерживается + Этот экран был закрыт из-за нескольких неудачных попыток. Пожалуйста, перезапустите приложение. + Ваши данные в CloudStream были скопированы. Хотя вероятность этого очень мала, все устройства могут вести себя по-разному. В редких случаях, когда доступ к приложению заблокирован, полностью удалите данные приложения и восстановите их из резервной копии. Мы приносим свои извинения за любые неудобства, связанные с этим. + Чтобы обеспечить бесперебойную загрузку и получение уведомлений о телепередачах, на которые вы подписаны, CloudStream необходимо разрешение на запуск в фоновом режиме. Нажав OK, вы перейдете к информации о приложении. Там перейдите к разделу 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и установите значение \"Использование батареи\" 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Пожалуйста, обратите внимание, что это разрешение не означает, что CS3 разрядит вашу батарею. Он будет работать в фоновом режиме только при необходимости, например, при получении уведомлений или загрузке видео с официальных расширений. Если вы решите отменить, вы можете изменить эту настройку позже в 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 490f74ac..ebaaa2ae 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -149,7 +149,6 @@ Použiť systémový jas Obnoviť dáta zo zálohy Dvojitým ťuknutím pretočiť - Pridá možnosť rýchlosti do prehrávača Automaticky sťahovať doplnky Pripojte sa na Discord Neodosiela žiadne dáta diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index a63d44fe..7b0d2870 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -250,7 +250,6 @@ Qoraal-hoosaadka Habaynta Qrl-hoosaadka Koromakaastiga Habka Xawaare-kordhinta - Waxaad kordhin karta xawaaraha aad ku daawanayso Midig iyo bidix u jiid si aad marba dhinac ugu dhaafiso muqaalka Laba jeer taabo midig ama bidix si aad u dhaafiso ama ku ceshato Laba jeer taabo si aad u dhaafiso diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 0f6f37cd..76508c43 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -83,8 +83,7 @@ Ta bort de svarta kanterna Undertexter Inställningar för undertexter - Eigengrau Läge - Lägger till hastighetsalternativet i spelaren + Uppspelningshastighet Svep för att strya tiden Svep åt vänster eller höger för att styra tiden i videospelaren Svep för att ändra inställningar @@ -138,7 +137,7 @@ Inga undertexter Standard Tillgängligt - Använtt + Använt App Filmer Tv Serier @@ -180,9 +179,9 @@ Använd system ljusstyrka Använder systemets ljusstyrka instället för en svart överlaga Visa filler avsnitt för anime - Visa Dubbad/Subbad anime - Anpassad till skärmstorlek - Leverantörspråk + Visa dubbad/undertextad anime + Anpassa till skärmstorlek + Tilläggsspråk Utsträckt Inzoomad Oförväntat uppspelingsfel @@ -205,7 +204,7 @@ Logga ut konto Nerladdningsplats - Kollar om + Tittar på nytt Automatisk DNS över HTTPS " " @@ -217,7 +216,7 @@ Chromecast-undertexter Dubbeltryck i mitten för att pausa Återställ data från backup - Konton + Konton och säkerhet Uppdateringar och backup Automatiska pluginuppdateringar %1$dd %2$dh %3$dm @@ -294,7 +293,7 @@ UHD Upplösning och titel Fel - Referer + Referent (valfritt) Nästa 1000 ms Lägga till en klon av en befintlig webbplats med en annan webbadress @@ -327,7 +326,7 @@ Trailer Applayout Funktioner - exempel.se + https://example.com Språkkod (en) Videocache på disken Rensa video- och bildcache @@ -348,7 +347,7 @@ Cache Säkerhetskopiering Undertexter - Aktivera NSFW på sidor som stöds + Aktivera NSFW på tillägg som stöds Undertextkodning lösenord123 Fördröjning av undertexter @@ -367,7 +366,7 @@ Titta på videor på dessa språk Föregående Spår - Uppdaterar + Uppdatering påbörjad Logg Videospelarens hoppsträcka (Sekunder) Ändra status @@ -379,12 +378,12 @@ Slumpknapp Visa sub Kunde inte öppna appen - raw.githubusercontent.com Proxy + GitHub Proxy Webbläsarens videospelare Installerar uppdatering till appen… Kunde inte nå GitHub, sätter på jsDelivr proxy… Leverantörer - MinTrevligaWebbsida + Nytt webbplatsnamn Ta bort reklam från undertexter VLC Alla språk @@ -432,7 +431,7 @@ Profiler Hjälp Kvalitet - Ström + Nätverks Stream Databasens namn All %s har redan laddats ner Ladda ner alla tillägg från den här databasen? @@ -480,7 +479,7 @@ Loggat in som %s Hoppa över val av konto vid start auto_rotera_video_nyckel - Förbi passera blockering av GitHub genom att använda jsDelivr. Kan göra att uppdateringar försenas med några dagar. + Gå förbi blockering av rå GitHub-URL:er med jsDelivr. Kan göra att uppdateringar försenas med några dagar. Funktion Önskad media Lägg titeln under affischen @@ -499,9 +498,7 @@ Visa community databaser Blandad inledning Skippa %s - CloudStream har inga webbplatser installerade som standard. Du måste installera webbplatserna från databaser. -\n -\nPå grund av en hjärnlös DMCA-borttagning av Sky UK Limited kan vi inte länka databaser på denna applikation. + CloudStream har inga webbplatser installerade som standard. Du måste installera webbplatser från arkiv. \n \nGå med i vår Discord eller sök online. Välj bibliotek @@ -551,7 +548,7 @@ Gester %s autentiserad Statister - Länka till strömmen + https://example.com/example.mp4 Radera databasen Profil bakgrund WP @@ -591,5 +588,20 @@ \nKommer att ha en kombinerad videoprioritet på 10. \n \nOBS: Om summan är 10 eller mer kommer spelaren automatiskt att hoppa över laddningen när den länken laddas! - Titel kopierad! + Meddelande om nytt avsnitt + Sök i andra tillägg + Visa rekommendationer + Lägger till ett hastighetsalternativ i spelaren + Testa alla tillägg + Detta test är endast avsett för utvecklare och verifierar eller förnekar inte att någon tillägg fungerar. + Lås med biometrik + Lösenord/PIN autentisering + Lås upp appen med Fingerprint, Face ID, PIN, mönster eller lösenord. + Lås upp CloudStream + Biometrisk autentisering stöds inte på den här enheten + Detta fönster stängs efter några misslyckade försök. Du måste starta om appen. + Favorit + Ta bort från favoriter + %s +\nkvarstår diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 234207b9..e981d05a 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -99,7 +99,6 @@ Logcat 🐈 காட்டு பிற பயன்பாடுகளுக்கு மேல் மினியேச்சர் பிளேயரில் பிளேபேக் தொடர்கிறது வசன வரிகள் - பிளேயரில் வேக விருப்பத்தை சேர்க்க வீடியோ பிளேயரில் நேரத்தைக் கட்டுப்படுத்த இடது அல்லது வலதுபுறம் ஸ்வைப் செய்யவும் பிரகாசம் அல்லது ஒலியளவை மாற்ற இடது அல்லது வலது பக்கத்தில் ஸ்வைப் செய்யவும் இடைநிறுத்துவதற்கு இருமுறை தட்டவும் diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index fc3946bb..b4308eb7 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -101,7 +101,6 @@ Subtitles Player subtitles settings Eigengravy Mode - Magdagdag ng \'speed option\' sa \'player\' Swipe to seek Swipe pakanan o pakaliwa upang makontrol ang oras ng pinapanood Swipe to change settings diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e8b7881a..c3e5959a 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -76,7 +76,7 @@ İndirme iptal edildi İndirme bitti %s - %s - Yayınla + Ağ akışı Bağlantılar yüklenirken hata oluştu Dahili depolama Dublajlı @@ -137,8 +137,7 @@ Oynatıcı alt yazı ayarları Chromecast alt yazıları Chromecast alt yazı ayarları - Eigengrau modu - Oynatıcıya hız seçeneği ekler + Oynatma hızı Atlamak için kaydır Zamanı ayarlamak için yanlardan kaydır Ayarları değiştirmek için kaydır @@ -162,8 +161,8 @@ Depolama izinleri eksik. Lütfen tekrar deneyin. %s yedeklenirken hata Ara - Hesaplar - Güncellemeler ve yedekleme + Hesaplar ve Güvenlik + Güncellemeler ve Yedekleme Bilgi Gelişmiş arama Arama sonuçlarını sağlayıcıya göre ayırır @@ -309,10 +308,10 @@ Genel Rastgele İçerik Anasayfada ve Kütüphanede rastgele düğmesini göster - Sağlayıcı dilleri + Uzantı dilleri Uygulama düzeni Tercih edilen medya - Desteklenen sağlayıcılarda +18 içeriği etkinleştir + Desteklenen Uzantılarda NSFW\'yi etkinleştirin Alt yazı kodlaması Sağlayıcılar Düzen @@ -330,11 +329,11 @@ opensubtitles_key nginx_key şifre123 - HavalıKullanıcıAdı + Kullanıcı Adı hello@world.com 127.0.0.1 - MyCoolSite - ornek.com + YeniSiteAdı + https://ornek.com Dil kodu (tr) ?attr/colorPrimary - @android:dimen/dialog_min_width_major - @android:dimen/dialog_min_width_minor + @dimen/abc_dialog_min_width_major + @dimen/abc_dialog_min_width_minor @drawable/dialog__window_background diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml index b0821494..88dc82fe 100644 --- a/app/src/main/res/xml/settings_account.xml +++ b/app/src/main/res/xml/settings_account.xml @@ -9,6 +9,9 @@ + diff --git a/app/src/main/res/xml/settings_general.xml b/app/src/main/res/xml/settings_general.xml index c4900bca..853bbda1 100644 --- a/app/src/main/res/xml/settings_general.xml +++ b/app/src/main/res/xml/settings_general.xml @@ -6,10 +6,7 @@ android:title="@string/app_language" android:icon="@drawable/ic_baseline_language_24" /> - + + android:title="@string/title_downloads"> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index 82505511..5d5b11d0 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -101,7 +101,8 @@ + android:title="@string/pref_category_gestures" + app:key="@string/pref_category_gestures_key"> + android:title="@string/pref_category_android_tv" + android:key="@string/pref_category_android_tv_key" > ("clean") { - delete(rootProject.layout.buildDirectory) -} +//tasks.register("clean") { +// delete(rootProject.layout.buildDirectory) +//} diff --git a/fastlane/metadata/android/af/short_description.txt b/fastlane/metadata/android/af/short_description.txt index 59cfb7e5..aaba87f3 100644 --- a/fastlane/metadata/android/af/short_description.txt +++ b/fastlane/metadata/android/af/short_description.txt @@ -1 +1 @@ -Laai af of stroom flieks, TV-reekse en anime. +Laai af en stroom flieks, TV-reekse en anime. diff --git a/fastlane/metadata/android/hi-IN/full_description.txt b/fastlane/metadata/android/hi-IN/full_description.txt index 89927810..465db20e 100644 --- a/fastlane/metadata/android/hi-IN/full_description.txt +++ b/fastlane/metadata/android/hi-IN/full_description.txt @@ -1,10 +1,10 @@ -क्लाउडस्ट्रीम-3 आपको मूवी, टीवी-सीरीज़ और एनीमे स्ट्रीम और डाउनलोड करने की सुविधा देता है। +क्लाउडस्ट्रीम-3 आपको फ़िल्में, टीवी शृंखलाएँ और एनिमे स्ट्रीम एवं डाउनलोड करने की सुविधा देता है। -ऐप बिना किसी विज्ञापन और एनालिटिक्स के आता है और -कई ट्रेलर और मूवी साइटों और अन्य का समर्थन करता है जैसे कि- +ऐप विज्ञापन और विश्लेषिकी से मुक्त है एवं +अनेकों ट्रेलर और मूवी साइटों के समर्थन जैसी सुविधाएँ देता है, जैसे कि – -बुकमार्क +पृष्ठचिह्न उपशीर्षक डाउनलोड -क्रोमकास्ट का समर्थन +क्रोमकास्ट समर्थन diff --git a/fastlane/metadata/android/hi-IN/short_description.txt b/fastlane/metadata/android/hi-IN/short_description.txt index a8a1eb4d..5a0f2d12 100644 --- a/fastlane/metadata/android/hi-IN/short_description.txt +++ b/fastlane/metadata/android/hi-IN/short_description.txt @@ -1 +1 @@ -फिल्में, टीवी-सीरीज़ और एनीमे स्ट्रीम करें और डाउनलोड करें। +फ़िल्में, टीवी धारावाहिक और एनिमे स्ट्रीम और डाउनलोड करें। diff --git a/fastlane/metadata/android/hu-HU/changelogs/2.txt b/fastlane/metadata/android/hu-HU/changelogs/2.txt new file mode 100644 index 00000000..012882d2 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/2.txt @@ -0,0 +1 @@ +- Változáslista hozzáadva! diff --git a/fastlane/metadata/android/hu-HU/full_description.txt b/fastlane/metadata/android/hu-HU/full_description.txt new file mode 100644 index 00000000..24deb97a --- /dev/null +++ b/fastlane/metadata/android/hu-HU/full_description.txt @@ -0,0 +1,10 @@ +A CloudStream-3 segítségével streamelhet vagy letölthet filmeket, TV sorozatokat vagy animéket.. + +Az app nem tartalmaz semmilyen reklámot vagy követést, +és támogat többféle film és előzetes oldalt, és sok minden mást, pl. + +Könyvjelzőket + +Felirat Letöltést + +Chromecast támogatást diff --git a/fastlane/metadata/android/hu-HU/short_description.txt b/fastlane/metadata/android/hu-HU/short_description.txt new file mode 100644 index 00000000..2e7e6127 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/short_description.txt @@ -0,0 +1 @@ +Streameljen vagy töltsön le filmeket, TV sorozatokat vagy animét. diff --git a/fastlane/metadata/android/hu-HU/title.txt b/fastlane/metadata/android/hu-HU/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/id/changelogs/2.txt b/fastlane/metadata/android/id/changelogs/2.txt new file mode 100644 index 00000000..677af86c --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/2.txt @@ -0,0 +1 @@ +- Log perubahan ditambahkan! diff --git a/fastlane/metadata/android/id/full_description.txt b/fastlane/metadata/android/id/full_description.txt new file mode 100644 index 00000000..fde69775 --- /dev/null +++ b/fastlane/metadata/android/id/full_description.txt @@ -0,0 +1,12 @@ +CloudStream-3 memudahkan Anda streaming dan mengunduh Film, Seri TV, dan Anime. + + +Aplikasi ini hadir tanpa iklan dan pelacak analitik dan +mendukung beberapa situs trailer & film, dan +masih banyak lagi, misalnya + +Bookmark + +Mengunduh subtitle + +Dukungan Chromecast diff --git a/fastlane/metadata/android/id/short_description.txt b/fastlane/metadata/android/id/short_description.txt new file mode 100644 index 00000000..d7a9ebe4 --- /dev/null +++ b/fastlane/metadata/android/id/short_description.txt @@ -0,0 +1 @@ +Stream dan unduh film, seri TV, dan anime. diff --git a/fastlane/metadata/android/id/title.txt b/fastlane/metadata/android/id/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/id/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/ko-KR/changelogs/2.txt b/fastlane/metadata/android/ko-KR/changelogs/2.txt new file mode 100644 index 00000000..f4c05b14 --- /dev/null +++ b/fastlane/metadata/android/ko-KR/changelogs/2.txt @@ -0,0 +1 @@ +- 변경기록이 추가됨! diff --git a/fastlane/metadata/android/ko-KR/full_description.txt b/fastlane/metadata/android/ko-KR/full_description.txt new file mode 100644 index 00000000..542c1ff7 --- /dev/null +++ b/fastlane/metadata/android/ko-KR/full_description.txt @@ -0,0 +1,10 @@ +클라우트스트림-3는 영화, TV-연속극 및 애니메이션 스트리밍을 할 수 있고 내려받을 수 있습니다. + +이 앱은 광고나 분석 없이 제공되고 +여러 예고편 & 영화 사이트 등을 지원합니다. + +북마크 + +자막 내려받기 + +크롬캐스트 지원 diff --git a/fastlane/metadata/android/ko-KR/short_description.txt b/fastlane/metadata/android/ko-KR/short_description.txt new file mode 100644 index 00000000..e85980b1 --- /dev/null +++ b/fastlane/metadata/android/ko-KR/short_description.txt @@ -0,0 +1 @@ +영화, TV 시리즈 및 애니메이션 스트림과 내려받기. diff --git a/fastlane/metadata/android/ko-KR/title.txt b/fastlane/metadata/android/ko-KR/title.txt new file mode 100644 index 00000000..a199a665 --- /dev/null +++ b/fastlane/metadata/android/ko-KR/title.txt @@ -0,0 +1 @@ +클라우드스티림 diff --git a/fastlane/metadata/android/mk-MK/changelogs/2.txt b/fastlane/metadata/android/mk-MK/changelogs/2.txt index 949f6579..ee4f49cc 100644 --- a/fastlane/metadata/android/mk-MK/changelogs/2.txt +++ b/fastlane/metadata/android/mk-MK/changelogs/2.txt @@ -1 +1 @@ -- Дневник на промени е додаден! +- Дневникот на промени е додаден! diff --git a/fastlane/metadata/android/mk-MK/full_description.txt b/fastlane/metadata/android/mk-MK/full_description.txt index cb06980e..b49c1683 100644 --- a/fastlane/metadata/android/mk-MK/full_description.txt +++ b/fastlane/metadata/android/mk-MK/full_description.txt @@ -1,8 +1,8 @@ CloudStream-3 ви дозволува да гледате и превземате филмови, телевизиски серии и аниме. Апликацијата нема реклами и аналитика. Таа поддржува повеќе страници за трејлери, филмови и многу повеќе. Апликацијата вклучува: - + Обележувачи (Bookmarks) - + Превземање на преводи - + Поддршка за Chromecast diff --git a/fastlane/metadata/android/ml-IN/changelogs/2.txt b/fastlane/metadata/android/ml-IN/changelogs/2.txt new file mode 100644 index 00000000..f7523831 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/changelogs/2.txt @@ -0,0 +1 @@ +-ചേഞ്ച്ലോഗ് ചേർത്തു! diff --git a/fastlane/metadata/android/ml-IN/full_description.txt b/fastlane/metadata/android/ml-IN/full_description.txt new file mode 100644 index 00000000..218f9f98 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/full_description.txt @@ -0,0 +1,10 @@ +ക്ലൗഡ് സ്ട്രീം-3 സിനിമകൾ, ടിവി സീരീസ്, ആനിമേഷൻ എന്നിവ സ്ട്രീം ചെയ്യാനും ഡൗൺലോഡ് ചെയ്യാനും നിങ്ങളെ അനുവദിക്കുന്നു. + +പരസ്യങ്ങളും അനലിറ്റിക്‌സും കൂടാതെ ആപ്പ് വരുന്നു ഒപ്പം +ഒന്നിലധികം ട്രെയിലർ, മൂവി സൈറ്റുകൾ എന്നിവയും മറ്റും പിന്തുണയ്ക്കുന്നു, ഉദാഹരണം + +ബുക്ക്മാർക്കുകൾ + +ഉപശീർഷകം ഡൗൺലോഡുകൾ + +ക്രോംകാസ്റ്റ് പിന്തുണ diff --git a/fastlane/metadata/android/ml-IN/short_description.txt b/fastlane/metadata/android/ml-IN/short_description.txt new file mode 100644 index 00000000..f12fe5b5 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/short_description.txt @@ -0,0 +1 @@ +സ്ട്രീം ഒപ്പം ഡൗൺലോഡ് സിനിമകളും, ടിവി സീരീസുകളും, ആനിമേഷനും . diff --git a/fastlane/metadata/android/ml-IN/title.txt b/fastlane/metadata/android/ml-IN/title.txt new file mode 100644 index 00000000..8e89348a --- /dev/null +++ b/fastlane/metadata/android/ml-IN/title.txt @@ -0,0 +1 @@ +ക്ലൗഡ് സ്ട്രീം diff --git a/fastlane/metadata/android/mt/changelogs/2.txt b/fastlane/metadata/android/mt/changelogs/2.txt new file mode 100644 index 00000000..66bbca8f --- /dev/null +++ b/fastlane/metadata/android/mt/changelogs/2.txt @@ -0,0 +1 @@ +- Changelog miżjud! diff --git a/fastlane/metadata/android/mt/full_description.txt b/fastlane/metadata/android/mt/full_description.txt new file mode 100644 index 00000000..da507aae --- /dev/null +++ b/fastlane/metadata/android/mt/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 iħallik tistrimja u tniżżel Films, Serje TV u Anime. + +L-app tiġi mingħajr reklami u analytics u +jappoġġja siti multipli ta' trejlers u films, u aktar, eż. + +Bookmarks + +Downloads tas-sottotitli + +Appoġġ tal-Chromecast diff --git a/fastlane/metadata/android/mt/short_description.txt b/fastlane/metadata/android/mt/short_description.txt new file mode 100644 index 00000000..542b8614 --- /dev/null +++ b/fastlane/metadata/android/mt/short_description.txt @@ -0,0 +1 @@ +Tistrimja u tniżżel films, serje tat-TV u Anime. diff --git a/fastlane/metadata/android/mt/title.txt b/fastlane/metadata/android/mt/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/mt/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/pl-PL/changelogs/2.txt b/fastlane/metadata/android/pl-PL/changelogs/2.txt new file mode 100644 index 00000000..e558535d --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/2.txt @@ -0,0 +1 @@ +- Dodano dziennik zmian! diff --git a/fastlane/metadata/android/pl-PL/full_description.txt b/fastlane/metadata/android/pl-PL/full_description.txt new file mode 100644 index 00000000..11f71ff7 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 umożliwia strumieniowe przesyłanie i pobieranie filmów, seriali telewizyjnych i anime. + +Aplikacja jest dostarczana bez reklam i analityki, obsługuje +wiele witryn ze zwiastunami, filmami i nie tylko, np. + +Zakładki + +Pobieranie napisów + +Obsługa Chromecasta diff --git a/fastlane/metadata/android/pt-BR/changelogs/2.txt b/fastlane/metadata/android/pt-BR/changelogs/2.txt new file mode 100644 index 00000000..c094fe97 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/2.txt @@ -0,0 +1 @@ +- Histórico de mudanças adicionado! diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt new file mode 100644 index 00000000..1406838e --- /dev/null +++ b/fastlane/metadata/android/pt-BR/full_description.txt @@ -0,0 +1,10 @@ +O CloudStream-3 permite que você faça transmissões, download de filmes, séries, e anime. + +O aplicativo não contém anúncios ou ferramentas de análise, +e suporta múltiplos sites de filmes e trailers, e muito mais, como: + +Favoritos + +Download de legendas + +Suporte à Chromecast diff --git a/fastlane/metadata/android/pt-BR/short_description.txt b/fastlane/metadata/android/pt-BR/short_description.txt new file mode 100644 index 00000000..46635de9 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/short_description.txt @@ -0,0 +1 @@ +Faça transmissões, download de filmes, séries, e anime. diff --git a/fastlane/metadata/android/pt-BR/title.txt b/fastlane/metadata/android/pt-BR/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/ru-RU/changelogs/2.txt b/fastlane/metadata/android/ru-RU/changelogs/2.txt new file mode 100644 index 00000000..4b9464b6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/2.txt @@ -0,0 +1 @@ +- Добавлен список изменений! diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt new file mode 100644 index 00000000..1790888e --- /dev/null +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 позволяет транслировать и скачивать фильмы, сериалы и аниме. + +Приложение поставляется без рекламы и аналитики и +поддерживает множество сайтов с трейлерами и фильмами, а также многое другое, например + +Книжные закладки + +Загрузка субтитров + +Поддержка Chromecast diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt new file mode 100644 index 00000000..a43bc8a1 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/short_description.txt @@ -0,0 +1 @@ +Транслируйте и скачивайте фильмы, сериалы и аниме. diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt new file mode 100644 index 00000000..3c0406a6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/title.txt @@ -0,0 +1 @@ +Облачный поток diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt index dde89d58..0afff90c 100644 --- a/fastlane/metadata/android/vi/title.txt +++ b/fastlane/metadata/android/vi/title.txt @@ -1 +1 @@ -CloudStream +double_tap_seek_time_key2 diff --git a/fastlane/metadata/android/zh-CN/changelogs/2.txt b/fastlane/metadata/android/zh-CN/changelogs/2.txt index 5512f16c..c8c4624d 100644 --- a/fastlane/metadata/android/zh-CN/changelogs/2.txt +++ b/fastlane/metadata/android/zh-CN/changelogs/2.txt @@ -1 +1 @@ -- 添加了更新日志! +- 新增更新日志! diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt index 56519df6..b2dcf1de 100644 --- a/fastlane/metadata/android/zh-CN/full_description.txt +++ b/fastlane/metadata/android/zh-CN/full_description.txt @@ -1,9 +1,10 @@ -CloudStream-3可以让你串流和下载电影、剧集和动漫。这款应用没有任何广告和分析。它支持多个预告片和电影网站等。特点包括: +CloudStream-3可以让你串流和下载电影、剧集和动漫。 + +这款应用没有任何广告和隐私分析并且 +它支持多个预告片和电影网站等。特点包括: 书签 -下载和串流电影、电视节目和动漫 - 下载字幕 支持投屏 diff --git a/library/build.gradle.kts b/library/build.gradle.kts new file mode 100644 index 00000000..42a8c943 --- /dev/null +++ b/library/build.gradle.kts @@ -0,0 +1,68 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec + +plugins { + kotlin("multiplatform") + id("maven-publish") + id("com.android.library") + id("com.codingfeline.buildkonfig") +} + +kotlin { + version = "1.0.0" + androidTarget() + jvm() + + sourceSets { + commonMain.dependencies { + implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser + ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API + Level 25 or Less. */ + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + } + } +} + +repositories { + mavenLocal() + maven("https://jitpack.io") +} + +tasks.withType { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +buildkonfig { + packageName = "com.lagradost.api" + exposeObjectWithName = "BuildConfig" + + defaultConfigs { + val isDebug = kotlin.runCatching { extra.get("isDebug") }.getOrNull() == true + buildConfigField(FieldSpec.Type.BOOLEAN, "DEBUG", isDebug.toString()) + } +} + +android { + compileSdk = 34 + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + + defaultConfig { + minSdk = 21 + targetSdk = 33 + } + + // If this is the same com.lagradost.cloudstream3.R stops working + namespace = "com.lagradost.api" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} +publishing { + publications { + withType { + groupId = "com.lagradost.api" + } + } +} \ No newline at end of file diff --git a/library/src/androidMain/AndroidManifest.xml b/library/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..568741e5 --- /dev/null +++ b/library/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/library/src/androidMain/kotlin/com/lagradost/api/Log.kt b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..12524411 --- /dev/null +++ b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,21 @@ +package com.lagradost.api + +import android.util.Log + +actual object Log { + actual fun d(tag: String, message: String) { + Log.d(tag, message) + } + + actual fun i(tag: String, message: String) { + Log.i(tag, message) + } + + actual fun w(tag: String, message: String) { + Log.w(tag, message) + } + + actual fun e(tag: String, message: String) { + Log.e(tag, message) + } +} \ No newline at end of file diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt new file mode 100644 index 00000000..48a709eb --- /dev/null +++ b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt @@ -0,0 +1,11 @@ +package com.lagradost.cloudstream3.utils + +import android.os.Handler +import android.os.Looper + +actual fun runOnMainThreadNative(work: () -> Unit) { + val mainHandler = Handler(Looper.getMainLooper()) + mainHandler.post { + work() + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/api/Log.kt b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..4b8e6329 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,8 @@ +package com.lagradost.api + +expect object Log { + fun d(tag: String, message: String) + fun i(tag: String, message: String) + fun w(tag: String, message: String) + fun e(tag: String, message: String) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt new file mode 100644 index 00000000..6502cc83 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt @@ -0,0 +1,35 @@ +package com.lagradost.cloudstream3 + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.lagradost.nicehttp.Requests +import com.lagradost.nicehttp.ResponseParser +import kotlin.reflect.KClass + +// Short name for requests client to make it nicer to use + +var app = Requests(responseParser = object : ResponseParser { + val mapper: ObjectMapper = jacksonObjectMapper().configure( + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, + false + ) + + override fun parse(text: String, kClass: KClass): T { + return mapper.readValue(text, kClass.java) + } + + override fun parseSafe(text: String, kClass: KClass): T? { + return try { + mapper.readValue(text, kClass.java) + } catch (e: Exception) { + null + } + } + + override fun writeValueAsString(obj: Any): String { + return mapper.writeValueAsString(obj) + } +}).apply { + defaultHeaders = mapOf("user-agent" to USER_AGENT) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt new file mode 100644 index 00000000..160ff098 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt @@ -0,0 +1,6 @@ +package com.lagradost.cloudstream3 + +const val USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" + +class ErrorLoadingException(message: String? = null) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt similarity index 86% rename from app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index 817d7db3..d3b4999a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -1,10 +1,7 @@ package com.lagradost.cloudstream3.mvvm -import android.util.Log -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import com.bumptech.glide.load.HttpException -import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.api.BuildConfig +import com.lagradost.api.Log import com.lagradost.cloudstream3.ErrorLoadingException import kotlinx.coroutines.* import java.io.InterruptedIOException @@ -49,18 +46,6 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) { } } -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { it?.let { t -> action(t) } } -} - -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { action(it) } -} - sealed class Resource { data class Success(val value: T) : Resource() data class Failure( @@ -158,14 +143,14 @@ fun throwAbleToResource( "Connection Timeout\nPlease try again later." ) } - is HttpException -> { - Resource.Failure( - false, - throwable.statusCode, - null, - throwable.message ?: "HttpException" - ) - } +// is HttpException -> { +// Resource.Failure( +// false, +// throwable.statusCode, +// null, +// throwable.message ?: "HttpException" +// ) +// } is UnknownHostException -> { Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}") } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt similarity index 90% rename from app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt index c3b244c2..f87ddc6a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -1,12 +1,11 @@ package com.lagradost.cloudstream3.utils -import android.os.Handler -import android.os.Looper import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* import java.util.Collections.synchronizedList +expect fun runOnMainThreadNative(work: (() -> Unit)) object Coroutines { fun T.main(work: suspend ((T) -> Unit)): Job { val value = this @@ -50,10 +49,7 @@ object Coroutines { } fun runOnMainThread(work: (() -> Unit)) { - val mainHandler = Handler(Looper.getMainLooper()) - mainHandler.post { - work() - } + runOnMainThreadNative(work) } /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsHunter.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/utils/JsHunter.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt similarity index 99% rename from app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt index 153dbd3e..d9f0b382 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt @@ -50,7 +50,7 @@ class JsUnpacker(packedJS: String?) { throw Exception("Unknown p.a.c.k.e.r. encoding") } val unbase = Unbase(radix) - p = Pattern.compile("\\b\\w+\\b") + p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""") m = p.matcher(payload) val decoded = StringBuilder(payload) var replaceOffset = 0 diff --git a/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..e9a0e6b4 --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,19 @@ +package com.lagradost.api + +actual object Log { + actual fun d(tag: String, message: String) { + println("DEBUG $tag: $message") + } + + actual fun i(tag: String, message: String) { + println("INFO $tag: $message") + } + + actual fun w(tag: String, message: String) { + println("WARNING $tag: $message") + } + + actual fun e(tag: String, message: String) { + println("ERROR $tag: $message") + } +} \ No newline at end of file diff --git a/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt new file mode 100644 index 00000000..0a9667cb --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt @@ -0,0 +1,5 @@ +package com.lagradost.cloudstream3.utils + +actual fun runOnMainThreadNative(work: () -> Unit) { + work.invoke() +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 17070047..eabd9f0e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ rootProject.name = "CloudStream" -include(":app") \ No newline at end of file +include(":app") +include(":library") \ No newline at end of file