diff --git a/app/build.gradle b/app/build.gradle index 3e5d172..f4ea422 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -47,6 +47,7 @@ android { } buildFeatures { viewBinding true + buildConfig true } } @@ -75,14 +76,16 @@ dependencies { implementation 'com.github.bumptech.glide:glide:4.15.1' // https://maven.google.com/web/index.html#com.google.android.exoplayer - implementation 'com.google.android.exoplayer:exoplayer-core:2.18.7' - implementation 'com.google.android.exoplayer:exoplayer-dash:2.18.7' - implementation 'com.google.android.exoplayer:exoplayer-hls:2.18.7' - implementation 'com.google.android.exoplayer:exoplayer-ui:2.18.7' + implementation 'androidx.media3:media3-exoplayer:1.1.1' + implementation 'androidx.media3:media3-exoplayer-dash:1.1.1' + implementation 'androidx.media3:media3-exoplayer-hls:1.1.1' + implementation 'androidx.media3:media3-ui:1.1.1' // https://github.com/vkay94/YouTubeTimeBar - implementation 'com.github.vkay94:YouTubeTimeBar:0.2.2' + // Forked version to support androidx.media3 + implementation 'com.github.kuylar:YouTubeTimeBar:0.2.3' // https://github.com/vkay94/DoubleTapPlayerView - implementation 'com.github.vkay94:DoubleTapPlayerView:1.0.4' + // Forked version to support androidx.media3 + implementation 'com.github.kuylar:DoubleTapPlayerView:1.0.5' } \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/SponsorBlockSegment.kt b/app/src/main/java/dev/kuylar/lighttube/SponsorBlockSegment.kt index c211af3..ee63727 100644 --- a/app/src/main/java/dev/kuylar/lighttube/SponsorBlockSegment.kt +++ b/app/src/main/java/dev/kuylar/lighttube/SponsorBlockSegment.kt @@ -6,14 +6,63 @@ import kotlin.math.roundToLong class SponsorBlockSegment( val category: String, val actionType: String, - segment: List, - val uuid: String, + val segment: List, + val uuid: String?, val videoDuration: Double, val locked: Long, val votes: Long, val description: String ) : YouTubeSegment { - override var color = 0xFFFFFF - override val endTimeMs = (segment[1] * 1000).roundToLong() - override val startTimeMs = (segment[0] * 1000).roundToLong() + override var color = 0x00000000 + override var endTimeMs: Long = 0 + override var startTimeMs: Long = 0 + + // we have to do this cus gson doesnt call init{} + constructor( + segment: SponsorBlockSegment + ) : this( + segment.category, + segment.actionType, + segment.segment, + segment.uuid, + segment.videoDuration, + segment.locked, + segment.votes, + segment.description + ) { + this.color = when (category) { + "sponsor" -> DEFAULT_SPONSOR_COLOR + "selfpromo" -> DEFAULT_SELFPROMO_COLOR + "interaction" -> DEFAULT_INTERACTION_COLOR + "intro" -> DEFAULT_INTRO_COLOR + "outro" -> DEFAULT_OUTRO_COLOR + "preview" -> DEFAULT_PREVIEW_COLOR + "music_offtopic" -> DEFAULT_MUSIC_OFFTOPIC_COLOR + else -> DEFAULT_SELFPROMO_COLOR + } + this.endTimeMs = (segment.segment[1] * 1000).roundToLong() + this.startTimeMs = (segment.segment[0] * 1000).roundToLong() + } + fun getCategoryTextId(): Int { + return when (category) { + "sponsor" -> R.string.sponsorblock_category_sponsor + "selfpromo" -> R.string.sponsorblock_category_selfpromo + "interaction" -> R.string.sponsorblock_category_interaction + "intro" -> R.string.sponsorblock_category_intro + "outro" -> R.string.sponsorblock_category_outro + "preview" -> R.string.sponsorblock_category_preview + "music_offtopic" -> R.string.sponsorblock_category_music_offtopic + else -> R.string.sponsorblock_category_unknown + } + } + + companion object { + private const val DEFAULT_SPONSOR_COLOR = 0xFF00D400.toInt() + private const val DEFAULT_SELFPROMO_COLOR = 0xFFFFFF00.toInt() + private const val DEFAULT_INTERACTION_COLOR = 0xFFCC00FF.toInt() + private const val DEFAULT_INTRO_COLOR = 0xFF00FFFF.toInt() + private const val DEFAULT_OUTRO_COLOR = 0xFF0202ED.toInt() + private const val DEFAULT_PREVIEW_COLOR = 0xFF008FD6.toInt() + private const val DEFAULT_MUSIC_OFFTOPIC_COLOR = 0xFFFF9900.toInt() + } } \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/StoryboardInfo.kt b/app/src/main/java/dev/kuylar/lighttube/StoryboardInfo.kt index eb8fbae..df06923 100644 --- a/app/src/main/java/dev/kuylar/lighttube/StoryboardInfo.kt +++ b/app/src/main/java/dev/kuylar/lighttube/StoryboardInfo.kt @@ -1,24 +1,22 @@ package dev.kuylar.lighttube -import android.util.Log import dev.kuylar.lighttube.ui.StoryboardTransformation -import java.lang.Exception -import kotlin.math.ceil import java.net.URLDecoder -import kotlin.math.abs +import kotlin.math.ceil import kotlin.math.floor import kotlin.math.truncate class StoryboardInfo( levelsString: String, - val recommendedLevel: String, + var recommendedLevel: String, videoLength: Long ) { - val levels: HashMap = HashMap() - val storyboardCount: Double - val secondsPerIndex: Double - val secondsPerFrame: Double - var lastPosition = -1L + private val levels: HashMap = HashMap() + private val storyboardCount: Double + private val secondsPerIndex: Double + private val secondsPerFrame: Double + val msPerFrame: Double + private var indexes = ArrayList>() init { levelsString.split("&").forEach { @@ -30,44 +28,62 @@ class StoryboardInfo( } storyboardCount = if (videoLength < 250) { ceil((videoLength / 2) / 25.0) - } else if (videoLength in 250..1000) { + } else if (videoLength in 250 ..1000) { ceil((videoLength / 4) / 25.0) } else { ceil((videoLength / 10) / 25.0) } - 1 - for (i in 0..(storyboardCount - 1).toInt()) { - Log.i("VideoPlayerManager", levels["2"]!!.replace("M0", "M$i")) - } + if (recommendedLevel == "2") { + secondsPerIndex = 125.0 + secondsPerFrame = 5.0 + msPerFrame = 5000.0 + + val frameCount = floor(videoLength / 5f).toInt() + 1 + val indexCount = ceil(frameCount / 25f).toInt() + + val lastIndexFrameCount = if (frameCount % 25 == 0) 25 else frameCount % 25 + + val lastIndexRows = ceil(lastIndexFrameCount / 5f).toInt() + val lastIndexColumns = if (lastIndexRows > 1) 5 else frameCount % 5 - secondsPerIndex = videoLength / storyboardCount - secondsPerFrame = secondsPerIndex + for (i in 1 until indexCount) { + indexes.add(Pair(5, 5)) + } + indexes.add(Pair(lastIndexRows, lastIndexColumns)) + } else { + secondsPerIndex = videoLength.toDouble() + secondsPerFrame = secondsPerIndex / 100 + msPerFrame = secondsPerFrame * 1000 + + indexes.add(Pair(10, 10)) + } } fun getImageUrl(position: Long): String { - return levels["2"]!!.replace("M0", "M${getStoryboardIndex(position)}") + return if (recommendedLevel == "2") + levels["2"]?.replace("M0", "M${getStoryboardIndex(position)}") ?: "" + else + levels["0"] ?: "" } private fun getStoryboardIndex(position: Long): Int { - return floor((position / 1000) / secondsPerIndex).toInt() + return if (recommendedLevel == "2") + floor((position / 1000) / secondsPerIndex).toInt() + else + 0 } fun getTransformation(position: Long): StoryboardTransformation { + val index = indexes[getStoryboardIndex(position)] val n = position / 1000 / secondsPerIndex - val positionInFrame = (n - truncate(n)) * 25 - val x = floor(positionInFrame % 5).toInt() - val y = floor(positionInFrame / 5).toInt() - Log.i("Storyboard", "pif: $positionInFrame x: $x, y: $y") - return StoryboardTransformation(x, y, 100, 50) - } - - fun throttle(position: Long) { - Log.i("Storyboard", "Difference: ${abs(lastPosition - position)}") - if (lastPosition == -1L) { - lastPosition = position - return + val xy = if (recommendedLevel == "2") { + val positionInFrame = (n - truncate(n)) * 25 + Pair(floor(positionInFrame % 5).toInt(), floor(positionInFrame / 5).toInt()) + } else { + val positionInFrame = (n - truncate(n)) * 100 + Pair(floor(positionInFrame % 10).toInt(), floor(positionInFrame / 10).toInt()) } - if (abs(lastPosition - position) < secondsPerFrame * 40) throw Exception("throttle") - lastPosition = position + return StoryboardTransformation(xy.first, xy.second, index.first, index.second) } } \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/Utils.kt b/app/src/main/java/dev/kuylar/lighttube/Utils.kt index 2a831fe..8fc14bc 100644 --- a/app/src/main/java/dev/kuylar/lighttube/Utils.kt +++ b/app/src/main/java/dev/kuylar/lighttube/Utils.kt @@ -1,6 +1,5 @@ package dev.kuylar.lighttube -import android.content.Context import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup @@ -62,14 +61,14 @@ class Utils { ).asString!! } + private fun getUserAgent(): String = + "LightTube-Android/${BuildConfig.VERSION_NAME} (https://github.com/kuylar/lighttube-android)" + fun getDislikeCount(videoId: String): Long { try { val req = Request.Builder().apply { url("https://returnyoutubedislikeapi.com/votes?videoId=$videoId") - header( - "User-Agent", - "LightTube-Android/1.0 (https://github.com/kuylar/lighttube-android)" - ) + header("User-Agent", getUserAgent()) }.build() http.newCall(req).execute().use { response -> @@ -93,12 +92,9 @@ class Utils { 0, 4 ) - }?category=sponsor&category=selfpromo&category=interaction&category=intro&category=outro&category=preview&category=music_offtopic&category=filler" - ) - header( - "User-Agent", - "LightTube-Android/1.0 (https://github.com/kuylar/lighttube-android)" + }?category=sponsor&category=selfpromo&category=interaction&category=intro&category=outro&category=preview&category=music_offtopic" ) + header("User-Agent", getUserAgent()) }.build() http.newCall(req).execute().use { response -> @@ -112,15 +108,12 @@ class Utils { } } - fun checkForUpdates(context: Context): UpdateInfo? { + fun checkForUpdates(): UpdateInfo? { var updateInfo: UpdateInfo? = null try { val req = Request.Builder().apply { - url("https://api.github.com/repos/kuylar/lighttube-android/releases") - header( - "User-Agent", - "LightTube-Android/1.0 (https://github.com/kuylar/lighttube-android)" - ) + url("https://api.github.com/repos/kuylar/lighttube-android/releases/latest") + header("User-Agent", getUserAgent()) }.build() http.newCall(req).execute().use { response -> @@ -129,11 +122,9 @@ class Utils { .create() .fromJson( response.body!!.string(), - object : TypeToken>() {}) - val latestVer = res.first().tagName.substring(1) - @Suppress("DEPRECATION") - val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) - val version = pInfo.versionName.split(" ").first() + GithubRelease::class.java) + val latestVer = res.tagName.substring(1) + val version = BuildConfig.VERSION_NAME.split(" ").first() val latestVersionCode = latestVer.replace(".", "").toInt() val currentVersionCode = version.replace(".", "").toInt() if (latestVersionCode > currentVersionCode) { @@ -141,7 +132,7 @@ class Utils { updateInfo = UpdateInfo( version, latestVer, - res.first().assets.first().browserDownloadUrl + res.assets.first().browserDownloadUrl ) } } diff --git a/app/src/main/java/dev/kuylar/lighttube/api/models/VideoDetails.kt b/app/src/main/java/dev/kuylar/lighttube/api/models/VideoDetails.kt index a5c3336..5df7d3b 100644 --- a/app/src/main/java/dev/kuylar/lighttube/api/models/VideoDetails.kt +++ b/app/src/main/java/dev/kuylar/lighttube/api/models/VideoDetails.kt @@ -1,7 +1,7 @@ package dev.kuylar.lighttube.api.models import android.os.Bundle -import com.google.android.exoplayer2.MediaMetadata +import androidx.media3.common.MediaMetadata import java.net.URLEncoder class VideoDetails( diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/StoryboardTransformation.kt b/app/src/main/java/dev/kuylar/lighttube/ui/StoryboardTransformation.kt index c99e5bc..5aa8e0c 100644 --- a/app/src/main/java/dev/kuylar/lighttube/ui/StoryboardTransformation.kt +++ b/app/src/main/java/dev/kuylar/lighttube/ui/StoryboardTransformation.kt @@ -10,12 +10,12 @@ import java.security.MessageDigest class StoryboardTransformation( private val x: Int, private val y: Int, - private val w: Int, - private val h: Int + private val r: Int, + private val c: Int ) : BitmapTransformation() { - private val ID = "dev.kuylar.lighttube.ui.StoryboardTransformation" - private val ID_BYTES = ID.toByteArray(Charset.forName("UTF-8")) + private fun getCacheKey(): String = + "dev.kuylar.lighttube.ui.StoryboardTransformation($x,$y,$r,$c)" override fun transform( pool: BitmapPool, @@ -24,17 +24,24 @@ class StoryboardTransformation( outHeight: Int ): Bitmap = Bitmap.createBitmap( toTransform, - x * (toTransform.width / 5), - y * 90, // hardcoded cus youtube sometimes doesnt has < 5 rows - w, - h + x * toTransform.width / c, + y * toTransform.height / r, + toTransform.width / c, + toTransform.height / r ) + override fun hashCode(): Int { - return ID.hashCode() + return getCacheKey().hashCode() + } + + override fun equals(other: Any?): Boolean { + return if (other is StoryboardTransformation) { + other.x == x && other.y == y && other.r == r && other.c == c + } else false } override fun updateDiskCacheKey(messageDigest: MessageDigest) { - messageDigest.update(ID_BYTES) + messageDigest.update(getCacheKey().toByteArray(Charset.forName("UTF-8"))) } } \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/VideoPlayerManager.kt b/app/src/main/java/dev/kuylar/lighttube/ui/VideoPlayerManager.kt index e62dd75..ce2ff25 100644 --- a/app/src/main/java/dev/kuylar/lighttube/ui/VideoPlayerManager.kt +++ b/app/src/main/java/dev/kuylar/lighttube/ui/VideoPlayerManager.kt @@ -1,28 +1,31 @@ package dev.kuylar.lighttube.ui -import android.content.pm.ActivityInfo import android.os.Handler import android.util.Log import android.view.View +import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import android.widget.Toast import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.os.bundleOf +import com.bumptech.glide.Glide import com.github.vkay94.dtpv.DoubleTapPlayerView import com.github.vkay94.dtpv.youtube.YouTubeOverlay import com.github.vkay94.timebar.LibTimeBar import com.github.vkay94.timebar.YouTubeChapter import com.github.vkay94.timebar.YouTubeSegment import com.github.vkay94.timebar.YouTubeTimeBar -import com.google.android.exoplayer2.C -import com.google.android.exoplayer2.ExoPlayer -import com.google.android.exoplayer2.MediaItem -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.Tracks -import com.google.android.exoplayer2.audio.AudioAttributes -import com.google.android.exoplayer2.ui.PlayerView +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.Tracks +import androidx.media3.common.AudioAttributes +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.github.vkay94.timebar.YouTubeTimeBarPreview import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.button.MaterialButton import com.google.android.material.progressindicator.CircularProgressIndicator @@ -40,19 +43,16 @@ import java.io.IOException import kotlin.concurrent.thread class VideoPlayerManager(private val activity: MainActivity) : Player.Listener, - LibTimeBar.SegmentListener { + LibTimeBar.SegmentListener, YouTubeTimeBarPreview.Listener { private var videoTracks: Tracks? = null private val playerHandler: Handler + private val playerBox: View = activity.findViewById(R.id.player_box) private val exoplayerView: DoubleTapPlayerView = activity.findViewById(R.id.player) private val doubleTapView: YouTubeOverlay = activity.findViewById(R.id.player_overlay) - private val fullscreenPlayer: DoubleTapPlayerView = - activity.findViewById(R.id.fullscreen_player) - private val fullscreenDoubleTapView: YouTubeOverlay = - activity.findViewById(R.id.fullscreen_player_overlay) private val player: ExoPlayer = ExoPlayer.Builder(activity).apply { setHandleAudioBecomingNoisy(true) }.build() - private lateinit var api: LightTubeApi + private var api: LightTubeApi private val fragmentManager = activity.supportFragmentManager private val miniplayerTitle: TextView = activity.findViewById(R.id.miniplayer_video_title) @@ -61,10 +61,18 @@ class VideoPlayerManager(private val activity: MainActivity) : Player.Listener, activity.findViewById(R.id.miniplayer_progress_bar) private val miniplayer: BottomSheetBehavior = activity.miniplayer private val playerControls = exoplayerView.findViewById(R.id.player_controls) - private var fullscreen = false private var chapters: ArrayList? = null private var storyboard: StoryboardInfo? = null + private val timeBar: YouTubeTimeBar = + exoplayerView.findViewById(androidx.media3.ui.R.id.exo_progress) + private val timeBarPreview: YouTubeTimeBarPreview = + playerBox.findViewById(R.id.player_storyboard) + private val playPauseButton: MaterialButton = exoplayerView.findViewById(R.id.player_play_pause) + private val sponsorblockSkipButton: MaterialButton = playerBox.findViewById(R.id.player_skip) + private val bufferingIndicator: CircularProgressIndicator = + playerBox.findViewById(R.id.player_buffering_progress) + init { exoplayerView.player = player player.addListener(this) @@ -96,109 +104,75 @@ class VideoPlayerManager(private val activity: MainActivity) : Player.Listener, } playerHandler.post(r) - // im sorry for this monstrosity too - forAllPlayerViews { view, isFullscreen -> - view.findViewById(R.id.player_play_pause).setOnClickListener { - if (player.isPlaying) player.pause() else player.play() - } + exoplayerView.findViewById(R.id.player_play_pause).setOnClickListener { + if (player.isPlaying) player.pause() else player.play() + } - view.findViewById(R.id.player_fullscreen).setOnClickListener { - toggleFullscreen() - } + exoplayerView.findViewById(R.id.player_fullscreen).setOnClickListener { + activity.enterFullscreen(exoplayerView, getAspectRatio() < 1) + } - view.findViewById(R.id.player_settings).setOnClickListener { - PlayerSettingsFragment(player).show(fragmentManager, null) - } + exoplayerView.findViewById(R.id.player_settings).setOnClickListener { + PlayerSettingsFragment(player).show(fragmentManager, null) + } - view.findViewById(R.id.player_captions).setOnClickListener { - if (player.currentTracks.groups.any { it.type == C.TRACK_TYPE_TEXT }) - PlayerSettingsFragment(player, "caption").show(fragmentManager, null) - } + exoplayerView.findViewById(R.id.player_captions).setOnClickListener { + if (player.currentTracks.groups.any { it.type == C.TRACK_TYPE_TEXT }) + PlayerSettingsFragment(player, "caption").show(fragmentManager, null) + } - view.findViewById(R.id.player_minimize).setOnClickListener { - if (fullscreen) toggleFullscreen() - else miniplayer.state = BottomSheetBehavior.STATE_COLLAPSED - } + exoplayerView.findViewById(R.id.player_minimize).setOnClickListener { + activity.exitFullscreen(exoplayerView) + miniplayer.state = BottomSheetBehavior.STATE_COLLAPSED + } - if (!isFullscreen) { - view.findViewById(R.id.player_metadata).visibility = View.GONE - } + timeBar.addSegmentListener(this) + timeBar.timeBarPreview(timeBarPreview) + timeBarPreview.previewListener(this) - val timeBar = - view.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress) - timeBar.addSegmentListener(this) - - if (isFullscreen) { - fullscreenDoubleTapView - .player(player) - .performListener(object : YouTubeOverlay.PerformListener { - override fun onAnimationStart() { - view.useController = false - fullscreenDoubleTapView.visibility = View.VISIBLE - } - - override fun onAnimationEnd() { - fullscreenDoubleTapView.visibility = View.GONE - view.useController = true - } - }) - view.controller(fullscreenDoubleTapView) - } else { - doubleTapView.player(player) - .performListener(object : YouTubeOverlay.PerformListener { - override fun onAnimationStart() { - view.useController = false - doubleTapView.visibility = View.VISIBLE - } - - override fun onAnimationEnd() { - doubleTapView.visibility = View.GONE - view.useController = true - } - }) - view.controller(doubleTapView) - } - } - } + doubleTapView.player(player) + .performListener(object : YouTubeOverlay.PerformListener { + override fun onAnimationStart() { + exoplayerView.useController = false + doubleTapView.visibility = View.VISIBLE + } - private fun forAllPlayerViews(runnable: ((DoubleTapPlayerView, Boolean) -> Unit)) { - runnable.invoke(exoplayerView, false) - runnable.invoke(fullscreenPlayer, true) + override fun onAnimationEnd() { + doubleTapView.visibility = View.GONE + exoplayerView.useController = true + } + }) + exoplayerView.controller(doubleTapView) } + private fun setCaptionsButtonState(buttonState: Int) { - forAllPlayerViews { playerView, _ -> - val button = - playerView.findViewById(R.id.player_captions) - when (buttonState) { - 0 -> { - button.alpha = 0.5f - button.isEnabled = false - button.icon = - ContextCompat.getDrawable(activity, R.drawable.ic_captions) - } + val button = + exoplayerView.findViewById(R.id.player_captions) + when (buttonState) { + 0 -> { + button.alpha = 0.5f + button.isEnabled = false + button.icon = + ContextCompat.getDrawable(activity, R.drawable.ic_captions) + } - 1 -> { - button.alpha = 1f - button.isEnabled = true - button.icon = - ContextCompat.getDrawable(activity, R.drawable.ic_captions) - } + 1 -> { + button.alpha = 1f + button.isEnabled = true + button.icon = + ContextCompat.getDrawable(activity, R.drawable.ic_captions) + } - 2 -> { - button.alpha = 1f - button.isEnabled = true - button.icon = - ContextCompat.getDrawable(activity, R.drawable.ic_captions_on) - } + 2 -> { + button.alpha = 1f + button.isEnabled = true + button.icon = + ContextCompat.getDrawable(activity, R.drawable.ic_captions_on) } } } - private fun getActivePlayerView(): DoubleTapPlayerView { - return if (fullscreen) fullscreenPlayer else exoplayerView - } - fun playVideo(id: String) { if (player.currentMediaItem?.mediaId == id) miniplayer.state = BottomSheetBehavior.STATE_EXPANDED @@ -249,6 +223,7 @@ class VideoPlayerManager(private val activity: MainActivity) : Player.Listener, }.build() } + @UnstableApi override fun onEvents(player: Player, events: Player.Events) { super.onEvents(player, events) @@ -259,10 +234,6 @@ class VideoPlayerManager(private val activity: MainActivity) : Player.Listener, player.currentMediaItem?.mediaMetadata?.title exoplayerView.findViewById(R.id.player_subtitle).text = player.currentMediaItem?.mediaMetadata?.artist - fullscreenPlayer.findViewById(R.id.player_title).text = - player.currentMediaItem?.mediaMetadata?.title - fullscreenPlayer.findViewById(R.id.player_subtitle).text = - player.currentMediaItem?.mediaMetadata?.artist if (player.currentMediaItem?.mediaId != null) setSponsors(player.currentMediaItem?.mediaId!!) try { @@ -281,7 +252,7 @@ class VideoPlayerManager(private val activity: MainActivity) : Player.Listener, } if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) { - getActivePlayerView().keepScreenOn = !(player.playbackState == Player.STATE_IDLE || + exoplayerView.keepScreenOn = !(player.playbackState == Player.STATE_IDLE || player.playbackState == Player.STATE_ENDED || !player.playWhenReady || !player.isPlaying) } @@ -308,6 +279,10 @@ class VideoPlayerManager(private val activity: MainActivity) : Player.Listener, } } + if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) { + activity.updateVideoAspectRatio(getAspectRatio()) + } + if (player.currentTracks.groups.none { it.type == C.TRACK_TYPE_TEXT }) { setCaptionsButtonState(0) } else if (player.trackSelectionParameters.overrides.filter { it.key.type == C.TRACK_TYPE_TEXT } @@ -317,34 +292,31 @@ class VideoPlayerManager(private val activity: MainActivity) : Player.Listener, setCaptionsButtonState(1) } - getActivePlayerView().findViewById(R.id.player_play_pause).icon = + playPauseButton.icon = AppCompatResources.getDrawable( activity, if (player.isPlaying) R.drawable.ic_pause else R.drawable.ic_play ) - getActivePlayerView().findViewById(R.id.player_buffering_progress).visibility = - if (player.playbackState == Player.STATE_BUFFERING) View.VISIBLE else View.GONE - } - - private fun setStoryboards(levels: String?, recommendedLevel: String?, length: Long?) { - return //FIXME: disabled until i write a working storyboard view - // if (levels == null || length == null || recommendedLevel == null) storyboard = null - // storyboard = StoryboardInfo(levels!!, recommendedLevel!!, length!!) + if (player.playbackState == Player.STATE_BUFFERING) { + playPauseButton.visibility = View.INVISIBLE + bufferingIndicator.visibility = View.VISIBLE + } else { + playPauseButton.visibility = View.VISIBLE + bufferingIndicator.visibility = View.GONE + } } private fun setSponsors(videoId: String) { thread { val sponsors = Utils.getSponsorBlockInfo(videoId) activity.runOnUiThread { - forAllPlayerViews { player, _ -> - if (sponsors != null) { - player.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress).segments = - sponsors.segments - } else { - player.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress).segments = - listOf(SponsorBlockSegment("", "", listOf(0.0, 0.0), "", 0.0, 0, 0, "")) - } + if (sponsors != null) { + timeBar.segments = + sponsors.segments.map { SponsorBlockSegment(it) } + } else { + timeBar.segments = + emptyList() } } } @@ -382,41 +354,7 @@ class VideoPlayerManager(private val activity: MainActivity) : Player.Listener, playerControls.visibility = if (visible) View.VISIBLE else View.GONE } - private fun toggleFullscreen() { - if (fullscreen) { - (fullscreenPlayer.parent as View).visibility = View.GONE - fullscreenPlayer.visibility = View.GONE - PlayerView.switchTargetView(player, fullscreenPlayer, exoplayerView) - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE - getActivePlayerView().findViewById(R.id.player_fullscreen).icon = - ContextCompat.getDrawable( - activity, - R.drawable.ic_fullscreen_exit - ) - fullscreen = false - } else { - (fullscreenPlayer.parent as View).visibility = View.VISIBLE - fullscreenPlayer.visibility = View.VISIBLE - PlayerView.switchTargetView(player, exoplayerView, fullscreenPlayer) - activity.requestedOrientation = - if (getAspectRatio() < 1) ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT - else ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - activity.window.decorView.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_FULLSCREEN - or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - ) - getActivePlayerView().findViewById(R.id.player_fullscreen).icon = - ContextCompat.getDrawable( - activity, - R.drawable.ic_fullscreen - ) - fullscreen = true - } - } - - private fun getAspectRatio(): Float { + fun getAspectRatio(): Float { return try { player.videoSize.width.toFloat() / player.videoSize.height.toFloat() } catch (e: Exception) { @@ -424,12 +362,6 @@ class VideoPlayerManager(private val activity: MainActivity) : Player.Listener, } } - fun exitFullscreen(): Boolean { - if (!fullscreen) return false - toggleFullscreen() - return true - } - fun setVolume(volume: Float) { player.volume = volume } @@ -459,14 +391,10 @@ class VideoPlayerManager(private val activity: MainActivity) : Player.Listener, if (videoId != player.currentMediaItem?.mediaId) return this.chapters = chapters if (chapters != null) { - exoplayerView.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress).chapters = - chapters - fullscreenPlayer.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress).chapters = + timeBar.chapters = chapters } else { - exoplayerView.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress).chapters = - listOf(VideoChapter(null, emptyList(), 0)) - fullscreenPlayer.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress).chapters = + timeBar.chapters = listOf(VideoChapter(null, emptyList(), 0)) } } @@ -476,11 +404,49 @@ class VideoPlayerManager(private val activity: MainActivity) : Player.Listener, } override fun onSegmentChanged(timeBar: LibTimeBar, newSegment: YouTubeSegment?) { - if (newSegment != null) - Toast.makeText( - activity, - (newSegment as SponsorBlockSegment).actionType, - Toast.LENGTH_LONG - ).show() + // this is not used as it can skip segments if they are very short + } + + fun getCurrentSegment(): SponsorBlockSegment? { + return timeBar.segments.firstOrNull { player.currentPosition in it.startTimeMs..it.endTimeMs } as SponsorBlockSegment? + } + + fun updateSkipButton(segment: SponsorBlockSegment?) { + if (segment == null) { + sponsorblockSkipButton.visibility = View.GONE + sponsorblockSkipButton.setOnClickListener {} + } else { + sponsorblockSkipButton.text = activity.getString( + R.string.sponsorblock_skip_template, + activity.getString(segment.getCategoryTextId()) + ) + sponsorblockSkipButton.setOnClickListener { + player.seekTo(segment.endTimeMs) + } + sponsorblockSkipButton.visibility = View.VISIBLE + } + } + + private fun setStoryboards(levels: String?, recommendedLevel: String?, length: Long?) { + if (levels != null && length != null && recommendedLevel != null && length >= 0) { + storyboard = StoryboardInfo(levels, recommendedLevel, length) + timeBarPreview.durationPerFrame(storyboard!!.msPerFrame.toLong()) + return + } + storyboard = null + } + + override fun loadThumbnail(imageView: ImageView, position: Long) { + if (storyboard == null) return + try { + Glide.with(activity) + .load(storyboard!!.getImageUrl(position)) + //.override(SIZE_ORIGINAL, SIZE_ORIGINAL) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .transform(storyboard!!.getTransformation(position)) + .into(imageView) + } catch (e: Exception) { + Log.e("Storyboard", "Failed to update storyboard", e) + } } } \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/activity/CrashHandlerActivity.kt b/app/src/main/java/dev/kuylar/lighttube/ui/activity/CrashHandlerActivity.kt index 2526292..ff9e5d6 100644 --- a/app/src/main/java/dev/kuylar/lighttube/ui/activity/CrashHandlerActivity.kt +++ b/app/src/main/java/dev/kuylar/lighttube/ui/activity/CrashHandlerActivity.kt @@ -1,32 +1,29 @@ package dev.kuylar.lighttube.ui.activity +import android.annotation.SuppressLint import android.content.pm.PackageManager import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import dev.kuylar.lighttube.BuildConfig import dev.kuylar.lighttube.databinding.ActivityCrashHandlerBinding +@SuppressLint("SetTextI18n") +// todo: make this translatable +// or make it so this is never shown to the user, both works class CrashHandlerActivity : AppCompatActivity() { private lateinit var binding: ActivityCrashHandlerBinding - @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityCrashHandlerBinding.inflate(layoutInflater) setContentView(binding.root) - val version = try { - val pInfo = packageManager.getPackageInfo(packageName, 0) - "${pInfo.versionName} (build #${pInfo.versionCode})" - } catch (e: PackageManager.NameNotFoundException) { - null - } - val sp = getSharedPreferences("main", MODE_PRIVATE) binding.errorInstance.text = sp.getString("instanceHost", "") - binding.errorVersion.text = version + binding.errorVersion.text = "${BuildConfig.VERSION_NAME} (build #${BuildConfig.VERSION_CODE})" binding.errorTrace.text = (intent.extras?.getString("trace") ?: "No stack trace provided") binding.errorTrace.setHorizontallyScrolling(true) } diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/activity/MainActivity.kt b/app/src/main/java/dev/kuylar/lighttube/ui/activity/MainActivity.kt index c4e8bbd..e04326d 100644 --- a/app/src/main/java/dev/kuylar/lighttube/ui/activity/MainActivity.kt +++ b/app/src/main/java/dev/kuylar/lighttube/ui/activity/MainActivity.kt @@ -2,10 +2,13 @@ package dev.kuylar.lighttube.ui.activity import android.annotation.SuppressLint import android.content.Intent +import android.content.pm.ActivityInfo import android.os.Bundle +import android.os.Handler import android.util.Log import android.view.Menu import android.view.View +import android.view.ViewGroup import android.widget.AdapterView.OnItemClickListener import android.widget.ArrayAdapter import androidx.activity.addCallback @@ -13,7 +16,13 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView.SearchAutoComplete import androidx.constraintlayout.motion.widget.MotionLayout +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.get +import androidx.core.view.updateLayoutParams import androidx.navigation.NavController import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration @@ -21,20 +30,23 @@ import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.button.MaterialButton import com.google.android.material.elevation.SurfaceColors import dev.kuylar.lighttube.R import dev.kuylar.lighttube.Utils import dev.kuylar.lighttube.api.LightTubeApi +import dev.kuylar.lighttube.api.models.LightTubeException import dev.kuylar.lighttube.databinding.ActivityMainBinding import dev.kuylar.lighttube.ui.VideoPlayerManager import dev.kuylar.lighttube.ui.fragment.UpdateFragment +import java.io.IOException import kotlin.concurrent.thread import kotlin.math.max import kotlin.math.min +import kotlin.math.roundToInt class MainActivity : AppCompatActivity() { - private lateinit var navController: NavController private lateinit var binding: ActivityMainBinding lateinit var miniplayer: BottomSheetBehavior @@ -42,6 +54,8 @@ class MainActivity : AppCompatActivity() { lateinit var player: VideoPlayerManager private lateinit var api: LightTubeApi private var loadingSuggestions = false + private var fullscreen = false + private lateinit var sponsorBlockButton: MaterialButton override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -62,6 +76,7 @@ class MainActivity : AppCompatActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + sponsorBlockButton = findViewById(R.id.player_skip) val navView: BottomNavigationView = binding.navView navController = findNavController(R.id.nav_host_fragment_activity_main) @@ -78,18 +93,8 @@ class MainActivity : AppCompatActivity() { navView.setupWithNavController(navController) onBackPressedDispatcher.addCallback(this) { -// if (player.closeSheets()) -// Toast.makeText(this@MainActivity, "closed player sheets", Toast.LENGTH_LONG).show() -// else if (player.exitFullscreen()) -// Toast.makeText(this@MainActivity, "exited fullscreen", Toast.LENGTH_LONG).show() -// else if (minimizePlayer()) -// Toast.makeText(this@MainActivity, "minimized player", Toast.LENGTH_LONG).show() -// else if (navController.popBackStack()) -// Toast.makeText(this@MainActivity, "pop back stack", Toast.LENGTH_LONG).show() -// else -// Toast.makeText(this@MainActivity, "nothing else to do", Toast.LENGTH_LONG).show() if (!player.closeSheets()) // attempt to close details/comments - if (!player.exitFullscreen()) // attempt to exit fullscreen + if (!tryExitFullscreen()) // attempt to exit fullscreen if (!minimizePlayer()) // attempt to minimize the player sheet if (!navController.popBackStack()) // attempt to go back on the fragment history finish() // close the app @@ -131,17 +136,20 @@ class MainActivity : AppCompatActivity() { else binding.navView.visibility = View.VISIBLE - player.toggleControls(slideOffset * 5 > 0.9) + player.toggleControls(slideOffset * 5 >= 1) } }) // check for updates thread { - val update = Utils.checkForUpdates(this) + val update = Utils.checkForUpdates() if (update != null) { val skippedUpdate = sp.getString("skippedUpdate", null) if (skippedUpdate == update.latestVersion) { - Log.i("UpdateChecker", "User skipped update $skippedUpdate. Not showing the update dialog.") + Log.i( + "UpdateChecker", + "User skipped update $skippedUpdate. Not showing the update dialog." + ) } else { runOnUiThread { UpdateFragment(update).show(supportFragmentManager, null) @@ -149,6 +157,18 @@ class MainActivity : AppCompatActivity() { } } } + + val handler = Handler(mainLooper) + var sponsorblockRunnable = Runnable {} + sponsorblockRunnable = Runnable { + try { + player.updateSkipButton(player.getCurrentSegment()) + } catch (e: Exception) { + Log.e("SponsorBlockLoop", "Failed to update SponsorBlock skip button", e) + } + handler.postDelayed(sponsorblockRunnable, 100) + } + handler.postDelayed(sponsorblockRunnable, 100) } private fun setApi() { @@ -176,7 +196,7 @@ class MainActivity : AppCompatActivity() { searchView.findViewById(androidx.appcompat.R.id.search_src_text) as SearchAutoComplete searchAutoComplete.onItemClickListener = - OnItemClickListener { adapterView, view, itemIndex, id -> + OnItemClickListener { adapterView, _, itemIndex, _ -> val queryString = adapterView.getItemAtPosition(itemIndex) as String searchAutoComplete.setText(queryString) searchAutoComplete.setSelection(queryString.length) @@ -222,7 +242,9 @@ class MainActivity : AppCompatActivity() { searchAutoComplete.showDropDown() loadingSuggestions = false } - } catch (e: Exception) { + } catch (e: LightTubeException) { + loadingSuggestions = false + } catch (e: IOException) { loadingSuggestions = false } } @@ -235,4 +257,92 @@ class MainActivity : AppCompatActivity() { } catch (_: Exception) { } } + + fun enterFullscreen(playerView: View, isPortrait: Boolean) { + fullscreen = true + val parentToMove = playerView.parent as View + (parentToMove.parent as ViewGroup).removeView(parentToMove) + binding.fullscreenPlayerContainer.addView(parentToMove) + + WindowCompat.setDecorFitsSystemWindows(window, false) + WindowInsetsControllerCompat(window, window.decorView).let { controller -> + controller.hide(WindowInsetsCompat.Type.systemBars()) + controller.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + + playerView.findViewById(R.id.player_fullscreen).apply { + icon = ResourcesCompat.getDrawable(resources, R.drawable.ic_fullscreen_exit, theme) + setOnClickListener { + exitFullscreen(playerView) + } + } + playerView.findViewById(R.id.player_metadata).visibility = View.VISIBLE + binding.fullscreenPlayerContainer.visibility = View.VISIBLE + miniplayer.isDraggable = false + requestedOrientation = + if (isPortrait) ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + else ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + + sponsorBlockButton.updateLayoutParams { + bottomMargin = + resources.getDimension(R.dimen.sponsorblock_margin_bottom_fullscreen).roundToInt() + } + } + + fun exitFullscreen(playerView: View) { + fullscreen = false + val parentToMove = playerView.parent as View + (parentToMove.parent as ViewGroup).removeView(parentToMove) + findViewById(R.id.player_container).addView(parentToMove) + + WindowCompat.setDecorFitsSystemWindows(window, true) + WindowInsetsControllerCompat( + window, + window.decorView + ).show(WindowInsetsCompat.Type.systemBars()) + + playerView.findViewById(R.id.player_fullscreen).apply { + icon = ResourcesCompat.getDrawable(resources, R.drawable.ic_fullscreen, theme) + setOnClickListener { + enterFullscreen(playerView, player.getAspectRatio() < 1) + } + } + playerView.findViewById(R.id.player_metadata).visibility = View.INVISIBLE + binding.fullscreenPlayerContainer.visibility = View.GONE + miniplayer.isDraggable = true + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_USER + + sponsorBlockButton.updateLayoutParams { + bottomMargin = + resources.getDimension(R.dimen.sponsorblock_margin_bottom_default).roundToInt() + } + } + + private fun tryExitFullscreen(): Boolean { + if (fullscreen) { + exitFullscreen(findViewById(R.id.player)) + return true + } + return false + } + + fun updateVideoAspectRatio(aspectRatio: Float) { + val clampedAspectRatio = if (aspectRatio.isNaN()) 16f / 9f else aspectRatio.coerceIn(1f, 2f) + Log.i( + "VideoPlayer", + "Updating player aspect ratio to $clampedAspectRatio (original: $aspectRatio)" + ) + miniplayerScene.getConstraintSet(R.id.end)?.let { + it.setDimensionRatio(R.id.player_container, clampedAspectRatio.toString()) + miniplayerScene.updateState(R.id.end, it) + miniplayerScene.rebuildScene() + + // SORRY BUT I COULDNT UPDATE THE LAYOUT OTHERWISE 😭😭😭 + miniplayerScene.setProgress(0.999f, 10f) + Handler(mainLooper).postDelayed({ + miniplayerScene.progress = 1f + }, 10) + } + } } \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/fragment/PlayerSettingsFragment.kt b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/PlayerSettingsFragment.kt index a3615f8..f36af2e 100644 --- a/app/src/main/java/dev/kuylar/lighttube/ui/fragment/PlayerSettingsFragment.kt +++ b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/PlayerSettingsFragment.kt @@ -1,3 +1,7 @@ +// TODO: Migrate to media3 +// Blocked by com.github.vkay94.timebar.YouTubeTimeBar +@file:Suppress("DEPRECATION") + package dev.kuylar.lighttube.ui.fragment import android.annotation.SuppressLint @@ -6,10 +10,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout -import com.google.android.exoplayer2.C -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.Tracks -import com.google.android.exoplayer2.trackselection.TrackSelectionOverride +import androidx.media3.common.C +import androidx.media3.common.Player +import androidx.media3.common.Tracks +import androidx.media3.common.TrackSelectionOverride import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dev.kuylar.lighttube.R import dev.kuylar.lighttube.databinding.FragmentPlayerSettingsBinding diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/fragment/VideoInfoFragment.kt b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/VideoInfoFragment.kt index dd29d22..e40f4e9 100644 --- a/app/src/main/java/dev/kuylar/lighttube/ui/fragment/VideoInfoFragment.kt +++ b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/VideoInfoFragment.kt @@ -110,12 +110,12 @@ class VideoInfoFragment : Fragment() { } fun closeSheets(): Boolean { - if (commentsSheet.state != BottomSheetBehavior.STATE_HIDDEN) { + if (commentsSheet.state == BottomSheetBehavior.STATE_EXPANDED) { commentsSheet.state = BottomSheetBehavior.STATE_HIDDEN return true } - if (detailsSheet.state != BottomSheetBehavior.STATE_HIDDEN) { + if (detailsSheet.state == BottomSheetBehavior.STATE_EXPANDED) { detailsSheet.state = BottomSheetBehavior.STATE_HIDDEN return true } diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/viewholder/SlimVideoInfoRenderer.kt b/app/src/main/java/dev/kuylar/lighttube/ui/viewholder/SlimVideoInfoRenderer.kt index 12710d7..8f03ec6 100644 --- a/app/src/main/java/dev/kuylar/lighttube/ui/viewholder/SlimVideoInfoRenderer.kt +++ b/app/src/main/java/dev/kuylar/lighttube/ui/viewholder/SlimVideoInfoRenderer.kt @@ -1,8 +1,8 @@ package dev.kuylar.lighttube.ui.viewholder import android.content.Intent -import android.os.Bundle import android.view.View +import androidx.core.os.bundleOf import androidx.navigation.findNavController import com.bumptech.glide.Glide import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -40,12 +40,10 @@ open class SlimVideoInfoRenderer(private val binding: RendererSlimVideoInfoBindi activity.player.setSheets(details = true, comments = false) } - binding.channelAvatar.setOnClickListener { + binding.channelContainer.setOnClickListener { activity.miniplayer.state = BottomSheetBehavior.STATE_COLLAPSED - val b = Bundle() - b.putString("id", video.channel.id) activity.findNavController(R.id.nav_host_fragment_activity_main) - .navigate(R.id.navigation_channel, b) + .navigate(R.id.navigation_channel, bundleOf(Pair("id", video.channel.id))) } binding.buttonShare.setOnClickListener { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 211b2c4..e77ed7f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,7 +1,6 @@ @@ -68,28 +67,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:elevation="10dp" - android:visibility="gone"> - - - - - + android:visibility="gone" /> \ No newline at end of file diff --git a/app/src/main/res/layout/exo_playback_control_view.xml b/app/src/main/res/layout/exo_playback_control_view.xml index e680ad4..ec21446 100644 --- a/app/src/main/res/layout/exo_playback_control_view.xml +++ b/app/src/main/res/layout/exo_playback_control_view.xml @@ -27,10 +27,13 @@ + android:layout_weight="1" + android:orientation="vertical" + android:visibility="invisible" + tools:visibility="visible"> - - - - + android:layout_height="wrap_content" + app:layout_constraintBottom_toTopOf="@id/exo_progress" + app:layout_constraintStart_toStartOf="parent"> + - - - + android:layout_height="match_parent" /> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/renderer_slim_video_info.xml b/app/src/main/res/layout/renderer_slim_video_info.xml index aff897f..468cdc6 100644 --- a/app/src/main/res/layout/renderer_slim_video_info.xml +++ b/app/src/main/res/layout/renderer_slim_video_info.xml @@ -8,24 +8,55 @@ android:paddingBottom="8dp"> - + android:background="?attr/selectableItemBackground" + android:orientation="vertical"> + + + + + + + + + + @@ -40,39 +71,13 @@ - - - - - - + tools:text="Channel Title" /> - - 16dp 16dp + + 96dp + 32dp \ 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 796afbf..a6ef62f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -99,4 +99,15 @@ Your data is stored in the instance you chose (%s). Download Skip this version Update skipped. You can always re-check updates under Settings → About → Check for Updates + + + Skip %s + sponsor + self promotion + interaction + intro + outro + preview + non-music section + segment \ No newline at end of file