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