[fix] Landscape Playlist

This commit is contained in:
acite
2025-09-27 21:07:28 +08:00
parent c21defb426
commit 9bad0dcbc2
4 changed files with 282 additions and 72 deletions

View File

@@ -0,0 +1,139 @@
package com.acitelight.aether.view
import android.R
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PaintingStyle.Companion.Stroke
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.ImageLoader
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import com.acitelight.aether.model.Video
@Composable
fun MiniPlaylistCard(modifier: Modifier, video: Video, imageLoader: ImageLoader, selected: Boolean, onClick: () -> Unit) {
val colorScheme = MaterialTheme.colorScheme
Card(
modifier = modifier
.height(80.dp)
.fillMaxWidth(),
colors = CardColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onSurface,
disabledContentColor = Color.Transparent,
disabledContainerColor = Color.Transparent
),
onClick = onClick
)
{
Row()
{
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(video.getCover())
.memoryCacheKey("${video.klass}/${video.id}/cover")
.diskCacheKey("${video.klass}/${video.id}/cover")
.listener(
onStart = { },
onError = { _, _ -> }
)
.build(),
contentDescription = null,
modifier = Modifier
.width(128.dp)
.fillMaxHeight()
.clip(RoundedCornerShape(8.dp))
.then(
if (selected)
Modifier.drawWithContent {
drawContent()
val strokeWidth = 3.dp.toPx()
val shape = RoundedCornerShape(8.dp)
val outline = shape.createOutline(size, layoutDirection, this)
drawOutline(
outline = outline,
color = colorScheme.primary,
style = Stroke(width = strokeWidth)
)
}
else
Modifier
),
contentScale = ContentScale.Crop,
imageLoader = imageLoader
)
Column(
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxHeight()
.fillMaxWidth()
.align(Alignment.CenterVertically),
verticalArrangement = Arrangement.Center,
)
{
Text(
modifier = Modifier,
text = video.video.name,
fontSize = 13.sp,
maxLines = 2,
fontWeight = FontWeight.Bold,
lineHeight = 14.sp,
color = if(selected) colorScheme.primary else colorScheme.onSurface
)
Spacer(modifier.weight(1f))
Text(
modifier = Modifier.height(16.dp),
text = video.klass,
fontSize = 8.sp,
lineHeight = 9.sp,
maxLines = 1,
fontWeight = FontWeight.Bold,
color = if(selected) colorScheme.primary else colorScheme.onSurface
)
Text(
modifier = Modifier.height(16.dp),
text = formatTime(video.video.duration),
fontSize = 8.sp,
lineHeight = 9.sp,
maxLines = 1,
fontWeight = FontWeight.Bold,
color = if(selected) colorScheme.primary else colorScheme.onSurface
)
}
}
}
}

View File

