From a89f8923061c6e8b3384e628c8fee869ba5c0e0c Mon Sep 17 00:00:00 2001 From: acite <1498045907@qq.com> Date: Fri, 26 Sep 2025 12:48:34 +0800 Subject: [PATCH] [feat&optimize] Video grouping recording, large-scale reconstruction --- .../acitelight/aether/model/VideoRecord.kt | 4 +- .../aether/model/VideoRecordDatabase.kt | 2 +- .../aether/service/RecentManager.kt | 14 +- .../com/acitelight/aether/view/BiliStyle.kt | 101 ++ .../aether/view/HorizontalGallery.kt | 55 + .../acitelight/aether/view/MiniVideoCard.kt | 106 ++ .../acitelight/aether/view/PlaylistPanel.kt | 60 + .../aether/view/PortalCorePlayer.kt | 409 +++++ .../acitelight/aether/view/SubtitleOverlay.kt | 83 + .../com/acitelight/aether/view/VideoPlayer.kt | 1375 +---------------- .../aether/view/VideoPlayerLandscape.kt | 427 +++++ .../aether/view/VideoPlayerPortal.kt | 255 +++ .../com/acitelight/aether/view/VideoScreen.kt | 9 +- .../aether/viewModel/VideoPlayerViewModel.kt | 164 +- 14 files changed, 1633 insertions(+), 1431 deletions(-) create mode 100644 app/src/main/java/com/acitelight/aether/view/BiliStyle.kt create mode 100644 app/src/main/java/com/acitelight/aether/view/HorizontalGallery.kt create mode 100644 app/src/main/java/com/acitelight/aether/view/MiniVideoCard.kt create mode 100644 app/src/main/java/com/acitelight/aether/view/PlaylistPanel.kt create mode 100644 app/src/main/java/com/acitelight/aether/view/PortalCorePlayer.kt create mode 100644 app/src/main/java/com/acitelight/aether/view/SubtitleOverlay.kt create mode 100644 app/src/main/java/com/acitelight/aether/view/VideoPlayerLandscape.kt create mode 100644 app/src/main/java/com/acitelight/aether/view/VideoPlayerPortal.kt diff --git a/app/src/main/java/com/acitelight/aether/model/VideoRecord.kt b/app/src/main/java/com/acitelight/aether/model/VideoRecord.kt index bfafc8b..b917c25 100644 --- a/app/src/main/java/com/acitelight/aether/model/VideoRecord.kt +++ b/app/src/main/java/com/acitelight/aether/model/VideoRecord.kt @@ -8,5 +8,7 @@ import androidx.room.PrimaryKey data class VideoRecord ( @PrimaryKey(autoGenerate = false) val id: String = "", @ColumnInfo(name = "name") val klass: String = "", - @ColumnInfo(name = "position") val position: Long + @ColumnInfo(name = "position") val position: Long, + @ColumnInfo(name = "time") val time: Long, + @ColumnInfo(name = "group") val group: String ) \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/model/VideoRecordDatabase.kt b/app/src/main/java/com/acitelight/aether/model/VideoRecordDatabase.kt index 59c6329..fd34c3a 100644 --- a/app/src/main/java/com/acitelight/aether/model/VideoRecordDatabase.kt +++ b/app/src/main/java/com/acitelight/aether/model/VideoRecordDatabase.kt @@ -18,7 +18,7 @@ abstract class VideoRecordDatabase : RoomDatabase() { val instance = Room.databaseBuilder( context.applicationContext, VideoRecordDatabase::class.java, - "videorecord_database" + "videorecords_database" ).build() INSTANCE = instance instance diff --git a/app/src/main/java/com/acitelight/aether/service/RecentManager.kt b/app/src/main/java/com/acitelight/aether/service/RecentManager.kt index 6020bb8..120c9a2 100644 --- a/app/src/main/java/com/acitelight/aether/service/RecentManager.kt +++ b/app/src/main/java/com/acitelight/aether/service/RecentManager.kt @@ -29,9 +29,9 @@ class RecentManager @Inject constructor( val file = File(context.filesDir, filename) val content = file.readText() content - } catch (e: FileNotFoundException) { + } catch (_: FileNotFoundException) { "[]" - } catch (e: IOException) { + } catch (_: IOException) { "[]" } } @@ -69,12 +69,12 @@ class RecentManager @Inject constructor( if (c != null) recentComic.add(recentComic.size, c) } } - } catch (e: NoSuchMethodError) { + } catch (_: NoSuchMethodError) { for (id in ids) { val c = mediaManager.queryComicInfoSingle(id) if (c != null) recentComic.add(recentComic.size, c) } - } catch (e: Exception) { + } catch (_: Exception) { for (id in ids) { val c = mediaManager.queryComicInfoSingle(id) if (c != null) recentComic.add(recentComic.size, c) @@ -93,9 +93,6 @@ class RecentManager @Inject constructor( suspend fun pushComic(context: Context, comicId: String) { mutex.withLock { - val c = readFile(context, "recent_comic.json") - - val o = recentComic.map { it.id }.toMutableList() @@ -152,14 +149,11 @@ class RecentManager @Inject constructor( suspend fun pushVideo(context: Context, video: VideoQueryIndex) { mutex.withLock{ - val content = readFile(context, "recent.json") val o = recentVideo.map{ VideoQueryIndex(it.klass, it.id) }.toMutableList() if(o.contains(video)) { val index = o.indexOf(video) - val temp = recentVideo[index] - recentVideo.removeAt(index) } recentVideo.add(0, mediaManager.queryVideo(video.klass, video.id)!!) diff --git a/app/src/main/java/com/acitelight/aether/view/BiliStyle.kt b/app/src/main/java/com/acitelight/aether/view/BiliStyle.kt new file mode 100644 index 0000000..0a4df45 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/view/BiliStyle.kt @@ -0,0 +1,101 @@ +package com.acitelight.aether.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BiliStyleSlider( + modifier: Modifier = Modifier, + value: Float, + onValueChange: (Float) -> Unit, + valueRange: ClosedFloatingPointRange = 0f..1f +) { + val colorScheme = MaterialTheme.colorScheme + val trackHeight = 3.dp + + Slider( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + modifier = modifier, + colors = SliderDefaults.colors( + thumbColor = Color(0xFFFFFFFF), + activeTrackColor = colorScheme.primary, + inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f) + ), + + track = { sliderPositions -> + Box( + Modifier + .height(trackHeight) + .fillMaxWidth() + .background(Color.LightGray.copy(alpha = 0.3f), RoundedCornerShape(50)) + ) { + Box( + Modifier + .align(Alignment.CenterStart) + .fillMaxWidth(value) + .fillMaxHeight() + .background(colorScheme.primary, RoundedCornerShape(50)) + ) + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BiliMiniSlider( + modifier: Modifier = Modifier, + value: Float, + onValueChange: (Float) -> Unit, + valueRange: ClosedFloatingPointRange = 0f..1f +) { + val colorScheme = MaterialTheme.colorScheme + val trackHeight = 3.dp + + Slider( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + modifier = modifier, + colors = SliderDefaults.colors( + thumbColor = Color(0xFFFFFFFF), + activeTrackColor = colorScheme.primary, + inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f) + ), + thumb = { + + }, + track = { sliderPositions -> + Box( + Modifier + .height(trackHeight) + .fillMaxWidth() + .background(Color.LightGray.copy(alpha = 0.3f), RoundedCornerShape(50)) + ) { + Box( + Modifier + .align(Alignment.CenterStart) + .fillMaxWidth(value) + .fillMaxHeight() + .background(colorScheme.primary, RoundedCornerShape(50)) + ) + } + } + ) +} diff --git a/app/src/main/java/com/acitelight/aether/view/HorizontalGallery.kt b/app/src/main/java/com/acitelight/aether/view/HorizontalGallery.kt new file mode 100644 index 0000000..d00fe4f --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/view/HorizontalGallery.kt @@ -0,0 +1,55 @@ +package com.acitelight.aether.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import coil3.ImageLoader +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import com.acitelight.aether.model.KeyImage +import com.acitelight.aether.viewModel.VideoPlayerViewModel + + +@Composable +fun HorizontalGallery(videoPlayerViewModel: VideoPlayerViewModel) { + val gallery by videoPlayerViewModel.currentGallery + LazyRow( + modifier = Modifier + .fillMaxWidth() + .height(120.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(horizontal = 24.dp) + ) { + items(gallery) { it -> + SingleImageItem(img = it, videoPlayerViewModel.imageLoader!!) + } + } +} + +@Composable +private fun SingleImageItem(img: KeyImage, imageLoader: ImageLoader) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(img.url) + .memoryCacheKey(img.key) + .diskCacheKey(img.key) + .build(), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)), + contentScale = ContentScale.Crop, + imageLoader = imageLoader + ) +} diff --git a/app/src/main/java/com/acitelight/aether/view/MiniVideoCard.kt b/app/src/main/java/com/acitelight/aether/view/MiniVideoCard.kt new file mode 100644 index 0000000..b44bdf3 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/view/MiniVideoCard.kt @@ -0,0 +1,106 @@ +package com.acitelight.aether.view + +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.graphics.Color +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 MiniVideoCard(modifier: Modifier, video: Video, onClick: () -> Unit, imageLoader: ImageLoader) { + 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)), + 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 = 14.sp, + maxLines = 2, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier.weight(1f)) + + Text( + modifier = Modifier.height(16.dp), + text = video.klass, + fontSize = 8.sp, + maxLines = 1, + fontWeight = FontWeight.Bold, + ) + + Text( + modifier = Modifier.height(16.dp), + text = formatTime(video.video.duration), + fontSize = 8.sp, + maxLines = 1, + fontWeight = FontWeight.Bold, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/view/PlaylistPanel.kt b/app/src/main/java/com/acitelight/aether/view/PlaylistPanel.kt new file mode 100644 index 0000000..861d30b --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/view/PlaylistPanel.kt @@ -0,0 +1,60 @@ +package com.acitelight.aether.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +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.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewModelScope +import com.acitelight.aether.viewModel.VideoPlayerViewModel +import kotlinx.coroutines.launch + + +@Composable +fun PlaylistPanel(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel) +{ + val name by videoPlayerViewModel.currentName + + LazyRow( + modifier = modifier + .fillMaxWidth() + .height(80.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + contentPadding = PaddingValues(horizontal = 24.dp) + ) { + items(videoPlayerViewModel.videos) { it -> + // SingleImageItem(img = it, videoPlayerViewModel.imageLoader!!) + Card( + modifier = Modifier.fillMaxHeight().width(140.dp), + onClick = { + if(name == it.video.name) + return@Card + + videoPlayerViewModel.viewModelScope.launch { + videoPlayerViewModel.startPlay(it) + } + } + ) { + Box(Modifier.padding(8.dp).fillMaxSize()) + { + Text(modifier = Modifier.align(Alignment.Center), text = it.video.name, maxLines = 4, fontWeight = FontWeight.Bold, fontSize = 12.sp, lineHeight = 13.sp) + } + } + } + } +} diff --git a/app/src/main/java/com/acitelight/aether/view/PortalCorePlayer.kt b/app/src/main/java/com/acitelight/aether/view/PortalCorePlayer.kt new file mode 100644 index 0000000..1df2786 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/view/PortalCorePlayer.kt @@ -0,0 +1,409 @@ +package com.acitelight.aether.view + +import android.app.Activity +import android.content.Context +import android.media.AudioManager +import android.view.View +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.VolumeUp +import androidx.compose.material.icons.filled.Brightness4 +import androidx.compose.material.icons.filled.FastForward +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +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 +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +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 androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.PlayerView +import com.acitelight.aether.viewModel.VideoPlayerViewModel +import kotlin.math.abs + + +@androidx.annotation.OptIn(UnstableApi::class) +@Composable +fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel, cover: Float) { + val exoPlayer: ExoPlayer = videoPlayerViewModel.player!! + val context = LocalContext.current + val activity = (context as? Activity)!! + + val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager } + val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) } + var volFactor by remember { + mutableFloatStateOf( + audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() / maxVolume.toFloat() + ) + } + + fun setVolume(value: Int) { + audioManager.setStreamVolume( + AudioManager.STREAM_MUSIC, + value.coerceIn(0, maxVolume), + AudioManager.FLAG_PLAY_SOUND + ) + } + + Box(modifier) + { + AndroidView( + factory = { + PlayerView( + it + ).apply { + player = exoPlayer + useController = false + subtitleView?.let { sv -> + sv.visibility = View.GONE + } + } + }, + modifier = Modifier + .align(Alignment.TopCenter) + .fillMaxWidth() + .pointerInput(Unit) { + detectDragGestures( + onDragStart = { offset -> + if (videoPlayerViewModel.locked) return@detectDragGestures + if (offset.x < size.width / 2) { + videoPlayerViewModel.draggingPurpose = -1 + } else { + videoPlayerViewModel.draggingPurpose = -2 + } + }, + onDragEnd = { + if (videoPlayerViewModel.isPlaying && videoPlayerViewModel.draggingPurpose == 0) + exoPlayer.play() + + videoPlayerViewModel.draggingPurpose = -1 + }, + onDrag = { change, dragAmount -> + if (videoPlayerViewModel.locked) return@detectDragGestures + if (abs(dragAmount.x) > abs(dragAmount.y) && + (videoPlayerViewModel.draggingPurpose == -1 || videoPlayerViewModel.draggingPurpose == -2) + ) { + videoPlayerViewModel.draggingPurpose = 0 + videoPlayerViewModel.planeVisibility = true + exoPlayer.pause() + } else if (videoPlayerViewModel.draggingPurpose == -1) videoPlayerViewModel.draggingPurpose = + 1 + else if (videoPlayerViewModel.draggingPurpose == -2) videoPlayerViewModel.draggingPurpose = + 2 + + if (videoPlayerViewModel.draggingPurpose == 0) { + exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong()) + videoPlayerViewModel.playProcess = + exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat() + } else if (videoPlayerViewModel.draggingPurpose == 2) { + val cu = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + volFactor = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + .toFloat() / maxVolume.toFloat() + if (dragAmount.y < 0) + setVolume(cu + 1) + else if (dragAmount.y > 0) + setVolume(cu - 1) + } else if (videoPlayerViewModel.draggingPurpose == 1) { + moveBrit(dragAmount.y, activity, videoPlayerViewModel) + } + + } + ) + } + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { + if (videoPlayerViewModel.locked) return@detectTapGestures + videoPlayerViewModel.isPlaying = !videoPlayerViewModel.isPlaying + if (videoPlayerViewModel.isPlaying) exoPlayer.play() else exoPlayer.pause() + }, + onTap = { + if (videoPlayerViewModel.locked) return@detectTapGestures + videoPlayerViewModel.planeVisibility = + !videoPlayerViewModel.planeVisibility + }, + onLongPress = { + if (videoPlayerViewModel.locked) return@detectTapGestures + videoPlayerViewModel.isLongPressing = true + exoPlayer.playbackParameters = exoPlayer.playbackParameters + .withSpeed(3.0f) + }, + onPress = { offset -> + val pressResult = tryAwaitRelease() + if (pressResult && videoPlayerViewModel.isLongPressing) { + videoPlayerViewModel.isLongPressing = false + exoPlayer.playbackParameters = exoPlayer.playbackParameters + .withSpeed(1.0f) + } + }, + ) + } + ) + + androidx.compose.animation.AnimatedVisibility( + visible = videoPlayerViewModel.isLongPressing, + enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }), + exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight }), + modifier = Modifier + .align(Alignment.TopCenter) + ) + { + Box( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 24.dp) + .background(Color(0x44000000), RoundedCornerShape(18)) + ) + { + Row { + Icon( + imageVector = Icons.Filled.FastForward, + contentDescription = "Fast Forward", + tint = Color.White, + modifier = Modifier + .size(36.dp) + .padding(4.dp) + .align(Alignment.CenterVertically) + ) + + Text( + text = "3X Speed...", + modifier = Modifier + .padding(4.dp) + .align(Alignment.CenterVertically), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFFFFFFFF) + ) + } + } + } + + androidx.compose.animation.AnimatedVisibility( + visible = videoPlayerViewModel.draggingPurpose == 0, + enter = fadeIn( + initialAlpha = 0f, + ), + exit = fadeOut( + targetAlpha = 0f + ), + modifier = Modifier.align(Alignment.Center) + ) + { + Text( + text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${ + formatTime( + (exoPlayer.duration) + ) + }", + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 12.dp), + fontSize = 18.sp + ) + } + + androidx.compose.animation.AnimatedVisibility( + visible = videoPlayerViewModel.draggingPurpose == 2, + enter = fadeIn( + initialAlpha = 0f, + ), + exit = fadeOut( + targetAlpha = 0f + ), + modifier = Modifier.align(Alignment.Center) + ) + { + Row(Modifier + .background(Color(0x88000000), RoundedCornerShape(18)) + .width(200.dp)) + { + Icon( + imageVector = Icons.AutoMirrored.Filled.VolumeUp, + contentDescription = "Vol", + tint = Color.White, + modifier = Modifier + .size(48.dp) + .padding(8.dp) + .align(Alignment.CenterVertically) + ) + BiliMiniSlider( + value = volFactor, + onValueChange = {}, + modifier = Modifier + .height(4.dp) + .padding(horizontal = 8.dp) + .align(Alignment.CenterVertically) + ) + } + } + + androidx.compose.animation.AnimatedVisibility( + visible = videoPlayerViewModel.draggingPurpose == 1, + enter = fadeIn( + initialAlpha = 0f, + ), + exit = fadeOut( + targetAlpha = 0f + ), + modifier = Modifier.align(Alignment.Center) + ) + { + Row(Modifier + .background(Color(0x88000000), RoundedCornerShape(18)) + .width(200.dp)) + { + Icon( + imageVector = Icons.Default.Brightness4, + contentDescription = "Brightness", + tint = Color.White, + modifier = Modifier + .size(48.dp) + .padding(8.dp) + .align(Alignment.CenterVertically) + ) + BiliMiniSlider( + value = videoPlayerViewModel.brit, + onValueChange = {}, + modifier = Modifier + .height(4.dp) + .padding(horizontal = 8.dp) + .align(Alignment.CenterVertically) + ) + } + } + + if (cover > 0.0f) + Spacer(Modifier + .background(MaterialTheme.colorScheme.primary.copy(cover)) + .fillMaxSize()) + + androidx.compose.animation.AnimatedVisibility( + visible = (!videoPlayerViewModel.planeVisibility) || videoPlayerViewModel.locked, + enter = fadeIn( + initialAlpha = 0f, + ), + exit = fadeOut( + targetAlpha = 0f + ), + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + ) { + BiliMiniSlider( + value = videoPlayerViewModel.playProcess, + onValueChange = {}, + modifier = Modifier + .height(4.dp) + .align(Alignment.BottomCenter) + ) + } + + androidx.compose.animation.AnimatedVisibility( + visible = videoPlayerViewModel.planeVisibility && (!videoPlayerViewModel.locked), + enter = fadeIn( + initialAlpha = 0f, + ), + exit = fadeOut( + targetAlpha = 0f + ), + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .height(42.dp) + ) + { + Row( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + Color.Black.copy(alpha = 0.4f), + ) + ) + ), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + IconButton( + onClick = { + videoPlayerViewModel.isPlaying = !videoPlayerViewModel.isPlaying + if (videoPlayerViewModel.isPlaying) exoPlayer.play() else exoPlayer.pause() + }, + Modifier + .size(36.dp) + .align(Alignment.CenterVertically) + ) { + Icon( + imageVector = if (videoPlayerViewModel.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, + contentDescription = "Play/Pause", + tint = Color.White, + modifier = Modifier.size(32.dp) + ) + } + + BiliStyleSlider( + value = videoPlayerViewModel.playProcess, + onValueChange = { value -> + exoPlayer.seekTo((exoPlayer.duration * value).toLong()) + }, + modifier = Modifier + .height(8.dp) + .align(Alignment.CenterVertically) + .weight(1f) + ) + + Text( + text = formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong()), + maxLines = 1, + fontSize = 12.sp, + color = Color(0xFFFFFFFF), + fontWeight = FontWeight.Bold, + modifier = Modifier + .width(80.dp) + .align(Alignment.CenterVertically) + .padding(start = 12.dp) + ) + } + } + + SubtitleOverlay( + cues = videoPlayerViewModel.cues, + modifier = Modifier.matchParentSize() + ) + } +} + diff --git a/app/src/main/java/com/acitelight/aether/view/SubtitleOverlay.kt b/app/src/main/java/com/acitelight/aether/view/SubtitleOverlay.kt new file mode 100644 index 0000000..5bf9d53 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/view/SubtitleOverlay.kt @@ -0,0 +1,83 @@ +package com.acitelight.aether.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +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.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.media3.common.text.Cue + +@Composable +fun SubtitleOverlay( + cues: List, + modifier: Modifier = Modifier, + maxLines: Int = 2, + textSize: TextUnit = 14.sp, + backgroundAlpha: Float = 0.6f, + horizontalMargin: Dp = 16.dp, + bottomMargin: Dp = 14.dp, + contentPadding: Dp = 6.dp, + cornerRadius: Dp = 6.dp, + textColor: Color = Color.White +) { + val raw = if (cues.isEmpty()) "" else cues.joinToString(separator = "\n") { + it.text?.toString() ?: "" + }.trim() + if (raw.isEmpty()) return + + val textAlign = when (cues.firstOrNull()?.textAlignment) { + android.text.Layout.Alignment.ALIGN_CENTER -> TextAlign.Center + android.text.Layout.Alignment.ALIGN_OPPOSITE -> TextAlign.End + android.text.Layout.Alignment.ALIGN_NORMAL -> TextAlign.Start + else -> TextAlign.Center + } + + val blurPx = with(LocalDensity.current) { (2.dp).toPx() } + + Box( + modifier = modifier, + contentAlignment = Alignment.BottomCenter + ) { + Box( + modifier = Modifier + .padding(start = horizontalMargin, end = horizontalMargin, bottom = bottomMargin) + .wrapContentWidth(Alignment.CenterHorizontally) + .clip(RoundedCornerShape(cornerRadius)) + .background(Color.Black.copy(alpha = backgroundAlpha)) + .padding(horizontal = 12.dp, vertical = contentPadding) + ) { + Text( + text = raw, + maxLines = maxLines, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + color = textColor, + fontSize = textSize, + shadow = Shadow( + color = Color.Black.copy(alpha = 0.85f), + offset = Offset(0f, 0f), + blurRadius = blurPx + ) + ), + textAlign = textAlign, + modifier = Modifier + ) + } + } +} \ 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 5c056cc..5893b80 100644 --- a/app/src/main/java/com/acitelight/aether/view/VideoPlayer.kt +++ b/app/src/main/java/com/acitelight/aether/view/VideoPlayer.kt @@ -110,6 +110,9 @@ import com.acitelight.aether.model.KeyImage import com.acitelight.aether.model.Video import kotlinx.coroutines.launch import kotlin.math.abs +import kotlin.math.exp +import kotlin.math.ln +import kotlin.math.pow fun formatTime(ms: Long): String { if (ms <= 0) return "00:00:00" @@ -120,6 +123,18 @@ fun formatTime(ms: Long): String { return String.format("%02d:%02d:%02d", hours, minutes, seconds) } +fun moveBrit(db: Float, activity: Activity, videoPlayerViewModel: VideoPlayerViewModel) { + val attr = activity.window.attributes + + val britUi = (videoPlayerViewModel.brit - db * 0.002f).coerceIn(0f, 1f) + videoPlayerViewModel.brit = britUi + + val gamma = 2.2f + val britSystem = britUi.pow(gamma).coerceIn(0.001f, 1f) + + attr.screenBrightness = britSystem + activity.window.attributes = attr +} @Composable fun isLandscape(): Boolean { @@ -127,150 +142,6 @@ fun isLandscape(): Boolean { return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun BiliStyleSlider( - modifier: Modifier = Modifier, - value: Float, - onValueChange: (Float) -> Unit, - valueRange: ClosedFloatingPointRange = 0f..1f -) { - val colorScheme = MaterialTheme.colorScheme - val trackHeight = 3.dp - - Slider( - value = value, - onValueChange = onValueChange, - valueRange = valueRange, - modifier = modifier, - colors = SliderDefaults.colors( - thumbColor = Color(0xFFFFFFFF), - activeTrackColor = colorScheme.primary, - inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f) - ), - - track = { sliderPositions -> - Box( - Modifier - .height(trackHeight) - .fillMaxWidth() - .background(Color.LightGray.copy(alpha = 0.3f), RoundedCornerShape(50)) - ) { - Box( - Modifier - .align(Alignment.CenterStart) - .fillMaxWidth(value) - .fillMaxHeight() - .background(colorScheme.primary, RoundedCornerShape(50)) - ) - } - } - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun BiliMiniSlider( - modifier: Modifier = Modifier, - value: Float, - onValueChange: (Float) -> Unit, - valueRange: ClosedFloatingPointRange = 0f..1f -) { - val colorScheme = MaterialTheme.colorScheme - val trackHeight = 3.dp - - Slider( - value = value, - onValueChange = onValueChange, - valueRange = valueRange, - modifier = modifier, - colors = SliderDefaults.colors( - thumbColor = Color(0xFFFFFFFF), - activeTrackColor = colorScheme.primary, - inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f) - ), - thumb = { - - }, - track = { sliderPositions -> - Box( - Modifier - .height(trackHeight) - .fillMaxWidth() - .background(Color.LightGray.copy(alpha = 0.3f), RoundedCornerShape(50)) - ) { - Box( - Modifier - .align(Alignment.CenterStart) - .fillMaxWidth(value) - .fillMaxHeight() - .background(colorScheme.primary, RoundedCornerShape(50)) - ) - } - } - ) -} - -@Composable -fun SubtitleOverlay( - cues: List, - modifier: Modifier = Modifier, - maxLines: Int = 2, - textSize: TextUnit = 14.sp, - backgroundAlpha: Float = 0.6f, - horizontalMargin: Dp = 16.dp, - bottomMargin: Dp = 14.dp, - contentPadding: Dp = 6.dp, - cornerRadius: Dp = 6.dp, - textColor: Color = Color.White -) { - val raw = if (cues.isEmpty()) "" else cues.joinToString(separator = "\n") { - it.text?.toString() ?: "" - }.trim() - if (raw.isEmpty()) return - - val textAlign = when (cues.firstOrNull()?.textAlignment) { - android.text.Layout.Alignment.ALIGN_CENTER -> TextAlign.Center - android.text.Layout.Alignment.ALIGN_OPPOSITE -> TextAlign.End - android.text.Layout.Alignment.ALIGN_NORMAL -> TextAlign.Start - else -> TextAlign.Center - } - - val blurPx = with(LocalDensity.current) { (2.dp).toPx() } - - Box( - modifier = modifier, - contentAlignment = Alignment.BottomCenter - ) { - Box( - modifier = Modifier - .padding(start = horizontalMargin, end = horizontalMargin, bottom = bottomMargin) - .wrapContentWidth(Alignment.CenterHorizontally) - .clip(RoundedCornerShape(cornerRadius)) - .background(Color.Black.copy(alpha = backgroundAlpha)) - .padding(horizontal = 12.dp, vertical = contentPadding) - ) { - Text( - text = raw, - maxLines = maxLines, - overflow = TextOverflow.Ellipsis, - style = TextStyle( - color = textColor, - fontSize = textSize, - shadow = Shadow( - color = Color.Black.copy(alpha = 0.85f), - offset = Offset(0f, 0f), - blurRadius = blurPx - ) - ), - textAlign = textAlign, - modifier = Modifier - ) - } - } -} - - @Composable fun VideoPlayer( videoPlayerViewModel: VideoPlayerViewModel = hiltViewModel(), @@ -320,1219 +191,3 @@ fun VideoPlayer( } } } - -@androidx.annotation.OptIn(UnstableApi::class) -@Composable -fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel, cover: Float) { - val exoPlayer: ExoPlayer = videoPlayerViewModel.player!! - val context = LocalContext.current - val activity = context as? Activity - - val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager } - val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) } - var volFactor by remember { - mutableFloatStateOf( - audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() / maxVolume.toFloat() - ) - } - - fun setVolume(value: Int) { - audioManager.setStreamVolume( - AudioManager.STREAM_MUSIC, - value.coerceIn(0, maxVolume), - AudioManager.FLAG_PLAY_SOUND - ) - } - - Box(modifier) - { - AndroidView( - factory = { - PlayerView( - it - ).apply { - player = exoPlayer - useController = false - subtitleView?.let { sv -> - sv.visibility = View.GONE - } - } - }, - modifier = Modifier - .align(Alignment.TopCenter) - .fillMaxWidth() - .pointerInput(Unit) { - detectDragGestures( - onDragStart = { offset -> - if (videoPlayerViewModel.locked) return@detectDragGestures - if (offset.x < size.width / 2) { - videoPlayerViewModel.draggingPurpose = -1 - } else { - videoPlayerViewModel.draggingPurpose = -2 - } - }, - onDragEnd = { - if (videoPlayerViewModel.isPlaying && videoPlayerViewModel.draggingPurpose == 0) - exoPlayer.play() - - videoPlayerViewModel.draggingPurpose = -1 - }, - onDrag = { change, dragAmount -> - if (videoPlayerViewModel.locked) return@detectDragGestures - if (abs(dragAmount.x) > abs(dragAmount.y) && - (videoPlayerViewModel.draggingPurpose == -1 || videoPlayerViewModel.draggingPurpose == -2) - ) { - videoPlayerViewModel.draggingPurpose = 0 - videoPlayerViewModel.planeVisibility = true - exoPlayer.pause() - } else if (videoPlayerViewModel.draggingPurpose == -1) videoPlayerViewModel.draggingPurpose = - 1 - else if (videoPlayerViewModel.draggingPurpose == -2) videoPlayerViewModel.draggingPurpose = - 2 - - if (videoPlayerViewModel.draggingPurpose == 0) { - exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong()) - videoPlayerViewModel.playProcess = - exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat() - } else if (videoPlayerViewModel.draggingPurpose == 2) { - val cu = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) - volFactor = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) - .toFloat() / maxVolume.toFloat() - if (dragAmount.y < 0) - setVolume(cu + 1) - else if (dragAmount.y > 0) - setVolume(cu - 1) - } else if (videoPlayerViewModel.draggingPurpose == 1) { - videoPlayerViewModel.brit = - (videoPlayerViewModel.brit - dragAmount.y * 0.002f).coerceIn( - 0f, - 1f - ) - - activity?.window?.attributes = activity.window.attributes.apply { - screenBrightness = videoPlayerViewModel.brit.coerceIn(0f, 1f) - } - activity?.window?.setAttributes(activity.window.attributes) - } - - } - ) - } - .pointerInput(Unit) { - detectTapGestures( - onDoubleTap = { - if (videoPlayerViewModel.locked) return@detectTapGestures - videoPlayerViewModel.isPlaying = !videoPlayerViewModel.isPlaying - if (videoPlayerViewModel.isPlaying) exoPlayer.play() else exoPlayer.pause() - }, - onTap = { - if (videoPlayerViewModel.locked) return@detectTapGestures - videoPlayerViewModel.planeVisibility = - !videoPlayerViewModel.planeVisibility - }, - onLongPress = { - if (videoPlayerViewModel.locked) return@detectTapGestures - videoPlayerViewModel.isLongPressing = true - exoPlayer.playbackParameters = exoPlayer.playbackParameters - .withSpeed(3.0f) - }, - onPress = { offset -> - val pressResult = tryAwaitRelease() - if (pressResult && videoPlayerViewModel.isLongPressing) { - videoPlayerViewModel.isLongPressing = false - exoPlayer.playbackParameters = exoPlayer.playbackParameters - .withSpeed(1.0f) - } - }, - ) - } - ) - - androidx.compose.animation.AnimatedVisibility( - visible = videoPlayerViewModel.isLongPressing, - enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }), - exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight }), - modifier = Modifier - .align(Alignment.TopCenter) - ) - { - Box( - modifier = Modifier - .align(Alignment.TopCenter) - .padding(top = 24.dp) - .background(Color(0x44000000), RoundedCornerShape(18)) - ) - { - Row { - Icon( - imageVector = Icons.Filled.FastForward, - contentDescription = "Fast Forward", - tint = Color.White, - modifier = Modifier - .size(36.dp) - .padding(4.dp) - .align(Alignment.CenterVertically) - ) - - Text( - text = "3X Speed...", - modifier = Modifier - .padding(4.dp) - .align(Alignment.CenterVertically), - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = Color(0xFFFFFFFF) - ) - } - } - } - - androidx.compose.animation.AnimatedVisibility( - visible = videoPlayerViewModel.draggingPurpose == 0, - enter = fadeIn( - initialAlpha = 0f, - ), - exit = fadeOut( - targetAlpha = 0f - ), - modifier = Modifier.align(Alignment.Center) - ) - { - Text( - text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${ - formatTime( - (exoPlayer.duration) - ) - }", - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(bottom = 12.dp), - fontSize = 18.sp - ) - } - - androidx.compose.animation.AnimatedVisibility( - visible = videoPlayerViewModel.draggingPurpose == 2, - enter = fadeIn( - initialAlpha = 0f, - ), - exit = fadeOut( - targetAlpha = 0f - ), - modifier = Modifier.align(Alignment.Center) - ) - { - Row(Modifier - .background(Color(0x88000000), RoundedCornerShape(18)) - .width(200.dp)) - { - Icon( - imageVector = Icons.AutoMirrored.Filled.VolumeUp, - contentDescription = "Vol", - tint = Color.White, - modifier = Modifier - .size(48.dp) - .padding(8.dp) - .align(Alignment.CenterVertically) - ) - BiliMiniSlider( - value = volFactor, - onValueChange = {}, - modifier = Modifier - .height(4.dp) - .padding(horizontal = 8.dp) - .align(Alignment.CenterVertically) - ) - } - } - - androidx.compose.animation.AnimatedVisibility( - visible = videoPlayerViewModel.draggingPurpose == 1, - enter = fadeIn( - initialAlpha = 0f, - ), - exit = fadeOut( - targetAlpha = 0f - ), - modifier = Modifier.align(Alignment.Center) - ) - { - Row(Modifier - .background(Color(0x88000000), RoundedCornerShape(18)) - .width(200.dp)) - { - Icon( - imageVector = Icons.Default.Brightness4, - contentDescription = "Brightness", - tint = Color.White, - modifier = Modifier - .size(48.dp) - .padding(8.dp) - .align(Alignment.CenterVertically) - ) - BiliMiniSlider( - value = videoPlayerViewModel.brit, - onValueChange = {}, - modifier = Modifier - .height(4.dp) - .padding(horizontal = 8.dp) - .align(Alignment.CenterVertically) - ) - } - } - - if (cover > 0.0f) - Spacer(Modifier - .background(MaterialTheme.colorScheme.primary.copy(cover)) - .fillMaxSize()) - - androidx.compose.animation.AnimatedVisibility( - visible = (!videoPlayerViewModel.planeVisibility) || videoPlayerViewModel.locked, - enter = fadeIn( - initialAlpha = 0f, - ), - exit = fadeOut( - targetAlpha = 0f - ), - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - ) { - BiliMiniSlider( - value = videoPlayerViewModel.playProcess, - onValueChange = {}, - modifier = Modifier - .height(4.dp) - .align(Alignment.BottomCenter) - ) - } - - androidx.compose.animation.AnimatedVisibility( - visible = videoPlayerViewModel.planeVisibility && (!videoPlayerViewModel.locked), - enter = fadeIn( - initialAlpha = 0f, - ), - exit = fadeOut( - targetAlpha = 0f - ), - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .height(42.dp) - ) - { - Row( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - Color.Black.copy(alpha = 0.4f), - ) - ) - ), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - IconButton( - onClick = { - videoPlayerViewModel.isPlaying = !videoPlayerViewModel.isPlaying - if (videoPlayerViewModel.isPlaying) exoPlayer.play() else exoPlayer.pause() - }, - Modifier - .size(36.dp) - .align(Alignment.CenterVertically) - ) { - Icon( - imageVector = if (videoPlayerViewModel.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, - contentDescription = "Play/Pause", - tint = Color.White, - modifier = Modifier.size(32.dp) - ) - } - - BiliStyleSlider( - value = videoPlayerViewModel.playProcess, - onValueChange = { value -> - exoPlayer.seekTo((exoPlayer.duration * value).toLong()) - }, - modifier = Modifier - .height(8.dp) - .align(Alignment.CenterVertically) - .weight(1f) - ) - - Text( - text = formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong()), - maxLines = 1, - fontSize = 12.sp, - color = Color(0xFFFFFFFF), - fontWeight = FontWeight.Bold, - modifier = Modifier - .width(80.dp) - .align(Alignment.CenterVertically) - .padding(start = 12.dp) - ) - - /* IconButton( - onClick = { - activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - }, - Modifier - .size(36.dp) - .align(Alignment.CenterVertically) - ) { - Icon( - imageVector = Icons.Default.Fullscreen, - contentDescription = "Fullscreen", - tint = Color.White, - modifier = Modifier.size(32.dp) - ) - } */ - } - } - - SubtitleOverlay( - cues = videoPlayerViewModel.cues, - modifier = Modifier.matchParentSize() - ) - } -} - -@Composable -fun VideoPlayerPortal( - videoPlayerViewModel: VideoPlayerViewModel, - navController: NavHostController -) { - val colorScheme = MaterialTheme.colorScheme - val configuration = LocalConfiguration.current - val screenHeight = configuration.screenHeightDp.dp - - val minHeight = 42.dp - var coverAlpha by remember { mutableFloatStateOf(0.0f) } - var maxHeight = remember { screenHeight * 0.65f } - var posed = remember { false } - val dens = LocalDensity.current - val listState = rememberLazyListState() - - var playerHeight by remember { mutableStateOf(screenHeight * 0.65f) } - - val nestedScrollConnection = remember { - object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val deltaY = available.y // px - val deltaDp = with(dens) { deltaY.toDp() } - - val r = if (deltaY < 0 && playerHeight > minHeight) { - val newHeight = (playerHeight + deltaDp).coerceIn(minHeight, maxHeight) - val consumedDp = newHeight - playerHeight - playerHeight = newHeight - val consumedPx = with(dens) { consumedDp.toPx() } - Offset(0f, consumedPx) - } else if (deltaY > 0 && playerHeight < maxHeight && listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0) { - val newHeight = (playerHeight + deltaDp).coerceIn(minHeight, maxHeight) - val consumedDp = newHeight - playerHeight - playerHeight = newHeight - val consumedPx = with(dens) { consumedDp.toPx() } - Offset(0f, consumedPx) - } else { - Offset.Zero - } - - val dh = playerHeight - minHeight - coverAlpha = (if (dh > 10.dp) - 0f - else - (10.dp.value - dh.value) / 10.0f) - - return r - } - } - } - - val klass by videoPlayerViewModel.currentKlass - val id by videoPlayerViewModel.currentId - val name by videoPlayerViewModel.currentName - val duration by videoPlayerViewModel.currentDuration - - ToggleFullScreen(false) - Column(Modifier - .nestedScroll(nestedScrollConnection) - .fillMaxHeight()) - { - Box { - PortalCorePlayer( - Modifier - .padding(top = 32.dp) - .heightIn(max = playerHeight) - .onGloballyPositioned { layoutCoordinates -> - if (!posed && videoPlayerViewModel.renderedFirst) { - maxHeight = with(dens) { layoutCoordinates.size.height.toDp() } - playerHeight = maxHeight - posed = true - } - }, - videoPlayerViewModel = videoPlayerViewModel, coverAlpha - ) - - androidx.compose.animation.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) - ) - } - } - } - - Row() - { - TabRow( - selectedTabIndex = videoPlayerViewModel.tabIndex, - modifier = Modifier.height(38.dp) - ) { - Tab( - selected = videoPlayerViewModel.tabIndex == 0, - onClick = { videoPlayerViewModel.tabIndex = 0 }, - text = { Text(text = "Introduction", maxLines = 1) }, - modifier = Modifier.height(38.dp) - ) - - Tab( - selected = videoPlayerViewModel.tabIndex == 1, - onClick = { videoPlayerViewModel.tabIndex = 1 }, - text = { Text(text = "Comment", maxLines = 1) }, - modifier = Modifier.height(38.dp) - ) - } - } - - LazyColumn(state = listState, modifier = Modifier.fillMaxWidth()) { - item { - Text( - modifier = Modifier - .align(Alignment.Start) - .padding(horizontal = 12.dp) - .padding(top = 12.dp), - text = name, - fontSize = 16.sp, - maxLines = 2, - fontWeight = FontWeight.Bold, - ) - - Row(Modifier - .align(Alignment.Start) - .padding(horizontal = 4.dp) - .alpha(0.5f)) { - Text( - modifier = Modifier.padding(horizontal = 8.dp), - text = klass, - fontSize = 14.sp, - maxLines = 1, - fontWeight = FontWeight.Bold, - ) - - Text( - modifier = Modifier.padding(horizontal = 8.dp), - text = formatTime(duration), - fontSize = 14.sp, - maxLines = 1, - fontWeight = FontWeight.Bold, - ) - } - - HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color) - - PlaylistPanel( - Modifier, - videoPlayerViewModel = videoPlayerViewModel - ) - - HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color) - - HorizontalGallery(videoPlayerViewModel) - HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color) - - for (i in Global.sameClassVideos ?: listOf()) { - if (i.id == id) continue - - MiniVideoCard( - modifier = Modifier - .padding(horizontal = 12.dp), - i, - { - videoPlayerViewModel.isPlaying = false - videoPlayerViewModel.player?.pause() - val route = "video_player_route/${"${i.klass}/${i.id}".toHex()}" - navController.navigate(route) - }, videoPlayerViewModel.imageLoader!! - ) - HorizontalDivider( - Modifier - .padding(vertical = 8.dp) - .alpha(0.25f), - 1.dp, - DividerDefaults.color - ) - } - } - } - } -} - -@Composable -fun SocialPanel(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel) { - val colorScheme = MaterialTheme.colorScheme - Row( - modifier, - horizontalArrangement = Arrangement.Center - ) - { - Column(modifier = Modifier.padding(horizontal = 12.dp)) { - IconButton( - onClick = { }, - modifier = Modifier - .padding(horizontal = 4.dp) - .align(Alignment.CenterHorizontally) - .size(36.dp), - ) { - Icon( - modifier = Modifier.size(28.dp), - imageVector = Icons.Filled.ThumbUp, - contentDescription = "ThumbUp", - tint = Color.Gray - ) - } - - Text( - modifier = Modifier.align(Alignment.CenterHorizontally), - text = videoPlayerViewModel.thumbUp.toString(), - fontSize = 12.sp, - maxLines = 1, - fontWeight = FontWeight.Bold - ) - } - - Column(modifier = Modifier.padding(horizontal = 12.dp)) { - IconButton( - onClick = { }, - modifier = Modifier - .padding(horizontal = 4.dp) - .align(Alignment.CenterHorizontally) - .size(36.dp), - ) { - Icon( - modifier = Modifier.size(28.dp), - imageVector = Icons.Filled.ThumbDown, - contentDescription = "ThumbDown", - tint = Color.Gray - ) - } - - Text( - modifier = Modifier.align(Alignment.CenterHorizontally), - text = videoPlayerViewModel.thumbDown.toString(), - fontSize = 12.sp, - maxLines = 1, - fontWeight = FontWeight.Bold - ) - } - - Column(modifier = Modifier.padding(horizontal = 12.dp)) { - IconButton( - onClick = { videoPlayerViewModel.star = !videoPlayerViewModel.star }, - modifier = Modifier - .padding(horizontal = 4.dp) - .align(Alignment.CenterHorizontally) - .size(36.dp), - ) { - Icon( - modifier = Modifier.size(28.dp), - imageVector = Icons.Filled.Star, - contentDescription = "Star", - tint = if (videoPlayerViewModel.star) colorScheme.primary else Color.Gray - ) - } - } - - Column(modifier = Modifier.padding(horizontal = 12.dp)) { - IconButton( - onClick = { }, - modifier = Modifier - .padding(horizontal = 4.dp) - .align(Alignment.CenterHorizontally) - .size(36.dp), - ) { - Icon( - modifier = Modifier.size(28.dp), - imageVector = Icons.Filled.Share, - contentDescription = "Forward", - tint = Color.Gray - ) - } - } - - Column(modifier = Modifier.padding(horizontal = 12.dp)) { - IconButton( - onClick = { }, - modifier = Modifier - .padding(horizontal = 4.dp) - .align(Alignment.CenterHorizontally) - .size(36.dp), - ) { - Icon( - modifier = Modifier.size(28.dp), - imageVector = Icons.Filled.Info, - contentDescription = "Detail", - tint = Color.Gray - ) - } - } - } -} - -@Composable -fun PlaylistPanel(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel) -{ - val name by videoPlayerViewModel.currentName - - LazyRow( - modifier = modifier - .fillMaxWidth() - .height(80.dp), - horizontalArrangement = Arrangement.spacedBy(6.dp), - contentPadding = PaddingValues(horizontal = 24.dp) - ) { - items(videoPlayerViewModel.videos) { it -> - // SingleImageItem(img = it, videoPlayerViewModel.imageLoader!!) - Card( - modifier = Modifier.fillMaxHeight().width(140.dp), - onClick = { - if(name == it.video.name) - return@Card - - videoPlayerViewModel.viewModelScope.launch { - videoPlayerViewModel.startPlay(it) - } - } - ) { - Box(Modifier.padding(8.dp).fillMaxSize()) - { - Text(modifier = Modifier.align(Alignment.Center), text = it.video.name, maxLines = 4, fontWeight = FontWeight.Bold, fontSize = 12.sp, lineHeight = 13.sp) - } - } - } - } -} - -@Composable -fun HorizontalGallery(videoPlayerViewModel: VideoPlayerViewModel) { - val gallery by videoPlayerViewModel.currentGallery - LazyRow( - modifier = Modifier - .fillMaxWidth() - .height(120.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(horizontal = 24.dp) - ) { - items(gallery) { it -> - SingleImageItem(img = it, videoPlayerViewModel.imageLoader!!) - } - } -} - -@Composable -fun SingleImageItem(img: KeyImage, imageLoader: ImageLoader) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(img.url) - .memoryCacheKey(img.key) - .diskCacheKey(img.key) - .build(), - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)), - contentScale = ContentScale.Crop, - imageLoader = imageLoader - ) -} - -@androidx.annotation.OptIn(UnstableApi::class) -@Composable -fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { - val context = LocalContext.current - val activity = context as? Activity - val exoPlayer: ExoPlayer = videoPlayerViewModel.player!!; - - val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager } - val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) } - var volFactor by remember { - mutableFloatStateOf( - audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() / maxVolume.toFloat() - ) - } - - val name by videoPlayerViewModel.currentName - - fun setVolume(value: Int) { - audioManager.setStreamVolume( - AudioManager.STREAM_MUSIC, - value.coerceIn(0, maxVolume), - AudioManager.FLAG_PLAY_SOUND - ) - } - - ToggleFullScreen(true) - Box(Modifier.fillMaxSize()) - { - Box( - modifier = Modifier - .background(Color.Black) - .align(Alignment.Center) - ) - { - AndroidView( - factory = { - PlayerView( - it - ).apply { - player = exoPlayer - useController = false - subtitleView?.let { sv -> - sv.visibility = View.GONE - } - } - }, - modifier = Modifier - .fillMaxWidth() - .pointerInput(Unit) { - detectDragGestures( - onDragStart = { offset -> - if (videoPlayerViewModel.locked) return@detectDragGestures - if (offset.x < size.width / 2) { - videoPlayerViewModel.draggingPurpose = -1; - } else { - videoPlayerViewModel.draggingPurpose = -2; - } - }, - onDragEnd = { - if (videoPlayerViewModel.isPlaying && videoPlayerViewModel.draggingPurpose == 0) - exoPlayer.play() - - videoPlayerViewModel.draggingPurpose = -1; - }, - onDrag = { change, dragAmount -> - if (videoPlayerViewModel.locked) return@detectDragGestures - if (abs(dragAmount.x) > abs(dragAmount.y) && - (videoPlayerViewModel.draggingPurpose == -1 || videoPlayerViewModel.draggingPurpose == -2) - ) { - videoPlayerViewModel.draggingPurpose = 0 - videoPlayerViewModel.planeVisibility = true - exoPlayer.pause() - } else if (videoPlayerViewModel.draggingPurpose == -1) videoPlayerViewModel.draggingPurpose = - 1 - else if (videoPlayerViewModel.draggingPurpose == -2) videoPlayerViewModel.draggingPurpose = - 2 - - if (videoPlayerViewModel.draggingPurpose == 0) { - exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong()) - videoPlayerViewModel.playProcess = - exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat() - } else if (videoPlayerViewModel.draggingPurpose == 2) { - val cu = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) - volFactor = - audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) - .toFloat() / maxVolume.toFloat() - if (dragAmount.y < 0) - setVolume(cu + 1); - else if (dragAmount.y > 0) - setVolume(cu - 1); - } else if (videoPlayerViewModel.draggingPurpose == 1) { - videoPlayerViewModel.brit = - (videoPlayerViewModel.brit - dragAmount.y * 0.002f).coerceIn( - 0f, - 1f - ); - - activity?.window?.attributes = - activity.window.attributes.apply { - screenBrightness = - videoPlayerViewModel.brit.coerceIn(0f, 1f) - } - activity?.window?.setAttributes(activity.window.attributes) - } - - } - ) - } - .pointerInput(Unit) { - detectTapGestures( - onDoubleTap = { - if (videoPlayerViewModel.locked) return@detectTapGestures - - videoPlayerViewModel.isPlaying = !videoPlayerViewModel.isPlaying - if (videoPlayerViewModel.isPlaying) exoPlayer.play() else exoPlayer.pause() - }, - onTap = { - if (videoPlayerViewModel.locked) return@detectTapGestures - - videoPlayerViewModel.planeVisibility = - !videoPlayerViewModel.planeVisibility - }, - onLongPress = { - if (videoPlayerViewModel.locked) return@detectTapGestures - - videoPlayerViewModel.isLongPressing = true - exoPlayer.playbackParameters = exoPlayer.playbackParameters - .withSpeed(3.0f) - }, - onPress = { offset -> - val pressResult = tryAwaitRelease() - if (pressResult && videoPlayerViewModel.isLongPressing) { - videoPlayerViewModel.isLongPressing = false - exoPlayer.playbackParameters = exoPlayer.playbackParameters - .withSpeed(1.0f) - } - }, - ) - } - ) - - androidx.compose.animation.AnimatedVisibility( - visible = videoPlayerViewModel.draggingPurpose == 0, - enter = fadeIn( - initialAlpha = 0f, - ), - exit = fadeOut( - targetAlpha = 0f - ), - modifier = Modifier.align(Alignment.Center) - ) - { - Text( - text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${ - formatTime( - (exoPlayer.duration).toLong() - ) - }", - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(bottom = 12.dp), - fontSize = 18.sp - ) - } - - androidx.compose.animation.AnimatedVisibility( - visible = videoPlayerViewModel.draggingPurpose == 2, - enter = fadeIn( - initialAlpha = 0f, - ), - exit = fadeOut( - targetAlpha = 0f - ), - modifier = Modifier.align(Alignment.Center) - ) - { - Row(Modifier - .background(Color(0x88000000), RoundedCornerShape(18)) - .width(200.dp)) - { - Icon( - imageVector = Icons.AutoMirrored.Filled.VolumeUp, - contentDescription = "Vol", - tint = Color.White, - modifier = Modifier - .size(48.dp) - .padding(8.dp) - .align(Alignment.CenterVertically) - ) - BiliMiniSlider( - value = volFactor, - onValueChange = {}, - modifier = Modifier - .height(4.dp) - .padding(horizontal = 8.dp) - .align(Alignment.CenterVertically) - ) - } - } - - androidx.compose.animation.AnimatedVisibility( - visible = videoPlayerViewModel.draggingPurpose == 1, - enter = fadeIn( - initialAlpha = 0f, - ), - exit = fadeOut( - targetAlpha = 0f - ), - modifier = Modifier.align(Alignment.Center) - ) - { - Row(Modifier - .background(Color(0x88000000), RoundedCornerShape(18)) - .width(200.dp)) - { - Icon( - imageVector = Icons.Default.Brightness4, - contentDescription = "Brightness", - tint = Color.White, - modifier = Modifier - .size(48.dp) - .padding(8.dp) - .align(Alignment.CenterVertically) - ) - BiliMiniSlider( - value = videoPlayerViewModel.brit, - onValueChange = {}, - modifier = Modifier - .height(4.dp) - .padding(horizontal = 8.dp) - .align(Alignment.CenterVertically) - ) - } - } - - AnimatedVisibility( - visible = videoPlayerViewModel.isLongPressing, - enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }), - exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight }), - modifier = Modifier - .align(Alignment.TopCenter) - ) - { - Box( - modifier = Modifier - .align(Alignment.TopCenter) - .padding(top = 24.dp) - .background(Color(0x44000000), RoundedCornerShape(18)) - ) - { - Row { - Icon( - imageVector = Icons.Filled.FastForward, - contentDescription = "Fast Forward", - tint = Color.White, - modifier = Modifier - .size(36.dp) - .padding(4.dp) - .align(Alignment.CenterVertically) - ) - - Text( - text = "3X Speed...", - modifier = Modifier - .padding(4.dp) - .align(Alignment.CenterVertically), - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - color = Color(0xFFFFFFFF) - ) - } - } - } - - AnimatedVisibility( - visible = videoPlayerViewModel.planeVisibility && (!videoPlayerViewModel.locked), - enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }), - exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight }), - modifier = Modifier - .align(Alignment.TopCenter) - .fillMaxWidth() - ) - { - Row( - Modifier - .align(Alignment.TopStart) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Black.copy(alpha = 0.4f), - Color.Transparent, - ) - ) - ) - ) - { - Text( - text = name, - fontWeight = FontWeight.Bold, - modifier = Modifier - .padding(horizontal = 46.dp).padding(top = 12.dp) - .align(Alignment.CenterVertically), - fontSize = 18.sp - ) - } - } - - AnimatedVisibility( - visible = videoPlayerViewModel.planeVisibility && (!videoPlayerViewModel.locked), - enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }), - exit = slideOutVertically(targetOffsetY = { fullHeight -> fullHeight }), - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - ) - { - Column( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - Color.Black.copy(alpha = 0.4f) - ) - ) - ) - .padding(horizontal = 36.dp) - ) { - Text( - text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${ - formatTime( - (exoPlayer.duration).toLong() - ) - }", - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(bottom = 12.dp), - fontSize = 12.sp - ) - BiliStyleSlider( - value = videoPlayerViewModel.playProcess, - onValueChange = { value -> - exoPlayer.seekTo((exoPlayer.duration * value).toLong()) - }, - modifier = Modifier - .height(16.dp) - .fillMaxWidth() - .padding(bottom = 8.dp) - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 8.dp) - .align(Alignment.Start), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - IconButton( - onClick = { - videoPlayerViewModel.isPlaying = !videoPlayerViewModel.isPlaying - if (videoPlayerViewModel.isPlaying) exoPlayer.play() else exoPlayer.pause() - }, - Modifier.size(42.dp) - ) { - Icon( - imageVector = if (videoPlayerViewModel.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, - contentDescription = "Play/Pause", - tint = Color.White, - modifier = Modifier.size(42.dp) - ) - } - } - } - } - - SubtitleOverlay( - cues = videoPlayerViewModel.cues, - modifier = Modifier.matchParentSize() - ) - } - } -} - -@Composable -fun MiniVideoCard(modifier: Modifier, video: Video, onClick: () -> Unit, imageLoader: ImageLoader) { - 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)), - 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 = 14.sp, - maxLines = 2, - fontWeight = FontWeight.Bold, - ) - - Spacer(modifier.weight(1f)) - - Text( - modifier = Modifier.height(16.dp), - text = video.klass, - fontSize = 8.sp, - maxLines = 1, - fontWeight = FontWeight.Bold, - ) - - Text( - modifier = Modifier.height(16.dp), - text = formatTime(video.video.duration), - fontSize = 8.sp, - maxLines = 1, - fontWeight = FontWeight.Bold, - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/view/VideoPlayerLandscape.kt b/app/src/main/java/com/acitelight/aether/view/VideoPlayerLandscape.kt new file mode 100644 index 0000000..8f6afdf --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/view/VideoPlayerLandscape.kt @@ -0,0 +1,427 @@ +package com.acitelight.aether.view + +import android.app.Activity +import android.content.Context +import android.media.AudioManager +import android.view.View +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +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.Row +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.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.VolumeUp +import androidx.compose.material.icons.filled.Brightness4 +import androidx.compose.material.icons.filled.FastForward +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +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 androidx.compose.ui.viewinterop.AndroidView +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 kotlin.math.abs + + +@androidx.annotation.OptIn(UnstableApi::class) +@Composable +fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) { + val context = LocalContext.current + val activity = (context as? Activity)!! + val exoPlayer: ExoPlayer = videoPlayerViewModel.player!!; + + val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager } + val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) } + var volFactor by remember { + mutableFloatStateOf( + audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() / maxVolume.toFloat() + ) + } + + val name by videoPlayerViewModel.currentName + + fun setVolume(value: Int) { + audioManager.setStreamVolume( + AudioManager.STREAM_MUSIC, + value.coerceIn(0, maxVolume), + AudioManager.FLAG_PLAY_SOUND + ) + } + + ToggleFullScreen(true) + Box(Modifier.fillMaxSize()) + { + Box( + modifier = Modifier + .background(Color.Black) + .align(Alignment.Center) + ) + { + AndroidView( + factory = { + PlayerView( + it + ).apply { + player = exoPlayer + useController = false + subtitleView?.let { sv -> + sv.visibility = View.GONE + } + } + }, + modifier = Modifier + .fillMaxWidth() + .pointerInput(Unit) { + detectDragGestures( + onDragStart = { offset -> + if (videoPlayerViewModel.locked) return@detectDragGestures + if (offset.x < size.width / 2) { + videoPlayerViewModel.draggingPurpose = -1; + } else { + videoPlayerViewModel.draggingPurpose = -2; + } + }, + onDragEnd = { + if (videoPlayerViewModel.isPlaying && videoPlayerViewModel.draggingPurpose == 0) + exoPlayer.play() + + videoPlayerViewModel.draggingPurpose = -1; + }, + onDrag = { change, dragAmount -> + if (videoPlayerViewModel.locked) return@detectDragGestures + if (abs(dragAmount.x) > abs(dragAmount.y) && + (videoPlayerViewModel.draggingPurpose == -1 || videoPlayerViewModel.draggingPurpose == -2) + ) { + videoPlayerViewModel.draggingPurpose = 0 + videoPlayerViewModel.planeVisibility = true + exoPlayer.pause() + } else if (videoPlayerViewModel.draggingPurpose == -1) videoPlayerViewModel.draggingPurpose = + 1 + else if (videoPlayerViewModel.draggingPurpose == -2) videoPlayerViewModel.draggingPurpose = + 2 + + if (videoPlayerViewModel.draggingPurpose == 0) { + exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong()) + videoPlayerViewModel.playProcess = + exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat() + } else if (videoPlayerViewModel.draggingPurpose == 2) { + val cu = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + volFactor = + audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + .toFloat() / maxVolume.toFloat() + if (dragAmount.y < 0) + setVolume(cu + 1); + else if (dragAmount.y > 0) + setVolume(cu - 1); + } else if (videoPlayerViewModel.draggingPurpose == 1) { + moveBrit(dragAmount.y, activity, videoPlayerViewModel) + } + + } + ) + } + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { + if (videoPlayerViewModel.locked) return@detectTapGestures + + videoPlayerViewModel.isPlaying = !videoPlayerViewModel.isPlaying + if (videoPlayerViewModel.isPlaying) exoPlayer.play() else exoPlayer.pause() + }, + onTap = { + if (videoPlayerViewModel.locked) return@detectTapGestures + + videoPlayerViewModel.planeVisibility = + !videoPlayerViewModel.planeVisibility + }, + onLongPress = { + if (videoPlayerViewModel.locked) return@detectTapGestures + + videoPlayerViewModel.isLongPressing = true + exoPlayer.playbackParameters = exoPlayer.playbackParameters + .withSpeed(3.0f) + }, + onPress = { offset -> + val pressResult = tryAwaitRelease() + if (pressResult && videoPlayerViewModel.isLongPressing) { + videoPlayerViewModel.isLongPressing = false + exoPlayer.playbackParameters = exoPlayer.playbackParameters + .withSpeed(1.0f) + } + }, + ) + } + ) + + androidx.compose.animation.AnimatedVisibility( + visible = videoPlayerViewModel.draggingPurpose == 0, + enter = fadeIn( + initialAlpha = 0f, + ), + exit = fadeOut( + targetAlpha = 0f + ), + modifier = Modifier.align(Alignment.Center) + ) + { + Text( + text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${ + formatTime( + (exoPlayer.duration).toLong() + ) + }", + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 12.dp), + fontSize = 18.sp + ) + } + + androidx.compose.animation.AnimatedVisibility( + visible = videoPlayerViewModel.draggingPurpose == 2, + enter = fadeIn( + initialAlpha = 0f, + ), + exit = fadeOut( + targetAlpha = 0f + ), + modifier = Modifier.align(Alignment.Center) + ) + { + Row(Modifier + .background(Color(0x88000000), RoundedCornerShape(18)) + .width(200.dp)) + { + Icon( + imageVector = Icons.AutoMirrored.Filled.VolumeUp, + contentDescription = "Vol", + tint = Color.White, + modifier = Modifier + .size(48.dp) + .padding(8.dp) + .align(Alignment.CenterVertically) + ) + BiliMiniSlider( + value = volFactor, + onValueChange = {}, + modifier = Modifier + .height(4.dp) + .padding(horizontal = 8.dp) + .align(Alignment.CenterVertically) + ) + } + } + + androidx.compose.animation.AnimatedVisibility( + visible = videoPlayerViewModel.draggingPurpose == 1, + enter = fadeIn( + initialAlpha = 0f, + ), + exit = fadeOut( + targetAlpha = 0f + ), + modifier = Modifier.align(Alignment.Center) + ) + { + Row(Modifier + .background(Color(0x88000000), RoundedCornerShape(18)) + .width(200.dp)) + { + Icon( + imageVector = Icons.Default.Brightness4, + contentDescription = "Brightness", + tint = Color.White, + modifier = Modifier + .size(48.dp) + .padding(8.dp) + .align(Alignment.CenterVertically) + ) + BiliMiniSlider( + value = videoPlayerViewModel.brit, + onValueChange = {}, + modifier = Modifier + .height(4.dp) + .padding(horizontal = 8.dp) + .align(Alignment.CenterVertically) + ) + } + } + + AnimatedVisibility( + visible = videoPlayerViewModel.isLongPressing, + enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }), + exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight }), + modifier = Modifier + .align(Alignment.TopCenter) + ) + { + Box( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 24.dp) + .background(Color(0x44000000), RoundedCornerShape(18)) + ) + { + Row { + Icon( + imageVector = Icons.Filled.FastForward, + contentDescription = "Fast Forward", + tint = Color.White, + modifier = Modifier + .size(36.dp) + .padding(4.dp) + .align(Alignment.CenterVertically) + ) + + Text( + text = "3X Speed...", + modifier = Modifier + .padding(4.dp) + .align(Alignment.CenterVertically), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFFFFFFFF) + ) + } + } + } + + AnimatedVisibility( + visible = videoPlayerViewModel.planeVisibility && (!videoPlayerViewModel.locked), + enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }), + exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight }), + modifier = Modifier + .align(Alignment.TopCenter) + .fillMaxWidth() + ) + { + Row( + Modifier + .align(Alignment.TopStart) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Black.copy(alpha = 0.4f), + Color.Transparent, + ) + ) + ) + ) + { + Text( + text = name, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(horizontal = 46.dp).padding(top = 12.dp) + .align(Alignment.CenterVertically), + fontSize = 18.sp + ) + } + } + + AnimatedVisibility( + visible = videoPlayerViewModel.planeVisibility && (!videoPlayerViewModel.locked), + enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }), + exit = slideOutVertically(targetOffsetY = { fullHeight -> fullHeight }), + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + ) + { + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + Color.Black.copy(alpha = 0.4f) + ) + ) + ) + .padding(horizontal = 36.dp) + ) { + Text( + text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${ + formatTime( + (exoPlayer.duration).toLong() + ) + }", + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 12.dp), + fontSize = 12.sp + ) + BiliStyleSlider( + value = videoPlayerViewModel.playProcess, + onValueChange = { value -> + exoPlayer.seekTo((exoPlayer.duration * value).toLong()) + }, + modifier = Modifier + .height(16.dp) + .fillMaxWidth() + .padding(bottom = 8.dp) + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + .align(Alignment.Start), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + IconButton( + onClick = { + videoPlayerViewModel.isPlaying = !videoPlayerViewModel.isPlaying + if (videoPlayerViewModel.isPlaying) exoPlayer.play() else exoPlayer.pause() + }, + Modifier.size(42.dp) + ) { + Icon( + imageVector = if (videoPlayerViewModel.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, + contentDescription = "Play/Pause", + tint = Color.White, + modifier = Modifier.size(42.dp) + ) + } + } + } + } + + SubtitleOverlay( + cues = videoPlayerViewModel.cues, + modifier = Modifier.matchParentSize() + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/view/VideoPlayerPortal.kt b/app/src/main/java/com/acitelight/aether/view/VideoPlayerPortal.kt new file mode 100644 index 0000000..04c9a6f --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/view/VideoPlayerPortal.kt @@ -0,0 +1,255 @@ +package com.acitelight.aether.view + +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.LockOpen +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DividerDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import com.acitelight.aether.Global +import com.acitelight.aether.ToggleFullScreen +import com.acitelight.aether.viewModel.VideoPlayerViewModel + + +@Composable +fun VideoPlayerPortal( + videoPlayerViewModel: VideoPlayerViewModel, + navController: NavHostController +) { + val colorScheme = MaterialTheme.colorScheme + val configuration = LocalConfiguration.current + val screenHeight = configuration.screenHeightDp.dp + + val minHeight = 42.dp + var coverAlpha by remember { mutableFloatStateOf(0.0f) } + var maxHeight = remember { screenHeight * 0.65f } + var posed = remember { false } + val dens = LocalDensity.current + val listState = rememberLazyListState() + + var playerHeight by remember { mutableStateOf(screenHeight * 0.65f) } + + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val deltaY = available.y // px + val deltaDp = with(dens) { deltaY.toDp() } + + val r = if (deltaY < 0 && playerHeight > minHeight) { + val newHeight = (playerHeight + deltaDp).coerceIn(minHeight, maxHeight) + val consumedDp = newHeight - playerHeight + playerHeight = newHeight + val consumedPx = with(dens) { consumedDp.toPx() } + Offset(0f, consumedPx) + } else if (deltaY > 0 && playerHeight < maxHeight && listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0) { + val newHeight = (playerHeight + deltaDp).coerceIn(minHeight, maxHeight) + val consumedDp = newHeight - playerHeight + playerHeight = newHeight + val consumedPx = with(dens) { consumedDp.toPx() } + Offset(0f, consumedPx) + } else { + Offset.Zero + } + + val dh = playerHeight - minHeight + coverAlpha = (if (dh > 10.dp) + 0f + else + (10.dp.value - dh.value) / 10.0f) + + return r + } + } + } + + val klass by videoPlayerViewModel.currentKlass + val id by videoPlayerViewModel.currentId + val name by videoPlayerViewModel.currentName + val duration by videoPlayerViewModel.currentDuration + + ToggleFullScreen(false) + Column(Modifier + .nestedScroll(nestedScrollConnection) + .fillMaxHeight()) + { + Box { + PortalCorePlayer( + Modifier + .padding(top = 32.dp) + .heightIn(max = playerHeight) + .onGloballyPositioned { layoutCoordinates -> + if (!posed && videoPlayerViewModel.renderedFirst) { + maxHeight = with(dens) { layoutCoordinates.size.height.toDp() } + playerHeight = maxHeight + posed = true + } + }, + videoPlayerViewModel = videoPlayerViewModel, coverAlpha + ) + + androidx.compose.animation.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) + ) + } + } + } + + Row() + { + TabRow( + selectedTabIndex = videoPlayerViewModel.tabIndex, + modifier = Modifier.height(38.dp) + ) { + Tab( + selected = videoPlayerViewModel.tabIndex == 0, + onClick = { videoPlayerViewModel.tabIndex = 0 }, + text = { Text(text = "Introduction", maxLines = 1) }, + modifier = Modifier.height(38.dp) + ) + + Tab( + selected = videoPlayerViewModel.tabIndex == 1, + onClick = { videoPlayerViewModel.tabIndex = 1 }, + text = { Text(text = "Comment", maxLines = 1) }, + modifier = Modifier.height(38.dp) + ) + } + } + + LazyColumn(state = listState, modifier = Modifier.fillMaxWidth()) { + item { + Text( + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 12.dp) + .padding(top = 12.dp), + text = name, + fontSize = 16.sp, + maxLines = 2, + fontWeight = FontWeight.Bold, + ) + + Row(Modifier + .align(Alignment.Start) + .padding(horizontal = 4.dp) + .alpha(0.5f)) { + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = klass, + fontSize = 14.sp, + maxLines = 1, + fontWeight = FontWeight.Bold, + ) + + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = formatTime(duration), + fontSize = 14.sp, + maxLines = 1, + fontWeight = FontWeight.Bold, + ) + } + + HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color) + + PlaylistPanel( + Modifier, + videoPlayerViewModel = videoPlayerViewModel + ) + + HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color) + + HorizontalGallery(videoPlayerViewModel) + HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color) + + for (i in Global.sameClassVideos ?: listOf()) { + if (i.id == id) continue + + MiniVideoCard( + modifier = Modifier + .padding(horizontal = 12.dp), + i, + { + videoPlayerViewModel.isPlaying = false + videoPlayerViewModel.player?.pause() + val route = "video_player_route/${"${i.klass}/${i.id}".toHex()}" + navController.navigate(route) + }, videoPlayerViewModel.imageLoader!! + ) + HorizontalDivider( + Modifier + .padding(vertical = 8.dp) + .alpha(0.25f), + 1.dp, + DividerDefaults.color + ) + } + } + } + } +} diff --git a/app/src/main/java/com/acitelight/aether/view/VideoScreen.kt b/app/src/main/java/com/acitelight/aether/view/VideoScreen.kt index 2c0a6ce..0ed1037 100644 --- a/app/src/main/java/com/acitelight/aether/view/VideoScreen.kt +++ b/app/src/main/java/com/acitelight/aether/view/VideoScreen.kt @@ -345,11 +345,14 @@ fun VideoCard( }, onLongClick = { videoScreenViewModel.viewModelScope.launch { - videoScreenViewModel.download(video) + for(i in videos) + { + videoScreenViewModel.download(i) + } } Toast.makeText( videoScreenViewModel.context, - "Start downloading ${video.video.name}", + "Start downloading ${video.video.group}", Toast.LENGTH_SHORT ).show() } @@ -413,7 +416,7 @@ fun VideoCard( color = Color.White ) - if (video.isLocal) + if (videos.all{ it.isLocal }) Card( Modifier .align(Alignment.TopStart) 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 274fab4..efa415d 100644 --- a/app/src/main/java/com/acitelight/aether/viewModel/VideoPlayerViewModel.kt +++ b/app/src/main/java/com/acitelight/aether/viewModel/VideoPlayerViewModel.kt @@ -45,8 +45,11 @@ import java.io.File import javax.inject.Inject import androidx.core.net.toUri import androidx.media3.common.Tracks +import androidx.media3.datasource.DefaultDataSource import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import com.acitelight.aether.model.KeyImage +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first @HiltViewModel class VideoPlayerViewModel @Inject constructor( @@ -65,18 +68,22 @@ class VideoPlayerViewModel @Inject constructor( // 1 : Volume // 2 : Brightness var draggingPurpose by mutableIntStateOf(-1) - var thumbUp by mutableIntStateOf(0) - var thumbDown by mutableIntStateOf(0) - var star by mutableStateOf(false) var locked by mutableStateOf(false) private var _init: Boolean = false var startPlaying by mutableStateOf(false) var renderedFirst = false var videos: List