diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 0ce93821..9a0debcf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -99,6 +99,10 @@ abstract class AbstractPlayerFragment( throw NotImplementedError() } + open fun onTracksInfoChanged() { + throw NotImplementedError() + } + open fun exitedPipMode() { throw NotImplementedError() } @@ -369,6 +373,7 @@ abstract class AbstractPlayerFragment( ), subtitlesUpdates = ::subtitlesChanged, embeddedSubtitlesFetched = ::embeddedSubtitlesFetched, + onTracksInfoChanged = ::onTracksInfoChanged ) if (player is CS3IPlayer) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 5f6ef921..677042d0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -8,6 +8,8 @@ import android.util.Log import android.widget.FrameLayout import androidx.preference.PreferenceManager import com.google.android.exoplayer2.* +import com.google.android.exoplayer2.C.TRACK_TYPE_AUDIO +import com.google.android.exoplayer2.C.TRACK_TYPE_VIDEO import com.google.android.exoplayer2.database.StandaloneDatabaseProvider import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource import com.google.android.exoplayer2.source.* @@ -24,6 +26,8 @@ import com.google.android.exoplayer2.upstream.cache.SimpleCache import com.google.android.exoplayer2.util.MimeTypes import com.google.android.exoplayer2.video.VideoSize import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError @@ -39,6 +43,7 @@ import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession const val TAG = "CS3ExoPlayer" +const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language" /** Cache */ @@ -108,6 +113,7 @@ class CS3IPlayer : IPlayer { private var playerUpdated: ((Any?) -> Unit)? = null private var embeddedSubtitlesFetched: ((List) -> Unit)? = null + private var onTracksInfoChanged: (() -> Unit)? = null override fun releaseCallbacks() { playerUpdated = null @@ -120,7 +126,7 @@ class CS3IPlayer : IPlayer { nextEpisode = null prevEpisode = null subtitlesUpdates = null - embeddedSubtitlesFetched = null + onTracksInfoChanged = null requestSubtitleUpdate = null } @@ -136,6 +142,7 @@ class CS3IPlayer : IPlayer { prevEpisode: (() -> Unit)?, subtitlesUpdates: (() -> Unit)?, embeddedSubtitlesFetched: ((List) -> Unit)?, + onTracksInfoChanged: (() -> Unit)?, ) { this.playerUpdated = playerUpdated this.updateIsPlaying = updateIsPlaying @@ -148,6 +155,7 @@ class CS3IPlayer : IPlayer { this.prevEpisode = prevEpisode this.subtitlesUpdates = subtitlesUpdates this.embeddedSubtitlesFetched = embeddedSubtitlesFetched + this.onTracksInfoChanged = onTracksInfoChanged } // I know, this is not a perfect solution, however it works for fixing subs @@ -212,6 +220,72 @@ class CS3IPlayer : IPlayer { var currentSubtitles: SubtitleData? = null + override fun setMaxVideoSize(width: Int, height: Int) { + exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters + ?.buildUpon() + ?.setMaxVideoSize(width, height) + ?.build() + ?: return + } + + override fun setPreferredAudioTrack(trackLanguage: String?) { + preferredAudioTrackLanguage = trackLanguage + exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters + ?.buildUpon() + ?.setPreferredAudioLanguage(trackLanguage) + ?.build() + ?: return + } + + + /** + * Gets all supported formats in a list + * */ + private fun List.getFormats(): List { + return this.map { + (0 until it.trackGroup.length).mapNotNull { i -> + if (it.isSupported) + it.trackGroup.getFormat(i) // to it.isSelected + else null + } + }.flatten() + } + + private fun Format.toAudioTrack(): AudioTrack { + return AudioTrack( + this.id, + this.label, +// isPlaying, + this.language + ) + } + + private fun Format.toVideoTrack(): VideoTrack { + return VideoTrack( + this.id, + this.label, +// isPlaying, + this.language, + this.width, + this.height + ) + } + + override fun getVideoTracks(): CurrentTracks { + val allTracks = exoPlayer?.currentTracksInfo?.trackGroupInfos ?: emptyList() + val videoTracks = allTracks.filter { it.trackType == TRACK_TYPE_VIDEO }.getFormats() + .map { it.toVideoTrack() } + val audioTracks = allTracks.filter { it.trackType == TRACK_TYPE_AUDIO }.getFormats() + .map { it.toAudioTrack() } + + return CurrentTracks( + exoPlayer?.videoFormat?.toVideoTrack(), + exoPlayer?.audioFormat?.toAudioTrack(), + videoTracks, + audioTracks + ) + } + /** * @return True if the player should be reloaded * */ @@ -350,6 +424,20 @@ class CS3IPlayer : IPlayer { } companion object { + /** + * Setting this variable is permanent across app sessions. + **/ + private var preferredAudioTrackLanguage: String? = null + get() { + return field ?: getKey(PREFERRED_AUDIO_LANGUAGE_KEY, field)?.also { + field = it + } + } + set(value) { + setKey(PREFERRED_AUDIO_LANGUAGE_KEY, value) + field = value + } + private var simpleCache: SimpleCache? = null var requestSubtitleUpdate: (() -> Unit)? = null @@ -460,13 +548,21 @@ class CS3IPlayer : IPlayer { return getMediaItemBuilder(mimeType).setUri(url).build() } - private fun getTrackSelector(context: Context): TrackSelector { + private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector { val trackSelector = DefaultTrackSelector(context) trackSelector.parameters = DefaultTrackSelector.ParametersBuilder(context) // .setRendererDisabled(C.TRACK_TYPE_VIDEO, true) .setRendererDisabled(C.TRACK_TYPE_TEXT, true) + // Experimental + .setTunnelingEnabled(true) .setDisabledTextTrackSelectionFlags(C.TRACK_TYPE_TEXT) - .clearSelectionOverrides() + // This will not force higher quality videos to fail + // but will make the m3u8 pick the correct preferred + .setMaxVideoSize(Int.MAX_VALUE, maxVideoHeight ?: Int.MAX_VALUE) + .setPreferredAudioLanguage(preferredAudioTrackLanguage) + + // This would also clear preferred audio +// .clearSelectionOverrides() .build() return trackSelector } @@ -486,6 +582,11 @@ class CS3IPlayer : IPlayer { playWhenReady: Boolean = true, cacheFactory: CacheDataSource.Factory? = null, trackSelector: TrackSelector? = null, + /** + * Sets the m3u8 preferred video quality, will not force stop anything with higher quality. + * Does not work if trackSelector is defined. + **/ + maxVideoHeight: Int? = null ): ExoPlayer { val exoPlayerBuilder = ExoPlayer.Builder(context) @@ -508,7 +609,7 @@ class CS3IPlayer : IPlayer { } else it }.toTypedArray() } - .setTrackSelector(trackSelector ?: getTrackSelector(context)) + .setTrackSelector(trackSelector ?: getTrackSelector(context, maxVideoHeight)) .setLoadControl( DefaultLoadControl.Builder() .setTargetBufferBytes( @@ -637,6 +738,12 @@ class CS3IPlayer : IPlayer { cacheFactory: CacheDataSource.Factory? = null ) { Log.i(TAG, "loadExo") + val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) + val maxVideoHeight = settingsManager.getInt( + context.getString(com.lagradost.cloudstream3.R.string.quality_pref_key), + Int.MAX_VALUE + ) + try { hasUsedFirstRender = false @@ -653,7 +760,8 @@ class CS3IPlayer : IPlayer { videoBufferMs = videoBufferMs, playWhenReady = isPlaying, // this keep the current state of the player cacheFactory = cacheFactory, - subtitleOffset = currentSubtitleOffset + subtitleOffset = currentSubtitleOffset, + maxVideoHeight = maxVideoHeight ) requestSubtitleUpdate = ::reloadSubs @@ -713,6 +821,7 @@ class CS3IPlayer : IPlayer { } embeddedSubtitlesFetched?.invoke(exoPlayerReportedTracks) + onTracksInfoChanged?.invoke() subtitlesUpdates?.invoke() } super.onTracksInfoChanged(tracksInfo) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 2ddef9a4..19f2b25b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -176,6 +176,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { throw NotImplementedError() } + open fun showTracksDialogue() { + throw NotImplementedError() + } + open fun openOnlineSubPicker( context: Context, imdbId: Long?, @@ -1101,6 +1105,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { // if nothing has loaded these buttons should not be visible player_skip_episode?.isVisible = false + player_tracks_btt?.isVisible = false player_skip_op?.isVisible = false shadow_overlay?.isVisible = false @@ -1296,6 +1301,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { showMirrorsDialogue() } + player_tracks_btt?.setOnClickListener { + showTracksDialogue() + } + player_intro_play?.setOnClickListener { player_intro_play?.isGone = true player.handleEvent(CSPlayerEvent.Play) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 04d630aa..05b599de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -18,6 +18,11 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager +import com.google.android.exoplayer2.C.TRACK_TYPE_AUDIO +import com.google.android.exoplayer2.C.TRACK_TYPE_VIDEO +import com.google.android.exoplayer2.Format +import com.google.android.exoplayer2.Format.NO_VALUE +import com.google.android.exoplayer2.TracksInfo import com.google.android.exoplayer2.util.MimeTypes import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.* @@ -53,6 +58,8 @@ import kotlinx.android.synthetic.main.dialog_online_subtitles.cancel_btt import kotlinx.android.synthetic.main.fragment_player.* import kotlinx.android.synthetic.main.player_custom_layout.* import kotlinx.android.synthetic.main.player_select_source_and_subs.* +import kotlinx.android.synthetic.main.player_select_source_and_subs.subtitles_click_settings +import kotlinx.android.synthetic.main.player_select_tracks.* import kotlinx.coroutines.Job class GeneratorPlayer : FullScreenPlayer() { @@ -109,6 +116,11 @@ class GeneratorPlayer : FullScreenPlayer() { viewModel.addSubtitles(subtitles.toSet()) } + override fun onTracksInfoChanged() { + val tracks = player.getVideoTracks() + player_tracks_btt?.isVisible = tracks.allVideoTracks.size > 1 || tracks.allAudioTracks.size > 1 + } + private fun noSubtitles(): Boolean { return setSubtitles(null) } @@ -477,6 +489,8 @@ class GeneratorPlayer : FullScreenPlayer() { } var selectSourceDialog: AlertDialog? = null +// var selectTracksDialog: AlertDialog? = null + override fun showMirrorsDialogue() { try { currentSelectedSubtitles = player.getCurrentPreferredSubtitle() @@ -668,6 +682,121 @@ class GeneratorPlayer : FullScreenPlayer() { } } + override fun showTracksDialogue() { + try { + //println("CURRENT SELECTED :$currentSelectedSubtitles of $currentSubs") + context?.let { ctx -> + val tracks = player.getVideoTracks() + + val isPlaying = player.getIsPlaying() + player.handleEvent(CSPlayerEvent.Pause) + + val currentVideoTracks = tracks.allVideoTracks.sortedBy { + it.height?.times(-1) + } + val currentAudioTracks = tracks.allAudioTracks + + val trackBuilder = AlertDialog.Builder(ctx, R.style.AlertDialogCustomBlack) + .setView(R.layout.player_select_tracks) + + val tracksDialog = trackBuilder.create() + +// selectTracksDialog = tracksDialog + + tracksDialog.show() + val videosList = tracksDialog.video_tracks_list + val audioList = tracksDialog.auto_tracks_list + + tracksDialog.video_tracks_holder.isVisible = currentVideoTracks.size > 1 + tracksDialog.audio_tracks_holder.isVisible = currentAudioTracks.size > 1 + + fun dismiss() { + if (isPlaying) { + player.handleEvent(CSPlayerEvent.Play) + } + activity?.hideSystemUI() + } + + val videosArrayAdapter = + ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) + + videosArrayAdapter.addAll(currentVideoTracks.mapIndexed { index, format -> + format.label + ?: (if (format.height == NO_VALUE || format.width == NO_VALUE) index else "${format.width}x${format.height}").toString() + }) + + videosList.choiceMode = AbsListView.CHOICE_MODE_SINGLE + videosList.adapter = videosArrayAdapter + + // Sometimes the data is not the same because some data gets resolved at different stages i think + var videoIndex = currentVideoTracks.indexOf(tracks.currentVideoTrack).takeIf { + it != -1 + } ?: currentVideoTracks.indexOfFirst { + tracks.currentVideoTrack?.id == it.id + } + + videosList.setSelection(videoIndex) + videosList.setItemChecked(videoIndex, true) + + videosList.setOnItemClickListener { _, _, which, _ -> + videoIndex = which + videosList.setItemChecked(which, true) + } + + tracksDialog.setOnDismissListener { + dismiss() +// selectTracksDialog = null + } + + var audioIndexStart = currentAudioTracks.indexOf(tracks.currentAudioTrack).takeIf { + it != -1 + } ?: currentVideoTracks.indexOfFirst { + tracks.currentAudioTrack?.id == it.id + } + + val audioArrayAdapter = + ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) +// audioArrayAdapter.add(ctx.getString(R.string.no_subtitles)) + audioArrayAdapter.addAll(currentAudioTracks.mapIndexed { index, format -> + format.label ?: format.language?.let { fromTwoLettersToLanguage(it) } ?: index.toString() + }) + + audioList.adapter = audioArrayAdapter + audioList.choiceMode = AbsListView.CHOICE_MODE_SINGLE + + audioList.setSelection(audioIndexStart) + audioList.setItemChecked(audioIndexStart, true) + + audioList.setOnItemClickListener { _, _, which, _ -> + audioIndexStart = which + audioList.setItemChecked(which, true) + } + + tracksDialog.cancel_btt?.setOnClickListener { + tracksDialog.dismissSafe(activity) + } + + tracksDialog.apply_btt?.setOnClickListener { + player.setPreferredAudioTrack( + currentAudioTracks.getOrNull(audioIndexStart)?.language + ) + + val currentVideo = currentVideoTracks.getOrNull(videoIndex) + val width = currentVideo?.width ?: NO_VALUE + val height = currentVideo?.height ?: NO_VALUE + if (width != NO_VALUE && height != NO_VALUE) { + player.setMaxVideoSize(width, height) + } + + tracksDialog.dismissSafe(activity) + } + } + } catch (e: Exception) { + logError(e) + } + } + + override fun playerError(exception: Exception) { Log.i(TAG, "playerError = $currentSelectedLink") super.playerError(exception) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 8561a0f4..e8934250 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -46,7 +46,42 @@ enum class CSPlayerLoading { //IsDone, } -class InvalidFileException(msg : String) : Exception(msg) + +interface Track { + /** + * Unique among the class, used to check which track is used. + * VideoTrack and AudioTrack can have the same id + **/ + val id: String? + val label: String? +// val isCurrentlyPlaying: Boolean + val language: String? +} + +data class VideoTrack( + override val id: String?, + override val label: String?, +// override val isCurrentlyPlaying: Boolean, + override val language: String?, + val width: Int?, + val height: Int?, +) : Track + +data class AudioTrack( + override val id: String?, + override val label: String?, +// override val isCurrentlyPlaying: Boolean, + override val language: String?, +) : Track + +data class CurrentTracks( + val currentVideoTrack: VideoTrack?, + val currentAudioTrack: AudioTrack?, + val allVideoTracks: List, + val allAudioTracks: List, +) + +class InvalidFileException(msg: String) : Exception(msg) //http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 const val STATE_RESUME_WINDOW = "resumeWindow" @@ -73,8 +108,8 @@ interface IPlayer { fun seekTime(time: Long) fun seekTo(time: Long) - fun getSubtitleOffset() : Long // in ms - fun setSubtitleOffset(offset : Long) // in ms + fun getSubtitleOffset(): Long // in ms + fun setSubtitleOffset(offset: Long) // in ms fun initCallbacks( playerUpdated: (Any?) -> Unit, // attach player to view @@ -88,7 +123,9 @@ interface IPlayer { prevEpisode: (() -> Unit)? = null, // this is used by the player to load the previous episode subtitlesUpdates: (() -> Unit)? = null, // callback from player to inform that subtitles have updated in some way embeddedSubtitlesFetched: ((List) -> Unit)? = null, // callback from player to give all embedded subtitles + onTracksInfoChanged: (() -> Unit)? = null, // Callback when tracks are changed, used for UI changes ) + fun releaseCallbacks() fun updateSubtitleStyle(style: SaveCaptionStyle) @@ -100,16 +137,16 @@ interface IPlayer { link: ExtractorLink? = null, data: ExtractorUri? = null, startPosition: Long? = null, - subtitles : Set, - subtitle : SubtitleData?, - autoPlay : Boolean? = true + subtitles: Set, + subtitle: SubtitleData?, + autoPlay: Boolean? = true ) 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 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) @@ -120,5 +157,13 @@ interface IPlayer { fun release() /** Get if player is actually used */ - fun isActive() : Boolean + fun isActive(): Boolean + + fun getVideoTracks(): CurrentTracks + + /** If no parameters are set it'll default to no set size */ + fun setMaxVideoSize(width: Int = Int.MAX_VALUE, height: Int = Int.MAX_VALUE) + + /** If no trackLanguage is set it'll default to first track */ + fun setPreferredAudioTrack(trackLanguage: String?) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index db0a7e16..26c9249d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -75,11 +75,14 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen } override fun showMirrorsDialogue() {} + override fun showTracksDialogue() {} + override fun openOnlineSubPicker(context: Context, imdbId: Long?, dismissCallback: () -> Unit) {} override fun subtitlesChanged() {} override fun embeddedSubtitlesFetched(subtitles: List) {} + override fun onTracksInfoChanged() {} override fun exitedPipMode() {} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index c309d36e..6c5117b4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -116,15 +116,16 @@ class M3u8Helper { return !url.contains("https://") && !url.contains("http://") } - suspend fun m3u8Generation(m3u8: M3u8Stream, returnThis: Boolean?): List { + suspend fun m3u8Generation(m3u8: M3u8Stream, returnThis: Boolean? = true): List { +// return listOf(m3u8) val list = mutableListOf() val m3u8Parent = getParentLink(m3u8.streamUrl) val response = app.get(m3u8.streamUrl, headers = m3u8.headers, verify = false).text - var hasAnyContent = false +// var hasAnyContent = false for (match in QUALITY_REGEX.findAll(response)) { - hasAnyContent = true +// hasAnyContent = true var (quality, m3u8Link, m3u8Link2) = match.destructured if (m3u8Link.isEmpty()) m3u8Link = m3u8Link2 if (absoluteExtensionDetermination(m3u8Link) == "m3u8") { @@ -141,16 +142,14 @@ class M3u8Helper { m3u8.headers ), false ) - } list += M3u8Stream( m3u8Link, quality.toIntOrNull(), m3u8.headers ) - } - if (returnThis ?: !hasAnyContent) { + if (returnThis != false) { list += M3u8Stream( m3u8.streamUrl, Qualities.Unknown.value, @@ -169,7 +168,10 @@ class M3u8Helper { val errored: Boolean = false ) - suspend fun hlsYield(qualities: List, startIndex: Int = 0): Iterator { + suspend fun hlsYield( + qualities: List, + startIndex: Int = 0 + ): Iterator { if (qualities.isEmpty()) return listOf( HlsDownloadData( byteArrayOf(), diff --git a/app/src/main/res/layout/fragment_player_tv.xml b/app/src/main/res/layout/fragment_player_tv.xml index f4cec44c..b84aaf7d 100644 --- a/app/src/main/res/layout/fragment_player_tv.xml +++ b/app/src/main/res/layout/fragment_player_tv.xml @@ -1,131 +1,131 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/player_background" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/black" + android:keepScreenOn="true" + android:orientation="horizontal" + android:screenOrientation="sensorLandscape" + app:backgroundTint="@android:color/black" + app:surface_type="texture_view"> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:show_timeout="0" /> + android:id="@+id/player_loading_overlay" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/black" + android:backgroundTint="@android:color/black" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:visibility="gone" + app:icon="@drawable/ic_baseline_skip_next_24" + tools:visibility="visible" /> + android:id="@+id/main_load" + android:layout_width="50dp" + android:layout_height="50dp" + android:layout_gravity="center" /> + android:id="@+id/video_go_back_holder_holder" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="5dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:layout_width="30dp" + android:layout_height="30dp" + android:layout_gravity="center" + android:src="@drawable/ic_baseline_arrow_back_24" + app:tint="@android:color/white" /> + android:layout_gravity="center" + android:background="@drawable/video_tap_button_always_white" + android:clickable="true" + android:focusable="true" + android:focusableInTouchMode="true" + android:nextFocusRight="@id/overlay_loading_skip_button" + android:nextFocusDown="@id/overlay_loading_skip_button" /> + + + android:layout_height="wrap_content" + android:layout_marginTop="15dp" + android:gravity="start" + android:textColor="@color/white" + android:textStyle="bold" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="78% at 18kb/s" /> - - + android:id="@+id/video_torrent_seeders" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="0dp" + android:gravity="start" + android:textColor="@color/white" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toBottomOf="@+id/player_video_title" + tools:text="17 seeders" /> \ No newline at end of file diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 5e10e471..9bbded4e 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -1,23 +1,23 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/player_holder" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:screenOrientation="landscape" + tools:orientation="vertical"> + android:id="@+id/subtitle_holder" + android:layout_width="match_parent" + android:layout_height="match_parent"> + android:id="@+id/shadow_overlay" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/black_overlay" /> + android:id="@+id/player_time_menu" + android:layout_width="match_parent" + android:layout_height="200dp" + android:orientation="horizontal" + app:layout_constraintBottom_toTopOf="@+id/player_center_menu" + app:layout_constraintTop_toBottomOf="@+id/topMenuRight"> + android:id="@+id/player_time_text" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" + android:shadowColor="@android:color/black" + android:shadowRadius="10.0" + android:textColor="@android:color/white" + android:textSize="30sp" + tools:text="+100" /> + android:id="@+id/player_episode_filler_holder" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:layout_margin="20dp"> + android:id="@+id/player_episode_filler" + style="@style/SmallBlackButton" + android:text="@string/filler" /> + + - + android:layout_height="wrap_content" + android:layout_marginStart="80dp" + android:layout_marginTop="35dp" + android:layout_marginEnd="80dp" + android:gravity="center" + android:textColor="@color/white" + android:textStyle="bold" + android:visibility="visible" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Hello world" /> - - + android:id="@+id/player_video_title_rez" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="80dp" + android:layout_marginTop="20dp" + android:layout_marginEnd="80dp" + android:gravity="center" + android:textColor="@color/white" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toBottomOf="@+id/player_video_title" + tools:text="1920x1080" /> @@ -138,30 +138,30 @@ app:layout_constraintTop_toTopOf="parent" />--> + android:id="@+id/player_go_back_holder" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="5dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:layout_width="30dp" + android:layout_height="30dp" + android:layout_gravity="center" + android:contentDescription="@string/go_back_img_des" + android:src="@drawable/ic_baseline_arrow_back_24" + app:tint="@android:color/white" /> + android:id="@+id/player_go_back" + android:layout_width="70dp" + android:layout_height="70dp" + android:layout_gravity="center" + android:background="@drawable/video_tap_button_always_white" + android:clickable="true" + android:contentDescription="@string/go_back_img_des" + android:focusable="true" /> @@ -169,362 +169,374 @@ + android:indeterminate="true" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> + android:id="@+id/player_pause_play_holder_holder" + android:layout_width="100dp" + android:layout_height="100dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:id="@+id/player_pause_play_holder" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="uselessParent"> + android:src="@drawable/netflix_pause" + app:tint="@color/white" + tools:ignore="ContentDescription" /> + android:id="@+id/player_center_menu" + android:layout_width="match_parent" + android:layout_height="100dp" + android:layout_gravity="center" + android:gravity="center" + android:layoutDirection="ltr" + android:orientation="horizontal" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:id="@+id/player_rew_holder" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical|start" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toLeftOf="@id/player_ffwd_holder" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_percent="0.5"> + android:textStyle="bold" + tools:text="10" /> + android:background="@drawable/video_tap_button_skip" + android:nextFocusLeft="@id/exo_rew" + android:nextFocusUp="@id/player_go_back" + android:nextFocusDown="@id/player_lock" + android:padding="10dp" + android:scaleType="fitCenter" + android:scaleX="-1" + android:src="@drawable/netflix_skip_forward" + android:tintMode="src_in" + app:tint="@color/white" + tools:ignore="ContentDescription" /> + android:id="@+id/player_ffwd_holder" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical|end" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toRightOf="@id/player_rew_holder" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_percent="0.5"> + android:id="@+id/exo_ffwd_text" + android:layout_width="200dp" + android:layout_height="40dp" + android:layout_gravity="center" + android:gravity="center" + android:textColor="@color/white" + android:textSize="19sp" + android:textStyle="bold" + tools:text="10" /> + android:background="@drawable/video_tap_button_skip" + android:nextFocusRight="@id/exo_rew" + android:nextFocusUp="@id/player_go_back" + android:nextFocusDown="@id/player_lock" + android:padding="10dp" + android:scaleType="fitCenter" + android:src="@drawable/netflix_skip_forward" + android:tintMode="src_in" + app:tint="@color/white" + tools:ignore="ContentDescription" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:layout_marginBottom="20dp" + android:gravity="center" + android:orientation="horizontal" + android:paddingTop="4dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent"> + android:id="@id/exo_prev" + style="@style/ExoMediaButton.Previous" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_repeat_toggle" + style="@style/ExoMediaButton" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_next" + style="@style/ExoMediaButton.Next" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_vr" + style="@style/ExoMediaButton.VR" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_play" + android:layout_width="0dp" + android:layout_height="0dp" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_pause" + android:layout_width="0dp" + android:layout_height="0dp" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@+id/bottom_player_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:gravity="center_vertical" + android:orientation="vertical" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent"> + android:id="@+id/player_video_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layoutDirection="ltr" + android:orientation="horizontal"> + android:id="@id/exo_position" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="20dp" + android:gravity="end" + android:includeFontPadding="false" + android:minWidth="50dp" + android:paddingLeft="4dp" + android:paddingRight="4dp" + android:textColor="@android:color/white" + android:textSize="14sp" + 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" /> + android:id="@id/exo_duration" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginEnd="20dp" + android:includeFontPadding="false" + android:minWidth="50dp" + android:paddingLeft="4dp" + android:paddingRight="4dp" + android:textColor="@android:color/white" + android:textSize="14sp" + android:textStyle="normal" + tools:text="23:20" /> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center"> + android:layout_width="wrap_content" + android:layout_height="60dp" + android:gravity="center" + android:orientation="horizontal" + android:paddingTop="10dp" + android:paddingBottom="10dp"> + app:iconSize="30dp" /> + android:id="@+id/player_lock_holder" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:orientation="horizontal"> + android:nextFocusRight="@id/player_speed_btt" + android:text="@string/video_aspect_ratio_resize" + app:icon="@drawable/ic_baseline_aspect_ratio_24" /> + android:nextFocusRight="@id/player_subtitle_offset_btt" + app:icon="@drawable/ic_baseline_speed_24" + tools:text="Speed" /> + android:visibility="gone" + app:icon="@drawable/ic_outline_subtitles_24" + tools:visibility="visible" /> - android:nextFocusRight="@id/player_skip_op" - android:text="@string/video_source" - app:icon="@drawable/ic_baseline_playlist_play_24" /> + android:nextFocusLeft="@id/player_sources_btt" + android:nextFocusRight="@id/player_skip_op" + android:text="@string/tracks" + app:icon="@drawable/ic_baseline_playlist_play_24" /> + android:nextFocusRight="@id/player_skip_episode" + android:text="@string/video_skip_op" + app:icon="@drawable/ic_baseline_fast_forward_24" /> + + @@ -532,66 +544,66 @@ + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layoutDirection="ltr" + android:orientation="horizontal"> + android:id="@+id/player_progressbar_left_holder" + android:layout_width="100dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:gravity="start" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toLeftOf="@+id/centerMenuView" + app:layout_constraintTop_toTopOf="parent" + tools:alpha="1" + tools:visibility="visible"> + android:id="@+id/player_progressbar_left_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_marginBottom="220dp" + android:src="@drawable/ic_baseline_volume_up_24" + app:tint="@android:color/white" + tools:ignore="ContentDescription"> + android:id="@+id/player_progressbar_left" + style="@android:style/Widget.Material.ProgressBar.Horizontal" + android:layout_width="4dp" + android:layout_height="150dp" + android:layout_centerInParent="true" + android:layout_gravity="end|center_vertical" + android:layout_marginStart="40dp" + android:indeterminate="false" + android:max="100" + android:progress="100" + android:progressDrawable="@drawable/progress_drawable_vertical" + tools:progress="30" /> + android:id="@+id/player_progressbar_right_holder" + android:layout_width="match_parent" + android:layout_height="wrap_content" + 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"> + android:id="@+id/player_progressbar_right" + style="@android:style/Widget.Material.ProgressBar.Horizontal" + android:layout_width="4dp" + android:layout_height="150dp" + android:layout_centerInParent="true" + android:layout_gravity="end|center_vertical" + android:layout_marginEnd="40dp" + android:indeterminate="false" + android:max="100" + android:progress="100" + android:progressDrawable="@drawable/progress_drawable_vertical" /> - + + + + + + + --> diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index 1abfafed..405b606f 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -1,24 +1,24 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/player_holder" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:screenOrientation="landscape" + android:tag="television" + tools:orientation="vertical"> + android:id="@+id/subtitle_holder" + android:layout_width="match_parent" + android:layout_height="match_parent"> + android:id="@+id/shadow_overlay" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/player_gradient_tv" /> + android:id="@+id/player_video_holder" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="16dp"> + + + android:layout_height="wrap_content" + android:layout_marginStart="80dp" + android:layout_marginTop="20dp" + android:layout_marginEnd="32dp" + android:gravity="end" + android:textColor="@color/white" + android:textSize="16sp" + android:textStyle="bold" + tools:text="Hello world" /> - - + android:id="@+id/player_video_title_rez" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="80dp" + android:layout_marginTop="40dp" + android:layout_marginEnd="32dp" + android:gravity="end" + android:textColor="@color/white" + android:textSize="16sp" + tools:text="1920x1080" /> + android:id="@+id/player_go_back_holder" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="5dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:layout_width="30dp" + android:layout_height="30dp" + android:layout_gravity="center" + android:contentDescription="@string/go_back_img_des" + android:src="@drawable/ic_baseline_arrow_back_24" + app:tint="@android:color/white" /> + android:id="@+id/player_go_back" + android:layout_width="70dp" + android:layout_height="70dp" + android:layout_gravity="center" + android:background="@drawable/video_tap_button_always_white" + android:clickable="true" + android:contentDescription="@string/go_back_img_des" + android:focusable="true" /> + android:indeterminate="true" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:layout_marginBottom="20dp" + android:gravity="center" + android:orientation="horizontal" + android:paddingTop="4dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent"> + android:id="@id/exo_prev" + style="@style/ExoMediaButton.Previous" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_repeat_toggle" + style="@style/ExoMediaButton" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_next" + style="@style/ExoMediaButton.Next" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_vr" + style="@style/ExoMediaButton.VR" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_play" + android:layout_width="0dp" + android:layout_height="0dp" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_pause" + android:layout_width="0dp" + android:layout_height="0dp" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@+id/bottom_player_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="64dp" + android:layout_marginEnd="64dp" + android:layout_marginBottom="10dp" + android:gravity="center_vertical" + android:orientation="vertical" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> + android:id="@+id/player_video_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layoutDirection="ltr" + android:orientation="horizontal"> + android:src="@drawable/netflix_pause" + app:tint="@color/player_button_tv" + tools:ignore="ContentDescription" /> + android:id="@id/exo_position" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="end" + android:includeFontPadding="false" + android:minWidth="50dp" + android:paddingLeft="4dp" + android:paddingRight="4dp" + android:textColor="@android:color/white" + android:textSize="14sp" + android:textStyle="normal" + tools:text="15:30" /> + android:id="@id/exo_progress" + android:layout_width="0dp" + android:layout_height="30dp" + android:layout_gravity="center" + android:layout_weight="1" + android:focusable="false" + android:focusableInTouchMode="false" + app:bar_height="2dp" + app:played_color="?attr/colorPrimary" + app:scrubber_color="?attr/colorPrimary" + app:scrubber_dragged_size="26dp" + app:scrubber_enabled_size="24dp" + app:unplayed_color="@color/videoProgress" /> + android:id="@id/exo_duration" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginEnd="20dp" + android:includeFontPadding="false" + android:minWidth="50dp" + android:paddingLeft="4dp" + android:paddingRight="4dp" + android:textColor="@android:color/white" + android:textSize="14sp" + android:textStyle="normal" + tools:text="23:20" /> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center"> + android:id="@+id/player_lock_holder" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:orientation="horizontal"> + android:id="@+id/player_resize_btt" + style="@style/VideoButtonTV" + android:nextFocusLeft="@id/player_lock" + android:nextFocusRight="@id/player_speed_btt" + android:nextFocusUp="@id/player_pause_play" + android:text="@string/video_aspect_ratio_resize" + app:icon="@drawable/ic_baseline_aspect_ratio_24" /> + android:id="@+id/player_speed_btt" + style="@style/VideoButtonTV" + android:nextFocusLeft="@id/player_resize_btt" + android:nextFocusRight="@id/player_subtitle_offset_btt" + android:nextFocusUp="@id/player_pause_play" + app:icon="@drawable/ic_baseline_speed_24" + tools:text="Speed" /> + android:visibility="gone" + app:icon="@drawable/ic_outline_subtitles_24" + tools:visibility="visible" /> + android:id="@+id/player_sources_btt" + style="@style/VideoButtonTV" + android:nextFocusLeft="@id/player_subtitle_offset_btt" + android:nextFocusRight="@id/player_tracks_btt" + android:nextFocusUp="@id/player_pause_play" + android:text="@string/video_source" + app:icon="@drawable/ic_baseline_playlist_play_24" /> + android:id="@+id/player_tracks_btt" + style="@style/VideoButtonTV" + android:nextFocusLeft="@id/player_sources_btt" + android:nextFocusRight="@id/player_skip_op" + android:nextFocusUp="@id/player_pause_play" + android:text="@string/tracks" + app:icon="@drawable/ic_baseline_playlist_play_24" /> - android:nextFocusLeft="@id/player_skip_op" - android:nextFocusRight="@id/player_lock" - android:nextFocusUp="@id/player_pause_play" - android:text="@string/next_episode" - app:icon="@drawable/ic_baseline_skip_next_24" /> + diff --git a/app/src/main/res/layout/player_select_tracks.xml b/app/src/main/res/layout/player_select_tracks.xml new file mode 100644 index 00000000..d32e1b4e --- /dev/null +++ b/app/src/main/res/layout/player_select_tracks.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fea0fe83..d0c1d83a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -609,5 +609,8 @@ Download all plugins from this repository? %s (Disabled) + Tracks + Audio tracks + Video tracks Apply on Restart