@@ -153,7 +153,6 @@ fun VideoPlayer(
} }
} }
val colorScheme = MaterialTheme.colorScheme
videoPlayerViewModel.init(videoId) videoPlayerViewModel.init(videoId)
activity.requestedOrientation = activity.requestedOrientation =
@@ -164,39 +163,7 @@ fun VideoPlayer(
if (videoPlayerViewModel.startPlaying) { if (videoPlayerViewModel.startPlaying) {
if (videoPlayerViewModel.isLandscape) { if (videoPlayerViewModel.isLandscape) {
Box {
VideoPlayerLandscape(videoPlayerViewModel) VideoPlayerLandscape(videoPlayerViewModel)
AnimatedVisibility(
visible = videoPlayerViewModel.locked || videoPlayerViewModel.planeVisibility,
enter = fadeIn(
initialAlpha = 0f,
),
exit = fadeOut(
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.CenterEnd)
) {
Card(
modifier = Modifier.padding(4.dp),
colors = CardDefaults.cardColors(
containerColor = colorScheme.primary.copy(
if (videoPlayerViewModel.locked) 0.2f else 1f
)
),
onClick = {
videoPlayerViewModel.locked = !videoPlayerViewModel.locked
}) {
Icon(
imageVector = if (videoPlayerViewModel.locked) Icons.Default.LockOpen else Icons.Default.Lock,
contentDescription = "Lock",
tint = Color.White.copy(if (videoPlayerViewModel.locked) 0.2f else 1f),
modifier = Modifier
.size(36.dp)
.padding(6.dp)
)
}
}
}
} else { } else {
VideoPlayerPortal(videoPlayerViewModel, navController) VideoPlayerPortal(videoPlayerViewModel, navController)
} }

View File

@@ -8,7 +8,9 @@ import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectDragGestures
@@ -16,28 +18,35 @@ import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.automirrored.filled.VolumeUp import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material.icons.filled.Brightness4 import androidx.compose.material.icons.filled.Brightness4
import androidx.compose.material.icons.filled.FastForward import androidx.compose.material.icons.filled.FastForward
import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material.icons.filled.FullscreenExit import androidx.compose.material.icons.filled.FullscreenExit
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -54,20 +63,23 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.viewModelScope
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import com.acitelight.aether.ToggleFullScreen import com.acitelight.aether.ToggleFullScreen
import com.acitelight.aether.viewModel.VideoPlayerViewModel import com.acitelight.aether.viewModel.VideoPlayerViewModel
import kotlinx.coroutines.launch
import kotlin.math.abs import kotlin.math.abs
@androidx.annotation.OptIn(UnstableApi::class) @androidx.annotation.OptIn(UnstableApi::class)
@Composable @Composable
fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
val colorScheme = MaterialTheme.colorScheme
val context = LocalContext.current val context = LocalContext.current
val activity = (context as? Activity)!! val activity = (context as? Activity)!!
val exoPlayer: ExoPlayer = videoPlayerViewModel.player!!; val exoPlayer: ExoPlayer = videoPlayerViewModel.player!!
val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager } val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager }
val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) } val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) }
@@ -78,6 +90,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
} }
val name by videoPlayerViewModel.currentName val name by videoPlayerViewModel.currentName
val id by videoPlayerViewModel.currentId
fun setVolume(value: Int) { fun setVolume(value: Int) {
audioManager.setStreamVolume( audioManager.setStreamVolume(
@@ -100,21 +113,10 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
.align(Alignment.Center) .align(Alignment.Center)
) )
{ {
AndroidView( Box(
factory = { Modifier
PlayerView( .fillMaxSize()
it .pointerInput(videoPlayerViewModel) {
).apply {
player = exoPlayer
useController = false
subtitleView?.let { sv ->
sv.visibility = View.GONE
}
}
},
modifier = Modifier
.fillMaxWidth()
.pointerInput(Unit) {
detectDragGestures( detectDragGestures(
onDragStart = { offset -> onDragStart = { offset ->
if (videoPlayerViewModel.locked) return@detectDragGestures if (videoPlayerViewModel.locked) return@detectDragGestures
@@ -157,9 +159,9 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
.toFloat() / maxVolume.toFloat() .toFloat() / maxVolume.toFloat()
if (dragAmount.y < 0) if (dragAmount.y < 0)
setVolume(cu + 1); setVolume(cu + 1)
else if (dragAmount.y > 0) else if (dragAmount.y > 0)
setVolume(cu - 1); setVolume(cu - 1)
} else if (videoPlayerViewModel.draggingPurpose == 1) { } else if (videoPlayerViewModel.draggingPurpose == 1) {
moveBrit(dragAmount.y, activity, videoPlayerViewModel) moveBrit(dragAmount.y, activity, videoPlayerViewModel)
} }
@@ -167,7 +169,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
} }
) )
} }
.pointerInput(Unit) { .pointerInput(videoPlayerViewModel) {
detectTapGestures( detectTapGestures(
onDoubleTap = { onDoubleTap = {
if (videoPlayerViewModel.locked) return@detectTapGestures if (videoPlayerViewModel.locked) return@detectTapGestures
@@ -177,6 +179,10 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
}, },
onTap = { onTap = {
if (videoPlayerViewModel.locked) return@detectTapGestures if (videoPlayerViewModel.locked) return@detectTapGestures
if (videoPlayerViewModel.showPlaylist) {
videoPlayerViewModel.showPlaylist = false
return@detectTapGestures
}
videoPlayerViewModel.planeVisibility = videoPlayerViewModel.planeVisibility =
!videoPlayerViewModel.planeVisibility !videoPlayerViewModel.planeVisibility
@@ -197,8 +203,23 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
} }
}, },
) )
}) {
AndroidView(
factory = {
PlayerView(
it
).apply {
player = exoPlayer
useController = false
subtitleView?.let { sv ->
sv.visibility = View.GONE
} }
}
},
modifier = Modifier.fillMaxWidth()
) )
}
androidx.compose.animation.AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.draggingPurpose == 0, visible = videoPlayerViewModel.draggingPurpose == 0,
@@ -213,9 +234,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
{ {
Text( Text(
text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${ text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${
formatTime( formatTime(exoPlayer.duration)
(exoPlayer.duration).toLong()
)
}", }",
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 12.dp), modifier = Modifier.padding(bottom = 12.dp),
@@ -234,9 +253,11 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) )
{ {
Row(Modifier Row(
Modifier
.background(Color(0x88000000), RoundedCornerShape(18)) .background(Color(0x88000000), RoundedCornerShape(18))
.width(200.dp)) .width(200.dp)
)
{ {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.VolumeUp, imageVector = Icons.AutoMirrored.Filled.VolumeUp,
@@ -269,9 +290,11 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) )
{ {
Row(Modifier Row(
Modifier
.background(Color(0x88000000), RoundedCornerShape(18)) .background(Color(0x88000000), RoundedCornerShape(18))
.width(200.dp)) .width(200.dp)
)
{ {
Icon( Icon(
imageVector = Icons.Default.Brightness4, imageVector = Icons.Default.Brightness4,
@@ -375,7 +398,8 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
Text( Text(
text = name, text = name,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 12.dp) modifier = Modifier
.padding(top = 12.dp)
.align(Alignment.CenterVertically), .align(Alignment.CenterVertically),
fontSize = 18.sp fontSize = 18.sp
) )
@@ -407,9 +431,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
) { ) {
Text( Text(
text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${ text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${
formatTime( formatTime(exoPlayer.duration)
(exoPlayer.duration).toLong()
)
}", }",
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 12.dp), modifier = Modifier.padding(bottom = 12.dp),
@@ -464,6 +486,87 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
modifier = Modifier.size(32.dp) modifier = Modifier.size(32.dp)
) )
} }
IconButton(
onClick = {
videoPlayerViewModel.showPlaylist = true
},
Modifier
.size(36.dp)
.align(Alignment.CenterVertically)
) {
Icon(
Icons.AutoMirrored.Filled.List,
contentDescription = "Playlist",
tint = Color.White,
modifier = Modifier.size(32.dp)
)
}
}
}
}
AnimatedVisibility(
visible = videoPlayerViewModel.locked || videoPlayerViewModel.planeVisibility,
enter = fadeIn(
initialAlpha = 0f,
),
exit = fadeOut(
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.CenterEnd)
)
{
Card(
modifier = Modifier.padding(4.dp),
colors = CardDefaults.cardColors(
containerColor = colorScheme.primary.copy(
if (videoPlayerViewModel.locked) 0.2f else 1f
)
),
onClick = {
videoPlayerViewModel.locked = !videoPlayerViewModel.locked
}) {
Icon(
imageVector = if (videoPlayerViewModel.locked) Icons.Default.LockOpen else Icons.Default.Lock,
contentDescription = "Lock",
tint = Color.White.copy(if (videoPlayerViewModel.locked) 0.2f else 1f),
modifier = Modifier
.size(36.dp)
.padding(6.dp)
)
}
}
AnimatedVisibility(
visible = videoPlayerViewModel.showPlaylist,
enter = slideInHorizontally(initialOffsetX = { full -> full }),
exit = slideOutHorizontally(targetOffsetX = { full -> full }),
modifier = Modifier.align(Alignment.CenterEnd)
)
{
Card(
Modifier
.fillMaxHeight()
.width(320.dp)
.align(Alignment.CenterEnd),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = colorScheme.surface.copy(0.75f))
)
{
LazyColumn(contentPadding = PaddingValues(vertical = 4.dp)) {
items(videoPlayerViewModel.videos) { item ->
MiniPlaylistCard(Modifier.padding(4.dp), video = item, imageLoader = videoPlayerViewModel.imageLoader!!,
selected = id == item.id)
{
if (name == item.video.name)
return@MiniPlaylistCard
videoPlayerViewModel.viewModelScope.launch {
videoPlayerViewModel.startPlay(item)
}
}
}
} }
} }
} }

View File

@@ -60,6 +60,7 @@ class VideoPlayerViewModel @Inject constructor(
val recentManager: RecentManager, val recentManager: RecentManager,
val videoLibrary: VideoLibrary, val videoLibrary: VideoLibrary,
) : ViewModel() { ) : ViewModel() {
var showPlaylist by mutableStateOf(false)
var isLandscape by mutableStateOf(false) var isLandscape by mutableStateOf(false)
var tabIndex by mutableIntStateOf(0) var tabIndex by mutableIntStateOf(0)
var isPlaying by mutableStateOf(true) var isPlaying by mutableStateOf(true)