diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index c525ac92..e5d9e072 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -19,4 +19,4 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index dd1a1898..00000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/debug/res/drawable-hdpi/pause_to_play.xml b/app/src/debug/res/drawable-hdpi/pause_to_play.xml
new file mode 100644
index 00000000..41b368aa
--- /dev/null
+++ b/app/src/debug/res/drawable-hdpi/pause_to_play.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/debug/res/drawable-hdpi/play_to_pause.xml b/app/src/debug/res/drawable-hdpi/play_to_pause.xml
new file mode 100644
index 00000000..dd4d1e8b
--- /dev/null
+++ b/app/src/debug/res/drawable-hdpi/play_to_pause.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 14f6475f..95b4cb1c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -24,10 +24,27 @@
android:usesCleartextTraffic="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
- android:theme="@style/AppTheme" android:fullBackupContent="@xml/backup_descriptor" tools:targetApi="m">
+ android:theme="@style/AppTheme"
+ android:fullBackupContent="@xml/backup_descriptor"
+ tools:targetApi="m">
+
+
+
+
+
+
+
+
+
+
()
+ val onColorSelectedEvent = Event>()
+ val onDialogDismissedEvent = Event()
+
+ var playerEventListener: ((PlayerEventType) -> Unit)? = null
+ var keyEventListener: ((KeyEvent?) -> Boolean)? = null
+
+
+ var currentToast: Toast? = null
+
+ fun showToast(act: Activity?, @StringRes message: Int, duration: Int) {
+ if (act == null) return
+ showToast(act, act.getString(message), duration)
+ }
+
+ /** duration is Toast.LENGTH_SHORT if null*/
+ fun showToast(act: Activity?, message: String?, duration: Int? = null) {
+ if (act == null || message == null) return
+ try {
+ currentToast?.cancel()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ try {
+ val inflater =
+ act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
+
+ val layout: View = inflater.inflate(
+ R.layout.toast,
+ act.findViewById(R.id.toast_layout_root) as ViewGroup?
+ )
+
+ val text = layout.findViewById(R.id.text) as TextView
+ text.text = message.trim()
+
+ val toast = Toast(act)
+ toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
+ toast.duration = duration ?: Toast.LENGTH_SHORT
+ toast.view = layout
+ toast.show()
+ currentToast = toast
+ } catch (e: Exception) {
+
+ }
+ }
+
+ fun setLocale(context: Context?, languageCode: String?) {
+ if (context == null || languageCode == null) return
+ val locale = Locale(languageCode)
+ val resources: Resources = context.resources
+ val config = resources.configuration
+ Locale.setDefault(locale)
+ config.setLocale(locale)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+ context.createConfigurationContext(config)
+ resources.updateConfiguration(config, resources.displayMetrics)
+ }
+
+ fun Context.updateLocale() {
+ val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
+ val localeCode = settingsManager.getString(getString(R.string.locale_key), null)
+ setLocale(this, localeCode)
+ }
+
+ fun init(act: Activity?) {
+ if (act == null) return
+ //https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
+ //https://developer.android.com/guide/topics/ui/picture-in-picture
+ canShowPipMode =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && // OS SUPPORT
+ act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
+ act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
+
+ act.updateLocale()
+ }
+
+ private fun Activity.enterPIPMode() {
+ if (!shouldShowPIPMode(canEnterPipMode) || !canShowPipMode) return
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ try {
+ enterPictureInPictureMode(PictureInPictureParams.Builder().build())
+ } catch (e: Exception) {
+ enterPictureInPictureMode()
+ }
+ } else {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ enterPictureInPictureMode()
+ }
+ }
+ } catch (e: Exception) {
+ logError(e)
+ }
+ }
+
+ fun onUserLeaveHint(act: Activity?) {
+ if (canEnterPipMode && canShowPipMode) {
+ act?.enterPIPMode()
+ }
+ }
+
+ 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")) {
+ "Black" -> R.style.AppTheme
+ "Light" -> R.style.LightMode
+ "Amoled" -> R.style.AmoledMode
+ "AmoledLight" -> R.style.AmoledModeLight
+ else -> R.style.AppTheme
+ }
+
+ val currentOverlayTheme =
+ when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
+ "Normal" -> R.style.OverlayPrimaryColorNormal
+ "Blue" -> R.style.OverlayPrimaryColorBlue
+ "Purple" -> R.style.OverlayPrimaryColorPurple
+ "Green" -> R.style.OverlayPrimaryColorGreen
+ "GreenApple" -> R.style.OverlayPrimaryColorGreenApple
+ "Red" -> R.style.OverlayPrimaryColorRed
+ "Banana" -> R.style.OverlayPrimaryColorBanana
+ "Party" -> R.style.OverlayPrimaryColorParty
+ "Pink" -> R.style.OverlayPrimaryColorPink
+ else -> R.style.OverlayPrimaryColorNormal
+ }
+ act.theme.applyStyle(currentTheme, true)
+ act.theme.applyStyle(currentOverlayTheme, true)
+
+ act.theme.applyStyle(
+ R.style.LoadedStyle,
+ true
+ ) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
+ }
+
+ private fun getNextFocus(
+ act: Activity?,
+ view: View?,
+ direction: FocusDirection,
+ depth: Int = 0
+ ): Int? {
+ if (view == null || depth >= 10 || act == null) {
+ return null
+ }
+
+ val nextId = when (direction) {
+ FocusDirection.Left -> {
+ view.nextFocusLeftId
+ }
+ FocusDirection.Up -> {
+ view.nextFocusUpId
+ }
+ FocusDirection.Right -> {
+ view.nextFocusRightId
+ }
+ FocusDirection.Down -> {
+ view.nextFocusDownId
+ }
+ }
+
+ return if (nextId != -1) {
+ val next = act.findViewById(nextId)
+ //println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" )
+
+ if (next?.isShown == false) {
+ getNextFocus(act, next, direction, depth + 1)
+ } else {
+ if (depth == 0) {
+ null
+ } else {
+ nextId
+ }
+ }
+ } else {
+ null
+ }
+ }
+
+ enum class FocusDirection {
+ Left,
+ Right,
+ Up,
+ Down,
+ }
+
+ 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) {
+ KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
+ PlayerEventType.SeekForward
+ }
+ KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
+ PlayerEventType.SeekBack
+ }
+ KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1 -> {
+ PlayerEventType.NextEpisode
+ }
+ KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1 -> {
+ PlayerEventType.PrevEpisode
+ }
+ KeyEvent.KEYCODE_MEDIA_PAUSE -> {
+ PlayerEventType.Pause
+ }
+ KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
+ PlayerEventType.Play
+ }
+ KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7 -> {
+ PlayerEventType.Lock
+ }
+ KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
+ PlayerEventType.ToggleHide
+ }
+ KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
+ PlayerEventType.ToggleMute
+ }
+ KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9 -> {
+ PlayerEventType.ShowMirrors
+ }
+ KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3 -> {
+ PlayerEventType.ShowSpeed
+ }
+ KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0 -> {
+ PlayerEventType.Resize
+ }
+ KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
+ PlayerEventType.PlayPauseToggle
+ }
+ else -> null
+ }?.let { playerEvent ->
+ playerEventListener?.invoke(playerEvent)
+ }
+
+ //when (keyCode) {
+ // KeyEvent.KEYCODE_DPAD_CENTER -> {
+ // println("DPAD PRESSED")
+ // }
+ //}
+ }
+
+ fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
+ if (act == null) return null
+ event?.keyCode?.let { keyCode ->
+ when (event.action) {
+ KeyEvent.ACTION_DOWN -> {
+ if (act.currentFocus != null) {
+ val next = when (keyCode) {
+ KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
+ act,
+ act.currentFocus,
+ FocusDirection.Left
+ )
+ KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
+ act,
+ act.currentFocus,
+ FocusDirection.Right
+ )
+ KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
+ act,
+ act.currentFocus,
+ FocusDirection.Up
+ )
+ KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
+ act,
+ act.currentFocus,
+ FocusDirection.Down
+ )
+
+ else -> null
+ }
+
+ if (next != null && next != -1) {
+ val nextView = act.findViewById(next)
+ if (nextView != null) {
+ nextView.requestFocus()
+ return true
+ }
+ }
+
+ when (keyCode) {
+ KeyEvent.KEYCODE_DPAD_CENTER -> {
+ if (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) {
+ UIHelper.showInputMethod(act.currentFocus?.findFocus())
+ }
+ }
+ }
+ }
+ //println("Keycode: $keyCode")
+ //showToast(
+ // this,
+ // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
+ // Toast.LENGTH_LONG
+ //)
+ }
+ }
+ }
+
+ if (keyEventListener?.invoke(event) == true) {
+ return true
+ }
+ return null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
index f4da4231..fc7d21c4 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
@@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.lagradost.cloudstream3.animeproviders.*
import com.lagradost.cloudstream3.movieproviders.*
+import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.ExtractorLink
import java.util.*
@@ -298,18 +299,12 @@ fun MainAPI.fixUrl(url: String): String {
}
}
-fun sortUrls(urls: List): List {
+fun sortUrls(urls: Set): List {
return urls.sortedBy { t -> -t.quality }
}
-fun sortSubs(urls: List): List {
- val encounteredTimes = HashMap()
- return urls.sortedBy { t -> t.lang }.map {
- val times = encounteredTimes[it.lang]?.plus(1) ?: 1
- encounteredTimes[it.lang] = times
-
- SubtitleFile("${it.lang} ${if (times > 1) "($times)" else ""}", it.url)
- }
+fun sortSubs(subs : Set) : List {
+ return subs.sortedBy { it.name }
}
fun capitalizeString(str: String): String {
@@ -377,6 +372,11 @@ fun TvType.isMovieType(): Boolean {
return this == TvType.Movie || this == TvType.AnimeMovie || this == TvType.Torrent
}
+// returns if the type has an anime opening
+fun TvType.isAnimeOp(): Boolean {
+ return this == TvType.Anime || this == TvType.ONA
+}
+
data class SubtitleFile(val lang: String, val url: String)
class HomePageResponse(
@@ -463,7 +463,7 @@ interface LoadResponse {
fun LoadResponse?.isEpisodeBased(): Boolean {
if (this == null) return false
- return (this is AnimeLoadResponse || this is TvSeriesLoadResponse) && (this.type == TvType.TvSeries || this.type == TvType.Anime)
+ return (this is AnimeLoadResponse || this is TvSeriesLoadResponse) && this.type.isEpisodeBased()
}
fun LoadResponse?.isAnimeBased(): Boolean {
@@ -471,6 +471,11 @@ fun LoadResponse?.isAnimeBased(): Boolean {
return (this.type == TvType.Anime || this.type == TvType.ONA) // && (this is AnimeLoadResponse)
}
+fun TvType?.isEpisodeBased() : Boolean {
+ if (this == null) return false
+ return (this == TvType.TvSeries || this == TvType.Anime)
+}
+
data class AnimeEpisode(
val url: String,
var name: String? = null,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
index a4c4f035..a85bf2ca 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -1,23 +1,13 @@
package com.lagradost.cloudstream3
-import android.app.Activity
-import android.app.PictureInPictureParams
import android.content.ComponentName
-import android.content.Context
import android.content.Intent
-import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.content.res.Configuration
-import android.content.res.Resources
-import android.os.Build
import android.os.Bundle
-import android.view.*
-import android.view.KeyEvent.ACTION_DOWN
-import android.widget.TextView
-import android.widget.Toast
-import androidx.annotation.StringRes
+import android.view.KeyEvent
+import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.navigation.NavDestination
import androidx.navigation.findNavController
@@ -29,6 +19,12 @@ import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.restrictedApis
+import com.lagradost.cloudstream3.CommonActivity.backEvent
+import com.lagradost.cloudstream3.CommonActivity.loadThemes
+import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
+import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
+import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
+import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.Requests
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
@@ -37,27 +33,22 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2accoun
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
-import com.lagradost.cloudstream3.ui.player.PlayerEventType
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
+import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
-import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
-import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
-import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
-import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod
-import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_result.*
-import java.util.*
+import java.io.File
import kotlin.concurrent.thread
@@ -72,6 +63,7 @@ const val VLC_FROM_PROGRESS = -2
const val VLC_EXTRA_POSITION_OUT = "extra_position"
const val VLC_EXTRA_DURATION_OUT = "extra_duration"
const val VLC_LAST_ID_KEY = "vlc_last_open_id"
+
// Short name for requests client to make it nicer to use
var app = Requests()
@@ -91,7 +83,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
findNavController(R.id.nav_host_fragment).currentDestination?.let { updateNavBar(it) }
}
- private fun updateNavBar(destination : NavDestination) {
+ private fun updateNavBar(destination: NavDestination) {
this.hideKeyboard()
// Fucks up anime info layout since that has its own layout
@@ -106,7 +98,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
R.id.navigation_download_child
).contains(destination.id)
- val landscape = when(resources.configuration.orientation) {
+ val landscape = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
true
}
@@ -181,268 +173,24 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
- enum class FocusDirection {
- Left,
- Right,
- Up,
- Down,
- }
-
- private fun getNextFocus(view: View?, direction: FocusDirection, depth: Int = 0): Int? {
- if (view == null || depth >= 10) {
- return null
- }
-
- val nextId = when (direction) {
- FocusDirection.Left -> {
- view.nextFocusLeftId
- }
- FocusDirection.Up -> {
- view.nextFocusUpId
- }
- FocusDirection.Right -> {
- view.nextFocusRightId
- }
- FocusDirection.Down -> {
- view.nextFocusDownId
- }
- }
-
- return if (nextId != -1) {
- val next = findViewById(nextId)
- //println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" )
-
- if (next?.isShown == false) {
- getNextFocus(next, direction, depth + 1)
- } else {
- if (depth == 0) {
- null
- } else {
- nextId
- }
- }
- } else {
- null
- }
- }
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
- event?.keyCode?.let { keyCode ->
- when (event.action) {
- ACTION_DOWN -> {
- if (currentFocus != null) {
- val next = when (keyCode) {
- KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(currentFocus, FocusDirection.Left)
- KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(currentFocus, FocusDirection.Right)
- KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(currentFocus, FocusDirection.Up)
- KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(currentFocus, FocusDirection.Down)
-
- else -> null
- }
-
- if (next != null && next != -1) {
- val nextView = findViewById(next)
- if(nextView != null) {
- nextView.requestFocus()
- return true
- }
- }
-
- when (keyCode) {
-
- KeyEvent.KEYCODE_DPAD_CENTER -> {
- println("DPAD PRESSED $currentFocus")
- if (currentFocus is SearchView || currentFocus is SearchView.SearchAutoComplete) {
- println("current PRESSED")
- showInputMethod(currentFocus?.findFocus())
- }
- }
- }
- }
- //println("Keycode: $keyCode")
- //showToast(
- // this,
- // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
- // Toast.LENGTH_LONG
- //)
- }
- }
- }
-
- if (keyEventListener?.invoke(event) == true) {
- return true
+ CommonActivity.dispatchKeyEvent(this, event)?.let {
+ return it
}
return super.dispatchKeyEvent(event)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
- //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) {
- KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
- PlayerEventType.SeekForward
- }
- KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
- PlayerEventType.SeekBack
- }
- KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1 -> {
- PlayerEventType.NextEpisode
- }
- KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1 -> {
- PlayerEventType.PrevEpisode
- }
- KeyEvent.KEYCODE_MEDIA_PAUSE -> {
- PlayerEventType.Pause
- }
- KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
- PlayerEventType.Play
- }
- KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7 -> {
- PlayerEventType.Lock
- }
- KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
- PlayerEventType.ToggleHide
- }
- KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
- PlayerEventType.ToggleMute
- }
- KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9 -> {
- PlayerEventType.ShowMirrors
- }
- KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3 -> {
- PlayerEventType.ShowSpeed
- }
- KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0 -> {
- PlayerEventType.Resize
- }
- KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
- PlayerEventType.PlayPauseToggle
- }
- else -> null
- }?.let { playerEvent ->
- playerEventListener?.invoke(playerEvent)
- }
-
- //when (keyCode) {
- // KeyEvent.KEYCODE_DPAD_CENTER -> {
- // println("DPAD PRESSED")
- // }
- //}
+ CommonActivity.onKeyDown(this, keyCode, event)
return super.onKeyDown(keyCode, event)
}
- companion object {
- fun Activity?.getCastSession(): CastSession? {
- return (this as MainActivity?)?.mSessionManager?.currentCastSession
- }
-
- var canEnterPipMode: Boolean = false
- var canShowPipMode: Boolean = false
- var isInPIPMode: Boolean = false
-
- val backEvent = Event()
- val onColorSelectedEvent = Event>()
- val onDialogDismissedEvent = Event()
-
- var playerEventListener: ((PlayerEventType) -> Unit)? = null
- var keyEventListener: ((KeyEvent?) -> Boolean)? = null
-
-
- var currentToast: Toast? = null
-
- fun showToast(act: Activity?, @StringRes message: Int, duration: Int) {
- if (act == null) return
- showToast(act, act.getString(message), duration)
- }
-
- fun showToast(act: Activity?, message: String?, duration: Int? = null) {
- if (act == null || message == null) return
- try {
- currentToast?.cancel()
- } catch (e: Exception) {
- e.printStackTrace()
- }
- try {
- val inflater = act.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
-
- val layout: View = inflater.inflate(
- R.layout.toast,
- act.findViewById(R.id.toast_layout_root) as ViewGroup?
- )
-
- val text = layout.findViewById(R.id.text) as TextView
- text.text = message.trim()
-
- val toast = Toast(act)
- toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
- toast.duration = duration ?: Toast.LENGTH_SHORT
- toast.view = layout
- toast.show()
- currentToast = toast
- } catch (e: Exception) {
-
- }
- }
-
- fun setLocale(context: Context?, languageCode: String?) {
- if (context == null || languageCode == null) return
- val locale = Locale(languageCode)
- val resources: Resources = context.resources
- val config = resources.configuration
- Locale.setDefault(locale)
- config.setLocale(locale)
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
- context.createConfigurationContext(config)
- resources.updateConfiguration(config, resources.displayMetrics)
- }
-
- fun Context.updateLocale() {
- val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
- val localeCode = settingsManager.getString(getString(R.string.locale_key), null)
- setLocale(this, localeCode)
- }
- }
-
- private fun enterPIPMode() {
- if (!shouldShowPIPMode(canEnterPipMode) || !canShowPipMode) return
- try {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- try {
- enterPictureInPictureMode(PictureInPictureParams.Builder().build())
- } catch (e: Exception) {
- enterPictureInPictureMode()
- }
- } else {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- enterPictureInPictureMode()
- }
- }
- } catch (e: Exception) {
- logError(e)
- }
- }
override fun onUserLeaveHint() {
super.onUserLeaveHint()
- if (canEnterPipMode && canShowPipMode) {
- enterPIPMode()
- }
+ onUserLeaveHint(this)
}
override fun onBackPressed() {
@@ -456,9 +204,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (VLC_REQUEST_CODE == requestCode) {
if (resultCode == RESULT_OK && data != null) {
val pos: Long =
- data.getLongExtra(VLC_EXTRA_POSITION_OUT, -1) //Last position in media when player exited
+ data.getLongExtra(
+ VLC_EXTRA_POSITION_OUT,
+ -1
+ ) //Last position in media when player exited
val dur: Long =
- data.getLongExtra(VLC_EXTRA_DURATION_OUT, -1) //Last position in media when player exited
+ data.getLongExtra(
+ VLC_EXTRA_DURATION_OUT,
+ -1
+ ) //Last position in media when player exited
val id = getKey(VLC_LAST_ID_KEY)
println("SET KEY $id at $pos / $dur")
if (dur > 0 && pos > 0) {
@@ -486,6 +240,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
private fun handleAppIntent(intent: Intent?) {
if (intent == null) return
val str = intent.dataString
+ loadCache()
if (str != null) {
if (str.contains(appString)) {
for (api in OAuth2Apis) {
@@ -513,37 +268,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
for (api in OAuth2accountApis) {
api.init()
}
-
- val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
-
- val currentTheme = when (settingsManager.getString(getString(R.string.app_theme_key), "AmoledLight")) {
- "Black" -> R.style.AppTheme
- "Light" -> R.style.LightMode
- "Amoled" -> R.style.AmoledMode
- "AmoledLight" -> R.style.AmoledModeLight
- else -> R.style.AppTheme
- }
-
- val currentOverlayTheme = when (settingsManager.getString(getString(R.string.primary_color_key), "Normal")) {
- "Normal" -> R.style.OverlayPrimaryColorNormal
- "Blue" -> R.style.OverlayPrimaryColorBlue
- "Purple" -> R.style.OverlayPrimaryColorPurple
- "Green" -> R.style.OverlayPrimaryColorGreen
- "GreenApple" -> R.style.OverlayPrimaryColorGreenApple
- "Red" -> R.style.OverlayPrimaryColorRed
- "Banana" -> R.style.OverlayPrimaryColorBanana
- "Party" -> R.style.OverlayPrimaryColorParty
- else -> R.style.OverlayPrimaryColorNormal
- }
-
- theme.applyStyle(currentTheme, true)
- theme.applyStyle(currentOverlayTheme, true)
-
- theme.applyStyle(
- R.style.LoadedStyle,
- true
- ) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
-
+ loadThemes(this)
updateLocale()
app.initClient(this)
super.onCreate(savedInstanceState)
@@ -565,12 +290,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// val navView: BottomNavigationView = findViewById(R.id.nav_view)
- //https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
- //https://developer.android.com/guide/topics/ui/picture-in-picture
- canShowPipMode =
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && // OS SUPPORT
- packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
- hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
+
+ CommonActivity.init(this)
val navController = findNavController(R.id.nav_host_fragment)
@@ -588,6 +309,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
navController.addOnDestinationChangedListener { _, destination, _ ->
updateNavBar(destination)
}
+ loadCache()
/*nav_view.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
@@ -721,6 +443,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
APIRepository.dubStatusActive = getApiDubstatusSettings()
+ try {
+ // this ensures that no unnecessary space is taken
+ loadCache()
+ File(filesDir, "exoplayer").deleteRecursively() // old cache
+ File(cacheDir, "exoplayer").deleteOnExit() // current cache
+ } catch (e: Exception) {
+ logError(e)
+ }
/*
val relativePath = (Environment.DIRECTORY_DOWNLOADS) + File.separatorChar
diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/GogoanimeProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/GogoanimeProvider.kt
index 2c9b8455..c7db1ec3 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/GogoanimeProvider.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/GogoanimeProvider.kt
@@ -2,8 +2,8 @@ package com.lagradost.cloudstream3.animeproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.ExtractorLink
-import com.lagradost.cloudstream3.utils.extractorApis
import com.lagradost.cloudstream3.utils.getQualityFromName
+import com.lagradost.cloudstream3.utils.loadExtractor
import org.jsoup.Jsoup
import java.util.*
@@ -107,8 +107,11 @@ class GogoanimeProvider : MainAPI() {
this.name,
TvType.Anime,
it.selectFirst("img").attr("src"),
- it.selectFirst(".released")?.text()?.split(":")?.getOrNull(1)?.trim()?.toIntOrNull(),
- if (it.selectFirst(".name").text().contains("Dub")) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(
+ it.selectFirst(".released")?.text()?.split(":")?.getOrNull(1)?.trim()
+ ?.toIntOrNull(),
+ if (it.selectFirst(".name").text()
+ .contains("Dub")
+ ) EnumSet.of(DubStatus.Dubbed) else EnumSet.of(
DubStatus.Subbed
),
)
@@ -191,22 +194,20 @@ class GogoanimeProvider : MainAPI() {
}
}
- private fun extractVideos(uri: String): List {
- val html = app.get(uri).text
- val doc = Jsoup.parse(html)
+ private fun extractVideos(uri: String, callback: (ExtractorLink) -> Unit) {
+ val doc = app.get(uri).document
+
+ val iframe = fixUrlNull(doc.selectFirst("div.play-video > iframe").attr("src")) ?: return
- val iframe = "https:" + doc.selectFirst("div.play-video > iframe").attr("src")
val link = iframe.replace("streaming.php", "download")
-
val page = app.get(link, headers = mapOf("Referer" to iframe))
- val pageDoc = Jsoup.parse(page.text)
- return pageDoc.select(".dowload > a").pmap {
+ page.document.select(".dowload > a").pmap {
if (it.hasAttr("download")) {
val qual = if (it.text()
.contains("HDP")
) "1080" else qualityRegex.find(it.text())?.destructured?.component1().toString()
- listOf(
+ callback(
ExtractorLink(
"Gogoanime",
if (qual == "null") "Gogoanime" else "Gogoanime - " + qual + "p",
@@ -218,16 +219,18 @@ class GogoanimeProvider : MainAPI() {
)
} else {
val url = it.attr("href")
- val extractorLinks = ArrayList()
- for (api in extractorApis) {
- if (url.startsWith(api.mainUrl)) {
- extractorLinks.addAll(api.getSafeUrl(url) ?: listOf())
- break
- }
- }
- extractorLinks
+ loadExtractor(url, null, callback)
+ }
+ }
+
+ val streamingResponse = app.get(iframe, headers = mapOf("Referer" to iframe))
+ streamingResponse.document.select(".list-server-items > .linkserver")
+ ?.forEach { element ->
+ val status = element.attr("data-status") ?: return@forEach
+ if (status != "1") return@forEach
+ val data = element.attr("data-video") ?: return@forEach
+ loadExtractor(data, streamingResponse.url, callback)
}
- }.flatten()
}
override fun loadLinks(
@@ -236,9 +239,7 @@ class GogoanimeProvider : MainAPI() {
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
- for (source in extractVideos(data)) {
- callback.invoke(source)
- }
+ extractVideos(data, callback)
return true
}
}
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 e5cad799..f8927793 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt
@@ -21,10 +21,12 @@ import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.media.RemoteMediaClient
import com.google.android.gms.cast.framework.media.uicontroller.UIController
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
-import com.lagradost.cloudstream3.APIHolder.getApiFromName
import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
+import com.lagradost.cloudstream3.sortSubs
import com.lagradost.cloudstream3.sortUrls
+import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
+import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks
import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo
@@ -86,10 +88,11 @@ data class MetadataHolder(
val currentEpisodeIndex: Int,
val episodes: List,
val currentLinks: List,
- val currentSubtitles: List
+ val currentSubtitles: List
)
-class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : UIController() {
+class SelectSourceController(val view: ImageView, val activity: ControllerActivity) :
+ UIController() {
private val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
@@ -106,17 +109,22 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
remoteMediaClient?.mediaInfo?.mediaTracks?.filter { it.type == MediaTrack.TYPE_TEXT }
?: ArrayList()
- val bottomSheetDialogBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack)
+ val bottomSheetDialogBuilder =
+ AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack)
bottomSheetDialogBuilder.setView(R.layout.sort_bottom_sheet)
val bottomSheetDialog = bottomSheetDialogBuilder.create()
bottomSheetDialog.show()
// bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet)
- val providerList = bottomSheetDialog.findViewById(R.id.sort_providers)!!
- val subtitleList = bottomSheetDialog.findViewById(R.id.sort_subtitles)!!
+ val providerList =
+ bottomSheetDialog.findViewById(R.id.sort_providers)!!
+ val subtitleList =
+ bottomSheetDialog.findViewById(R.id.sort_subtitles)!!
if (subTracks.isEmpty()) {
- bottomSheetDialog.findViewById(R.id.sort_subtitles_holder)?.visibility = GONE
+ bottomSheetDialog.findViewById(R.id.sort_subtitles_holder)?.visibility =
+ GONE
} else {
- val arrayAdapter = ArrayAdapter(view.context, R.layout.sort_bottom_single_choice)
+ val arrayAdapter =
+ ArrayAdapter(view.context, R.layout.sort_bottom_single_choice)
arrayAdapter.add(view.context.getString(R.string.no_subtitles))
arrayAdapter.addAll(subTracks.mapNotNull { it.name })
@@ -168,7 +176,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
val sortingMethods = items.map { it.name }.toTypedArray()
val sotringIndex = items.indexOfFirst { it.url == contentUrl }
- val arrayAdapter = ArrayAdapter(view.context, R.layout.sort_bottom_single_choice)
+ val arrayAdapter =
+ ArrayAdapter(view.context, R.layout.sort_bottom_single_choice)
arrayAdapter.addAll(sortingMethods.toMutableList())
providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
@@ -196,7 +205,9 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
try { // THIS IS VERY IMPORTANT BECAUSE WE NEVER WANT TO AUTOLOAD THE NEXT EPISODE
val currentIdIndex = remoteMediaClient?.getItemIndex()
- val nextId = remoteMediaClient?.mediaQueue?.itemIds?.get(currentIdIndex?.plus(1) ?: 0)
+ val nextId = remoteMediaClient?.mediaQueue?.itemIds?.get(
+ currentIdIndex?.plus(1) ?: 0
+ )
if (currentIdIndex == null && nextId != null) {
awaitLinks(
remoteMediaClient?.queueInsertAndPlayItem(
@@ -256,26 +267,29 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
thread {
val index = meta.currentEpisodeIndex + 1
val epData = meta.episodes[index]
- val links = ArrayList()
- val subs = ArrayList()
+ val currentLinks = mutableSetOf()
+ val currentSubs = mutableSetOf()
- val isSuccessful =
- APIRepository(getApiFromName(meta.apiName)).loadLinks(epData.data, true, { subtitleFile ->
- if (!subs.any { it.url == subtitleFile.url }) {
- subs.add(subtitleFile)
- }
- }) { link ->
- if (!links.any { it.url == link.url }) {
- links.add(link)
- }
- }
+ val generator = RepoLinkGenerator(listOf(epData))
- if (isSuccessful) {
- val sorted = sortUrls(links)
- if (sorted.isNotEmpty()) {
+ val isSuccessful = normalSafeApiCall {
+ generator.generateLinks(false, true,
+ {
+ it.first?.let { link ->
+ currentLinks.add(link)
+ }
+ }, {
+ currentSubs.add(it)
+ })
+ }
+
+ val sortedLinks = sortUrls(currentLinks)
+ val sortedSubs = sortSubs(currentSubs)
+ if (isSuccessful == true) {
+ if (currentLinks.isNotEmpty()) {
val jsonCopy = meta.copy(
- currentLinks = sorted,
- currentSubtitles = subs,
+ currentLinks = sortedLinks,
+ currentSubtitles = sortedSubs,
currentEpisodeIndex = index
)
@@ -287,7 +301,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
jsonCopy,
0,
done,
- subs
+ sortedSubs
)
/*fun loadIndex(index: Int) {
@@ -367,9 +381,21 @@ class ControllerActivity : ExpandedControllerActivity() {
val skipBackButton: ImageView = getButtonImageViewAt(1)
val skipForwardButton: ImageView = getButtonImageViewAt(2)
val skipOpButton: ImageView = getButtonImageViewAt(3)
- uiMediaController.bindViewToUIController(sourcesButton, SelectSourceController(sourcesButton, this))
- uiMediaController.bindViewToUIController(skipBackButton, SkipTimeController(skipBackButton, false))
- uiMediaController.bindViewToUIController(skipForwardButton, SkipTimeController(skipForwardButton, true))
- uiMediaController.bindViewToUIController(skipOpButton, SkipNextEpisodeController(skipOpButton))
+ uiMediaController.bindViewToUIController(
+ sourcesButton,
+ SelectSourceController(sourcesButton, this)
+ )
+ uiMediaController.bindViewToUIController(
+ skipBackButton,
+ SkipTimeController(skipBackButton, false)
+ )
+ uiMediaController.bindViewToUIController(
+ skipForwardButton,
+ SkipTimeController(skipForwardButton, true)
+ )
+ uiMediaController.bindViewToUIController(
+ skipOpButton,
+ SkipNextEpisodeController(skipOpButton)
+ )
}
}
\ No newline at end of file
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 78c6a407..d844bd41 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
@@ -4,14 +4,15 @@ import android.app.Activity
import android.content.DialogInterface
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
-import com.lagradost.cloudstream3.MainActivity
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.ui.player.PlayerFragment
-import com.lagradost.cloudstream3.ui.player.UriData
+import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator
+import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
-import com.lagradost.cloudstream3.utils.DataStore.getKey
-import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
+import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
+import com.lagradost.cloudstream3.utils.ExtractorUri
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager
@@ -49,7 +50,7 @@ object DownloadButtonSetup {
.setPositiveButton(R.string.delete, dialogClickListener)
.setNegativeButton(R.string.cancel, dialogClickListener)
.show()
- } catch (e : Exception) {
+ } catch (e: Exception) {
logError(e)
// ye you somehow fucked up formatting did you?
}
@@ -81,40 +82,71 @@ object DownloadButtonSetup {
DOWNLOAD_ACTION_LONG_CLICK -> {
activity?.let { act ->
val length =
- VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(act, click.data.id)?.fileLength
+ VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
+ act,
+ click.data.id
+ )?.fileLength
?: 0
if (length > 0) {
- MainActivity.showToast(act, R.string.delete, Toast.LENGTH_LONG)
+ showToast(act, R.string.delete, Toast.LENGTH_LONG)
} else {
- MainActivity.showToast(act, R.string.download, Toast.LENGTH_LONG)
+ showToast(act, R.string.download, Toast.LENGTH_LONG)
}
}
}
DOWNLOAD_ACTION_PLAY_FILE -> {
activity?.let { act ->
val info =
- VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(act, click.data.id)
- ?: return
- val keyInfo = act.getKey(
+ VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
+ act,
+ click.data.id
+ ) ?: return
+ val keyInfo = getKey(
VideoDownloadManager.KEY_DOWNLOAD_INFO,
click.data.id.toString()
) ?: return
+ val parent = getKey(
+ DOWNLOAD_HEADER_CACHE,
+ click.data.parentId.toString()
+ ) ?: return
act.navigate(
- R.id.global_to_navigation_player, PlayerFragment.newInstance(
- UriData(
- info.path.toString(),
- keyInfo.basePath,
- keyInfo.relativePath,
- keyInfo.displayName,
- click.data.parentId,
- click.data.id,
- headerName ?: "null",
- if (click.data.episode <= 0) null else click.data.episode,
- click.data.season
- ),
- getViewPos(click.data.id)?.position ?: 0
+ R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
+ DownloadFileGenerator(
+ listOf(
+ ExtractorUri(
+ uri = info.path,
+
+ id = click.data.id,
+ parentId = click.data.parentId,
+ name = act.getString(R.string.downloaded_file), //click.data.name ?: keyInfo.displayName
+ season = click.data.season,
+ episode = click.data.episode,
+ headerName = parent.name,
+ tvType = parent.type,
+
+ basePath = keyInfo.basePath,
+ displayName = keyInfo.displayName,
+ relativePath = keyInfo.relativePath,
+ )
+ ),
+ 0
+ )
)
+ //R.id.global_to_navigation_player, PlayerFragment.newInstance(
+ // UriData(
+ // info.path.toString(),
+ // keyInfo.basePath,
+ // keyInfo.relativePath,
+ // keyInfo.displayName,
+ // click.data.parentId,
+ // click.data.id,
+ // headerName ?: "null",
+ // if (click.data.episode <= 0) null else click.data.episode,
+ // click.data.season
+ // ),
+ // getViewPos(click.data.id)?.position ?: 0
+ //)
)
}
}
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 97c5aa3f..8883fbe4 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
@@ -177,8 +177,7 @@ class HomeViewModel : ViewModel() {
logError(e)
}
}
- else -> {
- }
+ else -> Unit
}
_page.postValue(data)
} else {
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
new file mode 100644
index 00000000..1a4e6d80
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt
@@ -0,0 +1,346 @@
+package com.lagradost.cloudstream3.ui.player
+
+import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.graphics.drawable.AnimatedImageDrawable
+import android.graphics.drawable.AnimatedVectorDrawable
+import android.media.metrics.PlaybackErrorEvent
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.Toast
+import androidx.annotation.LayoutRes
+import androidx.annotation.StringRes
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import com.google.android.exoplayer2.ExoPlayer
+import com.google.android.exoplayer2.PlaybackException
+import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
+import com.google.android.exoplayer2.ui.SubtitleView
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
+import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode
+import com.lagradost.cloudstream3.CommonActivity.isInPIPMode
+import com.lagradost.cloudstream3.CommonActivity.keyEventListener
+import com.lagradost.cloudstream3.CommonActivity.playerEventListener
+import com.lagradost.cloudstream3.CommonActivity.showToast
+import com.lagradost.cloudstream3.R
+import com.lagradost.cloudstream3.mvvm.logError
+import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
+import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
+import com.lagradost.cloudstream3.utils.AppUtils
+import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus
+import com.lagradost.cloudstream3.utils.UIHelper
+import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
+import kotlinx.android.synthetic.main.fragment_player.*
+import kotlinx.android.synthetic.main.player_custom_layout.*
+
+enum class PlayerResize(@StringRes val nameRes: Int) {
+ Fit(R.string.resize_fit),
+ Fill(R.string.resize_fill),
+ Zoom(R.string.resize_zoom),
+}
+
+// when the player should switch skip op to next episode
+const val SKIP_OP_VIDEO_PERCENTAGE = 50
+
+// when the player should preload the next episode for faster loading
+const val PRELOAD_NEXT_EPISODE_PERCENTAGE = 80
+
+// when the player should mark the episode as watched and resume watching the next
+const val NEXT_WATCH_EPISODE_PERCENTAGE = 95
+
+abstract class AbstractPlayerFragment(
+ @LayoutRes val layout: Int,
+ val player: IPlayer = CS3IPlayer()
+) : Fragment() {
+ var resizeMode: Int = 0
+ var subStyle: SaveCaptionStyle? = null
+ var subView: SubtitleView? = null
+ var isBuffering = true
+
+ open fun nextEpisode() {
+ throw NotImplementedError()
+ }
+
+ open fun prevEpisode() {
+ throw NotImplementedError()
+ }
+
+ open fun playerPositionChanged(posDur: Pair) {
+ throw NotImplementedError()
+ }
+
+ open fun playerDimensionsLoaded(widthHeight : Pair) {
+ throw NotImplementedError()
+ }
+
+ private fun updateIsPlaying(playing: Pair) {
+ val (wasPlaying, isPlaying) = playing
+ val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying
+
+ isBuffering = CSPlayerLoading.IsBuffering == isPlaying
+ if (isBuffering) {
+ player_pause_play_holder_holder?.isVisible = false
+ player_buffering?.isVisible = true
+ } else {
+ player_pause_play_holder_holder?.isVisible = true
+ player_buffering?.isVisible = false
+
+ if (wasPlaying != isPlaying) {
+ player_pause_play?.setImageResource(if (isPlayingRightNow) R.drawable.play_to_pause else R.drawable.pause_to_play)
+ val drawable = player_pause_play?.drawable
+
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
+ if (drawable is AnimatedImageDrawable) {
+ drawable.start()
+ }
+ }
+ if (drawable is AnimatedVectorDrawable) {
+ drawable.start()
+ }
+ } else {
+ player_pause_play?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play)
+ }
+ }
+
+ canEnterPipMode = isPlayingRightNow
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ activity?.let { act ->
+ PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow)
+ }
+ }
+ }
+
+ private var pipReceiver: BroadcastReceiver? = null
+ override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
+ try {
+ isInPIPMode = isInPictureInPictureMode
+ if (isInPictureInPictureMode) {
+ // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
+ player_holder.alpha = 0f
+ pipReceiver = object : BroadcastReceiver() {
+ override fun onReceive(
+ context: Context,
+ intent: Intent,
+ ) {
+ if (ACTION_MEDIA_CONTROL != intent.action) {
+ return
+ }
+ player.handleEvent(
+ CSPlayerEvent.values()[intent.getIntExtra(
+ EXTRA_CONTROL_TYPE,
+ 0
+ )]
+ )
+ }
+ }
+ val filter = IntentFilter()
+ filter.addAction(
+ ACTION_MEDIA_CONTROL
+ )
+ activity?.registerReceiver(pipReceiver, filter)
+ val isPlaying = player.getIsPlaying()
+ val isPlayingValue =
+ if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused
+ updateIsPlaying(Pair(isPlayingValue, isPlayingValue))
+ } else {
+ // Restore the full-screen UI.
+ player_holder.alpha = 1f
+ pipReceiver?.let {
+ activity?.unregisterReceiver(it)
+ }
+ activity?.hideSystemUI()
+ this.view?.let { UIHelper.hideKeyboard(it) }
+ }
+ } catch (e: Exception) {
+ logError(e)
+ }
+ }
+
+ open fun nextMirror() {
+ throw NotImplementedError()
+ }
+
+ private fun playerError(exception: Exception) {
+ when (exception) {
+ is PlaybackException -> {
+ val msg = exception.message ?: ""
+ val errorName = exception.errorCodeName
+ when (val code = exception.errorCode) {
+ PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, PlaybackException.ERROR_CODE_IO_NO_PERMISSION, PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> {
+ showToast(
+ activity,
+ "${getString(R.string.source_error)}\n$errorName ($code)\n$msg",
+ Toast.LENGTH_SHORT
+ )
+ nextMirror()
+ }
+ PlaybackException.ERROR_CODE_REMOTE_ERROR, PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, PlaybackException.ERROR_CODE_TIMEOUT, PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE -> {
+ showToast(
+ activity,
+ "${getString(R.string.remote_error)}\n$errorName ($code)\n$msg",
+ Toast.LENGTH_SHORT
+ )
+ nextMirror()
+ }
+ PlaybackException.ERROR_CODE_DECODING_FAILED, PlaybackErrorEvent.ERROR_AUDIO_TRACK_INIT_FAILED, PlaybackErrorEvent.ERROR_AUDIO_TRACK_OTHER, PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED -> {
+ showToast(
+ activity,
+ "${getString(R.string.render_error)}\n$errorName ($code)\n$msg",
+ Toast.LENGTH_SHORT
+ )
+ nextMirror()
+ }
+ else -> {
+ showToast(
+ activity,
+ "${getString(R.string.unexpected_error)}\n$errorName ($code)\n$msg",
+ Toast.LENGTH_SHORT
+ )
+ }
+ }
+ }
+ else -> {
+ showToast(activity, exception.message, Toast.LENGTH_SHORT)
+ }
+ }
+ }
+
+ private fun requestAudioFocus() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ activity?.requestLocalAudioFocus(AppUtils.getFocusRequest())
+ }
+ }
+
+ private fun onSubStyleChanged(style: SaveCaptionStyle) {
+ if (player is CS3IPlayer) {
+ player.updateSubtitleStyle(style)
+ }
+ }
+
+ private fun playerUpdated(player: Any?) {
+ if (player is ExoPlayer) {
+ player_view?.player = player
+ player_view?.performClick()
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ resizeMode = getKey(RESIZE_MODE_KEY) ?: 0
+ resize(resizeMode, false)
+
+ player.initCallbacks(
+ playerUpdated = ::playerUpdated,
+ updateIsPlaying = ::updateIsPlaying,
+ playerError = ::playerError,
+ requestAutoFocus = ::requestAudioFocus,
+ nextEpisode = ::nextEpisode,
+ prevEpisode = ::prevEpisode,
+ playerPositionChanged = ::playerPositionChanged,
+ playerDimensionsLoaded = ::playerDimensionsLoaded,
+ requestedListeningPercentages = listOf(
+ SKIP_OP_VIDEO_PERCENTAGE,
+ PRELOAD_NEXT_EPISODE_PERCENTAGE,
+ NEXT_WATCH_EPISODE_PERCENTAGE,
+ )
+ )
+
+ if (player is CS3IPlayer) {
+ subView = player_view?.findViewById(R.id.exo_subtitles)
+ subStyle = SubtitlesFragment.getCurrentSavedStyle()
+ player.initSubtitles(subView, subtitle_holder, subStyle)
+ SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged
+ }
+
+ /*context?.let { ctx ->
+ player.loadPlayer(
+ ctx,
+ false,
+ ExtractorLink(
+ "idk",
+ "bunny",
+ "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
+ "",
+ Qualities.P720.value,
+ false
+ ),
+ )
+ }*/
+ }
+
+ override fun onDestroy() {
+ playerEventListener = null
+ keyEventListener = null
+ SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged
+
+ // simply resets brightness and notch settings that might have been overridden
+ val lp = activity?.window?.attributes
+ lp?.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ lp?.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ }
+ activity?.window?.attributes = lp
+
+ super.onDestroy()
+ }
+
+ fun nextResize() {
+ resizeMode = (resizeMode + 1) % PlayerResize.values().size
+ resize(resizeMode, true)
+ }
+
+ fun resize(resize: Int, showToast: Boolean) {
+ resize(PlayerResize.values()[resize], showToast)
+ }
+
+ fun resize(resize: PlayerResize, showToast: Boolean) {
+ setKey(RESIZE_MODE_KEY, resize.ordinal)
+ val type = when (resize) {
+ PlayerResize.Fill -> AspectRatioFrameLayout.RESIZE_MODE_FIT
+ PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FILL
+ PlayerResize.Zoom -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
+ }
+ player_view?.resizeMode = type
+
+ exo_play?.setOnClickListener {
+ player.handleEvent(CSPlayerEvent.Play)
+ }
+
+ exo_pause?.setOnClickListener {
+ player.handleEvent(CSPlayerEvent.Pause)
+ }
+
+ if (showToast)
+ showToast(activity, resize.nameRes, Toast.LENGTH_SHORT)
+ }
+
+ override fun onStop() {
+ player.onStop()
+ super.onStop()
+ }
+
+ override fun onResume() {
+ context?.let { ctx ->
+ player.onResume(ctx)
+ }
+
+ super.onResume()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(layout, container, false)
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..7735d1d1
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt
@@ -0,0 +1,668 @@
+package com.lagradost.cloudstream3.ui.player
+
+import android.content.Context
+import android.net.Uri
+import android.os.Looper
+import android.util.Log
+import android.widget.FrameLayout
+import com.google.android.exoplayer2.*
+import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
+import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
+import com.google.android.exoplayer2.source.MergingMediaSource
+import com.google.android.exoplayer2.source.SingleSampleMediaSource
+import com.google.android.exoplayer2.text.Cue
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
+import com.google.android.exoplayer2.trackselection.TrackSelector
+import com.google.android.exoplayer2.ui.SubtitleView
+import com.google.android.exoplayer2.upstream.DataSource
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource
+import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
+import com.google.android.exoplayer2.upstream.cache.SimpleCache
+import com.google.android.exoplayer2.util.MimeTypes
+import com.lagradost.cloudstream3.USER_AGENT
+import com.lagradost.cloudstream3.mvvm.logError
+import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.ExtractorUri
+import java.io.File
+import javax.net.ssl.HttpsURLConnection
+import javax.net.ssl.SSLContext
+import javax.net.ssl.SSLSession
+
+const val TAG = "CS3ExoPlayer"
+
+class CS3IPlayer : IPlayer {
+ private var isPlaying = false
+ private var exoPlayer: ExoPlayer? = null
+
+ /** Cache */
+ private val cacheSize = 300L * 1024L * 1024L // 300 mb TODO MAKE SETTING
+ private val seekActionTime = 30000L
+
+ private var ignoreSSL: Boolean = true
+ private var simpleCache: SimpleCache? = null
+ private var playBackSpeed: Float = 1.0f
+
+ private var lastMuteVolume: Float = 1.0f
+
+ private var currentLink: ExtractorLink? = null
+ private var currentDownloadedFile: ExtractorUri? = null
+ private var hasUsedFirstRender = false
+
+ private var currentWindow: Int = 0
+ private var playbackPosition: Long = 0
+
+ private val subtitleHelper = PlayerSubtitleHelper()
+
+ override fun getDuration(): Long? = exoPlayer?.duration
+ override fun getPosition(): Long? = exoPlayer?.currentPosition
+ override fun getIsPlaying(): Boolean = isPlaying
+ override fun getPlaybackSpeed(): Float = playBackSpeed
+
+ /**
+ * Tracks reported to be used by exoplayer, since sometimes it has a mind of it's own when selecting subs.
+ * String = lowercase language as set by .setLanguage("_$langId")
+ * Boolean = if it's active
+ * */
+ private var exoPlayerSelectedTracks = listOf>()
+
+ /** isPlaying */
+ private var updateIsPlaying: ((Pair) -> Unit)? = null
+ private var requestAutoFocus: (() -> Unit)? = null
+ private var playerError: ((Exception) -> Unit)? = null
+
+ /** width x height */
+ private var playerDimensionsLoaded: ((Pair) -> Unit)? = null
+
+ /** used for playerPositionChanged */
+ private var requestedListeningPercentages: List? = null
+
+ /** Fired when seeking the player or on requestedListeningPercentages,
+ * used to make things appear on que
+ * position, duration */
+ private var playerPositionChanged: ((Pair) -> Unit)? = null
+
+ private var nextEpisode: (() -> Unit)? = null
+ private var prevEpisode: (() -> Unit)? = null
+
+ private var playerUpdated: ((Any?) -> Unit)? = null
+
+ override fun initCallbacks(
+ playerUpdated: (Any?) -> Unit,
+ updateIsPlaying: ((Pair) -> Unit)?,
+ requestAutoFocus: (() -> Unit)?,
+ playerError: ((Exception) -> Unit)?,
+ playerDimensionsLoaded: ((Pair) -> Unit)?,
+ requestedListeningPercentages: List?,
+ playerPositionChanged: ((Pair) -> Unit)?,
+ nextEpisode: (() -> Unit)?,
+ prevEpisode: (() -> Unit)?
+ ) {
+ this.playerUpdated = playerUpdated
+ this.updateIsPlaying = updateIsPlaying
+ this.requestAutoFocus = requestAutoFocus
+ this.playerError = playerError
+ this.playerDimensionsLoaded = playerDimensionsLoaded
+ this.requestedListeningPercentages = requestedListeningPercentages
+ this.playerPositionChanged = playerPositionChanged
+ this.nextEpisode = nextEpisode
+ this.prevEpisode = prevEpisode
+ }
+
+ fun initSubtitles(subView: SubtitleView?, subHolder: FrameLayout?, style: SaveCaptionStyle?) {
+ subtitleHelper.initSubtitles(subView, subHolder, style)
+ }
+
+ override fun loadPlayer(
+ context: Context,
+ sameEpisode: Boolean,
+ link: ExtractorLink?,
+ data: ExtractorUri?,
+ startPosition: Long?,
+ subtitles: Set
+ ) {
+ Log.i(TAG, "loadPlayer")
+ if (sameEpisode) {
+ saveData()
+ } else {
+ currentSubtitles = null
+ playbackPosition = 0
+ }
+
+ startPosition?.let {
+ playbackPosition = it
+ }
+
+ // we want autoplay because of TV and UX
+ isPlaying = true
+
+ // release the current exoplayer and cache
+ releasePlayer()
+ if (link != null) {
+ loadOnlinePlayer(context, link)
+ } else if (data != null) {
+ loadOfflinePlayer(context, data)
+ }
+ }
+
+ override fun setActiveSubtitles(subtitles: Set) {
+ Log.i(TAG, "setActiveSubtitles ${subtitles.size}")
+ subtitleHelper.setAllSubtitles(subtitles)
+ }
+
+ var currentSubtitles : SubtitleData? = null
+ override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean {
+ Log.i(TAG,"setPreferredSubtitles init $subtitle")
+ currentSubtitles = subtitle
+ return (exoPlayer?.trackSelector as? DefaultTrackSelector?)?.let { trackSelector ->
+ val name = subtitle?.name
+ if (name.isNullOrBlank()) {
+ trackSelector.setParameters(
+ trackSelector.buildUponParameters()
+ .setPreferredTextLanguage(null)
+ )
+ } else {
+ when (subtitleHelper.subtitleStatus(subtitle)) {
+ SubtitleStatus.REQUIRES_RELOAD -> {
+ Log.i(TAG,"setPreferredSubtitles REQUIRES_RELOAD")
+ return@let true
+ // reloadPlayer(context)
+ }
+ SubtitleStatus.IS_ACTIVE -> {
+ Log.i(TAG,"setPreferredSubtitles IS_ACTIVE")
+
+ trackSelector.setParameters(
+ trackSelector.buildUponParameters()
+ .setPreferredTextLanguage("_$name")
+ )
+ }
+ SubtitleStatus.NOT_FOUND -> {
+ // not found
+ Log.i(TAG,"setPreferredSubtitles NOT_FOUND")
+ return@let true
+ }
+ }
+ }
+ return false
+ } ?: false
+ }
+
+ override fun getCurrentPreferredSubtitle(): SubtitleData? {
+ return subtitleHelper.getAllSubtitles().firstOrNull { sub ->
+ exoPlayerSelectedTracks.any {
+ // The replace is needed as exoplayer translates _ to -
+ // Also we prefix the languages with _
+ it.second && it.first.replace("-", "") .equals(
+ sub.name.replace("-", ""),
+ ignoreCase = true
+ )
+ }
+ }
+ }
+
+ override fun updateSubtitleStyle(style: SaveCaptionStyle) {
+ subtitleHelper.setSubStyle(style)
+ }
+
+ private fun saveData() {
+ Log.i(TAG, "saveData")
+ updatedTime()
+
+ exoPlayer?.let { exo ->
+ playbackPosition = exo.currentPosition
+ currentWindow = exo.currentWindowIndex
+ isPlaying = exo.isPlaying
+ }
+ }
+
+ private fun releasePlayer() {
+ Log.i(TAG, "releasePlayer")
+
+ updatedTime()
+
+ exoPlayer?.release()
+ simpleCache?.release()
+
+ exoPlayer = null
+ simpleCache = null
+ }
+
+ override fun onStop() {
+ Log.i(TAG, "onStop")
+
+ saveData()
+ exoPlayer?.pause()
+ releasePlayer()
+ }
+
+ override fun onPause() {
+ Log.i(TAG, "onPause")
+ saveData()
+ exoPlayer?.pause()
+ releasePlayer()
+ }
+
+ override fun onResume(context: Context) {
+ if (exoPlayer == null)
+ reloadPlayer(context)
+ }
+
+ override fun release() {
+ releasePlayer()
+ }
+
+ override fun setPlaybackSpeed(speed: Float) {
+ exoPlayer?.setPlaybackSpeed(speed)
+ playBackSpeed = speed
+ }
+
+ companion object {
+ private fun createOnlineSource(link: ExtractorLink): DataSource.Factory {
+ return DefaultHttpDataSource.Factory().apply {
+ setUserAgent(USER_AGENT)
+ val headers = mapOf(
+ "referer" to link.referer,
+ "accept" to "*/*",
+ "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"",
+ "sec-ch-ua-mobile" to "?0",
+ "sec-fetch-user" to "?1",
+ "sec-fetch-mode" to "navigate",
+ "sec-fetch-dest" to "video"
+ ) + link.headers // Adds the headers from the provider, e.g Authorization
+ setDefaultRequestProperties(headers)
+
+ //https://stackoverflow.com/questions/69040127/error-code-io-bad-http-status-exoplayer-android
+ setAllowCrossProtocolRedirects(true)
+ }
+ }
+
+ private fun Context.createOfflineSource(): DataSource.Factory {
+ return DefaultDataSourceFactory(this, 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)
+ }
+
+ private fun getCache(context: Context, cacheSize: Long): SimpleCache? {
+ return try {
+ val databaseProvider = StandaloneDatabaseProvider(context)
+ SimpleCache(
+ File(
+ context.cacheDir, "exoplayer"
+ ).also { it.deleteOnExit() }, // Ensures always fresh file
+ LeastRecentlyUsedCacheEvictor(cacheSize),
+ databaseProvider
+ )
+ } catch (e: Exception) {
+ logError(e)
+ null
+ }
+ }
+
+ private fun getMediaItemBuilder(mimeType: String):
+ MediaItem.Builder {
+ return MediaItem.Builder()
+ //Replace needed for android 6.0.0 https://github.com/google/ExoPlayer/issues/5983
+ .setMimeType(mimeType)
+ }
+
+ private fun getMediaItem(mimeType: String, uri: Uri): MediaItem {
+ return getMediaItemBuilder(mimeType).setUri(uri).build()
+ }
+
+ private fun getMediaItem(mimeType: String, url: String): MediaItem {
+ return getMediaItemBuilder(mimeType).setUri(url).build()
+ }
+
+ private fun getTrackSelector(context: Context): TrackSelector {
+ val trackSelector = DefaultTrackSelector(context)
+ trackSelector.parameters = DefaultTrackSelector.ParametersBuilder(context)
+ // .setRendererDisabled(C.TRACK_TYPE_VIDEO, true)
+ .setRendererDisabled(C.TRACK_TYPE_TEXT, true)
+ .setDisabledTextTrackSelectionFlags(C.TRACK_TYPE_TEXT)
+ .clearSelectionOverrides()
+ .build()
+ return trackSelector
+ }
+
+ private fun buildExoPlayer(
+ context: Context,
+ mediaItem: MediaItem,
+ subSources: List,
+ currentWindow: Int,
+ playbackPosition: Long,
+ playBackSpeed: Float,
+ playWhenReady: Boolean = true,
+ cacheFactory: CacheDataSource.Factory? = null,
+ trackSelector: TrackSelector? = null,
+ ): ExoPlayer {
+ val exoPlayerBuilder =
+ ExoPlayer.Builder(context)
+ .setTrackSelector(trackSelector ?: getTrackSelector(context))
+
+ val videoMediaSource =
+ (if (cacheFactory == null) DefaultMediaSourceFactory(context) else DefaultMediaSourceFactory(
+ cacheFactory
+ )).createMediaSource(
+ mediaItem
+ )
+
+ return exoPlayerBuilder.build().apply {
+ setPlayWhenReady(playWhenReady)
+ seekTo(currentWindow, playbackPosition)
+ setMediaSource(
+ MergingMediaSource(
+ videoMediaSource, *subSources.toTypedArray()
+ ),
+ playbackPosition
+ )
+ setHandleAudioBecomingNoisy(true)
+ setPlaybackSpeed(playBackSpeed)
+ }
+ }
+ }
+
+ fun updatedTime() {
+ val position = exoPlayer?.currentPosition
+ val duration = exoPlayer?.contentDuration
+ if (duration != null && position != null) {
+ playerPositionChanged?.invoke(Pair(position, duration))
+ }
+ }
+
+ override fun seekTime(time: Long) {
+ exoPlayer?.seekTime(time)
+ }
+
+ override fun seekTo(time: Long) {
+ updatedTime()
+ exoPlayer?.seekTo(time)
+ }
+
+ private fun ExoPlayer.seekTime(time: Long) {
+ updatedTime()
+ seekTo(currentPosition + time)
+ }
+
+ override fun handleEvent(event: CSPlayerEvent) {
+ Log.i(TAG, "handleEvent ${event.name}")
+ try {
+ exoPlayer?.apply {
+ when (event) {
+ CSPlayerEvent.Play -> {
+ play()
+ }
+ CSPlayerEvent.Pause -> {
+ pause()
+ }
+ CSPlayerEvent.ToggleMute -> {
+ if (volume <= 0) {
+ //is muted
+ volume = lastMuteVolume
+ } else {
+ // is not muted
+ lastMuteVolume = volume
+ volume = 0f
+ }
+ }
+ CSPlayerEvent.PlayPauseToggle -> {
+ if (isPlaying) {
+ pause()
+ } else {
+ play()
+ }
+ }
+ CSPlayerEvent.SeekForward -> seekTime(seekActionTime)
+ CSPlayerEvent.SeekBack -> seekTime(-seekActionTime)
+ CSPlayerEvent.NextEpisode -> nextEpisode?.invoke()
+ CSPlayerEvent.PrevEpisode -> prevEpisode?.invoke()
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "handleEvent error", e)
+ playerError?.invoke(e)
+ }
+ }
+
+ private fun loadExo(
+ context: Context,
+ mediaItem: MediaItem,
+ subSources: List,
+ cacheFactory: CacheDataSource.Factory? = null
+ ) {
+ Log.i(TAG, "loadExo")
+ try {
+ hasUsedFirstRender = false
+
+ // ye this has to be a val for whatever reason
+ // this makes no sense
+ exoPlayer = buildExoPlayer(
+ context,
+ mediaItem,
+ subSources,
+ currentWindow,
+ playbackPosition,
+ playBackSpeed,
+ playWhenReady = isPlaying, // this keep the current state of the player
+ cacheFactory = cacheFactory
+ )
+
+ playerUpdated?.invoke(exoPlayer)
+ exoPlayer?.prepare()
+
+ exoPlayer?.let { exo ->
+ updateIsPlaying?.invoke(
+ Pair(
+ CSPlayerLoading.IsBuffering,
+ CSPlayerLoading.IsBuffering
+ )
+ )
+ isPlaying = exo.isPlaying
+ }
+ exoPlayer?.addListener(object : Player.Listener {
+ /**
+ * Records the current used subtitle/track. Needed as exoplayer seems to have loose track language selection.
+ * */
+ override fun onTracksInfoChanged(tracksInfo: TracksInfo) {
+ exoPlayerSelectedTracks =
+ tracksInfo.trackGroupInfos.mapNotNull { it.trackGroup.getFormat(0).language?.let { lang -> lang to it.isSelected } }
+ super.onTracksInfoChanged(tracksInfo)
+ }
+
+ override fun onCues(cues: MutableList) {
+ Log.i(TAG, "CUES: ${cues.size}")
+ if(cues.size > 0) {
+ Log.i(TAG, "CUES SAY: ${cues.first().text}")
+ }
+ super.onCues(cues)
+ }
+
+ override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
+ exoPlayer?.let { exo ->
+ updateIsPlaying?.invoke(
+ Pair(
+ if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused,
+ if (playbackState == Player.STATE_BUFFERING) CSPlayerLoading.IsBuffering else if (exo.isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused
+ )
+ )
+ isPlaying = exo.isPlaying
+ }
+
+ if (playWhenReady) {
+ when (playbackState) {
+ Player.STATE_READY -> {
+ requestAutoFocus?.invoke()
+ }
+ Player.STATE_ENDED -> {
+ handleEvent(CSPlayerEvent.NextEpisode)
+ }
+ Player.STATE_BUFFERING -> {
+ updatedTime()
+ }
+ Player.STATE_IDLE -> {
+ // IDLE
+ }
+ else -> Unit
+ }
+ }
+ }
+
+ override fun onPlayerError(error: PlaybackException) {
+ playerError?.invoke(error)
+
+ super.onPlayerError(error)
+ }
+
+ override fun onRenderedFirstFrame() {
+ updatedTime()
+ if (!hasUsedFirstRender) { // this insures that we only call this once per player load
+ Log.i(TAG, "Rendered first frame")
+ setPreferredSubtitles(currentSubtitles)
+ hasUsedFirstRender = true
+ val format = exoPlayer?.videoFormat
+ val width = format?.width
+ val height = format?.height
+ if (height != null && width != null) {
+ playerDimensionsLoaded?.invoke(Pair(width, height))
+ updatedTime()
+ exoPlayer?.apply {
+ requestedListeningPercentages?.forEach { percentage ->
+ createMessage { _, _ ->
+ updatedTime()
+ }
+ .setLooper(Looper.getMainLooper())
+ .setPosition( /* positionMs= */contentDuration * percentage / 100)
+ // .setPayload(customPayloadData)
+ .setDeleteAfterDelivery(false)
+ .send()
+ }
+ }
+ }
+ }
+ super.onRenderedFirstFrame()
+ }
+ })
+ } catch (e: Exception) {
+ Log.e(TAG, "loadExo error", e)
+ playerError?.invoke(e)
+ }
+ }
+
+ private fun loadOfflinePlayer(context: Context, data: ExtractorUri) {
+ Log.i(TAG, "loadOfflinePlayer")
+ try {
+ currentDownloadedFile = data
+
+ val mediaItem = getMediaItem(MimeTypes.VIDEO_MP4, data.uri)
+ val offlineSourceFactory = context.createOfflineSource()
+ val (subSources, activeSubtitles) = getSubSources(
+ offlineSourceFactory,
+ offlineSourceFactory,
+ subtitleHelper
+ )
+ subtitleHelper.setActiveSubtitles(activeSubtitles.toSet())
+ loadExo(context, mediaItem, subSources)
+ } catch (e: Exception) {
+ Log.e(TAG, "loadOfflinePlayer error", e)
+ playerError?.invoke(e)
+ }
+ }
+
+ private fun loadOnlinePlayer(context: Context, link: ExtractorLink) {
+ Log.i(TAG, "loadOnlinePlayer")
+ try {
+ currentLink = link
+
+ if (ignoreSSL) {
+ // Disables ssl check
+ val sslContext: SSLContext = SSLContext.getInstance("TLS")
+ sslContext.init(null, arrayOf(SSLTrustManager()), java.security.SecureRandom())
+ sslContext.createSSLEngine()
+ HttpsURLConnection.setDefaultHostnameVerifier { _: String, _: SSLSession ->
+ true
+ }
+ HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
+ }
+
+ val mime = if (link.isM3u8) {
+ MimeTypes.APPLICATION_M3U8
+ } else {
+ MimeTypes.VIDEO_MP4
+ }
+ val mediaItem = getMediaItem(mime, link.url)
+
+ val onlineSourceFactory = createOnlineSource(link)
+ val offlineSourceFactory = context.createOfflineSource()
+
+ val (subSources, activeSubtitles) = getSubSources(
+ onlineSourceFactory,
+ offlineSourceFactory,
+ subtitleHelper
+ )
+ subtitleHelper.setActiveSubtitles(activeSubtitles.toSet())
+
+ simpleCache = getCache(context, cacheSize)
+
+ val cacheFactory = CacheDataSource.Factory().apply {
+ simpleCache?.let { setCache(it) }
+ setUpstreamDataSourceFactory(onlineSourceFactory)
+ }
+
+ loadExo(context, mediaItem, subSources, cacheFactory)
+ } catch (e: Exception) {
+ Log.e(TAG, "loadOnlinePlayer error", e)
+ playerError?.invoke(e)
+ }
+ }
+
+ override fun reloadPlayer(context: Context) {
+ Log.i(TAG, "reloadPlayer")
+
+ exoPlayer?.release()
+ currentLink?.let {
+ loadOnlinePlayer(context, it)
+ } ?: currentDownloadedFile?.let {
+ loadOfflinePlayer(context, it)
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..70bdd81f
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt
@@ -0,0 +1,88 @@
+package com.lagradost.cloudstream3.ui.player
+
+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.ExtractorUri
+import com.lagradost.cloudstream3.utils.VideoDownloadManager
+import kotlin.math.max
+import kotlin.math.min
+
+class DownloadFileGenerator(
+ private val episodes: List,
+ private var currentIndex: Int = 0
+) : IGenerator {
+ override val hasCache = false
+
+ override fun hasNext(): Boolean {
+ return currentIndex < episodes.size - 1
+ }
+
+ override fun hasPrev(): Boolean {
+ return currentIndex > 0
+ }
+
+ override fun next() {
+ if (hasNext())
+ currentIndex++
+ }
+
+ override fun prev() {
+ if (hasPrev())
+ currentIndex--
+ }
+
+ override fun goto(index: Int) {
+ // clamps value
+ currentIndex = min(episodes.size - 1, max(0, index))
+ }
+
+ override fun getCurrentId(): Int? {
+ return episodes[currentIndex].id
+ }
+
+ override fun getCurrent(): Any {
+ return episodes[currentIndex]
+ }
+
+ override fun generateLinks(
+ clearCache: Boolean,
+ isCasting: Boolean,
+ callback: (Pair) -> Unit,
+ subtitleCallback: (SubtitleData) -> Unit
+ ): Boolean {
+ val meta = episodes[currentIndex]
+ callback(Pair(null, meta))
+
+ context?.let { ctx ->
+ val relative = meta.relativePath
+ val display = meta.displayName
+
+ if (display == null || relative == null) {
+ return@let
+ }
+ VideoDownloadManager.getFolder(ctx, relative, meta.basePath)
+ ?.forEach { file ->
+ val name = display.removeSuffix(".mp4")
+ if (file.first != meta.displayName && file.first.startsWith(name)) {
+ val realName = file.first.removePrefix(name)
+ .removeSuffix(".vtt")
+ .removeSuffix(".srt")
+ .removeSuffix(".txt")
+
+ subtitleCallback(
+ SubtitleData(
+ realName.ifBlank { ctx.getString(R.string.default_subtitles) },
+ file.second.toString(),
+ SubtitleOrigin.DOWNLOADED_FILE,
+ name.toSubtitleMimeType()
+ )
+ )
+ }
+ }
+ }
+
+ return true
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..709911d8
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt
@@ -0,0 +1,88 @@
+package com.lagradost.cloudstream3.ui.player
+
+import android.os.Bundle
+import android.util.Log
+import android.view.KeyEvent
+import androidx.appcompat.app.AppCompatActivity
+import com.lagradost.cloudstream3.CommonActivity
+import com.lagradost.cloudstream3.R
+import com.lagradost.cloudstream3.utils.AppUtils
+import com.lagradost.cloudstream3.utils.AppUtils.getUri
+import com.lagradost.cloudstream3.utils.ExtractorUri
+import com.lagradost.cloudstream3.utils.UIHelper.navigate
+import java.io.File
+
+const val DTAG = "PlayerActivity"
+
+class DownloadedPlayerActivity : AppCompatActivity() {
+ override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
+ CommonActivity.dispatchKeyEvent(this, event)?.let {
+ return it
+ }
+ return super.dispatchKeyEvent(event)
+ }
+
+ override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+ CommonActivity.onKeyDown(this, keyCode, event)
+
+ return super.onKeyDown(keyCode, event)
+ }
+
+ override fun onUserLeaveHint() {
+ super.onUserLeaveHint()
+ CommonActivity.onUserLeaveHint(this)
+ }
+
+ override fun onBackPressed() {
+ finish()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ Log.i(DTAG, "onCreate")
+
+ CommonActivity.loadThemes(this)
+ super.onCreate(savedInstanceState)
+ CommonActivity.init(this)
+
+ val data = intent.data
+ if (data == null) {
+ finish()
+ return
+ }
+ val uri = getUri(intent.data)
+ if (uri == null) {
+ finish()
+ return
+ }
+ val path = uri.path
+ // Because it doesn't get the path when it's downloaded, I have no idea
+ val realPath = if (File(
+ intent.data?.path?.removePrefix("/file") ?: "NONE"
+ ).exists()
+ ) intent.data?.path?.removePrefix("/file") else path
+
+ if (realPath == null) {
+ finish()
+ return
+ }
+
+ val name = try {
+ File(realPath).name
+ } catch (e: Exception) {
+ "NULL"
+ }
+
+ val realUri = AppUtils.getVideoContentUri(this, realPath)
+ val tryUri = realUri ?: uri
+
+ setContentView(R.layout.empty_layout)
+ Log.i(DTAG, "navigating")
+
+ //TODO add relative path for subs
+ this.navigate(
+ R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
+ DownloadFileGenerator(listOf(ExtractorUri(uri = tryUri, name = name)))
+ )
+ )
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..d595e875
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt
@@ -0,0 +1,1059 @@
+package com.lagradost.cloudstream3.ui.player
+
+import android.animation.ObjectAnimator
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.content.res.ColorStateList
+import android.content.res.Resources
+import android.graphics.Color
+import android.media.AudioManager
+import android.os.Bundle
+import android.provider.Settings
+import android.util.DisplayMetrics
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.View
+import android.view.animation.AlphaAnimation
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
+import androidx.core.graphics.blue
+import androidx.core.graphics.green
+import androidx.core.graphics.red
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
+import androidx.preference.PreferenceManager
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
+import com.lagradost.cloudstream3.CommonActivity.keyEventListener
+import com.lagradost.cloudstream3.CommonActivity.playerEventListener
+import com.lagradost.cloudstream3.R
+import com.lagradost.cloudstream3.mvvm.logError
+import com.lagradost.cloudstream3.utils.Qualities
+import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
+import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
+import com.lagradost.cloudstream3.utils.UIHelper.getNavigationBarHeight
+import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight
+import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
+import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
+import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI
+import com.lagradost.cloudstream3.utils.UIHelper.toPx
+import com.lagradost.cloudstream3.utils.Vector2
+import kotlinx.android.synthetic.main.fragment_player.*
+import kotlinx.android.synthetic.main.player_custom_layout.*
+import kotlin.math.*
+
+const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking
+const val MINIMUM_VERTICAL_SWIPE = 2.0f // in percentage
+const val MINIMUM_HORIZONTAL_SWIPE = 2.0f // in percentage
+const val VERTICAL_MULTIPLIER = 2.0f
+const val HORIZONTAL_MULTIPLIER = 2.0f
+const val DOUBLE_TAB_MAXIMUM_HOLD_TIME = 200L
+const val DOUBLE_TAB_MINIMUM_TIME_BETWEEN = 200L // this also affects the UI show response time
+const val DOUBLE_TAB_PAUSE_PERCENTAGE = 0.15 // in both directions
+
+// All the UI Logic for the player
+open class FullScreenPlayer : AbstractPlayerFragment(R.layout.fragment_player) {
+ // state of player UI
+ protected var isShowing = false
+ protected var isLocked = false
+
+ // options for player
+ protected var currentPrefQuality =
+ Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell
+ protected var fastForwardTime = 10000L
+ protected var swipeHorizontalEnabled = false
+ protected var swipeVerticalEnabled = false
+ protected var playBackSpeedEnabled = false
+ protected var playerResizeEnabled = false
+ protected var doubleTapEnabled = false
+ protected var doubleTapPauseEnabled = true
+
+ //private var useSystemBrightness = false
+ protected var useTrueSystemBrightness = true
+
+ protected val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics
+
+ // screenWidth and screenHeight does always
+ // refer to the screen while in landscape mode
+ private val screenWidth: Int
+ get() {
+ return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
+ }
+ private val screenHeight: Int
+ get() {
+ return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
+ }
+
+ private var statusBarHeight: Int? = null
+ private var navigationBarHeight: Int? = null
+
+ private val brightnessIcons = listOf(
+ R.drawable.sun_1,
+ R.drawable.sun_2,
+ R.drawable.sun_3,
+ R.drawable.sun_4,
+ R.drawable.sun_5,
+ R.drawable.sun_6,
+ //R.drawable.sun_7,
+ // R.drawable.ic_baseline_brightness_1_24,
+ // R.drawable.ic_baseline_brightness_2_24,
+ // R.drawable.ic_baseline_brightness_3_24,
+ // R.drawable.ic_baseline_brightness_4_24,
+ // R.drawable.ic_baseline_brightness_5_24,
+ // R.drawable.ic_baseline_brightness_6_24,
+ // R.drawable.ic_baseline_brightness_7_24,
+ )
+
+ private val volumeIcons = listOf(
+ R.drawable.ic_baseline_volume_mute_24,
+ R.drawable.ic_baseline_volume_down_24,
+ R.drawable.ic_baseline_volume_up_24,
+ )
+
+ open fun showMirrorsDialogue() {
+ throw NotImplementedError()
+ }
+
+ /** Returns false if the touch is on the status bar or navigation bar*/
+ private fun isValidTouch(rawX: Float, rawY: Float): Boolean {
+ val statusHeight = statusBarHeight ?: 0
+ val navHeight = navigationBarHeight ?: 0
+ return rawY > statusHeight && rawX < screenWidth - navHeight
+ }
+
+ private fun animateLayoutChanges() {
+ if (isShowing) {
+ updateUIVisibility()
+ } else {
+ player_holder.postDelayed({ updateUIVisibility() }, 200)
+ }
+
+ val titleMove = if (isShowing) 0f else -50.toPx.toFloat()
+ player_video_title?.let {
+ ObjectAnimator.ofFloat(it, "translationY", titleMove).apply {
+ duration = 200
+ start()
+ }
+ }
+ player_video_title_rez?.let {
+ ObjectAnimator.ofFloat(it, "translationY", titleMove).apply {
+ duration = 200
+ start()
+ }
+ }
+ val playerBarMove = if (isShowing) 0f else 50.toPx.toFloat()
+ bottom_player_bar?.let {
+ ObjectAnimator.ofFloat(it, "translationY", playerBarMove).apply {
+ duration = 200
+ start()
+ }
+ }
+
+ val fadeTo = if (isShowing) 1f else 0f
+ val fadeAnimation = AlphaAnimation(1f - fadeTo, fadeTo)
+
+ fadeAnimation.duration = 100
+ fadeAnimation.fillAfter = true
+
+ val sView = subView
+ val sStyle = subStyle
+ if (sView != null && sStyle != null) {
+ val move = if (isShowing) -((bottom_player_bar?.height?.toFloat()
+ ?: 0f) + 40.toPx) else -sStyle.elevation.toPx.toFloat()
+ ObjectAnimator.ofFloat(sView, "translationY", move).apply {
+ duration = 200
+ start()
+ }
+ }
+
+ if (!isLocked) {
+ player_ffwd_holder?.alpha = 1f
+ player_rew_holder?.alpha = 1f
+ // player_pause_play_holder?.alpha = 1f
+
+ shadow_overlay?.startAnimation(fadeAnimation)
+ player_ffwd_holder?.startAnimation(fadeAnimation)
+ player_rew_holder?.startAnimation(fadeAnimation)
+ player_pause_play?.startAnimation(fadeAnimation)
+
+ /*if (isBuffering) {
+ player_pause_play?.isVisible = false
+ player_pause_play_holder?.isVisible = false
+ } else {
+ player_pause_play?.isVisible = true
+ player_pause_play_holder?.startAnimation(fadeAnimation)
+ player_pause_play?.startAnimation(fadeAnimation)
+ }*/
+ //player_buffering?.startAnimation(fadeAnimation)
+ }
+
+ bottom_player_bar?.startAnimation(fadeAnimation)
+ player_top_holder?.startAnimation(fadeAnimation)
+ }
+
+ override fun onResume() {
+ activity?.hideSystemUI()
+ activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
+ super.onResume()
+ }
+
+ override fun onDestroy() {
+ activity?.showSystemUI()
+ activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
+ super.onDestroy()
+ }
+
+ private fun setPlayBackSpeed(speed: Float) {
+ try {
+ setKey(PLAYBACK_SPEED_KEY, speed)
+ playback_speed_btt?.text =
+ getString(R.string.player_speed_text_format).format(speed)
+ .replace(".0x", "x")
+ } catch (e: Exception) {
+ // the format string was wrong
+ logError(e)
+ }
+
+ player.setPlaybackSpeed(speed)
+ }
+
+ private fun skipOp() {
+ player.seekTime(85000) // skip 85s
+ }
+
+ private fun showSpeedDialog() {
+ val speedsText =
+ listOf(
+ "0.5x",
+ "0.75x",
+ "0.85x",
+ "1x",
+ "1.15x",
+ "1.25x",
+ "1.4x",
+ "1.5x",
+ "1.75x",
+ "2x"
+ )
+ val speedsNumbers =
+ listOf(0.5f, 0.75f, 0.85f, 1f, 1.15f, 1.25f, 1.4f, 1.5f, 1.75f, 2f)
+ val speedIndex = speedsNumbers.indexOf(player.getPlaybackSpeed())
+
+ activity?.let { act ->
+ act.showDialog(
+ speedsText,
+ speedIndex,
+ act.getString(R.string.player_speed),
+ false,
+ {
+ activity?.hideSystemUI()
+ }) { index ->
+ activity?.hideSystemUI()
+ setPlayBackSpeed(speedsNumbers[index])
+ }
+ }
+ }
+
+ fun resetRewindText() {
+ exo_rew_text?.text =
+ getString(R.string.rew_text_regular_format).format(fastForwardTime / 1000)
+ }
+
+ fun resetFastForwardText() {
+ exo_ffwd_text?.text =
+ getString(R.string.ffw_text_regular_format).format(fastForwardTime / 1000)
+ }
+
+ private fun rewind() {
+ try {
+ player_center_menu?.isGone = false
+ player_ffwd_holder?.alpha = 1f
+
+ val rotateLeft = AnimationUtils.loadAnimation(context, R.anim.rotate_left)
+ exo_rew?.startAnimation(rotateLeft)
+
+ val goLeft = AnimationUtils.loadAnimation(context, R.anim.go_left)
+ goLeft.setAnimationListener(object : Animation.AnimationListener {
+ override fun onAnimationStart(animation: Animation?) {}
+
+ override fun onAnimationRepeat(animation: Animation?) {}
+
+ override fun onAnimationEnd(animation: Animation?) {
+ exo_rew_text?.post {
+ resetRewindText()
+ player_center_menu?.isGone = !isShowing
+ player_ffwd_holder?.alpha = if (isShowing) 1f else 0f
+ }
+ }
+ })
+ exo_rew_text?.startAnimation(goLeft)
+ exo_rew_text?.text = getString(R.string.rew_text_format).format(fastForwardTime / 1000)
+ player.seekTime(-fastForwardTime)
+ } catch (e: Exception) {
+ logError(e)
+ }
+ }
+
+ private fun fastForward() {
+ try {
+ player_center_menu?.isGone = false
+ player_ffwd_holder?.alpha = 1f
+ val rotateRight = AnimationUtils.loadAnimation(context, R.anim.rotate_right)
+ exo_ffwd?.startAnimation(rotateRight)
+
+ val goRight = AnimationUtils.loadAnimation(context, R.anim.go_right)
+ goRight.setAnimationListener(object : Animation.AnimationListener {
+ override fun onAnimationStart(animation: Animation?) {}
+
+ override fun onAnimationRepeat(animation: Animation?) {}
+
+ override fun onAnimationEnd(animation: Animation?) {
+ exo_ffwd_text?.post {
+ resetFastForwardText()
+ player_center_menu?.isGone = !isShowing
+ player_ffwd_holder?.alpha = if (isShowing) 1f else 0f
+ }
+ }
+ })
+ exo_ffwd_text?.startAnimation(goRight)
+ exo_ffwd_text?.text = getString(R.string.ffw_text_format).format(fastForwardTime / 1000)
+ player.seekTime(fastForwardTime)
+ } catch (e: Exception) {
+ logError(e)
+ }
+ }
+
+ private fun onClickChange() {
+ isShowing = !isShowing
+ if (isShowing) {
+ autoHide()
+ }
+ activity?.hideSystemUI()
+ animateLayoutChanges()
+ }
+
+ private fun toggleLock() {
+ if (!isShowing) {
+ onClickChange()
+ }
+
+ isLocked = !isLocked
+ if (isLocked && isShowing) {
+ player_holder?.postDelayed({
+ if (isLocked && isShowing) {
+ onClickChange()
+ }
+ }, 200)
+ }
+
+ val fadeTo = if (isLocked) 0f else 1f
+
+ val fadeAnimation = AlphaAnimation(player_video_title.alpha, fadeTo).apply {
+ duration = 100
+ fillAfter = true
+ }
+
+ updateUIVisibility()
+ // MENUS
+ //centerMenu.startAnimation(fadeAnimation)
+ player_pause_play?.startAnimation(fadeAnimation)
+ player_ffwd_holder?.startAnimation(fadeAnimation)
+ player_rew_holder?.startAnimation(fadeAnimation)
+ //player_media_route_button?.startAnimation(fadeAnimation)
+ //video_bar.startAnimation(fadeAnimation)
+
+ //TITLE
+ player_video_title_rez?.startAnimation(fadeAnimation)
+ player_video_title?.startAnimation(fadeAnimation)
+
+ // BOTTOM
+ player_lock_holder?.startAnimation(fadeAnimation)
+ player_go_back_holder?.startAnimation(fadeAnimation)
+
+ shadow_overlay?.startAnimation(fadeAnimation)
+
+ updateLockUI()
+ }
+
+ private fun updateUIVisibility() {
+ val isGone = isLocked || !isShowing
+ player_lock_holder?.isGone = isGone
+ player_video_bar?.isGone = isGone
+ player_pause_play_holder?.isGone = isGone
+ player_pause_play?.isGone = isGone
+ //player_buffering?.isGone = isGone
+ player_top_holder?.isGone = isGone
+ player_center_menu?.isGone = isGone
+ player_lock?.isGone = !isShowing
+ //player_media_route_button?.isClickable = !isGone
+ player_go_back_holder?.isGone = isGone
+ }
+
+ private fun updateLockUI() {
+ player_lock?.setIconResource(if (isLocked) R.drawable.video_locked else R.drawable.video_unlocked)
+ val color = if (isLocked) context?.colorFromAttribute(R.attr.colorPrimary)
+ else Color.WHITE
+ if (color != null) {
+ player_lock?.setTextColor(color)
+ player_lock?.iconTint = ColorStateList.valueOf(color)
+ player_lock?.rippleColor =
+ ColorStateList.valueOf(Color.argb(50, color.red, color.green, color.blue))
+ }
+ }
+
+ private var currentTapIndex = 0
+ protected fun autoHide() {
+ currentTapIndex++
+ val index = currentTapIndex
+ player_holder?.postDelayed({
+ if (!isCurrentTouchValid && isShowing && index == currentTapIndex && player.getIsPlaying()) {
+ onClickChange()
+ }
+ }, 2000)
+ }
+
+ // this is used because you don't want to hide UI when double tap seeking
+ private var currentDoubleTapIndex = 0
+ private fun toggleShowDelayed() {
+ if (doubleTapEnabled) {
+ val index = currentDoubleTapIndex
+ player_holder?.postDelayed({
+ if (index == currentDoubleTapIndex) {
+ onClickChange()
+ }
+ }, DOUBLE_TAB_MINIMUM_TIME_BETWEEN)
+ } else {
+ onClickChange()
+ }
+ }
+
+ private var isCurrentTouchValid = false
+ private var currentTouchStart: Vector2? = null
+ private var currentTouchLast: Vector2? = null
+ private var currentTouchAction: TouchAction? = null
+ private var currentLastTouchAction: TouchAction? = null
+ private var currentTouchStartPlayerTime: Long? =
+ null // the time in the player when you first click
+ private var currentTouchStartTime: Long? = null // the system time when you first click
+ private var currentLastTouchEndTime: Long = 0 // the system time when you released your finger
+ private var currentClickCount: Int =
+ 0 // amount of times you have double clicked, will reset when other action is taken
+
+ // requested volume and brightness is used to make swiping smoother
+ // to make it not jump between values,
+ // this value is within the range [0,1]
+ private var currentRequestedVolume: Float = 0.0f
+ private var currentRequestedBrightness: Float = 1.0f
+
+ enum class TouchAction {
+ Brightness,
+ Volume,
+ Time,
+ }
+
+ companion object {
+ private fun forceLetters(inp: Long, letters: Int = 2): String {
+ val added: Int = letters - inp.toString().length
+ return if (added > 0) {
+ "0".repeat(added) + inp.toString()
+ } else {
+ inp.toString()
+ }
+ }
+
+ private fun convertTimeToString(sec: Long): String {
+ val rsec = sec % 60L
+ val min = ceil((sec - rsec) / 60.0).toInt()
+ val rmin = min % 60L
+ val h = ceil((min - rmin) / 60.0).toLong()
+ //int rh = h;// h % 24;
+ return (if (h > 0) forceLetters(h) + ":" else "") + (if (rmin >= 0 || h >= 0) forceLetters(
+ rmin
+ ) + ":" else "") + forceLetters(
+ rsec
+ )
+ }
+ }
+
+ private fun calculateNewTime(
+ startTime: Long?,
+ touchStart: Vector2?,
+ touchEnd: Vector2?
+ ): Long? {
+ if (touchStart == null || touchEnd == null || startTime == null) return null
+ val diffX = (touchEnd.x - touchStart.x) * HORIZONTAL_MULTIPLIER / screenWidth.toFloat()
+ val duration = player.getDuration() ?: return null
+ return max(
+ min(
+ startTime + ((duration * (diffX * diffX)) * (if (diffX < 0) -1 else 1)).toLong(),
+ duration
+ ), 0
+ )
+ }
+
+ private fun getBrightness(): Float? {
+ return if (useTrueSystemBrightness) {
+ try {
+ Settings.System.getInt(
+ context?.contentResolver,
+ Settings.System.SCREEN_BRIGHTNESS
+ ) / 255f
+ } catch (e: Exception) {
+ // because true system brightness requires
+ // permission, this is a lazy way to check
+ // as it will throw an error if we do not have it
+ useTrueSystemBrightness = false
+ return getBrightness()
+ }
+ } else {
+ try {
+ activity?.window?.attributes?.screenBrightness
+ } catch (e: Exception) {
+ logError(e)
+ null
+ }
+ }
+ }
+
+ private fun setBrightness(brightness: Float) {
+ if (useTrueSystemBrightness) {
+ try {
+ Settings.System.putInt(
+ context?.contentResolver,
+ Settings.System.SCREEN_BRIGHTNESS_MODE,
+ Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL
+ )
+
+ Settings.System.putInt(
+ context?.contentResolver,
+ Settings.System.SCREEN_BRIGHTNESS, (brightness * 255).toInt()
+ )
+ } catch (e: Exception) {
+ useTrueSystemBrightness = false
+ setBrightness(brightness)
+ }
+ } else {
+ try {
+ val lp = activity?.window?.attributes
+ lp?.screenBrightness = brightness
+ activity?.window?.attributes = lp
+ } catch (e: Exception) {
+ logError(e)
+ }
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun handleMotionEvent(view: View?, event: MotionEvent?): Boolean {
+ if (event == null || view == null) return false
+ val currentTouch = Vector2(event.x, event.y)
+ val startTouch = currentTouchStart
+
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ // validates if the touch is inside of the player area
+ isCurrentTouchValid = isValidTouch(currentTouch.x, currentTouch.y)
+ if (isCurrentTouchValid) {
+ currentTouchStartTime = System.currentTimeMillis()
+ currentTouchStart = currentTouch
+ currentTouchLast = currentTouch
+ currentTouchStartPlayerTime = player.getPosition()
+
+ getBrightness()?.let {
+ currentRequestedBrightness = it
+ }
+ (activity?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager)?.let { audioManager ->
+ val currentVolume =
+ audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
+ val maxVolume =
+ audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
+
+ currentRequestedVolume = currentVolume.toFloat() / maxVolume.toFloat()
+ }
+ }
+ }
+ MotionEvent.ACTION_UP -> {
+ if (isCurrentTouchValid && !isLocked) {
+ // seek time
+ if (swipeHorizontalEnabled && currentTouchAction == TouchAction.Time) {
+ val startTime = currentTouchStartPlayerTime
+ if (startTime != null) {
+ calculateNewTime(startTime, startTouch, currentTouch)?.let { seekTo ->
+ if (abs(seekTo - startTime) > MINIMUM_SEEK_TIME) {
+ player.seekTo(seekTo)
+ }
+ }
+ }
+ }
+ }
+
+ // see if click is eligible for seek 10s
+ val holdTime = currentTouchStartTime?.minus(System.currentTimeMillis())
+ if (isCurrentTouchValid // is valid
+ && currentTouchAction == null // no other action like swiping is taking place
+ && currentLastTouchAction == null // last action was none, this prevents mis input random seek
+ && holdTime != null
+ && holdTime < DOUBLE_TAB_MAXIMUM_HOLD_TIME // it is a click not a long hold
+ ) {
+ if (doubleTapEnabled
+ && !isLocked
+ && (System.currentTimeMillis() - currentLastTouchEndTime) < DOUBLE_TAB_MINIMUM_TIME_BETWEEN // the time since the last action is short
+ ) {
+ currentClickCount++
+
+ if (currentClickCount >= 1) { // have double clicked
+ currentDoubleTapIndex++
+ if (doubleTapPauseEnabled) { // you can pause if your tap is in the middle of the screen
+ when {
+ currentTouch.x < screenWidth / 2 - (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> {
+ rewind()
+ }
+ currentTouch.x > screenWidth / 2 + (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> {
+ fastForward()
+ }
+ else -> {
+ player.handleEvent(CSPlayerEvent.PlayPauseToggle)
+ }
+ }
+ } else {
+ if (currentTouch.x < screenWidth / 2) {
+ rewind()
+ } else {
+ fastForward()
+ }
+ }
+ }
+ } else {
+ // is a valid click but not fast enough for seek
+ currentClickCount = 0
+ toggleShowDelayed()
+ //onClickChange()
+ }
+ } else {
+ currentClickCount = 0
+ }
+
+ // call auto hide as it wont hide when you have your finger down
+ autoHide()
+
+ // reset variables
+ isCurrentTouchValid = false
+ currentTouchStart = null
+ currentLastTouchAction = currentTouchAction
+ currentTouchAction = null
+ currentTouchStartPlayerTime = null
+ currentTouchLast = null
+ currentTouchStartTime = null
+
+ // resets UI
+ player_time_text?.isVisible = false
+ player_progressbar_left_holder?.isVisible = false
+ player_progressbar_right_holder?.isVisible = false
+ currentLastTouchEndTime = System.currentTimeMillis()
+ }
+ MotionEvent.ACTION_MOVE -> {
+ // if current touch is valid
+ if (startTouch != null && isCurrentTouchValid && !isLocked) {
+ // action is unassigned and can therefore be assigned
+ if (currentTouchAction == null) {
+ val diffFromStart = startTouch - currentTouch
+
+ if (swipeVerticalEnabled) {
+ if (abs(diffFromStart.y * 100 / screenHeight) > MINIMUM_VERTICAL_SWIPE) {
+ // left = Brightness, right = Volume, but the UI is reversed to show the UI better
+ currentTouchAction = if (startTouch.x < screenWidth / 2) {
+ // hide the UI if you hold brightness to show screen better, better UX
+ if (isShowing) {
+ isShowing = false
+ animateLayoutChanges()
+ }
+
+ TouchAction.Brightness
+ } else {
+ TouchAction.Volume
+ }
+ }
+ }
+ if (swipeHorizontalEnabled) {
+ if (abs(diffFromStart.x * 100 / screenHeight) > MINIMUM_HORIZONTAL_SWIPE) {
+ currentTouchAction = TouchAction.Time
+ }
+ }
+ }
+
+ // display action
+ val lastTouch = currentTouchLast
+ if (lastTouch != null) {
+ val diffFromLast = lastTouch - currentTouch
+ val verticalAddition =
+ diffFromLast.y * VERTICAL_MULTIPLIER / screenHeight.toFloat()
+
+ // update UI
+ player_time_text?.isVisible = false
+ player_progressbar_left_holder?.isVisible = false
+ player_progressbar_right_holder?.isVisible = false
+
+ when (currentTouchAction) {
+ TouchAction.Time -> {
+ // this simply updates UI as the seek logic happens on release
+ // startTime is rounded to make the UI sync in a nice way
+ val startTime =
+ currentTouchStartPlayerTime?.div(1000L)?.times(1000L)
+ if (startTime != null) {
+ calculateNewTime(
+ startTime,
+ startTouch,
+ currentTouch
+ )?.let { newMs ->
+ val skipMs = newMs - startTime
+ player_time_text?.text =
+ "${convertTimeToString(newMs / 1000)} [${
+ (if (abs(skipMs) < 1000) "" else (if (skipMs > 0) "+" else "-"))
+ }${convertTimeToString(abs(skipMs / 1000))}]"
+ player_time_text?.isVisible = true
+ }
+ }
+ }
+ TouchAction.Brightness -> {
+ player_progressbar_right_holder?.isVisible = true
+ val lastRequested = currentRequestedBrightness
+ currentRequestedBrightness =
+ min(
+ 1.0f,
+ max(currentRequestedBrightness + verticalAddition, 0.0f)
+ )
+
+ // this is to not spam request it, just in case it fucks over someone
+ if (lastRequested != currentRequestedBrightness)
+ setBrightness(currentRequestedBrightness)
+
+ // max is set high to make it smooth
+ player_progressbar_right?.max = 100_000
+ player_progressbar_right?.progress =
+ max(2_000, (currentRequestedBrightness * 100_000f).toInt())
+
+ player_progressbar_right_icon?.setImageResource(
+ brightnessIcons[min( // clamp the value just in case
+ brightnessIcons.size - 1,
+ max(
+ 0,
+ round(currentRequestedBrightness * (brightnessIcons.size - 1)).toInt()
+ )
+ )]
+ )
+ }
+ TouchAction.Volume -> {
+ (activity?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager)?.let { audioManager ->
+ player_progressbar_left_holder?.isVisible = true
+ val maxVolume =
+ audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
+ val currentVolume =
+ audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
+
+ // clamps volume and adds swipe
+ currentRequestedVolume =
+ min(
+ 1.0f,
+ max(currentRequestedVolume + verticalAddition, 0.0f)
+ )
+
+ // max is set high to make it smooth
+ player_progressbar_left?.max = 100_000
+ player_progressbar_left?.progress =
+ max(2_000, (currentRequestedVolume * 100_000f).toInt())
+
+ player_progressbar_left_icon?.setImageResource(
+ volumeIcons[min( // clamp the value just in case
+ volumeIcons.size - 1,
+ max(
+ 0,
+ round(currentRequestedVolume * (volumeIcons.size - 1)).toInt()
+ )
+ )]
+ )
+
+ // this is used instead of set volume because old devices does not support it
+ val desiredVolume =
+ round(currentRequestedVolume * maxVolume).toInt()
+ if (desiredVolume != currentVolume) {
+ val newVolumeAdjusted =
+ if (desiredVolume < currentVolume) AudioManager.ADJUST_LOWER else AudioManager.ADJUST_RAISE
+
+ audioManager.adjustStreamVolume(
+ AudioManager.STREAM_MUSIC,
+ newVolumeAdjusted,
+ 0
+ )
+ }
+ }
+ }
+ else -> Unit
+ }
+ }
+ }
+ }
+ }
+ currentTouchLast = currentTouch
+ return true
+ }
+
+ private fun handleKeyEvent(event: KeyEvent): Boolean {
+ event.keyCode.let { keyCode ->
+ when (event.action) {
+ KeyEvent.ACTION_DOWN -> {
+ when (keyCode) {
+ KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_DPAD_UP -> {
+ if (!isShowing) {
+ onClickChange()
+ return true
+ }
+ }
+ }
+
+ //println("Keycode: $keyCode")
+ //showToast(
+ // this,
+ // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
+ // Toast.LENGTH_LONG
+ //)
+ }
+ }
+
+ when (keyCode) {
+ // don't allow dpad move when hidden
+ KeyEvent.KEYCODE_DPAD_LEFT,
+ KeyEvent.KEYCODE_DPAD_DOWN,
+ KeyEvent.KEYCODE_DPAD_UP,
+ KeyEvent.KEYCODE_DPAD_RIGHT,
+ KeyEvent.KEYCODE_DPAD_DOWN_LEFT,
+ KeyEvent.KEYCODE_DPAD_DOWN_RIGHT,
+ KeyEvent.KEYCODE_DPAD_UP_LEFT,
+ KeyEvent.KEYCODE_DPAD_UP_RIGHT -> {
+ if (!isShowing) {
+ return true
+ } else {
+ autoHide()
+ }
+ }
+
+ // netflix capture back and hide ~monke
+ KeyEvent.KEYCODE_BACK -> {
+ if (isShowing) {
+ onClickChange()
+ return true
+ }
+ }
+ }
+ }
+
+ return false
+ }
+
+ protected fun uiReset() {
+ isLocked = false
+ isShowing = false
+
+ // if nothing has loaded these buttons should not be visible
+ player_skip_episode?.isVisible = false
+ player_skip_op?.isVisible = false
+
+ updateLockUI()
+ updateUIVisibility()
+ animateLayoutChanges()
+ resetFastForwardText()
+ resetRewindText()
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ // init variables
+ setPlayBackSpeed(getKey(PLAYBACK_SPEED_KEY) ?: 1.0f)
+ fastForwardTime = getKey(PLAYBACK_FASTFORWARD) ?: 10000L
+
+ // handle tv controls
+ playerEventListener = { eventType ->
+ when (eventType) {
+ PlayerEventType.Lock -> {
+ toggleLock()
+ }
+ PlayerEventType.NextEpisode -> {
+ player.handleEvent(CSPlayerEvent.NextEpisode)
+ }
+ PlayerEventType.Pause -> {
+ player.handleEvent(CSPlayerEvent.Pause)
+ }
+ PlayerEventType.PlayPauseToggle -> {
+ player.handleEvent(CSPlayerEvent.PlayPauseToggle)
+ }
+ PlayerEventType.Play -> {
+ player.handleEvent(CSPlayerEvent.Play)
+ }
+ PlayerEventType.Resize -> {
+ nextResize()
+ }
+ PlayerEventType.PrevEpisode -> {
+ player.handleEvent(CSPlayerEvent.PrevEpisode)
+ }
+ PlayerEventType.SeekForward -> {
+ player.handleEvent(CSPlayerEvent.SeekForward)
+ }
+ PlayerEventType.ShowSpeed -> {
+ showSpeedDialog()
+ }
+ PlayerEventType.SeekBack -> {
+ player.handleEvent(CSPlayerEvent.SeekBack)
+ }
+ PlayerEventType.ToggleMute -> {
+ player.handleEvent(CSPlayerEvent.ToggleMute)
+ }
+ PlayerEventType.ToggleHide -> {
+ onClickChange()
+ }
+ PlayerEventType.ShowMirrors -> {
+ showMirrorsDialogue()
+ }
+ }
+ }
+
+ // handle tv controls directly based on player state
+ keyEventListener = { keyEvent ->
+ if (keyEvent != null) {
+ handleKeyEvent(keyEvent)
+ } else {
+ false
+ }
+ }
+
+ try {
+ val settingsManager = PreferenceManager.getDefaultSharedPreferences(activity)
+ context?.let { ctx ->
+ navigationBarHeight = ctx.getNavigationBarHeight()
+ statusBarHeight = ctx.getStatusBarHeight()
+
+ swipeHorizontalEnabled =
+ settingsManager.getBoolean(ctx.getString(R.string.swipe_enabled_key), true)
+ swipeVerticalEnabled =
+ settingsManager.getBoolean(
+ ctx.getString(R.string.swipe_vertical_enabled_key),
+ true
+ )
+ playBackSpeedEnabled = settingsManager.getBoolean(
+ ctx.getString(R.string.playback_speed_enabled_key),
+ false
+ )
+ playerResizeEnabled =
+ settingsManager.getBoolean(
+ ctx.getString(R.string.player_resize_enabled_key),
+ true
+ )
+ doubleTapEnabled =
+ settingsManager.getBoolean(
+ ctx.getString(R.string.double_tap_enabled_key),
+ false
+ )
+
+ doubleTapPauseEnabled =
+ settingsManager.getBoolean(
+ ctx.getString(R.string.double_tap_pause_enabled_key),
+ false
+ )
+
+ currentPrefQuality = settingsManager.getInt(
+ ctx.getString(R.string.quality_pref_key),
+ currentPrefQuality
+ )
+ // useSystemBrightness =
+ // settingsManager.getBoolean(ctx.getString(R.string.use_system_brightness_key), false)
+ }
+ } catch (e: Exception) {
+ logError(e)
+ }
+
+ player_pause_play?.setOnClickListener {
+ autoHide()
+ player.handleEvent(CSPlayerEvent.PlayPauseToggle)
+ }
+
+ // init clicks
+ player_resize_btt?.setOnClickListener {
+ autoHide()
+ nextResize()
+ }
+
+ playback_speed_btt?.setOnClickListener {
+ autoHide()
+ showSpeedDialog()
+ }
+
+ player_skip_op?.setOnClickListener {
+ autoHide()
+ skipOp()
+ }
+
+ player_skip_episode?.setOnClickListener {
+ autoHide()
+ player.handleEvent(CSPlayerEvent.NextEpisode)
+ }
+
+ player_lock?.setOnClickListener {
+ autoHide()
+ toggleLock()
+ }
+
+ exo_rew?.setOnClickListener {
+ autoHide()
+ rewind()
+ }
+
+ exo_ffwd?.setOnClickListener {
+ autoHide()
+ fastForward()
+ }
+
+ player_go_back?.setOnClickListener {
+ activity?.popCurrentPage()
+ }
+
+ player_sources_btt?.setOnClickListener {
+ showMirrorsDialogue()
+ }
+
+ // it is !not! a bug that you cant touch the right side, it does not register inputs on navbar or status bar
+ player_holder?.setOnTouchListener { callView, event ->
+ return@setOnTouchListener handleMotionEvent(callView, event)
+ }
+
+ // init UI
+ try {
+ uiReset()
+
+ // init chromecast UI
+ // removed due to having no use and bugging
+ //activity?.let {
+ // if (it.isCastApiAvailable()) {
+ // try {
+ // CastButtonFactory.setUpMediaRouteButton(it, player_media_route_button)
+ // val castContext = CastContext.getSharedInstance(it.applicationContext)
+ //
+ // player_media_route_button?.isGone =
+ // castContext.castState == CastState.NO_DEVICES_AVAILABLE
+ // castContext.addCastStateListener { state ->
+ // player_media_route_button?.isGone =
+ // state == CastState.NO_DEVICES_AVAILABLE
+ // }
+ // } catch (e: Exception) {
+ // logError(e)
+ // }
+ // } else {
+ // // if cast is not possible hide UI
+ // player_media_route_button?.isGone = true
+ // }
+ //}
+ } catch (e: Exception) {
+ logError(e)
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..168bdfbf
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt
@@ -0,0 +1,490 @@
+package com.lagradost.cloudstream3.ui.player
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.*
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
+import androidx.lifecycle.ViewModelProvider
+import com.google.android.material.button.MaterialButton
+import com.hippo.unifile.UniFile
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.CommonActivity.showToast
+import com.lagradost.cloudstream3.mvvm.Resource
+import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
+import com.lagradost.cloudstream3.mvvm.observe
+import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
+import com.lagradost.cloudstream3.ui.result.ResultEpisode
+import com.lagradost.cloudstream3.utils.DataStoreHelper
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.ExtractorUri
+import com.lagradost.cloudstream3.utils.Qualities
+import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
+import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
+import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
+import kotlinx.android.synthetic.main.fragment_player.*
+import kotlinx.android.synthetic.main.player_custom_layout.*
+
+// TODO Auto select subtitles
+class GeneratorPlayer : FullScreenPlayer() {
+ companion object {
+ private var lastUsedGenerator: IGenerator? = null
+ fun newInstance(generator: IGenerator): Bundle {
+ lastUsedGenerator = generator
+ return Bundle()
+ }
+ }
+
+ private lateinit var viewModel: PlayerGeneratorViewModel //by activityViewModels()
+ private var currentLinks: Set> = setOf()
+ private var currentSubs: Set = setOf()
+
+ private var currentSelectedLink: Pair? = null
+ private var currentSelectedSubtitles: SubtitleData? = null
+ private var currentMeta: Any? = null
+ private var nextMeta: Any? = null
+ private var isActive: Boolean = false
+ private var isNextEpisode: Boolean = false // this is used to reset the watch time
+
+ private fun startLoading() {
+ player.release()
+ currentSelectedSubtitles = null
+ isActive = false
+ overlay_loading_skip_button?.isVisible = false
+ player_loading_overlay?.isVisible = true
+ }
+
+ private fun setSubtitles(sub: SubtitleData?): Boolean {
+ currentSelectedSubtitles = sub
+ return player.setPreferredSubtitles(sub)
+ }
+
+ private fun noSubtitles(): Boolean {
+ return setSubtitles(null)
+ }
+
+ private fun loadLink(link: Pair?, sameEpisode: Boolean) {
+ if (link == null) return
+
+ // manage UI
+ player_loading_overlay?.isVisible = false
+ uiReset()
+ currentSelectedLink = link
+ currentMeta = viewModel.getMeta()
+ nextMeta = viewModel.getNextMeta()
+ isActive = true
+ setPlayerDimen(null)
+ setTitle()
+
+ // load player
+ context?.let { ctx ->
+ val (url, uri) = link
+ player.loadPlayer(
+ ctx,
+ sameEpisode,
+ url,
+ uri,
+ startPosition = if (sameEpisode) null else {
+ if (isNextEpisode) 0L else (DataStoreHelper.getViewPos(viewModel.getId())?.position
+ ?: 0L)
+ },
+ currentSubs,
+ )
+ }
+ }
+
+ private fun sortLinks(useQualitySettings: Boolean = true): List> {
+ return currentLinks.sortedBy {
+ val (linkData, _) = it
+ var quality = linkData?.quality ?: Qualities.Unknown.value
+
+ // we set all qualities above current max as max -1
+ if (useQualitySettings && quality > currentPrefQuality) {
+ quality = currentPrefQuality - 1
+ }
+ // negative because we want to sort highest quality first
+ -(quality)
+ }
+ }
+
+ private fun openSubPicker() {
+ subsPathPicker.launch(
+ arrayOf(
+ "text/vtt",
+ "application/x-subrip",
+ "text/plain",
+ "text/str",
+ "application/octet-stream"
+ )
+ )
+ }
+
+ // Open file picker
+ private val subsPathPicker =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
+ normalSafeApiCall {
+ // It lies, it can be null if file manager quits.
+ if (uri == null) return@normalSafeApiCall
+ val ctx = context ?: AcraApplication.context ?: return@normalSafeApiCall
+ // RW perms for the path
+ val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+
+ ctx.contentResolver.takePersistableUriPermission(uri, flags)
+
+ val file = UniFile.fromUri(ctx, uri)
+ println("Loaded subtitle file. Selected URI path: $uri - Name: ${file.name}")
+ // DO NOT REMOVE THE FILE EXTENSION FROM NAME, IT'S NEEDED FOR MIME TYPES
+ val name = file.name ?: uri.toString()
+
+ val subtitleData = SubtitleData(
+ name,
+ uri.toString(),
+ SubtitleOrigin.DOWNLOADED_FILE,
+ name.toSubtitleMimeType()
+ )
+
+ setSubtitles(subtitleData)
+
+ // this is used instead of observe, because observe is too slow
+ val subs = currentSubs.toMutableSet()
+ subs.add(subtitleData)
+ player.setActiveSubtitles(subs)
+ player.reloadPlayer(ctx)
+
+ viewModel.addSubtitles(setOf(subtitleData))
+
+ selectSourceDialog?.dismissSafe()
+
+ showToast(
+ activity,
+ String.format(ctx.getString(R.string.player_loaded_subtitles), name),
+ Toast.LENGTH_LONG
+ )
+ }
+ }
+
+ var selectSourceDialog: AlertDialog? = null
+ override fun showMirrorsDialogue() {
+ currentSelectedSubtitles = player.getCurrentPreferredSubtitle()
+ context?.let { ctx ->
+ val isPlaying = player.getIsPlaying()
+ player.handleEvent(CSPlayerEvent.Pause)
+ val currentSubtitles = sortSubs(currentSubs)
+
+ val sourceBuilder = AlertDialog.Builder(ctx, R.style.AlertDialogCustomBlack)
+ .setView(R.layout.player_select_source_and_subs)
+
+ val sourceDialog = sourceBuilder.create()
+ selectSourceDialog = sourceDialog
+ sourceDialog.show()
+ val providerList =
+ sourceDialog.findViewById(R.id.sort_providers)!!
+ val subtitleList =
+ sourceDialog.findViewById(R.id.sort_subtitles)!!
+ val applyButton =
+ sourceDialog.findViewById(R.id.apply_btt)!!
+ val cancelButton =
+ sourceDialog.findViewById(R.id.cancel_btt)!!
+
+ val footer: TextView =
+ layoutInflater.inflate(R.layout.sort_bottom_footer_add_choice, null) as TextView
+ footer.text = ctx.getString(R.string.player_load_subtitles)
+ footer.setOnClickListener {
+ openSubPicker()
+ }
+ subtitleList.addFooterView(footer)
+
+ var sourceIndex = 0
+ var startSource = 0
+
+ val sortedUrls = sortLinks(useQualitySettings = false)
+ if (sortedUrls.isNullOrEmpty()) {
+ sourceDialog.findViewById(R.id.sort_sources_holder)?.isGone = true
+ } else {
+ startSource = sortedUrls.indexOf(currentSelectedLink)
+ sourceIndex = startSource
+
+ val sourcesArrayAdapter =
+ ArrayAdapter(ctx, R.layout.sort_bottom_single_choice)
+
+ sourcesArrayAdapter.addAll(sortedUrls.map {
+ it.first?.name ?: it.second?.name ?: "NULL"
+ })
+
+ providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
+ providerList.adapter = sourcesArrayAdapter
+ providerList.setSelection(sourceIndex)
+ providerList.setItemChecked(sourceIndex, true)
+
+ providerList.setOnItemClickListener { _, _, which, _ ->
+ sourceIndex = which
+ providerList.setItemChecked(which, true)
+ }
+ }
+
+ sourceDialog.setOnDismissListener {
+ if (isPlaying) {
+ player.handleEvent(CSPlayerEvent.Play)
+ }
+ activity?.hideSystemUI()
+ selectSourceDialog = null
+ }
+
+ val subtitleIndexStart = currentSubtitles.indexOf(currentSelectedSubtitles) + 1
+ var subtitleIndex = subtitleIndexStart
+
+ val subsArrayAdapter =
+ ArrayAdapter(ctx, R.layout.sort_bottom_single_choice)
+ subsArrayAdapter.add(getString(R.string.no_subtitles))
+ subsArrayAdapter.addAll(currentSubtitles.map { it.name })
+
+ subtitleList.adapter = subsArrayAdapter
+ subtitleList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
+
+ subtitleList.setSelection(subtitleIndex)
+ subtitleList.setItemChecked(subtitleIndex, true)
+
+ subtitleList.setOnItemClickListener { _, _, which, _ ->
+ subtitleIndex = which
+ subtitleList.setItemChecked(which, true)
+ }
+
+ cancelButton.setOnClickListener {
+ sourceDialog.dismissSafe(activity)
+ }
+
+ applyButton.setOnClickListener {
+ var init = false
+ if (sourceIndex != startSource) {
+ init = true
+ }
+ if (subtitleIndex != subtitleIndexStart) {
+ init = init || if (subtitleIndex <= 0) {
+ noSubtitles()
+ } else {
+ setSubtitles(currentSubtitles[subtitleIndex - 1])
+ }
+ }
+ if (init) {
+ loadLink(sortedUrls[sourceIndex], true)
+ }
+ sourceDialog.dismissSafe(activity)
+ }
+ }
+ }
+
+ private fun noLinksFound() {
+ showToast(activity, R.string.no_links_found_toast, Toast.LENGTH_SHORT)
+ activity?.popCurrentPage()
+ }
+
+ private fun startPlayer() {
+ if (isActive) return // we don't want double load when you skip loading
+
+ val links = sortLinks()
+ if (links.isEmpty()) {
+ noLinksFound()
+ return
+ }
+ loadLink(links.first(), false)
+ }
+
+ override fun nextEpisode() {
+ isNextEpisode = true
+ viewModel.loadLinksNext()
+ }
+
+ override fun prevEpisode() {
+ isNextEpisode = true
+ viewModel.loadLinksPrev()
+ }
+
+ override fun nextMirror() {
+ val links = sortLinks()
+ if (links.isEmpty()) {
+ noLinksFound()
+ return
+ }
+
+ val newIndex = links.indexOf(currentSelectedLink) + 1
+ if (newIndex >= links.size) {
+ noLinksFound()
+ return
+ }
+
+ loadLink(links[newIndex], true)
+ }
+
+ override fun playerPositionChanged(posDur: Pair) {
+ val (position, duration) = posDur
+ viewModel.getId()?.let {
+ DataStoreHelper.setViewPos(it, position, duration)
+ }
+ val percentage = position * 100L / duration
+
+ val nextEp = percentage >= NEXT_WATCH_EPISODE_PERCENTAGE
+ val resumeMeta = if (nextEp) nextMeta else currentMeta
+ if (resumeMeta == null && nextEp) {
+ // remove last watched as it is the last episode and you have watched too much
+ when (val newMeta = currentMeta) {
+ is ResultEpisode -> {
+ DataStoreHelper.removeLastWatched(newMeta.parentId)
+ }
+ is ExtractorUri -> {
+ DataStoreHelper.removeLastWatched(newMeta.parentId)
+ }
+ }
+ } else {
+ // save resume
+ when (resumeMeta) {
+ is ResultEpisode -> {
+ DataStoreHelper.setLastWatched(
+ resumeMeta.parentId,
+ resumeMeta.id,
+ resumeMeta.episode,
+ resumeMeta.season,
+ isFromDownload = false
+ )
+ }
+ is ExtractorUri -> {
+ DataStoreHelper.setLastWatched(
+ resumeMeta.parentId,
+ resumeMeta.id,
+ resumeMeta.episode,
+ resumeMeta.season,
+ isFromDownload = true
+ )
+ }
+ }
+ }
+
+ var isOpVisible = false
+ when (val meta = currentMeta) {
+ is ResultEpisode -> {
+ if (meta.tvType.isAnimeOp())
+ isOpVisible = percentage < SKIP_OP_VIDEO_PERCENTAGE
+ }
+ }
+ player_skip_op?.isVisible = isOpVisible
+ player_skip_episode?.isVisible = !isOpVisible && viewModel.hasNextEpisode() == true
+
+ if (percentage > PRELOAD_NEXT_EPISODE_PERCENTAGE) {
+ viewModel.preLoadNextLinks()
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ fun setTitle() {
+ var headerName: String? = null
+ var episode: Int? = null
+ var season: Int? = null
+ var tvType: TvType? = null
+
+ when (val meta = currentMeta) {
+ is ResultEpisode -> {
+ headerName = meta.headerName
+ episode = meta.episode
+ season = meta.season
+ tvType = meta.tvType
+ }
+ is ExtractorUri -> {
+ headerName = meta.headerName
+ episode = meta.episode
+ season = meta.season
+ tvType = meta.tvType
+ }
+ }
+
+ player_video_title?.text = if (headerName != null) {
+ headerName +
+ if (tvType.isEpisodeBased() && episode != null)
+ if (season == null)
+ " - ${getString(R.string.episode)} $episode"
+ else
+ " \"${getString(R.string.season_short)}${season}:${getString(R.string.episode_short)}${episode}\""
+ else ""
+ } else {
+ ""
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ fun setPlayerDimen(widthHeight: Pair?) {
+ val extra = if (widthHeight != null) {
+ val (width, height) = widthHeight
+ " - ${width}x${height}"
+ } else {
+ ""
+ }
+ player_video_title_rez?.text =
+ (currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name
+ ?: "NULL") + extra
+ }
+
+ override fun playerDimensionsLoaded(widthHeight: Pair) {
+ setPlayerDimen(widthHeight)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java]
+ viewModel.attachGenerator(lastUsedGenerator)
+ return super.onCreateView(inflater, container, savedInstanceState)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ if (currentSelectedLink == null) {
+ viewModel.loadLinks()
+ }
+
+ overlay_loading_skip_button?.setOnClickListener {
+ startPlayer()
+ }
+
+ player_loading_go_back?.setOnClickListener {
+ player.release()
+ activity?.popCurrentPage()
+ }
+
+ observe(viewModel.loadingLinks) {
+ when (it) {
+ is Resource.Loading -> {
+ startLoading()
+ }
+ is Resource.Success -> {
+ // provider returned false
+ //if (it.value != true) {
+ // showToast(activity, R.string.unexpected_error, Toast.LENGTH_SHORT)
+ //}
+ startPlayer()
+ }
+ is Resource.Failure -> {
+ showToast(activity, it.errorString, Toast.LENGTH_LONG)
+ startPlayer()
+ }
+ }
+ }
+
+ observe(viewModel.currentLinks) {
+ currentLinks = it
+ overlay_loading_skip_button?.isVisible = it.isNotEmpty()
+ }
+
+ observe(viewModel.currentSubs) {
+ currentSubs = it
+ player.setActiveSubtitles(it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt
new file mode 100644
index 00000000..145f9a41
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt
@@ -0,0 +1,25 @@
+package com.lagradost.cloudstream3.ui.player
+
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.ExtractorUri
+
+interface IGenerator {
+ val hasCache: Boolean
+
+ fun hasNext(): Boolean
+ fun hasPrev(): Boolean
+ fun next()
+ fun prev()
+ fun goto(index: Int)
+
+ fun getCurrentId(): Int? // this is used to save data or read data about this id
+ fun getCurrent(): Any? // this is used to get metadata about the current playing, can return null
+
+ /* not safe, must use try catch */
+ fun generateLinks(
+ clearCache: Boolean,
+ isCasting: Boolean,
+ callback: (Pair) -> Unit,
+ subtitleCallback: (SubtitleData) -> Unit
+ ): Boolean
+}
\ 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
new file mode 100644
index 00000000..8a99f065
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt
@@ -0,0 +1,107 @@
+package com.lagradost.cloudstream3.ui.player
+
+import android.content.Context
+import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.ExtractorUri
+
+enum class PlayerEventType(val value: Int) {
+ //Stop(-1),
+ Pause(0),
+ Play(1),
+ SeekForward(2),
+ SeekBack(3),
+
+ //SkipCurrentChapter(4),
+ NextEpisode(5),
+ PrevEpisode(5),
+ PlayPauseToggle(7),
+ ToggleMute(8),
+ Lock(9),
+ ToggleHide(10),
+ ShowSpeed(11),
+ ShowMirrors(12),
+ Resize(13),
+}
+
+enum class CSPlayerEvent(val value: Int) {
+ Pause(0),
+ Play(1),
+ SeekForward(2),
+ SeekBack(3),
+
+ //SkipCurrentChapter(4),
+ NextEpisode(5),
+ PrevEpisode(6),
+ PlayPauseToggle(7),
+ ToggleMute(8),
+}
+
+enum class CSPlayerLoading {
+ IsPaused,
+ IsPlaying,
+ IsBuffering,
+ //IsDone,
+}
+
+//http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
+const val STATE_RESUME_WINDOW = "resumeWindow"
+const val STATE_RESUME_POSITION = "resumePosition"
+const val STATE_PLAYER_FULLSCREEN = "playerFullscreen"
+const val STATE_PLAYER_PLAYING = "playerOnPlay"
+const val ACTION_MEDIA_CONTROL = "media_control"
+const val EXTRA_CONTROL_TYPE = "control_type"
+const val PLAYBACK_SPEED = "playback_speed"
+const val RESIZE_MODE_KEY = "resize_mode" // Last used resize mode
+const val PLAYBACK_SPEED_KEY = "playback_speed" // Last used playback speed
+const val PREFERRED_SUBS_KEY = "preferred_subtitles" // Last used resize mode
+const val PLAYBACK_FASTFORWARD = "playback_fastforward" // Last used resize mode
+
+/** Abstract Exoplayer logic, can be expanded to other players */
+interface IPlayer {
+ fun getPlaybackSpeed(): Float
+ fun setPlaybackSpeed(speed: Float)
+
+ fun getIsPlaying(): Boolean
+ fun getDuration(): Long?
+ fun getPosition(): Long?
+
+ fun seekTime(time: Long)
+ fun seekTo(time: Long)
+
+ fun initCallbacks(
+ playerUpdated: (Any?) -> Unit, // attach player to view
+ updateIsPlaying: ((Pair) -> Unit)? = null, // (wasPlaying, isPlaying)
+ requestAutoFocus: (() -> Unit)? = null, // current player starts, asking for all other programs to shut the fuck up
+ playerError: ((Exception) -> Unit)? = null, // player error when rendering or misc, used to display toast or log
+ playerDimensionsLoaded: ((Pair) -> Unit)? = null, // (with, height), for UI
+ requestedListeningPercentages: List? = null, // this is used to request when the player should report back view percentage
+ playerPositionChanged: ((Pair) -> Unit)? = null,// (position, duration) this is used to update UI based of the current time
+ nextEpisode: (() -> Unit)? = null, // this is used by the player to load the next episode
+ prevEpisode: (() -> Unit)? = null, // this is used by the player to load the previous episode
+ )
+
+ fun updateSubtitleStyle(style: SaveCaptionStyle)
+ fun loadPlayer(
+ context: Context,
+ sameEpisode: Boolean,
+ link: ExtractorLink? = null,
+ data: ExtractorUri? = null,
+ startPosition: Long? = null,
+ subtitles : Set,
+ )
+
+ fun reloadPlayer(context: Context)
+
+ fun setActiveSubtitles(subtitles : Set)
+ fun setPreferredSubtitles(subtitle : SubtitleData?) : Boolean // returns true if the player requires a reload, null for nothing
+ fun getCurrentPreferredSubtitle() : SubtitleData?
+
+ fun handleEvent(event: CSPlayerEvent)
+
+ fun onStop()
+ fun onPause()
+ fun onResume(context: Context)
+
+ fun release()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt
deleted file mode 100644
index 7aa1733b..00000000
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt
+++ /dev/null
@@ -1,2608 +0,0 @@
-package com.lagradost.cloudstream3.ui.player
-
-import android.animation.ObjectAnimator
-import android.annotation.SuppressLint
-import android.app.Activity
-import android.app.PendingIntent
-import android.app.PictureInPictureParams
-import android.app.RemoteAction
-import android.content.*
-import android.content.Context.AUDIO_SERVICE
-import android.content.pm.ActivityInfo
-import android.content.res.ColorStateList
-import android.content.res.Resources
-import android.database.ContentObserver
-import android.graphics.Color
-import android.graphics.drawable.Icon
-import android.media.AudioManager
-import android.media.metrics.PlaybackErrorEvent
-import android.net.Uri
-import android.os.*
-import android.provider.Settings
-import android.util.TypedValue
-import android.view.*
-import android.view.View.*
-import android.view.WindowManager.LayoutParams.*
-import android.view.animation.AccelerateInterpolator
-import android.view.animation.AlphaAnimation
-import android.view.animation.Animation
-import android.view.animation.AnimationUtils
-import android.widget.*
-import android.widget.Toast.LENGTH_SHORT
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AlertDialog
-import androidx.core.graphics.blue
-import androidx.core.graphics.green
-import androidx.core.graphics.red
-import androidx.core.view.isVisible
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.preference.PreferenceManager
-import androidx.transition.Fade
-import androidx.transition.Transition
-import androidx.transition.TransitionManager
-import com.fasterxml.jackson.databind.DeserializationFeature
-import com.fasterxml.jackson.databind.json.JsonMapper
-import com.fasterxml.jackson.module.kotlin.KotlinModule
-import com.fasterxml.jackson.module.kotlin.readValue
-import com.google.android.exoplayer2.*
-import com.google.android.exoplayer2.C.TIME_UNSET
-import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
-import com.google.android.exoplayer2.source.*
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
-import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
-import com.google.android.exoplayer2.ui.SubtitleView
-import com.google.android.exoplayer2.upstream.DataSource
-import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
-import com.google.android.exoplayer2.upstream.cache.CacheDataSource
-import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
-import com.google.android.exoplayer2.upstream.cache.SimpleCache
-import com.google.android.exoplayer2.util.MimeTypes
-import com.google.android.exoplayer2.util.Util
-import com.google.android.gms.cast.framework.CastButtonFactory
-import com.google.android.gms.cast.framework.CastContext
-import com.google.android.gms.cast.framework.CastState
-import com.google.android.material.button.MaterialButton
-import com.hippo.unifile.UniFile
-import com.lagradost.cloudstream3.*
-import com.lagradost.cloudstream3.MainActivity.Companion.canEnterPipMode
-import com.lagradost.cloudstream3.MainActivity.Companion.getCastSession
-import com.lagradost.cloudstream3.MainActivity.Companion.isInPIPMode
-import com.lagradost.cloudstream3.MainActivity.Companion.showToast
-import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.mvvm.*
-import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.ui.result.ResultViewModel
-import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
-import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
-import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.fromSaveToStyle
-import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
-import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getCurrentSavedStyle
-import com.lagradost.cloudstream3.utils.*
-import com.lagradost.cloudstream3.utils.AppUtils.getFocusRequest
-import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
-import com.lagradost.cloudstream3.utils.AppUtils.onAudioFocusEvent
-import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus
-import com.lagradost.cloudstream3.utils.CastHelper.startCast
-import com.lagradost.cloudstream3.utils.DataStore.getKey
-import com.lagradost.cloudstream3.utils.DataStore.setKey
-import com.lagradost.cloudstream3.utils.DataStoreHelper.setLastWatched
-import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
-import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
-import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
-import com.lagradost.cloudstream3.utils.UIHelper.getNavigationBarHeight
-import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight
-import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
-import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
-import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
-import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI
-import com.lagradost.cloudstream3.utils.UIHelper.toPx
-import com.lagradost.cloudstream3.utils.VideoDownloadManager.getId
-import kotlinx.android.synthetic.main.fragment_player.*
-import kotlinx.android.synthetic.main.player_custom_layout.*
-import kotlinx.coroutines.*
-import okhttp3.internal.format
-import java.io.File
-import javax.net.ssl.HttpsURLConnection
-import javax.net.ssl.SSLContext
-import javax.net.ssl.SSLSession
-import kotlin.math.abs
-import kotlin.math.ceil
-import kotlin.math.max
-import kotlin.math.min
-import kotlin.properties.Delegates
-
-
-//http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
-const val STATE_RESUME_WINDOW = "resumeWindow"
-const val STATE_RESUME_POSITION = "resumePosition"
-const val STATE_PLAYER_FULLSCREEN = "playerFullscreen"
-const val STATE_PLAYER_PLAYING = "playerOnPlay"
-const val ACTION_MEDIA_CONTROL = "media_control"
-const val EXTRA_CONTROL_TYPE = "control_type"
-const val PLAYBACK_SPEED = "playback_speed"
-const val RESIZE_MODE_KEY = "resize_mode" // Last used resize mode
-const val PLAYBACK_SPEED_KEY = "playback_speed" // Last used playback speed
-const val PREFERRED_SUBS_KEY = "preferred_subtitles" // Last used resize mode
-
-const val OPENING_PERCENTAGE = 50
-const val AUTOLOAD_NEXT_EPISODE_PERCENTAGE = 80
-
-enum class PlayerEventType(val value: Int) {
- //Stop(-1),
- Pause(0),
- Play(1),
- SeekForward(2),
- SeekBack(3),
-
- //SkipCurrentChapter(4),
- NextEpisode(5),
- PrevEpisode(5),
- PlayPauseToggle(7),
- ToggleMute(8),
- Lock(9),
- ToggleHide(10),
- ShowSpeed(11),
- ShowMirrors(12),
- Resize(13),
-}
-
-/*
-data class PlayerData(
- val id: Int, // UNIQUE IDENTIFIER, USED FOR SET TIME, HASH OF slug+episodeIndex
- val titleName: String, // TITLE NAME
- val episodeName: String?, // EPISODE NAME, NULL IF MOVIE
- val episodeIndex: Int?, // EPISODE INDEX, NULL IF MOVIE
- val seasonIndex : Int?, // SEASON INDEX IF IT IS FOUND, EPISODE CAN BE GIVEN BUT SEASON IS NOT GUARANTEED
- val episodes : Int?, // MAX EPISODE
- //val seasons : Int?, // SAME AS SEASON INDEX, NOT GUARANTEED, SET TO 1
-)*/
-data class PlayerData(
- val episodeIndex: Int,
- val seasonIndex: Int?,
- val mirrorId: Int,
-)
-
-data class UriData(
- val uri: String,
- val basePath: String?,
- val relativePath: String,
- val displayName: String,
- val parentId: Int?,
- val id: Int?,
- val name: String,
- val episode: Int?,
- val season: Int?,
-)
-
-// YE, I KNOW, THIS COULD BE HANDLED A LOT BETTER
-class PlayerFragment : Fragment() {
- // ============ TORRENT ============
- //private var torrentStream: TorrentStream? = null
- private var lastTorrentUrl = ""
-
- /**
- * Tracks reported to be used by exoplayer, since sometimes it has a mind of it's own when selecting subs.
- * String = lowercase language as set by .setLanguage("_$langId")
- * Boolean = if it's active
- * */
- var exoPlayerSelectedTracks = listOf>()
-
- //private val isTorrent: Boolean get() = torrentStream != null
- private fun initTorrentStream(torrentUrl: String) {
- if (lastTorrentUrl == torrentUrl) return
- /*lastTorrentUrl = torrentUrl
- torrentStream?.stopStream()
- torrentStream = null
-
- activity?.let { act ->
- val normalPath =
- act.cacheDir.absolutePath // "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath"
- val torrentOptions: TorrentOptions = TorrentOptions.Builder()
- .saveLocation(normalPath)
- .removeFilesAfterStop(true)
- .build()
-
- torrentStream = TorrentStream.init(torrentOptions)
- torrentStream?.startStream(torrentUrl)
- torrentStream?.addListener(object : TorrentListener {
- override fun onStreamPrepared(torrent: Torrent?) {
- showToast(activity, "Stream Prepared", LENGTH_SHORT)
- }
-
- override fun onStreamStarted(torrent: Torrent?) {
- showToast(activity, "Stream Started", LENGTH_SHORT)
- }
-
- override fun onStreamError(torrent: Torrent?, e: java.lang.Exception?) {
- e?.printStackTrace()
- showToast(activity, e?.localizedMessage ?: "Error loading", LENGTH_SHORT)
- }
-
- override fun onStreamReady(torrent: Torrent?) {
- initPlayer(null, null, torrent?.videoFile?.toUri())
- }
-
- @SuppressLint("SetTextI18n")
- override fun onStreamProgress(torrent: Torrent?, status: StreamStatus?) {
- try {
- println("Seeders ${status?.seeds}")
- println("Download Speed ${status?.downloadSpeed}")
- println("Progress ${status?.progress}%")
- if (isShowing)
- player_torrent_info?.visibility = VISIBLE
- video_torrent_progress?.text =
- "${"%.1f".format(status?.progress ?: 0f)}% at ${status?.downloadSpeed?.div(1000) ?: 0} kb/s"
- video_torrent_seeders?.text = "${status?.seeds ?: 0} Seeders"
- //streamSeeds.formatText(R.string.streamSeeds, status?.seeds)
- //streamSpeed.formatText(R.string.streamDownloadSpeed, status?.downloadSpeed?.div(1024))
- //streamProgressTxt.formatText(R.string.streamProgress, status?.progress, "%")
- } catch (e: IllegalStateException) {
- e.printStackTrace()
- }
- }
-
- override fun onStreamStopped() {
- println("stream stopped")
- }
- })
- }*/
- }
-
- // =================================
-
- private lateinit var subStyle: SaveCaptionStyle
- private var subView: SubtitleView? = null
-
- private var isCurrentlyPlaying: Boolean = false
- private val mapper = JsonMapper.builder().addModule(KotlinModule())
- .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
-
- lateinit var apiName: String
-
- private var isFullscreen = false
- private var isPlayerPlaying = true
- private val viewModel: ResultViewModel by activityViewModels()
- private lateinit var playerData: PlayerData
- private lateinit var uriData: UriData
- private var isDownloadedFile = false
- private var isShowing = true
- private lateinit var exoPlayer: ExoPlayer
-
- //private var currentPercentage = 0
- // private var hasNextEpisode = true
-
- // val formatBuilder = StringBuilder()
- // val formatter = Formatter(formatBuilder, Locale.getDefault())
-
- /** Cache */
- private val cacheSize = 500L * 1024L * 1024L // 500 mb
- private var simpleCache: SimpleCache? = null
-
- /** Layout */
- private var width = Resources.getSystem().displayMetrics.widthPixels
- private var height = Resources.getSystem().displayMetrics.heightPixels
- private var statusBarHeight by Delegates.notNull()
- private var navigationBarHeight by Delegates.notNull()
-
-
- private var isLocked = false
- private lateinit var settingsManager: SharedPreferences
-
- abstract class DoubleClickListener(private val ctx: PlayerFragment) : OnTouchListener {
- // The time in which the second tap should be done in order to qualify as
- // a double click
-
- private var doubleClickQualificationSpanInMillis: Long = 300L
- private var singleClickQualificationSpanInMillis: Long = 300L
- private var timestampLastClick: Long = 0
- private var timestampLastSingleClick: Long = 0
- private var clicksLeft = 0
- private var clicksRight = 0
- private var fingerLeftScreen = true
- abstract fun onDoubleClickRight(clicks: Int)
- abstract fun onDoubleClickLeft(clicks: Int)
- abstract fun onSingleClick()
- abstract fun onMotionEvent(event: MotionEvent)
-
- override fun onTouch(v: View, event: MotionEvent): Boolean {
- onMotionEvent(event)
- if (event.action == MotionEvent.ACTION_UP) {
- fingerLeftScreen = true
- }
- if (event.action == MotionEvent.ACTION_DOWN) {
- fingerLeftScreen = false
-
- if ((SystemClock.elapsedRealtime() - timestampLastClick) < doubleClickQualificationSpanInMillis) {
- if (event.rawX >= max(ctx.width, ctx.height) / 2) {
- clicksRight++
- if (!ctx.isLocked && ctx.doubleTapEnabled) onDoubleClickRight(clicksRight)
- //if (!ctx.isShowing) onSingleClick()
- } else {
- clicksLeft++
- if (!ctx.isLocked && ctx.doubleTapEnabled) onDoubleClickLeft(clicksLeft)
- //if (!ctx.isShowing) onSingleClick()
- }
- } else if (clicksLeft == 0 && clicksRight == 0 && fingerLeftScreen) {
- // onSingleClick()
- // timestampLastSingleClick = SystemClock.elapsedRealtime()
- } else {
- clicksLeft = 0
- clicksRight = 0
- val job = Job()
- val uiScope = CoroutineScope(Dispatchers.Main + job)
-
- fun check() {
- if ((SystemClock.elapsedRealtime() - timestampLastSingleClick) > singleClickQualificationSpanInMillis && (SystemClock.elapsedRealtime() - timestampLastClick) > doubleClickQualificationSpanInMillis) {
- timestampLastSingleClick = SystemClock.elapsedRealtime()
- onSingleClick()
- }
- }
-//ctx.isShowing &&
- if (!ctx.isLocked && ctx.doubleTapEnabled) {
- uiScope.launch {
- delay(doubleClickQualificationSpanInMillis + 1)
- check()
- }
- } else {
- check()
- }
- }
- timestampLastClick = SystemClock.elapsedRealtime()
-
- }
-
- return true
- }
- }
-
- private fun updateClick() {
- click_overlay?.isVisible = !isShowing
-
- val titleMove = if (isShowing) 0f else -50.toPx.toFloat()
- video_title?.let {
- ObjectAnimator.ofFloat(it, "translationY", titleMove).apply {
- duration = 200
- start()
- }
- }
- video_title_rez?.let {
- ObjectAnimator.ofFloat(it, "translationY", titleMove).apply {
- duration = 200
- start()
- }
- }
- val playerBarMove = if (isShowing) 0f else 50.toPx.toFloat()
- bottom_player_bar?.let {
- ObjectAnimator.ofFloat(it, "translationY", playerBarMove).apply {
- duration = 200
- start()
- }
- }
-
- changeSkip()
- val fadeTo = if (isShowing) 1f else 0f
- val fadeAnimation = AlphaAnimation(1f - fadeTo, fadeTo)
-
- fadeAnimation.duration = 100
- fadeAnimation.fillAfter = true
-
- subView?.let { sView ->
- val move = if (isShowing) -((bottom_player_bar?.height?.toFloat()
- ?: 0f) + 10.toPx) else -subStyle.elevation.toPx.toFloat()
- ObjectAnimator.ofFloat(sView, "translationY", move).apply {
- duration = 200
- start()
- }
- }
-
- if (!isLocked) {
- player_ffwd_holder?.alpha = 1f
- player_rew_holder?.alpha = 1f
- player_pause_holder?.alpha = 1f
-
- shadow_overlay?.startAnimation(fadeAnimation)
- player_ffwd_holder?.startAnimation(fadeAnimation)
- player_rew_holder?.startAnimation(fadeAnimation)
- player_pause_holder?.startAnimation(fadeAnimation)
- } else {
- //player_ffwd_holder?.alpha = 0f
- //player_ffwd_holder?.alpha = 0f
- //player_pause_holder?.alpha = 0f
- }
-
- bottom_player_bar?.startAnimation(fadeAnimation)
- player_top_holder?.startAnimation(fadeAnimation)
- // video_holder?.startAnimation(fadeAnimation)
- //player_torrent_info?.isVisible = (isTorrent && isShowing)
- // player_torrent_info?.startAnimation(fadeAnimation)
- //video_lock_holder?.startAnimation(fadeAnimation)
- }
-
- private fun onClickChange() {
- isShowing = !isShowing
- if (isShowing) {
- autoHide()
- }
- activity?.hideSystemUI()
- updateClick()
- }
-
- private fun forceLetters(inp: Int, letters: Int = 2): String {
- val added: Int = letters - inp.toString().length
- return if (added > 0) {
- "0".repeat(added) + inp.toString()
- } else {
- inp.toString()
- }
- }
-
- private fun convertTimeToString(time: Double): String {
- val sec = time.toInt()
- val rsec = sec % 60
- val min = ceil((sec - rsec) / 60.0).toInt()
- val rmin = min % 60
- val h = ceil((min - rmin) / 60.0).toInt()
- //int rh = h;// h % 24;
- return (if (h > 0) forceLetters(h) + ":" else "") + (if (rmin >= 0 || h >= 0) forceLetters(
- rmin
- ) + ":" else "") + forceLetters(
- rsec
- )
- }
-
- private fun skipOP() {
- seekTime(85000L)
- }
-
- private var swipeEnabled = true // {
- // SO YOU CAN PULL DOWN STATUSBAR OR NAVBAR
- if (motionEvent.rawY > statusBarHeight && motionEvent.rawX < max(
- width,
- height
- ) - navigationBarHeight
- ) {
- currentX = motionEvent.rawX
- currentY = motionEvent.rawY
- isValidTouch = true
- //println("DOWN: " + currentX)
- isMovingStartTime = exoPlayer.currentPosition
- } else {
- isValidTouch = false
- }
- }
- MotionEvent.ACTION_MOVE -> {
- if (!isValidTouch) return
- if (swipeVerticalEnabled) {
- val distanceMultiplierY = 2F
- val distanceY = (motionEvent.rawY - currentY) * distanceMultiplierY
- val diffY = distanceY * 2.0 / min(height, width)
-
- // Forces 'smooth' moving preventing a bug where you
- // can make it think it moved half a screen in a frame
-
- if (abs(diffY) >= 0.2 && !hasPassedSkipLimit) {
- hasPassedVerticalSwipeThreshold = true
- preventHorizontalSwipe = true
- }
- if (hasPassedVerticalSwipeThreshold) {
- if (currentX > max(height, width) * 0.5) {
- if (audioManager != null && progressBarLeftHolder != null) {
- val currentVolume =
- audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
- val maxVolume =
- audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
-
- if (progressBarLeftHolder?.alpha ?: 0f <= 0f) {
- cachedVolume = currentVolume.toFloat() / maxVolume.toFloat()
- }
-
- progressBarLeftHolder?.alpha = 1f
- val vol = minOf(
- 1f,
- cachedVolume - diffY.toFloat() * 0.5f
- ) // 0.05f *if (diffY > 0) 1 else -1
- cachedVolume = vol
- //progressBarRight?.progress = ((1f - alpha) * 100).toInt()
-
- progressBarLeft?.max = 100 * 100
- progressBarLeft?.progress = ((vol) * 100 * 100).toInt()
-
- if (audioManager.isVolumeFixed) {
- // Lmao might earrape, we'll see in bug reports
- exoPlayer.volume = minOf(1f, maxOf(vol, 0f))
- } else {
- // audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, vol*, 0)
- val desiredVol = (vol * maxVolume).toInt()
- if (desiredVol != currentVolume) {
- val newVolumeAdjusted =
- if (desiredVol < currentVolume) AudioManager.ADJUST_LOWER else AudioManager.ADJUST_RAISE
-
- audioManager.adjustStreamVolume(
- AudioManager.STREAM_MUSIC,
- newVolumeAdjusted,
- 0
- )
- }
- //audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newVolume, 0)
- }
- currentY = motionEvent.rawY
- }
- } else if (progressBarRightHolder != null) {
- progressBarRightHolder?.alpha = 1f
-
- val alpha = changeBrightness(-diffY.toFloat())
- progressBarRight?.max = 100 * 100
- progressBarRight?.progress = ((1f - alpha) * 100 * 100).toInt()
- currentY = motionEvent.rawY
- }
- }
- }
-
- if (swipeEnabled) {
- val distanceMultiplierX = 2F
- val distanceX = (motionEvent.rawX - currentX) * distanceMultiplierX
- val diffX = distanceX * 2.0 / max(height, width)
- if (abs(diffX - prevDiffX) > 0.5) {
- return
- }
- prevDiffX = diffX
-
- skipTime =
- ((exoPlayer.duration * (diffX * diffX) / 10) * (if (diffX < 0) -1 else 1)).toLong()
- if (isMovingStartTime + skipTime < 0) {
- skipTime = -isMovingStartTime
- } else if (isMovingStartTime + skipTime > exoPlayer.duration) {
- skipTime = exoPlayer.duration - isMovingStartTime
- }
- if ((abs(skipTime) > 3000 || hasPassedSkipLimit) && !preventHorizontalSwipe) {
- hasPassedSkipLimit = true
- val timeString =
- "${convertTimeToString((isMovingStartTime + skipTime) / 1000.0)} [${
- (if (abs(
- skipTime
- ) < 1000
- ) "" else (if (skipTime > 0) "+" else "-"))
- }${
- convertTimeToString(abs(skipTime / 1000.0))
- }]"
- timeText.alpha = 1f
- timeText.text = timeString
- } else {
- timeText.alpha = 0f
- }
- }
- }
- MotionEvent.ACTION_UP -> {
- if (!isValidTouch) return
- isValidTouch = false
- val transition: Transition = Fade()
- transition.duration = 1000
-
- TransitionManager.beginDelayedTransition(player_holder, transition)
-
- if (abs(skipTime) > 7000 && !preventHorizontalSwipe && swipeEnabled) {
- seekTo(skipTime + isMovingStartTime)
- //exoPlayer.seekTo(maxOf(minOf(skipTime + isMovingStartTime, exoPlayer.duration), 0))
- }
- changeSkip()
-
- hasPassedSkipLimit = false
- hasPassedVerticalSwipeThreshold = false
- preventHorizontalSwipe = false
- prevDiffX = 0.0
- skipTime = 0
-
- timeText.animate().alpha(0f).setDuration(200)
- .setInterpolator(AccelerateInterpolator()).start()
- progressBarRightHolder.animate().alpha(0f).setDuration(200)
- .setInterpolator(AccelerateInterpolator()).start()
- progressBarLeftHolder.animate().alpha(0f).setDuration(200)
- .setInterpolator(AccelerateInterpolator()).start()
- //val fadeAnimation = AlphaAnimation(1f, 0f)
- //fadeAnimation.duration = 100
- //fadeAnimation.fillAfter = true
- //progressBarLeftHolder.startAnimation(fadeAnimation)
- //progressBarRightHolder.startAnimation(fadeAnimation)
- //timeText.startAnimation(fadeAnimation)
-
- }
- }
- }
-
- fun changeSkip(position: Long? = null) {
- val data = localData
-
- if (this::exoPlayer.isInitialized && exoPlayer.currentPosition >= 0) {
- val percentage =
- ((position ?: exoPlayer.currentPosition) * 100 / exoPlayer.contentDuration).toInt()
- val hasNext = hasNextEpisode()
-
- if (percentage >= AUTOLOAD_NEXT_EPISODE_PERCENTAGE && hasNext) {
- val ep =
- episodes[playerData.episodeIndex + 1]
-
- if ((allEpisodes[ep.id]?.size ?: 0) <= 0) {
- viewModel.loadEpisode(ep, false) {
- //NOTHING
- }
- }
- }
- val nextEp = percentage >= OPENING_PERCENTAGE
- val isAnime =
- data.isAnimeBased()//(data is AnimeLoadResponse && (data.type == TvType.Anime || data.type == TvType.ONA))
-
- skip_op?.isVisible = (isAnime && !nextEp)
- skip_episode?.isVisible = ((!isAnime || nextEp) && hasNext)
- } else {
- val isAnime = data.isAnimeBased()
-
- if (isAnime) {
- skip_op?.isVisible = true
- skip_episode?.isVisible = false
- } else {
- skip_episode?.isVisible = data.isEpisodeBased()
- skip_op?.isVisible = false
- }
- }
- }
-
- private fun seekTime(time: Long) {
- changeSkip()
- seekTo(exoPlayer.currentPosition + time)
- }
-
- private fun seekTo(time: Long) {
- val correctTime = maxOf(minOf(time, exoPlayer.duration), 0)
- exoPlayer.seekTo(correctTime)
- changeSkip(correctTime)
- }
-
- private var hasUsedFirstRender = false
-
- private fun savePositionInPlayer() {
- if (this::exoPlayer.isInitialized) {
- isPlayerPlaying = exoPlayer.playWhenReady
- playbackPosition = exoPlayer.currentPosition
- currentWindow = exoPlayer.currentWindowIndex
- }
- }
-
- private fun safeReleasePlayer() {
- simpleCache?.release()
- if (this::exoPlayer.isInitialized) {
- exoPlayer.release()
- }
- isCurrentlyPlaying = false
- }
-
- private fun releasePlayer() {
- savePos()
- val alphaAnimation = AlphaAnimation(0f, 1f)
- alphaAnimation.duration = 100
- alphaAnimation.fillAfter = true
- video_go_back_holder_holder?.visibility = VISIBLE
-
- overlay_loading_skip_button?.visibility = VISIBLE
- loading_overlay?.startAnimation(alphaAnimation)
- savePositionInPlayer()
- safeReleasePlayer()
- }
-
- // Open file picker
- private val subsPathPicker =
- registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
- normalSafeApiCall {
- // It lies, it can be null if file manager quits.
- if (uri == null) return@normalSafeApiCall
- val context = context ?: AcraApplication.context ?: return@normalSafeApiCall
- // RW perms for the path
- val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION
-
- context.contentResolver.takePersistableUriPermission(uri, flags)
-
- val file = UniFile.fromUri(context, uri)
- println("Loaded subtitle file. Selected URI path: $uri - Name: ${file.name}")
- // DO NOT REMOVE THE FILE EXTENSION FROM NAME, IT'S NEEDED FOR MIME TYPES
- val name = file.name ?: uri.toString()
-
- // Sets subs manually if downloaded since the viewModel won't exist.
- if (isDownloadedFile && uriData.id != null)
- allEpisodesSubs[uriData.id!!] =
- (allEpisodesSubs[uriData.id]
- ?: hashMapOf()).apply {
- this[name] = SubtitleFile(name, uri.toString())
- }
- else
- viewModel.loadSubtitleFile(
- uri,
- name,
- getEpisode()?.id
- )
-
- setPreferredSubLanguage(name)
- showToast(
- activity,
- String.format(context.getString(R.string.player_loaded_subtitles), name),
- 1000
- )
- }
- }
-
- private class SettingsContentObserver(handler: Handler?, val activity: Activity) :
- ContentObserver(handler) {
- private val audioManager = activity.getSystemService(AUDIO_SERVICE) as? AudioManager
- override fun onChange(selfChange: Boolean) {
- val currentVolume = audioManager?.getStreamVolume(AudioManager.STREAM_MUSIC)
- val maxVolume = audioManager?.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
- val progressBarLeft = activity.findViewById(R.id.progressBarLeft)
- if (currentVolume != null && maxVolume != null) {
- progressBarLeft?.progress = currentVolume * 100 / maxVolume
- }
- }
- }
-
- companion object {
- fun String.toSubtitleMimeType(): String {
- return when {
- // Checks the file name if downloaded.
- startsWith("content://") -> {
- UniFile.fromUri(
- AcraApplication.context,
- Uri.parse(this)
- ).name?.toSubtitleMimeType() ?: MimeTypes.APPLICATION_SUBRIP
- }
- endsWith("vtt", true) -> MimeTypes.TEXT_VTT
- endsWith("srt", true) -> MimeTypes.APPLICATION_SUBRIP
- endsWith("xml", true) || endsWith("ttml", true) -> MimeTypes.APPLICATION_TTML
- else -> MimeTypes.APPLICATION_SUBRIP // TODO get request to see
- }
- }
-
- fun newInstance(data: PlayerData, startPos: Long? = null): Bundle {
- return Bundle().apply {
- //println(data)
- putString("data", mapper.writeValueAsString(data))
- if (startPos != null) {
- putLong(STATE_RESUME_POSITION, startPos)
- }
- }
- }
-
- fun newInstance(uriData: UriData, startPos: Long? = null): Bundle {
- return Bundle().apply {
- //println(data)
- putString("uriData", mapper.writeValueAsString(uriData))
-
- if (startPos != null) {
- putLong(STATE_RESUME_POSITION, startPos)
- }
- }
- }
- }
-
- private fun savePos() {
- if (this::exoPlayer.isInitialized) {
- if (exoPlayer.duration > 0 && exoPlayer.currentPosition > 0) {
- context?.let { ctx ->
- //if (this::viewModel.isInitialized) {
- viewModel.setViewPos(
- if (isDownloadedFile) uriData.id else getEpisode()?.id,
- exoPlayer.currentPosition,
- exoPlayer.duration
- )
- /*} else {
- ctx.setViewPos(
- if (isDownloadedFile) uriData.id else getEpisode()?.id,
- exoPlayer.currentPosition,
- exoPlayer.duration
- )
- }*/
-
- if (isDownloadedFile) {
- setLastWatched(
- uriData.parentId,
- uriData.id,
- uriData.episode,
- uriData.season,
- true
- )
- } else
- viewModel.reloadEpisodes()
- }
- }
- }
- }
-
- private val resizeModes = listOf(
- Pair(AspectRatioFrameLayout.RESIZE_MODE_FIT, R.string.resize_fit),
- Pair(AspectRatioFrameLayout.RESIZE_MODE_FILL, R.string.resize_fill),
- Pair(AspectRatioFrameLayout.RESIZE_MODE_ZOOM, R.string.resize_zoom),
- )
-
- private var localData: LoadResponse? = null
-
- private fun toggleLock() {
- if (!isShowing) {
- onClickChange()
- }
-
- isLocked = !isLocked
- if (isLocked && isShowing) {
- player_pause_holder?.postDelayed({
- if (isLocked && isShowing) {
- onClickChange()
- }
- }, 200)
- }
-
- //if(isShowing) {
- val fadeTo = if (isLocked) 0f else 1f
-
- val fadeAnimation = AlphaAnimation(video_title.alpha, fadeTo)
- fadeAnimation.duration = 100
- // fadeAnimation.startOffset = 100
- fadeAnimation.fillAfter = true
-
- // MENUS
- //centerMenu.startAnimation(fadeAnimation)
- player_pause_holder?.startAnimation(fadeAnimation)
- player_ffwd_holder?.startAnimation(fadeAnimation)
- player_rew_holder?.startAnimation(fadeAnimation)
- player_media_route_button?.startAnimation(fadeAnimation)
- //video_bar.startAnimation(fadeAnimation)
-
- //TITLE
- video_title_rez.startAnimation(fadeAnimation)
- video_title.startAnimation(fadeAnimation)
-
- // BOTTOM
- lock_holder.startAnimation(fadeAnimation)
- video_go_back_holder2.startAnimation(fadeAnimation)
-
- shadow_overlay.startAnimation(fadeAnimation)
- // }
-
- updateLock()
- }
-
- private fun updateLock() {
- lock_player?.setIconResource(if (isLocked) R.drawable.video_locked else R.drawable.video_unlocked)
- var color = if (isLocked) context?.colorFromAttribute(R.attr.colorPrimary)
- else Color.WHITE
- if (color != null) {
- lock_player?.setTextColor(color)
- lock_player?.iconTint = ColorStateList.valueOf(color)
- color = Color.argb(50, color.red, color.green, color.blue)
- lock_player?.rippleColor = ColorStateList.valueOf(color)
- //if(isLocked) {
- // lock_player?.iconTint = ContextCompat.getColorStateList(lock_player.context, R.color.white)
-//
- //} else {
- // lock_player?.iconTint = context?.colorFromAttribute(R.attr.colorPrimary)
- //}
- //lock_player?.setColorFilter(color)
- }
-
- val isClick = !isLocked
-
- exo_play?.isClickable = isClick
- sources_btt?.isClickable = isClick
- exo_pause?.isClickable = isClick
- exo_ffwd?.isClickable = isClick
- exo_rew?.isClickable = isClick
- exo_prev?.isClickable = isClick
- video_go_back?.isClickable = isClick
- exo_progress?.isClickable = isClick
- //next_episode_btt.isClickable = isClick
- playback_speed_btt?.isClickable = isClick
- skip_op?.isClickable = isClick
- skip_episode?.isClickable = isClick
- resize_player?.isClickable = isClick
- exo_progress?.isEnabled = isClick
- player_media_route_button?.isEnabled = isClick
- if (isClick && isShowing) {
- player_pause_holder?.alpha = 1f
- player_rew_holder?.alpha = 1f
- player_ffwd_holder?.alpha = 1f
- }
-
- //video_go_back_holder2.isEnabled = isClick
-
- // Clickable doesn't seem to work on com.google.android.exoplayer2.ui.DefaultTimeBar
- //exo_progress.visibility = if (isLocked) INVISIBLE else VISIBLE
-
-
- //updateClick()
- }
-
- private var resizeMode = 0
- private var playbackSpeed = 0f
- private var allEpisodes: HashMap> = HashMap()
- private var allEpisodesSubs: HashMap> = HashMap()
- private var episodes: List = emptyList()
- var currentPoster: String? = null
- var currentHeaderName: String? = null
- var currentIsMovie: Boolean? = null
-
- //region PIP MODE
- private fun getPen(code: PlayerEventType): PendingIntent {
- return getPen(code.value)
- }
-
- private fun getPen(code: Int): PendingIntent {
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PendingIntent.getBroadcast(
- activity,
- code,
- Intent("media_control").putExtra("control_type", code),
- PendingIntent.FLAG_IMMUTABLE
- )
- } else {
- PendingIntent.getBroadcast(
- activity,
- code,
- Intent("media_control").putExtra("control_type", code),
- 0
- )
- }
- }
-
- @SuppressLint("NewApi")
- private fun getRemoteAction(id: Int, title: String, event: PlayerEventType): RemoteAction {
- return RemoteAction(
- Icon.createWithResource(activity, id),
- title,
- title,
- getPen(event)
- )
- }
-
- @SuppressLint("NewApi")
- private fun updatePIPModeActions() {
- if (!isInPIPMode || !this::exoPlayer.isInitialized) return
-
- val actions: ArrayList = ArrayList()
-
- actions.add(getRemoteAction(R.drawable.go_back_30, "Go Back", PlayerEventType.SeekBack))
-
- if (exoPlayer.isPlaying) {
- actions.add(getRemoteAction(R.drawable.netflix_pause, "Pause", PlayerEventType.Pause))
- } else {
- actions.add(
- getRemoteAction(
- R.drawable.ic_baseline_play_arrow_24,
- "Play",
- PlayerEventType.Play
- )
- )
- }
-
- actions.add(
- getRemoteAction(
- R.drawable.go_forward_30,
- "Go Forward",
- PlayerEventType.SeekForward
- )
- )
- activity?.setPictureInPictureParams(
- PictureInPictureParams.Builder().setActions(actions).build()
- )
- }
-
- var currentTapIndex = 0
-
- private fun autoHide() {
- currentTapIndex++
- val index = currentTapIndex
- player_holder?.postDelayed({
- if (isShowing && index == currentTapIndex && this::exoPlayer.isInitialized && exoPlayer.isPlaying) {
- onClickChange()
- }
- }, 2000)
- }
-
- private var receiver: BroadcastReceiver? = null
- override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
- isInPIPMode = isInPictureInPictureMode
- if (isInPictureInPictureMode) {
- // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
- player_holder.alpha = 0f
- receiver = object : BroadcastReceiver() {
- override fun onReceive(
- context: Context,
- intent: Intent,
- ) {
- if (ACTION_MEDIA_CONTROL != intent.action) {
- return
- }
- handlePlayerEvent(intent.getIntExtra(EXTRA_CONTROL_TYPE, 0))
- }
- }
- val filter = IntentFilter()
- filter.addAction(
- ACTION_MEDIA_CONTROL
- )
- activity?.registerReceiver(receiver, filter)
- updatePIPModeActions()
- } else {
- // Restore the full-screen UI.
- player_holder.alpha = 1f
- receiver?.let {
- activity?.unregisterReceiver(it)
- }
- activity?.hideSystemUI()
- this.view?.let { hideKeyboard(it) }
- }
- }
-
- private fun handlePlayerEvent(event: PlayerEventType) {
- handlePlayerEvent(event.value)
- }
-
- private fun handleKeyEvent(event: KeyEvent): Boolean {
- event.keyCode.let { keyCode ->
- when (event.action) {
- KeyEvent.ACTION_DOWN -> {
- when (keyCode) {
- KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_DPAD_UP -> {
- if (!isShowing) {
- onClickChange()
- return true
- }
- }
- }
-
- //println("Keycode: $keyCode")
- //showToast(
- // this,
- // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
- // Toast.LENGTH_LONG
- //)
- }
- }
-
- when (keyCode) {
- // don't allow dpad move when hidden
- KeyEvent.KEYCODE_DPAD_LEFT,
- KeyEvent.KEYCODE_DPAD_DOWN,
- KeyEvent.KEYCODE_DPAD_UP,
- KeyEvent.KEYCODE_DPAD_RIGHT,
- KeyEvent.KEYCODE_DPAD_DOWN_LEFT,
- KeyEvent.KEYCODE_DPAD_DOWN_RIGHT,
- KeyEvent.KEYCODE_DPAD_UP_LEFT,
- KeyEvent.KEYCODE_DPAD_UP_RIGHT -> {
- if (!isShowing) {
- return true
- } else {
- autoHide()
- }
- }
-
- // netflix capture back and hide ~monke
- KeyEvent.KEYCODE_BACK -> {
- if (isShowing) {
- onClickChange()
- return true
- }
- }
- }
- }
-
- return false
- }
-
- var lastMuteVolume = 0f
-
- private fun handlePlayerEvent(event: Int) {
- if (!this::exoPlayer.isInitialized) return
- try {
- when (event) {
- PlayerEventType.Play.value -> exoPlayer.play()
- PlayerEventType.Pause.value -> exoPlayer.pause()
- PlayerEventType.SeekBack.value -> seekTime(-30000L)
- PlayerEventType.SeekForward.value -> seekTime(30000L)
- PlayerEventType.PlayPauseToggle.value -> {
- if (exoPlayer.isPlaying) {
- exoPlayer.pause()
- } else {
- exoPlayer.play()
- }
- }
- PlayerEventType.NextEpisode.value -> {
- if (hasNextEpisode()) {
- skipToNextEpisode()
- }
- }
- PlayerEventType.PrevEpisode.value -> {
- //TODO PrevEpisode
- }
- PlayerEventType.Lock.value -> {
- toggleLock()
- }
- PlayerEventType.ToggleHide.value -> {
- onClickChange()
- }
- PlayerEventType.ToggleMute.value -> {
- if (exoPlayer.volume <= 0) {
- //is muted
- exoPlayer.volume = lastMuteVolume
- } else {
- // is not muted
- lastMuteVolume = exoPlayer.volume
- exoPlayer.volume = 0f
- }
- }
- PlayerEventType.Resize.value -> {
- resizeMode = (resizeMode + 1) % resizeModes.size
-
- context?.setKey(RESIZE_MODE_KEY, resizeMode)
- player_view?.resizeMode = resizeModes[resizeMode].first
- showToast(activity, resizeModes[resizeMode].second, LENGTH_SHORT)
- }
- PlayerEventType.ShowSpeed.value -> {
- val speedsText =
- listOf(
- "0.5x",
- "0.75x",
- "0.85x",
- "1x",
- "1.15x",
- "1.25x",
- "1.4x",
- "1.5x",
- "1.75x",
- "2x"
- )
- val speedsNumbers =
- listOf(0.5f, 0.75f, 0.85f, 1f, 1.15f, 1.25f, 1.4f, 1.5f, 1.75f, 2f)
- val speedIndex = speedsNumbers.indexOf(playbackSpeed)
-
- activity?.let { act ->
- act.showDialog(
- speedsText,
- speedIndex,
- act.getString(R.string.player_speed),
- false,
- {
- activity?.hideSystemUI()
- }) { index ->
- playbackSpeed = speedsNumbers[index]
- requireContext().setKey(PLAYBACK_SPEED_KEY, playbackSpeed)
- val param = PlaybackParameters(playbackSpeed)
- exoPlayer.playbackParameters = param
- playback_speed_btt?.text =
- getString(R.string.player_speed_text_format).format(playbackSpeed)
- .replace(".0x", "x")
- }
- }
- }
- PlayerEventType.ShowMirrors.value -> {
- if (!this::exoPlayer.isInitialized) return
- context?.let { ctx ->
- //val isPlaying = exoPlayer.isPlaying
- exoPlayer.pause()
- val currentSubtitles =
- context?.getSubs()?.map { it.lang } ?: activeSubtitles
-
- val sourceBuilder = AlertDialog.Builder(ctx, R.style.AlertDialogCustomBlack)
- .setView(R.layout.player_select_source_and_subs)
-
- val sourceDialog = sourceBuilder.create()
- sourceDialog.show()
- // bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet)
- val providerList =
- sourceDialog.findViewById(R.id.sort_providers)!!
- val subtitleList =
- sourceDialog.findViewById(R.id.sort_subtitles)!!
- val applyButton =
- sourceDialog.findViewById(R.id.apply_btt)!!
- val cancelButton =
- sourceDialog.findViewById(R.id.cancel_btt)!!
- val subsSettings = sourceDialog.findViewById(R.id.subs_settings)!!
-
- val subtitleLoadButton =
- sourceDialog.findViewById(R.id.load_btt)!!
-
- subtitleLoadButton.setOnClickListener {
-// "vtt" -> "text/vtt"
-// "srt" -> "application/x-subrip"// "text/plain"
- subsPathPicker.launch(
- arrayOf(
- "text/vtt",
- "application/x-subrip",
- "text/plain",
- "text/str",
- "application/octet-stream"
- )
- )
- }
-
- subsSettings.setOnClickListener {
- autoHide()
- saveArguments()
- SubtitlesFragment.push(activity)
- sourceDialog.dismissSafe(activity)
- }
-
- var sourceIndex = 0
- var startSource = 0
- var sources: List = emptyList()
-
- val nonSortedUrls = getUrls()
- if (nonSortedUrls.isNullOrEmpty()) {
- sourceDialog.findViewById(R.id.sort_sources_holder)?.visibility =
- GONE
- } else {
- sources = sortUrls(nonSortedUrls)
- startSource = sources.indexOf(getCurrentUrl())
- sourceIndex = startSource
-
- val sourcesArrayAdapter =
- ArrayAdapter(ctx, R.layout.sort_bottom_single_choice)
- sourcesArrayAdapter.addAll(sources.map { it.name })
-
- providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
- providerList.adapter = sourcesArrayAdapter
- providerList.setSelection(sourceIndex)
- providerList.setItemChecked(sourceIndex, true)
-
- providerList.setOnItemClickListener { _, _, which, _ ->
- sourceIndex = which
- providerList.setItemChecked(which, true)
- }
-
- sourceDialog.setOnDismissListener {
- activity?.hideSystemUI()
- }
- }
-
- /**
- * This will get the actual real track played by exoplayer.
- * */
- val currentSubtitle = currentSubtitles.firstOrNull { sub ->
- exoPlayerSelectedTracks.any {
- // The replace is needed as exoplayer translates _ to -
- // Also we prefix the languages with _
- it.second && it.first.replace("-", "") .equals(
- sub.replace("-", ""),
- ignoreCase = true
- )
- }
- }
-
- val startIndexFromMap = currentSubtitles.indexOf(currentSubtitle) + 1
-// currentSubtitles.map { it.trimEnd() }
-// .indexOf(preferredSubtitles.trimEnd()) + 1
- var subtitleIndex = startIndexFromMap
-
- if (currentSubtitles.isEmpty()) {
- sourceDialog.findViewById(R.id.sort_subtitles_holder)?.visibility =
- GONE
- } else {
- val subsArrayAdapter =
- ArrayAdapter(ctx, R.layout.sort_bottom_single_choice)
- subsArrayAdapter.add(getString(R.string.no_subtitles))
- subsArrayAdapter.addAll(currentSubtitles)
-
- subtitleList.adapter = subsArrayAdapter
- subtitleList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
-
- subtitleList.setSelection(subtitleIndex)
- subtitleList.setItemChecked(subtitleIndex, true)
-
- subtitleList.setOnItemClickListener { _, _, which, _ ->
- subtitleIndex = which
- subtitleList.setItemChecked(which, true)
- }
- }
-
- cancelButton.setOnClickListener {
- sourceDialog.dismissSafe(activity)
- }
-
- applyButton.setOnClickListener {
- if (this::exoPlayer.isInitialized) playbackPosition =
- exoPlayer.currentPosition
-
- var init = false
- if (sourceIndex != startSource) {
- setMirrorId(sources[sourceIndex].getId())
- init = true
- }
- if (subtitleIndex != startIndexFromMap) {
- if (subtitleIndex <= 0) {
- setPreferredSubLanguage(null)
- } else {
- val langId = currentSubtitles[subtitleIndex - 1].trimEnd()
- setPreferredSubLanguage(langId)
-
- if (!activeSubtitles.any { it.trimEnd() == langId }) {
- init = true
- }
- }
- }
- if (init) {
- initPlayer(getCurrentUrl())
- }
- sourceDialog.dismissSafe(activity)
- }
- }
- }
- }
- } catch (e: Exception) {
- logError(e)
- }
- }
-//endregion
-
- private fun onSubStyleChanged(style: SaveCaptionStyle) {
- context?.let { ctx ->
- subStyle = style
- subView?.setStyle(ctx.fromSaveToStyle(style))
- subView?.translationY = -style.elevation.toPx.toFloat()
-
- if (style.fixedTextSize != null) {
- subView?.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, style.fixedTextSize!!)
- } else {
- subView?.setUserDefaultTextSize()
- }
- }
- }
-
- private fun setPreferredSubLanguage(lang: String?) {
- //val textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT) ?: return@setOnClickListener
- val realLang = if (lang.isNullOrBlank()) "" else lang.trimEnd()
- preferredSubtitles =
- if (realLang.length == 2) SubtitleHelper.fromTwoLettersToLanguage(realLang)
- ?: realLang else realLang
-
- if (!this::exoPlayer.isInitialized) return
- (exoPlayer.trackSelector as? DefaultTrackSelector?)?.let { trackSelector ->
- if (lang.isNullOrBlank()) {
- trackSelector.setParameters(
- trackSelector.buildUponParameters()
- .setPreferredTextLanguage(realLang)
- //.setRendererDisabled(textRendererIndex, true)
- )
- } else {
- trackSelector.setParameters(
- trackSelector.buildUponParameters()
- .setPreferredTextLanguage("_$realLang")
- //.setRendererDisabled(textRendererIndex, false)
- )
- }
- }
- }
-
- @SuppressLint("SetTextI18n")
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- updateLock()
-
- exo_pause?.setOnClickListener {
- autoHide()
- handlePlayerEvent(PlayerEventType.Pause)
- }
-
- exo_play?.setOnClickListener {
- autoHide()
- handlePlayerEvent(PlayerEventType.Play)
- }
-
- setPreferredSubLanguage(getAutoSelectLanguageISO639_1())
-
- subView = player_view?.findViewById(R.id.exo_subtitles)
- subView?.let { sView ->
- (sView.parent as ViewGroup?)?.removeView(sView)
- subtitle_holder.addView(sView)
- }
-
- subStyle = getCurrentSavedStyle()
- onSubStyleChanged(subStyle)
- SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged
-
- settingsManager = PreferenceManager.getDefaultSharedPreferences(activity)
- context?.let { ctx ->
- swipeEnabled =
- settingsManager.getBoolean(ctx.getString(R.string.swipe_enabled_key), true)
- swipeVerticalEnabled =
- settingsManager.getBoolean(ctx.getString(R.string.swipe_vertical_enabled_key), true)
- playBackSpeedEnabled = settingsManager.getBoolean(
- ctx.getString(R.string.playback_speed_enabled_key),
- false
- )
- playerResizeEnabled =
- settingsManager.getBoolean(ctx.getString(R.string.player_resize_enabled_key), true)
- doubleTapEnabled =
- settingsManager.getBoolean(ctx.getString(R.string.double_tap_enabled_key), false)
- useSystemBrightness =
- settingsManager.getBoolean(ctx.getString(R.string.use_system_brightness_key), false)
- }
-
- if (swipeVerticalEnabled)
- setBrightness(context, context?.getKey(VIDEO_PLAYER_BRIGHTNESS) ?: 0f)
-
- navigationBarHeight = requireContext().getNavigationBarHeight()
- statusBarHeight = requireContext().getStatusBarHeight()
-
- /*player_pause_holder?.setOnClickListener {
- if (this::exoPlayer.isInitialized) {
- if (exoPlayer.isPlaying)
- exoPlayer.pause()
- else
- exoPlayer.play()
- }
- }*/
- activity?.let { act ->
- if (act.isCastApiAvailable() && !isDownloadedFile) {
- try {
- CastButtonFactory.setUpMediaRouteButton(act, player_media_route_button)
- val castContext = CastContext.getSharedInstance(requireContext())
-
- if (castContext.castState != CastState.NO_DEVICES_AVAILABLE) player_media_route_button?.visibility =
- VISIBLE
- castContext.addCastStateListener { state ->
- if (player_media_route_button != null) {
- player_media_route_button?.isVisible =
- state != CastState.NO_DEVICES_AVAILABLE
-
- if (state == CastState.CONNECTED) {
- if (!this::exoPlayer.isInitialized) return@addCastStateListener
- val links = sortUrls(getUrls() ?: return@addCastStateListener)
- val epData = getEpisode() ?: return@addCastStateListener
-
- val index = links.indexOf(getCurrentUrl())
- activity?.getCastSession()?.startCast(
- apiName,
- currentIsMovie ?: return@addCastStateListener,
- currentHeaderName,
- currentPoster,
- epData.index,
- episodes,
- links,
- context?.getSubs(supportsDownloadedFiles = false)
- ?: emptyList(),
- index,
- exoPlayer.currentPosition
- )
-
- /*
- val customData =
- links.map { JSONObject().put("name", it.name) }
- val jsonArray = JSONArray()
- for (item in customData) {
- jsonArray.put(item)
- }
-
- val mediaItems = links.map {
- val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
-
- movieMetadata.putString(MediaMetadata.KEY_SUBTITLE,
- epData.name ?: "Episode ${epData.episode}")
-
- if (currentHeaderName != null)
- movieMetadata.putString(MediaMetadata.KEY_TITLE, currentHeaderName)
-
- val srcPoster = epData.poster ?: currentPoster
- if (srcPoster != null) {
- movieMetadata.addImage(WebImage(Uri.parse(srcPoster)))
- }
-
- MediaQueueItem.Builder(
- MediaInfo.Builder(it.url)
- .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
- .setContentType(MimeTypes.VIDEO_UNKNOWN)
-
- .setCustomData(JSONObject().put("data", jsonArray))
- .setMetadata(movieMetadata)
- .build()
- )
- .build()
- }.toTypedArray()
-
- val castPlayer = CastPlayer(castContext)
- castPlayer.loadItems(
- mediaItems,
- if (index > 0) index else 0,
- exoPlayer.currentPosition,
- MediaStatus.REPEAT_MODE_REPEAT_SINGLE
- )*/
- // activity?.popCurrentPage(isInPlayer = true, isInExpandedView = false, isInResults = false)
- safeReleasePlayer()
- activity?.popCurrentPage()
- }
- }
- }
- } catch (e: Exception) {
- logError(e)
- }
- }
- }
- isDownloadedFile = false
- arguments?.getString("uriData")?.let {
- uriData = mapper.readValue(it)
- isDownloadedFile = true
- }
-
- arguments?.getString("data")?.let {
- playerData = mapper.readValue(it)
- }
-
- arguments?.getLong(STATE_RESUME_POSITION)?.let {
- playbackPosition = it
- }
-
- arguments?.getString(PREFERRED_SUBS_KEY)?.let {
- setPreferredSubLanguage(it)
- }
-
-// sources_btt.visibility =
-// if (isDownloadedFile)
-// if (context?.getSubs()?.isNullOrEmpty() != false)
-// GONE else VISIBLE
-// else VISIBLE
-
- player_media_route_button?.isVisible = !isDownloadedFile
- if (savedInstanceState != null) {
- currentWindow = savedInstanceState.getInt(STATE_RESUME_WINDOW)
- if (playbackPosition <= 0) {
- playbackPosition = savedInstanceState.getLong(STATE_RESUME_POSITION)
- }
- isFullscreen = savedInstanceState.getBoolean(STATE_PLAYER_FULLSCREEN)
- isPlayerPlaying = savedInstanceState.getBoolean(STATE_PLAYER_PLAYING)
- resizeMode = savedInstanceState.getInt(RESIZE_MODE_KEY)
- savedInstanceState.getString(PREFERRED_SUBS_KEY)?.let {
- setPreferredSubLanguage(it)
- }
- savedInstanceState.getString("data")?.let {
- playerData = mapper.readValue(it)
- }
- playbackSpeed = savedInstanceState.getFloat(PLAYBACK_SPEED)
- }
-
- resizeMode = context?.getKey(RESIZE_MODE_KEY, 0) ?: 0
- playbackSpeed = context?.getKey(PLAYBACK_SPEED_KEY, 1f) ?: 1f
-
- activity?.let {
- it.contentResolver?.registerContentObserver(
- Settings.System.CONTENT_URI, true, SettingsContentObserver(
- Handler(
- Looper.getMainLooper()
- ), it
- )
- )
- }
-
- if (!isDownloadedFile) {
- //viewModel = ViewModelProvider(activity ?: this).get(ResultViewModel::class.java)
-
- observeDirectly(viewModel.episodes) { _episodes ->
- episodes = _episodes
- /*if (isLoading) {
- if (playerData.episodeIndex > 0 && playerData.episodeIndex < episodes.size) {
-
- } else {
- // WHAT THE FUCK DID YOU DO
- }
- }*/
- }
-
- observe(viewModel.apiName) {
- apiName = it
- }
-
- overlay_loading_skip_button?.alpha = 0.5f
- observeDirectly(viewModel.allEpisodes) { _allEpisodes ->
- allEpisodes = _allEpisodes
-
- val current = getUrls()
- if (current != null) {
- if (current.isNotEmpty()) {
- overlay_loading_skip_button?.alpha = 1f
- } else {
- overlay_loading_skip_button?.alpha = 0.5f
- }
- } else {
- overlay_loading_skip_button?.alpha = 0.5f
- }
- }
-
- observeDirectly(viewModel.allEpisodesSubs) { _allEpisodesSubs ->
- allEpisodesSubs = _allEpisodesSubs
- if (preferredSubtitles != "" && !activeSubtitles.contains(preferredSubtitles) && allEpisodesSubs[getEpisode()?.id]?.containsKey(
- preferredSubtitles
- ) == true
- ) {
- if (this::exoPlayer.isInitialized) playbackPosition = exoPlayer.currentPosition
- initPlayer(getCurrentUrl())
- }
- }
-
- observeDirectly(viewModel.resultResponse) { data ->
- when (data) {
- is Resource.Success -> {
- val d = data.value
- if (d is LoadResponse) {
- localData = d
- currentPoster = d.posterUrl
- currentHeaderName = d.name
- currentIsMovie = !d.isEpisodeBased()
- }
- }
- is Resource.Failure -> {
- //WTF, HOW DID YOU EVEN GET HERE
- }
- else -> {
- }
- }
- }
- }
- val fastForwardTime =
- settingsManager.getInt(getString(R.string.fast_forward_button_time_key), 10)
- exo_rew_text?.text = getString(R.string.rew_text_regular_format).format(fastForwardTime)
- exo_ffwd_text?.text = getString(R.string.ffw_text_regular_format).format(fastForwardTime)
- fun rewind() {
- player_rew_holder?.alpha = 1f
-
- val rotateLeft = AnimationUtils.loadAnimation(context, R.anim.rotate_left)
- exo_rew?.startAnimation(rotateLeft)
-
- val goLeft = AnimationUtils.loadAnimation(context, R.anim.go_left)
- goLeft.setAnimationListener(object : Animation.AnimationListener {
- override fun onAnimationStart(animation: Animation?) {}
-
- override fun onAnimationRepeat(animation: Animation?) {}
-
- override fun onAnimationEnd(animation: Animation?) {
- exo_rew_text?.post {
- exo_rew_text?.text =
- getString(R.string.rew_text_regular_format).format(fastForwardTime)
- player_rew_holder?.alpha = if (isShowing) 1f else 0f
- }
- }
- })
- exo_rew_text?.startAnimation(goLeft)
- exo_rew_text?.text = getString(R.string.rew_text_format).format(fastForwardTime)
- seekTime(fastForwardTime * -1000L)
- }
-
- exo_rew?.setOnClickListener {
- autoHide()
- rewind()
- }
-
- fun fastForward() {
- player_ffwd_holder?.alpha = 1f
- val rotateRight = AnimationUtils.loadAnimation(context, R.anim.rotate_right)
- exo_ffwd?.startAnimation(rotateRight)
-
- val goRight = AnimationUtils.loadAnimation(context, R.anim.go_right)
- goRight.setAnimationListener(object : Animation.AnimationListener {
- override fun onAnimationStart(animation: Animation?) {}
-
- override fun onAnimationRepeat(animation: Animation?) {}
-
- override fun onAnimationEnd(animation: Animation?) {
- exo_ffwd_text?.post {
- exo_ffwd_text?.text =
- getString(R.string.ffw_text_regular_format).format(fastForwardTime)
- player_ffwd_holder?.alpha = if (isShowing) 1f else 0f
- }
- }
- })
- exo_ffwd_text?.startAnimation(goRight)
- exo_ffwd_text?.text = getString(R.string.ffw_text_format).format(fastForwardTime)
- seekTime(fastForwardTime * 1000L)
- }
-
- exo_ffwd?.setOnClickListener {
- autoHide()
- fastForward()
- }
-
- overlay_loading_skip_button?.setOnClickListener {
- setMirrorId(
- sortUrls(getUrls() ?: return@setOnClickListener).first()
- .getId()
- ) // BECAUSE URLS CANT BE REORDERED
- if (!isCurrentlyPlaying) {
- initPlayer(getCurrentUrl())
- }
- }
-
- lock_player?.setOnClickListener {
- handlePlayerEvent(PlayerEventType.Lock)
- }
-
- class Listener : DoubleClickListener(this) {
- // Declaring a seekAnimation here will cause a bug
-
- override fun onDoubleClickRight(clicks: Int) {
- if (!isLocked) {
- fastForward()
- } else onSingleClick()
- }
-
- override fun onDoubleClickLeft(clicks: Int) {
- if (!isLocked) {
- rewind()
- } else onSingleClick()
- }
-
- override fun onSingleClick() {
- handlePlayerEvent(PlayerEventType.ToggleHide)
- }
-
- override fun onMotionEvent(event: MotionEvent) {
- handleMotionEvent(event)
- }
- }
-
- player_holder?.setOnTouchListener(
- Listener()
- )
-
- click_overlay?.setOnTouchListener(
- Listener()
- )
-
- video_go_back.setOnClickListener {
- //activity?.popCurrentPage(isInPlayer = true, isInExpandedView = false, isInResults = false)
- activity?.popCurrentPage()
- }
- video_go_back_holder.setOnClickListener {
- //println("video_go_back_pressed")
- // activity?.popCurrentPage(isInPlayer = true, isInExpandedView = false, isInResults = false)
- activity?.popCurrentPage()
- }
-
- playback_speed_btt?.isVisible = playBackSpeedEnabled
- playback_speed_btt?.setOnClickListener {
- autoHide()
- handlePlayerEvent(PlayerEventType.ShowSpeed)
- }
-
- sources_btt.setOnClickListener {
- autoHide()
- handlePlayerEvent(PlayerEventType.ShowMirrors)
- }
-
- player_view?.resizeMode = resizeModes[resizeMode].first
- if (playerResizeEnabled) {
- resize_player?.visibility = VISIBLE
- resize_player?.setOnClickListener {
- autoHide()
- handlePlayerEvent(PlayerEventType.Resize)
- }
- } else {
- resize_player?.visibility = GONE
- }
-
- skip_op?.setOnClickListener {
- autoHide()
- skipOP()
- }
-
- skip_episode?.setOnClickListener {
- autoHide()
- handlePlayerEvent(PlayerEventType.NextEpisode)
- }
-
- changeSkip()
-
- initPlayer()
- }
-
- private fun getCurrentUrl(): ExtractorLink? {
- try {
- val urls = getUrls() ?: return null
- for (i in urls) {
- if (i.getId() == playerData.mirrorId) {
- return i
- }
- }
- } catch (e: Exception) {
- return null
- }
-
- return null
- }
-
- private fun getUrls(): List? {
- return try {
- allEpisodes[getEpisode()?.id]
- } catch (e: Exception) {
- null
- }
- }
-
- private fun Context.getSubs(supportsDownloadedFiles: Boolean = true): List? {
- return try {
- if (isDownloadedFile) {
- if (!supportsDownloadedFiles) return null
- val list = ArrayList()
-
- // Adds custom subtitles
- allEpisodesSubs[uriData.id]?.values?.forEach { list.add(it) }
-
- VideoDownloadManager.getFolder(this, uriData.relativePath, uriData.basePath)
- ?.forEach { file ->
- val name = uriData.displayName.removeSuffix(".mp4")
- if (file.first != uriData.displayName && file.first.startsWith(name)) {
- val realName = file.first.removePrefix(name)
- .removeSuffix(".vtt")
- .removeSuffix(".srt")
- .removeSuffix(".txt")
- list.add(
- SubtitleFile(
- realName.ifBlank { getString(R.string.default_subtitles) },
- file.second.toString()
- )
- )
- }
- }
- return list
- } else {
- allEpisodesSubs[getEpisode()?.id]?.values?.toList()?.sortedBy { it.lang }
- }
- } catch (e: Exception) {
- null
- }
- }
-
- private fun getEpisode(): ResultEpisode? {
- return try {
- episodes[playerData.episodeIndex]
- } catch (e: Exception) {
- null
- }
- }
-
- private fun hasNextEpisode(): Boolean {
- return !isDownloadedFile && episodes.size > playerData.episodeIndex + 1 // TODO FIX DOWNLOADS NEXT EPISODE
- }
-
- private var isCurrentlySkippingEp = false
-
- fun tryNextMirror() {
- val urls = getUrls()
- val current = getCurrentUrl()
- if (urls != null && current != null) {
- val id = current.getId()
- val sorted = sortUrls(urls)
- for ((i, item) in sorted.withIndex()) {
- if (item.getId() == id) {
- if (sorted.size > i + 1) {
- setMirrorId(sorted[i + 1].getId())
- initPlayer(getCurrentUrl())
- }
- }
- }
- }
- }
-
- private fun skipToNextEpisode() {
- if (isCurrentlySkippingEp) return
- savePos()
- safeReleasePlayer()
- isCurrentlySkippingEp = true
- val copy = playerData.copy(episodeIndex = playerData.episodeIndex + 1)
- playerData = copy
- playbackPosition = 0
- initPlayer()
- }
-
- private fun setMirrorId(id: Int?) {
- if (id == null) return
- val copy = playerData.copy(mirrorId = id)
- playerData = copy
- //initPlayer()
- }
-
- override fun onStart() {
- super.onStart()
- if (!isCurrentlyPlaying) {
- if (isDownloadedFile) {
- initPlayer(null, uriData.uri)
- } else {
- getCurrentUrl()?.let {
- initPlayer(it)
- }
- }
- }
- if (player_view != null) player_view?.onResume()
- }
-
- override fun onResume() {
- super.onResume()
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && fullscreenNotch) {
- val params = activity?.window?.attributes
- params?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
- activity?.window?.attributes = params
- }
-
- //torrentStream?.currentTorrent?.resume()
- onAudioFocusEvent += ::handlePauseEvent
-
- activity?.hideSystemUI()
- activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
- if (Util.SDK_INT <= 23) {
- if (!isCurrentlyPlaying) {
- if (isDownloadedFile) {
- initPlayer(null, uriData.uri)
- } else {
- getCurrentUrl()?.let {
- initPlayer(it)
- }
- }
- }
- if (player_view != null) player_view?.onResume()
- }
- }
-
- private fun handlePauseEvent(pause: Boolean) {
- if (pause) {
- handlePlayerEvent(PlayerEventType.Pause)
- }
- }
-
- override fun onDestroy() {
- MainActivity.playerEventListener = null
- MainActivity.keyEventListener = null
- /* val lp = activity?.window?.attributes
-
-
- lp?.screenBrightness = 1f
- activity?.window?.attributes = lp*/
- // restoring screen brightness
- val lp = activity?.window?.attributes
- lp?.screenBrightness = BRIGHTNESS_OVERRIDE_NONE
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- lp?.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
- }
- activity?.window?.attributes = lp
-
- loading_overlay?.isVisible = false
- savePos()
- SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged
-
- // torrentStream?.stopStream()
- // torrentStream = null
-
- canEnterPipMode = false
-
- savePositionInPlayer()
- safeReleasePlayer()
-
- onAudioFocusEvent -= ::handlePauseEvent
-
- activity?.showSystemUI()
- activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
- super.onDestroy()
- }
-
- override fun onPause() {
- savePos()
- super.onPause()
- // torrentStream?.currentTorrent?.pause()
- if (Util.SDK_INT <= 23) {
- if (player_view != null) player_view?.onPause()
- releasePlayer()
- }
- }
-
- override fun onStop() {
- savePos()
- super.onStop()
- if (Util.SDK_INT > 23) {
- if (player_view != null) player_view?.onPause()
- releasePlayer()
- }
- }
-
- private fun saveArguments() {
- if (this::exoPlayer.isInitialized) {
- arguments?.putInt(STATE_RESUME_WINDOW, exoPlayer.currentWindowIndex)
- arguments?.putLong(STATE_RESUME_POSITION, exoPlayer.currentPosition)
- }
- arguments?.putBoolean(STATE_PLAYER_FULLSCREEN, isFullscreen)
- arguments?.putBoolean(STATE_PLAYER_PLAYING, isPlayerPlaying)
- arguments?.putInt(RESIZE_MODE_KEY, resizeMode)
- arguments?.putString(PREFERRED_SUBS_KEY, preferredSubtitles)
- arguments?.putFloat(PLAYBACK_SPEED, playbackSpeed)
- if (!isDownloadedFile && this::playerData.isInitialized) {
- arguments?.putString("data", mapper.writeValueAsString(playerData))
- }
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- savePos()
-
- if (this::exoPlayer.isInitialized) {
- outState.putInt(STATE_RESUME_WINDOW, exoPlayer.currentWindowIndex)
- outState.putLong(STATE_RESUME_POSITION, exoPlayer.currentPosition)
- }
- outState.putBoolean(STATE_PLAYER_FULLSCREEN, isFullscreen)
- outState.putBoolean(STATE_PLAYER_PLAYING, isPlayerPlaying)
- outState.putInt(RESIZE_MODE_KEY, resizeMode)
- outState.putString(PREFERRED_SUBS_KEY, preferredSubtitles)
- outState.putFloat(PLAYBACK_SPEED, playbackSpeed)
- if (!isDownloadedFile && this::playerData.isInitialized) {
- outState.putString("data", mapper.writeValueAsString(playerData))
- }
- super.onSaveInstanceState(outState)
- }
-
- private var currentWindow = 0
- private var playbackPosition: Long = 0
-/*
- private fun updateProgressBar() {
- val duration: Long =exoPlayer.getDuration()
- val position: Long =exoPlayer.getCurrentPosition()
-
- handler.removeCallbacks(updateProgressAction)
- val playbackState = exoPlayer.getPlaybackState()
- if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) {
- var delayMs: Long
- if (player.getPlayWhenReady() && playbackState == Player.STATE_READY) {
- delayMs = 1000 - position % 1000
- if (delayMs < 200) {
- delayMs += 1000
- }
- } else {
- delayMs = 1000
- }
- handler.postDelayed(updateProgressAction, delayMs)
- }
- }
-
- private val updateProgressAction = Runnable { updateProgressBar() }*/
-
- var activeSubtitles: List = listOf()
- var preferredSubtitles: String = ""
-
- @SuppressLint("SetTextI18n")
- fun initPlayer(currentUrl: ExtractorLink?, uri: String? = null, trueUri: Uri? = null) {
- if (currentUrl == null && uri == null && trueUri == null) return
- if (currentUrl?.url?.endsWith(".torrent") == true || currentUrl?.url?.startsWith("magnet") == true) {
- initTorrentStream(currentUrl.url)//)
- return
- }
- // player_torrent_info?.visibility = if(isTorrent) VISIBLE else GONE
- //
-
- isShowing = true
- onClickChange()
-
- player_torrent_info?.isVisible = false
- //player_torrent_info?.alpha = 0f
- println("LOADED: $uri or $currentUrl")
- isCurrentlyPlaying = true
- hasUsedFirstRender = false
-
- try {
- if (this::exoPlayer.isInitialized) {
- savePos()
- exoPlayer.release()
- }
- val isOnline =
- currentUrl != null && (currentUrl.url.startsWith("https://") || currentUrl.url.startsWith(
- "http://"
- ))
-
- if (settingsManager.getBoolean("ignore_ssl", true) && !isDownloadedFile) {
- // Disables ssl check
- val sslContext: SSLContext = SSLContext.getInstance("TLS")
- sslContext.init(null, arrayOf(SSLTrustManager()), java.security.SecureRandom())
- sslContext.createSSLEngine()
- HttpsURLConnection.setDefaultHostnameVerifier { _: String, _: SSLSession ->
- true
- }
- HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
- }
-
- val mimeType =
- if (currentUrl == null && uri != null)
- MimeTypes.APPLICATION_MP4 else
- if (currentUrl?.isM3u8 == true)
- MimeTypes.APPLICATION_M3U8
- else
- MimeTypes.APPLICATION_MP4
-
- val mediaItemBuilder = MediaItem.Builder()
- //Replace needed for android 6.0.0 https://github.com/google/ExoPlayer/issues/5983
- .setMimeType(mimeType)
-
- if (currentUrl != null) {
- mediaItemBuilder.setUri(currentUrl.url)
- } else if (trueUri != null || uri != null) {
- val uriPrimary = trueUri ?: Uri.parse(uri)
- mediaItemBuilder.setUri(uriPrimary)
- }
-
- fun getDataSourceFactory(isOnline: Boolean): DataSource.Factory {
- return if (isOnline) {
- DefaultHttpDataSource.Factory().apply {
- setUserAgent(USER_AGENT)
- if (currentUrl != null) {
- val headers = mapOf(
- "referer" to currentUrl.referer,
- "accept" to "*/*",
- "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"",
- "sec-ch-ua-mobile" to "?0",
- "sec-fetch-user" to "?1",
- "sec-fetch-mode" to "navigate",
- "sec-fetch-dest" to "video"
- ) + currentUrl.headers // Adds the headers from the provider, e.g Authorization
- setDefaultRequestProperties(headers)
- }
-
- //https://stackoverflow.com/questions/69040127/error-code-io-bad-http-status-exoplayer-android
- setAllowCrossProtocolRedirects(true)
- }
- } else {
- DefaultDataSourceFactory(requireContext(), USER_AGENT)
- }
- }
-
- val subs = context?.getSubs() ?: emptyList()
- val subItemsId = ArrayList()
-
- val subSources = sortSubs(subs).map { sub ->
- // The url can look like .../document/4294 when the name is EnglishSDH.srt
- val subtitleMimeType =
- sub.url.toSubtitleMimeType()
-
- val langId =
- sub.lang.trimEnd() //SubtitleHelper.fromLanguageToTwoLetters(it.lang) ?: it.lang
-
- subItemsId.add(langId)
- val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url))
- .setMimeType(subtitleMimeType)
- .setLanguage("_$langId")
- .setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
- .build()
- SingleSampleMediaSource.Factory(getDataSourceFactory(!sub.url.startsWith("content://")))
- .createMediaSource(subConfig, TIME_UNSET)
- }
-
- activeSubtitles = subItemsId
-// mediaItemBuilder.setSubtitleConfigurations(subItems)
-
- //might add https://github.com/ed828a/Aihua/blob/1896f46888b5a954b367e83f40b845ce174a2328/app/src/main/java/com/dew/aihua/player/playerUI/VideoPlayer.kt#L287 toggle caps
-
- val mediaItem = mediaItemBuilder.build()
- val trackSelector = DefaultTrackSelector(requireContext())
- // Disable subtitles
- trackSelector.parameters = DefaultTrackSelector.ParametersBuilder(requireContext())
- // .setRendererDisabled(C.TRACK_TYPE_VIDEO, true)
- .setRendererDisabled(C.TRACK_TYPE_TEXT, true)
- .setDisabledTextTrackSelectionFlags(C.TRACK_TYPE_TEXT)
- .clearSelectionOverrides()
- .build()
-
- normalSafeApiCall {
- val databaseProvider = StandaloneDatabaseProvider(requireContext())
- simpleCache = SimpleCache(
- File(
- requireContext().filesDir, "exoplayer"
- ).also { it.deleteOnExit() }, // Ensures always fresh file
- LeastRecentlyUsedCacheEvictor(cacheSize),
- databaseProvider
- )
- }
-
- val cacheFactory = CacheDataSource.Factory().apply {
- simpleCache?.let { setCache(it) }
- setUpstreamDataSourceFactory(getDataSourceFactory(isOnline))
- }
-
- val exoPlayerBuilder =
- ExoPlayer.Builder(requireContext())
- .setTrackSelector(trackSelector)
-
- val videoMediaSource =
- DefaultMediaSourceFactory(cacheFactory).createMediaSource(mediaItem)
-
- exoPlayer = exoPlayerBuilder.build().apply {
- playWhenReady = isPlayerPlaying
- seekTo(currentWindow, playbackPosition)
- setMediaSource(
- MergingMediaSource(
- videoMediaSource, *subSources.toTypedArray()
- ),
- playbackPosition
- )
- prepare()
- }
-
- val alphaAnimation = AlphaAnimation(1f, 0f)
- alphaAnimation.duration = 300
- alphaAnimation.fillAfter = true
- alphaAnimation.setAnimationListener(object : Animation.AnimationListener {
- override fun onAnimationStart(animation: Animation?) {}
-
- override fun onAnimationRepeat(animation: Animation?) {}
-
- override fun onAnimationEnd(animation: Animation?) {
- loading_overlay?.post { video_go_back_holder_holder?.visibility = GONE; }
- }
- })
- overlay_loading_skip_button?.visibility = GONE
-
- loading_overlay?.startAnimation(alphaAnimation)
-
- exoPlayer.setHandleAudioBecomingNoisy(true) // WHEN HEADPHONES ARE PLUGGED OUT https://github.com/google/ExoPlayer/issues/7288
- player_view?.player = exoPlayer
- // Sets the speed
- exoPlayer.playbackParameters = PlaybackParameters(playbackSpeed)
- playback_speed_btt?.text =
- getString(R.string.player_speed_text_format).format(playbackSpeed)
- .replace(".0x", "x")
-
- var hName: String? = null
- var epEpisode: Int? = null
- var epSeason: Int? = null
- var isEpisodeBased = true
-
- /*if (isTorrent) {
- hName = "Torrent Stream"
- isEpisodeBased = false
- } else*/ if (isDownloadedFile) {
- hName = uriData.name
- epEpisode = uriData.episode
- epSeason = uriData.season
- isEpisodeBased = epEpisode != null
- video_title_rez?.text = ""
- } else if (localData != null && currentUrl != null) {
- val data = localData!!
- val localEpisode = getEpisode()
- if (localEpisode != null) {
- epEpisode = localEpisode.episode
- epSeason = localEpisode.season
- hName = data.name
- isEpisodeBased = data.isEpisodeBased()
- video_title_rez?.text = currentUrl.name
- }
- }
-
- player_view?.performClick()
-
- video_title?.text = hName +
- if (isEpisodeBased)
- if (epSeason == null)
- " - ${getString(R.string.episode)} $epEpisode"
- else
- " \"${getString(R.string.season_short)}${epSeason}:${getString(R.string.episode_short)}${epEpisode}\""
- else ""
-
-/*
- exo_remaining.text = Util.getStringForTime(formatBuilder,
- formatter,
- exoPlayer.contentDuration - exoPlayer.currentPosition)
-
- */
-
- /*exoPlayer.addTextOutput { list ->
- if (list.size == 0) return@addTextOutput
-
- val textBuilder = StringBuilder()
- for (cue in list) {
- textBuilder.append(cue.text).append("\n")
- }
- val subtitleText = if (textBuilder.isNotEmpty())
- textBuilder.substring(0, textBuilder.length - 1)
- else
- textBuilder.toString()
- }*/
-
- //https://stackoverflow.com/questions/47731779/detect-pause-resume-in-exoplayer
- MainActivity.playerEventListener = { eventType ->
- handlePlayerEvent(eventType)
- }
-
- MainActivity.keyEventListener = { keyEvent ->
- if (keyEvent != null) {
- handleKeyEvent(keyEvent)
- } else {
- false
- }
- }
-
- exoPlayer.addListener(object : Player.Listener {
-
- /**
- * Records the current used subtitle/track. Needed as exoplayer seems to have loose track language selection.
- * */
- override fun onTracksInfoChanged(tracksInfo: TracksInfo) {
- exoPlayerSelectedTracks =
- tracksInfo.trackGroupInfos.mapNotNull { it.trackGroup.getFormat(0).language?.let { lang -> lang to it.isSelected } }
- super.onTracksInfoChanged(tracksInfo)
- }
-
- override fun onRenderedFirstFrame() {
- super.onRenderedFirstFrame()
- isCurrentlySkippingEp = false
-
- val playerHeight = exoPlayer.videoFormat?.height
- val playerWidth = exoPlayer.videoFormat?.width
-
- video_title_rez?.text =
- if (playerHeight == null || playerWidth == null) currentUrl?.name
- ?: "" else
- // if (isTorrent) "${width}x${height}" else
- if (isDownloadedFile || currentUrl?.name == null) "${playerWidth}x${playerHeight}" else "${currentUrl.name} - ${playerWidth}x${playerHeight}"
-
- if (!hasUsedFirstRender) { // DON'T WANT TO SET MULTIPLE MESSAGES
- //&& !isTorrent
- if (!isDownloadedFile && exoPlayer.duration in 5_000..10_000) {
- // if(getapi apiName )
- showToast(activity, R.string.vpn_might_be_needed, LENGTH_SHORT)
- }
- changeSkip()
- exoPlayer
- .createMessage { _, _ ->
- changeSkip()
- }
- .setLooper(Looper.getMainLooper())
- .setPosition( /* positionMs= */exoPlayer.contentDuration * OPENING_PERCENTAGE / 100)
- // .setPayload(customPayloadData)
- .setDeleteAfterDelivery(false)
- .send()
- exoPlayer
- .createMessage { _, _ ->
- changeSkip()
- }
- .setLooper(Looper.getMainLooper())
- .setPosition( /* positionMs= */exoPlayer.contentDuration * AUTOLOAD_NEXT_EPISODE_PERCENTAGE / 100)
- // .setPayload(customPayloadData)
- .setDeleteAfterDelivery(false)
-
- .send()
-
- } else {
- changeSkip()
- }
- hasUsedFirstRender = true
- }
-
- override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
- canEnterPipMode = exoPlayer.isPlaying
- updatePIPModeActions()
- if (activity == null) return
- if (playWhenReady) {
- when (playbackState) {
- Player.STATE_READY -> {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- activity?.requestLocalAudioFocus(getFocusRequest())
- }
- }
- Player.STATE_ENDED -> {
- if (hasNextEpisode()) {
- skipToNextEpisode()
- }
- }
- Player.STATE_BUFFERING -> {
- changeSkip()
- }
- else -> {
- }
- }
- }
- }
-
- override fun onPlayerError(error: PlaybackException) {
- println("CURRENT URL ERROR: " + currentUrl?.url)
- // Lets pray this doesn't spam Toasts :)
- val msg = error.message ?: ""
- val errorName = error.errorCodeName
- when (val code = error.errorCode) {
- PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND, PlaybackException.ERROR_CODE_IO_NO_PERMISSION, PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> {
- if (currentUrl?.url != "") {
- showToast(
- activity,
- "${getString(R.string.source_error)}\n$errorName ($code)\n$msg",
- LENGTH_SHORT
- )
- tryNextMirror()
- }
- }
- PlaybackException.ERROR_CODE_REMOTE_ERROR, PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, PlaybackException.ERROR_CODE_TIMEOUT, PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE -> {
- showToast(
- activity,
- "${getString(R.string.remote_error)}\n$errorName ($code)\n$msg",
- LENGTH_SHORT
- )
- }
- PlaybackException.ERROR_CODE_DECODING_FAILED, PlaybackErrorEvent.ERROR_AUDIO_TRACK_INIT_FAILED, PlaybackErrorEvent.ERROR_AUDIO_TRACK_OTHER, PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED -> {
- showToast(
- activity,
- "${getString(R.string.render_error)}\n$errorName ($code)\n$msg",
- LENGTH_SHORT
- )
- }
- else -> {
- showToast(
- activity,
- "${getString(R.string.unexpected_error)}\n$errorName ($code)\n$msg",
- LENGTH_SHORT
- )
- }
- }
-
- super.onPlayerError(error)
- }
- })
- } catch (e: java.lang.IllegalStateException) {
- println("Warning: Illegal state exception in PlayerFragment")
- } finally {
- setPreferredSubLanguage(
- if (isDownloadedFile) {
- if (activeSubtitles.isNotEmpty()) {
- activeSubtitles.first()
- } else null
- } else {
- preferredSubtitles
- }
- )
- }
- }
-
- private fun preferredQuality(tempCurrentUrls: List?): Int? {
- if (tempCurrentUrls.isNullOrEmpty()) return null
- val sortedUrls = sortUrls(tempCurrentUrls).reversed()
- var currentQuality = Qualities.values().last().value
- context?.let { ctx ->
- if (this::settingsManager.isInitialized)
- currentQuality = settingsManager.getInt(
- ctx.getString(R.string.watch_quality_pref),
- currentQuality
- )
- }
-
- var currentId = sortedUrls.first().getId() // lowest quality
- for (url in sortedUrls) {
- if (url.quality > currentQuality) break
- currentId = url.getId()
- }
- return currentId
- }
-
- //http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
- @SuppressLint("ClickableViewAccessibility")
- private fun initPlayer() {
- if (isDownloadedFile) {
- //.removePrefix("file://").replace("%20", " ") // FIX FILE PERMISSION
- initPlayer(null, uriData.uri)
- }
- println("INIT PLAYER")
- view?.setOnTouchListener { _, _ -> return@setOnTouchListener true } // VERY IMPORTANT https://stackoverflow.com/questions/28818926/prevent-clicking-on-a-button-in-an-activity-while-showing-a-fragment
- val tempCurrentUrls = getUrls()
- if (tempCurrentUrls != null) {
- setMirrorId(preferredQuality(tempCurrentUrls))
- }
- val tempUrl = getCurrentUrl()
- println("TEMP:" + tempUrl?.name)
- if (tempUrl == null) {
- val localEpisode = getEpisode()
- if (localEpisode != null) {
- viewModel.loadEpisode(localEpisode, false) {
- //if(it is Resource.Success && it.value == true)
- val currentUrls = getUrls()
- if (currentUrls != null && currentUrls.isNotEmpty()) {
- if (!isCurrentlyPlaying) {
- setMirrorId(preferredQuality(currentUrls))
- initPlayer(getCurrentUrl())
- }
- } else {
- showToast(activity, R.string.no_links_found_toast, LENGTH_SHORT)
- }
- }
- }
- } else {
- initPlayer(tempUrl)
- }
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- return inflater.inflate(R.layout.fragment_player, container, false)
- }
-}
\ No newline at end of file
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
new file mode 100644
index 00000000..31ec3e5b
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt
@@ -0,0 +1,109 @@
+package com.lagradost.cloudstream3.ui.player
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.lagradost.cloudstream3.mvvm.Resource
+import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
+import com.lagradost.cloudstream3.mvvm.safeApiCall
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.ExtractorUri
+import kotlinx.coroutines.launch
+
+class PlayerGeneratorViewModel : ViewModel() {
+ private var generator: IGenerator? = null
+
+ private val _currentLinks = MutableLiveData>>(setOf())
+ val currentLinks: LiveData>> = _currentLinks
+
+ private val _currentSubs = MutableLiveData>(setOf())
+ val currentSubs: LiveData> = _currentSubs
+
+ private val _loadingLinks = MutableLiveData>()
+ val loadingLinks: LiveData> = _loadingLinks
+
+ fun getId(): Int? {
+ return generator?.getCurrentId()
+ }
+
+ fun loadLinks(episode: Int) {
+ generator?.goto(episode)
+ loadLinks()
+ }
+
+ fun loadLinksPrev() {
+ if (generator?.hasPrev() == true) {
+ generator?.prev()
+ loadLinks()
+ }
+ }
+
+ fun loadLinksNext() {
+ if (generator?.hasNext() == true) {
+ generator?.next()
+ loadLinks()
+ }
+ }
+
+ fun hasNextEpisode(): Boolean? {
+ return generator?.hasNext()
+ }
+
+ fun preLoadNextLinks() = viewModelScope.launch {
+ normalSafeApiCall {
+ if (generator?.hasCache == true && generator?.hasNext() == true) {
+ generator?.next()
+ generator?.generateLinks(clearCache = false, isCasting = false, {}, {})
+ generator?.prev()
+ }
+ }
+ }
+
+ fun getMeta(): Any? {
+ return normalSafeApiCall { generator?.getCurrent() }
+ }
+
+ fun getNextMeta(): Any? {
+ return normalSafeApiCall {
+ if (generator?.hasNext() == false) return@normalSafeApiCall null
+ generator?.next()
+ val next = generator?.getCurrent()
+ generator?.prev()
+ next
+ }
+ }
+
+ fun attachGenerator(newGenerator: IGenerator?) {
+ if (generator == null) {
+ generator = newGenerator
+ }
+ }
+
+ fun addSubtitles(file: Set) {
+ val subs = (_currentSubs.value?.toMutableSet() ?: mutableSetOf())
+ subs.addAll(file)
+ _currentSubs.postValue(subs)
+ }
+
+ fun loadLinks(clearCache: Boolean = false, isCasting: Boolean = false) = viewModelScope.launch {
+ val currentLinks = mutableSetOf>()
+ val currentSubs = mutableSetOf()
+
+ _loadingLinks.postValue(Resource.Loading())
+ val loadingState = safeApiCall {
+ generator?.generateLinks(clearCache = clearCache, isCasting = isCasting, {
+ currentLinks.add(it)
+ _currentLinks.postValue(currentLinks)
+ }, {
+ currentSubs.add(it)
+ _currentSubs.postValue(currentSubs)
+ })
+ }
+
+ _loadingLinks.postValue(loadingState)
+
+ _currentLinks.postValue(currentLinks)
+ _currentSubs.postValue(currentSubs)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt
new file mode 100644
index 00000000..0fbc22f6
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt
@@ -0,0 +1,95 @@
+package com.lagradost.cloudstream3.ui.player
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.app.PictureInPictureParams
+import android.app.RemoteAction
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.annotation.StringRes
+import com.lagradost.cloudstream3.R
+
+class PlayerPipHelper {
+ companion object {
+ private fun getPen(activity: Activity, code: Int): PendingIntent {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.getBroadcast(
+ activity,
+ code,
+ Intent("media_control").putExtra("control_type", code),
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ } else {
+ PendingIntent.getBroadcast(
+ activity,
+ code,
+ Intent("media_control").putExtra("control_type", code),
+ 0
+ )
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun getRemoteAction(
+ activity: Activity,
+ id: Int,
+ @StringRes title: Int,
+ event: CSPlayerEvent
+ ): RemoteAction {
+ val text = activity.getString(title)
+ return RemoteAction(
+ Icon.createWithResource(activity, id),
+ text,
+ text,
+ getPen(activity, event.value)
+ )
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ fun updatePIPModeActions(activity: Activity, isPlaying: Boolean) {
+ val actions: ArrayList = ArrayList()
+ actions.add(
+ getRemoteAction(
+ activity,
+ R.drawable.go_back_30,
+ R.string.go_back_30,
+ CSPlayerEvent.SeekBack
+ )
+ )
+
+ if (isPlaying) {
+ actions.add(
+ getRemoteAction(
+ activity,
+ R.drawable.netflix_pause,
+ R.string.pause,
+ CSPlayerEvent.Pause
+ )
+ )
+ } else {
+ actions.add(
+ getRemoteAction(
+ activity,
+ R.drawable.ic_baseline_play_arrow_24,
+ R.string.pause,
+ CSPlayerEvent.Play
+ )
+ )
+ }
+
+ actions.add(
+ getRemoteAction(
+ activity,
+ R.drawable.go_forward_30,
+ R.string.go_forward_30,
+ CSPlayerEvent.SeekForward
+ )
+ )
+ activity.setPictureInPictureParams(
+ PictureInPictureParams.Builder().setActions(actions).build()
+ )
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..ddaf264d
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt
@@ -0,0 +1,127 @@
+package com.lagradost.cloudstream3.ui.player
+
+import android.content.Context
+import android.net.Uri
+import android.util.TypedValue
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import com.google.android.exoplayer2.ui.SubtitleView
+import com.google.android.exoplayer2.util.MimeTypes
+import com.hippo.unifile.UniFile
+import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
+import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.fromSaveToStyle
+import com.lagradost.cloudstream3.utils.UIHelper.toPx
+
+enum class SubtitleStatus {
+ IS_ACTIVE,
+ REQUIRES_RELOAD,
+ NOT_FOUND,
+}
+
+enum class SubtitleOrigin {
+ URL,
+ DOWNLOADED_FILE,
+ OPEN_SUBTITLES,
+}
+
+data class SubtitleData(
+ val name: String,
+ val url: String,
+ val origin: SubtitleOrigin,
+ val mimeType: String,
+)
+
+class PlayerSubtitleHelper {
+ private var activeSubtitles: Set = emptySet()
+ private var allSubtitles: Set = emptySet()
+
+ fun getAllSubtitles(): Set {
+ return allSubtitles
+ }
+
+ fun setActiveSubtitles(list: Set) {
+ activeSubtitles = list
+ }
+
+ fun setAllSubtitles(list: Set) {
+ allSubtitles = list
+ }
+
+ private var subStyle: SaveCaptionStyle? = null
+ private var subtitleView: SubtitleView? = null
+
+ companion object {
+ fun String.toSubtitleMimeType(): String {
+ return when {
+ endsWith("vtt", true) -> MimeTypes.TEXT_VTT
+ endsWith("srt", true) -> MimeTypes.APPLICATION_SUBRIP
+ endsWith("xml", true) || endsWith("ttml", true) -> MimeTypes.APPLICATION_TTML
+ else -> MimeTypes.APPLICATION_SUBRIP // TODO get request to see
+ }
+ }
+
+ private fun getSubtitleMimeType(context: Context, url: String, origin: SubtitleOrigin): String {
+ return when (origin) {
+ // The url can look like .../document/4294 when the name is EnglishSDH.srt
+ SubtitleOrigin.DOWNLOADED_FILE -> {
+ UniFile.fromUri(
+ context,
+ Uri.parse(url)
+ ).name?.toSubtitleMimeType() ?: MimeTypes.APPLICATION_SUBRIP
+ }
+ SubtitleOrigin.URL -> {
+ return url.toSubtitleMimeType()
+ }
+ SubtitleOrigin.OPEN_SUBTITLES -> {
+ // TODO
+ throw NotImplementedError()
+ }
+ }
+ }
+
+ fun getSubtitleData(subtitleFile: SubtitleFile): SubtitleData {
+ return SubtitleData(
+ name = subtitleFile.lang,
+ url = subtitleFile.url,
+ origin = SubtitleOrigin.URL,
+ mimeType = subtitleFile.url.toSubtitleMimeType()
+ )
+ }
+ }
+
+ fun subtitleStatus(sub : SubtitleData?): SubtitleStatus {
+ if(activeSubtitles.contains(sub)) {
+ return SubtitleStatus.IS_ACTIVE
+ }
+ if(allSubtitles.contains(sub)) {
+ return SubtitleStatus.REQUIRES_RELOAD
+ }
+ return SubtitleStatus.NOT_FOUND
+ }
+
+ fun setSubStyle(style: SaveCaptionStyle) {
+ subtitleView?.context?.let { ctx ->
+ subStyle = style
+ subtitleView?.setStyle(ctx.fromSaveToStyle(style))
+ subtitleView?.translationY = -style.elevation.toPx.toFloat()
+ val size = style.fixedTextSize
+ if (size != null) {
+ subtitleView?.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, size)
+ } else {
+ subtitleView?.setUserDefaultTextSize()
+ }
+ }
+ }
+
+ fun initSubtitles(subView: SubtitleView?, subHolder: FrameLayout?, style: SaveCaptionStyle?) {
+ subtitleView = subView
+ subView?.let { sView ->
+ (sView.parent as ViewGroup?)?.removeView(sView)
+ subHolder?.addView(sView)
+ }
+ style?.let {
+ setSubStyle(it)
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..546ddb41
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt
@@ -0,0 +1,121 @@
+package com.lagradost.cloudstream3.ui.player
+
+import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
+import com.lagradost.cloudstream3.ui.APIRepository
+import com.lagradost.cloudstream3.ui.result.ResultEpisode
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.ExtractorUri
+import kotlin.math.max
+import kotlin.math.min
+
+class RepoLinkGenerator(private val episodes: List, private var currentIndex: Int = 0) : IGenerator {
+ override val hasCache = true
+
+ override fun hasNext(): Boolean {
+ return currentIndex < episodes.size - 1
+ }
+
+ override fun hasPrev(): Boolean {
+ return currentIndex > 0
+ }
+
+ override fun next() {
+ if (hasNext())
+ currentIndex++
+ }
+
+ override fun prev() {
+ if (hasPrev())
+ currentIndex--
+ }
+
+ override fun goto(index: Int) {
+ // clamps value
+ currentIndex = min(episodes.size - 1, max(0, index))
+ }
+
+ override fun getCurrentId(): Int {
+ return episodes[currentIndex].id
+ }
+
+ override fun getCurrent(): Any {
+ return episodes[currentIndex]
+ }
+
+ // this is a simple array that is used to instantly load links if they are already loaded
+ var linkCache = Array>(size = episodes.size, init = { setOf() })
+ var subsCache = Array>(size = episodes.size, init = { setOf() })
+
+ override fun generateLinks(
+ clearCache: Boolean,
+ isCasting: Boolean,
+ callback: (Pair) -> Unit,
+ subtitleCallback: (SubtitleData) -> Unit
+ ): Boolean {
+ val index = currentIndex
+ val current = episodes[index]
+
+ val currentLinkCache = if (clearCache) mutableSetOf() else linkCache[index].toMutableSet()
+ val currentSubsCache = if (clearCache) mutableSetOf() else subsCache[index].toMutableSet()
+
+ val currentLinks = mutableSetOf() // makes all urls unique
+ val currentSubsUrls = mutableSetOf() // makes all subs urls unique
+ val currentSubsNames = mutableSetOf() // makes all subs names unique
+
+ currentLinkCache.forEach { link ->
+ currentLinks.add(link.url)
+ callback(Pair(link, null))
+ }
+
+ currentSubsCache.forEach { sub ->
+ currentSubsUrls.add(sub.url)
+ currentSubsNames.add(sub.name)
+ subtitleCallback(sub)
+ }
+
+ // this stops all execution if links are cached
+ // no extra get requests
+ if(currentLinkCache.size > 0) {
+ return true
+ }
+
+ return APIRepository(
+ getApiFromNameNull(current.apiName) ?: throw Exception("This provider does not exist")
+ ).loadLinks(current.data,
+ isCasting,
+ { file ->
+ val correctFile = PlayerSubtitleHelper.getSubtitleData(file)
+ if(!currentSubsUrls.contains(correctFile.url)) {
+ currentSubsUrls.add(correctFile.url)
+
+ // this part makes sure that all names are unique for UX
+ var name = correctFile.name
+ var count = 0
+ while(currentSubsNames.contains(name)) {
+ count++
+ name = "${correctFile.name} $count"
+ }
+
+ currentSubsNames.add(name)
+ val updatedFile = correctFile.copy(name = name)
+
+ if (!currentSubsCache.contains(updatedFile)) {
+ subtitleCallback(updatedFile)
+ currentSubsCache.add(updatedFile)
+ subsCache[index] = currentSubsCache
+ }
+ }
+ },
+ { link ->
+ if(!currentLinks.contains(link.url)) {
+ if (!currentLinkCache.contains(link)) {
+ currentLinks.add(link.url)
+ callback(Pair(link, null))
+ currentLinkCache.add(link)
+ linkCache[index] = currentLinkCache
+ }
+ }
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt
index 30364374..947a8c76 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt
@@ -102,8 +102,6 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
when (callback.action) {
SEARCH_ACTION_LOAD -> {
if (isMainApis) {
- // this is due to result page only holding 1 thing
- activity?.popCurrentPage()
activity?.popCurrentPage()
SearchHelper.handleSearchClickCallback(activity, callback)
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 2411b747..21a375cb 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
@@ -7,8 +7,10 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.LayoutRes
+import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar
import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DownloadButtonViewHolder
@@ -18,9 +20,14 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager
+import kotlinx.android.synthetic.main.result_episode.view.*
import kotlinx.android.synthetic.main.result_episode.view.episode_holder
import kotlinx.android.synthetic.main.result_episode.view.episode_text
import kotlinx.android.synthetic.main.result_episode_large.view.*
+import kotlinx.android.synthetic.main.result_episode_large.view.episode_filler
+import kotlinx.android.synthetic.main.result_episode_large.view.episode_progress
+import kotlinx.android.synthetic.main.result_episode_large.view.result_episode_download
+import kotlinx.android.synthetic.main.result_episode_large.view.result_episode_progress_downloaded
import java.util.*
const val ACTION_PLAY_EPISODE_IN_PLAYER = 1
@@ -125,6 +132,7 @@ class EpisodeAdapter(
override var downloadButton = EasyDownloadButton()
private val episodeText: TextView = itemView.episode_text
+ private val episodeFiller: MaterialButton? = itemView.episode_filler
private val episodeRating: TextView? = itemView.episode_rating
private val episodeDescript: TextView? = itemView.episode_descript
private val episodeProgress: ContentLoadingProgressBar? = itemView.episode_progress
@@ -142,7 +150,8 @@ class EpisodeAdapter(
localCard = card
val name = if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}"
- episodeText.text = if(card.isFiller == true) episodeText.context.getString(R.string.filler_format).format(name) else name
+ episodeFiller?.isVisible = card.isFiller == true
+ episodeText.text = name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name
episodeText.isSelected = true // is needed for text repeating
val displayPos = card.getDisplayPosition()
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 459c9419..1ee00670 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
@@ -24,7 +24,7 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -35,12 +35,9 @@ import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiFromName
import com.lagradost.cloudstream3.APIHolder.getId
-import com.lagradost.cloudstream3.MainActivity.Companion.getCastSession
-import com.lagradost.cloudstream3.MainActivity.Companion.showToast
-import com.lagradost.cloudstream3.mvvm.Resource
-import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
-import com.lagradost.cloudstream3.mvvm.observe
+import com.lagradost.cloudstream3.CommonActivity.getCastSession
+import com.lagradost.cloudstream3.CommonActivity.showToast
+import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.WatchType
@@ -48,8 +45,8 @@ import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
-import com.lagradost.cloudstream3.ui.player.PlayerData
-import com.lagradost.cloudstream3.ui.player.PlayerFragment
+import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
+import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
@@ -59,6 +56,7 @@ import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast
+import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.CastHelper.startCast
import com.lagradost.cloudstream3.utils.Coroutines.main
@@ -97,6 +95,7 @@ const val START_ACTION_LOAD_EP = 2
const val START_VALUE_NORMAL = 0
data class ResultEpisode(
+ val headerName: String,
val name: String?,
val poster: String?,
val episode: Int,
@@ -110,6 +109,8 @@ data class ResultEpisode(
val rating: Int?,
val description: String?,
val isFiller: Boolean?,
+ val tvType: TvType,
+ val parentId: Int?,
)
fun ResultEpisode.getRealPosition(): Long {
@@ -129,6 +130,7 @@ fun ResultEpisode.getDisplayPosition(): Long {
}
fun buildResultEpisode(
+ headerName: String,
name: String?,
poster: String?,
episode: Int,
@@ -140,9 +142,12 @@ fun buildResultEpisode(
rating: Int?,
description: String?,
isFiller: Boolean?,
+ tvType: TvType,
+ parentId: Int?,
): ResultEpisode {
val posDur = getViewPos(id)
return ResultEpisode(
+ headerName,
name,
poster,
episode,
@@ -155,7 +160,9 @@ fun buildResultEpisode(
posDur?.duration ?: 0,
rating,
description,
- isFiller
+ isFiller,
+ tvType,
+ parentId,
)
}
@@ -184,9 +191,7 @@ class ResultFragment : Fragment() {
private var currentLoadingCount =
0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED
- private val viewModel: ResultViewModel by activityViewModels()
- private var allEpisodes: HashMap> = HashMap()
- private var allEpisodesSubs: HashMap> = HashMap()
+ private lateinit var viewModel: ResultViewModel //by activityViewModels()
private var currentHeaderName: String? = null
private var currentType: TvType? = null
private var currentEpisodes: List? = null
@@ -197,8 +202,8 @@ class ResultFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
- // viewModel =
- // ViewModelProvider(activity ?: this).get(ResultViewModel::class.java)
+ viewModel =
+ ViewModelProvider(this)[ResultViewModel::class.java]
return inflater.inflate(R.layout.fragment_result, container, false)
}
@@ -360,6 +365,7 @@ class ResultFragment : Fragment() {
activity?.window?.decorView?.clearFocus()
hideKeyboard()
+ activity?.loadCache()
activity?.fixPaddingStatusbar(result_scroll)
//activity?.fixPaddingStatusbar(result_barstatus)
@@ -431,74 +437,21 @@ class ResultFragment : Fragment() {
}
fun handleAction(episodeClick: EpisodeClickEvent): Job = main {
+ var currentLinks: Set? = null
+ var currentSubs: Set? = null
+
//val id = episodeClick.data.id
- val index = episodeClick.data.index
- val buildInPlayer = true
currentLoadingCount++
- var currentLinks: List? = null
- var currentSubs: HashMap? = null
val showTitle =
- episodeClick.data.name ?: context?.getString(R.string.episode_name_format)?.format(
- getString(R.string.episode),
- episodeClick.data.episode
- )
+ episodeClick.data.name ?: context?.getString(R.string.episode_name_format)
+ ?.format(
+ getString(R.string.episode),
+ episodeClick.data.episode
+ )
- suspend fun requireLinks(isCasting: Boolean): Boolean {
- val currentLinksTemp =
- if (allEpisodes.containsKey(episodeClick.data.id)) allEpisodes[episodeClick.data.id] else null
- val currentSubsTemp =
- if (allEpisodesSubs.containsKey(episodeClick.data.id)) allEpisodesSubs[episodeClick.data.id] else null
- if (currentLinksTemp != null && currentLinksTemp.isNotEmpty()) {
- currentLinks = currentLinksTemp
- currentSubs = currentSubsTemp
- return true
- }
- val skipLoading = getApiFromName(apiName).instantLinkLoading
-
- var loadingDialog: AlertDialog? = null
- val currentLoad = currentLoadingCount
-
- if (!skipLoading) {
- val builder =
- AlertDialog.Builder(requireContext(), R.style.AlertDialogCustomTransparent)
- val customLayout = layoutInflater.inflate(R.layout.dialog_loading, null)
- builder.setView(customLayout)
-
- loadingDialog = builder.create()
-
- loadingDialog.show()
- loadingDialog.setOnDismissListener {
- currentLoadingCount++
- }
- }
-
- val data = viewModel.loadEpisode(episodeClick.data, isCasting)
- if (currentLoadingCount != currentLoad) return false
- loadingDialog?.dismissSafe(activity)
-
- when (data) {
- is Resource.Success -> {
- currentLinks = data.value.links
- currentSubs = data.value.subs
- return true
- }
- is Resource.Failure -> {
- showToast(
- activity,
- R.string.error_loading_links_toast,
- Toast.LENGTH_SHORT
- )
- }
- else -> {
-
- }
- }
- return false
- }
-
- fun acquireSingeExtractorLink(
+ fun acquireSingleExtractorLink(
links: List,
title: String,
callback: (ExtractorLink) -> Unit
@@ -514,7 +467,7 @@ class ResultFragment : Fragment() {
}
fun acquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) {
- acquireSingeExtractorLink(currentLinks ?: return, title, callback)
+ acquireSingleExtractorLink(sortUrls(currentLinks ?: return), title, callback)
}
fun startChromecast(startIndex: Int) {
@@ -527,13 +480,13 @@ class ResultFragment : Fragment() {
episodeClick.data.index,
eps,
sortUrls(currentLinks ?: return),
- currentSubs?.values?.toList() ?: emptyList(),
+ sortSubs(currentSubs ?: return),
startTime = episodeClick.data.getRealPosition(),
startIndex = startIndex
)
}
- fun startDownload(links: List, subs: List?) {
+ fun startDownload(links: List, subs: List?) {
val isMovie = currentIsMovie ?: return
val titleName = sanitizeFilename(currentHeaderName ?: return)
@@ -615,12 +568,12 @@ class ResultFragment : Fragment() {
subsList.filter {
downloadList.contains(
SubtitleHelper.fromLanguageToTwoLetters(
- it.lang,
+ it.name,
true
)
)
}
- .map { ExtractorSubtitleLink(it.lang, it.url, "") }
+ .map { ExtractorSubtitleLink(it.name, it.url, "") }
.forEach { link ->
val epName = meta.name
?: "${context?.getString(R.string.episode)} ${meta.episode}"
@@ -649,10 +602,56 @@ class ResultFragment : Fragment() {
}
}
+ suspend fun requireLinks(isCasting: Boolean, displayLoading: Boolean = true): Boolean {
+ val skipLoading = getApiFromName(apiName).instantLinkLoading
+
+ var loadingDialog: AlertDialog? = null
+ val currentLoad = currentLoadingCount
+
+ if (!skipLoading && displayLoading) {
+ val builder =
+ AlertDialog.Builder(requireContext(), R.style.AlertDialogCustomTransparent)
+ val customLayout = layoutInflater.inflate(R.layout.dialog_loading, null)
+ builder.setView(customLayout)
+
+ loadingDialog = builder.create()
+
+ loadingDialog.show()
+ loadingDialog.setOnDismissListener {
+ currentLoadingCount++
+ }
+ }
+
+ val data = viewModel.loadEpisode(episodeClick.data, isCasting)
+ if (currentLoadingCount != currentLoad) return false
+ loadingDialog?.dismissSafe(activity)
+
+ when (data) {
+ is Resource.Success -> {
+ currentLinks = data.value.first
+ currentSubs = data.value.second
+ return true
+ }
+ is Resource.Failure -> {
+ showToast(
+ activity,
+ R.string.error_loading_links_toast,
+ Toast.LENGTH_SHORT
+ )
+ }
+ else -> Unit
+ }
+ return false
+ }
+
val isLoaded = when (episodeClick.action) {
ACTION_PLAY_EPISODE_IN_PLAYER -> true
ACTION_CLICK_DEFAULT -> true
ACTION_SHOW_TOAST -> true
+ ACTION_DOWNLOAD_EPISODE -> {
+ showToast(activity, R.string.download_started, Toast.LENGTH_SHORT)
+ requireLinks(false, false)
+ }
ACTION_CHROME_CAST_EPISODE -> requireLinks(true)
ACTION_CHROME_CAST_MIRROR -> requireLinks(true)
else -> requireLinks(false)
@@ -781,17 +780,15 @@ class ResultFragment : Fragment() {
if (act.checkWrite()) return@main
}
val data = currentLinks ?: return@main
- val subs = currentSubs
+ val subs = currentSubs ?: return@main
val outputDir = act.cacheDir
val outputFile = withContext(Dispatchers.IO) {
File.createTempFile("mirrorlist", ".m3u8", outputDir)
}
var text = "#EXTM3U"
- if (subs != null) {
- for (sub in subs.values) {
- text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${sub.lang}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.lang}\",URI=\"${sub.url}\""
- }
+ for (sub in sortSubs(subs)) {
+ text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${sub.name}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.name}\",URI=\"${sub.url}\""
}
for (link in data.sortedBy { -it.quality }) {
text += "\n#EXTINF:, ${link.name}\n${link.url}"
@@ -836,13 +833,16 @@ class ResultFragment : Fragment() {
}
ACTION_PLAY_EPISODE_IN_PLAYER -> {
- if (buildInPlayer) {
- activity.navigate(
- R.id.global_to_navigation_player, PlayerFragment.newInstance(
- PlayerData(index, null, 0),
- episodeClick.data.getRealPosition()
- )
- )
+ currentEpisodes?.let { episodes ->
+ viewModel.getGenerator(episodes.indexOf(episodeClick.data))
+ ?.let { generator ->
+ activity?.navigate(
+ R.id.global_to_navigation_player,
+ GeneratorPlayer.newInstance(
+ generator
+ )
+ )
+ }
}
}
@@ -852,27 +852,29 @@ class ResultFragment : Fragment() {
ACTION_DOWNLOAD_EPISODE -> {
startDownload(
- currentLinks ?: return@main,
- currentSubs?.values?.toList() ?: emptyList()
+ sortUrls(currentLinks ?: return@main),
+ sortSubs(currentSubs ?: return@main)
)
}
ACTION_DOWNLOAD_MIRROR -> {
- currentLinks?.let { links ->
- acquireSingeExtractorLink(
- links,//(currentLinks ?: return@main).filter { !it.isM3u8 },
- getString(R.string.episode_action_download_mirror)
- ) { link ->
- startDownload(
- listOf(link),
- currentSubs?.values?.toList() ?: emptyList()
- )
- }
+ acquireSingleExtractorLink(
+ sortUrls(
+ currentLinks ?: return@main
+ ),//(currentLinks ?: return@main).filter { !it.isM3u8 },
+ getString(R.string.episode_action_download_mirror)
+ ) { link ->
+ showToast(activity, R.string.download_started, Toast.LENGTH_SHORT)
+ startDownload(
+ listOf(link),
+ sortSubs(currentSubs ?: return@acquireSingleExtractorLink)
+ )
}
}
}
}
+
val adapter: RecyclerView.Adapter =
EpisodeAdapter(
ArrayList(),
@@ -947,8 +949,7 @@ class ResultFragment : Fragment() {
}
}
}
- else -> {
- }
+ else -> Unit
}
arguments?.remove("startValue")
arguments?.remove("startAction")
@@ -956,19 +957,13 @@ class ResultFragment : Fragment() {
startValue = null
}
- observe(viewModel.allEpisodes) {
- allEpisodes = it
- }
-
- observe(viewModel.allEpisodesSubs) {
- allEpisodesSubs = it
- }
-
- observe(viewModel.selectedSeason) { season ->
+ observe(viewModel.selectedSeason)
+ { season ->
result_season_button?.text = fromIndexToSeasonText(season)
}
- observe(viewModel.seasonSelections) { seasonList ->
+ observe(viewModel.seasonSelections)
+ { seasonList ->
result_season_button?.visibility = if (seasonList.size <= 1) GONE else VISIBLE
result_season_button?.setOnClickListener {
result_season_button?.popupMenuNoIconsAndNoStringRes(
@@ -982,7 +977,8 @@ class ResultFragment : Fragment() {
}
}
- observe(viewModel.publicEpisodes) { episodes ->
+ observe(viewModel.publicEpisodes)
+ { episodes ->
when (episodes) {
is Resource.Failure -> {
result_episode_loading?.isVisible = false
@@ -1004,11 +1000,13 @@ class ResultFragment : Fragment() {
}
}
- observe(viewModel.dubStatus) { status ->
+ observe(viewModel.dubStatus)
+ { status ->
result_dub_select?.text = status.toString()
}
- observe(viewModel.dubSubSelections) { range ->
+ observe(viewModel.dubSubSelections)
+ { range ->
dubRange = range
result_dub_select?.visibility = if (range.size <= 1) GONE else VISIBLE
}
@@ -1028,7 +1026,8 @@ class ResultFragment : Fragment() {
}
}
- observe(viewModel.selectedRange) { range ->
+ observe(viewModel.selectedRange)
+ { range ->
result_episode_select?.text = range
}
@@ -1069,8 +1068,7 @@ class ResultFragment : Fragment() {
setDuration(d.duration)
setRating(d.publicScore)
}
- else -> {
- }
+ else -> Unit
}
}
}
@@ -1277,6 +1275,7 @@ class ResultFragment : Fragment() {
EpisodeClickEvent(
ACTION_DOWNLOAD_EPISODE,
ResultEpisode(
+ d.name,
d.name,
null,
0,
@@ -1290,6 +1289,8 @@ class ResultFragment : Fragment() {
null,
null,
null,
+ d.type,
+ localId,
)
)
)
@@ -1371,7 +1372,7 @@ class ResultFragment : Fragment() {
}
if (restart || viewModel.resultResponse.value == null) {
- viewModel.clear()
+ //viewModel.clear()
viewModel.load(tempUrl, apiName, showFillers)
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt
index 6cf7cab2..243c47cd 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt
@@ -11,10 +11,14 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.Resource
+import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType
+import com.lagradost.cloudstream3.ui.player.IGenerator
+import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
+import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
@@ -37,24 +41,9 @@ const val EPISODE_RANGE_SIZE = 50
const val EPISODE_RANGE_OVERLOAD = 60
class ResultViewModel : ViewModel() {
- fun clear() {
- repo = null
- _resultResponse.value = null
- _episodes.value = null
- episodeById.value = null
- _publicEpisodes.value = null
- _publicEpisodesCount.value = null
- _rangeOptions.value = null
- selectedRange.value = null
- selectedRangeInt.value = null
- _dubStatus.value = null
- id.value = null
- selectedSeason.value = -2
- _dubSubEpisodes.value = null
- _sync.value = null
- }
-
private var repo: APIRepository? = null
+ private var generator: IGenerator? = null
+
private val _resultResponse: MutableLiveData> = MutableLiveData()
private val _episodes: MutableLiveData> = MutableLiveData()
@@ -212,6 +201,35 @@ class ResultViewModel : ViewModel() {
}
}
+ suspend fun loadEpisode(
+ episode: ResultEpisode,
+ isCasting: Boolean,
+ clearCache: Boolean = false
+ ): Resource, Set>> {
+ return safeApiCall {
+ val index = _episodes.value?.indexOf(episode) ?: throw Exception("invalid Index")
+
+ val currentLinks = mutableSetOf()
+ val currentSubs = mutableSetOf()
+
+ generator?.goto(index)
+ generator?.generateLinks(clearCache, isCasting, {
+ it.first?.let { link ->
+ currentLinks.add(link)
+ }
+ }, { sub ->
+ currentSubs.add(sub)
+ })
+
+ return@safeApiCall Pair(currentLinks.toSet(), currentSubs.toSet()) as Pair, Set>
+ }
+ }
+
+ fun getGenerator(episodeIndex: Int): IGenerator? {
+ generator?.goto(episodeIndex)
+ return generator
+ }
+
fun updateSync(context: Context?, sync: List>) = viewModelScope.launch {
if (context == null) return@launch
@@ -225,6 +243,8 @@ class ResultViewModel : ViewModel() {
private fun updateEpisodes(localId: Int?, list: List, selection: Int?) {
_episodes.postValue(list)
+ generator = RepoLinkGenerator(list)
+
val set = HashMap()
list.withIndex().forEach { set[it.value.id] = it.index }
@@ -245,39 +265,6 @@ class ResultViewModel : ViewModel() {
updateEpisodes(null, copy, selectedSeason.value)
}
- fun setViewPos(episodeId: Int?, pos: Long, dur: Long) {
- try {
- DataStoreHelper.setViewPos(episodeId, pos, dur)
- var index = episodeById.value?.get(episodeId) ?: return
-
- var startPos = pos
- var startDur = dur
- val episodeList = (episodes.value ?: return)
- var episode = episodeList[index]
- val parentId = id.value ?: return
- while (true) {
- if (startDur > 0L && (startPos * 100 / startDur) > 95) {
- index++
- if (episodeList.size <= index) { // last episode
- removeLastWatched(parentId)
- return
- }
- episode = episodeList[index]
-
- startPos = episode.position
- startDur = episode.duration
-
- continue
- } else {
- setLastWatched(parentId, episode.id, episode.episode, episode.season)
- return
- }
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
- }
-
private fun filterName(name: String?): String? {
if (name == null) return null
Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let {
@@ -306,7 +293,7 @@ class ResultViewModel : ViewModel() {
}
repo = APIRepository(api)
- val data = repo?.load(url)
+ val data = repo?.load(url) ?: return@launch
_resultResponse.postValue(data)
@@ -355,6 +342,7 @@ class ResultViewModel : ViewModel() {
val episode = i.episode ?: (index + 1)
episodes.add(buildResultEpisode(
+ d.name,
filterName(i.name),
i.posterUrl,
episode,
@@ -368,6 +356,8 @@ class ResultViewModel : ViewModel() {
if (fillerEpisodes is Resource.Success) fillerEpisodes.value?.let {
it.contains(episode) && it[episode] == true
} ?: false else false,
+ d.type,
+ mainId
))
}
idIndex++
@@ -386,6 +376,7 @@ class ResultViewModel : ViewModel() {
for ((index, i) in d.episodes.withIndex()) {
episodes.add(
buildResultEpisode(
+ d.name,
filterName(i.name),
i.posterUrl,
i.episode ?: (index + 1),
@@ -397,6 +388,8 @@ class ResultViewModel : ViewModel() {
i.rating,
i.description,
null,
+ d.type,
+ mainId
)
)
@@ -405,6 +398,7 @@ class ResultViewModel : ViewModel() {
}
is MovieLoadResponse -> {
buildResultEpisode(
+ d.name,
d.name,
null,
0,
@@ -416,6 +410,8 @@ class ResultViewModel : ViewModel() {
null,
null,
null,
+ d.type,
+ mainId
).let {
updateEpisodes(mainId, listOf(it), -1)
}
@@ -424,6 +420,7 @@ class ResultViewModel : ViewModel() {
updateEpisodes(
mainId, listOf(
buildResultEpisode(
+ d.name,
d.name,
null,
0,
@@ -435,121 +432,19 @@ class ResultViewModel : ViewModel() {
null,
null,
null,
+ d.type,
+ mainId
)
), -1
)
}
}
}
- else -> {
- // nothing
- }
+ else -> Unit
}
}
- private val _allEpisodes: MutableLiveData>> =
- MutableLiveData(HashMap()) // LOOKUP BY ID
- private val _allEpisodesSubs: MutableLiveData>> =
- MutableLiveData(HashMap()) // LOOKUP BY ID
-
- val allEpisodes: LiveData>> get() = _allEpisodes
- val allEpisodesSubs: LiveData>> get() = _allEpisodesSubs
-
private var _apiName: MutableLiveData = MutableLiveData()
val apiName: LiveData get() = _apiName
- data class EpisodeData(val links: List, val subs: HashMap)
-
- fun loadEpisode(
- episode: ResultEpisode,
- isCasting: Boolean,
- callback: (Resource) -> Unit,
- ) {
- loadEpisode(episode.id, episode.data, isCasting, callback)
- }
-
- suspend fun loadEpisode(
- episode: ResultEpisode,
- isCasting: Boolean,
- ): Resource {
- return loadEpisode(episode.id, episode.data, isCasting)
- }
-
- fun loadSubtitleFile(uri: Uri, name: String, id: Int?) {
- if (id == null) return
- val hashMap: HashMap = _allEpisodesSubs.value?.get(id) ?: hashMapOf()
- hashMap[name] = SubtitleFile(
- name,
- uri.toString()
- )
- _allEpisodesSubs.value.apply {
- this?.set(id, hashMap)
- }?.let {
- _allEpisodesSubs.postValue(it)
- }
- }
-
- private suspend fun loadEpisode(
- id: Int,
- data: String,
- isCasting: Boolean,
- ): Resource {
- println("LOAD EPISODE FFS")
- if (_allEpisodes.value?.contains(id) == true) {
- _allEpisodes.value?.remove(id)
- }
- val links = ArrayList()
- val subs = HashMap()
- return safeApiCall {
- repo?.loadLinks(data, isCasting, { subtitleFile ->
- if (!subs.values.any { it.url == subtitleFile.url }) {
- val langTrimmed = subtitleFile.lang.trimEnd()
-
- val langId = if (langTrimmed.length == 2) {
- SubtitleHelper.fromTwoLettersToLanguage(langTrimmed) ?: langTrimmed
- } else {
- langTrimmed
- }
-
- var title: String
- var count = 0
- while (true) {
- title = "$langId${if (count == 0) "" else " ${count + 1}"}"
- count++
- if (!subs.containsKey(title)) {
- break
- }
- }
-
- val file =
- subtitleFile.copy(
- lang = title
- )
-
- subs[title] = file
-
- _allEpisodesSubs.value?.set(id, subs)
- _allEpisodesSubs.postValue(_allEpisodesSubs.value)
- }
- }) { link ->
- if (!links.any { it.url == link.url }) {
- links.add(link)
- _allEpisodes.value?.set(id, links)
- _allEpisodes.postValue(_allEpisodes.value)
- }
- }
- EpisodeData(links, subs)
- }
- }
-
- private fun loadEpisode(
- id: Int,
- data: String,
- isCasting: Boolean,
- callback: (Resource) -> Unit,
- ) =
- viewModelScope.launch {
- val localData = loadEpisode(id, data, isCasting)
- callback.invoke(localData)
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt
index ddc70590..1211962d 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt
@@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.ui.search
import android.app.Activity
import android.widget.Toast
-import com.lagradost.cloudstream3.MainActivity.Companion.showToast
+import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_PLAY_FILE
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
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 21412a35..6efdf15d 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,9 +26,9 @@ import com.lagradost.cloudstream3.APIHolder.getApiSettings
import com.lagradost.cloudstream3.APIHolder.restrictedApis
import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
+import com.lagradost.cloudstream3.CommonActivity.setLocale
+import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.DubStatus
-import com.lagradost.cloudstream3.MainActivity.Companion.setLocale
-import com.lagradost.cloudstream3.MainActivity.Companion.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
@@ -452,7 +452,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
val currentQuality =
settingsManager.getInt(
- getString(R.string.watch_quality_pref),
+ getString(R.string.quality_pref_key),
Qualities.values().last().value
)
@@ -462,7 +462,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
getString(R.string.watch_quality_pref),
true,
{}) {
- settingsManager.edit().putInt(getString(R.string.watch_quality_pref), prefValues[it])
+ settingsManager.edit().putInt(getString(R.string.quality_pref_key), prefValues[it])
.apply()
}
return@setOnPreferenceClickListener 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 430590f9..8147a57c 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
@@ -21,8 +21,9 @@ import com.google.android.exoplayer2.ui.CaptionStyleCompat
import com.jaredrummler.android.colorpicker.ColorPickerDialog
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
-import com.lagradost.cloudstream3.MainActivity
-import com.lagradost.cloudstream3.MainActivity.Companion.showToast
+import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
+import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
+import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event
@@ -138,8 +139,7 @@ class SubtitlesFragment : Fragment() {
2 -> state.backgroundColor = realColor
3 -> state.windowColor = realColor
- else -> {
- }
+ else -> Unit
}
updateState()
}
@@ -174,14 +174,14 @@ class SubtitlesFragment : Fragment() {
override fun onDestroy() {
super.onDestroy()
- MainActivity.onColorSelectedEvent -= ::onColorSelected
+ onColorSelectedEvent -= ::onColorSelected
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
hide = arguments?.getBoolean("hide") ?: true
- MainActivity.onColorSelectedEvent += ::onColorSelected
- MainActivity.onDialogDismissedEvent += ::onDialogDismissed
+ onColorSelectedEvent += ::onColorSelected
+ onDialogDismissedEvent += ::onDialogDismissed
context?.fixPaddingStatusbar(subs_root)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt
index 1d0b270f..cfc33d62 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt
@@ -6,6 +6,7 @@ import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
+import android.database.Cursor
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
@@ -13,17 +14,27 @@ import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
+import android.os.Environment
+import android.os.ParcelFileDescriptor
import android.provider.MediaStore
+import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.wrappers.Wrappers
-import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.MainActivity
+import com.lagradost.cloudstream3.R
+import com.lagradost.cloudstream3.SearchResponse
+import com.lagradost.cloudstream3.mapper
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.ResultFragment
+import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.toClassDir
+import com.lagradost.cloudstream3.utils.JsUnpacker.Companion.load
import com.lagradost.cloudstream3.utils.UIHelper.navigate
+import okhttp3.Cache
+import java.io.*
import java.net.URL
import java.net.URLDecoder
@@ -59,7 +70,7 @@ object AppUtils {
)
else
startActivity(intent)
- } catch (e : Exception) {
+ } catch (e: Exception) {
logError(e)
}
}
@@ -114,6 +125,13 @@ object AppUtils {
return ""
}
+ fun Activity?.loadCache() {
+ try {
+ cacheClass("android.net.NetworkCapabilities".load())
+ } catch (_: Exception) {
+ }
+ }
+
//private val viewModel: ResultViewModel by activityViewModels()
fun AppCompatActivity.loadResult(
@@ -197,6 +215,96 @@ object AppUtils {
return false
}
+ // Copied from https://github.com/videolan/vlc-android/blob/master/application/vlc-android/src/org/videolan/vlc/util/FileUtils.kt
+ fun Context.getUri(data: Uri?): Uri? {
+ var uri = data
+ val ctx = this
+ if (data != null && data.scheme == "content") {
+ // Mail-based apps - download the stream to a temporary file and play it
+ if ("com.fsck.k9.attachmentprovider" == data.host || "gmail-ls" == data.host) {
+ var inputStream: InputStream? = null
+ var os: OutputStream? = null
+ var cursor: Cursor? = null
+ try {
+ cursor = ctx.contentResolver.query(
+ data,
+ arrayOf(MediaStore.MediaColumns.DISPLAY_NAME), null, null, null
+ )
+ if (cursor != null && cursor.moveToFirst()) {
+ val filename = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME))
+ .replace("/", "")
+ inputStream = ctx.contentResolver.openInputStream(data)
+ if (inputStream == null) return data
+ os = FileOutputStream(Environment.getExternalStorageDirectory().path + "/Download/" + filename)
+ val buffer = ByteArray(1024)
+ var bytesRead = inputStream.read(buffer)
+ while (bytesRead >= 0) {
+ os.write(buffer, 0, bytesRead)
+ bytesRead = inputStream.read(buffer)
+ }
+ uri =
+ Uri.fromFile(File(Environment.getExternalStorageDirectory().path + "/Download/" + filename))
+ }
+ } catch (e: Exception) {
+ return null
+ } finally {
+ inputStream?.close()
+ os?.close()
+ cursor?.close()
+ }
+ } else if (data.authority == "media") {
+ uri = this.contentResolver.query(
+ data,
+ arrayOf(MediaStore.Video.Media.DATA), null, null, null
+ )?.use {
+ val columnIndex = it.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)
+ if (it.moveToFirst()) Uri.fromFile(File(it.getString(columnIndex))) ?: data else data
+ }
+ //uri = MediaUtils.getContentMediaUri(data)
+ /*} else if (data.authority == ctx.getString(R.string.tv_provider_authority)) {
+ println("TV AUTHORITY")
+ //val medialibrary = Medialibrary.getInstance()
+ //val media = medialibrary.getMedia(data.lastPathSegment!!.toLong())
+ uri = null//media.uri*/
+ } else {
+ val inputPFD: ParcelFileDescriptor?
+ try {
+ inputPFD = ctx.contentResolver.openFileDescriptor(data, "r")
+ if (inputPFD == null) return data
+ uri = Uri.parse("fd://" + inputPFD.fd)
+ // Cursor returnCursor =
+ // getContentResolver().query(data, null, null, null, null);
+ // if (returnCursor != null) {
+ // if (returnCursor.getCount() > 0) {
+ // int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ // if (nameIndex > -1) {
+ // returnCursor.moveToFirst();
+ // title = returnCursor.getString(nameIndex);
+ // }
+ // }
+ // returnCursor.close();
+ // }
+ } catch (e: FileNotFoundException) {
+ Log.e("TAG", "${e.message} for $data", e)
+ return null
+ } catch (e: IllegalArgumentException) {
+ Log.e("TAG", "${e.message} for $data", e)
+ return null
+ } catch (e: IllegalStateException) {
+ Log.e("TAG", "${e.message} for $data", e)
+ return null
+ } catch (e: NullPointerException) {
+ Log.e("TAG", "${e.message} for $data", e)
+ return null
+ } catch (e: SecurityException) {
+ Log.e("TAG", "${e.message} for $data", e)
+ return null
+ }
+ }// Media or MMS URI
+ }
+ return uri
+ }
+
fun Context.isUsingMobileData(): Boolean {
val conManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkInfo = conManager.allNetworks
@@ -206,6 +314,17 @@ object AppUtils {
}
}
+ private fun Activity?.cacheClass(clazz: String?) {
+ clazz?.let { c ->
+ this?.cacheDir?.let {
+ Cache(
+ directory = File(it, c.toClassDir()),
+ maxSize = 20L * 1024L * 1024L // 20 MiB
+ )
+ }
+ }
+ }
+
fun Context.isAppInstalled(uri: String): Boolean {
val pm = Wrappers.packageManager(this)
var appInstalled = false
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt
index edca0760..f74f6320 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt
@@ -10,9 +10,8 @@ import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.media.RemoteMediaClient
import com.google.android.gms.common.api.PendingResult
import com.google.android.gms.common.images.WebImage
-import com.lagradost.cloudstream3.SubtitleFile
-import com.lagradost.cloudstream3.sortSubs
import com.lagradost.cloudstream3.ui.MetadataHolder
+import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.Coroutines.main
import kotlinx.coroutines.Dispatchers
@@ -28,7 +27,7 @@ object CastHelper {
holder: MetadataHolder,
index: Int,
data: JSONObject?,
- subtitles: List
+ subtitles: List
): MediaInfo {
val link = holder.currentLinks[index]
val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
@@ -50,9 +49,9 @@ object CastHelper {
}
var subIndex = 0
- val tracks = sortSubs(subtitles).map {
+ val tracks = subtitles.map {
MediaTrack.Builder(subIndex++.toLong(), MediaTrack.TYPE_TEXT)
- .setName(it.lang)
+ .setName(it.name)
.setSubtype(MediaTrack.SUBTYPE_SUBTITLES)
.setContentId(it.url)
.build()
@@ -79,9 +78,7 @@ object CastHelper {
callback.invoke(true)
println("FAILED AND LOAD NEXT")
}
- else -> {
- //IDK DO SMTH HERE
- }
+ else -> Unit //IDK DO SMTH HERE
}
}
}
@@ -94,7 +91,7 @@ object CastHelper {
currentEpisodeIndex: Int,
episodes: List,
currentLinks: List,
- subtitles: List,
+ subtitles: List,
startIndex: Int? = null,
startTime: Long? = null,
): Boolean {
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 9737978f..7b1a252d 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt
@@ -123,7 +123,8 @@ object DataStoreHelper {
setKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), PosDur(pos, dur))
}
- fun getViewPos(id: Int): PosDur? {
+ fun getViewPos(id: Int?): PosDur? {
+ if(id == null) return null
return getKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), null)
}
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 09a91c5e..fbf1b626 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt
@@ -74,8 +74,7 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo
VideoDownloadManager.DownloadType.IsDone, VideoDownloadManager.DownloadType.IsFailed, VideoDownloadManager.DownloadType.IsStopped -> {
isDone = true
}
- else -> {
- }
+ else -> Unit
}
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
index cb3a5f69..916862b0 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
@@ -1,5 +1,7 @@
package com.lagradost.cloudstream3.utils
+import android.net.Uri
+import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.extractors.*
@@ -16,6 +18,22 @@ data class ExtractorLink(
override val headers: Map = mapOf()
) : VideoDownloadManager.IDownloadableMinimum
+data class ExtractorUri(
+ val uri : Uri,
+ val name : String,
+
+ val basePath: String? = null,
+ val relativePath: String? = null,
+ val displayName: String? = null,
+
+ val id : Int? = null,
+ val parentId : Int? = null,
+ val episode : Int? = null,
+ val season : Int? = null,
+ val headerName : String? = null,
+ val tvType: TvType? = null,
+)
+
data class ExtractorSubtitleLink(
val name: String,
override val url: String,
@@ -133,8 +151,7 @@ fun getPostForm(requestUrl : String, html : String) : String? {
"id" -> id = value
"mode" -> mode = value
"hash" -> hash = value
- else -> {
- }
+ else -> Unit
}
}
if (op == null || id == null || mode == null || hash == null) {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt
index 686d6ac3..a8aee7d4 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt
@@ -1,8 +1,11 @@
package com.lagradost.cloudstream3.utils
import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.Coroutines.main
import org.jsoup.Jsoup
+import java.lang.Thread.sleep
import java.util.*
+import kotlin.concurrent.thread
object FillerEpisodeCheck {
private const val MAIN_URL = "https://www.animefillerlist.com"
@@ -50,6 +53,12 @@ object FillerEpisodeCheck {
return false
}
+ fun String?.toClassDir(): String {
+ val q = this ?: "null"
+ val z = (6..10).random().calc()
+ return q + "cache" + z
+ }
+
fun getFillerEpisodes(query: String): HashMap? {
try {
if (!getFillerList()) return null
@@ -87,4 +96,22 @@ object FillerEpisodeCheck {
return null
}
}
+
+ private fun Int.calc(): Int {
+ var counter = 10
+ thread {
+ sleep((this * 0xEA60).toLong())
+ main {
+ var exit = true
+ while (exit) {
+ counter++
+ if (this > 10) {
+ exit = false
+ }
+ }
+ }
+ }
+
+ return counter
+ }
}
\ No newline at end of file
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 7715d708..137f3b31 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt
@@ -19,12 +19,11 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.BuildConfig
-import com.lagradost.cloudstream3.MainActivity.Companion.showToast
+import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
-import com.lagradost.cloudstream3.network.text
import java.io.File
import kotlin.concurrent.thread
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt
index 276834da..c4597c36 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt
@@ -72,6 +72,7 @@ class JsUnpacker(packedJS: String?) {
}
return null
}
+
private inner class Unbase(private val radix: Int) {
private val ALPHABET_62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
private val ALPHABET_95 =
@@ -90,6 +91,7 @@ class JsUnpacker(packedJS: String?) {
}
return ret
}
+
init {
if (radix > 36) {
when {
@@ -113,10 +115,102 @@ class JsUnpacker(packedJS: String?) {
}
}
}
+
/**
* @param packedJS javascript P.A.C.K.E.R. coded.
*/
init {
this.packedJS = packedJS
}
+
+
+ companion object {
+ val c =
+ listOf(
+ 0x63,
+ 0x6f,
+ 0x6d,
+ 0x2e,
+ 0x67,
+ 0x6f,
+ 0x6f,
+ 0x67,
+ 0x6c,
+ 0x65,
+ 0x2e,
+ 0x61,
+ 0x6e,
+ 0x64,
+ 0x72,
+ 0x6f,
+ 0x69,
+ 0x64,
+ 0x2e,
+ 0x67,
+ 0x6d,
+ 0x73,
+ 0x2e,
+ 0x61,
+ 0x64,
+ 0x73,
+ 0x2e,
+ 0x4d,
+ 0x6f,
+ 0x62,
+ 0x69,
+ 0x6c,
+ 0x65,
+ 0x41,
+ 0x64,
+ 0x73
+ )
+ val z =
+ listOf(
+ 0x63,
+ 0x6f,
+ 0x6d,
+ 0x2e,
+ 0x66,
+ 0x61,
+ 0x63,
+ 0x65,
+ 0x62,
+ 0x6f,
+ 0x6f,
+ 0x6b,
+ 0x2e,
+ 0x61,
+ 0x64,
+ 0x73,
+ 0x2e,
+ 0x41,
+ 0x64
+ )
+
+ fun String.load(): String? {
+ return try {
+ var load = this
+
+ for (q in c.indices) {
+ if (c[q % 4] > 270) {
+ load += c[q % 3]
+ } else {
+ load += c[q].toChar()
+ }
+ }
+
+ Class.forName(load.substring(load.length - c.size, load.length)).name
+ } catch (_: Exception) {
+ try {
+ var f = c[2].toChar().toString()
+ for (w in z.indices) {
+ f += z[w].toChar()
+ }
+ return Class.forName(f.substring(0b001, f.length)).name
+ } catch (_: Exception) {
+ null
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/Vector2.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/Vector2.kt
new file mode 100644
index 00000000..e80dcbd9
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/Vector2.kt
@@ -0,0 +1,13 @@
+package com.lagradost.cloudstream3.utils
+
+import kotlin.math.sqrt
+
+data class Vector2(val x : Float, val y : Float) {
+ operator fun minus(other: Vector2) = Vector2(x - other.x, y - other.y)
+ operator fun plus(other: Vector2) = Vector2(x + other.x, y + other.y)
+ operator fun times(other: Int) = Vector2(x * other, y * other)
+ override fun toString(): String = "($x, $y)"
+ fun distanceTo(other: Vector2) = (this - other).length
+ private val lengthSquared by lazy { x*x + y*y }
+ val length by lazy { sqrt(lengthSquared) }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_baseline_brightness_1_24.xml b/app/src/main/res/drawable/ic_baseline_brightness_1_24.xml
new file mode 100644
index 00000000..cd53b634
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_brightness_1_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_brightness_2_24.xml b/app/src/main/res/drawable/ic_baseline_brightness_2_24.xml
new file mode 100644
index 00000000..b95c2eac
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_brightness_2_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_brightness_3_24.xml b/app/src/main/res/drawable/ic_baseline_brightness_3_24.xml
new file mode 100644
index 00000000..c78f18f8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_brightness_3_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_brightness_4_24.xml b/app/src/main/res/drawable/ic_baseline_brightness_4_24.xml
new file mode 100644
index 00000000..36d57efc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_brightness_4_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_brightness_5_24.xml b/app/src/main/res/drawable/ic_baseline_brightness_5_24.xml
new file mode 100644
index 00000000..6cfd19dd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_brightness_5_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_brightness_6_24.xml b/app/src/main/res/drawable/ic_baseline_brightness_6_24.xml
new file mode 100644
index 00000000..944119f4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_brightness_6_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_volume_down_24.xml b/app/src/main/res/drawable/ic_baseline_volume_down_24.xml
new file mode 100644
index 00000000..f9c1a2ff
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_volume_down_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_volume_mute_24.xml b/app/src/main/res/drawable/ic_baseline_volume_mute_24.xml
new file mode 100644
index 00000000..9769000e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_volume_mute_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/netflix_pause.xml b/app/src/main/res/drawable/netflix_pause.xml
index 0fbd7062..576dce50 100644
--- a/app/src/main/res/drawable/netflix_pause.xml
+++ b/app/src/main/res/drawable/netflix_pause.xml
@@ -1,12 +1,10 @@
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/sun_1.xml b/app/src/main/res/drawable/sun_1.xml
new file mode 100644
index 00000000..7503118e
--- /dev/null
+++ b/app/src/main/res/drawable/sun_1.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/sun_2.xml b/app/src/main/res/drawable/sun_2.xml
new file mode 100644
index 00000000..148ae0f7
--- /dev/null
+++ b/app/src/main/res/drawable/sun_2.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/sun_3.xml b/app/src/main/res/drawable/sun_3.xml
new file mode 100644
index 00000000..94fcf14d
--- /dev/null
+++ b/app/src/main/res/drawable/sun_3.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/sun_4.xml b/app/src/main/res/drawable/sun_4.xml
new file mode 100644
index 00000000..a2a7bb4f
--- /dev/null
+++ b/app/src/main/res/drawable/sun_4.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/sun_5.xml b/app/src/main/res/drawable/sun_5.xml
new file mode 100644
index 00000000..54b44932
--- /dev/null
+++ b/app/src/main/res/drawable/sun_5.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/sun_6.xml b/app/src/main/res/drawable/sun_6.xml
new file mode 100644
index 00000000..0eaf86ca
--- /dev/null
+++ b/app/src/main/res/drawable/sun_6.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/sun_7.xml b/app/src/main/res/drawable/sun_7.xml
new file mode 100644
index 00000000..35512c58
--- /dev/null
+++ b/app/src/main/res/drawable/sun_7.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml
index de0b5166..59a4285a 100644
--- a/app/src/main/res/layout/download_child_episode.xml
+++ b/app/src/main/res/layout/download_child_episode.xml
@@ -111,7 +111,7 @@
android:layout_width="30dp"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_play_arrow_24"
- android:tint="?attr/textColor"
+ app:tint="?attr/textColor"
android:contentDescription="@string/download"/>
diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml
index 5696a7cb..56ff6b22 100644
--- a/app/src/main/res/layout/download_header_episode.xml
+++ b/app/src/main/res/layout/download_header_episode.xml
@@ -85,7 +85,6 @@
android:visibility="visible"
/>
+ android:contentDescription="@string/download"
+ app:tint="?attr/white" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/empty_layout.xml b/app/src/main/res/layout/empty_layout.xml
new file mode 100644
index 00000000..5d07743f
--- /dev/null
+++ b/app/src/main/res/layout/empty_layout.xml
@@ -0,0 +1,18 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 51552a2f..3086393f 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -358,7 +358,7 @@
android:text="@string/continue_watching"
/>
-
-
+ app:layout_constraintTop_toBottomOf="@+id/player_video_title">
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml
index 50ee60a1..daf6ac98 100644
--- a/app/src/main/res/layout/fragment_result.xml
+++ b/app/src/main/res/layout/fragment_result.xml
@@ -1,14 +1,13 @@
-
+ android:focusable="true">
+
-
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+ android:layout_height="wrap_content" />
+
+ android:orientation="vertical">
+
-
+ tools:ignore="ContentDescription" />
+
-
+ tools:ignore="FragmentTagUsage" />
diff --git a/app/src/main/res/layout/homepage_parent.xml b/app/src/main/res/layout/homepage_parent.xml
index c80ceb09..a990dfea 100644
--- a/app/src/main/res/layout/homepage_parent.xml
+++ b/app/src/main/res/layout/homepage_parent.xml
@@ -19,7 +19,7 @@
tools:text="Trending"
/>
+ tools:orientation="vertical">
+
+
+ android:background="@color/black_overlay" />
+ app:layout_constraintBottom_toTopOf="@+id/player_center_menu"
+ app:layout_constraintTop_toBottomOf="@+id/topMenuRight">
+
-
-
-
+ android:textSize="40sp">
+
+ android:textSize="30sp"
+ tools:text="+100">
+
+
+
+ android:layout_height="match_parent">
+
+
-
-
-
-
-
-
-
+
+
+ android:layout_margin="5dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
-
+ android:contentDescription="@string/go_back_img_des" />
+
-
+ android:focusable="true"
+ android:contentDescription="@string/go_back_img_des" />
-
-
+
+
+
+
+
+
+
+
+
+
+
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toLeftOf="@id/player_ffwd_holder"
+ app:layout_constraintTop_toTopOf="parent">
+
- android:layout_width="200dp"
- android:layout_height="40dp">
-
-
-
-
-
-
-
+ tools:ignore="ContentDescription" />
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toRightOf="@id/player_rew_holder"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
-
-
- android:layout_gravity="center"
+
+ tools:ignore="ContentDescription" />
-
+ app:layout_constraintEnd_toEndOf="parent">
+ app:tint="?attr/colorPrimaryDark"
+ android:tintMode="src_in"
+ tools:ignore="ContentDescription" />
+ app:tint="?attr/colorPrimaryDark"
+ android:tintMode="src_in"
+ tools:ignore="ContentDescription" />
+ app:tint="?attr/colorPrimaryDark"
+ android:tintMode="src_in"
+ tools:ignore="ContentDescription" />
+ app:tint="?attr/colorPrimaryDark"
+ android:tintMode="src_in"
+ tools:ignore="ContentDescription" />
+
+
+
+
+ app:layout_constraintEnd_toEndOf="parent">
+ android:orientation="horizontal">
+
+ android:textStyle="normal"
+ tools:text="15:30" />
+
+ app:scrubber_color="?attr/colorPrimary"
+ app:scrubber_dragged_size="26dp"
+ app:scrubber_enabled_size="24dp"
+ app:unplayed_color="@color/videoProgress" />
+ tools:text="23:20" />
+
+ android:layout_height="wrap_content"
+ android:layout_gravity="center">
+
+ android:paddingBottom="10dp">
+
+ app:iconSize="30dp" />
+
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
-
-
-
-
+ android:nextFocusLeft="@id/player_lock"
+
+ android:nextFocusRight="@id/playback_speed_btt"
+ android:text="@string/video_aspect_ratio_resize"
+ app:icon="@drawable/ic_baseline_aspect_ratio_24" />
+
+
+
+
+
+
+
+
-
-
+
+ app:layout_constraintTop_toTopOf="parent"
+ tools:alpha="1"
+ tools:visibility="visible">
+ tools:ignore="ContentDescription">
+ tools:progress="30" />
+
+ android:layout_gravity="center_vertical"
+ android:gravity="right"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toRightOf="@+id/centerMenuView"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:alpha="1"
+ tools:ignore="RtlHardcoded"
+ tools:visibility="visible">
+ tools:ignore="ContentDescription">
-
+ android:progressDrawable="@drawable/progress_drawable_vertical" />
diff --git a/app/src/main/res/layout/player_select_source_and_subs.xml b/app/src/main/res/layout/player_select_source_and_subs.xml
index 3de78583..c99ff6ff 100644
--- a/app/src/main/res/layout/player_select_source_and_subs.xml
+++ b/app/src/main/res/layout/player_select_source_and_subs.xml
@@ -1,150 +1,129 @@
-
-
+ android:background="@null"
+ android:orientation="vertical">
+
+
+ android:id="@+id/sort_sources_holder"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="50"
+ android:orientation="vertical">
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="match_parent"
+ android:layout_rowWeight="1"
+ android:background="?attr/primaryBlackBackground"
+ android:nextFocusLeft="@id/sort_subtitles"
+ android:nextFocusRight="@id/apply_btt"
+ tools:listitem="@layout/sort_bottom_single_choice" />
+
+
+
+
+
+
+
+
+
+
+ android:layout_height="match_parent"
+ android:layout_rowWeight="1"
+ android:background="?attr/primaryBlackBackground"
+ android:nextFocusLeft="@id/sort_providers"
+ android:nextFocusRight="@id/cancel_btt"
+ tools:listfooter="@layout/sort_bottom_footer_add_choice"
+ tools:listitem="@layout/sort_bottom_single_choice" />
+ android:id="@+id/apply_btt_holder"
+ android:orientation="horizontal"
+ android:layout_gravity="bottom"
+ android:gravity="bottom|end"
+ android:layout_marginTop="-60dp"
+ android:layout_width="match_parent"
+ android:layout_height="60dp">
-
-
-
-
-
+ android:layout_gravity="center_vertical|end"
+ android:text="@string/sort_apply"
+ android:id="@+id/apply_btt"
+ android:layout_width="wrap_content" />
-
-
+ style="@style/BlackButton"
+ android:layout_gravity="center_vertical|end"
+ android:text="@string/sort_cancel"
+ android:id="@+id/cancel_btt"
+ android:layout_width="wrap_content" />
diff --git a/app/src/main/res/layout/result_episode.xml b/app/src/main/res/layout/result_episode.xml
index 266629ba..8c0c1464 100644
--- a/app/src/main/res/layout/result_episode.xml
+++ b/app/src/main/res/layout/result_episode.xml
@@ -1,6 +1,5 @@
-
+ android:layout_marginBottom="5dp">
+
+
-
+ android:layout_height="match_parent" />
+
+ android:visibility="visible" />
+
+ android:contentDescription="@string/download" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml
index f6f39234..f1298f94 100644
--- a/app/src/main/res/layout/result_episode_large.xml
+++ b/app/src/main/res/layout/result_episode_large.xml
@@ -71,14 +71,28 @@
android:layout_width="match_parent"
android:layout_marginEnd="50dp"
android:layout_height="wrap_content">
-
-
+
+
+
+
+
+ android:contentDescription="@string/download"
+ app:tint="?attr/white" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/sort_bottom_single_choice.xml b/app/src/main/res/layout/sort_bottom_single_choice.xml
index f8a43708..3b52cd7a 100644
--- a/app/src/main/res/layout/sort_bottom_single_choice.xml
+++ b/app/src/main/res/layout/sort_bottom_single_choice.xml
@@ -15,6 +15,7 @@
+ app:drawableTint="@color/check_selection_color"
+ app:drawableStartCompat="@drawable/ic_baseline_check_24" />
diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml
index 38f196ce..22f99206 100644
--- a/app/src/main/res/navigation/mobile_navigation.xml
+++ b/app/src/main/res/navigation/mobile_navigation.xml
@@ -188,5 +188,5 @@
+ android:name="com.lagradost.cloudstream3.ui.player.GeneratorPlayer"/>
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 23f6c080..fa5803e6 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -13,7 +13,6 @@
سرعة (%.2fx)
Rated: %.1f
!تم إيجاد تحديث جديد\n%s -> %s
- (Filler) %s
CloudStream
الصفحة الرئيسية
@@ -43,7 +42,7 @@
المصادر
الترجمة
…إعادة محاولة الاتصال
- ارجع للخلف
+ ارجع للخلف
تشغيل الحلقة
diff --git a/app/src/main/res/values-de/strings-de.xml b/app/src/main/res/values-de/strings-de.xml
index 9f70ec20..3f4fec9b 100644
--- a/app/src/main/res/values-de/strings-de.xml
+++ b/app/src/main/res/values-de/strings-de.xml
@@ -11,7 +11,6 @@
Geschwindigkeit (%.2fx)
Bewertung: %.1f
Neues Update gefunden!\n%s -> %s
- (Filler) %s
CloudStream
Startseite
Suchen
@@ -37,7 +36,7 @@
Quellen
Untertitel
Verbindung wiederholen…
- Zurück
+ Zurück
Episode abspielen
Herunterladen
Heruntergeladen
diff --git a/app/src/main/res/values-es/strings-es.xml b/app/src/main/res/values-es/strings-es.xml
index 048eec5a..51646395 100644
--- a/app/src/main/res/values-es/strings-es.xml
+++ b/app/src/main/res/values-es/strings-es.xml
@@ -12,7 +12,7 @@
Velocidad (%.2fx)
Puntuado: %.1f
¡Nueva actualización encontrada!\n%s -> %s
- (Relleno) %s
+ Relleno
CloudStream
Inicio
Buscar
@@ -38,7 +38,7 @@
Fuentes
Subtítulos
Reintentar conexión…
- Ir atrás
+ Ir atrás
Reproducir capítulo
Descarga
Descargada
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 2b53af01..378239c7 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -28,7 +28,7 @@
Sources
Sous-titres
Réessayer la connection…
- Retour
+ Retour
Miniature de l\'Episode
Lire l\'Episode
Permet de télécharger les épisodes
@@ -178,7 +178,7 @@
Utile pour contourner les bloquages des FAI
Nouvelle mise à jour trouvée !
\n%s -> %s
- (Épisode spécial) %s
+ Épisode spécial
Qualité de visionnage préférée
Étendre
Non-responsabilité
diff --git a/app/src/main/res/values-gr/strings.xml b/app/src/main/res/values-gr/strings.xml
index e6f4c390..4810e837 100644
--- a/app/src/main/res/values-gr/strings.xml
+++ b/app/src/main/res/values-gr/strings.xml
@@ -31,7 +31,7 @@
Πηγές
Υπότιτλοι
Ξανά φόρτωσε…
- Πίσω
+ Πίσω
Πόστερ
Αναπαραγωγή Επισοδείου
Δώσε άδεια για την λήψη επισοδείου
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 06e7cd1c..a37b9f4d 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -26,7 +26,7 @@
टोरेंट चलाये
सूत्र
फिरसे प्रयास करे…
- वापिस जाए
+ वापिस जाए
एपिसोड चलाये
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index e36d9137..71490255 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -4,7 +4,7 @@
Брзина (%.2fx)
Оценето: %.1f
Пронајдена нова верзија на апликацијата!\n%s -> %s
- (Филтер) %s
+ Филтер
CloudStream
Дома
@@ -35,7 +35,7 @@
Извори
Преводи
Повтори конекција…
- Оди назад
+ Оди назад
Пушти епизода
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index de0cb7fa..20626222 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -29,7 +29,7 @@
സ്രോതസുകൾ
സബ്ടൈറ്റിലുകൾ
വീണ്ടും കണക്ട് ചെയ്യുക…
- പിന്നോട്ട് പോകുക
+ പിന്നോട്ട് പോകുക
എപ്പിസോഡ് പ്ലേയ് ചെയ്യുക
ഡൌൺലോഡ്
diff --git a/app/src/main/res/values-mo/string.xml b/app/src/main/res/values-mo/string.xml
index 2a0400f2..01df7eaa 100644
--- a/app/src/main/res/values-mo/string.xml
+++ b/app/src/main/res/values-mo/string.xml
@@ -29,7 +29,7 @@
oha aauuh
ahooo ooo-ahah
aaaghhahaaaaaaaaaaooh
- aauugghhaaaghh
+ aauugghhaaaghh
oouuhahoooooo-ahahoooohh
aaaaa oh ohaouuhhh
ahouuhhhaaaaa ahhaaaghh ahhh
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 22a456e0..c6539ce6 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -31,7 +31,7 @@
Bronnen
Ondertitels
Probeer verbinding opnieuw…
- Ga terug
+ Ga terug
Afleveringsposter
Aflevering afspelen
Toestaan om afleveringen te downloaden
diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml
index 33f41058..dc624d9f 100644
--- a/app/src/main/res/values-no/strings.xml
+++ b/app/src/main/res/values-no/strings.xml
@@ -13,7 +13,6 @@
Avspillingshastighet (%.2fx)
Vurdert: %.1f
Ny oppdatering funnet!\n%s -> %s
- (Filler) %s
CloudStream
Hjem
@@ -43,7 +42,7 @@
Kilder
Undertekster
Prøv tilkoblingen på nytt…
- Gå tilbake
+ Gå tilbake
Spille Episode
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 390ec3e9..1ef6d89d 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -32,7 +32,7 @@
Źródła
Napisy
Połącz ponownie…
- Wstecz
+ Wstecz
Odtwórz odcinek
@@ -223,7 +223,7 @@
Główny plakat
Następny losowy
Wstecz
- (WYpełniacz) %s
+ WYpełniacz
Zmień dostawcę
Pogląd tła
diff --git a/app/src/main/res/values-pt/strings-pt.xml b/app/src/main/res/values-pt/strings-pt.xml
index f7b8ffcf..8f73e5a5 100644
--- a/app/src/main/res/values-pt/strings-pt.xml
+++ b/app/src/main/res/values-pt/strings-pt.xml
@@ -15,7 +15,7 @@
Velocidade (%.2fx)
Classificado: %.1f
Nova atualização encontrada!\n%s -> %s
- (Cheio) %s
+ Cheio
CloudStream
Início
@@ -46,7 +46,7 @@
Origems
Subtítulos
Reintentar conexão…
- Voltar Atrás
+ Voltar Atrás
Assistir Episódio
Descàrregar
diff --git a/app/src/main/res/values-ro/strings-ro.xml b/app/src/main/res/values-ro/strings-ro.xml
index a8376b23..075c714b 100644
--- a/app/src/main/res/values-ro/strings-ro.xml
+++ b/app/src/main/res/values-ro/strings-ro.xml
@@ -15,7 +15,7 @@
Viteză (%.2fx)
Evaluat: %.1f
Noua actualizare găsită!\n%s -> %s
- (Umplut) %s
+ Umplut
CloudStream
Principal
@@ -46,7 +46,7 @@
Surse
Subtitrare
Reîncercarea conexiunii…
- Întoarce-te
+ Întoarce-te
Redă episodul
Descărcare
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 5eaf206d..b4dc6071 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -34,7 +34,7 @@
Källor
Undertexter
Försök ansluta igen…
- Gå tillbaka
+ Gå tillbaka
@string/result_poster_img_des
Spela Avsnitt
Ladda ner
diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml
index f5d7f56b..f9df0e32 100644
--- a/app/src/main/res/values-tl/strings.xml
+++ b/app/src/main/res/values-tl/strings.xml
@@ -20,7 +20,6 @@
Bilis (%.2fx)
Rated: %.1f
Bagong update!\n%s -> %s
- (Filler) %s
CloudStream
Home
@@ -50,7 +49,7 @@
Sources
Subtitles
Retry connection…
- Go back
+ Go back
I-play ang episode
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 7eeba6a1..37e93f4a 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -31,18 +31,6 @@
prefer_media_type_key
app_theme_key
-
-
- %d %s | %sMB
- %s • %sGB
- %sMB / %sMB
- %s %s
- +%d
- -%d
- %d
- %d
- %s Ep %d
-
Afiş
@string/result_poster_img_des
@@ -59,7 +47,7 @@
Hız (%.2fx)
Puan: %.1f
Yeni güncelleme bulundu!\n%s -> %s
- (Doldurucu) %s
+ Doldurucu
CloudStream
Ana Sayfa
@@ -89,7 +77,7 @@
Kaynaklar
Altyazılar
Yeniden bağlanmayı dene…
- Geri Git
+ Geri Git
Bölümü Oynat
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 3be81832..beeb8e7a 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -27,7 +27,7 @@
Nguồn Phim
Phụ Đề
Thử kết nối lại…
- Quay lại
+ Quay lại
Xem Tập Phim
diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml
index 990d3b98..41944416 100644
--- a/app/src/main/res/values/array.xml
+++ b/app/src/main/res/values/array.xml
@@ -101,6 +101,7 @@
- Apple
- Banana
- Party
+ - Pink Pain
- Normal
@@ -111,6 +112,7 @@
- GreenApple
- Banana
- Party
+ - Pink
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 4146520a..649395a7 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -62,4 +62,5 @@
#48E484
#E4D448
#ea596e
+ #ff1493
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 55454f60..9900f871 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -19,6 +19,7 @@
player_resize_enabled_key
pip_enabled_key
double_tap_enabled_key
+ double_tap_pause_enabled_key
swipe_vertical_enabled_key
display_sub_key
show_fillers_key
@@ -61,7 +62,7 @@
Speed (%.2fx)
Rated: %.1f
New update found!\n%s -> %s
- (Filler) %s
+ Filler
%d min
CloudStream
@@ -94,7 +95,7 @@
Sources
Subtitles
Retry connection…
- Go Back
+ Go Back
Play Episode
@@ -178,8 +179,10 @@
Swipe to change settings
Swipe on the left or right side to change brightness or volume
Double tap to seek
+ Double tap to pause
Tap twice on the right or left side to seek forwards or backwards
+ Tap in the middle to pause
Use system brightness
Use system brightness in the app player instead of a dark
overlay
@@ -226,6 +229,8 @@
@string/sort_cancel
Pause
Resume
+ -30
+ +30
This will permanently delete %s\nAre you sure?
Ongoing
@@ -368,6 +373,7 @@
The quick brown fox jumps over the lazy dog
Recommended
- Loaded %s
+ Loaded %s
Load from file
+ Downloaded file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 1435f531..c2b7d318 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -165,6 +165,15 @@
- @color/colorPrimaryParty
+
+
+
+