From 9bad0dcbc2019dd2fcf4b3c81fc06890417a58bb Mon Sep 17 00:00:00 2001 From: acite <1498045907@qq.com> Date: Sat, 27 Sep 2025 21:07:28 +0800 Subject: [PATCH] [fix] Landscape Playlist --- .../aether/view/MiniPlaylistCard.kt | 139 ++++++++++++++ .../com/acitelight/aether/view/VideoPlayer.kt | 35 +--- .../aether/view/VideoPlayerLandscape.kt | 179 ++++++++++++++---- .../aether/viewModel/VideoPlayerViewModel.kt | 1 + 4 files changed, 282 insertions(+), 72 deletions(-) create mode 100644 app/src/main/java/com/acitelight/aether/view/MiniPlaylistCard.kt diff --git a/app/src/main/java/com/acitelight/aether/view/MiniPlaylistCard.kt b/app/src/main/java/com/acitelight/aether/view/MiniPlaylistCard.kt new file mode 100644 index 0000000..f648ac3 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/view/MiniPlaylistCard.kt @@ -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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/view/VideoPlayer.kt b/app/src/main/java/com/acitelight/aether/view/VideoPlayer.kt index 79dd1b8..f6a4a39 100644 --- a/app/src/main/java/com/acitelight/aether/view/VideoPlayer.kt +++ b/app/src/main/java/com/acitelight/aether/view/VideoPlayer.kt @@ -153,7 +153,6 @@ fun VideoPlayer( } } - val colorScheme = MaterialTheme.colorScheme videoPlayerViewModel.init(videoId) activity.requestedOrientation = @@ -164,39 +163,7 @@ fun VideoPlayer( if (videoPlayerViewModel.startPlaying) { if (videoPlayerViewModel.isLandscape) { - Box { - 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) - ) - } - } - } + VideoPlayerLandscape(videoPlayerViewModel) } else { VideoPlayerPortal(videoPlayerViewModel, navController) } diff --git a/app/src/main/java/com/acitelight/aether/view/VideoPlayerLandscape.kt b/app/src/main/java/com/acitelight/aether/view/VideoPlayerLandscape.kt index 8d2968b..05f8289 100644 --- a/app/src/main/java/com/acitelight/aether/view/VideoPlayerLandscape.kt +++ b/app/src/main/java/com/acitelight/aether/view/VideoPlayerLandscape.kt @@ -8,7 +8,9 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background 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.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size 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.material.icons.Icons 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.filled.ArrowBack -import androidx.compose.material.icons.filled.ArrowBackIosNew import androidx.compose.material.icons.filled.Brightness4 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.Lock +import androidx.compose.material.icons.filled.LockOpen import androidx.compose.material.icons.filled.Pause 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.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.sp import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.viewModelScope import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.PlayerView import com.acitelight.aether.ToggleFullScreen import com.acitelight.aether.viewModel.VideoPlayerViewModel +import kotlinx.coroutines.launch import kotlin.math.abs @androidx.annotation.OptIn(UnstableApi::class) @Composable fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { + val colorScheme = MaterialTheme.colorScheme val context = LocalContext.current 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 maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) } @@ -78,6 +90,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { } val name by videoPlayerViewModel.currentName + val id by videoPlayerViewModel.currentId fun setVolume(value: Int) { audioManager.setStreamVolume( @@ -100,25 +113,14 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { .align(Alignment.Center) ) { - AndroidView( - factory = { - PlayerView( - it - ).apply { - player = exoPlayer - useController = false - subtitleView?.let { sv -> - sv.visibility = View.GONE - } - } - }, - modifier = Modifier - .fillMaxWidth() - .pointerInput(Unit) { + Box( + Modifier + .fillMaxSize() + .pointerInput(videoPlayerViewModel) { detectDragGestures( onDragStart = { offset -> if (videoPlayerViewModel.locked) return@detectDragGestures - if(offset.y > size.height * 0.9 || offset.y < size.height * 0.1) + if (offset.y > size.height * 0.9 || offset.y < size.height * 0.1) videoPlayerViewModel.draggingPurpose = -3 // Set gesture protection for the bottom of the screen // (Prevent conflicts with system gestures, such as dropdown status bar, bottom swipe up menu) @@ -157,9 +159,9 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) .toFloat() / maxVolume.toFloat() if (dragAmount.y < 0) - setVolume(cu + 1); + setVolume(cu + 1) else if (dragAmount.y > 0) - setVolume(cu - 1); + setVolume(cu - 1) } else if (videoPlayerViewModel.draggingPurpose == 1) { moveBrit(dragAmount.y, activity, videoPlayerViewModel) } @@ -167,7 +169,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { } ) } - .pointerInput(Unit) { + .pointerInput(videoPlayerViewModel) { detectTapGestures( onDoubleTap = { if (videoPlayerViewModel.locked) return@detectTapGestures @@ -177,6 +179,10 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { }, onTap = { if (videoPlayerViewModel.locked) return@detectTapGestures + if (videoPlayerViewModel.showPlaylist) { + videoPlayerViewModel.showPlaylist = false + return@detectTapGestures + } 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( visible = videoPlayerViewModel.draggingPurpose == 0, @@ -213,9 +234,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { { Text( text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${ - formatTime( - (exoPlayer.duration).toLong() - ) + formatTime(exoPlayer.duration) }", fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 12.dp), @@ -234,9 +253,11 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { modifier = Modifier.align(Alignment.Center) ) { - Row(Modifier - .background(Color(0x88000000), RoundedCornerShape(18)) - .width(200.dp)) + Row( + Modifier + .background(Color(0x88000000), RoundedCornerShape(18)) + .width(200.dp) + ) { Icon( imageVector = Icons.AutoMirrored.Filled.VolumeUp, @@ -269,9 +290,11 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { modifier = Modifier.align(Alignment.Center) ) { - Row(Modifier - .background(Color(0x88000000), RoundedCornerShape(18)) - .width(200.dp)) + Row( + Modifier + .background(Color(0x88000000), RoundedCornerShape(18)) + .width(200.dp) + ) { Icon( imageVector = Icons.Default.Brightness4, @@ -375,7 +398,8 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { Text( text = name, fontWeight = FontWeight.Bold, - modifier = Modifier.padding(top = 12.dp) + modifier = Modifier + .padding(top = 12.dp) .align(Alignment.CenterVertically), fontSize = 18.sp ) @@ -407,9 +431,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { ) { Text( text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${ - formatTime( - (exoPlayer.duration).toLong() - ) + formatTime(exoPlayer.duration) }", fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 12.dp), @@ -464,6 +486,87 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { 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) + } + } + } } } } diff --git a/app/src/main/java/com/acitelight/aether/viewModel/VideoPlayerViewModel.kt b/app/src/main/java/com/acitelight/aether/viewModel/VideoPlayerViewModel.kt index e3dde4f..c48016e 100644 --- a/app/src/main/java/com/acitelight/aether/viewModel/VideoPlayerViewModel.kt +++ b/app/src/main/java/com/acitelight/aether/viewModel/VideoPlayerViewModel.kt @@ -60,6 +60,7 @@ class VideoPlayerViewModel @Inject constructor( val recentManager: RecentManager, val videoLibrary: VideoLibrary, ) : ViewModel() { + var showPlaylist by mutableStateOf(false) var isLandscape by mutableStateOf(false) var tabIndex by mutableIntStateOf(0) var isPlaying by mutableStateOf(true)