From 12de92455960f012bb0298723127061b90932327 Mon Sep 17 00:00:00 2001 From: RowdyRushya Date: Fri, 19 Jul 2024 09:10:34 -0700 Subject: [PATCH 001/235] updating vidplay encryption method (#1202) --- .../cloudstream3/extractors/VidSrcTo.kt | 2 +- .../cloudstream3/extractors/Vidplay.kt | 101 ++++++++---------- 2 files changed, 43 insertions(+), 60 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt index 578f5fb93e8..e974f23a8df 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -40,7 +40,7 @@ class VidSrcTo : ExtractorApi() { 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) + "F2Cloud" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) } } catch (e: Exception) { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt index 6202800faeb..d7e7ce1894d 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -4,13 +4,14 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +import java.net.URLDecoder import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec -import kotlin.run +import kotlin.io.encoding.Base64 // Code found in https://github.com/KillerDogeEmpire/vidplay-keys // special credits to @KillerDogeEmpire for providing key @@ -33,6 +34,7 @@ class VidplayOnline : Vidplay() { override val mainUrl = "https://vidplay.online" } +@OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class) open class Vidplay : ExtractorApi() { override val name = "Vidplay" override val mainUrl = "https://vidplay.site" @@ -58,83 +60,64 @@ open class Vidplay : ExtractorApi() { } override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit ) { + val myKeys = getKeys() + val domain = url.substringBefore("/e/") val id = url.substringBefore("?").substringAfterLast("/") - val encodeId = encodeId(id, getKeys()) - val mediaUrl = callFutoken(encodeId, url) - val res = app.get( - "$mediaUrl", headers = mapOf( - "Accept" to "application/json, text/javascript, */*; q=0.01", - "X-Requested-With" to "XMLHttpRequest", - ), referer = url - ).parsedSafe()?.result - + val encodedId = encode(id, myKeys.get(0)) + val t = url.substringAfter("t=").substringBefore("&") + val h = encode(id, myKeys.get(1)) + val mediaUrl = "$domain/mediainfo/$encodedId?t=$t&h=$h" + val encodedRes = + app.get("$mediaUrl").parsedSafe()?.result + ?: throw Exception("Unable to fetch link") + val decodedRes = decode(encodedRes, myKeys.get(2)) + val res = tryParseJson(decodedRes) res?.sources?.map { - M3u8Helper.generateM3u8( - this.name, - it.file ?: return@map, - "$mainUrl/" - ).forEach(callback) + M3u8Helper.generateM3u8(this.name, it.file ?: return@map, "$mainUrl/").forEach(callback) } res?.tracks?.filter { it.kind == "captions" }?.map { - subtitleCallback.invoke( - SubtitleFile(it.label ?: return@map, it.file ?: return@map) - ) + subtitleCallback.invoke(SubtitleFile(it.label ?: return@map, it.file ?: return@map)) } - } - private suspend fun callFutoken(id: String, url: String): String? { - 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) { - a.add((k[i % k.length].code + id[i].code).toString()) - } - return "$mainUrl/mediainfo/${a.joinToString(",")}?${url.substringAfter("?")}" + private fun encode(input: String, key: String): String { + val rc4Key = SecretKeySpec(key.toByteArray(Charsets.UTF_8), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.ENCRYPT_MODE, rc4Key) + val encryptedBytes = cipher.doFinal(input.toByteArray(Charsets.UTF_8)) + return Base64.UrlSafe.encode(encryptedBytes) } - private fun encodeId(id: String, keyList: List): String { - val cipher1 = Cipher.getInstance("RC4") - val cipher2 = Cipher.getInstance("RC4") - cipher1.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(keyList[0].toByteArray(), "RC4"), - cipher1.parameters - ) - cipher2.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(keyList[1].toByteArray(), "RC4"), - cipher2.parameters - ) - var input = id.toByteArray() - input = cipher1.doFinal(input) - input = cipher2.doFinal(input) - return base64Encode(input).replace("/", "_") + fun decode(input: String, key: String): String { + val decodedBytes = Base64.UrlSafe.decode(input) + val rc4Key = SecretKeySpec(key.toByteArray(Charsets.UTF_8), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.DECRYPT_MODE, rc4Key) + val decryptedBytes = cipher.doFinal(decodedBytes) + val decodedString = String(decryptedBytes, Charsets.UTF_8) + return URLDecoder.decode(decodedString, "UTF-8") } data class Tracks( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, - @JsonProperty("kind") val kind: String? = null, + @JsonProperty("file") val file: String? = null, + @JsonProperty("label") val label: String? = null, + @JsonProperty("kind") val kind: String? = null, ) data class Sources( - @JsonProperty("file") val file: String? = null, + @JsonProperty("file") val file: String? = null, ) data class Result( - @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), - @JsonProperty("tracks") val tracks: ArrayList? = arrayListOf(), - ) - - data class Response( - @JsonProperty("result") val result: Result? = null, + @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), + @JsonProperty("tracks") val tracks: ArrayList? = arrayListOf(), ) + data class Response(@JsonProperty("result") val result: String? = null) } From 63465ed7a9cfb2121eb91015671872f9a14deefd Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:24:06 +0200 Subject: [PATCH 002/235] fix autohide --- .../lagradost/cloudstream3/ui/player/FullScreenPlayer.kt | 9 +++++++++ .../lagradost/cloudstream3/ui/player/GeneratorPlayer.kt | 1 + 2 files changed, 10 insertions(+) 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 75a861c0957..97075136199 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 @@ -728,6 +728,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private var currentTapIndex = 0 protected fun autoHide() { currentTapIndex++ + delayHide() + } + + override fun playerStatusChanged() { + super.playerStatusChanged() + delayHide() + } + + private fun delayHide() { val index = currentTapIndex playerBinding?.playerHolder?.postDelayed({ if (!isCurrentTouchValid && isShowing && index == currentTapIndex && player.getIsPlaying()) { 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 d827d31e98f..f6c78b070db 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 @@ -158,6 +158,7 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun playerStatusChanged() { + super.playerStatusChanged() if (player.getIsPlaying()) { viewModel.forceClearCache = false } From 073af50f5f09a3ca6b4bc21d0c84ff1bf2264e8f Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:28:36 +0200 Subject: [PATCH 003/235] fixed html plot in preview --- app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 59f499c5f2e..e8cbc4d84ca 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -107,6 +107,7 @@ import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST import com.lagradost.cloudstream3.ui.result.SyncViewModel import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setText +import com.lagradost.cloudstream3.ui.result.setTextHtml import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SearchFragment import com.lagradost.cloudstream3.ui.search.SearchResultBuilder @@ -1404,7 +1405,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa resultviewPreviewMetaDuration.setText(d.durationText) resultviewPreviewMetaRating.setText(d.ratingText) - resultviewPreviewDescription.setText(d.plotText) + resultviewPreviewDescription.setTextHtml(d.plotText) resultviewPreviewPoster.setImage( d.posterImage ?: d.posterBackgroundImage ) From bb8144a52ecd4f6518eaca41bebd7cdf2ce31e1d Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 19 Jul 2024 20:35:29 +0300 Subject: [PATCH 004/235] feat(TV UI): Player's Top controls redesign (#1203) --- .../cloudstream3/ui/player/CS3IPlayer.kt | 4 + .../ui/player/FullScreenPlayer.kt | 72 +++++++-- .../cloudstream3/ui/player/GeneratorPlayer.kt | 19 ++- .../cloudstream3/ui/player/IPlayer.kt | 2 + .../res/drawable/ic_baseline_equalizer_24.xml | 9 ++ .../res/drawable/ic_baseline_replay_24.xml | 9 ++ .../res/drawable/ic_baseline_restart_24.xml | 9 ++ .../drawable/ic_baseline_skip_next_24_big.xml | 10 ++ .../ic_baseline_skip_next_rounded_24.xml | 9 ++ .../main/res/layout/fragment_player_tv.xml | 11 +- .../main/res/layout/player_custom_layout.xml | 119 +++++++++++--- .../res/layout/player_custom_layout_tv.xml | 148 ++++++++++++++---- app/src/main/res/layout/subtitle_offset.xml | 31 ++-- .../main/res/layout/trailer_custom_layout.xml | 123 ++++++++++++--- app/src/main/res/values/strings.xml | 1 + 15 files changed, 467 insertions(+), 109 deletions(-) create mode 100644 app/src/main/res/drawable/ic_baseline_equalizer_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_replay_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_restart_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml create mode 100644 app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml 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 8e322f73bb3..735e4095534 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 @@ -912,7 +912,11 @@ class CS3IPlayer : IPlayer { } CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source) + CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source) + + CSPlayerEvent.Restart -> seekTo(0, source) + CSPlayerEvent.NextEpisode -> event( EpisodeSeekEvent( offset = 1, 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 97075136199..a75b98992c0 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,7 +14,13 @@ import android.os.Bundle import android.provider.Settings import android.text.Editable import android.text.format.DateUtils -import android.view.* +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.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES import android.view.animation.AlphaAnimation import android.view.animation.Animation @@ -58,7 +64,11 @@ import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UserPreferenceDelegate import com.lagradost.cloudstream3.utils.Vector2 -import kotlin.math.* +import kotlin.math.abs +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min +import kotlin.math.round const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking const val MINIMUM_VERTICAL_SWIPE = 2.0f // in percentage @@ -77,7 +87,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected open var isFullScreenPlayer = true protected var playerBinding: PlayerCustomLayoutBinding? = null - private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false) + private var durationMode: Boolean by UserPreferenceDelegate("duration_mode", false) + // state of player UI protected var isShowing = false protected var isLocked = false @@ -243,7 +254,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat() - playerBinding?.apply { playerOpenSource.let { ObjectAnimator.ofFloat(it, "translationY", playerSourceMove).apply { @@ -284,7 +294,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.getCurrentPreferredSubtitle() == null } - private fun restoreOrientationWithSensor(activity: Activity){ + private fun restoreOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation var orientation = 0 when (currentOrientation) { @@ -300,7 +310,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { activity.requestedOrientation = orientation } - private fun toggleOrientationWithSensor(activity: Activity){ + private fun toggleOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation var orientation = 0 when (currentOrientation) { @@ -345,12 +355,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun updateOrientation(ignoreDynamicOrientation: Boolean = false) { activity?.apply { - if(lockRotation) { - if(isLocked) { + if (lockRotation) { + if (isLocked) { lockOrientation(this) - } - else { - if(ignoreDynamicOrientation){ + } else { + if (ignoreDynamicOrientation) { // restore when lock is disabled restoreOrientationWithSensor(this) } else { @@ -958,7 +967,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } else -> { - player.handleEvent(CSPlayerEvent.PlayPauseToggle, PlayerEventSource.UI) + player.handleEvent( + CSPlayerEvent.PlayPauseToggle, + PlayerEventSource.UI + ) } } } else if (doubleTapEnabled && isFullScreenPlayer) { @@ -1234,6 +1246,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { // if nothing has loaded these buttons should not be visible playerBinding?.apply { playerSkipEpisode.isVisible = false + playerGoForward.isVisible = false playerTracksBtt.isVisible = false playerSkipOp.isVisible = false shadowOverlay.isVisible = false @@ -1307,6 +1320,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.handleEvent(CSPlayerEvent.SeekBack) } + PlayerEventType.Restart -> { + player.handleEvent(CSPlayerEvent.Restart) + } + PlayerEventType.ToggleMute -> { player.handleEvent(CSPlayerEvent.ToggleMute) } @@ -1428,6 +1445,25 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } playerBinding?.apply { + + if (isLayout(TV or EMULATOR)) { + mapOf( + playerGoBack to playerGoBackText, + playerRestart to playerRestartText, + playerGoForward to playerGoForwardText + ).forEach { (button, text) -> + button.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + text.isSelected = false + text.isVisible = false + return@setOnFocusChangeListener + } + text.isSelected = true + text.isVisible = true + } + } + } + playerPausePlay.setOnClickListener { autoHide() player.handleEvent(CSPlayerEvent.PlayPauseToggle) @@ -1471,6 +1507,16 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.handleEvent(CSPlayerEvent.NextEpisode) } + playerGoForward.setOnClickListener { + autoHide() + player.handleEvent(CSPlayerEvent.NextEpisode) + } + + playerRestart.setOnClickListener { + autoHide() + player.handleEvent(CSPlayerEvent.Restart) + } + playerLock.setOnClickListener { autoHide() toggleLock() @@ -1564,7 +1610,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun setRemainingTimeCounter(showRemaining: Boolean) { durationMode = showRemaining - playerBinding?.exoDuration?.isInvisible= showRemaining + playerBinding?.exoDuration?.isInvisible = showRemaining playerBinding?.timeLeft?.isVisible = showRemaining } 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 f6c78b070db..1f7cc5bd3f8 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 @@ -44,6 +44,7 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog import com.lagradost.cloudstream3.ui.result.* import com.lagradost.cloudstream3.ui.settings.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.subtitles.SUBTITLE_AUTO_SELECT_KEY @@ -1097,8 +1098,15 @@ class GeneratorPlayer : FullScreenPlayer() { } playerBinding?.playerSkipOp?.isVisible = isOpVisible - playerBinding?.playerSkipEpisode?.isVisible = - !isOpVisible && viewModel.hasNextEpisode() == true + + when { + isLayout(PHONE) -> + playerBinding?.playerSkipEpisode?.isVisible = + !isOpVisible && viewModel.hasNextEpisode() == true + + else -> + playerBinding?.playerGoForward?.isVisible = viewModel.hasNextEpisode() == true + } if (percentage >= PRELOAD_NEXT_EPISODE_PERCENTAGE) { viewModel.preLoadNextLinks() @@ -1254,7 +1262,7 @@ class GeneratorPlayer : FullScreenPlayer() { fun setPlayerDimen(widthHeight: Pair?) { val extra = if (widthHeight != null) { val (width, height) = widthHeight - "${width}x${height}" + "- ${width}x${height}" } else { "" } @@ -1265,7 +1273,7 @@ class GeneratorPlayer : FullScreenPlayer() { 0 -> "" 1 -> extra 2 -> source - 3 -> "$source - $extra" + 3 -> "$source $extra" else -> "" } playerBinding?.playerVideoTitleRez?.apply { @@ -1290,7 +1298,8 @@ 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 - layout = if (isLayout(TV or EMULATOR)) 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/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 4bd5c7699e2..5f7161f7ee5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -26,6 +26,7 @@ enum class PlayerEventType(val value: Int) { Resize(13), SearchSubtitlesOnline(14), SkipOp(15), + Restart(16), } enum class CSPlayerEvent(val value: Int) { @@ -39,6 +40,7 @@ enum class CSPlayerEvent(val value: Int) { PrevEpisode(6), PlayPauseToggle(7), ToggleMute(8), + Restart(9), } enum class CSPlayerLoading { diff --git a/app/src/main/res/drawable/ic_baseline_equalizer_24.xml b/app/src/main/res/drawable/ic_baseline_equalizer_24.xml new file mode 100644 index 00000000000..cd20ad156ac --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_equalizer_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_replay_24.xml b/app/src/main/res/drawable/ic_baseline_replay_24.xml new file mode 100644 index 00000000000..e247aa9243a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_replay_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_restart_24.xml b/app/src/main/res/drawable/ic_baseline_restart_24.xml new file mode 100644 index 00000000000..aed3a562c44 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_restart_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml b/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml new file mode 100644 index 00000000000..a8c43bbdb34 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml b/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml new file mode 100644 index 00000000000..452c4dd9969 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_player_tv.xml b/app/src/main/res/layout/fragment_player_tv.xml index 07cbb3c3075..3c0ac05e35e 100644 --- a/app/src/main/res/layout/fragment_player_tv.xml +++ b/app/src/main/res/layout/fragment_player_tv.xml @@ -68,7 +68,9 @@ android:layout_height="wrap_content" android:layout_margin="5dp" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toTopOf="parent" + android:visibility="gone" + tools:visibility="visible"> diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 83be8832958..be97b978b49 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -172,27 +172,108 @@ android:id="@+id/player_go_back_holder" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="5dp" + android:layout_margin="20dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - + + + + + + + + + + + + + + + + + + + + + + + - @@ -626,7 +707,7 @@ android:nextFocusLeft="@id/player_sources_btt" android:nextFocusRight="@id/player_skip_op" android:text="@string/tracks" - app:icon="@drawable/ic_baseline_playlist_play_24" /> + app:icon="@drawable/ic_baseline_equalizer_24" /> + diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index d8406b35afd..98eb58aca2a 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -231,7 +231,7 @@ @@ -240,6 +240,7 @@ android:id="@+id/player_video_title" android:layout_width="match_parent" android:layout_height="wrap_content" + android:textAlignment="viewEnd" android:gravity="end" android:textColor="@color/white" android:textSize="16sp" @@ -250,6 +251,7 @@ android:id="@+id/player_video_title_rez" android:layout_width="match_parent" android:layout_height="wrap_content" + android:textAlignment="viewEnd" android:gravity="end" android:textColor="@color/white" android:textSize="16sp" @@ -285,28 +287,116 @@ android:id="@+id/player_go_back_holder" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="5dp" + android:layout_marginStart="17dp" + android:layout_marginTop="20dp" + android:layout_marginEnd="17dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - + + + + + + + + + + + + + + + + + + + + + + + - @@ -509,6 +599,7 @@ android:layout_height="wrap_content" android:layoutDirection="ltr" android:orientation="horizontal"> + + app:layout_constraintStart_toStartOf="parent" + app:tint="@color/player_button_tv" + tools:ignore="ContentDescription" /> + tools:text="-23:20" /> @@ -672,12 +763,13 @@ android:id="@+id/player_skip_episode" style="@style/VideoButtonTV" android:nextFocusLeft="@id/player_skip_op" - android:nextFocusRight="@id/player_resize_btt" android:nextFocusUp="@id/player_pause_play" android:nextFocusDown="@id/player_resize_btt" android:text="@string/next_episode" - app:icon="@drawable/ic_baseline_skip_next_24" /> + app:icon="@drawable/ic_baseline_skip_next_24" + android:visibility="gone" + tools:visibility="visible"/> + app:icon="@drawable/ic_baseline_equalizer_24" /> diff --git a/app/src/main/res/layout/subtitle_offset.xml b/app/src/main/res/layout/subtitle_offset.xml index d5e303b6f39..82c24e61b31 100644 --- a/app/src/main/res/layout/subtitle_offset.xml +++ b/app/src/main/res/layout/subtitle_offset.xml @@ -30,28 +30,27 @@ @@ -67,29 +66,29 @@ diff --git a/app/src/main/res/layout/trailer_custom_layout.xml b/app/src/main/res/layout/trailer_custom_layout.xml index 59104ca7884..20b7363046a 100644 --- a/app/src/main/res/layout/trailer_custom_layout.xml +++ b/app/src/main/res/layout/trailer_custom_layout.xml @@ -137,33 +137,110 @@ - + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e3f788fe84..e68c22b9335 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -96,6 +96,7 @@ Next Random @string/play_episode Go back + Play from the Beginning @string/home_change_provider_img_des Change Provider Preview Background From 4c7379c766837ffc11c62d4fa2c4e7aaa8afd5fc Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Sat, 20 Jul 2024 19:14:11 +0200 Subject: [PATCH 005/235] Revert #979 Episode download cache --- .../main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 1d23e5037cb..802c1a648e6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -67,8 +67,6 @@ object BackupUtils { 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 "download_path_key" // No access rights after restore data from backup @@ -266,4 +264,4 @@ object BackupUtils { } editor.apply() } -} \ No newline at end of file +} From 0c418fdf9bd41f6a94e9a8063c48bc4acd44ffb1 Mon Sep 17 00:00:00 2001 From: RowdyRushya Date: Sat, 20 Jul 2024 15:06:04 -0700 Subject: [PATCH 006/235] Updated VidSrc encryption methods (#1205) --- .../extractors/VidSrcExtractor.kt | 301 +++++++++++++----- 1 file changed, 227 insertions(+), 74 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt index a27bf1887bc..5da919e2f09 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt @@ -3,98 +3,251 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.* -import kotlinx.coroutines.delay -import java.net.URI +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.loadExtractor +import java.util.Base64 class VidSrcExtractor2 : VidSrcExtractor() { - override val mainUrl = "https://vidsrc.me/embed" - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val newUrl = url.lowercase().replace(mainUrl, super.mainUrl) - super.getUrl(newUrl, referer, subtitleCallback, callback) - } + override val mainUrl = "https://vidsrc.me" } open class VidSrcExtractor : ExtractorApi() { override val name = "VidSrc" - private val absoluteUrl = "https://v2.vidsrc.me" - override val mainUrl = "$absoluteUrl/embed" + override val mainUrl = "https://vidsrc.net" + private val apiUrl = "https://vidsrc.stream" override val requiresReferer = false - companion object { - /** Infinite function to validate the vidSrc pass */ - suspend fun validatePass(url: String) { - val uri = URI(url) - val host = uri.host - - // Basically turn https://tm3p.vidsrc.stream/ -> https://vidsrc.stream/ - val referer = host.split(".").let { - val size = it.size - "https://" + it.subList(maxOf(0, size - 2), size).joinToString(".") + "/" - } - - while (true) { - app.get(url, referer = referer) - delay(60_000) - } - } - } - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit ) { val iframedoc = app.get(url).document - val serverslist = - iframedoc.select("div#sources.button_content div#content div#list div").map { - val datahash = it.attr("data-hash") - if (datahash.isNotBlank()) { - val links = try { - app.get( - "$absoluteUrl/srcrcp/$datahash", - referer = "https://rcp.vidsrc.me/" - ).url - } catch (e: Exception) { - "" - } - links - } else "" - } + val srcrcpList = + iframedoc.select("div.serversList > div.server").mapNotNull { + val datahash = it.attr("data-hash") ?: return@mapNotNull null + val rcpLink = "$apiUrl/rcp/$datahash" + val rcpRes = app.get(rcpLink, referer = apiUrl).text + val srcrcpLink = + Regex("src:\\s*'(.*)',").find(rcpRes)?.destructured?.component1() + ?: return@mapNotNull null + "https:$srcrcpLink" + } - serverslist.amap { server -> - val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/") - if (linkfixed.contains("/prorcp")) { - val srcresponse = app.get(server, referer = absoluteUrl).text - val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)") - val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap - val passRegex = Regex("""['"](.*set_pass[^"']*)""") - val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace( - Regex("""^//"""), "https://" - ) + srcrcpList.amap { server -> + val res = app.get(server, referer = apiUrl) + if (res.url.contains("/prorcp")) { + val encodedElement = res.document.select("div#reporting_content+div") + val decodedUrl = + decodeUrl(encodedElement.attr("id"), encodedElement.text()) ?: return@amap callback.invoke( - ExtractorLink( - this.name, - this.name, - srcm3u8, - "https://vidsrc.stream/", - Qualities.Unknown.value, - extractorData = pass, - isM3u8 = true - ) + ExtractorLink( + this.name, + this.name, + decodedUrl, + apiUrl, + Qualities.Unknown.value, + isM3u8 = true + ) ) } else { - loadExtractor(linkfixed, url, subtitleCallback, callback) + loadExtractor(res.url, url, subtitleCallback, callback) } } } -} \ No newline at end of file + private fun decodeUrl(encType: String, url: String): String? { + return when (encType) { + "NdonQLf1Tzyx7bMG" -> bMGyx71TzQLfdonN(url) + "sXnL9MQIry" -> Iry9MQXnLs(url) + "IhWrImMIGL" -> IGLImMhWrI(url) + "xTyBxQyGTA" -> GTAxQyTyBx(url) + "ux8qjPHC66" -> C66jPHx8qu(url) + "eSfH1IRMyL" -> MyL1IRSfHe(url) + "KJHidj7det" -> detdj7JHiK(url) + "o2VSUnjnZl" -> nZlUnj2VSo(url) + "Oi3v1dAlaM" -> laM1dAi3vO(url) + "TsA2KGDGux" -> GuxKGDsA2T(url) + "JoAHUMCLXV" -> LXVUMCoAHJ(url) + else -> null + } + } + + private fun bMGyx71TzQLfdonN(a: String): String { + val b = 3 + val c = mutableListOf() + var d = 0 + while (d < a.length) { + c.add(a.substring(d, minOf(d + b, a.length))) + d += b + } + val e = c.reversed().joinToString("") + return e + } + + private fun Iry9MQXnLs(a: String): String { + val b = "pWB9V)[*4I`nJpp?ozyB~dbr9yt!_n4u" + val d = a.chunked(2).map { it.toInt(16).toChar() }.joinToString("") + var c = "" + for (e in d.indices) { + c += (d[e].code xor b[e % b.length].code).toChar() + } + var e = "" + for (ch in c) { + e += (ch.code - 3).toChar() + } + return String(Base64.getDecoder().decode(e)) + } + + private fun IGLImMhWrI(a: String): String { + val b = a.reversed() + val c = + b + .map { + when (it) { + in 'a'..'m', in 'A'..'M' -> it + 13 + in 'n'..'z', in 'N'..'Z' -> it - 13 + else -> it + } + } + .joinToString("") + val d = c.reversed() + return String(Base64.getDecoder().decode(d)) + } + + private fun GTAxQyTyBx(a: String): String { + val b = a.reversed() + val c = b.filterIndexed { index, _ -> index % 2 == 0 } + return String(Base64.getDecoder().decode(c)) + } + + private fun C66jPHx8qu(a: String): String { + val b = a.reversed() + val c = "X9a(O;FMV2-7VO5x;Ao:dN1NoFs?j," + val d = b.chunked(2).map { it.toInt(16).toChar() }.joinToString("") + var e = "" + for (i in d.indices) { + e += (d[i].code xor c[i % c.length].code).toChar() + } + return e + } + + private fun MyL1IRSfHe(a: String): String { + val b = a.reversed() + val c = b.map { (it.code - 1).toChar() }.joinToString("") + val d = c.chunked(2).map { it.toInt(16).toChar() }.joinToString("") + return d + } + + private fun detdj7JHiK(a: String): String { + val b = a.substring(10, a.length - 16) + val c = "3SAY~#%Y(V%>5d/Yg\"\$G[Lh1rK4a;7ok" + val d = String(Base64.getDecoder().decode(b)) + val e = c.repeat((d.length + c.length - 1) / c.length).substring(0, d.length) + var f = "" + for (i in d.indices) { + f += (d[i].code xor e[i].code).toChar() + } + return f + } + + private fun nZlUnj2VSo(a: String): String { + val b = + mapOf( + 'x' to 'a', + 'y' to 'b', + 'z' to 'c', + 'a' to 'd', + 'b' to 'e', + 'c' to 'f', + 'd' to 'g', + 'e' to 'h', + 'f' to 'i', + 'g' to 'j', + 'h' to 'k', + 'i' to 'l', + 'j' to 'm', + 'k' to 'n', + 'l' to 'o', + 'm' to 'p', + 'n' to 'q', + 'o' to 'r', + 'p' to 's', + 'q' to 't', + 'r' to 'u', + 's' to 'v', + 't' to 'w', + 'u' to 'x', + 'v' to 'y', + 'w' to 'z', + 'X' to 'A', + 'Y' to 'B', + 'Z' to 'C', + 'A' to 'D', + 'B' to 'E', + 'C' to 'F', + 'D' to 'G', + 'E' to 'H', + 'F' to 'I', + 'G' to 'J', + 'H' to 'K', + 'I' to 'L', + 'J' to 'M', + 'K' to 'N', + 'L' to 'O', + 'M' to 'P', + 'N' to 'Q', + 'O' to 'R', + 'P' to 'S', + 'Q' to 'T', + 'R' to 'U', + 'S' to 'V', + 'T' to 'W', + 'U' to 'X', + 'V' to 'Y', + 'W' to 'Z' + ) + return a.map { b[it] ?: it }.joinToString("") + } + + private fun laM1dAi3vO(a: String): String { + val b = a.reversed() + val c = b.replace("-", "+").replace("_", "/") + val d = String(Base64.getDecoder().decode(c)) + var e = "" + val f = 5 + for (ch in d) { + e += (ch.code - f).toChar() + } + return e + } + + private fun GuxKGDsA2T(a: String): String { + val b = a.reversed() + val c = b.replace("-", "+").replace("_", "/") + val d = String(Base64.getDecoder().decode(c)) + var e = "" + val f = 7 + for (ch in d) { + e += (ch.code - f).toChar() + } + return e + } + + private fun LXVUMCoAHJ(a: String): String { + val b = a.reversed() + val c = b.replace("-", "+").replace("_", "/") + val d = String(Base64.getDecoder().decode(c)) + var e = "" + val f = 3 + for (ch in d) { + e += (ch.code - f).toChar() + } + return e + } +} From c8a863e332d0d952568385f438b2a035cca5b816 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 24 Jul 2024 22:38:16 +0200 Subject: [PATCH 007/235] Fixed ExampleInstrumentedTest --- .../java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index faacdf50141..c7f02baff8a 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -154,7 +154,7 @@ class ExampleInstrumentedTest { fun providerCorrectHomepage() { runBlocking { getAllProviders().toList().amap { api -> - TestingUtils.testHomepage(api, ::println) + TestingUtils.testHomepage(api, TestingUtils.Logger()) } } println("Done providerCorrectHomepage") @@ -166,7 +166,6 @@ class ExampleInstrumentedTest { TestingUtils.getDeferredProviderTests( this, getAllProviders(), - ::println ) { _, _ -> } } } From dfd127265a066dfec18e797b3b2ddc7bf2ae51ef Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 25 Jul 2024 21:23:31 +0300 Subject: [PATCH 008/235] Trailers Fix (#1213) --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6e439d53858..1ad35d89e31 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -200,7 +200,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:592f159") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:2d36945") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding From e3ff1cf4554bc1bea1333a456c62becb987fd01b Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 25 Jul 2024 21:23:49 +0300 Subject: [PATCH 009/235] feat(UI): Show Episode Runtime (#1207) --- .../metaproviders/TraktProvider.kt | 1 + .../cloudstream3/ui/result/EpisodeAdapter.kt | 15 +++++++-- .../cloudstream3/ui/result/ResultFragment.kt | 3 ++ .../ui/result/ResultViewModel2.kt | 6 ++-- .../main/res/layout/result_episode_large.xml | 30 +++++++++++++---- .../com/lagradost/cloudstream3/MainAPI.kt | 32 +++++++++++++++++-- 6 files changed, 75 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 7c375e0af6f..a1b9ff34e55 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -236,6 +236,7 @@ open class TraktProvider : MainAPI() { posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()), rating = episode.rating?.times(10)?.roundToInt(), description = episode.overview, + runTime = episode.runtime ).apply { this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) { 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 ed5e51f12b4..06be6bd5fdf 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 @@ -27,7 +27,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper import java.text.DateFormat import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 const val ACTION_PLAY_EPISODE_IN_VLC_PLAYER = 2 @@ -58,6 +59,7 @@ const val ACTION_MARK_AS_WATCHED = 18 const val ACTION_FCAST = 19 const val TV_EP_SIZE = 400 + data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( @@ -274,7 +276,10 @@ class EpisodeAdapter( episodeDate.setText( txt( R.string.episode_upcoming_format, - secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "") + secondsToReadable( + card.airDate.minus(unixTimeMS).div(1000).toInt(), + "" + ) ) ) } else { @@ -292,6 +297,12 @@ class EpisodeAdapter( episodeDate.isVisible = false } + episodeRuntime.setText( + txt( + card.runTime?.times(60L)?.toInt()?.let { secondsToReadable(it, "") } + ) + ) + if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) 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 c687eaa064d..3eab0c71476 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 @@ -51,6 +51,7 @@ data class ResultEpisode( /** Sum of all previous season episode counts + episode */ val totalEpisodeIndex: Int? = null, val airDate: Long? = null, + val runTime: Int? = null, ) fun ResultEpisode.getRealPosition(): Long { @@ -87,6 +88,7 @@ fun buildResultEpisode( parentId: Int, totalEpisodeIndex: Int? = null, airDate: Long? = null, + runTime: Int? = null, ): ResultEpisode { val posDur = getViewPos(id) val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None @@ -111,6 +113,7 @@ fun buildResultEpisode( videoWatchState, totalEpisodeIndex, airDate, + runTime, ) } 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 8e8dfe30a29..5086426fb36 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 @@ -2371,7 +2371,8 @@ class ResultViewModel2 : ViewModel() { loadResponse.type, mainId, totalIndex, - airDate = i.date + airDate = i.date, + runTime = i.runTime, ) val season = eps.seasonIndex ?: 0 @@ -2426,7 +2427,8 @@ class ResultViewModel2 : ViewModel() { loadResponse.type, mainId, totalIndex, - airDate = episode.date + airDate = episode.date, + runTime = episode.runTime, ) val season = ep.seasonIndex ?: 0 diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index e5a6881a6ed..935beac1a7e 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -44,7 +44,7 @@ android:nextFocusRight="@id/download_button" android:scaleType="centerCrop" tools:src="@drawable/example_poster" - tools:visibility="invisible"/> + tools:visibility="invisible" /> + tools:visibility="invisible" /> - + android:layout_gravity="start" + android:orientation="horizontal"> + + + + + + + Date: Thu, 25 Jul 2024 20:25:17 +0200 Subject: [PATCH 010/235] Add the option to hide video controls (#1210) --- .../ui/player/FullScreenPlayer.kt | 25 +++++++++++++++++++ .../ui/settings/SettingsPlayer.kt | 6 ++--- app/src/main/res/values-af/strings.xml | 1 + app/src/main/res/values-ajp/strings.xml | 1 + app/src/main/res/values-am/strings.xml | 1 + app/src/main/res/values-ar/strings.xml | 1 + app/src/main/res/values-ars/strings.xml | 1 + app/src/main/res/values-as/strings.xml | 1 + app/src/main/res/values-bg/strings.xml | 1 + app/src/main/res/values-bn/strings.xml | 1 + app/src/main/res/values-bp/strings.xml | 1 + app/src/main/res/values-cs/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-el/strings.xml | 1 + app/src/main/res/values-eo/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fa/strings.xml | 1 + app/src/main/res/values-fil/strings.xml | 4 ++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-gl/strings.xml | 1 + app/src/main/res/values-hi/strings.xml | 1 + app/src/main/res/values-hr/strings.xml | 1 + app/src/main/res/values-hu/strings.xml | 1 + app/src/main/res/values-in/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-iw/strings.xml | 1 + app/src/main/res/values-ja/strings.xml | 1 + app/src/main/res/values-kn/strings.xml | 1 + app/src/main/res/values-ko/strings.xml | 1 + app/src/main/res/values-lt/strings.xml | 1 + app/src/main/res/values-lv/strings.xml | 1 + app/src/main/res/values-mk/strings.xml | 1 + app/src/main/res/values-ml/strings.xml | 1 + app/src/main/res/values-ms/strings.xml | 1 + app/src/main/res/values-mt/strings.xml | 1 + app/src/main/res/values-my/strings.xml | 1 + app/src/main/res/values-ne/strings.xml | 1 + app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-nn/strings.xml | 1 + app/src/main/res/values-no/strings.xml | 1 + app/src/main/res/values-or/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 1 + app/src/main/res/values-qt/strings.xml | 1 + app/src/main/res/values-ro/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sk/strings.xml | 1 + app/src/main/res/values-so/strings.xml | 1 + app/src/main/res/values-sv/strings.xml | 1 + app/src/main/res/values-ta/strings.xml | 1 + app/src/main/res/values-ti/strings.xml | 1 + app/src/main/res/values-tl/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values-ur/strings.xml | 1 + app/src/main/res/values-vi/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values-zh/strings.xml | 1 + app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/settings_player.xml | 6 +++++ 60 files changed, 93 insertions(+), 5 deletions(-) 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 a75b98992c0..ef7d6bc11b2 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 @@ -25,15 +25,18 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.animation.AnimationUtils +import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.core.graphics.blue import androidx.core.graphics.green import androidx.core.graphics.red +import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.preference.PreferenceManager +import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.screenHeight @@ -120,6 +123,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected var doubleTapPauseEnabled = true protected var playerRotateEnabled = false protected var autoPlayerRotateEnabled = false + private var hideControlsNames = false protected var subtitleDelay set(value) = try { @@ -1419,6 +1423,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { false ) + hideControlsNames = settingsManager.getBoolean(ctx.getString(R.string.hide_player_control_names_key), false) + val profiles = QualityDataHelper.getProfiles() val type = if (ctx.isUsingMobileData()) QualityDataHelper.QualityProfileType.Data @@ -1439,6 +1445,9 @@ open class FullScreenPlayer : AbstractPlayerFragment() { playerSpeedBtt.isVisible = playBackSpeedEnabled playerResizeBtt.isVisible = playerResizeEnabled playerRotateBtt.isVisible = playerRotateEnabled + if (hideControlsNames) { + hideControlsNames() + } } } catch (e: Exception) { logError(e) @@ -1591,6 +1600,22 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } + private fun PlayerCustomLayoutBinding.hideControlsNames() { + fun iterate(layout: LinearLayout) { + layout.children.forEach { + if (it is MaterialButton) { + it.textSize = 0f + it.iconPadding = 0 + it.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START + it.setPadding(0,0,0,0) + } else if (it is LinearLayout) { + iterate(it) + } + } + } + iterate(playerLockHolder.parent as LinearLayout) + } + override fun playerDimensionsLoaded(width: Int, height: Int) { isVerticalOrientation = height > width updateOrientation() 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 20279cd17ac..7560d75fa50 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 @@ -87,10 +87,6 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - /*(getPref(R.string.double_tap_seek_time_key) as? SeekBarPreference?)?.let { - - }*/ - getPref(R.string.prefer_limit_title_rez_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.limit_title_rez_pref_names) val prefValues = resources.getIntArray(R.array.limit_title_rez_pref_values) @@ -109,6 +105,8 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + getPref(R.string.hide_player_control_names_key)?.hideOn(TV) + getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener { val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 45e9a1d4664..4adafee4d04 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -106,4 +106,5 @@ Voer lettertipes in deur dit in %s te plaas Rolverdeling: %s Nuwe episode notifikasie + hide_player_control_names_key diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index c78b6924d59..718b5235732 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -640,4 +640,5 @@ تجاهل متاح الريپوزيتوري فتاح %s ع تلفونك أو كمپيوترك، وحط الكود اللي فوق + hide_player_control_names_key diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml index 7fd3274b944..26fb84dd38d 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -108,4 +108,5 @@ ተጨማሪ መረጃ ዓይነቶችን በመጠቀም ይፈልጉ ቅርጸ-ቁምፊዎችን በ%s ውስጥ በማስቀመጥ ያጫኑ + hide_player_control_names_key diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c2ed35cb8a6..e85fee04160 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -666,4 +666,5 @@ قم بزيارة %s على هاتفك الذكي أو جهاز الكمبيوتر وأدخل الرمز أعلاه لا يمكن الحصول على رمز PIN للجهاز، حاول المصادقة المحلية تنتهي صلاحية الرمز خلال %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index f3811d3da9f..f028ef5d8c0 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -352,4 +352,5 @@ وثائقي موقع عنوان مشغل الفيديو بحد أقصى لعدد الأحرف + hide_player_control_names_key diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml index 7fb0e7bdf41..dd1b2eedd18 100644 --- a/app/src/main/res/values-as/strings.xml +++ b/app/src/main/res/values-as/strings.xml @@ -621,4 +621,5 @@ ছাবটাইটেল বাছনি কৰক পৰ্ব খেলাওক প্ৰয়োগ কৰক + hide_player_control_names_key diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 66e2988225f..89801322d08 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -601,4 +601,5 @@ Покажи предложения Добавя опция за промяна на скоростта в плеъра Този тест е направен за програмисти и не проверява работата на никакви добавки. + hide_player_control_names_key diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 3500e85adfb..1a02eebc3d0 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -359,4 +359,5 @@ অ্যাকাউন্ট প্রস্থান %1$d%2$s + hide_player_control_names_key diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 6dc38cd8ffe..51138312d28 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -656,4 +656,5 @@ Não é possível obter o código PIN do dispositivo, tente a autenticação local O código PIN expirou! O código expira em %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8e40b12bc2c..2f7dcfedd8c 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -658,4 +658,5 @@ Účty Lokální ověření PIN kód vypršel! + hide_player_control_names_key diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ee378ff60dd..12a68dbc287 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -615,4 +615,5 @@ Zurücksetzen Akkuverbrauch der App ist bereits auf unbeschränkt eingestellt CloudStreams App-Info kann nicht geöffnet werden. + hide_player_control_names_key diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e7fa1f6afd0..269626cb586 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -625,4 +625,5 @@ Τα δεδομένα σας στο CloudStream έχουν κάνει back up. Αν και η πιθανότητα είναι πολύ χαμηλή, όλες οι συσκευές συμπεριφέρονται διαφορετικά. Στη σπάνια περίπτωση, που απαγορευτεί η πρόσβασή σας από την εφαρμογή, διαγράψτε τα δεδομένα εφαρμογής και επαναφέρετέ τα από ένα ήδη υπάρχον backup. Συγνώμη για οποιαδήποτε ταλαιπωρία. Λογαριασμοί Ασφάλεια + hide_player_control_names_key diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 275a4bfb1bd..9d3d07bc742 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -127,4 +127,5 @@ Elŝutite Elŝutante Elŝuto Malsukcesite + hide_player_control_names_key diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 82f29381be0..bd281b5522f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -634,4 +634,5 @@ ¡El código PIN ya ha caducado! El código caduca en %1$d mín y %2$d s No puedo obtener el código PIN del dispositivo; intente con la autenticación local + hide_player_control_names_key diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index db432a61220..86dee8ef404 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -191,4 +191,5 @@ پیش‌فرض کارتون تورنت + hide_player_control_names_key diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index 42eba3ccd1c..2189dd75d68 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -1,2 +1,4 @@ - + + hide_player_control_names_key + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index fa1e1b61b75..78f3f2a52bc 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -620,4 +620,5 @@ Verrouillage biométrique Sélectionnez un appareil de diffusion Saison %1$d Episode %2$d sera publié dans + hide_player_control_names_key diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index ae3105cf183..d04792f8d76 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -164,4 +164,5 @@ Selecciona o modo para filtrar a descarga dos complementos Instala automáticamente todos os complementos aínda non instalados dos repositorios engadidos. Mostrar actualizacións da aplicación + hide_player_control_names_key diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index e08a3b8bfa1..bd50953c86f 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -209,4 +209,5 @@ रूपरेखा रंग उपशीर्षक ऊंचाई अक्षर शैली + hide_player_control_names_key diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 54448e58961..90dbee79bf7 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -651,4 +651,5 @@ CloudStream Wiki Računi Sigurnost + hide_player_control_names_key diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index ebaff0417e1..72213b02727 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -592,4 +592,5 @@ A PIN 4 karakter hosszú kell legyen Auto elforgatás Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása + hide_player_control_names_key diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 951ba41788e..0edae603446 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -647,4 +647,5 @@ CloudStream Wiki Keamanan Akun + hide_player_control_names_key diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ff7ea6bdac5..8671a73a4b5 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -654,4 +654,5 @@ Impossibile ottenere il codice PIN del dispositivo, prova l\'autenticazione locale Il codice PIN è scaduto! Il codice scadrà tra %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 22626f502cf..1f34f0e1f69 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -550,4 +550,5 @@ \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! + hide_player_control_names_key diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index acb2cfc3898..fb2ca02dd57 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -242,4 +242,5 @@ 現在のエピソードが終了したら次のエピソードを開始する 長押しするとデフォルトにリセットされます ダウンロードを再開 + hide_player_control_names_key diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index f3fb665d69b..75f62bcceb0 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -130,4 +130,5 @@ Brightness ಅಥವಾ volume ಬದಲಾಯಿಸಲು ಎಡ ಅಥವಾ ಬಲಭಾಗದಲ್ಲಿ ಮೇಲಕ್ಕೆ ಅಥವಾ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ ಈಗಿನ ಎಪಿಸೋಡ್ ಮುಗಿದಾಗ ಮುಂದಿನ ಎಪಿಸೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸ್ವೈಪ್ ಮಾಡಿ + hide_player_control_names_key diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index bda82057427..ec570e6962d 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -634,4 +634,5 @@ %s의 PIN 입력 즐겨찾기에서 제거 캐스트미러 + hide_player_control_names_key diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index f61bcfc0f55..0cb3addfb86 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -260,4 +260,5 @@ Ar tikrai norite išeiti\? Pašalinti iš žiūrimų Garso takelis + hide_player_control_names_key diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 566c721de57..96272e71d7f 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -527,4 +527,5 @@ Abonēto šovu atjaunināšana Abonēts Abonēts %s + hide_player_control_names_key diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 5e4d5c06f01..d4023ec463a 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -624,4 +624,5 @@ Грешка при пристапот до таблата со исечоци, обидете се повторно. Грешка при копирање, копирајте го logcat и контактирајте со поддршката за апликацијата. Аудио книга + hide_player_control_names_key diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index d97e666c166..213d4a00e87 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -280,4 +280,5 @@ എഡ്ജ് തരം ഔട്ട്ലൈൻ നിറം പശ്ചാത്തല നിറം + hide_player_control_names_key diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index dca98e53c88..aae74f4ef28 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -57,4 +57,5 @@ Tutup Ep cuba + hide_player_control_names_key diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml index b2c0356a87c..37da058084c 100644 --- a/app/src/main/res/values-mt/strings.xml +++ b/app/src/main/res/values-mt/strings.xml @@ -123,4 +123,5 @@ Bookmarks Neħħi Falla t-tniżżil + hide_player_control_names_key diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 0ebe3c6b246..e7007d127f8 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -550,4 +550,5 @@ သင်နဂိုတည်းကသတ်မှတ်ပြီး လိုက်ဘရီရွေးချယ်ရန် ဖြင့်ဖွင့်မည် + hide_player_control_names_key diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 49cb6cfa91b..bc0199a1d33 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -128,4 +128,5 @@ प्लेयरको उपशीर्षकको सेटिङ रिपोजिटरी को नाम र यूआरएल कपी गरियो! + hide_player_control_names_key diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index b685489bf83..6029f78b827 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -608,4 +608,5 @@ Link opnieuw geladen Autoroteer Roteer + hide_player_control_names_key diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 95c527f9a8c..930841dbdda 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -195,4 +195,5 @@ Bilde i bilde Fortsett å sjå Prøv tilkopling på nytt… + hide_player_control_names_key diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 7b013653386..115cd2d324a 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -538,4 +538,5 @@ Bruk Hjelp Profilbakgrunn + hide_player_control_names_key diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index bdc55780cbd..07fc8a1d649 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -159,4 +159,5 @@ କୌଣସି ତଥ୍ୟ ନାହିଁ %1$s ଅ %2$d ଆଦ୍ୟ ବାଦ୍ ଦିଅ + hide_player_control_names_key diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 8e940c6136a..209c9d8e8a2 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -635,4 +635,5 @@ Odrzuć Otwórz repozytorium Odwiedź %s na swoim smartfonie lub komputerze i wprowadź powyższy kod + hide_player_control_names_key diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index ce20a8afbf9..594063831ef 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -621,4 +621,5 @@ Fcast Escolha o dispositivo Transmitir + hide_player_control_names_key diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 5de97c7da9c..258552e2ca5 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -247,4 +247,5 @@ oooooh uuaagh @string/home_play oouuhhh ahhooo-ahah + hide_player_control_names_key diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 344eae21901..609190cf95c 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -641,4 +641,5 @@ Selectați divece-ul pe care doriți să faceți cast Cast mirror Fcast + hide_player_control_names_key diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5a9b843e2b2..7f19ac8c119 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -622,4 +622,5 @@ Выйдет %s Fcast Выберите девайс для трансляции + hide_player_control_names_key diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index a53e1f53c15..947e2b6d307 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -377,4 +377,5 @@ Pridať repozitár Názov repozitára Zobraziť komunitné repozitáre + hide_player_control_names_key diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index c750ea7a151..5dc0bc230ae 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -485,4 +485,5 @@ Bilowga Bilow isku qasan Qoraalka dhamaadka + hide_player_control_names_key diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 04230ab8f9e..dd2dffb97af 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -626,4 +626,5 @@ CloudStream Wiki Konton Säkerhet + hide_player_control_names_key diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 9378e400e9f..44729e0931e 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -615,4 +615,5 @@ %கள் பிடித்தவைகளிலிருந்து அகற்றப்பட்டன உங்கள் கிளவுட்ச்ட்ரீம் தரவு இப்போது காப்புப் பிரதி எடுக்கப்பட்டுள்ளது. இதன் சாத்தியம் மிகக் குறைவு என்றாலும், எல்லா சாதனங்களும் வித்தியாசமாக நடந்து கொள்ளலாம். அரிய விசயத்தில், பயன்பாட்டை அணுகுவதிலிருந்து நீங்கள் பூட்டப்படுகிறீர்கள், பயன்பாட்டு தரவை முழுவதுமாக அழித்து, காப்புப்பிரதியிலிருந்து மீட்டெடுக்கவும். இதிலிருந்து எழும் ஏதேனும் சிரமத்திற்கு நாங்கள் மிகவும் வருந்துகிறோம். ஊடகம் + hide_player_control_names_key diff --git a/app/src/main/res/values-ti/strings.xml b/app/src/main/res/values-ti/strings.xml index 46235bbd7bd..6c154c8d857 100644 --- a/app/src/main/res/values-ti/strings.xml +++ b/app/src/main/res/values-ti/strings.xml @@ -3,4 +3,5 @@ %1$s ክፋል %2$d ክፋል %d በ ላይ ይወጣል ተዋሳእቲ፡ %s + hide_player_control_names_key diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index b4308eb7fb6..dd964877596 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -265,4 +265,5 @@ Mga Subtitle ng Chromecast Mga setting ng mga subtitle ng Chromecast Maglaro ng Trailer + hide_player_control_names_key diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3273a90192f..a55750e9ec5 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -681,4 +681,5 @@ Cihaz PIN kodu alınamıyor, yerel kimlik doğrulamayı deneyin PIN kodunun süresi doldu! Kodun süresi %1$dm %2$ds içinde doluyor + hide_player_control_names_key diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f5770e86b7c..fd24274ca1f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -634,4 +634,5 @@ Термін дії коду закінчується через %1$dхв %2$dс Автентифікація по місцю Відхилити + hide_player_control_names_key diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 04cfd381665..c87be59c92a 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -617,4 +617,5 @@ دیگر ایکسٹینشنز میں تلاش کریں سفارشات دکھائیں آپ کے CloudStream ڈیٹا کا اب بیک اپ لیا گیا ہے۔ اگرچہ اس کا امکان بہت کم ہے، لیکن مختلف ڈیوائس مختلف طریقے سے کام کر سکتے ہیں۔ اگر آپ ایپ تک رسائی حاصل کرنے سے قاصر ہیں تو، ایپ کا ڈیٹا مکمل طور پر صاف کریں اور بیک اپ سے بحال کریں۔ اس سے ہونے والی کسی بھی تکلیف کے لیے ہم بہت معذرت خواہ ہیں۔ + hide_player_control_names_key diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 92e088bf222..44868647cfe 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -643,4 +643,5 @@ Truy cập %s trên điện thoại hoặc máy tính và nhập mã bên trên Mã PIN đã hết hạn! Mã sẽ hết hạn trong %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c50f284c129..69eb8741bb2 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -671,4 +671,5 @@ 為了確保下載與通知已訂閱的電視節目的不間斷,CloudStream 需要取得在背景執行的權限。若點選「確定」,將移至「應用程式資訊」,請找到「應用程式電池使用」並將電池用量設置為「無限制」。請注意,取得此權限並不表示 CS3 會明顯增加電池用量,而是只在必要時在背景執行,例如取得通知或使用官方擴充功能下載影片時。若選擇「取消」,您可以稍後在「一般設定」中調整此設定。 CloudStream Wiki 此裝置不支援生物特徵認證 + hide_player_control_names_key diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 97ba24eab43..f2db04e2a85 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -673,4 +673,5 @@ 选择投射设备 %1$d季%2$d集将在 投射镜像 + hide_player_control_names_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e68c22b9335..21067fff91c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -797,4 +797,6 @@ Can\'t get the device PIN code, try local authentication PIN code is now expired ! Code expires in %1$dm %2$ds + hide_player_control_names_key + Hide names of the player\'s controls \ 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 5d5b11d0539..0039af3ad6b 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -37,6 +37,12 @@ android:icon="@drawable/ic_baseline_text_format_24" android:key="@string/prefer_limit_title_rez_key" android:title="@string/limit_title_rez" /> + Date: Thu, 25 Jul 2024 20:26:21 +0200 Subject: [PATCH 011/235] Bump 4.4.0 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1ad35d89e31..2040cf39a93 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -60,8 +60,8 @@ android { minSdk = 21 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.2" + versionCode = 64 + versionName = "4.4.0" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") From a28ee413680da64d059bdc90510f67b816e62568 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:59:37 -0600 Subject: [PATCH 012/235] Fix for navigation UI bug (#1220) --- .../lagradost/cloudstream3/MainActivity.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index e8cbc4d84ca..bc2cb88eaa5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -572,6 +572,35 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa binding?.apply { navRailView.isVisible = isNavVisible && landscape navView.isVisible = isNavVisible && !landscape + + /** + * We need to make sure if we return to a sub-fragment, + * the correct navigation item is selected so that it does not + * highlight the wrong one in UI. + */ + when (destination.id) { + in listOf(R.id.navigation_downloads, R.id.navigation_download_child) -> { + navRailView.menu.findItem(R.id.navigation_downloads).isChecked = true + navView.menu.findItem(R.id.navigation_downloads).isChecked = true + } + in listOf( + R.id.navigation_settings, + R.id.navigation_subtitles, + R.id.navigation_chrome_subtitles, + R.id.navigation_settings_player, + R.id.navigation_settings_updates, + R.id.navigation_settings_ui, + R.id.navigation_settings_account, + R.id.navigation_settings_providers, + R.id.navigation_settings_general, + R.id.navigation_settings_extensions, + R.id.navigation_settings_plugins, + R.id.navigation_test_providers + ) -> { + navRailView.menu.findItem(R.id.navigation_settings).isChecked = true + navView.menu.findItem(R.id.navigation_settings).isChecked = true + } + } } } From 0aa48f335a818e0ebf0e1cf045d302a782e79857 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:26:22 -0600 Subject: [PATCH 013/235] Fix subscription icon displaying for movie types in result previews (#1222) --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5086426fb36..ce0fbdc5de1 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 @@ -2163,7 +2163,7 @@ class ResultViewModel2 : ViewModel() { // lets say that we have subscribed, then we must be able to unsubscribe no matter what else if (data != null) { _subscribeStatus.postValue(true) - } + } else _subscribeStatus.postValue(null) } private fun postFavorites(loadResponse: LoadResponse) { @@ -2861,4 +2861,4 @@ class ResultViewModel2 : ViewModel() { } } } -} +} \ No newline at end of file From 04dda008c40bae83ca076c24f2c8f75f6fcdb870 Mon Sep 17 00:00:00 2001 From: epireyn <48213068+epireyn@users.noreply.github.com> Date: Mon, 29 Jul 2024 00:39:04 +0200 Subject: [PATCH 014/235] Clean up and mark questionable code issues (#1209) --- .../lagradost/cloudstream3/AcraApplication.kt | 12 +- .../lagradost/cloudstream3/CommonActivity.kt | 16 +- .../cloudstream3/DownloaderTestImpl.kt | 6 +- .../lagradost/cloudstream3/MainActivity.kt | 49 +++---- .../cloudstream3/NativeCrashHandler.kt | 53 ------- .../metaproviders/SyncRedirector.kt | 6 +- .../metaproviders/TraktProvider.kt | 2 +- .../cloudstream3/network/CloudflareKiller.kt | 6 +- .../cloudstream3/plugins/CloudstreamPlugin.kt | 3 +- .../lagradost/cloudstream3/plugins/Plugin.kt | 10 +- .../cloudstream3/plugins/PluginManager.kt | 34 +++-- .../cloudstream3/plugins/RepositoryManager.kt | 2 +- .../cloudstream3/plugins/VotingApi.kt | 9 +- .../subtitles/AbstractSubProvider.kt | 2 +- .../subtitles/AbstractSubtitleEntities.kt | 1 - .../syncproviders/AccountManager.kt | 12 +- .../syncproviders/providers/Addic7ed.kt | 14 +- .../syncproviders/providers/AniListApi.kt | 40 ++--- .../syncproviders/providers/LocalList.kt | 2 - .../syncproviders/providers/MALApi.kt | 128 ++++++++-------- .../providers/OpenSubtitlesApi.kt | 29 ++-- .../syncproviders/providers/SimklApi.kt | 137 +++++++++--------- .../syncproviders/providers/SubSource.kt | 1 + .../cloudstream3/ui/APIRepository.kt | 6 +- .../lagradost/cloudstream3/ui/BaseAdapter.kt | 2 + .../cloudstream3/ui/ControllerActivity.kt | 2 + .../cloudstream3/ui/CustomRecyclerViews.kt | 4 +- .../cloudstream3/ui/EasterEggMonke.kt | 2 +- .../ui/NonFinalAdapterListUpdateCallback.kt | 2 +- .../lagradost/cloudstream3/ui/WatchType.kt | 4 +- .../cloudstream3/ui/WebviewFragment.kt | 3 + .../ui/download/button/BaseFetchButton.kt | 1 + .../ui/download/button/DownloadButton.kt | 2 +- .../ui/home/HomeChildItemAdapter.kt | 5 +- .../cloudstream3/ui/home/HomeFragment.kt | 3 +- .../ui/home/HomeParentItemAdapter.kt | 13 +- .../ui/home/HomeParentItemAdapterPreview.kt | 7 +- .../cloudstream3/ui/home/HomeViewModel.kt | 10 +- .../ui/library/LibraryFragment.kt | 6 +- .../ui/library/ViewpagerAdapter.kt | 3 +- .../ui/player/AbstractPlayerFragment.kt | 7 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 82 ++--------- .../ui/player/CustomSubtitleDecoderFactory.kt | 7 +- .../ui/player/CustomTextRenderer.kt | 3 +- .../ui/player/DownloadFileGenerator.kt | 2 +- .../ui/player/DownloadedPlayerActivity.kt | 4 - .../ui/player/FullScreenPlayer.kt | 42 +++--- .../cloudstream3/ui/player/GeneratorPlayer.kt | 20 ++- .../cloudstream3/ui/player/IPlayer.kt | 1 - .../cloudstream3/ui/player/LinkGenerator.kt | 1 - .../ui/player/NonFinalTextRenderer.java | 16 +- .../ui/player/OfflinePlaybackHelper.kt | 2 +- .../ui/player/PlayerGeneratorViewModel.kt | 2 +- .../ui/player/PlayerSubtitleHelper.kt | 3 + .../ui/player/PreviewGenerator.kt | 21 ++- .../ui/player/RepoLinkGenerator.kt | 1 + .../player/source_priority/PriorityAdapter.kt | 5 - .../player/source_priority/ProfilesAdapter.kt | 2 - .../source_priority/QualityDataHelper.kt | 7 +- .../source_priority/QualityProfileDialog.kt | 2 +- .../source_priority/SourcePriorityDialog.kt | 2 +- .../cloudstream3/ui/result/ActorAdaptor.kt | 5 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 6 +- .../cloudstream3/ui/result/ImageAdapter.kt | 15 +- .../ui/result/ResultFragmentPhone.kt | 49 ++++--- .../ui/result/ResultFragmentTv.kt | 45 +----- .../ui/result/ResultViewModel2.kt | 2 +- .../cloudstream3/ui/result/SelectAdaptor.kt | 3 +- .../cloudstream3/ui/search/SearchAdaptor.kt | 7 +- .../ui/search/SearchHistoryAdaptor.kt | 8 +- .../ui/search/SearchResultBuilder.kt | 7 +- .../ui/search/SyncSearchViewModel.kt | 4 +- .../ui/settings/AccountAdapter.kt | 7 +- .../ui/settings/SettingsFragment.kt | 1 - .../ui/settings/SettingsGeneral.kt | 2 - .../ui/settings/SettingsPlayer.kt | 9 +- .../ui/settings/SettingsProviders.kt | 2 +- .../ui/settings/SettingsUpdates.kt | 10 +- .../ui/settings/extensions/PluginAdapter.kt | 23 +-- .../ui/settings/extensions/PluginsFragment.kt | 2 +- .../settings/extensions/PluginsViewModel.kt | 1 - .../ui/setup/SetupFragmentMedia.kt | 1 - .../subtitles/ChromecastSubtitlesFragment.kt | 29 ++-- .../ui/subtitles/SubtitlesFragment.kt | 9 +- .../lagradost/cloudstream3/utils/AniSkip.kt | 2 +- .../cloudstream3/utils/AppContextUtils.kt | 5 +- .../cloudstream3/utils/BackupUtils.kt | 36 ++--- .../lagradost/cloudstream3/utils/DataStore.kt | 25 +++- .../utils/DownloadFileWorkManager.kt | 1 - .../cloudstream3/utils/InAppUpdater.kt | 40 ++--- .../cloudstream3/utils/PackageInstaller.kt | 5 +- .../cloudstream3/utils/PowerManagerAPI.kt | 6 +- .../lagradost/cloudstream3/utils/SyncUtil.kt | 8 +- .../lagradost/cloudstream3/utils/UIHelper.kt | 2 +- .../utils/VideoDownloadManager.kt | 8 +- .../cloudstream3/widget/FlowLayout.kt | 2 +- .../cloudstream3/PluginAdapterTest.kt | 16 ++ 97 files changed, 563 insertions(+), 721 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt create mode 100644 app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 598ff540af3..d6f978fe5b4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -35,6 +35,7 @@ import java.io.File import java.io.FileNotFoundException import java.io.PrintStream import java.lang.ref.WeakReference +import java.util.Locale import kotlin.concurrent.thread import kotlin.system.exitProcess @@ -81,14 +82,8 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) : ACRA.errorReporter.handleException(error) try { PrintStream(errorFile).use { ps -> - ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")) - ps.println( - String.format( - "Fatal exception on thread %s (%d)", - thread.name, - thread.id - ) - ) + ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}") + ps.println("Fatal exception on thread ${thread.name} (${thread.id})") error.printStackTrace(ps) } } catch (ignored: FileNotFoundException) { @@ -106,7 +101,6 @@ class AcraApplication : Application() { override fun onCreate() { super.onCreate() - //NativeCrashHandler.initCrashHandler() ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index ba303feff7e..63912114f65 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -164,7 +164,7 @@ object CommonActivity { val toast = Toast(act) toast.duration = duration ?: Toast.LENGTH_SHORT toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) - toast.view = binding.root + toast.view = binding.root //fixme Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version. currentToast = toast toast.show() @@ -464,20 +464,6 @@ object CommonActivity { fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) { - //println("Keycode: $keyCode") - //showToast( - // this, - // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}", - // Toast.LENGTH_LONG - //) - - // Tested keycodes on remote: - // KeyEvent.KEYCODE_MEDIA_FAST_FORWARD - // KeyEvent.KEYCODE_MEDIA_REWIND - // KeyEvent.KEYCODE_MENU - // KeyEvent.KEYCODE_MEDIA_NEXT - // KeyEvent.KEYCODE_MEDIA_PREVIOUS - // KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE // 149 keycode_numpad 5 when (keyCode) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt index 934dd58a351..8da7ca384b5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt @@ -11,7 +11,7 @@ import java.util.concurrent.TimeUnit class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() { - private val client: OkHttpClient + private val client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build() override fun execute(request: Request): Response { val httpMethod: String = request.httpMethod() val url: String = request.url() @@ -74,8 +74,4 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do return instance } } - - init { - client = builder.readTimeout(30, TimeUnit.SECONDS).build() - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index bc2cb88eaa5..eed69a5064b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -82,13 +82,13 @@ 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.APP_STRING +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_PLAYER +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_REPO +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_SEARCH import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi import com.lagradost.cloudstream3.syncproviders.SyncAPI @@ -347,7 +347,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa println("Repository url: $realUrl") loadRepository(realUrl) return true - } else if (str.contains(appString)) { + } else if (str.contains(APP_STRING)) { for (api in OAuth2Apis) { if (str.contains("/${api.redirectUrl}")) { ioSafe { @@ -377,15 +377,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } // This specific intent is used for the gradle deployWithAdb // https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46 - if (str == "$appString:") { + if (str == "$APP_STRING:") { PluginManager.hotReloadAllLocalPlugins(activity) } - } else if (safeURI(str)?.scheme == appStringRepo) { - val url = str.replaceFirst(appStringRepo, "https") + } else if (safeURI(str)?.scheme == APP_STRING_REPO) { + val url = str.replaceFirst(APP_STRING_REPO, "https") loadRepository(url) return true - } else if (safeURI(str)?.scheme == appStringSearch) { - val query = str.substringAfter("$appStringSearch://") + } else if (safeURI(str)?.scheme == APP_STRING_SEARCH) { + val query = str.substringAfter("$APP_STRING_SEARCH://") nextSearchQuery = try { URLDecoder.decode(query, "UTF-8") @@ -399,7 +399,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa R.id.navigation_search activity?.findViewById(R.id.nav_rail_view)?.selectedItemId = R.id.navigation_search - } else if (safeURI(str)?.scheme == appStringPlayer) { + } else if (safeURI(str)?.scheme == APP_STRING_PLAYER) { val uri = Uri.parse(str) val name = uri.getQueryParameter("name") val url = URLDecoder.decode(uri.authority, "UTF-8") @@ -413,9 +413,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ) ) ) - } else if (safeURI(str)?.scheme == appStringResumeWatching) { + } else if (safeURI(str)?.scheme == APP_STRING_RESUME_WATCHING) { val id = - str.substringAfter("$appStringResumeWatching://").toIntOrNull() + str.substringAfter("$APP_STRING_RESUME_WATCHING://").toIntOrNull() ?: return false ioSafe { val resumeWatchingCard = @@ -469,7 +469,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ) DubStatus.Dubbed else DubStatus.Subbed, null ) } else { - viewModel.loadSmall(this, result) + viewModel.loadSmall(result) } } @@ -605,7 +605,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } //private var mCastSession: CastSession? = null - lateinit var mSessionManager: SessionManager + var mSessionManager: SessionManager? = null private val mSessionManagerListener: SessionManagerListener by lazy { SessionManagerListenerImpl() } private inner class SessionManagerListenerImpl : SessionManagerListener { @@ -645,8 +645,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa setActivityInstance(this) try { if (isCastApiAvailable()) { - //mCastSession = mSessionManager.currentCastSession - mSessionManager.addSessionManagerListener(mSessionManagerListener) + mSessionManager?.addSessionManagerListener(mSessionManagerListener) } } catch (e: Exception) { logError(e) @@ -662,7 +661,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } try { if (isCastApiAvailable()) { - mSessionManager.removeSessionManagerListener(mSessionManagerListener) + mSessionManager?.removeSessionManagerListener(mSessionManagerListener) //mCastSession = null } } catch (e: Exception) { @@ -766,7 +765,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa list.forEach { custom -> allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } ?.let { - allProviders.add(it.javaClass.newInstance().apply { + allProviders.add(it.javaClass.getDeclaredConstructor().newInstance().apply { name = custom.name lang = custom.lang mainUrl = custom.url.trimEnd('/') @@ -1147,7 +1146,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa super.onCreate(savedInstanceState) try { if (isCastApiAvailable()) { - mSessionManager = CastContext.getSharedInstance(this).sessionManager + CastContext.getSharedInstance(this) {it.run()}.addOnSuccessListener { mSessionManager = it.sessionManager } } } catch (t: Throwable) { logError(t) @@ -1449,13 +1448,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa val value = viewModel.watchStatus.value ?: WatchType.NONE this@MainActivity.showBottomDialog( - WatchType.values().map { getString(it.stringRes) }.toList(), + WatchType.entries.map { getString(it.stringRes) }.toList(), value.ordinal, this@MainActivity.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { viewModel.updateWatchStatus( - WatchType.values()[it], + WatchType.entries[it], this@MainActivity ) } @@ -1465,12 +1464,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ?: SyncWatchType.NONE this@MainActivity.showBottomDialog( - SyncWatchType.values().map { getString(it.stringRes) }.toList(), + SyncWatchType.entries.map { getString(it.stringRes) }.toList(), value.ordinal, this@MainActivity.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - syncViewModel.setStatus(SyncWatchType.values()[it].internalId) + syncViewModel.setStatus(SyncWatchType.entries[it].internalId) syncViewModel.publishUserData() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt deleted file mode 100644 index 7be904405a9..00000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.lagradost.cloudstream3 - -import com.lagradost.cloudstream3.MainActivity.Companion.lastError -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -object NativeCrashHandler { - // external fun triggerNativeCrash() - /*private external fun initNativeCrashHandler() - private external fun getSignalStatus(): Int - - private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch { - - //launch { - // delay(10000) - // triggerNativeCrash() - //} - - while (true) { - delay(10_000) - val signal = getSignalStatus() - // Signal is initialized to zero - if (signal == 0) continue - - // Do not crash in safe mode! - if (lastError != null) continue - if (checkSafeModeFile()) continue - - AcraApplication.exceptionHandler?.uncaughtException( - Thread.currentThread(), - RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n") - ) - } - } - - fun initCrashHandler() { - try { - System.loadLibrary("native-lib") - initNativeCrashHandler() - } catch (t: Throwable) { - // Make debug crash. - if (BuildConfig.DEBUG) throw t - logError(t) - return - } - - initSignalPolling() - }*/ -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt index 75e96bec137..bc646a8d2a2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt @@ -2,15 +2,13 @@ package com.lagradost.cloudstream3.metaproviders import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.SyncIdName object SyncRedirector { - val syncApis = SyncApis private val syncIds = listOf( - SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""), - SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""") + SyncIdName.MyAnimeList to Regex("""myanimelist\.net/anime/(\d+)"""), + SyncIdName.Anilist to Regex("""anilist\.co/anime/(\d+)""") ) suspend fun redirect( diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index a1b9ff34e55..addee9a02b7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -296,7 +296,7 @@ open class TraktProvider : MainAPI() { return try { val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) val dateTime = dateString?.let { format.parse(it)?.time } ?: return false - APIHolder.unixTimeMS < dateTime + unixTimeMS < dateTime } catch (t: Throwable) { logError(t) false diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt index ce2fb3a216d..85a9db5db73 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.network -import android.util.Base64 import android.util.Log import android.webkit.CookieManager import androidx.annotation.AnyThread @@ -10,7 +9,10 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.nicehttp.Requests.Companion.await import com.lagradost.nicehttp.cookies import kotlinx.coroutines.runBlocking -import okhttp3.* +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response import java.net.URI diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt index e89ccfeb316..ddf5b286870 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt @@ -2,5 +2,4 @@ package com.lagradost.cloudstream3.plugins @Suppress("unused") @Target(AnnotationTarget.CLASS) -annotation class CloudstreamPlugin( -) \ No newline at end of file +annotation class CloudstreamPlugin \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index 7f08af924ad..fc8365876bf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -34,7 +34,7 @@ abstract class Plugin { */ fun registerMainAPI(element: MainAPI) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI") - element.sourcePlugin = this.__filename + element.sourcePlugin = this.filename // Race condition causing which would case duplicates if not for distinctBy synchronized(APIHolder.allProviders) { APIHolder.allProviders.add(element) @@ -48,7 +48,7 @@ abstract class Plugin { */ fun registerExtractorAPI(element: ExtractorApi) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi") - element.sourcePlugin = this.__filename + element.sourcePlugin = this.filename extractorApis.add(element) } @@ -68,7 +68,11 @@ abstract class Plugin { */ var resources: Resources? = null /** Full file path to the plugin. */ - var __filename: String? = null + @Deprecated("Renamed to `filename` to follow conventions", replaceWith = ReplaceWith("filename")) + var __filename: String? + get() = filename + set(value) {filename = value} + var filename: String? = null /** * This will add a button in the settings allowing you to add custom settings 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 6b2b75f25a2..bc2a1780314 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -1,13 +1,16 @@ package com.lagradost.cloudstream3.plugins +import android.Manifest import android.app.* import android.content.Context +import android.content.pm.PackageManager import android.content.res.AssetManager import android.content.res.Resources import android.os.Build import android.os.Environment import android.util.Log import android.widget.Toast +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.FragmentActivity @@ -163,7 +166,7 @@ object PluginManager { private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins" - public var currentlyLoading: String? = null + var currentlyLoading: String? = null // Maps filepath to plugin val plugins: MutableMap = @@ -339,7 +342,7 @@ object PluginManager { //Omit non-NSFW if mode is set to NSFW only if (mode == AutoDownloadMode.NsfwOnly) { - if (tvtypes.contains(TvType.NSFW.name) == false) { + if (!tvtypes.contains(TvType.NSFW.name)) { return@mapNotNull null } } @@ -504,10 +507,12 @@ object PluginManager { val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also { Log.d(TAG, "No manifest version for ${data.internalName}") } + + @Suppress("UNCHECKED_CAST") val pluginClass: Class<*> = loader.loadClass(manifest.pluginClassName) as Class val pluginInstance: Plugin = - pluginClass.newInstance() as Plugin + pluginClass.getDeclaredConstructor().newInstance() as Plugin // Sets with the proper version setPluginData(data.copy(version = version)) @@ -517,14 +522,16 @@ object PluginManager { return true } - pluginInstance.__filename = file.absolutePath + pluginInstance.filename = file.absolutePath if (manifest.requiresResources) { Log.d(TAG, "Loading resources for ${data.internalName}") // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk - val assets = AssetManager::class.java.newInstance() + val assets = AssetManager::class.java.getDeclaredConstructor().newInstance() val addAssetPath = AssetManager::class.java.getMethod("addAssetPath", String::class.java) addAssetPath.invoke(assets, file.absolutePath) + + @Suppress("DEPRECATION") pluginInstance.resources = Resources( assets, context.resources.displayMetrics, @@ -566,14 +573,14 @@ object PluginManager { // remove all registered apis synchronized(APIHolder.apis) { - APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach { + APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { removePluginMapping(it) } } synchronized(APIHolder.allProviders) { - APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename } + APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename } } - extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename } + extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename } classLoaders.values.removeIf { v -> v == plugin } @@ -720,9 +727,14 @@ object PluginManager { } val notification = builder.build() - with(NotificationManagerCompat.from(context)) { - // notificationId is a unique int for each notification that you must define - notify((System.currentTimeMillis() / 1000).toInt(), notification) + // notificationId is a unique int for each notification that you must define + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + NotificationManagerCompat.from(context) + .notify((System.currentTimeMillis() / 1000).toInt(), notification) } return notification } catch (e: Exception) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index b80a590ef30..c6ec9df7fd7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -73,7 +73,7 @@ object RepositoryManager { val PREBUILT_REPOSITORIES: Array by lazy { getKey("PREBUILT_REPOSITORIES") ?: emptyArray() } - val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$") + private val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$") /* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */ fun convertRawGitUrl(url: String): String { diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt index a45ab5f0239..d1b702f4ce3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.sync.withLock object VotingApi { // please do not cheat the votes lol private const val LOGKEY = "VotingApi" - private const val apiDomain = "https://counterapi.com/api" + private const val API_DOMAIN = "https://counterapi.com/api" private fun transformUrl(url: String): String = // dont touch or all votes get reset MessageDigest @@ -49,13 +49,13 @@ object VotingApi { // please do not cheat the votes lol .joinToString("-") private suspend fun readVote(pluginUrl: String): Int { - var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true" + val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true" Log.d(LOGKEY, "Requesting: $url") return app.get(url).parsedSafe()?.value ?: 0 } private suspend fun writeVote(pluginUrl: String): Boolean { - var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}" + val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}" Log.d(LOGKEY, "Requesting: $url") return app.get(url).parsedSafe()?.value != null } @@ -69,8 +69,7 @@ object VotingApi { // please do not cheat the votes lol getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false fun canVote(pluginUrl: String): Boolean { - if (!PluginManager.urlPlugins.contains(pluginUrl)) return false - return true + return PluginManager.urlPlugins.contains(pluginUrl) } private val voteLock = Mutex() diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt index 857fba11676..df64caabc9f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt @@ -59,7 +59,7 @@ class SubtitleResource { return file } - fun unzip(file: File): List> { + private fun unzip(file: File): List> { val entries = mutableListOf>() ZipInputStream(file.inputStream()).use { zipInputStream -> 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 ed4ccb74a9b..685b499bb59 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.subtitles -import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.TvType class AbstractSubtitleEntities { 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 0259ccad35a..2e14c3c46fd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -56,22 +56,22 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { subSourceApi ) - const val appString = "cloudstreamapp" - const val appStringRepo = "cloudstreamrepo" - const val appStringPlayer = "cloudstreamplayer" + const val APP_STRING = "cloudstreamapp" + const val APP_STRING_REPO = "cloudstreamrepo" + const val APP_STRING_PLAYER = "cloudstreamplayer" // Instantly start the search given a query - const val appStringSearch = "cloudstreamsearch" + const val APP_STRING_SEARCH = "cloudstreamsearch" // Instantly resume watching a show - const val appStringResumeWatching = "cloudstreamcontinuewatching" + const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching" val unixTime: Long get() = System.currentTimeMillis() / 1000L val unixTimeMs: Long get() = System.currentTimeMillis() - const val maxStale = 60 * 10 + const val MAX_STALE = 60 * 10 fun secondsToReadable(seconds: Int, completedValue: String): String { var secondsLong = seconds.toLong() diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt index 507c5e2acf7..db4676393ab 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt @@ -18,13 +18,13 @@ class Addic7ed : AbstractSubApi { override fun logOut() {} companion object { - const val host = "https://www.addic7ed.com" + const val HOST = "https://www.addic7ed.com" const val TAG = "ADDIC7ED" } private fun fixUrl(url: String): String { - return if (url.startsWith("/")) host + url - else if (!url.startsWith("http")) "$host/$url" + return if (url.startsWith("/")) HOST + url + else if (!url.startsWith("http")) "$HOST/$url" else url } @@ -62,7 +62,7 @@ class Addic7ed : AbstractSubApi { } val title = queryText.substringBefore("(").trim() - val url = "$host/search.php?search=${title}&Submit=Search" + val url = "$HOST/search.php?search=${title}&Submit=Search" val hostDocument = app.get(url).document var searchResult = "" if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url @@ -74,8 +74,8 @@ class Addic7ed : AbstractSubApi { hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(") ?.substringBefore(",") val doc = app.get( - "$host/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined", - referer = "$host/" + "$HOST/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined", + referer = "$HOST/" ).document doc.select("#season tr:contains($queryLang)").mapNotNull { node -> if (node.selectFirst("td")?.text() @@ -97,7 +97,7 @@ class Addic7ed : AbstractSubApi { val link = fixUrl(node.select("a.buttonDownload").attr("href")) val isHearingImpaired = !node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty() - cleanResources(results, name, link, mapOf("referer" to "$host/"), isHearingImpaired) + cleanResources(results, name, link, mapOf("referer" to "$HOST/"), isHearingImpaired) } return results } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 8a82cf94e5d..e51d3d654a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -63,7 +63,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleRedirect(url: String): Boolean { val sanitizer = - splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR + splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR val token = sanitizer["access_token"]!! val expiresIn = sanitizer["expires_in"]!! @@ -87,7 +87,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun search(name: String): List? { val data = searchShows(name) ?: return null - return data.data?.Page?.media?.map { + return data.data?.page?.media?.map { SyncAPI.SyncSearchResult( it.title.romaji ?: return null, this.name, @@ -101,7 +101,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getResult(id: String): SyncAPI.SyncResult { val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1) ?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId") - val season = getSeason(internalId).data.Media + val season = getSeason(internalId).data.media return SyncAPI.SyncResult( season.id.toString(), @@ -301,12 +301,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { //println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}") val shows = searchShows(name.replace(blackListRegex, "")) - shows?.data?.Page?.media?.find { + shows?.data?.page?.media?.find { (malId ?: "NONE") == it.idMal.toString() }?.let { return it } val filtered = - shows?.data?.Page?.media?.filter { + shows?.data?.page?.media?.filter { (((it.startDate.year ?: year.toString()) == year.toString() || year == null)) } @@ -496,7 +496,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { val data = postApi(q, true) val d = parseJson(data ?: return null) - val main = d.data?.Media + val main = d.data?.media if (main?.mediaListEntry != null) { return AniListTitleHolder( title = main.title, @@ -536,7 +536,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { headers = mapOf( "Authorization" to "Bearer " + (getAuth() ?: return@suspendSafeApiCall null), - if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" + if (cache) "Cache-Control" to "max-stale=$MAX_STALE" else "Cache-Control" to "no-cache" ), cacheTime = 0, data = mapOf( @@ -647,7 +647,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class Data( - @JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection + @JsonProperty("MediaListCollection") val mediaListCollection: MediaListCollection ) private fun getAniListListCached(): Array? { @@ -659,7 +659,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { if (checkToken()) return null return if (requireLibraryRefresh) { - val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray() + val list = getFullAniListList()?.data?.mediaListCollection?.lists?.toTypedArray() if (list != null) { setKey(ANILIST_CACHED_LIST, list) } @@ -678,7 +678,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { // To fill empty lists when AniList does not return them val baseMap = - AniListStatusType.values().filter { it.value >= 0 }.associate { + AniListStatusType.entries.filter { it.value >= 0 }.associate { it.stringRes to emptyList() } @@ -764,7 +764,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { /** Used to query a saved MediaItem on the list to get the id for removal */ data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null) - data class MediaListItem(@JsonProperty("MediaList") val MediaList: MediaListId? = null) + data class MediaListItem(@JsonProperty("MediaList") val mediaList: MediaListId? = null) data class MediaListId(@JsonProperty("id") val id: Long? = null) private suspend fun postDataAboutId( @@ -787,7 +787,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { """ val response = postApi(idQuery) val listId = - tryParseJson(response)?.data?.MediaList?.id ?: return false + tryParseJson(response)?.data?.mediaList?.id ?: return false """ mutation(${'$'}id: Int = $listId) { DeleteMediaListEntry(id: ${'$'}id) { @@ -836,7 +836,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { val data = postApi(q) if (data.isNullOrBlank()) return null val userData = parseJson(data) - val u = userData.data?.Viewer + val u = userData.data?.viewer val user = AniListUser( u?.id, u?.name, @@ -858,8 +858,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { suspend fun getSeasonRecursive(id: Int) { val season = getSeason(id) seasons.add(season) - if (season.data.Media.format?.startsWith("TV") == true) { - season.data.Media.relations?.edges?.forEach { + if (season.data.media.format?.startsWith("TV") == true) { + season.data.media.relations?.edges?.forEach { if (it.node?.format != null) { if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) { getSeasonRecursive(it.node.id) @@ -878,7 +878,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class SeasonData( - @JsonProperty("Media") val Media: SeasonMedia, + @JsonProperty("Media") val media: SeasonMedia, ) data class SeasonMedia( @@ -1050,7 +1050,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class AniListData( - @JsonProperty("Viewer") val Viewer: AniListViewer?, + @JsonProperty("Viewer") val viewer: AniListViewer?, ) data class AniListRoot( @@ -1090,7 +1090,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class LikeData( - @JsonProperty("Viewer") val Viewer: LikeViewer?, + @JsonProperty("Viewer") val viewer: LikeViewer?, ) data class LikeRoot( @@ -1130,7 +1130,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class GetDataData( - @JsonProperty("Media") val Media: GetDataMedia?, + @JsonProperty("Media") val media: GetDataMedia?, ) data class GetDataRoot( @@ -1163,7 +1163,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class GetSearchPage( - @JsonProperty("Page") val Page: GetSearchData?, + @JsonProperty("Page") val page: GetSearchData?, ) data class GetSearchData( 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 00f8d00cfb8..f819cd3bb63 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 @@ -119,8 +119,6 @@ class LocalList : SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, -// ListSorting.RatingHigh, -// ListSorting.RatingLow, ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index 24ef7136bda..6046a0f234e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -19,14 +19,18 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import java.net.URL import java.security.SecureRandom import java.text.ParseException import java.text.SimpleDateFormat -import java.util.* +import java.time.Instant +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.TimeZone /** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */ const val MAL_MAX_SEARCH_LIMIT = 25 @@ -51,7 +55,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } override fun loginInfo(): AuthAPI.LoginInfo? { - //getMalUser(true)? getKey(accountId, MAL_USER_KEY)?.let { user -> return AuthAPI.LoginInfo( profilePicture = user.picture, @@ -84,7 +87,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { this.name, node.id.toString(), "$mainUrl/anime/${node.id}/", - node.main_picture?.large ?: node.main_picture?.medium + node.mainPicture?.large ?: node.mainPicture?.medium ) } } @@ -178,7 +181,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private fun parseDate(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd")?.parse(string ?: return null)?.time + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(string ?: return null)?.time } catch (e: Exception) { null } @@ -190,7 +193,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { apiName = this.name, syncId = node.id.toString(), url = "$mainUrl/anime/${node.id}", - posterUrl = node.main_picture?.large + posterUrl = node.mainPicture?.large ) } @@ -244,12 +247,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { val internalId = id.toIntOrNull() ?: return null val data = - getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status") + getDataAboutMalId(internalId)?.myListStatus //?: throw ErrorLoadingException("No my_list_status") return SyncAPI.SyncStatus( score = data?.score, - status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)) , + status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)), isFavorite = null, - watchedEpisodes = data?.num_episodes_watched, + watchedEpisodes = data?.numEpisodesWatched, ) } @@ -291,7 +294,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private fun parseDateLong(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse( + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse( string ?: return null )?.time?.div(1000) } catch (e: Exception) { @@ -302,7 +305,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleRedirect(url: String): Boolean { val sanitizer = - splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR + splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR val state = sanitizer["state"]!! if (state == "RequestID$requestId") { val currentCode = sanitizer["code"]!! @@ -351,9 +354,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { try { if (response != "") { val token = parseJson(response) - setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime)) - setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token) - setKey(accountId, MAL_TOKEN_KEY, token.access_token) + setKey(accountId, MAL_UNIXTIME_KEY, (token.expiresIn + unixTime)) + setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refreshToken) + setKey(accountId, MAL_TOKEN_KEY, token.accessToken) requireLibraryRefresh = true } } catch (e: Exception) { @@ -395,53 +398,53 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class Node( @JsonProperty("id") val id: Int, @JsonProperty("title") val title: String, - @JsonProperty("main_picture") val main_picture: MainPicture?, - @JsonProperty("alternative_titles") val alternative_titles: AlternativeTitles?, - @JsonProperty("media_type") val media_type: String?, - @JsonProperty("num_episodes") val num_episodes: Int?, + @JsonProperty("main_picture") val mainPicture: MainPicture?, + @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?, + @JsonProperty("media_type") val mediaType: String?, + @JsonProperty("num_episodes") val numEpisodes: Int?, @JsonProperty("status") val status: String?, - @JsonProperty("start_date") val start_date: String?, - @JsonProperty("end_date") val end_date: String?, - @JsonProperty("average_episode_duration") val average_episode_duration: Int?, + @JsonProperty("start_date") val startDate: String?, + @JsonProperty("end_date") val endDate: String?, + @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?, @JsonProperty("synopsis") val synopsis: String?, @JsonProperty("mean") val mean: Double?, @JsonProperty("genres") val genres: List?, @JsonProperty("rank") val rank: Int?, @JsonProperty("popularity") val popularity: Int?, - @JsonProperty("num_list_users") val num_list_users: Int?, - @JsonProperty("num_favorites") val num_favorites: Int?, - @JsonProperty("num_scoring_users") val num_scoring_users: Int?, - @JsonProperty("start_season") val start_season: StartSeason?, + @JsonProperty("num_list_users") val numListUsers: Int?, + @JsonProperty("num_favorites") val numFavorites: Int?, + @JsonProperty("num_scoring_users") val numScoringUsers: Int?, + @JsonProperty("start_season") val startSeason: StartSeason?, @JsonProperty("broadcast") val broadcast: Broadcast?, @JsonProperty("nsfw") val nsfw: String?, - @JsonProperty("created_at") val created_at: String?, - @JsonProperty("updated_at") val updated_at: String? + @JsonProperty("created_at") val createdAt: String?, + @JsonProperty("updated_at") val updatedAt: String? ) data class ListStatus( @JsonProperty("status") val status: String?, @JsonProperty("score") val score: Int, - @JsonProperty("num_episodes_watched") val num_episodes_watched: Int, - @JsonProperty("is_rewatching") val is_rewatching: Boolean, - @JsonProperty("updated_at") val updated_at: String, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, + @JsonProperty("is_rewatching") val isRewatching: Boolean, + @JsonProperty("updated_at") val updatedAt: String, ) data class Data( @JsonProperty("node") val node: Node, - @JsonProperty("list_status") val list_status: ListStatus?, + @JsonProperty("list_status") val listStatus: ListStatus?, ) { fun toLibraryItem(): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( this.node.title, "https://myanimelist.net/anime/${this.node.id}/", this.node.id.toString(), - this.list_status?.num_episodes_watched, - this.node.num_episodes, - this.list_status?.score?.times(10), - parseDateLong(this.list_status?.updated_at), + this.listStatus?.numEpisodesWatched, + this.node.numEpisodes, + this.listStatus?.score?.times(10), + parseDateLong(this.listStatus?.updatedAt), "MAL", TvType.Anime, - this.node.main_picture?.large ?: this.node.main_picture?.medium, + this.node.mainPicture?.large ?: this.node.mainPicture?.medium, null, null, plot = this.node.synopsis, @@ -470,8 +473,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ) data class Broadcast( - @JsonProperty("day_of_the_week") val day_of_the_week: String?, - @JsonProperty("start_time") val start_time: String? + @JsonProperty("day_of_the_week") val dayOfTheWeek: String?, + @JsonProperty("start_time") val startTime: String? ) private fun getMalAnimeListCached(): Array? { @@ -491,14 +494,14 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata { val list = getMalAnimeListSmart()?.groupBy { - convertToStatus(it.list_status?.status ?: "").stringRes + convertToStatus(it.listStatus?.status ?: "").stringRes }?.mapValues { group -> group.value.map { it.toLibraryItem() } } ?: emptyMap() // To fill empty lists when MAL does not return them val baseMap = - MalStatusType.values().filter { it.value >= 0 }.associate { + MalStatusType.entries.filter { it.value >= 0 }.associate { it.stringRes to emptyList() } @@ -573,7 +576,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ).text val values = parseJson(res) val titles = - values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) } + values.data.map { MalTitleHolder(it.listStatus, it.node.id, it.node.title) } for (t in titles) { allTitles[t.id] = t } @@ -582,11 +585,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } } - fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { + private fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { // No time remaining if the show has already ended try { endDate?.let { - if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null + if (SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(it) + ?.before(Date.from(Instant.now())) != false + ) return@convertJapanTimeToTimeRemaining null } } catch (e: ParseException) { logError(e) @@ -603,7 +608,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH) val currentYear = currentDate.get(Calendar.YEAR) - val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm") + val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm", Locale.getDefault()) dateFormat.timeZone = TimeZone.getTimeZone("Japan") val parsedDate = dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null @@ -647,13 +652,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { id: Int, status: MalStatusType? = null, score: Int? = null, - num_watched_episodes: Int? = null, + numWatchedEpisodes: Int? = null, ): Boolean { val res = setScoreRequest( id, if (status == null) null else malStatusAsString[maxOf(0, status.value)], score, - num_watched_episodes + numWatchedEpisodes ) return if (res.isNullOrBlank()) { @@ -670,17 +675,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } } + @Suppress("UNCHECKED_CAST") private suspend fun setScoreRequest( id: Int, status: String? = null, score: Int? = null, - num_watched_episodes: Int? = null, + numWatchedEpisodes: Int? = null, ): String? { val data = mapOf( "status" to status, "score" to score?.toString(), - "num_watched_episodes" to num_watched_episodes?.toString() - ).filter { it.value != null } as Map + "num_watched_episodes" to numWatchedEpisodes?.toString() + ).filterValues { it != null } as Map return app.put( "$apiUrl/v2/anime/$id/my_list_status", @@ -693,10 +699,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class ResponseToken( - @JsonProperty("token_type") val token_type: String, - @JsonProperty("expires_in") val expires_in: Int, - @JsonProperty("access_token") val access_token: String, - @JsonProperty("refresh_token") val refresh_token: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("expires_in") val expiresIn: Int, + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("refresh_token") val refreshToken: String, ) data class MalRoot( @@ -705,7 +711,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class MalDatum( @JsonProperty("node") val node: MalNode, - @JsonProperty("list_status") val list_status: MalStatus, + @JsonProperty("list_status") val listStatus: MalStatus, ) data class MalNode( @@ -722,16 +728,16 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class MalStatus( @JsonProperty("status") val status: String, @JsonProperty("score") val score: Int, - @JsonProperty("num_episodes_watched") val num_episodes_watched: Int, - @JsonProperty("is_rewatching") val is_rewatching: Boolean, - @JsonProperty("updated_at") val updated_at: String, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, + @JsonProperty("is_rewatching") val isRewatching: Boolean, + @JsonProperty("updated_at") val updatedAt: String, ) data class MalUser( @JsonProperty("id") val id: Int, @JsonProperty("name") val name: String, @JsonProperty("location") val location: String, - @JsonProperty("joined_at") val joined_at: String, + @JsonProperty("joined_at") val joinedAt: String, @JsonProperty("picture") val picture: String?, ) @@ -744,9 +750,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class SmallMalAnime( @JsonProperty("id") val id: Int, @JsonProperty("title") val title: String?, - @JsonProperty("num_episodes") val num_episodes: Int, - @JsonProperty("my_list_status") val my_list_status: MalStatus?, - @JsonProperty("main_picture") val main_picture: MalMainPicture?, + @JsonProperty("num_episodes") val numEpisodes: Int, + @JsonProperty("my_list_status") val myListStatus: MalStatus?, + @JsonProperty("main_picture") val mainPicture: MalMainPicture?, ) data class MalSearchNode( 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 6412ff1b79b..37b95614661 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 @@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager -import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.AppUtils import okhttp3.Interceptor import okhttp3.Response @@ -30,10 +29,10 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi companion object { const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile - const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" - const val host = "https://api.opensubtitles.com/api/v1" + const val API_KEY = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" + const val HOST = "https://api.opensubtitles.com/api/v1" const val TAG = "OPENSUBS" - const val coolDownDuration: Long = 1000L * 30L // CoolDown if 429 error code in ms + const val COOLDOWN_DURATION: Long = 1000L * 30L // CoolDown if 429 error code in ms var currentCoolDown: Long = 0L var currentSession: SubtitleOAuthEntity? = null } @@ -49,7 +48,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi chain.request().newBuilder() .removeHeader("user-agent") .addHeader("user-agent", userAgent) - .addHeader("Api-Key", apiKey) + .addHeader("Api-Key", API_KEY) .build() ) } @@ -66,7 +65,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi } private fun throwGotTooManyRequests() { - currentCoolDown = unixTimeMs + coolDownDuration + currentCoolDown = unixTimeMs + COOLDOWN_DURATION throw ErrorLoadingException("Too many requests") } @@ -115,7 +114,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi private suspend fun initLogin(username: String, password: String): Boolean { //Log.i(TAG, "DATA = [$username] [$password]") val response = app.post( - url = "$host/login", + url = "$HOST/login", headers = mapOf( "Content-Type" to "application/json", ), @@ -134,7 +133,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi SubtitleOAuthEntity( user = username, pass = password, - access_token = token.token ?: run { + accessToken = token.token ?: run { return false }) ) @@ -197,8 +196,8 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val searchQueryUrl = when (imdbId > 0) { //Use imdb_id to search if its valid - true -> "$host/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" - false -> "$host/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" + true -> "$HOST/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" + false -> "$HOST/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" } val req = app.get( @@ -233,7 +232,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber val year = featureDetails?.year ?: query.year val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie - val isHearingImpaired = attr.hearing_impaired ?: false + val isHearingImpaired = attr.hearingImpaired ?: false //Log.i(TAG, "Result id/name => ${item.id} / $name") item.attributes?.files?.forEach { file -> val resultData = file.fileId?.toString() ?: "" @@ -266,11 +265,11 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi throwIfCantDoRequest() val req = app.post( - url = "$host/download", + url = "$HOST/download", headers = mapOf( Pair( "Authorization", - "Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}" + "Bearer ${currentSession?.accessToken ?: throw ErrorLoadingException("No access token active in current session")}" ), Pair("Content-Type", "application/json"), Pair("Accept", "*/*") @@ -299,7 +298,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi data class SubtitleOAuthEntity( var user: String, var pass: String, - var access_token: String, + var accessToken: String, ) data class OAuthToken( @@ -324,7 +323,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi @JsonProperty("url") var url: String? = null, @JsonProperty("files") var files: List? = listOf(), @JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(), - @JsonProperty("hearing_impaired") var hearing_impaired: Boolean? = null, + @JsonProperty("hearing_impaired") var hearingImpaired: Boolean? = null, ) data class ResultFiles( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 27975d1902d..e5db626ba79 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -38,6 +38,7 @@ import java.security.SecureRandom import java.text.SimpleDateFormat import java.time.Instant import java.util.Date +import java.util.Locale import java.util.TimeZone import kotlin.time.Duration import kotlin.time.DurationUnit @@ -144,8 +145,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } companion object { - private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID - private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET + private const val CLIENT_ID: String = BuildConfig.SIMKL_CLIENT_ID + private const val CLIENT_SECRET: String = BuildConfig.SIMKL_CLIENT_SECRET private var lastLoginState = "" const val SIMKL_TOKEN_KEY: String = "simkl_token" @@ -154,10 +155,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time" /** 2014-09-01T09:10:11Z -> 1409562611 */ - private const val simklDateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + private const val SIMKL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'" fun getUnixTime(string: String?): Long? { return try { - SimpleDateFormat(simklDateFormat).apply { + SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply { this.timeZone = TimeZone.getTimeZone("UTC") }.parse( string ?: return null @@ -171,7 +172,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** 1409562611 -> 2014-09-01T09:10:11Z */ fun getDateTime(unixTime: Long?): String? { return try { - SimpleDateFormat(simklDateFormat).apply { + SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply { this.timeZone = TimeZone.getTimeZone("UTC") }.format( Date.from( @@ -208,7 +209,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { companion object { fun fromString(string: String): SimklListStatusType? { - return SimklListStatusType.values().firstOrNull { + return SimklListStatusType.entries.firstOrNull { it.originalName == string } } @@ -219,17 +220,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonInclude(JsonInclude.Include.NON_EMPTY) data class TokenRequest( @JsonProperty("code") val code: String, - @JsonProperty("client_id") val client_id: String = clientId, - @JsonProperty("client_secret") val client_secret: String = clientSecret, - @JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl", - @JsonProperty("grant_type") val grant_type: String = "authorization_code" + @JsonProperty("client_id") val clientId: String = CLIENT_ID, + @JsonProperty("client_secret") val clientSecret: String = CLIENT_SECRET, + @JsonProperty("redirect_uri") val redirectUri: String = "$APP_STRING://simkl", + @JsonProperty("grant_type") val grantType: String = "authorization_code" ) data class TokenResponse( /** No expiration date */ - val access_token: String, - val token_type: String, - val scope: String + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("scope") val scope: String ) // ------------------- @@ -261,15 +262,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { // ------------------- data class ActivitiesResponse( - val all: String?, - val tv_shows: UpdatedAt, - val anime: UpdatedAt, - val movies: UpdatedAt, + @JsonProperty("all") val all: String?, + @JsonProperty("tv_shows") val tvShows: UpdatedAt, + @JsonProperty("anime") val anime: UpdatedAt, + @JsonProperty("movies") val movies: UpdatedAt, ) { data class UpdatedAt( - val all: String?, - val removed_from_list: String?, - val rated_at: String?, + @JsonProperty("all") val all: String?, + @JsonProperty("removed_from_list") val removedFromList: String?, + @JsonProperty("rated_at") val ratedAt: String?, ) } @@ -308,7 +309,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("title") val title: String?, @JsonProperty("year") val year: Int?, @JsonProperty("ids") val ids: Ids?, - @JsonProperty("total_episodes") val total_episodes: Int? = null, + @JsonProperty("total_episodes") val totalEpisodes: Int? = null, @JsonProperty("status") val status: String? = null, @JsonProperty("poster") val poster: String? = null, @JsonProperty("type") val type: String? = null, @@ -540,7 +541,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } debugPrint { "Requesting episodes from $url" } - return app.get(url, params = mapOf("client_id" to clientId)) + return app.get(url, params = mapOf("client_id" to CLIENT_ID)) .parsedSafe>()?.also { val cacheTime = if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value @@ -558,7 +559,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("seasons") seasons: List? = null, @JsonProperty("episodes") episodes: List? = null, @JsonProperty("rating") val rating: Int? = null, - @JsonProperty("rated_at") val rated_at: String? = null, + @JsonProperty("rated_at") val ratedAt: String? = null, ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -567,7 +568,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("year") year: Int?, @JsonProperty("ids") ids: Ids?, @JsonProperty("rating") val rating: Int, - @JsonProperty("rated_at") val rated_at: String? = getDateTime(unixTime) + @JsonProperty("rated_at") val ratedAt: String? = getDateTime(unixTime) ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -576,7 +577,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("year") year: Int?, @JsonProperty("ids") ids: Ids?, @JsonProperty("to") val to: String, - @JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime) + @JsonProperty("watched_at") val watchedAt: String? = getDateTime(unixTime) ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -631,24 +632,24 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } interface Metadata { - val last_watched_at: String? + val lastWatchedAt: String? val status: String? - val user_rating: Int? - val last_watched: String? - val watched_episodes_count: Int? - val total_episodes_count: Int? + val userRating: Int? + val lastWatched: String? + val watchedEpisodesCount: Int? + val totalEpisodesCount: Int? fun getIds(): ShowMetadata.Show.Ids fun toLibraryItem(): SyncAPI.LibraryItem } data class MovieMetadata( - override val last_watched_at: String?, - override val status: String, - override val user_rating: Int?, - override val last_watched: String?, - override val watched_episodes_count: Int?, - override val total_episodes_count: Int?, + @JsonProperty("last_watched_at") override val lastWatchedAt: String?, + @JsonProperty("status") override val status: String, + @JsonProperty("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, val movie: ShowMetadata.Show ) : Metadata { override fun getIds(): ShowMetadata.Show.Ids { @@ -660,10 +661,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.movie.title, "https://simkl.com/tv/${movie.ids.simkl}", movie.ids.simkl.toString(), - this.watched_episodes_count, - this.total_episodes_count, - this.user_rating?.times(10), - getUnixTime(last_watched_at) ?: 0, + this.watchedEpisodesCount, + this.totalEpisodesCount, + this.userRating?.times(10), + getUnixTime(lastWatchedAt) ?: 0, "Simkl", TvType.Movie, this.movie.poster?.let { getPosterUrl(it) }, @@ -675,12 +676,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } data class ShowMetadata( - @JsonProperty("last_watched_at") override val last_watched_at: String?, + @JsonProperty("last_watched_at") override val lastWatchedAt: String?, @JsonProperty("status") override val status: String, - @JsonProperty("user_rating") override val user_rating: Int?, - @JsonProperty("last_watched") override val last_watched: String?, - @JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?, - @JsonProperty("total_episodes_count") override val total_episodes_count: Int?, + @JsonProperty("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, @JsonProperty("show") val show: Show ) : Metadata { override fun getIds(): Show.Ids { @@ -692,10 +693,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.show.title, "https://simkl.com/tv/${show.ids.simkl}", show.ids.simkl.toString(), - this.watched_episodes_count, - this.total_episodes_count, - this.user_rating?.times(10), - getUnixTime(last_watched_at) ?: 0, + this.watchedEpisodesCount, + this.totalEpisodesCount, + this.userRating?.times(10), + getUnixTime(lastWatchedAt) ?: 0, "Simkl", TvType.Anime, this.show.poster?.let { getPosterUrl(it) }, @@ -749,7 +750,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { chain.request() .newBuilder() .addHeader("Authorization", "Bearer $token") - .addHeader("simkl-api-key", clientId) + .addHeader("simkl-api-key", CLIENT_ID) .build() ) } @@ -810,7 +811,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { val episodeConstructor = SimklEpisodeConstructor( searchResult.ids?.simkl, searchResult.type, - searchResult.total_episodes, + searchResult.totalEpisodes, searchResult.hasEnded() ) @@ -832,12 +833,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ) } ?: return null, - score = foundItem.user_rating, - watchedEpisodes = foundItem.watched_episodes_count, - maxEpisodes = searchResult.total_episodes, + score = foundItem.userRating, + watchedEpisodes = foundItem.watchedEpisodesCount, + maxEpisodes = searchResult.totalEpisodes, episodeConstructor = episodeConstructor, - oldEpisodes = foundItem.watched_episodes_count ?: 0, - oldScore = foundItem.user_rating, + oldEpisodes = foundItem.watchedEpisodesCount ?: 0, + oldScore = foundItem.userRating, oldStatus = foundItem.status ) } else { @@ -845,7 +846,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { status = SyncWatchType.fromInternalId(SimklListStatusType.None.value), score = 0, watchedEpisodes = 0, - maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes, + maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.totalEpisodes, episodeConstructor = episodeConstructor, oldEpisodes = 0, oldStatus = null, @@ -891,12 +892,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ - suspend fun searchByIds(serviceMap: Map): Array? { + private suspend fun searchByIds(serviceMap: Map): Array? { if (serviceMap.isEmpty()) return emptyArray() return app.get( "$mainUrl/search/id", - params = mapOf("client_id" to clientId) + serviceMap.map { (service, id) -> + params = mapOf("client_id" to CLIENT_ID) + serviceMap.map { (service, id) -> service.originalName to id } ).parsedSafe() @@ -904,14 +905,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun search(name: String): List? { return app.get( - "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name) + "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name) ).parsedSafe>()?.mapNotNull { it.toSyncSearchResult() } } override fun authenticate(activity: FragmentActivity?) { lastLoginState = BigInteger(130, SecureRandom()).toString(32) val url = - "https://simkl.com/oauth/authorize?response_type=code&client_id=$clientId&redirect_uri=$appString://${redirectUrl}&state=$lastLoginState" + "https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}&state=$lastLoginState" openBrowser(url, activity) } @@ -961,15 +962,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { val activities = getActivities() val lastCacheUpdate = getKey(accountId, SIMKL_CACHED_LIST_TIME) val lastRemoval = listOf( - activities?.tv_shows?.removed_from_list, - activities?.anime?.removed_from_list, - activities?.movies?.removed_from_list + activities?.tvShows?.removedFromList, + activities?.anime?.removedFromList, + activities?.movies?.removedFromList ).maxOf { getUnixTime(it) ?: -1 } val lastRealUpdate = listOf( - activities?.tv_shows?.all, + activities?.tvShows?.all, activities?.anime?.all, activities?.movies?.all, ).maxOf { @@ -1039,7 +1040,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getDevicePin(): OAuth2API.PinAuthData? { val pinAuthResp = app.get( - "$mainUrl/oauth/pin?client_id=$clientId&redirect_uri=$appString://${redirectUrl}" + "$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}" ).parsedSafe() ?: return null return OAuth2API.PinAuthData( @@ -1053,7 +1054,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean { val pinAuthResp = app.get( - "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$clientId" + "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$CLIENT_ID" ).parsedSafe() ?: return false if (pinAuthResp.accessToken != null) { @@ -1088,7 +1089,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ).parsedSafe() ?: return false switchToNewAccount() - setKey(accountId, SIMKL_TOKEN_KEY, token.access_token) + setKey(accountId, SIMKL_TOKEN_KEY, token.accessToken) val user = getUser() if (user == null) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt index 0e233ece8da..8dad1f88cfe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt @@ -59,6 +59,7 @@ class SubSourceApi : AbstractSubProvider { it?.subs?.filter { sub -> sub.releaseName!!.contains( String.format( + null, "E%02d", query.epNumber ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index a075cc2e3ba..9150cfc5e0c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -50,7 +50,7 @@ class APIRepository(val api: MainAPI) { private val cache = threadSafeListOf() private var cacheIndex: Int = 0 - const val cacheSize = 20 + const val CACHE_SIZE = 20 } private fun afterPluginsLoaded(forceReload: Boolean) { @@ -94,9 +94,9 @@ class APIRepository(val api: MainAPI) { val add = SavedLoadResponse(unixTime, response, lookingForHash) synchronized(cache) { - if (cache.size > cacheSize) { + if (cache.size > CACHE_SIZE) { cache[cacheIndex] = add // rolling cache - cacheIndex = (cacheIndex + 1) % cacheSize + cacheIndex = (cacheIndex + 1) % CACHE_SIZE } else { cache.add(add) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt index d90177f5069..e930961c550 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt @@ -112,6 +112,7 @@ abstract class BaseAdapter< holder.onViewDetachedFromWindow() } + @Suppress("UNCHECKED_CAST") fun save(recyclerView: RecyclerView) { for (child in recyclerView.children) { val holder = @@ -124,6 +125,7 @@ abstract class BaseAdapter< stateViewModel.layoutManagerStates[id]?.clear() } + @Suppress("UNCHECKED_CAST") private fun getState(holder: ViewHolderState): S? = stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 6bafa975f49..1eaac5056ef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -6,6 +6,7 @@ import android.view.Menu import android.view.View.* import android.widget.* import androidx.appcompat.app.AlertDialog +import androidx.media3.common.util.UnstableApi import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule @@ -263,6 +264,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi var isLoadingMore = false + override fun onMediaStatusUpdated() { super.onMediaStatusUpdated() val meta = getCurrentMetaData() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt index 1a9549e175e..78ad2a6bfcb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt @@ -8,8 +8,8 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs -class GrdLayoutManager(val context: Context, _spanCount: Int) : - GridLayoutManager(context, _spanCount) { +class GrdLayoutManager(val context: Context, spanCount: Int) : + GridLayoutManager(context, spanCount) { override fun onFocusSearchFailed( focused: View, focusDirection: Int, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt index c70417763d0..4879d2e0b8e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt @@ -51,7 +51,7 @@ class EasterEggMonke : AppCompatActivity() { FrameLayout.LayoutParams.WRAP_CONTENT) binding.frame.addView(newStar) - newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX + newStar.scaleX += Math.random().toFloat() * 1.5f newStar.scaleY = newStar.scaleX starW *= newStar.scaleX starH *= newStar.scaleY diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt index f721401e7d2..12a5ae2a29e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt @@ -15,7 +15,7 @@ open class NonFinalAdapterListUpdateCallback /** * Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter. * - * @param adapter The Adapter to send updates to. + * @param mAdapter The Adapter to send updates to. */(private var mAdapter: RecyclerView.Adapter<*>) : ListUpdateCallback { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt index 9532d1a9b91..b778ba5a754 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt @@ -13,7 +13,7 @@ enum class WatchType(val internalId: Int, @StringRes val stringRes: Int, @Drawab NONE(5, R.string.type_none, R.drawable.ic_baseline_add_24); companion object { - fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE + fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE } } @@ -36,6 +36,6 @@ enum class SyncWatchType(val internalId: Int, @StringRes val stringRes: Int, @Dr REWATCHING(5, R.string.type_re_watching, R.drawable.ic_baseline_bookmark_24); companion object { - fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE + fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt index 15e66b38f3e..5e2b97e57d5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt @@ -8,8 +8,10 @@ import android.webkit.JavascriptInterface import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient +import androidx.annotation.OptIn import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import androidx.media3.common.util.UnstableApi import androidx.navigation.fragment.findNavController import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.USER_AGENT @@ -29,6 +31,7 @@ class WebviewFragment : Fragment() { } binding?.webView?.webViewClient = object : WebViewClient() { + @OptIn(UnstableApi::class) override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest? diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index f10e103e753..45132131977 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -54,6 +54,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } init { + @Suppress("LeakingThis") resetViewData() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt index d97a4b88798..20a4446114b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt @@ -13,7 +13,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper class DownloadButton(context: Context, attributeSet: AttributeSet) : PieFetchButton(context, attributeSet) { - var mainText: TextView? = null + private var mainText: TextView? = null override fun onAttachedToWindow() { super.onAttachedToWindow() progressText = findViewById(R.id.result_movie_download_text_precentage) 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 ebed901f928..b25486eb174 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 @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder 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.isBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState(view) { @@ -54,7 +54,7 @@ class HomeChildItemAdapter( var hasNext: Boolean = false override fun onCreateContent(parent: ViewGroup): ViewHolderState { - val expanded = parent.context.IsBottomLayout() + val expanded = parent.context.isBottomLayout() /* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid val root = LayoutInflater.from(parent.context).inflate(layout, parent, false) @@ -133,7 +133,6 @@ class HomeChildItemAdapter( item, position, holder.itemView, - null, // nextFocusBehavior, nextFocusUp, nextFocusDown ) 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 82a92d80a45..49de25034aa 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 @@ -17,7 +17,6 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.* import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -234,7 +233,7 @@ class HomeFragment : Fragment() { return bottomSheetDialogBuilder } - fun getPairList( + private fun getPairList( anime: Chip?, cartoons: Chip?, tvs: Chip?, 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 916cb9aeb80..8bc0aa287ea 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,6 +1,8 @@ package com.lagradost.cloudstream3.ui.home +import android.os.Build import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -53,12 +55,12 @@ open class ParentItemAdapter( "value", recyclerView?.layoutManager?.onSaveInstanceState() ) - (recyclerView?.adapter as? BaseAdapter<*,*>)?.save(recyclerView) + (recyclerView?.adapter as? BaseAdapter<*, *>)?.save(recyclerView) } override fun restore(state: Bundle) { (binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState( - state.getParcelable("value") + state.getSafeParcelable("value") ) } } @@ -169,4 +171,9 @@ open class ParentItemAdapter( submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) } .toMutableList()) } -} \ No newline at end of file +} + +@Suppress("DEPRECATION") +inline fun Bundle.getSafeParcelable(key: String): T? = + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelable(key) + else getParcelable(key, T::class.java) \ 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 2e98dd1f9f7..339ef1e1254 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 @@ -117,15 +117,12 @@ class HomeParentItemAdapterPreview( } override fun restore(state: Bundle) { - state.getParcelable("resumeRecyclerView")?.let { recycle -> + state.getSafeParcelable("resumeRecyclerView")?.let { recycle -> resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - state.getParcelable("bookmarkRecyclerView")?.let { recycle -> + state.getSafeParcelable("bookmarkRecyclerView")?.let { recycle -> bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - //state.getInt("previewViewpager").let { recycle -> - // previewViewpager.setCurrentItem(recycle,true) - //} } val previewAdapter = HomeScrollAdapter(fragment = fragment) 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 9e70d088e68..24ca4df20c8 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 @@ -152,7 +152,7 @@ class HomeViewModel : ViewModel() { } }?.distinctBy { it.first } ?: return@launchSafe - val length = WatchType.values().size + val length = WatchType.entries.size val currentWatchTypes = mutableSetOf() for (watch in watchStatusIds) { @@ -387,7 +387,9 @@ class HomeViewModel : ViewModel() { } is Resource.Failure -> { + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") _page.postValue(data!!) + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") _preview.postValue(data!!) } @@ -397,9 +399,7 @@ class HomeViewModel : ViewModel() { } fun click(callback: SearchClickCallback) { - if (callback.action == SEARCH_ACTION_FOCUSED) { - //focusCallback(callback.card) - } else { + if (callback.action != SEARCH_ACTION_FOCUSED) { SearchHelper.handleSearchClickCallback(callback) } } @@ -516,7 +516,7 @@ class HomeViewModel : ViewModel() { } else { _page.postValue(Resource.Loading()) if (preferredApiName != null) - _apiName.postValue(preferredApiName) + _apiName.postValue(preferredApiName!!) } } else { // if the api is found, then set it to it and save key 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 7144de09709..5b240693bc9 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 @@ -600,8 +600,4 @@ class LibraryFragment : Fragment() { } } -class MenuSearchView(context: Context) : SearchView(context) { - override fun onActionViewCollapsed() { - super.onActionViewCollapsed() - } -} \ No newline at end of file +class MenuSearchView(context: Context) : SearchView(context) \ No newline at end of file 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 cfd22220c5b..0110187f603 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 @@ -15,6 +15,7 @@ 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.home.getSafeParcelable import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -32,7 +33,7 @@ class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding) } override fun restore(state: Bundle) { - state.getParcelable("pageRecyclerview")?.let { recycle -> + state.getSafeParcelable("pageRecyclerview")?.let { recycle -> binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle) } } 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 9d838c97451..88c34c8762c 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 @@ -25,6 +25,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.media3.common.PlaybackException +import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import androidx.media3.ui.* @@ -216,7 +217,7 @@ abstract class AbstractPlayerFragment( return } player.handleEvent( - CSPlayerEvent.values()[intent.getIntExtra( + CSPlayerEvent.entries[intent.getIntExtra( EXTRA_CONTROL_TYPE, 0 )], source = PlayerEventSource.UI @@ -603,12 +604,12 @@ abstract class AbstractPlayerFragment( } fun nextResize() { - resizeMode = (resizeMode + 1) % PlayerResize.values().size + resizeMode = (resizeMode + 1) % PlayerResize.entries.size resize(resizeMode, true) } fun resize(resize: Int, showToast: Boolean) { - resize(PlayerResize.values()[resize], showToast) + resize(PlayerResize.entries[resize], showToast) } @SuppressLint("UnsafeOptInUsageError") 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 735e4095534..86d67b28a4c 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 @@ -9,7 +9,11 @@ import android.os.Looper import android.util.Log import android.util.Rational import android.widget.FrameLayout -import androidx.media3.common.C.* +import androidx.annotation.OptIn +import androidx.media3.common.C.TIME_UNSET +import androidx.media3.common.C.TRACK_TYPE_AUDIO +import androidx.media3.common.C.TRACK_TYPE_TEXT +import androidx.media3.common.C.TRACK_TYPE_VIDEO import androidx.media3.common.Format import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes @@ -19,9 +23,10 @@ import androidx.media3.common.TrackGroup import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.Tracks import androidx.media3.common.VideoSize +import androidx.media3.common.util.UnstableApi import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.datasource.DataSource -import androidx.media3.datasource.DefaultDataSourceFactory +import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.cache.CacheDataSource @@ -66,7 +71,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File -import java.lang.IllegalArgumentException import java.util.UUID import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext @@ -84,7 +88,7 @@ const val toleranceBeforeUs = 300_000L * seek position, in microseconds. Must be non-negative. */ const val toleranceAfterUs = 300_000L - +@OptIn(UnstableApi::class) class CS3IPlayer : IPlayer { private var isPlaying = false private var exoPlayer: ExoPlayer? = null @@ -257,7 +261,6 @@ class CS3IPlayer : IPlayer { private var currentSubtitles: SubtitleData? = null - @SuppressLint("UnsafeOptInUsageError") private fun List.getTrack(id: String?): Pair? { if (id == null) return null // This beast of an expression does: @@ -342,7 +345,6 @@ class CS3IPlayer : IPlayer { }.flatten() } - @SuppressLint("UnsafeOptInUsageError") private fun Tracks.Group.getFormats(): List> { return (0 until this.mediaTrackGroup.length).mapNotNull { i -> if (this.isSupported) @@ -371,7 +373,6 @@ class CS3IPlayer : IPlayer { ) } - @SuppressLint("UnsafeOptInUsageError") override fun getVideoTracks(): CurrentTracks { val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList() val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO } @@ -391,7 +392,6 @@ class CS3IPlayer : IPlayer { /** * @return True if the player should be reloaded * */ - @SuppressLint("UnsafeOptInUsageError") override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { Log.i(TAG, "setPreferredSubtitles init $subtitle") currentSubtitles = subtitle @@ -451,7 +451,7 @@ class CS3IPlayer : IPlayer { } ?: false } - var currentSubtitleOffset: Long = 0 + private var currentSubtitleOffset: Long = 0 override fun setSubtitleOffset(offset: Long) { currentSubtitleOffset = offset @@ -459,7 +459,7 @@ class CS3IPlayer : IPlayer { } override fun getSubtitleOffset(): Long { - return currentSubtitleOffset //currentTextRenderer?.getRenderOffsetMs() ?: currentSubtitleOffset + return currentSubtitleOffset } override fun getCurrentPreferredSubtitle(): SubtitleData? { @@ -470,7 +470,6 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") override fun getAspectRatio(): Rational? { return exoPlayer?.videoFormat?.let { format -> Rational(format.width, format.height) @@ -481,14 +480,13 @@ class CS3IPlayer : IPlayer { subtitleHelper.setSubStyle(style) } - @SuppressLint("UnsafeOptInUsageError") override fun saveData() { Log.i(TAG, "saveData") updatedTime() exoPlayer?.let { exo -> playbackPosition = exo.currentPosition - currentWindow = exo.currentWindowIndex + currentWindow = exo.currentMediaItemIndex isPlaying = exo.isPlaying } } @@ -500,7 +498,7 @@ class CS3IPlayer : IPlayer { updatedTime() exoPlayer?.apply { - setPlayWhenReady(false) + playWhenReady = false stop() release() } @@ -563,7 +561,6 @@ class CS3IPlayer : IPlayer { var requestSubtitleUpdate: (() -> Unit)? = null - @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(headers: Map): HttpDataSource.Factory { val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT) return source.apply { @@ -571,7 +568,6 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory { val provider = getApiFromNameNull(link.source) val interceptor = provider?.getVideoInterceptor(link) @@ -604,53 +600,10 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") private fun Context.createOfflineSource(): DataSource.Factory { - return DefaultDataSourceFactory(this, USER_AGENT) + return DefaultDataSource.Factory(this, DefaultHttpDataSource.Factory().setUserAgent(USER_AGENT)) } - /*private fun getSubSources( - onlineSourceFactory: DataSource.Factory?, - offlineSourceFactory: DataSource.Factory?, - subHelper: PlayerSubtitleHelper, - ): Pair, List> { - val activeSubtitles = ArrayList() - val subSources = subHelper.getAllSubtitles().mapNotNull { sub -> - val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url)) - .setMimeType(sub.mimeType) - .setLanguage("_${sub.name}") - .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) - .build() - when (sub.origin) { - SubtitleOrigin.DOWNLOADED_FILE -> { - if (offlineSourceFactory != null) { - activeSubtitles.add(sub) - SingleSampleMediaSource.Factory(offlineSourceFactory) - .createMediaSource(subConfig, C.TIME_UNSET) - } else { - null - } - } - SubtitleOrigin.URL -> { - if (onlineSourceFactory != null) { - activeSubtitles.add(sub) - SingleSampleMediaSource.Factory(onlineSourceFactory) - .createMediaSource(subConfig, C.TIME_UNSET) - } else { - null - } - } - SubtitleOrigin.OPEN_SUBTITLES -> { - // TODO - throw NotImplementedError() - } - } - } - println("SUBSRC: ${subSources.size} activeSubtitles : ${activeSubtitles.size} of ${subHelper.getAllSubtitles().size} ") - return Pair(subSources, activeSubtitles) - }*/ - - @SuppressLint("UnsafeOptInUsageError") private fun getCache(context: Context, cacheSize: Long): SimpleCache? { return try { val databaseProvider = StandaloneDatabaseProvider(context) @@ -682,7 +635,6 @@ class CS3IPlayer : IPlayer { return getMediaItemBuilder(mimeType).setUri(url).build() } - @SuppressLint("UnsafeOptInUsageError") private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector { val trackSelector = DefaultTrackSelector(context) trackSelector.parameters = trackSelector.buildUponParameters() @@ -696,7 +648,6 @@ class CS3IPlayer : IPlayer { var currentTextRenderer: CustomTextRenderer? = null - @SuppressLint("UnsafeOptInUsageError") private fun buildExoPlayer( context: Context, mediaItemSlices: List, @@ -736,7 +687,7 @@ class CS3IPlayer : IPlayer { textRendererOutput, eventHandler.looper, CustomSubtitleDecoderFactory() - ).also { this.currentTextRenderer = it } + ).also { renderer -> this.currentTextRenderer = renderer } currentTextRenderer } else it }.toTypedArray() @@ -1033,7 +984,7 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") + //fixme: Use onPlaybackStateChanged(int) and onPlayWhenReadyChanged(boolean, int) instead. override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { exoPlayer?.let { exo -> event( @@ -1169,7 +1120,6 @@ class CS3IPlayer : IPlayer { private var lastTimeStamps: List = emptyList() - @SuppressLint("UnsafeOptInUsageError") override fun addTimeStamps(timeStamps: List) { lastTimeStamps = timeStamps timeStamps.forEach { timestamp -> @@ -1187,7 +1137,6 @@ class CS3IPlayer : IPlayer { updatedTime(source = PlayerEventSource.Player) } - @SuppressLint("UnsafeOptInUsageError") fun onRenderFirst() { if (hasUsedFirstRender) { // this insures that we only call this once per player load return @@ -1254,7 +1203,6 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") private fun getSubSources( onlineSourceFactory: HttpDataSource.Factory?, offlineSourceFactory: DataSource.Factory?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt index 20d093a636c..07ce413ea5a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player import android.content.Context import android.util.Log +import androidx.annotation.OptIn import androidx.preference.PreferenceManager import androidx.media3.common.Format import androidx.media3.common.MimeTypes @@ -31,7 +32,7 @@ import java.nio.charset.Charset * @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not * enough to identify the subtitle format. **/ -@UnstableApi +@OptIn(UnstableApi::class) class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { companion object { fun updateForcedEncoding(context: Context) { @@ -72,7 +73,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { RegexOption.IGNORE_CASE ), ) - val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\d\s]*?[])}]\s*""")) + val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\s]*?[])}]\s*""")) //https://emptycharacter.com/ //https://www.fileformat.info/info/unicode/char/200b/index.htm @@ -262,7 +263,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { } /** See https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java */ -@UnstableApi +@OptIn(UnstableApi::class) class CustomSubtitleDecoderFactory : SubtitleDecoderFactory { override fun supportsFormat(format: Format): Boolean { // return SubtitleDecoderFactory.DEFAULT.supportsFormat(format) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt index d6b0735d0f9..f2b863fb4eb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt @@ -1,11 +1,12 @@ package com.lagradost.cloudstream3.ui.player import android.os.Looper +import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.text.SubtitleDecoderFactory import androidx.media3.exoplayer.text.TextOutput -@UnstableApi +@OptIn(UnstableApi::class) class CustomTextRenderer( offset: Long, output: TextOutput?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index 3b242172f0b..a8a3106aaa0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -49,7 +49,7 @@ class DownloadFileGenerator( return null } - fun cleanDisplayName(name: String): String { + private fun cleanDisplayName(name: String): String { return name.substringBeforeLast('.').trim() } 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 92ef279d442..4279b54218b 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 @@ -8,14 +8,10 @@ import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.safefile.SafeFile import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri -const val DTAG = "PlayerActivity" - class DownloadedPlayerActivity : AppCompatActivity() { private val dTAG = "DownloadedPlayerAct" 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 ef7d6bc11b2..b2e807496ca 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 @@ -25,6 +25,7 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.animation.AnimationUtils +import androidx.annotation.OptIn import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.core.graphics.blue @@ -35,6 +36,7 @@ import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged +import androidx.media3.common.util.UnstableApi import androidx.preference.PreferenceManager import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.CommonActivity.keyEventListener @@ -50,7 +52,6 @@ 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.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 @@ -245,6 +246,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { fadeAnimation.duration = 100 fadeAnimation.fillAfter = true + @OptIn(UnstableApi::class) val sView = subView val sStyle = subStyle if (sView != null && sStyle != null) { @@ -300,42 +302,40 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun restoreOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation - var orientation = 0 - when (currentOrientation) { + val orientation = when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - - Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> - orientation = dynamicOrientation() + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE Configuration.ORIENTATION_PORTRAIT -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + + else -> dynamicOrientation() } activity.requestedOrientation = orientation } private fun toggleOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation - var orientation = 0 - when (currentOrientation) { + val orientation: Int = when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT - - Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> - orientation = dynamicOrientation() + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT Configuration.ORIENTATION_PORTRAIT -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + + else -> dynamicOrientation() } activity.requestedOrientation = orientation } open fun lockOrientation(activity: Activity) { - val display = + @Suppress("DEPRECATION") + val display = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + else activity.display!! val rotation = display.rotation val currentOrientation = activity.resources.configuration.orientation - var orientation = 0 + val orientation: Int when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> orientation = @@ -344,15 +344,14 @@ open class FullScreenPlayer : AbstractPlayerFragment() { else ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE - Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> - orientation = dynamicOrientation() - Configuration.ORIENTATION_PORTRAIT -> orientation = if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + + else -> orientation = dynamicOrientation() } activity.requestedOrientation = orientation } @@ -1167,6 +1166,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { return true } + @SuppressLint("GestureBackNavigation") private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean { if (hasNavigated) { autoHide() @@ -1581,7 +1581,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } // cs3 is peak media center - setRemainingTimeCounter(durationMode || Globals.isLayout(Globals.TV)) + setRemainingTimeCounter(durationMode || isLayout(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 1f7cc5bd3f8..8e8f6bf5f2a 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 @@ -6,6 +6,7 @@ import android.app.Dialog import android.content.Context import android.content.Intent import android.content.res.ColorStateList +import android.os.Build import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -13,6 +14,7 @@ import android.view.View import android.view.ViewGroup import android.widget.* import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.OptIn import androidx.core.animation.addListener import androidx.core.content.ContextCompat import androidx.core.view.isGone @@ -21,6 +23,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import androidx.media3.common.Format.NO_VALUE import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -63,6 +66,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.safefile.SafeFile import kotlinx.coroutines.Job +import java.io.Serializable import java.util.* import kotlin.math.abs @@ -234,7 +238,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun closestQuality(target: Int?): Qualities { if (target == null) return Qualities.Unknown - return Qualities.values().minBy { abs(it.value - target) } + return Qualities.entries.minBy { abs(it.value - target) } } private fun getLinkPriority( @@ -367,8 +371,6 @@ class GeneratorPlayer : FullScreenPlayer() { binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE binding.subtitleAdapter.adapter = arrayAdapter - val adapter = - binding.subtitleAdapter.adapter as? ArrayAdapter binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ -> currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener @@ -379,8 +381,8 @@ class GeneratorPlayer : FullScreenPlayer() { fun setSubtitlesList(list: List) { currentSubtitles = list - adapter?.clear() - adapter?.addAll(currentSubtitles) + arrayAdapter.clear() + arrayAdapter.addAll(currentSubtitles) } val currentTempMeta = getMetaData() @@ -522,7 +524,7 @@ class GeneratorPlayer : FullScreenPlayer() { //TODO: Set year text from currently loaded movie on Player //dialog.subtitles_search_year?.setText(currentTempMeta.year) } - + @OptIn(UnstableApi::class) private fun openSubPicker() { try { subsPathPicker.launch( @@ -795,7 +797,6 @@ class GeneratorPlayer : FullScreenPlayer() { settingsManager.edit().putString( ctx.getString(R.string.subtitles_encoding_key), prefValues[it] ).apply() - updateForcedEncoding(ctx) dismiss() player.seekTime(-1) // to update subtitles, a dirty trick @@ -1290,7 +1291,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun unwrapBundle(savedInstanceState: Bundle?) { Log.i(TAG, "unwrapBundle = $savedInstanceState") savedInstanceState?.let { bundle -> - sync.addSyncs(bundle.getSerializable("syncData") as? HashMap?) + sync.addSyncs(bundle.getSafeSerializable>("syncData")) } } @@ -1507,3 +1508,6 @@ class GeneratorPlayer : FullScreenPlayer() { } } } + +@Suppress("DEPRECATION") +inline fun Bundle.getSafeSerializable(key: String) : T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getSerializable(key) as? T else getSerializable(key, T::class.java) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 5f7161f7ee5..89c6f73b2e8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -8,7 +8,6 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink enum class PlayerEventType(val value: Int) { - //Stop(-1), Pause(0), Play(1), SeekForward(2), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index 89e3c8de987..07ea56dd345 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -4,7 +4,6 @@ import android.net.Uri import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.loadExtractor diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java index 3482f21c7ce..232440ccaff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java @@ -29,6 +29,7 @@ import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.text.Cue; @@ -66,7 +67,7 @@ * obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s * is delegated to a {@link TextOutput}. */ -@UnstableApi +@OptIn(markerClass = UnstableApi.class) public class NonFinalTextRenderer extends BaseRenderer implements Callback { private static final String TAG = "TextRenderer"; @@ -74,7 +75,7 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { /** * @param trackType The track type that the renderer handles. One of the {@link C} {@code * TRACK_TYPE_*} constants. - * @param outputHandler + * @param outputHandler todo description */ public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) { super(trackType); @@ -416,13 +417,11 @@ private void clearOutput() { @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_UPDATE_OUTPUT: - invokeUpdateOutputInternal((List) msg.obj); - return true; - default: - throw new IllegalStateException(); + if (msg.what == MSG_UPDATE_OUTPUT) { + invokeUpdateOutputInternal((List) msg.obj); + return true; } + throw new IllegalStateException(); } private void invokeUpdateOutputInternal(List cues) { @@ -441,7 +440,6 @@ private void invokeUpdateOutputInternal(List cues) { } ).collect(Collectors.toList()); - output.onCues(fixedCues); output.onCues(new CueGroup(fixedCues, 0L)); } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt index e6de1266b34..f00f8a61b09 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt @@ -4,8 +4,8 @@ import android.app.Activity import android.content.ContentUris import android.net.Uri import androidx.core.content.ContextCompat.getString +import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.safefile.SafeFile 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 1ba5a29f1d7..122eaa975a5 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 @@ -20,7 +20,7 @@ import kotlinx.coroutines.launch class PlayerGeneratorViewModel : ViewModel() { companion object { - val TAG = "PlayViewGen" + const val TAG = "PlayViewGen" } private var generator: IGenerator? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index 25d7e3dddad..02a7ee03d9c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -4,7 +4,9 @@ import android.util.Log import android.util.TypedValue import android.view.ViewGroup import android.widget.FrameLayout +import androidx.annotation.OptIn import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat @@ -47,6 +49,7 @@ data class SubtitleData( } } +@OptIn(UnstableApi::class) class PlayerSubtitleHelper { private var activeSubtitles: Set = emptySet() private var allSubtitles: Set = emptySet() 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 7c78ce63896..2d1feaab3a5 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 @@ -239,7 +239,11 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG // generated images 1:1 to idx of hsl private var images: Array = arrayOf() - private val TAG = "PreviewImgM3u8" + companion object { + private const val TAG = "PreviewImgM3u8" + } + + // prefixSum[i] = sum(hsl.ts[0..i].time) // where [0] = 0, [1] = hsl.ts[0].time aka time at start of segment, do [b] - [a] for range a,b @@ -388,13 +392,6 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG logError(t) continue } - - /* - val buffer = hsl.resolveLinkSafe(index) ?: continue - tmpFile?.writeBytes(buffer) - val buff = FileOutputStream(tmpFile) - retriever.setDataSource(buff.fd) - val frame = retriever.getFrameAtTime(0L)*/ } } @@ -412,14 +409,16 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe null } + companion object { + private const val TAG = "PreviewImgMp4" + } + override fun hasPreview(): Boolean { synchronized(images) { return loadedLod >= MIN_LOD } } - val TAG = "PreviewImgMp4" - override fun getPreviewImage(fraction: Float): Bitmap? { synchronized(images) { if (loadedLod < MIN_LOD) { @@ -524,7 +523,7 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) Log.i(TAG, "Generating preview for ${fraction * 100}%") val frame = durationUs * fraction - val img = retriever.image(frame.toLong(), params); + val img = retriever.image(frame.toLong(), params) if (!scope.isActive) return if (img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt index 90bd1ca7303..6943c6412dd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.player import android.util.Log +import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.LoadResponse diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt index 1e2c9f67338..ce457740d34 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt @@ -17,7 +17,6 @@ class PriorityAdapter(override val items: MutableList>) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return PriorityViewHolder( PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false), - //LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false) ) } @@ -31,10 +30,6 @@ class PriorityAdapter(override val items: MutableList>) : val binding: PlayerPrioritizeItemBinding, ) : RecyclerView.ViewHolder(binding.root) { fun bind(item: SourcePriority) { - /* val plusButton: ImageView = itemView.add_button - val subtractButton: ImageView = itemView.subtract_button - val priorityText: TextView = itemView.priority_text - val priorityNumber: TextView = itemView.priority_number*/ binding.priorityText.text = item.name fun updatePriority() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt index b587276fa66..45f6aa66088 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt @@ -29,8 +29,6 @@ class ProfilesAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProfilesViewHolder( PlayerQualityProfileItemBinding.inflate(LayoutInflater.from(parent.context),parent,false) - //LayoutInflater.from(parent.context) - // .inflate(R.layout.player_quality_profile_item, parent, false) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt index 96249db406f..3267efd73ce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.player.source_priority -import android.content.Context import androidx.annotation.StringRes import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey @@ -104,7 +103,7 @@ object QualityDataHelper { * Must under all circumstances at least return one profile **/ fun getProfiles(): List { - val availableTypes = QualityProfileType.values().toMutableList() + val availableTypes = QualityProfileType.entries.toMutableList() val profiles = (1..PROFILE_COUNT).map { profileNumber -> // Get the real type val type = getQualityProfileType(profileNumber) @@ -140,12 +139,12 @@ object QualityDataHelper { } } - QualityProfileType.values().forEach { + QualityProfileType.entries.forEach { if (it.unique) insertType(profiles, it) } debugAssert({ - !QualityProfileType.values().all { type -> + !QualityProfileType.entries.all { type -> !type.unique || profiles.any { it.type == type } } }, { "All unique quality types do not exist" }) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt index e3629158487..0537092c18b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt @@ -65,7 +65,7 @@ class QualityProfileDialog( setDefaultBtt.setOnClickListener { val currentProfile = getCurrentProfile() ?: return@setOnClickListener - val choices = QualityDataHelper.QualityProfileType.values() + val choices = QualityDataHelper.QualityProfileType.entries .filter { it != QualityDataHelper.QualityProfileType.None } val choiceNames = choices.map { txt(it.stringRes).asString(context) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt index 1b59882e282..bc6282af2ca 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -47,7 +47,7 @@ class SourcePriorityDialog( ) qualitiesRecyclerView.adapter = PriorityAdapter( - Qualities.values().mapNotNull { + Qualities.entries.mapNotNull { SourcePriority( it, Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 611889057dc..0ca326ddf82 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -3,8 +3,6 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.annotation.IdRes -import androidx.annotation.LayoutRes import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -70,8 +68,7 @@ class ActorAdaptor( } } - private inner class CardViewHolder - constructor( + private inner class CardViewHolder( val binding: CastItemBinding, private val focusCallback: (View?) -> Unit = {} ) : 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 06be6bd5fdf..d12521b39e6 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 @@ -169,8 +169,7 @@ class EpisodeAdapter( return cardList.size } - class EpisodeCardViewHolderLarge - constructor( + class EpisodeCardViewHolderLarge( val binding: ResultEpisodeLargeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, @@ -335,8 +334,7 @@ class EpisodeAdapter( } } - class EpisodeCardViewHolderSmall - constructor( + class EpisodeCardViewHolderSmall( val binding: ResultEpisodeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, 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 7b7bae43dfb..eecd6262f0a 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 @@ -8,18 +8,6 @@ import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding 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) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val newConvertView = convertView ?: run { - val mInflater = context - .getSystemService(Activity.LAYOUT_INFLATER_SERVICE) as LayoutInflater - mInflater.inflate(resource, null) - } - getItem(position)?.let { (newConvertView as? ImageView?)?.setImageResource(it) } - return newConvertView - } -}*/ const val IMAGE_CLICK = 0 const val IMAGE_LONG_CLICK = 1 @@ -66,8 +54,7 @@ class ImageAdapter( diffResult.dispatchUpdatesTo(this) } - class ImageViewHolder - constructor(val binding: ResultMiniImageBinding) : + class ImageViewHolder(val binding: ResultMiniImageBinding) : RecyclerView.ViewHolder(binding.root) { fun bind( img: Int, 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 2f2970986b1..f1399e8d1a6 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 @@ -78,11 +78,12 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.VideoDownloadHelper open class ResultFragmentPhone : FullScreenPlayer() { - private val gestureRegionsListener = object : PanelsChildGestureRegionObserver.GestureRegionsListener { - override fun onGestureRegionsUpdate(gestureRegions: List) { - binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + private val gestureRegionsListener = + object : PanelsChildGestureRegionObserver.GestureRegionsListener { + override fun onGestureRegionsUpdate(gestureRegions: List) { + binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + } } - } protected lateinit var viewModel: ResultViewModel2 protected lateinit var syncModel: SyncViewModel @@ -336,7 +337,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { } - // ===== ===== ===== resultBinding?.apply { @@ -430,16 +430,16 @@ open class ResultFragmentPhone : FullScreenPlayer() { 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 - } + // 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) + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + showToast(txt(message, name), Toast.LENGTH_SHORT) } context?.let { openBatteryOptimizationSettings(it) } } @@ -473,8 +473,16 @@ open class ResultFragmentPhone : FullScreenPlayer() { if (act.isCastApiAvailable()) { try { CastButtonFactory.setUpMediaRouteButton(act, this) - val castContext = CastContext.getSharedInstance(act.applicationContext) - isGone = castContext.castState == CastState.NO_DEVICES_AVAILABLE + CastContext.getSharedInstance(act.applicationContext) { + it.run() + }.addOnCompleteListener { + isGone = if (it.isSuccessful) { + it.result.castState == CastState.NO_DEVICES_AVAILABLE + } else { + true + } + + } // this shit leaks for some reason //castContext.addCastStateListener { state -> // media_route_button?.isGone = state == CastState.NO_DEVICES_AVAILABLE @@ -961,12 +969,12 @@ open class ResultFragmentPhone : FullScreenPlayer() { setOnClickListener { fab -> activity?.showBottomDialog( - WatchType.values().map { fab.context.getString(it.stringRes) }.toList(), + WatchType.entries.map { fab.context.getString(it.stringRes) }.toList(), watchType.ordinal, fab.context.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - viewModel.updateWatchStatus(WatchType.values()[it], context) + viewModel.updateWatchStatus(WatchType.entries[it], context) } } } @@ -1046,7 +1054,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { text?.asStringNull(ctx) ?: return@mapNotNull null ) }) { - viewModel.changeDubStatus(DubStatus.values()[itemId]) + viewModel.changeDubStatus(DubStatus.entries[itemId]) } } } @@ -1103,7 +1111,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { override fun onPause() { super.onPause() - PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(gestureRegionsListener) + PanelsChildGestureRegionObserver.Provider.get() + .addGestureRegionsUpdateListener(gestureRegionsListener) } private fun setRecommendations(rec: List?, validApiName: String?) { 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 a0207060a85..1878f0b8f4b 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 @@ -56,7 +56,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage class ResultFragmentTv : Fragment() { - protected lateinit var viewModel: ResultViewModel2 + private lateinit var viewModel: ResultViewModel2 private var binding: FragmentResultTvBinding? = null override fun onDestroyView() { @@ -418,10 +418,6 @@ class ResultFragmentTv : Fragment() { resultCastItems.layoutManager = object : LinearListLayout(view.context) { - override fun onInterceptFocusSearch(focused: View, direction: Int): View? { - return super.onInterceptFocusSearch(focused, direction) - } - override fun onRequestChildFocus( parent: RecyclerView, state: RecyclerView.State, @@ -649,7 +645,7 @@ class ResultFragmentTv : Fragment() { binding?.apply { - (data as? Resource.Success)?.value?.let { (text, ep) -> + (data as? Resource.Success)?.value?.let { (_, ep) -> resultPlayMovieButton.setOnClickListener { viewModel.handleAction( @@ -817,45 +813,8 @@ class ResultFragmentTv : Fragment() { } } - /* - * Okay so what is this fuckery? - * Basically Android TV will crash if you request a new focus while - * the adapter gets updated. - * - * This means that if you load thumbnails and request a next focus at the same time - * the app will crash without any way to catch it! - * - * How to bypass this? - * This code basically steals the focus for 500ms and puts it in an inescapable view - * then lets out the focus by requesting focus to result_episodes - */ - - val hasEpisodes = - !(resultEpisodes.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty() - /*val focus = activity?.currentFocus - - if (hasEpisodes) { - // Make it impossible to focus anywhere else! - temporaryNoFocus.isFocusable = true - temporaryNoFocus.requestFocus() - }*/ (resultEpisodes.adapter as? EpisodeAdapter)?.updateList(episodes.value) - - /* if (hasEpisodes) main { - - delay(500) - // This might make some people sad as it changes the focus when leaving an episode :( - if(focus?.requestFocus() == true) { - temporaryNoFocus.isFocusable = false - return@main - } - temporaryNoFocus.isFocusable = false - temporaryNoFocus.requestFocus() - } - - if (hasNoFocus()) - binding?.resultEpisodes?.requestFocus()*/ } } } 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 ce0fbdc5de1..6443a9239e3 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 @@ -2723,7 +2723,7 @@ class ResultViewModel2 : ViewModel() { val id: Int?, ) : LoadResponse - fun loadSmall(activity: Activity?, searchResponse: SearchResponse) = ioSafe { + fun loadSmall(searchResponse: SearchResponse) = ioSafe { val url = searchResponse.url _page.postValue(Resource.Loading(url)) _episodes.postValue(Resource.Loading()) 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 5a23bfc165b..8752e275c7f 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 @@ -63,8 +63,7 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter Unit, resView: AutofitRecyclerView diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt index 0a2ecb81e1d..4ef5fa698b2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt @@ -1,16 +1,11 @@ package com.lagradost.cloudstream3.ui.search import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.databinding.AccountSingleBinding import com.lagradost.cloudstream3.databinding.SearchHistoryItemBinding data class SearchHistoryItem( @@ -63,8 +58,7 @@ class SearchHistoryAdaptor( diffResult.dispatchUpdatesTo(this) } - class CardViewHolder - constructor( + class CardViewHolder( val binding: SearchHistoryItemBinding, private val clickCallback: (SearchHistoryCallback) -> Unit, ) : 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 f597132bd4b..92575e58fba 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 @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.search +import android.annotation.SuppressLint import android.content.Context import android.view.View import android.widget.ImageView @@ -37,16 +38,12 @@ object SearchResultBuilder { } } - /** - * @param nextFocusBehavior True if first, False if last, Null if between. - * Used to prevent escaping the adapter horizontally (focus wise). - */ + @SuppressLint("StringFormatInvalid") fun bind( clickCallback: (SearchClickCallback) -> Unit, card: SearchResponse, position: Int, itemView: View, - nextFocusBehavior: Boolean? = null, nextFocusUp: Int? = null, nextFocusDown: Int? = null, colorCallback : ((Palette) -> Unit)? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt index 9e03079f633..71077e91f7c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt @@ -3,11 +3,9 @@ package com.lagradost.cloudstream3.ui.search import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis +//TODO Relevance of this class since it's not used class SyncSearchViewModel { - private val repos = SyncApis - data class SyncSearchResultSearchResponse( override val name: String, override val url: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt index 1dc79dc0539..d7bd69f115f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.settings +import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -13,7 +14,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.LoginInfo) class AccountAdapter( - val cardList: List, + private val cardList: List, private val clickCallback: (AccountClickCallback) -> Unit ) : RecyclerView.Adapter() { @@ -42,12 +43,12 @@ class AccountAdapter( return cardList[position].accountIndex.toLong() } - class CardViewHolder - constructor(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) : + class CardViewHolder(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) : RecyclerView.ViewHolder(binding.root) { // private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! // private val accountName: TextView = itemView.findViewById(R.id.account_name)!! + @SuppressLint("StringFormatInvalid") fun bind(card: AuthAPI.LoginInfo) { // just in case name is null account index will show, should never happened binding.accountName.text = card.name ?: "%s %d".format( 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 6ba93c0fa46..88335eeafd5 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 @@ -26,7 +26,6 @@ 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.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper 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 fd61962c36e..7cb1a848fbb 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,8 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient 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 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 7560d75fa50..21707ca7a5b 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 @@ -10,7 +10,6 @@ import com.lagradost.cloudstream3.mvvm.logError 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 @@ -108,7 +107,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { getPref(R.string.hide_player_control_names_key)?.hideOn(TV) getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener { - val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() + val prefValues = Qualities.entries.map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) val prefNames = prefValues.map { Qualities.getStringByInt(it) } @@ -116,7 +115,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { val currentQuality = settingsManager.getInt( getString(R.string.quality_pref_key), - Qualities.values().last().value + Qualities.entries.last().value ) activity?.showBottomDialog( @@ -132,7 +131,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { } getPref(R.string.quality_pref_mobile_data_key)?.setOnPreferenceClickListener { - val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() + val prefValues = Qualities.entries.map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) val prefNames = prefValues.map { Qualities.getStringByInt(it) } @@ -140,7 +139,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { val currentQuality = settingsManager.getInt( getString(R.string.quality_pref_mobile_data_key), - Qualities.values().last().value + Qualities.entries.last().value ) activity?.showBottomDialog( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index cfb46c3955f..cb7d25fd7d5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -34,7 +34,7 @@ class SettingsProviders : PreferenceFragmentCompat() { getPref(R.string.display_sub_key)?.setOnPreferenceClickListener { activity?.getApiDubstatusSettings()?.let { current -> - val dublist = DubStatus.values() + val dublist = DubStatus.entries val names = dublist.map { it.name } val currentList = ArrayList() 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 4aaa5e12f11..260c667474b 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 @@ -128,7 +128,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { } binding.saveBtt.setOnClickListener { - val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) + val date = SimpleDateFormat("yyyy_MM_dd_HH_mm", Locale.getDefault()).format(Date(currentTimeMillis())) var fileStream: OutputStream? = null try { fileStream = VideoDownloadManager.setupStream( @@ -169,10 +169,10 @@ class SettingsUpdates : PreferenceFragmentCompat() { prefValues.indexOf(currentInstaller), getString(R.string.apk_installer_settings), true, - {}) { + {}) { num -> try { settingsManager.edit() - .putInt(getString(R.string.apk_installer_key), prefValues[it]) + .putInt(getString(R.string.apk_installer_key), prefValues[num]) .apply() } catch (e: Exception) { logError(e) @@ -209,9 +209,9 @@ class SettingsUpdates : PreferenceFragmentCompat() { prefValues.indexOf(current), getString(R.string.automatic_plugin_download_mode_title), true, - {}) { + {}) { num -> settingsManager.edit() - .putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() + .putInt(getString(R.string.auto_download_plugins_key), prefValues[num]).apply() (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } return@setOnPreferenceClickListener true 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 909c30be1f3..9fb3f282f79 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 @@ -1,9 +1,11 @@ package com.lagradost.cloudstream3.ui.settings.extensions +import android.annotation.SuppressLint import android.text.format.Formatter.formatShortFileSize import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup +import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible @@ -27,11 +29,10 @@ import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx -import org.junit.Assert -import org.junit.Test import java.text.DecimalFormat import kotlin.math.floor import kotlin.math.log10 +import kotlin.math.pow data class PluginViewData( @@ -95,21 +96,13 @@ class PluginAdapter( } companion object { - private tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { if (current >= max) return max if (current >= target) return current return findClosestBase2(target, current * 2, max) } - @Test - fun testFindClosestBase2() { - Assert.assertEquals(16, findClosestBase2(0)) - Assert.assertEquals(256, findClosestBase2(170)) - Assert.assertEquals(256, findClosestBase2(256)) - Assert.assertEquals(512, findClosestBase2(257)) - Assert.assertEquals(512, findClosestBase2(700)) - } - private val iconSizeExact = 32.toPx private val iconSize by lazy { findClosestBase2(iconSizeExact, 16, 512) @@ -122,10 +115,7 @@ class PluginAdapter( val base = value / 3 return if (value >= 3 && base < suffix.size) { DecimalFormat("#0.00").format( - numValue / Math.pow( - 10.0, - (base * 3).toDouble() - ) + numValue / 10.0.pow((base * 3).toDouble()) ) + suffix[base] } else { DecimalFormat().format(numValue) @@ -136,6 +126,7 @@ class PluginAdapter( inner class PluginViewHolder(val binding: RepositoryItemBinding) : RecyclerView.ViewHolder(binding.root) { + @SuppressLint("SetTextI18n") fun bind( data: PluginViewData, ) { 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 c5319c37b53..4878049b4cf 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 @@ -190,7 +190,7 @@ class PluginsFragment : Fragment() { bindChips( binding?.tvtypesChipsScroll?.tvtypesChips, emptyList(), - TvType.values().toList(), + TvType.entries.toList(), callback = { list -> pluginViewModel.tvTypes.clear() pluginViewModel.tvTypes.addAll(list.map { it.name }) 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 56014eb4dcc..fd5422b2d8c 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 @@ -10,7 +10,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager 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.TvType diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index f919721391d..49a93608c10 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -10,7 +10,6 @@ import androidx.core.util.forEach import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager -import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding 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 bb9558b82bc..c76a218e547 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 @@ -15,8 +15,11 @@ import android.widget.Toast import androidx.fragment.app.Fragment 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.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DEPRESSED +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DROP_SHADOW +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_NONE +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_OUTLINE +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_RAISED import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent @@ -42,7 +45,7 @@ data class SaveChromeCaptionStyle( @JsonProperty("fontGenericFamily") var fontGenericFamily: Int? = null, @JsonProperty("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent @JsonProperty("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK - @JsonProperty("edgeType") var edgeType: Int = TextTrackStyle.EDGE_TYPE_OUTLINE, + @JsonProperty("edgeType") var edgeType: Int = EDGE_TYPE_OUTLINE, @JsonProperty("foregroundColor") var foregroundColor: Int = Color.WHITE, @JsonProperty("fontScale") var fontScale: Float = 1.05f, @JsonProperty("windowColor") var windowColor: Int = Color.TRANSPARENT, @@ -99,7 +102,7 @@ class ChromecastSubtitlesFragment : Fragment() { } private fun onColorSelected(stuff: Pair) { - context?.setColor(stuff.first, stuff.second) + setColor(stuff.first, stuff.second) if (hide) activity?.hideSystemUI() } @@ -122,7 +125,7 @@ class ChromecastSubtitlesFragment : Fragment() { return if (color == Color.TRANSPARENT) Color.BLACK else color } - private fun Context.setColor(id: Int, color: Int?) { + private fun setColor(id: Int, color: Int?) { val realColor = color ?: getDefColor(id) when (id) { 0 -> state.foregroundColor = realColor @@ -135,7 +138,7 @@ class ChromecastSubtitlesFragment : Fragment() { updateState() } - private fun Context.updateState() { + private fun updateState() { //subtitle_text?.setStyle(fromSaveToStyle(state)) } @@ -173,7 +176,7 @@ class ChromecastSubtitlesFragment : Fragment() { fixPaddingStatusbar(binding?.subsRoot) state = getCurrentSavedStyle() - context?.updateState() + updateState() val isTvSettings = isLayout(TV or EMULATOR) @@ -195,7 +198,7 @@ class ChromecastSubtitlesFragment : Fragment() { } this.setOnLongClickListener { - it.context.setColor(id, null) + setColor(id, null) showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } @@ -247,13 +250,13 @@ class ChromecastSubtitlesFragment : Fragment() { dismissCallback ) { index -> state.edgeType = edgeTypes.map { it.first }[index] - textView.context.updateState() + updateState() } } binding?.subsEdgeType?.setOnLongClickListener { state.edgeType = defaultState.edgeType - it.context.updateState() + updateState() showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } @@ -323,12 +326,12 @@ class ChromecastSubtitlesFragment : Fragment() { dismissCallback ) { index -> state.fontFamily = fontTypes.map { it.first }[index] - textView.context.updateState() + updateState() } } - binding?.subsFont?.setOnLongClickListener { textView -> + binding?.subsFont?.setOnLongClickListener { _ -> state.fontFamily = defaultState.fontFamily - textView.context.updateState() + updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } 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 1466afed132..8821905e3d1 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 @@ -14,11 +14,13 @@ import android.view.ViewGroup import android.widget.TextView import android.widget.Toast import androidx.annotation.FontRes +import androidx.annotation.OptIn import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty import androidx.media3.common.text.Cue +import androidx.media3.common.util.UnstableApi import androidx.media3.ui.CaptionStyleCompat import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -28,7 +30,6 @@ 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.DataStore.setKey @@ -46,7 +47,7 @@ const val SUBTITLE_KEY = "subtitle_settings" const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select" const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download" -data class SaveCaptionStyle( +data class SaveCaptionStyle @OptIn(UnstableApi::class) constructor( @JsonProperty("foregroundColor") var foregroundColor: Int, @JsonProperty("backgroundColor") var backgroundColor: Int, @JsonProperty("windowColor") var windowColor: Int, @@ -67,7 +68,7 @@ data class SaveCaptionStyle( const val DEF_SUBS_ELEVATION = 20 -@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +@OptIn(androidx.media3.common.util.UnstableApi::class) class SubtitlesFragment : Fragment() { companion object { val applyStyleEvent = Event() @@ -167,7 +168,7 @@ class SubtitlesFragment : Fragment() { activity?.hideSystemUI() } - private fun onDialogDismissed(id: Int) { + private fun onDialogDismissed(@Suppress("UNUSED_PARAMETER") id: Int) { if (hide) activity?.hideSystemUI() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt index e9b69c5b013..f0c948a4a34 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt @@ -83,7 +83,7 @@ object EpisodeSkip { startMs = start, endMs = end ) - }?.let { list -> + }.let { list -> out.addAll(list) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index f0aae7bc035..b13de062a23 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -43,7 +43,6 @@ import androidx.recyclerview.widget.RecyclerView import androidx.tvprovider.media.tv.* import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor import androidx.viewpager2.widget.ViewPager2 -import com.fasterxml.jackson.module.kotlin.readValue import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState import com.google.android.gms.common.ConnectionResult @@ -58,7 +57,7 @@ import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEv import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.plugins.RepositoryManager -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WebviewFragment import com.lagradost.cloudstream3.ui.player.SubtitleData @@ -161,7 +160,7 @@ object AppContextUtils { .setTitle(title) .setPosterArtUri(Uri.parse(card.posterUrl)) .setIntentUri(Uri.parse(card.id?.let { - "$appStringResumeWatching://$it" + "$APP_STRING_RESUME_WATCHING://$it" } ?: card.url)) .setInternalProviderId(card.url) .setLastEngagementTimeUtcMillis( 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 802c1a648e6..b25be59f98a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -81,12 +81,12 @@ object BackupUtils { // Kinda hack, but I couldn't think of a better way data class BackupVars( - @JsonProperty("_Bool") val _Bool: Map?, - @JsonProperty("_Int") val _Int: Map?, - @JsonProperty("_String") val _String: Map?, - @JsonProperty("_Float") val _Float: Map?, - @JsonProperty("_Long") val _Long: Map?, - @JsonProperty("_StringSet") val _StringSet: Map?>?, + @JsonProperty("_Bool") val bool: Map?, + @JsonProperty("_Int") val int: Map?, + @JsonProperty("_String") val string: Map?, + @JsonProperty("_Float") val float: Map?, + @JsonProperty("_Long") val long: Map?, + @JsonProperty("_StringSet") val stringSet: Map?>?, ) data class BackupFile( @@ -134,21 +134,21 @@ object BackupUtils { ) { if (context == null) return if (restoreSettings) { - context.restoreMap(backupFile.settings._Bool, true) - context.restoreMap(backupFile.settings._Int, true) - context.restoreMap(backupFile.settings._String, true) - context.restoreMap(backupFile.settings._Float, true) - context.restoreMap(backupFile.settings._Long, true) - context.restoreMap(backupFile.settings._StringSet, true) + context.restoreMap(backupFile.settings.bool, true) + context.restoreMap(backupFile.settings.int, true) + context.restoreMap(backupFile.settings.string, true) + context.restoreMap(backupFile.settings.float, true) + context.restoreMap(backupFile.settings.long, true) + context.restoreMap(backupFile.settings.stringSet, true) } if (restoreDataStore) { - context.restoreMap(backupFile.datastore._Bool) - context.restoreMap(backupFile.datastore._Int) - context.restoreMap(backupFile.datastore._String) - context.restoreMap(backupFile.datastore._Float) - context.restoreMap(backupFile.datastore._Long) - context.restoreMap(backupFile.datastore._StringSet) + context.restoreMap(backupFile.datastore.bool) + context.restoreMap(backupFile.datastore.int) + context.restoreMap(backupFile.datastore.string) + context.restoreMap(backupFile.datastore.float) + context.restoreMap(backupFile.datastore.long) + context.restoreMap(backupFile.datastore.stringSet) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index 19c817b91b5..b5192aae287 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -56,14 +56,25 @@ data class Editor( ) { /** Always remember to call apply after */ fun setKeyRaw(path: String, value: T) { - when (value) { - is Boolean -> editor.putBoolean(path, value) - is Int -> editor.putInt(path, value) - is String -> editor.putString(path, value) - is Float -> editor.putFloat(path, value) - is Long -> editor.putLong(path, value) - (value as? Set != null) -> editor.putStringSet(path, value as Set) + @Suppress("UNCHECKED_CAST") + if (isStringSet(value)) { + editor.putStringSet(path, value as Set) + } else { + when (value) { + is Boolean -> editor.putBoolean(path, value) + is Int -> editor.putInt(path, value) + is String -> editor.putString(path, value) + is Float -> editor.putFloat(path, value) + is Long -> editor.putLong(path, value) + } + } + } + + private fun isStringSet(value: Any?) : Boolean { + if (value is Set<*>) { + return value.filterIsInstance().size == value.size } + return false } fun apply() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt index 421e4420499..c92da214d05 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt @@ -7,7 +7,6 @@ import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index 89bb0031acc..59f534ff266 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -32,26 +32,26 @@ import java.io.InputStreamReader class InAppUpdater { companion object { - const val GITHUB_USER_NAME = "recloudstream" - const val GITHUB_REPO = "cloudstream" + private const val GITHUB_USER_NAME = "recloudstream" + private const val GITHUB_REPO = "cloudstream" - const val LOG_TAG = "InAppUpdater" + private const val LOG_TAG = "InAppUpdater" // === IN APP UPDATER === data class GithubAsset( @JsonProperty("name") val name: String, @JsonProperty("size") val size: Int, // Size bytes - @JsonProperty("browser_download_url") val browser_download_url: String, // download link - @JsonProperty("content_type") val content_type: String, // application/vnd.android.package-archive + @JsonProperty("browser_download_url") val browserDownloadUrl: String, // download link + @JsonProperty("content_type") val contentType: String, // application/vnd.android.package-archive ) data class GithubRelease( - @JsonProperty("tag_name") val tag_name: String, // Version code + @JsonProperty("tag_name") val tagName: String, // Version code @JsonProperty("body") val body: String, // Desc @JsonProperty("assets") val assets: List, - @JsonProperty("target_commitish") val target_commitish: String, // branch + @JsonProperty("target_commitish") val targetCommitish: String, // branch @JsonProperty("prerelease") val prerelease: Boolean, - @JsonProperty("node_id") val node_id: String //Node Id + @JsonProperty("node_id") val nodeId: String //Node Id ) data class GithubObject( @@ -61,7 +61,7 @@ class InAppUpdater { ) data class GithubTag( - @JsonProperty("object") val github_object: GithubObject, + @JsonProperty("object") val githubObject: GithubObject, ) data class Update( @@ -114,7 +114,7 @@ class InAppUpdater { response.filter { rel -> !rel.prerelease }.sortedWith(compareBy { release -> - release.assets.firstOrNull { it.content_type == "application/vnd.android.package-archive" }?.name?.let { it1 -> + release.assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }?.name?.let { it1 -> versionRegex.find( it1 )?.groupValues?.let { @@ -134,7 +134,7 @@ class InAppUpdater { foundAsset?.name?.let { assetName -> val foundVersion = versionRegex.find(assetName) val shouldUpdate = - if (foundAsset.browser_download_url != "" && foundVersion != null) currentVersion?.versionName?.let { versionName -> + if (foundAsset.browserDownloadUrl != "" && foundVersion != null) currentVersion?.versionName?.let { versionName -> versionRegexLocal.find(versionName)?.groupValues?.let { it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() } @@ -146,10 +146,10 @@ class InAppUpdater { return if (foundVersion != null) { Update( shouldUpdate, - foundAsset.browser_download_url, + foundAsset.browserDownloadUrl, foundVersion.groupValues[2], found.body, - found.node_id + found.nodeId ) } else { Update(false, null, null, null, null) @@ -168,33 +168,33 @@ class InAppUpdater { val found = response.lastOrNull { rel -> - rel.prerelease || rel.tag_name == "pre-release" + rel.prerelease || rel.tagName == "pre-release" } val foundAsset = found?.assets?.filter { it -> - it.content_type == "application/vnd.android.package-archive" + it.contentType == "application/vnd.android.package-archive" }?.getOrNull(0) val tagResponse = parseJson(app.get(tagUrl, headers = headers).text) - Log.d(LOG_TAG, "Fetched GitHub tag: ${tagResponse.github_object.sha.take(7)}") + Log.d(LOG_TAG, "Fetched GitHub tag: ${tagResponse.githubObject.sha.take(7)}") val shouldUpdate = (getString(R.string.commit_hash) .trim { c -> c.isWhitespace() } .take(7) != - tagResponse.github_object.sha + tagResponse.githubObject.sha .trim { c -> c.isWhitespace() } .take(7)) return if (foundAsset != null) { Update( shouldUpdate, - foundAsset.browser_download_url, - tagResponse.github_object.sha.take(10), + foundAsset.browserDownloadUrl, + tagResponse.githubObject.sha.take(10), found.body, - found.node_id + found.nodeId ) } else { Update(false, null, null, null, null) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt index bc81a5b9f22..4b3f02f11bd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt @@ -11,7 +11,6 @@ import android.os.Build import android.widget.Toast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.utils.Coroutines.main import java.io.InputStream @@ -57,7 +56,7 @@ class ApkInstaller(private val service: PackageInstallerService) { PackageInstaller.STATUS_FAILURE )) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT) + val userAction = intent.getSafeParcelableExtra(Intent.EXTRA_INTENT) userAction?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(userAction) } @@ -146,3 +145,5 @@ class ApkInstaller(private val service: PackageInstallerService) { } } +@Suppress("DEPRECATION") +inline fun Intent.getSafeParcelableExtra(key: String): T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelableExtra(key) else getParcelableExtra(key, T::class.java) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt index 27609730c28..0d3da8e75c8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt @@ -17,8 +17,8 @@ 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" +private const val PACKAGE_NAME = BuildConfig.APPLICATION_ID +private const val TAG = "PowerManagerAPI" object BatteryOptimizationChecker { @@ -72,7 +72,7 @@ object BatteryOptimizationChecker { val intent = Intent() try { intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - .setData(Uri.fromParts("package", packageName, null)) + .setData(Uri.fromParts("package", PACKAGE_NAME, null)) context.startActivity(intent, Bundle()) } catch (t: Throwable) { Log.e(TAG, "Unable to invoke any intent", t) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index 71d3a1eff01..351e77c8d72 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -73,8 +73,8 @@ object SyncUtil { val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text val mapped = parseJson(response) - val overrideMal = mapped?.malId ?: mapped?.Mal?.id ?: mapped?.Anilist?.malId - val overrideAnilist = mapped?.aniId ?: mapped?.Anilist?.id + val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId + val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id if (overrideMal != null) { return overrideMal.toString() to overrideAnilist?.toString() @@ -135,8 +135,8 @@ object SyncUtil { @JsonProperty("createdAt") val createdAt: String?, @JsonProperty("updatedAt") val updatedAt: String?, @JsonProperty("deletedAt") val deletedAt: String?, - @JsonProperty("Mal") val Mal: Mal?, - @JsonProperty("Anilist") val Anilist: Anilist?, + @JsonProperty("Mal") val mal: Mal?, + @JsonProperty("Anilist") val anilist: Anilist?, @JsonProperty("malUrl") val malUrl: String? ) 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 8670de53feb..ad1b6502d6a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -553,7 +553,7 @@ object UIHelper { return result } - fun Context?.IsBottomLayout(): Boolean { + fun Context?.isBottomLayout(): Boolean { if (this == null) return true val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) return settingsManager.getBoolean(getString(R.string.bottom_title_key), true) 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 197bacc6629..a3f6d78918c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -293,6 +293,7 @@ object VideoDownloadManager { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) } else { + //fixme Specify a better flag PendingIntent.getActivity(context, 0, intent, 0) } builder.setContentIntent(pendingIntent) @@ -475,10 +476,10 @@ object VideoDownloadManager { } } - private const val reservedChars = "|\\?*<\":>+[]/\'" + private const val RESERVED_CHARS = "|\\?*<\":>+[]/\'" fun sanitizeFilename(name: String, removeSpaces: Boolean = false): String { var tempName = name - for (c in reservedChars) { + for (c in RESERVED_CHARS) { tempName = tempName.replace(c, ' ') } if (removeSpaces) tempName = tempName.replace(" ", "") @@ -1699,7 +1700,7 @@ object VideoDownloadManager { } */ fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? = - getDownloadFileInfo(context, id, removeKeys = true) + getDownloadFileInfo(context, id) private fun DownloadedFileInfo.toFile(context: Context): SafeFile? { return basePathToFile(context, this.basePath)?.gotoDirectory(relativePath) @@ -1709,7 +1710,6 @@ object VideoDownloadManager { private fun getDownloadFileInfo( context: Context, id: Int, - removeKeys: Boolean = false ): DownloadedFileInfoResult? { try { val info = diff --git a/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt index d4725d53e64..2aea0b8df68 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt @@ -19,7 +19,7 @@ class FlowLayout : ViewGroup { @SuppressLint("CustomViewStyleable") internal constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) { val t = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout) - itemSpacing = t.getDimensionPixelSize(R.styleable.FlowLayout_Layout_itemSpacing, 0); + itemSpacing = t.getDimensionPixelSize(R.styleable.FlowLayout_Layout_itemSpacing, 0) t.recycle() } diff --git a/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt b/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt new file mode 100644 index 00000000000..5dbf4d7ca52 --- /dev/null +++ b/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt @@ -0,0 +1,16 @@ +package com.lagradost.cloudstream3 + +import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter.Companion.findClosestBase2 +import org.junit.Assert +import org.junit.Test + +class PluginAdapterTest { + @Test + fun testFindClosestBase2() { + Assert.assertEquals(16, findClosestBase2(0)) + Assert.assertEquals(256, findClosestBase2(170)) + Assert.assertEquals(256, findClosestBase2(256)) + Assert.assertEquals(512, findClosestBase2(257)) + Assert.assertEquals(512, findClosestBase2(700)) + } +} \ No newline at end of file From 82f8ab489e88145e0f498dc3537d26d59157b7dc Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 29 Jul 2024 00:58:35 +0200 Subject: [PATCH 015/235] Fix prerelease test function --- .../ui/settings/extensions/PluginAdapter.kt | 17 ++++++++++++++--- .../lagradost/cloudstream3/PluginAdapterTest.kt | 16 ---------------- 2 files changed, 14 insertions(+), 19 deletions(-) delete mode 100644 app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt 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 9fb3f282f79..d159539d669 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 @@ -33,7 +33,8 @@ import java.text.DecimalFormat import kotlin.math.floor import kotlin.math.log10 import kotlin.math.pow - +import org.junit.Test +import org.junit.Assert data class PluginViewData( val plugin: Plugin, @@ -96,13 +97,23 @@ class PluginAdapter( } companion object { - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { + private tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { if (current >= max) return max if (current >= target) return current return findClosestBase2(target, current * 2, max) } + // DO NOT MOVE, as running this test will result in ExceptionInInitializerError on prerelease due to static variables using Resources.getSystem() + // this test function is only to show how the function works + @Test + fun testFindClosestBase2() { + Assert.assertEquals(16, findClosestBase2(0)) + Assert.assertEquals(256, findClosestBase2(170)) + Assert.assertEquals(256, findClosestBase2(256)) + Assert.assertEquals(512, findClosestBase2(257)) + Assert.assertEquals(512, findClosestBase2(700)) + } + private val iconSizeExact = 32.toPx private val iconSize by lazy { findClosestBase2(iconSizeExact, 16, 512) diff --git a/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt b/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt deleted file mode 100644 index 5dbf4d7ca52..00000000000 --- a/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.lagradost.cloudstream3 - -import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter.Companion.findClosestBase2 -import org.junit.Assert -import org.junit.Test - -class PluginAdapterTest { - @Test - fun testFindClosestBase2() { - Assert.assertEquals(16, findClosestBase2(0)) - Assert.assertEquals(256, findClosestBase2(170)) - Assert.assertEquals(256, findClosestBase2(256)) - Assert.assertEquals(512, findClosestBase2(257)) - Assert.assertEquals(512, findClosestBase2(700)) - } -} \ No newline at end of file From 150ad5fc9f4c90a8de8c42bd53b190a594606156 Mon Sep 17 00:00:00 2001 From: epireyn <48213068+epireyn@users.noreply.github.com> Date: Mon, 29 Jul 2024 01:00:44 +0200 Subject: [PATCH 016/235] Add sorting by release date (#1206) --- .../cloudstream3/syncproviders/SyncApi.kt | 6 +++++- .../syncproviders/providers/AniListApi.kt | 10 +++++++--- .../syncproviders/providers/LocalList.kt | 5 +++++ .../syncproviders/providers/MALApi.kt | 9 +++++++++ .../syncproviders/providers/SimklApi.kt | 7 ++++++- .../ui/library/LibraryViewModel.kt | 4 +++- .../cloudstream3/utils/DataStoreHelper.kt | 20 +++++++++++++++---- app/src/main/res/values/strings.xml | 2 ++ 8 files changed, 53 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt index 878e0cb3bfe..dcb8bbead06 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt @@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiText import me.xdrop.fuzzywuzzy.FuzzySearch +import java.util.Date interface SyncAPI : OAuth2API { /** @@ -124,6 +125,8 @@ interface SyncAPI : OAuth2API { ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed() ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) } ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime } + ListSorting.ReleaseDateNew -> items.sortedByDescending { it.releaseDate } + ListSorting.ReleaseDateOld -> items.sortedBy { it.releaseDate } else -> items } } @@ -158,9 +161,10 @@ interface SyncAPI : OAuth2API { override var posterUrl: String?, override var posterHeaders: Map?, override var quality: SearchQuality?, + val releaseDate: Date?, override var id: Int? = null, val plot : String? = null, val rating: Int? = null, - val tags: List? = null, + val tags: List? = null ) : SearchResponse } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index e51d3d654a1..6112c7dbede 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -16,15 +16,16 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject +import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear import java.net.URL import java.net.URLEncoder -import java.util.* +import java.util.Locale class AniListApi(index: Int) : AccountManager(index), SyncAPI { override var name = "AniList" @@ -631,8 +632,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ?: this.media.coverImage.medium, null, null, + this.media.seasonYear.toYear(), null, - plot = this.media.description + plot = this.media.description, ) } } @@ -689,6 +691,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) 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 f819cd3bb63..0d9a4d1383d 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 @@ -119,6 +119,11 @@ class LocalList : SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, +// ListSorting.RatingHigh, +// ListSorting.RatingLow, + ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index 6046a0f234e..08c1865311e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -27,6 +27,7 @@ import java.security.SecureRandom import java.text.ParseException import java.text.SimpleDateFormat import java.time.Instant +import java.time.format.DateTimeFormatter import java.util.Calendar import java.util.Date import java.util.Locale @@ -448,6 +449,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { null, null, plot = this.node.synopsis, + releaseDate = if (this.node.startDate == null) null else try {Date.from( + Instant.from( + DateTimeFormatter.ofPattern(if (this.node.startDate.length == 4) "yyyy" else if (this.node.startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd") + .parse(this.node.startDate) + ) + )} catch (_: RuntimeException) {null} ) } } @@ -512,6 +519,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index e5db626ba79..50517f9d1ac 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -31,6 +31,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear import okhttp3.Interceptor import okhttp3.Response import java.math.BigInteger @@ -670,7 +671,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.movie.poster?.let { getPosterUrl(it) }, null, null, - movie.ids.simkl, + this.movie.year?.toYear(), + movie.ids.simkl ) } } @@ -702,6 +704,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.show.poster?.let { getPosterUrl(it) }, null, null, + this.show.year?.toYear(), show.ids.simkl ) } @@ -1027,6 +1030,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) 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 1bd01c86449..6c602e6c527 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 @@ -23,6 +23,8 @@ enum class ListSorting(@StringRes val stringRes: Int) { UpdatedOld(R.string.sort_updated_old), AlphabeticalA(R.string.sort_alphabetical_a), AlphabeticalZ(R.string.sort_alphabetical_z), + ReleaseDateNew(R.string.sort_release_date_new), + ReleaseDateOld(R.string.sort_release_date_old), } const val LAST_SYNC_API_KEY = "last_sync_api" @@ -132,4 +134,4 @@ class LibraryViewModel : ViewModel() { MainActivity.reloadLibraryEvent -= ::reloadPages super.onCleared() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 43124a531b1..2fa5f6a3a52 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -2,8 +2,8 @@ package com.lagradost.cloudstream3.utils import android.content.Context import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys @@ -11,6 +11,13 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.DubStatus +import com.lagradost.cloudstream3.EpisodeResponse +import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchQuality +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType @@ -18,6 +25,9 @@ import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia +import java.util.Calendar +import java.util.Date +import java.util.GregorianCalendar import kotlin.reflect.KClass import kotlin.reflect.KProperty @@ -195,6 +205,8 @@ object DataStoreHelper { return this } + fun Int.toYear() : Date = GregorianCalendar.getInstance().also { it.set(Calendar.YEAR, this) }.time + /** * Used to display notifications on new episodes and posters in library. **/ @@ -242,7 +254,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } @@ -273,7 +285,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } @@ -304,7 +316,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21067fff91c..37a3f993118 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -797,6 +797,8 @@ Can\'t get the device PIN code, try local authentication PIN code is now expired ! Code expires in %1$dm %2$ds + Release Date (New to Old) + Release Date (Old to New) hide_player_control_names_key Hide names of the player\'s controls \ No newline at end of file From b2f08847e1dc736aa54a26c84d8f71e8ea83c166 Mon Sep 17 00:00:00 2001 From: epireyn <48213068+epireyn@users.noreply.github.com> Date: Mon, 29 Jul 2024 01:01:45 +0200 Subject: [PATCH 017/235] Add system dark theme (#1208) --- app/src/main/AndroidManifest.xml | 2 +- .../lagradost/cloudstream3/CommonActivity.kt | 28 +++++++++++++++++-- .../lagradost/cloudstream3/MainActivity.kt | 2 ++ .../cloudstream3/ui/settings/SettingsUI.kt | 14 +++++++--- app/src/main/res/values-es/array.xml | 2 ++ app/src/main/res/values-pl/array.xml | 2 ++ app/src/main/res/values-tr/array.xml | 2 ++ app/src/main/res/values-vi/array.xml | 2 ++ app/src/main/res/values/array.xml | 2 ++ 9 files changed, 49 insertions(+), 7 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a23ef72588f..888be999025 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -97,7 +97,7 @@ --> = Build.VERSION_CODES.Q) { + loadThemes(act) + } + } + + private fun mapSystemTheme(act: Activity): Int { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val currentNightMode = + act.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return when (currentNightMode) { + Configuration.UI_MODE_NIGHT_NO -> R.style.LightMode // Night mode is not active, we're using the light theme + else -> R.style.AppTheme // Night mode is active, we're using dark theme + } + } else { + return R.style.AppTheme + } + } + fun loadThemes(act: Activity?) { if (act == null) return val settingsManager = PreferenceManager.getDefaultSharedPreferences(act) val currentTheme = when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) { + "System" -> mapSystemTheme(act) "Black" -> R.style.AppTheme "Light" -> R.style.LightMode "Amoled" -> R.style.AmoledMode @@ -352,8 +376,8 @@ object CommonActivity { currentLook = currentLook.parent as? View ?: break }*/ - private fun View.hasContent() : Boolean { - return isShown && when(this) { + private fun View.hasContent(): Boolean { + return isShown && when (this) { //is RecyclerView -> this.childCount > 0 is ViewGroup -> this.childCount > 0 else -> true diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index eed69a5064b..b59265eee46 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -68,6 +68,7 @@ import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.setActivityInstance import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale +import com.lagradost.cloudstream3.CommonActivity.updateTheme import com.lagradost.cloudstream3.databinding.ActivityMainBinding import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding @@ -484,6 +485,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) updateLocale() // android fucks me by chaining lang when rotating the phone + updateTheme(this) // Update if system theme val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment 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 cc14e7617da..8c3ad0adeb6 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 @@ -88,10 +88,9 @@ class SettingsUI : PreferenceFragmentCompat() { getPref(R.string.app_theme_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.themes_names).toMutableList() val prefValues = resources.getStringArray(R.array.themes_names_values).toMutableList() - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less + val removeIncompatible = { text: String -> val toRemove = prefValues - .mapIndexed { idx, s -> if (s.startsWith("Monet")) idx else null } + .mapIndexed { idx, s -> if (s.startsWith(text)) idx else null } .filterNotNull() var offset = 0 toRemove.forEach { idx -> @@ -100,6 +99,12 @@ class SettingsUI : PreferenceFragmentCompat() { offset += 1 } } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less + removeIncompatible("Monet") + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { // Remove system on android 9 and less + removeIncompatible("System") + } val currentLayout = settingsManager.getString(getString(R.string.app_theme_key), prefValues.first()) @@ -123,7 +128,8 @@ class SettingsUI : PreferenceFragmentCompat() { } getPref(R.string.primary_color_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.themes_overlay_names).toMutableList() - val prefValues = resources.getStringArray(R.array.themes_overlay_names_values).toMutableList() + val prefValues = + resources.getStringArray(R.array.themes_overlay_names_values).toMutableList() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less val toRemove = prefValues diff --git a/app/src/main/res/values-es/array.xml b/app/src/main/res/values-es/array.xml index 05d49f98650..eb197f43e84 100644 --- a/app/src/main/res/values-es/array.xml +++ b/app/src/main/res/values-es/array.xml @@ -247,6 +247,7 @@ Gris Amoled Destello + Sistema Material You @@ -254,6 +255,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values-pl/array.xml b/app/src/main/res/values-pl/array.xml index 9f76f423722..a43d7bcfe5f 100644 --- a/app/src/main/res/values-pl/array.xml +++ b/app/src/main/res/values-pl/array.xml @@ -256,6 +256,7 @@ Szary Amoled Flashbang + System Material You @@ -263,6 +264,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values-tr/array.xml b/app/src/main/res/values-tr/array.xml index 5c723f724ea..22a94ebf0f4 100644 --- a/app/src/main/res/values-tr/array.xml +++ b/app/src/main/res/values-tr/array.xml @@ -281,6 +281,7 @@ Gri Amoled Flaş Bombası + Sistem Material You @@ -288,6 +289,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values-vi/array.xml b/app/src/main/res/values-vi/array.xml index aac94100ebf..f363befda0f 100644 --- a/app/src/main/res/values-vi/array.xml +++ b/app/src/main/res/values-vi/array.xml @@ -248,6 +248,7 @@ Xám Amoled Sáng + Hệ thống Material You @@ -255,6 +256,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 3be125101b9..03715fafd41 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -318,6 +318,7 @@ Gray Amoled Flashbang + System Material You @@ -325,6 +326,7 @@ Black Amoled Light + System Monet From 63e27c2ea5c7c57d41846038678c38654e28e495 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 30 Jul 2024 21:16:11 +0300 Subject: [PATCH 018/235] Fix Trailers on API<33 (#1226) Recent NewPipeExtractor updates pushed minimum sdk to 33 which needs desugar_jdk_libs_nio --- app/build.gradle.kts | 8 ++++---- library/build.gradle.kts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2040cf39a93..ee6cda6c7ee 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -140,7 +140,7 @@ android { abortOnError = false checkReleaseBuilds = false } - + buildFeatures { buildConfig = true } @@ -200,7 +200,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:2d36945") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:176da72") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding @@ -213,7 +213,7 @@ dependencies { implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors implementation("androidx.tvprovider:tvprovider:1.0.0") implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures - implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication + implementation("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview implementation("io.github.g0dkar:qrcode-kotlin:4.2.0") // QR code for PIN Auth on TV @@ -223,7 +223,7 @@ dependencies { 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 implementation("com.uwetrottmann.tmdb2:tmdb-java:2.11.0") // TMDB API v3 Wrapper Made with RetroFit - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.0.4") //nio flavor needed for NewPipeExtractor 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. */ diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 516e1ee9c1c..00bc3c14740 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -27,7 +27,7 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") implementation("me.xdrop:fuzzywuzzy:1.4.0") // Match extractors implementation("org.mozilla:rhino:1.7.15") // run JavaScript - implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") + implementation("com.github.teamnewpipe:NewPipeExtractor:176da72") } } } From 30adb1cd9d8aa7d538d027fb4e9506cd31224869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Tue, 30 Jul 2024 21:38:51 +0300 Subject: [PATCH 019/235] fixed: Test Search & VidMoxy, RapidVid extractors (#1219) --- .../cloudstream3/utils/TestingUtils.kt | 5 ++- .../extractors/ContentXExtractor.kt | 41 +++++++++---------- .../extractors/HDMomPlayerExtractor.kt | 21 +++++----- .../extractors/HDPlayerSystemExtractor.kt | 23 +++++------ .../extractors/MailRuExtractor.kt | 21 ++++------ .../extractors/OdnoklassnikiExtractor.kt | 18 ++++---- .../extractors/PeaceMakerstExtractor.kt | 37 ++++++++--------- .../extractors/RapidVidExtractor.kt | 37 ++++++++++------- .../extractors/SibNetExtractor.kt | 11 +++-- .../cloudstream3/extractors/TRsTXExtractor.kt | 30 +++++++------- .../extractors/TauVideoExtractor.kt | 11 +++-- .../extractors/VidMoxyExtractor.kt | 37 ++++++++++------- .../extractors/VideoSeyredExtractor.kt | 13 +++--- 13 files changed, 155 insertions(+), 150 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index 5e2b2bc1d99..049f92fb4cc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -4,6 +4,7 @@ import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* import org.junit.Assert +import kotlin.random.Random object TestingUtils { open class TestResult(val success: Boolean) { @@ -280,8 +281,8 @@ object TestingUtils { // Test Search Results val searchQueries = - // Use the first 3 home page results as queries since they are guaranteed to exist - (homePageList.take(3).map { it.name } + + // Use the random 3 home page results as queries since they are guaranteed to exist + (homePageList.shuffled(Random).take(3).map { it.name.split(" ").first() } + // If home page is sparse then use generic search queries listOf("over", "iron", "guy")).take(3) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt index 27a5c52a06e..13a717b6dbe 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt @@ -12,53 +12,52 @@ open class ContentX : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - Log.d("Kekik_${this.name}", "url » ${url}") + val extRef = referer ?: "" - val i_source = app.get(url, referer=ext_ref).text - val i_extract = Regex("""window\.openPlayer\('([^']+)'""").find(i_source)!!.groups[1]?.value ?: throw ErrorLoadingException("i_extract is null") + val iSource = app.get(url, referer=extRef).text + val iExtract = Regex("""window\.openPlayer\('([^']+)'""").find(iSource)!!.groups[1]?.value ?: throw ErrorLoadingException("iExtract is null") - val sub_urls = mutableSetOf() - Regex("""\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(i_source).forEach { - val (sub_url, sub_lang) = it.destructured + val subUrls = mutableSetOf() + Regex("""\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(iSource).forEach { + val (subUrl, subLang) = it.destructured - if (sub_url in sub_urls) { return@forEach } - sub_urls.add(sub_url) + if (subUrl in subUrls) { return@forEach } + subUrls.add(subUrl) subtitleCallback.invoke( SubtitleFile( - lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), - url = fixUrl(sub_url.replace("\\", "")) + lang = subLang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(subUrl.replace("\\", "")) ) ) } - val vid_source = app.get("${mainUrl}/source2.php?v=${i_extract}", referer=ext_ref).text - val vid_extract = Regex("""file\":\"([^\"]+)""").find(vid_source)!!.groups[1]?.value ?: throw ErrorLoadingException("vid_extract is null") - val m3u_link = vid_extract.replace("\\", "") + val vidSource = app.get("${mainUrl}/source2.php?v=${iExtract}", referer=extRef).text + val vidExtract = Regex("""file\":\"([^\"]+)""").find(vidSource)!!.groups[1]?.value ?: throw ErrorLoadingException("vidExtract is null") + val m3uLink = vidExtract.replace("\\", "") callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3u_link, + url = m3uLink, referer = url, quality = Qualities.Unknown.value, isM3u8 = true ) ) - val i_dublaj = Regex(""",\"([^']+)\",\"Türkçe""").find(i_source)!!.groups[1]?.value - if (i_dublaj != null) { - val dublaj_source = app.get("${mainUrl}/source2.php?v=${i_dublaj}", referer=ext_ref).text - val dublaj_extract = Regex("""file\":\"([^\"]+)""").find(dublaj_source)!!.groups[1]?.value ?: throw ErrorLoadingException("dublaj_extract is null") - val dublaj_link = dublaj_extract.replace("\\", "") + val iDublaj = Regex(""",\"([^']+)\",\"Türkçe""").find(iSource)!!.groups[1]?.value + if (iDublaj != null) { + val dublajSource = app.get("${mainUrl}/source2.php?v=${iDublaj}", referer=extRef).text + val dublajExtract = Regex("""file\":\"([^\"]+)""").find(dublajSource)!!.groups[1]?.value ?: throw ErrorLoadingException("dublajExtract is null") + val dublajLink = dublajExtract.replace("\\", "") callback.invoke( ExtractorLink( source = "${this.name} Türkçe Dublaj", name = "${this.name} Türkçe Dublaj", - url = dublaj_link, + url = dublajLink, referer = url, quality = Qualities.Unknown.value, isM3u8 = true diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt index 1f70ce6107d..1152cb4b131 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt @@ -16,24 +16,23 @@ open class HDMomPlayer : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val m3u_link:String? - val ext_ref = referer ?: "" - val i_source = app.get(url, referer=ext_ref).text + val m3uLink:String? + val extRef = referer ?: "" + val iSource = app.get(url, referer=extRef).text - val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(i_source)?.groupValues + val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(iSource)?.groupValues if (bePlayer != null) { val bePlayerPass = bePlayer.get(1) val bePlayerData = bePlayer.get(2) val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") - Log.d("Kekik_${this.name}", "encrypted » ${encrypted}") - m3u_link = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1) + m3uLink = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1) } else { - m3u_link = Regex("""file:\"([^\"]+)""").find(i_source)?.groupValues?.get(1) + m3uLink = Regex("""file:\"([^\"]+)""").find(iSource)?.groupValues?.get(1) - val track_str = Regex("""tracks:\[([^\]]+)""").find(i_source)?.groupValues?.get(1) - if (track_str != null) { - val tracks:List = jacksonObjectMapper().readValue("[${track_str}]") + val trackStr = Regex("""tracks:\[([^\]]+)""").find(iSource)?.groupValues?.get(1) + if (trackStr != null) { + val tracks:List = jacksonObjectMapper().readValue("[${trackStr}]") for (track in tracks) { if (track.file == null || track.label == null) continue @@ -53,7 +52,7 @@ open class HDMomPlayer : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = m3u_link ?: throw ErrorLoadingException("m3u link not found"), + url = m3uLink ?: throw ErrorLoadingException("m3u link not found"), referer = url, quality = Qualities.Unknown.value, isM3u8 = true diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt index 8318c3fbd3a..e3cf3aee3e8 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt @@ -13,37 +13,36 @@ open class HDPlayerSystem : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val vid_id = if (url.contains("video/")) { + val extRef = referer ?: "" + val vidId = if (url.contains("video/")) { url.substringAfter("video/") } else { url.substringAfter("?data=") } - val post_url = "${mainUrl}/player/index.php?data=${vid_id}&do=getVideo" - Log.d("Kekik_${this.name}", "post_url » ${post_url}") + val postUrl = "${mainUrl}/player/index.php?data=${vidId}&do=getVideo" val response = app.post( - post_url, + postUrl, data = mapOf( - "hash" to vid_id, - "r" to ext_ref + "hash" to vidId, + "r" to extRef ), - referer = ext_ref, + referer = extRef, headers = mapOf( "Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With" to "XMLHttpRequest" ) ) - val video_response = response.parsedSafe() ?: throw ErrorLoadingException("failed to parse response") - val m3u_link = video_response.securedLink + val videoResponse = response.parsedSafe() ?: throw ErrorLoadingException("failed to parse response") + val m3uLink = videoResponse.securedLink callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3u_link, - referer = ext_ref, + url = m3uLink, + referer = extRef, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt index ce742e97da8..07346c703bd 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt @@ -13,28 +13,25 @@ open class MailRu : ExtractorApi() { override val requiresReferer = false override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - Log.d("Kekik_${this.name}", "url » ${url}") + val extRef = referer ?: "" - val vid_id = url.substringAfter("video/embed/").trim() - val video_req = app.get("${mainUrl}/+/video/meta/${vid_id}", referer=url) - val video_key = video_req.cookies["video_key"].toString() - Log.d("Kekik_${this.name}", "video_key » ${video_key}") + val vidId = url.substringAfter("video/embed/").trim() + val videoReq = app.get("${mainUrl}/+/video/meta/${vidId}", referer=url) + val videoKey = videoReq.cookies["video_key"].toString() - val video_data = AppUtils.tryParseJson(video_req.text) ?: throw ErrorLoadingException("Video not found") + val videoData = AppUtils.tryParseJson(videoReq.text) ?: throw ErrorLoadingException("Video not found") - for (video in video_data.videos) { - Log.d("Kekik_${this.name}", "video » ${video}") + for (video in videoData.videos) { - val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url + val videoUrl = if (video.url.startsWith("//")) "https:${video.url}" else video.url callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = video_url, + url = videoUrl, referer = url, - headers = mapOf("Cookie" to "video_key=${video_key}"), + headers = mapOf("Cookie" to "video_key=${videoKey}"), quality = getQualityFromName(video.key), isM3u8 = false ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt index 6db0830c0f8..31b3d50b7f2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt @@ -13,22 +13,20 @@ open class Odnoklassniki : ExtractorApi() { override val requiresReferer = false override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - Log.d("Kekik_${this.name}", "url » ${url}") + val extRef = referer ?: "" - val user_agent = 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") + val userAgent = 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") - val video_req = app.get(url, headers=user_agent).text.replace("\\"", "\"").replace("\\\\", "\\") + val videoReq = app.get(url, headers=userAgent).text.replace("\\"", "\"").replace("\\\\", "\\") .replace(Regex("\\\\u([0-9A-Fa-f]{4})")) { matchResult -> Integer.parseInt(matchResult.groupValues[1], 16).toChar().toString() } - val videos_str = Regex("""\"videos\":(\[[^\]]*\])""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found") - val videos = AppUtils.tryParseJson>(videos_str) ?: throw ErrorLoadingException("Video not found") + val videosStr = Regex("""\"videos\":(\[[^\]]*\])""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found") + val videos = AppUtils.tryParseJson>(videosStr) ?: throw ErrorLoadingException("Video not found") for (video in videos) { - Log.d("Kekik_${this.name}", "video » ${video}") - val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url + val videoUrl = if (video.url.startsWith("//")) "https:${video.url}" else video.url val quality = video.name.uppercase() .replace("MOBILE", "144p") @@ -44,10 +42,10 @@ open class Odnoklassniki : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = video_url, + url = videoUrl, referer = url, quality = getQualityFromName(quality), - headers = user_agent, + headers = userAgent, isM3u8 = false ) ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt index 0a0050360a1..3a5cf727c32 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt @@ -13,39 +13,38 @@ open class PeaceMakerst : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val m3u_link:String? - val ext_ref = referer ?: "" - val post_url = "${url}?do=getVideo" - Log.d("Kekik_${this.name}", "post_url » ${post_url}") + val m3uLink:String? + val extRef = referer ?: "" + val postUrl = "${url}?do=getVideo" val response = app.post( - post_url, + postUrl, data = mapOf( "hash" to url.substringAfter("video/"), - "r" to ext_ref, + "r" to extRef, "s" to "" ), - referer = ext_ref, + referer = extRef, headers = mapOf( "Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With" to "XMLHttpRequest" ) ) if (response.text.contains("teve2.com.tr\\/embed\\/")) { - val teve2_id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"") - val teve2_response = app.get( - "https://www.teve2.com.tr/action/media/${teve2_id}", - referer = "https://www.teve2.com.tr/embed/${teve2_id}" + val teve2Id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"") + val teve2Response = app.get( + "https://www.teve2.com.tr/action/media/${teve2Id}", + referer = "https://www.teve2.com.tr/embed/${teve2Id}" ).parsedSafe() ?: throw ErrorLoadingException("teve2 response is null") - m3u_link = teve2_response.media.link.serviceUrl + "//" + teve2_response.media.link.securePath + m3uLink = teve2Response.media.link.serviceUrl + "//" + teve2Response.media.link.securePath } else { - val video_response = response.parsedSafe() ?: throw ErrorLoadingException("peace response is null") - val video_sources = video_response.videoSources - if (video_sources.isNotEmpty()) { - m3u_link = video_sources.lastOrNull()?.file + val videoResponse = response.parsedSafe() ?: throw ErrorLoadingException("peace response is null") + val videoSources = videoResponse.videoSources + if (videoSources.isNotEmpty()) { + m3uLink = videoSources.lastOrNull()?.file } else { - m3u_link = null + m3uLink = null } } @@ -53,8 +52,8 @@ open class PeaceMakerst : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = m3u_link ?: throw ErrorLoadingException("m3u link not found"), - referer = ext_ref, + url = m3uLink ?: throw ErrorLoadingException("m3u link not found"), + referer = extRef, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt index 607d2d7896a..1088f2e961e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt @@ -12,36 +12,45 @@ open class RapidVid : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_req = app.get(url, referer=ext_ref).text + val extRef = referer ?: "" + val videoReq = app.get(url, referer=extRef).text - val sub_urls = mutableSetOf() - Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach { - val (sub_url, sub_lang) = it.destructured + val subUrls = mutableSetOf() + Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(videoReq).forEach { + val (subUrl, subLang) = it.destructured - if (sub_url in sub_urls) { return@forEach } - sub_urls.add(sub_url) + if (subUrl in subUrls) { return@forEach } + subUrls.add(subUrl) subtitleCallback.invoke( SubtitleFile( - lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), - url = fixUrl(sub_url.replace("\\", "")) + lang = subLang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(subUrl.replace("\\", "")) ) ) } - val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + var extractedValue = Regex("""file": "(.*)",""").find(videoReq)?.groupValues?.get(1) + var decoded: String? = null - val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() - val decoded = String(bytes, Charsets.UTF_8) - Log.d("Kekik_${this.name}", "decoded » ${decoded}") + if (extractedValue != null) { + val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() + decoded = String(bytes, Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } else { + val evalJWSsetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val JWSsetup = getAndUnpack(getAndUnpack(evalJWSsetup)).replace("\\\\", "\\") + extractedValue = Regex("""file":"(.*)","label""").find(JWSsetup)?.groupValues?.get(1)?.replace("\\\\x", "") + + val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() + decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } callback.invoke( ExtractorLink( source = this.name, name = this.name, url = decoded, - referer = ext_ref, + referer = extRef, quality = Qualities.Unknown.value, isM3u8 = true ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt index ebd57f9ce18..89f731f70a5 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt @@ -12,18 +12,17 @@ open class SibNet : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val i_source = app.get(url, referer=ext_ref).text - var m3u_link = Regex("""player.src\(\[\{src: \"([^\"]+)""").find(i_source)?.groupValues?.get(1) ?: throw ErrorLoadingException("m3u link not found") + val extRef = referer ?: "" + val iSource = app.get(url, referer=extRef).text + var m3uLink = Regex("""player.src\(\[\{src: \"([^\"]+)""").find(iSource)?.groupValues?.get(1) ?: throw ErrorLoadingException("m3u link not found") - m3u_link = "${mainUrl}${m3u_link}" - Log.d("Kekik_${this.name}", "m3u_link » ${m3u_link}") + m3uLink = "${mainUrl}${m3uLink}" callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3u_link, + url = m3uLink, referer = url, quality = Qualities.Unknown.value, type = INFER_TYPE diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt index de5ca9a28c9..f2a75b94f72 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt @@ -13,13 +13,13 @@ open class TRsTX : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" + val extRef = referer ?: "" - val video_req = app.get(url, referer=ext_ref).text + val videoReq = app.get(url, referer=extRef).text - val file = Regex("""file\":\"([^\"]+)""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val file = Regex("""file\":\"([^\"]+)""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") val postLink = "${mainUrl}/" + file.replace("\\", "") - val rawList = app.post(postLink, referer=ext_ref).parsedSafe>() ?: throw ErrorLoadingException("Post link not found") + val rawList = app.post(postLink, referer=extRef).parsedSafe>() ?: throw ErrorLoadingException("Post link not found") val postJson: List = rawList.drop(1).map { item -> val mapItem = item as Map<*, *> @@ -28,37 +28,35 @@ open class TRsTX : ExtractorApi() { file = mapItem["file"] as? String ) } - Log.d("Kekik_${this.name}", "postJson » ${postJson}") - val vid_links = mutableSetOf() - val vid_map = mutableListOf>() + val vidLinks = mutableSetOf() + val vidMap = mutableListOf>() for (item in postJson) { if (item.file == null || item.title == null) continue val fileUrl = "${mainUrl}/playlist/" + item.file.substring(1) + ".txt" - val videoData = app.post(fileUrl, referer=ext_ref).text + val videoData = app.post(fileUrl, referer=extRef).text - if (videoData in vid_links) { continue } - vid_links.add(videoData) + if (videoData in vidLinks) { continue } + vidLinks.add(videoData) - vid_map.add(mapOf( + vidMap.add(mapOf( "title" to item.title, "videoData" to videoData )) } - for (mapEntry in vid_map) { - Log.d("Kekik_${this.name}", "mapEntry » ${mapEntry}") + for (mapEntry in vidMap) { val title = mapEntry["title"] ?: continue - val m3u_link = mapEntry["videoData"] ?: continue + val m3uLink = mapEntry["videoData"] ?: continue callback.invoke( ExtractorLink( source = this.name, name = "${this.name} - ${title}", - url = m3u_link, - referer = ext_ref, + url = m3uLink, + referer = extRef, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt index 157374a347e..0893b4dec4a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt @@ -13,12 +13,11 @@ open class TauVideo : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_key = url.split("/").last() - val video_url = "${mainUrl}/api/video/${video_key}" - Log.d("Kekik_${this.name}", "video_url » ${video_url}") + val extRef = referer ?: "" + val videoKey = url.split("/").last() + val videoUrl = "${mainUrl}/api/video/${videoKey}" - val api = app.get(video_url).parsedSafe() ?: throw ErrorLoadingException("TauVideo") + val api = app.get(videoUrl).parsedSafe() ?: throw ErrorLoadingException("TauVideo") for (video in api.urls) { callback.invoke( @@ -26,7 +25,7 @@ open class TauVideo : ExtractorApi() { source = this.name, name = this.name, url = video.url, - referer = ext_ref, + referer = extRef, quality = getQualityFromName(video.label), type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt index e57772cefaa..f7c3dd5e4c2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt @@ -12,36 +12,45 @@ open class VidMoxy : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_req = app.get(url, referer=ext_ref).text + val extRef = referer ?: "" + val videoReq = app.get(url, referer=extRef).text - val sub_urls = mutableSetOf() - Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach { - val (sub_url, sub_lang) = it.destructured + val subUrls = mutableSetOf() + Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(videoReq).forEach { + val (subUrl, subLang) = it.destructured - if (sub_url in sub_urls) { return@forEach } - sub_urls.add(sub_url) + if (subUrl in subUrls) { return@forEach } + subUrls.add(subUrl) subtitleCallback.invoke( SubtitleFile( - lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), - url = fixUrl(sub_url.replace("\\", "")) + lang = subLang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(subUrl.replace("\\", "")) ) ) } - val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + var extractedValue = Regex("""file": "(.*)",""").find(videoReq)?.groupValues?.get(1) + var decoded: String? = null - val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() - val decoded = String(bytes, Charsets.UTF_8) - Log.d("Kekik_${this.name}", "decoded » ${decoded}") + if (extractedValue != null) { + val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() + decoded = String(bytes, Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } else { + val evaljwSetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val jwSetup = getAndUnpack(getAndUnpack(evaljwSetup)).replace("\\\\", "\\") + extractedValue = Regex("""file":"(.*)","label""").find(jwSetup)?.groupValues?.get(1)?.replace("\\\\x", "") + + val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() + decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } callback.invoke( ExtractorLink( source = this.name, name = this.name, url = decoded, - referer = ext_ref, + referer = extRef, quality = Qualities.Unknown.value, isM3u8 = true ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt index 1161ff6600a..c85e64165c6 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt @@ -15,14 +15,13 @@ open class VideoSeyred : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_id = url.substringAfter("embed/").substringBefore("?") - val video_url = "${mainUrl}/playlist/${video_id}.json" - Log.d("Kekik_${this.name}", "video_url » ${video_url}") + val extRef = referer ?: "" + val videoId = url.substringAfter("embed/").substringBefore("?") + val videoUrl = "${mainUrl}/playlist/${videoId}.json" - val response_raw = app.get(video_url) - val response_list:List = jacksonObjectMapper().readValue(response_raw.text) ?: throw ErrorLoadingException("VideoSeyred") - val response = response_list[0] ?: throw ErrorLoadingException("VideoSeyred") + val responseRaw = app.get(videoUrl) + val responseList:List = jacksonObjectMapper().readValue(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred") + val response = responseList[0] ?: throw ErrorLoadingException("VideoSeyred") for (track in response.tracks) { if (track.label != null && track.kind == "captions") { From 8fcb3e3121de93017f7b3cbf959b4429040a2184 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 30 Jul 2024 12:45:25 -0600 Subject: [PATCH 020/235] Fix cast recycler scrolling (#1221) --- .../ui/result/ResultFragmentPhone.kt | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) 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 f1399e8d1a6..97bc49eaeee 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 @@ -23,6 +23,7 @@ import androidx.core.widget.NestedScrollView import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider import com.discord.panels.OverlappingPanelsLayout +import com.discord.panels.PanelState import com.discord.panels.PanelsChildGestureRegionObserver import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastContext @@ -118,6 +119,14 @@ open class ResultFragmentPhone : FullScreenPlayer() { return root } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { register(it) } + } + } + var currentTrailers: List = emptyList() var currentTrailerIndex = 0 @@ -210,9 +219,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { } override fun onDestroyView() { - - //somehow this still leaks and I dont know why???? - // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt PanelsChildGestureRegionObserver.Provider.get().let { obs -> resultBinding?.resultCastItems?.let { obs.unregister(it) @@ -329,13 +335,18 @@ open class ResultFragmentPhone : FullScreenPlayer() { syncModel.addFromUrl(storedData.url) val api = APIHolder.getApiFromNameNull(storedData.apiName) - PanelsChildGestureRegionObserver.Provider.get().apply { - resultBinding?.resultCastItems?.let { - register(it) + // This may not be 100% reliable, and may delay for small period + // before resultCastItems will be scrollable again, but this does work + // most of the time. + binding?.resultOverlappingPanels?.registerEndPanelStateListeners( + object : OverlappingPanelsLayout.PanelStateListener { + override fun onPanelStateChange(panelState: PanelState) { + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { register(it) } + } + } } - addGestureRegionsUpdateListener(gestureRegionsListener) - } - + ) // ===== ===== ===== @@ -674,6 +685,9 @@ open class ResultFragmentPhone : FullScreenPlayer() { observe(viewModel.page) { data -> if (data == null) return@observe resultBinding?.apply { + PanelsChildGestureRegionObserver.Provider.get().apply { + register(resultCastItems) + } (data as? Resource.Success)?.value?.let { d -> resultVpn.setText(d.vpnText) resultInfo.setText(d.metaText) @@ -1167,4 +1181,4 @@ open class ResultFragmentPhone : FullScreenPlayer() { } } } -} +} \ No newline at end of file From ab379ab31c30069aac3afdeec76410e69ea6bc95 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 30 Jul 2024 12:54:54 -0600 Subject: [PATCH 021/235] Support for multi deleting downloads and other major improvements/fixes (#1177) --- .../lagradost/cloudstream3/MainActivity.kt | 51 +- .../ui/download/DownloadAdapter.kt | 388 ++++++++++----- .../ui/download/DownloadButtonSetup.kt | 31 +- .../ui/download/DownloadChildFragment.kt | 172 +++++-- .../ui/download/DownloadFragment.kt | 182 +++++-- .../ui/download/DownloadViewModel.kt | 451 +++++++++++++++--- .../ui/download/button/BaseFetchButton.kt | 7 +- .../ui/download/button/PieFetchButton.kt | 6 +- .../ui/player/DownloadFileGenerator.kt | 32 +- .../ui/player/DownloadedPlayerActivity.kt | 11 +- .../ui/result/ResultTrailerPlayer.kt | 30 +- .../cloudstream3/utils/AppContextUtils.kt | 14 +- .../utils/BackPressedCallbackHelper.kt | 30 ++ .../cloudstream3/utils/SnackbarHelper.kt | 84 ++++ .../cloudstream3/utils/SubtitleUtils.kt | 56 +++ .../utils/VideoDownloadManager.kt | 45 +- .../res/layout/download_child_episode.xml | 17 +- .../res/layout/download_header_episode.xml | 11 + .../res/layout/fragment_child_downloads.xml | 59 ++- .../main/res/layout/fragment_downloads.xml | 111 +++-- app/src/main/res/values/strings.xml | 12 +- 21 files changed, 1369 insertions(+), 431 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index b59265eee46..5408d2a893b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -133,6 +133,8 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback @@ -151,6 +153,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog +import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute @@ -1254,17 +1257,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa this.setKey(getString(R.string.jsdelivr_proxy_key), false) } else { this.setKey(getString(R.string.jsdelivr_proxy_key), true) - val parentView: View = findViewById(android.R.id.content) - Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG) - .let { snackbar -> - snackbar.setAction(R.string.revert) { - setKey(getString(R.string.jsdelivr_proxy_key), false) - } - snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground)) - snackbar.setTextColor(colorFromAttribute(R.attr.textColor)) - snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary)) - snackbar.show() - } + showSnackbar( + this@MainActivity, + R.string.jsdelivr_enabled, + Snackbar.LENGTH_LONG, + R.string.revert + ) { setKey(getString(R.string.jsdelivr_proxy_key), false) } } } } @@ -1603,7 +1601,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa if (isLayout(TV or EMULATOR)) { if (navDestination.matchDestination(R.id.navigation_home)) { - attachBackPressedCallback() + attachBackPressedCallback { + showConfirmExitDialog() + window?.navigationBarColor = + colorFromAttribute(R.attr.primaryGrayBackground) + updateLocale() + } } else detachBackPressedCallback() } } @@ -1848,28 +1851,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa finish() } - private var backPressedCallback: OnBackPressedCallback? = null - - private fun attachBackPressedCallback() { - if (backPressedCallback == null) { - backPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - showConfirmExitDialog() - window?.navigationBarColor = - colorFromAttribute(R.attr.primaryGrayBackground) - updateLocale() - } - } - } - - backPressedCallback?.isEnabled = true - onBackPressedDispatcher.addCallback(this, backPressedCallback ?: return) - } - - private fun detachBackPressedCallback() { - backPressedCallback?.isEnabled = false - } - suspend fun checkGithubConnectivity(): Boolean { return try { app.get( @@ -1880,4 +1861,4 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa false } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt index 9a0263342b2..204584296dd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -1,9 +1,9 @@ package com.lagradost.cloudstream3.ui.download -import android.annotation.SuppressLint import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.ViewGroup +import android.widget.CheckBox import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil @@ -31,47 +31,30 @@ const val DOWNLOAD_ACTION_LONG_CLICK = 5 const val DOWNLOAD_ACTION_GO_TO_CHILD = 0 const val DOWNLOAD_ACTION_LOAD_RESULT = 1 -abstract class VisualDownloadCached( - open val currentBytes: Long, - open val totalBytes: Long, - open val data: VideoDownloadHelper.DownloadCached -) { - - // Just to be extra-safe with areContentsTheSame - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is VisualDownloadCached) return false - - if (currentBytes != other.currentBytes) return false - if (totalBytes != other.totalBytes) return false - if (data != other.data) return false - - return true - } - - override fun hashCode(): Int { - var result = currentBytes.hashCode() - result = 31 * result + totalBytes.hashCode() - result = 31 * result + data.hashCode() - return result - } +sealed class VisualDownloadCached { + abstract val currentBytes: Long + abstract val totalBytes: Long + abstract val data: VideoDownloadHelper.DownloadCached + abstract var isSelected: Boolean + + data class Child( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadEpisodeCached, + override var isSelected: Boolean, + ) : VisualDownloadCached() + + data class Header( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadHeaderCached, + override var isSelected: Boolean, + val child: VideoDownloadHelper.DownloadEpisodeCached?, + val currentOngoingDownloads: Int, + val totalDownloads: Int, + ) : VisualDownloadCached() } -data class VisualDownloadChildCached( - override val currentBytes: Long, - override val totalBytes: Long, - override val data: VideoDownloadHelper.DownloadEpisodeCached, -): VisualDownloadCached(currentBytes, totalBytes, data) - -data class VisualDownloadHeaderCached( - override val currentBytes: Long, - override val totalBytes: Long, - override val data: VideoDownloadHelper.DownloadHeaderCached, - val child: VideoDownloadHelper.DownloadEpisodeCached?, - val currentOngoingDownloads: Int, - val totalDownloads: Int, -): VisualDownloadCached(currentBytes, totalBytes, data) - data class DownloadClickEvent( val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached @@ -83,108 +66,180 @@ data class DownloadHeaderClickEvent( ) class DownloadAdapter( - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val mediaClickCallback: (DownloadClickEvent) -> Unit, + private val onHeaderClickEvent: (DownloadHeaderClickEvent) -> Unit, + private val onItemClickEvent: (DownloadClickEvent) -> Unit, + private val onItemSelectionChanged: (Int, Boolean) -> Unit, ) : ListAdapter(DiffCallback()) { + private var isMultiDeleteState: Boolean = false + companion object { private const val VIEW_TYPE_HEADER = 0 private const val VIEW_TYPE_CHILD = 1 } inner class DownloadViewHolder( - private val binding: ViewBinding, - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val mediaClickCallback: (DownloadClickEvent) -> Unit, + private val binding: ViewBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(card: VisualDownloadCached?) { when (binding) { - is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadHeaderCached) - is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadChildCached) + is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadCached.Header) + is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadCached.Child) } } - @SuppressLint("SetTextI18n") - private fun bindHeader(card: VisualDownloadHeaderCached?) { - if (binding !is DownloadHeaderEpisodeBinding) return - card ?: return - val d = card.data + private fun bindHeader(card: VisualDownloadCached.Header?) { + if (binding !is DownloadHeaderEpisodeBinding || card == null) return + val data = card.data binding.apply { - downloadHeaderPoster.apply { - setImage(d.poster) - setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_LOAD_RESULT, d)) + episodeHolder.apply { + if (isMultiDeleteState) { + setOnClickListener { + toggleIsChecked(deleteCheckbox, data.id) + } } - } - downloadHeaderTitle.text = d.name - val formattedSizeString = formatShortFileSize(itemView.context, card.totalBytes) + setOnLongClickListener { + toggleIsChecked(deleteCheckbox, data.id) + true + } + } - if (card.child != null) { - downloadHeaderGotoChild.isVisible = false - - val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes) - if (status == DownloadStatusTell.IsDone) { - // We do this here instead if we are finished downloading - // so that we can use the value from the view model - // rather than extra unneeded disk operations and to prevent a - // delay in updating download icon state. - downloadButton.setProgress(card.currentBytes, card.totalBytes) - downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes) - // We will let the view model handle this - downloadButton.doSetProgress = false - downloadButton.progressBar.progressDrawable = - downloadButton.getDrawableFromStatus(status) - ?.let { ContextCompat.getDrawable(downloadButton.context, it) } - downloadHeaderInfo.text = formattedSizeString + downloadHeaderPoster.apply { + setImage(data.poster) + if (isMultiDeleteState) { + setOnClickListener { + toggleIsChecked(deleteCheckbox, data.id) + } } else { - downloadButton.doSetProgress = true - downloadButton.progressBar.progressDrawable = - ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + setOnClickListener { + onHeaderClickEvent.invoke( + DownloadHeaderClickEvent( + DOWNLOAD_ACTION_LOAD_RESULT, + data + ) + ) + } } - downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback) - downloadButton.isVisible = true - - episodeHolder.setOnClickListener { - mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child)) - } - } else { - downloadButton.isVisible = false - downloadHeaderGotoChild.isVisible = true - - try { - downloadHeaderInfo.text = downloadHeaderInfo.context.getString(R.string.extra_info_format) - .format( - card.totalDownloads, - downloadHeaderInfo.context.resources.getQuantityString( - R.plurals.episodes, - card.totalDownloads - ), - formattedSizeString - ) - } catch (e: Exception) { - // You probably formatted incorrectly - downloadHeaderInfo.text = "Error" - logError(e) + setOnLongClickListener { + toggleIsChecked(deleteCheckbox, data.id) + true } + } + downloadHeaderTitle.text = data.name + val formattedSize = formatShortFileSize(itemView.context, card.totalBytes) + + if (card.child != null) { + handleChildDownload(card, formattedSize) + } else handleParentDownload(card, formattedSize) - episodeHolder.setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_GO_TO_CHILD, d)) + if (isMultiDeleteState) { + deleteCheckbox.setOnCheckedChangeListener { _, isChecked -> + onItemSelectionChanged.invoke(data.id, isChecked) } + } else deleteCheckbox.setOnCheckedChangeListener(null) + + deleteCheckbox.apply { + isVisible = isMultiDeleteState + isChecked = card.isSelected + } + } + } + + private fun DownloadHeaderEpisodeBinding.handleChildDownload( + card: VisualDownloadCached.Header, + formattedSize: String + ) { + card.child ?: return + downloadHeaderGotoChild.isVisible = false + + val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes) + if (status == DownloadStatusTell.IsDone) { + // We do this here instead if we are finished downloading + // so that we can use the value from the view model + // rather than extra unneeded disk operations and to prevent a + // delay in updating download icon state. + downloadButton.setProgress(card.currentBytes, card.totalBytes) + downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes) + // We will let the view model handle this + downloadButton.doSetProgress = false + downloadButton.progressBar.progressDrawable = + downloadButton.getDrawableFromStatus(status) + ?.let { ContextCompat.getDrawable(downloadButton.context, it) } + downloadHeaderInfo.text = formattedSize + } else { + // We need to make sure we restore the correct progress + // when we refresh data in the adapter. + downloadButton.resetView() + val drawable = downloadButton.getDrawableFromStatus(status)?.let { + ContextCompat.getDrawable(downloadButton.context, it) + } + downloadButton.statusView.setImageDrawable(drawable) + downloadButton.progressBar.progressDrawable = + ContextCompat.getDrawable( + downloadButton.context, + downloadButton.progressDrawable + ) + } + + downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, onItemClickEvent) + downloadButton.isVisible = !isMultiDeleteState + + if (!isMultiDeleteState) { + episodeHolder.setOnClickListener { + onItemClickEvent.invoke( + DownloadClickEvent( + DOWNLOAD_ACTION_PLAY_FILE, + card.child + ) + ) + } + } + } + + private fun DownloadHeaderEpisodeBinding.handleParentDownload( + card: VisualDownloadCached.Header, + formattedSize: String + ) { + downloadButton.isVisible = false + downloadHeaderGotoChild.isVisible = !isMultiDeleteState + + try { + downloadHeaderInfo.text = + downloadHeaderInfo.context.getString(R.string.extra_info_format).format( + card.totalDownloads, + downloadHeaderInfo.context.resources.getQuantityString( + R.plurals.episodes, + card.totalDownloads + ), + formattedSize + ) + } catch (e: Exception) { + downloadHeaderInfo.text = "" + logError(e) + } + + if (!isMultiDeleteState) { + episodeHolder.setOnClickListener { + onHeaderClickEvent.invoke( + DownloadHeaderClickEvent( + DOWNLOAD_ACTION_GO_TO_CHILD, + card.data + ) + ) } } } - private fun bindChild(card: VisualDownloadChildCached?) { - if (binding !is DownloadChildEpisodeBinding) return - card ?: return - val d = card.data + private fun bindChild(card: VisualDownloadCached.Child?) { + if (binding !is DownloadChildEpisodeBinding || card == null) return + val data = card.data binding.apply { - val posDur = getViewPos(d.id) + val posDur = getViewPos(data.id) downloadChildEpisodeProgress.apply { isVisible = posDur != null posDur?.let { @@ -194,36 +249,87 @@ class DownloadAdapter( } } - val status = downloadButton.getStatus(d.id, card.currentBytes, card.totalBytes) + val status = downloadButton.getStatus(data.id, card.currentBytes, card.totalBytes) if (status == DownloadStatusTell.IsDone) { // We do this here instead if we are finished downloading // so that we can use the value from the view model // rather than extra unneeded disk operations and to prevent a // delay in updating download icon state. downloadButton.setProgress(card.currentBytes, card.totalBytes) - downloadButton.applyMetaData(d.id, card.currentBytes, card.totalBytes) + downloadButton.applyMetaData(data.id, card.currentBytes, card.totalBytes) // We will let the view model handle this downloadButton.doSetProgress = false downloadButton.progressBar.progressDrawable = downloadButton.getDrawableFromStatus(status) ?.let { ContextCompat.getDrawable(downloadButton.context, it) } - downloadChildEpisodeTextExtra.text = formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes) + downloadChildEpisodeTextExtra.text = + formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes) } else { - downloadButton.doSetProgress = true + // We need to make sure we restore the correct progress + // when we refresh data in the adapter. + downloadButton.resetView() + val drawable = downloadButton.getDrawableFromStatus(status)?.let { + ContextCompat.getDrawable(downloadButton.context, it) + } + downloadButton.statusView.setImageDrawable(drawable) downloadButton.progressBar.progressDrawable = - ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + ContextCompat.getDrawable( + downloadButton.context, + downloadButton.progressDrawable + ) } - downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, mediaClickCallback) - downloadButton.isVisible = true + downloadButton.setDefaultClickListener( + data, + downloadChildEpisodeTextExtra, + onItemClickEvent + ) + downloadButton.isVisible = !isMultiDeleteState downloadChildEpisodeText.apply { - text = context.getNameFull(d.name, d.episode, d.season) + text = context.getNameFull(data.name, data.episode, data.season) isSelected = true // Needed for text repeating } downloadChildEpisodeHolder.setOnClickListener { - mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) + onItemClickEvent.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, data)) + } + + downloadChildEpisodeHolder.apply { + when { + isMultiDeleteState -> { + setOnClickListener { + toggleIsChecked(deleteCheckbox, data.id) + } + } + + else -> { + setOnClickListener { + onItemClickEvent.invoke( + DownloadClickEvent( + DOWNLOAD_ACTION_PLAY_FILE, + data + ) + ) + } + } + } + + setOnLongClickListener { + toggleIsChecked(deleteCheckbox, data.id) + true + } + } + + if (isMultiDeleteState) { + deleteCheckbox.setOnCheckedChangeListener { _, isChecked -> + onItemSelectionChanged.invoke(data.id, isChecked) + } + } else deleteCheckbox.setOnCheckedChangeListener(null) + + deleteCheckbox.apply { + isVisible = isMultiDeleteState + isChecked = card.isSelected } } } @@ -236,7 +342,7 @@ class DownloadAdapter( VIEW_TYPE_CHILD -> DownloadChildEpisodeBinding.inflate(inflater, parent, false) else -> throw IllegalArgumentException("Invalid view type") } - return DownloadViewHolder(binding, clickCallback, mediaClickCallback) + return DownloadViewHolder(binding) } override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) { @@ -245,18 +351,52 @@ class DownloadAdapter( override fun getItemViewType(position: Int): Int { return when (getItem(position)) { - is VisualDownloadChildCached -> VIEW_TYPE_CHILD - is VisualDownloadHeaderCached -> VIEW_TYPE_HEADER + is VisualDownloadCached.Child -> VIEW_TYPE_CHILD + is VisualDownloadCached.Header -> VIEW_TYPE_HEADER else -> throw IllegalArgumentException("Invalid data type at position $position") } } + fun setIsMultiDeleteState(value: Boolean) { + if (isMultiDeleteState == value) return + isMultiDeleteState = value + notifyItemRangeChanged(0, itemCount) + } + + fun notifyAllSelected() { + currentList.indices.forEach { index -> + if (!currentList[index].isSelected) { + notifyItemChanged(index) + } + } + } + + fun notifySelectionStates() { + currentList.indices.forEach { index -> + if (currentList[index].isSelected) { + notifyItemChanged(index) + } + } + } + + private fun toggleIsChecked(checkbox: CheckBox, itemId: Int) { + val isChecked = !checkbox.isChecked + checkbox.isChecked = isChecked + onItemSelectionChanged.invoke(itemId, isChecked) + } + class DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + override fun areItemsTheSame( + oldItem: VisualDownloadCached, + newItem: VisualDownloadCached + ): Boolean { return oldItem.data.id == newItem.data.id } - override fun areContentsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + override fun areContentsTheSame( + oldItem: VisualDownloadCached, + newItem: VisualDownloadCached + ): Boolean { return oldItem == newItem } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index c8c40e292cd..bf2c1b49bc7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -1,11 +1,10 @@ package com.lagradost.cloudstream3.ui.download import android.content.DialogInterface -import android.widget.Toast import androidx.appcompat.app.AlertDialog +import com.google.android.material.snackbar.Snackbar import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator @@ -14,9 +13,11 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE +import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager +import kotlinx.coroutines.MainScope object DownloadButtonSetup { fun handleDownloadClick(click: DownloadClickEvent) { @@ -29,9 +30,15 @@ object DownloadButtonSetup { DialogInterface.OnClickListener { _, which -> when (which) { DialogInterface.BUTTON_POSITIVE -> { - VideoDownloadManager.deleteFileAndUpdateSettings(ctx, id) + VideoDownloadManager.deleteFilesAndUpdateSettings( + ctx, + setOf(id), + MainScope() + ) } + DialogInterface.BUTTON_NEGATIVE -> { + // Do nothing on cancel } } } @@ -56,11 +63,13 @@ object DownloadButtonSetup { } } } + DOWNLOAD_ACTION_PAUSE_DOWNLOAD -> { VideoDownloadManager.downloadEvent.invoke( Pair(click.data.id, VideoDownloadManager.DownloadActionType.Pause) ) } + DOWNLOAD_ACTION_RESUME_DOWNLOAD -> { activity?.let { ctx -> if (VideoDownloadManager.downloadStatus.containsKey(id) && VideoDownloadManager.downloadStatus[id] == VideoDownloadManager.DownloadType.IsPaused) { @@ -79,6 +88,7 @@ object DownloadButtonSetup { } } } + DOWNLOAD_ACTION_LONG_CLICK -> { activity?.let { act -> val length = @@ -88,12 +98,15 @@ object DownloadButtonSetup { )?.fileLength ?: 0 if (length > 0) { - showToast(R.string.delete, Toast.LENGTH_LONG) - } else { - showToast(R.string.download, Toast.LENGTH_LONG) + showSnackbar( + act, + R.string.offline_file, + Snackbar.LENGTH_LONG + ) } } } + DOWNLOAD_ACTION_PLAY_FILE -> { activity?.let { act -> val info = @@ -119,7 +132,7 @@ object DownloadButtonSetup { id = click.data.id, parentId = click.data.parentId, - name = act.getString(R.string.downloaded_file), //click.data.name ?: keyInfo.displayName + name = act.getString(R.string.downloaded_file), // click.data.name ?: keyInfo.displayName season = click.data.season, episode = click.data.episode, headerName = parent.name, @@ -132,7 +145,7 @@ object DownloadButtonSetup { ) ) ) - //R.id.global_to_navigation_player, PlayerFragment.newInstance( + // R.id.global_to_navigation_player, PlayerFragment.newInstance( // UriData( // info.path.toString(), // keyInfo.basePath, @@ -145,7 +158,7 @@ object DownloadButtonSetup { // click.data.season // ), // getViewPos(click.data.id)?.position ?: 0 - //) + // ) ) } } 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 03db948c428..09c48a04337 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 @@ -1,29 +1,33 @@ package com.lagradost.cloudstream3.ui.download import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding +import com.lagradost.cloudstream3.mvvm.observe 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.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback 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 -import kotlinx.coroutines.withContext class DownloadChildFragment : Fragment() { + private lateinit var downloadsViewModel: DownloadViewModel + private var binding: FragmentChildDownloadsBinding? = null + companion object { fun newInstance(headerName: String, folder: String): Bundle { return Bundle().apply { @@ -34,61 +38,54 @@ class DownloadChildFragment : Fragment() { } override fun onDestroyView() { - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } - downloadDeleteEventListener = null + detachBackPressedCallback() binding = null super.onDestroyView() } - private var binding: FragmentChildDownloadsBinding? = null - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false) binding = localBinding return localBinding.root } - private fun updateList(folder: String) = main { - context?.let { ctx -> - val data = withContext(Dispatchers.IO) { ctx.getKeys(folder) } - val eps = withContext(Dispatchers.IO) { - data.mapNotNull { key -> - context?.getKey(key) - }.mapNotNull { - val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(ctx, it.id) - ?: return@mapNotNull null - VisualDownloadChildCached( - currentBytes = info.fileLength, - totalBytes = info.totalBytes, - data = it, - ) - } - }.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 } - if (eps.isEmpty()) { - activity?.onBackPressedDispatcher?.onBackPressed() - return@main - } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(eps) + /** + * We never want to retain multi-delete state + * when navigating to downloads. Setting this state + * immediately can sometimes result in the observer + * not being notified in time to update the UI. + * + * By posting to the main looper, we ensure that this + * operation is executed after the view has been fully created + * and all initializations are completed, allowing the + * observer to properly receive and handle the state change. + */ + Handler(Looper.getMainLooper()).post { + downloadsViewModel.setIsMultiDeleteState(false) } - } - private var downloadDeleteEventListener: ((Int) -> Unit)? = null - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + /** + * We have to make sure selected items are + * cleared here as well so we don't run in an + * inconsistent state where selected items do + * not match the multi delete state we are in. + */ + downloadsViewModel.clearSelectedItems() val folder = arguments?.getString("folder") val name = arguments?.getString("name") if (folder == null) { - activity?.onBackPressedDispatcher?.onBackPressed() // TODO FIX + activity?.onBackPressedDispatcher?.onBackPressed() return } - fixPaddingStatusbar(binding?.downloadChildRoot) binding?.downloadChildToolbar?.apply { title = name @@ -101,13 +98,55 @@ class DownloadChildFragment : Fragment() { setAppBarNoScrollFlagsOnTV() } + binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV() + + observe(downloadsViewModel.childCards) { + if (it.isEmpty()) { + activity?.onBackPressedDispatcher?.onBackPressed() + return@observe + } + + (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(it) + } + observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState -> + val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter + adapter?.setIsMultiDeleteState(isMultiDeleteState) + binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState + if (!isMultiDeleteState) { + detachBackPressedCallback() + downloadsViewModel.clearSelectedItems() + binding?.downloadChildToolbar?.isVisible = true + } + } + observe(downloadsViewModel.selectedBytes) { + updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it) + } + observe(downloadsViewModel.selectedItemIds) { + handleSelectedChange(it) + updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L) + + binding?.btnDelete?.isVisible = it.isNotEmpty() + binding?.selectItemsText?.isVisible = it.isEmpty() + + val allSelected = downloadsViewModel.isAllSelected() + if (allSelected) { + binding?.btnToggleAll?.setText(R.string.deselect_all) + } else binding?.btnToggleAll?.setText(R.string.select_all) + } + val adapter = DownloadAdapter( {}, - { downloadClickEvent -> - handleDownloadClick(downloadClickEvent) - if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { - setUpDownloadDeleteListener(folder) - } + { click -> + if (click.action == DOWNLOAD_ACTION_DELETE_FILE) { + context?.let { ctx -> + downloadsViewModel.handleSingleDelete(ctx, click.data.id) + } + } else handleDownloadClick(click) + }, + { itemId, isChecked -> + if (isChecked) { + downloadsViewModel.addSelected(itemId) + } else downloadsViewModel.removeSelected(itemId) } ) @@ -122,18 +161,47 @@ class DownloadChildFragment : Fragment() { ) } - updateList(folder) + context?.let { downloadsViewModel.updateChildList(it, folder) } + fixPaddingStatusbar(binding?.downloadChildRoot) } - private fun setUpDownloadDeleteListener(folder: String) { - downloadDeleteEventListener = { id: Int -> - val list = (binding?.downloadChildList?.adapter as? DownloadAdapter)?.currentList - if (list != null) { - if (list.any { it.data.id == id }) { - updateList(folder) + private fun handleSelectedChange(selected: MutableSet) { + if (selected.isNotEmpty()) { + binding?.downloadDeleteAppbar?.isVisible = true + binding?.downloadChildToolbar?.isVisible = false + activity?.attachBackPressedCallback { + downloadsViewModel.setIsMultiDeleteState(false) + } + + binding?.btnDelete?.setOnClickListener { + context?.let { ctx -> + downloadsViewModel.handleMultiDelete(ctx) } } + + binding?.btnCancel?.setOnClickListener { + downloadsViewModel.setIsMultiDeleteState(false) + } + + binding?.btnToggleAll?.setOnClickListener { + val allSelected = downloadsViewModel.isAllSelected() + val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter + if (allSelected) { + adapter?.notifySelectionStates() + downloadsViewModel.clearSelectedItems() + } else { + adapter?.notifyAllSelected() + downloadsViewModel.selectAllItems() + } + } + + downloadsViewModel.setIsMultiDeleteState(true) } - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + } + + private fun updateDeleteButton(count: Int, selectedBytes: Long) { + val formattedSize = formatShortFileSize(context, selectedBytes) + binding?.btnDelete?.text = + getString(R.string.delete_format).format(count, formattedSize) } } \ No newline at end of file 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 23d546e1083..447b4f1310f 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 @@ -8,6 +8,8 @@ import android.content.Intent import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION import android.os.Build import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View @@ -17,7 +19,6 @@ import android.widget.TextView import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes -import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged @@ -27,7 +28,7 @@ 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.isEpisodeBased import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick @@ -40,20 +41,22 @@ 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.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE -import com.lagradost.cloudstream3.utils.DataStore +import com.lagradost.cloudstream3.utils.DataStore.getFolderName 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.VideoDownloadManager import java.net.URI const val DOWNLOAD_NAVIGATE_TO = "downloadpage" class DownloadFragment : Fragment() { private lateinit var downloadsViewModel: DownloadViewModel + private var binding: FragmentDownloadsBinding? = null private fun View.setLayoutWidth(weight: Long) { val param = LinearLayout.LayoutParams( @@ -65,14 +68,11 @@ class DownloadFragment : Fragment() { } override fun onDestroyView() { - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } - downloadDeleteEventListener = null + detachBackPressedCallback() binding = null super.onDestroyView() } - private var binding: FragmentDownloadsBinding? = null - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -84,12 +84,34 @@ class DownloadFragment : Fragment() { return localBinding.root } - private var downloadDeleteEventListener: ((Int) -> Unit)? = null - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) hideKeyboard() binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() + binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV() + + /** + * We never want to retain multi-delete state + * when navigating to downloads. Setting this state + * immediately can sometimes result in the observer + * not being notified in time to update the UI. + * + * By posting to the main looper, we ensure that this + * operation is executed after the view has been fully created + * and all initializations are completed, allowing the + * observer to properly receive and handle the state change. + */ + Handler(Looper.getMainLooper()).post { + downloadsViewModel.setIsMultiDeleteState(false) + } + + /** + * We have to make sure selected items are + * cleared here as well so we don't run in an + * inconsistent state where selected items do + * not match the multi delete state we are in. + */ + downloadsViewModel.clearSelectedItems() observe(downloadsViewModel.headerCards) { (binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(it) @@ -97,25 +119,82 @@ class DownloadFragment : Fragment() { binding?.textNoDownloads?.isVisible = it.isEmpty() } observe(downloadsViewModel.availableBytes) { - updateStorageInfo(view.context, it, R.string.free_storage, binding?.downloadFreeTxt, binding?.downloadFree) + updateStorageInfo( + view.context, + it, + R.string.free_storage, + binding?.downloadFreeTxt, + binding?.downloadFree + ) } observe(downloadsViewModel.usedBytes) { - updateStorageInfo(view.context, it, R.string.used_storage, binding?.downloadUsedTxt, binding?.downloadUsed) - binding?.downloadStorageAppbar?.isVisible = it > 0 + updateStorageInfo( + view.context, + it, + R.string.used_storage, + binding?.downloadUsedTxt, + binding?.downloadUsed + ) + + // Prevent race condition and make sure + // we don't display it early + if ( + downloadsViewModel.isMultiDeleteState.value == null || + downloadsViewModel.isMultiDeleteState.value == false + ) binding?.downloadStorageAppbar?.isVisible = it > 0 } observe(downloadsViewModel.downloadBytes) { - updateStorageInfo(view.context, it, R.string.app_storage, binding?.downloadAppTxt, binding?.downloadApp) + updateStorageInfo( + view.context, + it, + R.string.app_storage, + binding?.downloadAppTxt, + binding?.downloadApp + ) + } + observe(downloadsViewModel.selectedBytes) { + updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it) + } + observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState -> + val adapter = binding?.downloadList?.adapter as? DownloadAdapter + adapter?.setIsMultiDeleteState(isMultiDeleteState) + binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState + if (!isMultiDeleteState) { + detachBackPressedCallback() + downloadsViewModel.clearSelectedItems() + // Prevent race condition and make sure + // we don't display it early + if (downloadsViewModel.usedBytes.value?.let { it > 0 } == true) { + binding?.downloadStorageAppbar?.isVisible = true + } + } + } + observe(downloadsViewModel.selectedItemIds) { + handleSelectedChange(it) + updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L) + + binding?.btnDelete?.isVisible = it.isNotEmpty() + binding?.selectItemsText?.isVisible = it.isEmpty() + + val allSelected = downloadsViewModel.isAllSelected() + if (allSelected) { + binding?.btnToggleAll?.setText(R.string.deselect_all) + } else binding?.btnToggleAll?.setText(R.string.select_all) } val adapter = DownloadAdapter( + { click -> handleItemClick(click) }, { click -> - handleItemClick(click) + if (click.action == DOWNLOAD_ACTION_DELETE_FILE) { + context?.let { ctx -> + downloadsViewModel.handleSingleDelete(ctx, click.data.id) + } + } else handleDownloadClick(click) }, - { downloadClickEvent -> - handleDownloadClick(downloadClickEvent) - if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { - setUpDownloadDeleteListener() - } + { itemId, isChecked -> + if (isChecked) { + downloadsViewModel.addSelected(itemId) + } else downloadsViewModel.removeSelected(itemId) } ) @@ -126,7 +205,6 @@ class DownloadFragment : Fragment() { setLinearListLayout( isHorizontal = false, nextRight = FOCUS_SELF, - nextUp = FOCUS_SELF, nextDown = FOCUS_SELF, ) } @@ -147,35 +225,68 @@ class DownloadFragment : Fragment() { handleScroll(scrollY - oldScrollY) } } - downloadsViewModel.updateList(requireContext()) + + context?.let { downloadsViewModel.updateHeaderList(it) } fixPaddingStatusbar(binding?.downloadRoot) } private fun handleItemClick(click: DownloadHeaderClickEvent) { when (click.action) { DOWNLOAD_ACTION_GO_TO_CHILD -> { - if (!click.data.type.isMovieType()) { - val folder = DataStore.getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString()) + if (click.data.type.isEpisodeBased()) { + val folder = + getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString()) activity?.navigate( R.id.action_navigation_downloads_to_navigation_download_child, DownloadChildFragment.newInstance(click.data.name, folder) ) } } + DOWNLOAD_ACTION_LOAD_RESULT -> { - (activity as AppCompatActivity?)?.loadResult(click.data.url, click.data.apiName) + activity?.loadResult(click.data.url, click.data.apiName) } } } - private fun setUpDownloadDeleteListener() { - downloadDeleteEventListener = { id -> - val list = (binding?.downloadList?.adapter as? DownloadAdapter)?.currentList - if (list?.any { it.data.id == id } == true) { - context?.let { downloadsViewModel.updateList(it) } + private fun handleSelectedChange(selected: MutableSet) { + if (selected.isNotEmpty()) { + binding?.downloadDeleteAppbar?.isVisible = true + binding?.downloadStorageAppbar?.isVisible = false + activity?.attachBackPressedCallback { + downloadsViewModel.setIsMultiDeleteState(false) + } + + binding?.btnDelete?.setOnClickListener { + context?.let { ctx -> + downloadsViewModel.handleMultiDelete(ctx) + } + } + + binding?.btnCancel?.setOnClickListener { + downloadsViewModel.setIsMultiDeleteState(false) + } + + binding?.btnToggleAll?.setOnClickListener { + val allSelected = downloadsViewModel.isAllSelected() + val adapter = binding?.downloadList?.adapter as? DownloadAdapter + if (allSelected) { + adapter?.notifySelectionStates() + downloadsViewModel.clearSelectedItems() + } else { + adapter?.notifyAllSelected() + downloadsViewModel.selectAllItems() + } } + + downloadsViewModel.setIsMultiDeleteState(true) } - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + } + + private fun updateDeleteButton(count: Int, selectedBytes: Long) { + val formattedSize = formatShortFileSize(context, selectedBytes) + binding?.btnDelete?.text = + getString(R.string.delete_format).format(count, formattedSize) } private fun updateStorageInfo( @@ -185,7 +296,10 @@ class DownloadFragment : Fragment() { textView: TextView?, view: View? ) { - textView?.text = getString(R.string.storage_size_format).format(getString(stringRes), formatShortFileSize(context, bytes)) + textView?.text = getString(R.string.storage_size_format).format( + getString(stringRes), + formatShortFileSize(context, bytes) + ) view?.setLayoutWidth(bytes) } @@ -218,7 +332,9 @@ class DownloadFragment : Fragment() { if (!preventAutoSwitching) activateSwitchOnHls(text?.toString(), binding) } - (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt(0)?.text?.toString()?.let { copy -> + (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt( + 0 + )?.text?.toString()?.let { copy -> val fixedText = copy.trim() binding.streamUrl.setText(fixedText) activateSwitchOnHls(fixedText, binding) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt index 83d96592809..137f1355e25 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt @@ -1,122 +1,439 @@ package com.lagradost.cloudstream3.ui.download import android.content.Context +import android.content.DialogInterface import android.os.Environment import android.os.StatFs +import androidx.appcompat.app.AlertDialog import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lagradost.cloudstream3.isMovieType +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.isEpisodeBased import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStore.getFolderName import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import com.lagradost.cloudstream3.utils.VideoDownloadManager.deleteFilesAndUpdateSettings import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadFileInfoAndUpdateSettings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class DownloadViewModel : ViewModel() { - private val _headerCards = - MutableLiveData>().apply { listOf() } - val headerCards: LiveData> = _headerCards - private val _usedBytes = MutableLiveData() - private val _availableBytes = MutableLiveData() - private val _downloadBytes = MutableLiveData() + private val _headerCards = MutableLiveData>() + val headerCards: LiveData> = _headerCards + + private val _childCards = MutableLiveData>() + val childCards: LiveData> = _childCards + private val _usedBytes = MutableLiveData() val usedBytes: LiveData = _usedBytes + + private val _availableBytes = MutableLiveData() val availableBytes: LiveData = _availableBytes + + private val _downloadBytes = MutableLiveData() val downloadBytes: LiveData = _downloadBytes - private var previousVisual: List? = null + private val _selectedBytes = MutableLiveData(0) + val selectedBytes: LiveData = _selectedBytes + + private val _isMultiDeleteState = MutableLiveData(false) + val isMultiDeleteState: LiveData = _isMultiDeleteState + + private val _selectedItemIds = MutableLiveData>(mutableSetOf()) + val selectedItemIds: LiveData> = _selectedItemIds + + private var previousVisual: List? = null + + fun setIsMultiDeleteState(value: Boolean) { + _isMultiDeleteState.postValue(value) + } + + fun addSelected(itemId: Int) { + updateSelectedItems { it.add(itemId) } + } + + fun removeSelected(itemId: Int) { + updateSelectedItems { it.remove(itemId) } + } + + fun selectAllItems() { + val items = headerCards.value.orEmpty() + childCards.value.orEmpty() + updateSelectedItems { it.addAll(items.map { item -> item.data.id }) } + } + + fun clearSelectedItems() { + // We need this to be done immediately + // so we can't use postValue + _selectedItemIds.value = mutableSetOf() + updateSelectedItems { it.clear() } + } + + fun isAllSelected(): Boolean { + val currentSelected = selectedItemIds.value ?: return false + val items = headerCards.value.orEmpty() + childCards.value.orEmpty() + return items.count() == currentSelected.count() && items.all { it.data.id in currentSelected } + } + + private fun updateSelectedItems(action: (MutableSet) -> Unit) { + val currentSelected = selectedItemIds.value ?: mutableSetOf() + action(currentSelected) + _selectedItemIds.postValue(currentSelected) + updateSelectedBytes() + updateSelectedCards() + } + + private fun updateSelectedBytes() = viewModelScope.launchSafe { + val selectedItemsList = getSelectedItemsData() ?: return@launchSafe + val totalSelectedBytes = selectedItemsList.sumOf { it.totalBytes } + _selectedBytes.postValue(totalSelectedBytes) + } + + private fun updateSelectedCards() = viewModelScope.launchSafe { + val currentSelected = selectedItemIds.value ?: return@launchSafe + + headerCards.value?.let { headers -> + headers.forEach { header -> + header.isSelected = header.data.id in currentSelected + } + _headerCards.postValue(headers) + } - fun updateList(context: Context) = viewModelScope.launchSafe { - val children = withContext(Dispatchers.IO) { - context.getKeys(DOWNLOAD_EPISODE_CACHE) + childCards.value?.let { children -> + children.forEach { child -> + child.isSelected = child.data.id in currentSelected + } + _childCards.postValue(children) + } + } + + fun updateHeaderList(context: Context) = viewModelScope.launchSafe { + val visual = withContext(Dispatchers.IO) { + val children = context.getKeys(DOWNLOAD_EPISODE_CACHE) .mapNotNull { context.getKey(it) } .distinctBy { it.id } // Remove duplicates + + val (totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) = + calculateDownloadStats(context, children) + + val cached = context.getKeys(DOWNLOAD_HEADER_CACHE) + .mapNotNull { context.getKey(it) } + + createVisualDownloadList( + context, cached, totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads + ) + } + + if (visual != previousVisual) { + previousVisual = visual + updateStorageStats(visual) + _headerCards.postValue(visual) } + } + private fun calculateDownloadStats( + context: Context, + children: List + ): Triple, Map, Map> { // parentId : bytes - val totalBytesUsedByChild = HashMap() + val totalBytesUsedByChild = mutableMapOf() // parentId : bytes - val currentBytesUsedByChild = HashMap() + val currentBytesUsedByChild = mutableMapOf() // parentId : downloadsCount - val totalDownloads = HashMap() + val totalDownloads = mutableMapOf() - // Gets all children downloads - withContext(Dispatchers.IO) { - children.forEach { c -> - val childFile = getDownloadFileInfoAndUpdateSettings(context, c.id) ?: return@forEach + children.forEach { child -> + val childFile = getDownloadFileInfoAndUpdateSettings(context, child.id) ?: return@forEach + if (childFile.fileLength <= 1) return@forEach - if (childFile.fileLength <= 1) return@forEach - val len = childFile.totalBytes - val flen = childFile.fileLength + val len = childFile.totalBytes + val flen = childFile.fileLength - totalBytesUsedByChild[c.parentId] = totalBytesUsedByChild[c.parentId]?.plus(len) ?: len - currentBytesUsedByChild[c.parentId] = currentBytesUsedByChild[c.parentId]?.plus(flen) ?: flen - totalDownloads[c.parentId] = totalDownloads[c.parentId]?.plus(1) ?: 1 - } + totalBytesUsedByChild.merge(child.parentId, len, Long::plus) + currentBytesUsedByChild.merge(child.parentId, flen, Long::plus) + totalDownloads.merge(child.parentId, 1, Int::plus) } + return Triple(totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) + } + + private fun createVisualDownloadList( + context: Context, + cached: List, + totalBytesUsedByChild: Map, + currentBytesUsedByChild: Map, + totalDownloads: Map + ): List { + return cached.mapNotNull { + val downloads = totalDownloads[it.id] ?: 0 + val bytes = totalBytesUsedByChild[it.id] ?: 0 + val currentBytes = currentBytesUsedByChild[it.id] ?: 0 + if (bytes <= 0 || downloads <= 0) return@mapNotNull null + + val isSelected = selectedItemIds.value?.contains(it.id) ?: false + val movieEpisode = if (it.type.isEpisodeBased()) null else context.getKey( + DOWNLOAD_EPISODE_CACHE, + getFolderName(it.id.toString(), it.id.toString()) + ) - val cached = withContext(Dispatchers.IO) { // Won't fetch useless keys - totalDownloads.entries.filter { it.value > 0 }.mapNotNull { - context.getKey( - DOWNLOAD_HEADER_CACHE, - it.key.toString() + VisualDownloadCached.Header( + currentBytes = currentBytes, + totalBytes = bytes, + data = it, + child = movieEpisode, + currentOngoingDownloads = 0, + totalDownloads = downloads, + isSelected = isSelected, + ) + // Prevent order being almost completely random, + // making things difficult to find. + }.sortedWith(compareBy { + // Sort by isEpisodeBased() ascending. We put those that + // are episode based at the bottom for UI purposes and to + // make it easier to find by grouping them together. + it.data.type.isEpisodeBased() + }.thenBy { + // Then we sort alphabetically by name (case-insensitive). + // Again, we do this to make things easier to find. + it.data.name.lowercase() + }) + } + + fun updateChildList(context: Context, folder: String) = viewModelScope.launchSafe { + val visual = withContext(Dispatchers.IO) { + context.getKeys(folder).mapNotNull { key -> + context.getKey(key) + }.mapNotNull { + val isSelected = selectedItemIds.value?.contains(it.id) ?: false + val info = getDownloadFileInfoAndUpdateSettings(context, it.id) ?: return@mapNotNull null + VisualDownloadCached.Child( + currentBytes = info.fileLength, + totalBytes = info.totalBytes, + isSelected = isSelected, + data = it, ) } + }.sortedWith(compareBy( + // Sort by season first, and then by episode number, + // to ensure sorting is consistent. + { it.data.season ?: 0 }, + { it.data.episode } + )) + + if (previousVisual != visual) { + previousVisual = visual + _childCards.postValue(visual) } + } - val visual = withContext(Dispatchers.IO) { - cached.mapNotNull { - val downloads = totalDownloads[it.id] ?: 0 - val bytes = totalBytesUsedByChild[it.id] ?: 0 - val currentBytes = currentBytesUsedByChild[it.id] ?: 0 - if (bytes <= 0 || downloads <= 0) return@mapNotNull null - val movieEpisode = - if (!it.type.isMovieType()) null - else context.getKey( - DOWNLOAD_EPISODE_CACHE, - getFolderName(it.id.toString(), it.id.toString()) + private fun removeItems(idsToRemove: Set) = viewModelScope.launchSafe { + val updatedHeaders = headerCards.value.orEmpty().filter { it.data.id !in idsToRemove } + val updatedChildren = childCards.value.orEmpty().filter { it.data.id !in idsToRemove } + _headerCards.postValue(updatedHeaders) + _childCards.postValue(updatedChildren) + } + + private fun updateStorageStats(visual: List) { + try { + val stat = StatFs(Environment.getExternalStorageDirectory().path) + val localBytesAvailable = stat.availableBytes + val localTotalBytes = stat.blockSizeLong * stat.blockCountLong + val localDownloadedBytes = visual.sumOf { it.totalBytes } + val localUsedBytes = localTotalBytes - localBytesAvailable + _usedBytes.postValue(localUsedBytes) + _availableBytes.postValue(localBytesAvailable) + _downloadBytes.postValue(localDownloadedBytes) + } catch (t: Throwable) { + _downloadBytes.postValue(0) + logError(t) + } + } + + fun handleMultiDelete(context: Context) = viewModelScope.launchSafe { + val selectedItemsList = getSelectedItemsData().orEmpty() + val deleteData = processSelectedItems(context, selectedItemsList) + val message = buildDeleteMessage(context, deleteData) + showDeleteConfirmationDialog(context, message, deleteData.ids, deleteData.parentIds) + } + + fun handleSingleDelete( + context: Context, + itemId: Int + ) = viewModelScope.launchSafe { + val itemData = getItemDataFromId(itemId) + val deleteData = processSelectedItems(context, itemData) + val message = buildDeleteMessage(context, deleteData) + showDeleteConfirmationDialog(context, message, deleteData.ids, deleteData.parentIds) + } + + private fun processSelectedItems( + context: Context, + selectedItemsList: List + ): DeleteData { + val names = mutableListOf() + val seriesNames = mutableListOf() + + val ids = mutableSetOf() + val parentIds = mutableSetOf() + + var parentName: String? = null + + selectedItemsList.forEach { item -> + when (item) { + is VisualDownloadCached.Header -> { + if (item.data.type.isEpisodeBased()) { + val episodes = context.getKeys(DOWNLOAD_EPISODE_CACHE) + .mapNotNull { + context.getKey( + it + ) + } + .filter { it.parentId == item.data.id } + .map { it.id } + ids.addAll(episodes) + parentIds.add(item.data.id) + + val episodeInfo = "${item.data.name} (${item.totalDownloads} ${ + context.resources.getQuantityString( + R.plurals.episodes, + item.totalDownloads + ).lowercase() + })" + seriesNames.add(episodeInfo) + } else { + ids.add(item.data.id) + names.add(item.data.name) + } + } + + is VisualDownloadCached.Child -> { + ids.add(item.data.id) + val parent = context.getKey( + DOWNLOAD_HEADER_CACHE, + item.data.parentId.toString() ) - VisualDownloadHeaderCached( - currentBytes = currentBytes, - totalBytes = bytes, - data = it, - child = movieEpisode, - currentOngoingDownloads = 0, - totalDownloads = downloads, + parentName = parent?.name + names.add( + context.getNameFull( + item.data.name, + item.data.episode, + item.data.season + ) + ) + } + } + } + + return DeleteData(ids, parentIds, seriesNames, names, parentName) + } + + private fun buildDeleteMessage( + context: Context, + data: DeleteData + ): String { + val formattedNames = data.names.sortedBy { it.lowercase() } + .joinToString(separator = "\n") { "• $it" } + val formattedSeriesNames = data.seriesNames.sortedBy { it.lowercase() } + .joinToString(separator = "\n") { "• $it" } + + return when { + data.ids.count() == 1 -> { + context.getString(R.string.delete_message).format( + data.names.firstOrNull() ) - }.sortedBy { - (it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0) - } // Episode sorting by episode, lowest to highest + } + + data.seriesNames.isNotEmpty() && data.names.isEmpty() -> { + context.getString(R.string.delete_message_series_only).format(formattedSeriesNames) + } + + data.parentName != null && data.names.isNotEmpty() -> { + context.getString(R.string.delete_message_series_episodes) + .format(data.parentName, formattedNames) + } + + data.seriesNames.isNotEmpty() -> { + val seriesSection = context.getString(R.string.delete_message_series_section) + .format(formattedSeriesNames) + context.getString(R.string.delete_message_multiple) + .format(formattedNames) + "\n\n" + seriesSection + } + + else -> context.getString(R.string.delete_message_multiple).format(formattedNames) } + } - // Only update list if different from the previous one to prevent duplicate initialization - if (visual != previousVisual) { - previousVisual = visual + private fun showDeleteConfirmationDialog( + context: Context, + message: String, + ids: Set, + parentIds: Set + ) { + val builder = AlertDialog.Builder(context) + val dialogClickListener = + DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + viewModelScope.launchSafe { + setIsMultiDeleteState(false) + deleteFilesAndUpdateSettings(context, ids, this) { successfulIds -> + // We always remove parent because if we are deleting from here + // and we have it as non-empty, it was triggered on + // parent header card + removeItems(successfulIds + parentIds) + } + } + } - try { - val stat = StatFs(Environment.getExternalStorageDirectory().path) - val localBytesAvailable = stat.availableBytes - val localTotalBytes = stat.blockSizeLong * stat.blockCountLong - val localDownloadedBytes = visual.sumOf { it.totalBytes } - - _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) - _availableBytes.postValue(localBytesAvailable) - _downloadBytes.postValue(localDownloadedBytes) - } catch (t: Throwable) { - _downloadBytes.postValue(0) - logError(t) + DialogInterface.BUTTON_NEGATIVE -> { + // Do nothing on cancel + } + } } - _headerCards.postValue(visual) + try { + val title = if (ids.count() == 1) { + R.string.delete_file + } else R.string.delete_files + builder.setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.delete, dialogClickListener) + .setNegativeButton(R.string.cancel, dialogClickListener) + .show().setDefaultFocus() + } catch (e: Exception) { + logError(e) + } + } + + private fun getSelectedItemsData(): List? { + val headers = headerCards.value.orEmpty() + val children = childCards.value.orEmpty() + + return selectedItemIds.value?.mapNotNull { id -> + headers.find { it.data.id == id } ?: children.find { it.data.id == id } } } + + private fun getItemDataFromId(itemId: Int): List { + val headers = headerCards.value.orEmpty() + val children = childCards.value.orEmpty() + + return (headers + children).filter { it.data.id == itemId } + } + + private data class DeleteData( + val ids: Set, + val parentIds: Set, + val seriesNames: List, + val names: List, + val parentName: String? + ) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index 45132131977..908e3a80abc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -93,7 +93,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : abstract fun setStatus(status: VideoDownloadManager.DownloadType?) - fun getStatus(id:Int, downloadedBytes: Long, totalBytes: Long): DownloadStatusTell { + fun getStatus(id: Int, downloadedBytes: Long, totalBytes: Long): DownloadStatusTell { // some extra padding for just in case return VideoDownloadManager.downloadStatus[id] ?: if (downloadedBytes > 1024L && downloadedBytes + 1024L >= totalBytes) { @@ -101,7 +101,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } else DownloadStatusTell.IsPaused } - fun applyMetaData(id:Int, downloadedBytes: Long, totalBytes: Long) { + fun applyMetaData(id: Int, downloadedBytes: Long, totalBytes: Long) { val status = getStatus(id, downloadedBytes, totalBytes) currentMetaData.apply { @@ -140,7 +140,8 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } else { if (doSetProgress) { progressText?.apply { - val currentFormattedSizeString = formatShortFileSize(context, downloadedBytes) + val currentFormattedSizeString = + formatShortFileSize(context, downloadedBytes) val totalFormattedSizeString = formatShortFileSize(context, totalBytes) text = // if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else 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 abc159d09ab..29c2daa2cf4 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 @@ -58,7 +58,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } private var progressBarBackground: View - private var statusView: ImageView + var statusView: ImageView open fun onInflate() {} @@ -248,7 +248,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } */ @MainThread - private fun setStatusInternal(status : DownloadStatusTell?) { + private fun setStatusInternal(status: DownloadStatusTell?) { val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { val animation = AnimationUtils.loadAnimation(context, waitingAnimation) @@ -286,7 +286,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : if (Looper.myLooper() == Looper.getMainLooper()) { try { setStatusInternal(status) - } catch (t : Throwable) { + } catch (t: Throwable) { logError(t) // Just in case setStatusInternal throws because thread progressBarBackground.post { setStatusInternal(status) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index a8a3106aaa0..a0668abc48f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -4,7 +4,9 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.VideoDownloadManager +import com.lagradost.cloudstream3.utils.SubtitleUtils.cleanDisplayName +import com.lagradost.cloudstream3.utils.SubtitleUtils.isMatchingSubtitle +import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFolder import kotlin.math.max import kotlin.math.min @@ -49,10 +51,6 @@ class DownloadFileGenerator( return null } - private fun cleanDisplayName(name: String): String { - return name.substringBeforeLast('.').trim() - } - override suspend fun generateLinks( clearCache: Boolean, type: LoadType, @@ -69,28 +67,9 @@ class DownloadFileGenerator( val cleanDisplay = cleanDisplayName(display) - VideoDownloadManager.getFolder(ctx, relative, meta.basePath) - ?.forEach { (name, uri) -> - // only these files are allowed, so no videos as subtitles - if (listOf( - ".vtt", - ".srt", - ".txt", - ".ass", - ".ttml", - ".sbv", - ".dfxp" - ).none { name.contains(it, true) } - ) return@forEach - - // cant have the exact same file as a subtitle - if (name.equals(display, true)) return@forEach - + getFolder(ctx, relative, meta.basePath)?.forEach { (name, uri) -> + if (isMatchingSubtitle(name, display, cleanDisplay)) { val cleanName = cleanDisplayName(name) - - // we only want files with the approx same name - if (!cleanName.startsWith(cleanDisplay, true)) return@forEach - val realName = cleanName.removePrefix(cleanDisplay) subtitleCallback( @@ -104,6 +83,7 @@ class DownloadFileGenerator( ) ) } + } return true } 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 4279b54218b..c38160c2df5 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 @@ -4,13 +4,13 @@ import android.content.Intent import android.os.Bundle import android.util.Log import android.view.KeyEvent -import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback class DownloadedPlayerActivity : AppCompatActivity() { private val dTAG = "DownloadedPlayerAct" @@ -70,14 +70,7 @@ class DownloadedPlayerActivity : AppCompatActivity() { return } - onBackPressedDispatcher.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - finish() - } - } - ) + attachBackPressedCallback { finish() } } override fun onResume() { 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 135dc5306d2..2ab60c2f884 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 @@ -7,7 +7,6 @@ import android.os.Bundle import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import androidx.activity.OnBackPressedCallback import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.CommonActivity.screenHeight @@ -17,6 +16,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.PlayerEventSource import com.lagradost.cloudstream3.ui.player.SubtitleData +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback open class ResultTrailerPlayer : ResultFragmentPhone() { @@ -156,7 +157,9 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { uiReset() if (isFullScreenPlayer) { - attachBackPressedCallback() + activity?.attachBackPressedCallback { + updateFullscreen(false) + } } else detachBackPressedCallback() } @@ -175,27 +178,4 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { fixPlayerSize() } } - - private var backPressedCallback: OnBackPressedCallback? = null - - private fun attachBackPressedCallback() { - if (backPressedCallback == null) { - backPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - updateFullscreen(false) - } - } - } - - backPressedCallback?.isEnabled = true - - activity?.onBackPressedDispatcher?.addCallback( - activity ?: return, - backPressedCallback ?: return - ) - } - - private fun detachBackPressedCallback() { - backPressedCallback?.isEnabled = false - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index b13de062a23..8d65acf7eae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -677,9 +677,15 @@ object AppContextUtils { } fun Context.isNetworkAvailable(): Boolean { - val manager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val activeNetworkInfo = manager.activeNetworkInfo - return activeNetworkInfo != null && activeNetworkInfo.isConnected || manager.allNetworkInfo?.any { it.isConnected } ?: false + val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val network = connectivityManager.activeNetwork ?: return false + val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } else { + @Suppress("DEPRECATION") + connectivityManager.activeNetworkInfo?.isConnected == true + } } fun splitQuery(url: URL): Map { @@ -1018,4 +1024,4 @@ object AppContextUtils { } return currentAudioFocusRequest } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt new file mode 100644 index 00000000000..1326ab27443 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt @@ -0,0 +1,30 @@ +package com.lagradost.cloudstream3.utils + +import androidx.activity.ComponentActivity +import androidx.activity.OnBackPressedCallback + +object BackPressedCallbackHelper { + private var backPressedCallback: OnBackPressedCallback? = null + + fun ComponentActivity.attachBackPressedCallback(callback: () -> Unit) { + if (backPressedCallback == null) { + backPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + callback.invoke() + } + } + } + + backPressedCallback?.isEnabled = true + + onBackPressedDispatcher.addCallback( + this@attachBackPressedCallback, + backPressedCallback ?: return + ) + } + + fun detachBackPressedCallback() { + backPressedCallback?.isEnabled = false + backPressedCallback = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt new file mode 100644 index 00000000000..e6a77795e32 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt @@ -0,0 +1,84 @@ +package com.lagradost.cloudstream3.utils + +import android.app.Activity +import android.view.View +import androidx.annotation.MainThread +import androidx.annotation.StringRes +import com.google.android.material.snackbar.Snackbar +import com.lagradost.api.Log +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.result.UiText +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute + +object SnackbarHelper { + + private const val TAG = "COMPACT" + private var currentSnackbar: Snackbar? = null + + @MainThread + fun showSnackbar( + act: Activity?, + message: UiText, + duration: Int = Snackbar.LENGTH_SHORT, + actionText: UiText? = null, + actionCallback: (() -> Unit)? = null + ) { + if (act == null) return + showSnackbar(act, message.asString(act), duration, + actionText?.asString(act), actionCallback) + } + + @MainThread + fun showSnackbar( + act: Activity?, + @StringRes message: Int, + duration: Int = Snackbar.LENGTH_SHORT, + @StringRes actionText: Int? = null, + actionCallback: (() -> Unit)? = null + ) { + if (act == null) return + showSnackbar(act, act.getString(message), duration, + actionText?.let { act.getString(it) }, actionCallback) + } + + @MainThread + fun showSnackbar( + act: Activity?, + message: String?, + duration: Int = Snackbar.LENGTH_SHORT, + actionText: String? = null, + actionCallback: (() -> Unit)? = null + ) { + if (act == null || message == null) { + Log.w(TAG, "Invalid showSnackbar: act = $act, message = $message") + return + } + Log.i(TAG, "showSnackbar: $message") + + try { + currentSnackbar?.dismiss() + } catch (e: Exception) { + logError(e) + } + + try { + val parentView = act.findViewById(android.R.id.content) + val snackbar = Snackbar.make(parentView, message, duration) + + actionCallback?.let { + snackbar.setAction(actionText) { actionCallback.invoke() } + } + + snackbar.show() + currentSnackbar = snackbar + + snackbar.setBackgroundTint(act.colorFromAttribute(R.attr.primaryBlackBackground)) + snackbar.setTextColor(act.colorFromAttribute(R.attr.textColor)) + snackbar.setActionTextColor(act.colorFromAttribute(R.attr.colorPrimary)) + + } catch (e: Exception) { + logError(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt new file mode 100644 index 00000000000..93a53395408 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt @@ -0,0 +1,56 @@ +package com.lagradost.cloudstream3.utils + +import android.content.Context +import com.lagradost.api.Log +import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFolder +import com.lagradost.safefile.SafeFile + +object SubtitleUtils { + + // Only these files are allowed, so no videos as subtitles + private val allowedExtensions = listOf( + ".vtt", ".srt", ".txt", ".ass", + ".ttml", ".sbv", ".dfxp" + ) + + fun deleteMatchingSubtitles(context: Context, info: VideoDownloadManager.DownloadedFileInfo) { + val relative = info.relativePath + val display = info.displayName + val cleanDisplay = cleanDisplayName(display) + + getFolder(context, relative, info.basePath)?.forEach { (name, uri) -> + if (isMatchingSubtitle(name, display, cleanDisplay)) { + val subtitleFile = SafeFile.fromUri(context, uri) + if (subtitleFile == null || !subtitleFile.delete()) { + Log.e("SubtitleDeletion", "Failed to delete subtitle file: ${subtitleFile?.name()}") + } + } + } + } + + /** + * @param name the file name of the subtitle + * @param display the file name of the video + * @param cleanDisplay the cleanDisplayName of the video file name + */ + fun isMatchingSubtitle( + name: String, + display: String, + cleanDisplay: String + ): Boolean { + // Check if the file has a valid subtitle extension + val hasValidExtension = allowedExtensions.any { name.contains(it, ignoreCase = true) } + + // We can't have the exact same file as a subtitle + val isNotDisplayName = !name.equals(display, ignoreCase = true) + + // Check if the file name starts with a cleaned version of the display name + val startsWithCleanDisplay = cleanDisplayName(name).startsWith(cleanDisplay, ignoreCase = true) + + return hasValidExtension && isNotDisplayName && startsWithCleanDisplay + } + + fun cleanDisplayName(name: String): String { + return name.substringBeforeLast('.').trim() + } +} \ No newline at end of file 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 a3f6d78918c..2190e03f647 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -20,6 +20,7 @@ import androidx.work.WorkManager import com.bumptech.glide.Glide import com.bumptech.glide.load.model.GlideUrl import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.api.Log import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -29,12 +30,14 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey +import com.lagradost.cloudstream3.utils.SubtitleUtils.deleteMatchingSubtitles import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.safefile.MediaFileContentType import com.lagradost.safefile.SafeFile @@ -42,6 +45,8 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -1733,7 +1738,37 @@ object VideoDownloadManager { } } - fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { + fun deleteFilesAndUpdateSettings( + context: Context, + ids: Set, + scope: CoroutineScope, + onComplete: (Set) -> Unit = {} + ) { + scope.launchSafe(Dispatchers.IO) { + val deleteJobs = ids.map { id -> + async { + id to deleteFileAndUpdateSettings(context, id) + } + } + val results = deleteJobs.awaitAll() + + val (successfulResults, failedResults) = results.partition { it.second } + val successfulIds = successfulResults.map { it.first }.toSet() + + if (failedResults.isNotEmpty()) { + failedResults.forEach { (id, _) -> + // TODO show a toast if some failed? + Log.e("FileDeletion", "Failed to delete file with ID: $id") + } + } else { + Log.i("FileDeletion", "All files deleted successfully") + } + + onComplete.invoke(successfulIds) + } + } + + private fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { val success = deleteFile(context, id) if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) return success @@ -1759,11 +1794,17 @@ object VideoDownloadManager { private fun deleteFile(context: Context, id: Int): Boolean { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false + val file = info.toFile(context) + downloadEvent.invoke(id to DownloadActionType.Stop) downloadProgressEvent.invoke(Triple(id, 0, 0)) downloadStatusEvent.invoke(id to DownloadType.IsStopped) downloadDeleteEvent.invoke(id) - return info.toFile(context)?.delete() ?: false + + val isFileDeleted = file?.delete() == true || file?.exists() == false + if (isFileDeleted) deleteMatchingSubtitles(context, info) + + return isFileDeleted } fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? { diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml index 4974a0276b1..e53e63d312f 100644 --- a/app/src/main/res/layout/download_child_episode.xml +++ b/app/src/main/res/layout/download_child_episode.xml @@ -2,10 +2,8 @@ @@ -78,7 +73,6 @@ tools:text="128MB / 237MB" /> - + + \ 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 a0b64ce3c23..957869d44c2 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -77,5 +77,16 @@ 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_child_downloads.xml b/app/src/main/res/layout/fragment_child_downloads.xml index 9afaea0bac3..64ed1d7006d 100644 --- a/app/src/main/res/layout/fragment_child_downloads.xml +++ b/app/src/main/res/layout/fragment_child_downloads.xml @@ -7,13 +7,69 @@ android:layout_height="match_parent" android:background="?attr/primaryGrayBackground" android:orientation="vertical" - tools:context=".ui.download.DownloadFragment"> + tools:context=".ui.download.DownloadChildFragment"> + + + + +