2 Commits

Author SHA1 Message Date
acite
b5940aecc3 [feat] Beautify the interface, title bar in full screen mode 2025-08-27 03:56:26 +08:00
acite
76054da910 [feat] Gestures for volume and brightness control 2025-08-27 03:43:52 +08:00
3 changed files with 308 additions and 36 deletions

View File

@@ -5,6 +5,7 @@ import androidx.compose.material.icons.Icons
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.net.http.SslCertificate.saveState import android.net.http.SslCertificate.saveState
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
@@ -56,6 +57,9 @@ import com.acitelight.aether.view.VideoScreen
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
window.attributes = window.attributes.apply {
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
}
enableEdgeToEdge() enableEdgeToEdge()
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
setContent { setContent {

View File

@@ -1,8 +1,10 @@
package com.acitelight.aether.view package com.acitelight.aether.view
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.media.AudioManager
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@@ -12,7 +14,6 @@ import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -50,7 +51,10 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape 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.ArrowBack
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.FastForward
import androidx.compose.material.icons.filled.Fullscreen import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
@@ -58,6 +62,7 @@ import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.ThumbDown import androidx.compose.material.icons.filled.ThumbDown
import androidx.compose.material.icons.filled.ThumbUp import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material.icons.filled.VolumeUp
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardColors import androidx.compose.material3.CardColors
import androidx.compose.material3.DividerDefaults import androidx.compose.material3.DividerDefaults
@@ -68,6 +73,8 @@ import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow import androidx.compose.material3.TabRow
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -95,6 +102,7 @@ import com.acitelight.aether.Global
import com.acitelight.aether.ToggleFullScreen import com.acitelight.aether.ToggleFullScreen
import com.acitelight.aether.model.KeyImage import com.acitelight.aether.model.KeyImage
import com.acitelight.aether.model.Video import com.acitelight.aether.model.Video
import kotlin.math.abs
fun formatTime(ms: Long): String { fun formatTime(ms: Long): String {
if (ms <= 0) return "00:00:00" if (ms <= 0) return "00:00:00"
@@ -223,6 +231,18 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
val context = LocalContext.current val context = LocalContext.current
val activity = context as? Activity 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) Box(modifier)
{ {
AndroidView( AndroidView(
@@ -239,19 +259,53 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
.fillMaxWidth() .fillMaxWidth()
.pointerInput(Unit) { .pointerInput(Unit) {
detectDragGestures( detectDragGestures(
onDragStart = { onDragStart = { offset ->
videoPlayerViewModel.dragging = true if(offset.x < size.width / 2)
videoPlayerViewModel.planeVisibility = true {
exoPlayer.pause() videoPlayerViewModel.draggingPurpose = -1;
}else{
videoPlayerViewModel.draggingPurpose = -2;
}
}, },
onDragEnd = { onDragEnd = {
videoPlayerViewModel.dragging = false if (videoPlayerViewModel.isPlaying && videoPlayerViewModel.draggingPurpose == 0)
if (videoPlayerViewModel.isPlaying)
exoPlayer.play() exoPlayer.play()
videoPlayerViewModel.draggingPurpose = -1;
}, },
onDrag = { change, dragAmount -> onDrag = { change, dragAmount ->
exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong()) if(abs(dragAmount.x) > abs(dragAmount.y) &&
videoPlayerViewModel.playProcess = exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat() (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)
}
} }
) )
} }
@@ -312,7 +366,7 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
} }
androidx.compose.animation.AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.dragging, visible = videoPlayerViewModel.draggingPurpose == 0,
enter = fadeIn( enter = fadeIn(
initialAlpha = 0f, initialAlpha = 0f,
), ),
@@ -320,7 +374,8 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
targetAlpha = 0f targetAlpha = 0f
), ),
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) { )
{
Text( Text(
text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${ text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${
formatTime( formatTime(
@@ -328,12 +383,74 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
) )
}", }",
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier modifier = Modifier.padding(bottom = 12.dp),
.padding(bottom = 12.dp),
fontSize = 18.sp 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) if(cover > 0.0f)
Spacer(Modifier.background(Color(0x00FF6699 - 0x00222222 + ((0x000000FF * cover).toLong() shl 24) )).fillMaxSize()) Spacer(Modifier.background(Color(0x00FF6699 - 0x00222222 + ((0x000000FF * cover).toLong() shl 24) )).fillMaxSize())
@@ -371,7 +488,13 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 2.dp) .padding(horizontal = 2.dp)
.align(Alignment.BottomCenter), .align(Alignment.BottomCenter).background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.4f),
)
)),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
IconButton( IconButton(
@@ -713,6 +836,18 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
val activity = context as? Activity val activity = context as? Activity
val exoPlayer: ExoPlayer = videoPlayerViewModel._player!!; val exoPlayer: ExoPlayer = videoPlayerViewModel._player!!;
val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager }
val 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
)
}
BackHandler { BackHandler {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
} }
@@ -738,19 +873,53 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
.fillMaxWidth() .fillMaxWidth()
.pointerInput(Unit) { .pointerInput(Unit) {
detectDragGestures( detectDragGestures(
onDragStart = { onDragStart = { offset ->
videoPlayerViewModel.planeVisibility = true if(offset.x < size.width / 2)
videoPlayerViewModel.dragging = true; {
exoPlayer.pause() videoPlayerViewModel.draggingPurpose = -1;
}else{
videoPlayerViewModel.draggingPurpose = -2;
}
}, },
onDragEnd = { onDragEnd = {
videoPlayerViewModel.dragging = false; if (videoPlayerViewModel.isPlaying && videoPlayerViewModel.draggingPurpose == 0)
if (videoPlayerViewModel.isPlaying)
exoPlayer.play() exoPlayer.play()
videoPlayerViewModel.draggingPurpose = -1;
}, },
onDrag = { change, dragAmount -> onDrag = { change, dragAmount ->
exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong()) if(abs(dragAmount.x) > abs(dragAmount.y) &&
videoPlayerViewModel.playProcess = exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat() (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)
}
} }
) )
} }
@@ -782,7 +951,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
) )
androidx.compose.animation.AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.dragging, visible = videoPlayerViewModel.draggingPurpose == 0,
enter = fadeIn( enter = fadeIn(
initialAlpha = 0f, initialAlpha = 0f,
), ),
@@ -790,7 +959,8 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
targetAlpha = 0f targetAlpha = 0f
), ),
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) { )
{
Text( Text(
text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${ text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${
formatTime( formatTime(
@@ -803,6 +973,68 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
) )
} }
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( AnimatedVisibility(
visible = videoPlayerViewModel.isLongPressing, visible = videoPlayerViewModel.isLongPressing,
enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }), enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }),
@@ -832,21 +1064,50 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
} }
} }
IconButton( AnimatedVisibility(
onClick = { visible = videoPlayerViewModel.planeVisibility,
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }),
}, exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight }),
modifier = Modifier modifier = Modifier
.align(Alignment.TopCenter)
.fillMaxWidth()
)
{
Row(Modifier
.align(Alignment.TopStart) .align(Alignment.TopStart)
.padding(8.dp) .padding(horizontal = 32.dp).background(
) { brush = Brush.verticalGradient(
Icon( colors = listOf(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, Color.Black.copy(alpha = 0.4f),
contentDescription = "Back", Color.Transparent,
tint = Color.White )
) )))
{
IconButton(
onClick = {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
},
modifier = Modifier.size(36.dp).align(Alignment.CenterVertically)
) {
Icon(
modifier = Modifier.size(36.dp),
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = Color.White
)
}
Text(
text = "${videoPlayerViewModel.video?.video?.name}",
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 12.dp).align(Alignment.CenterVertically),
fontSize = 18.sp
)
}
} }
AnimatedVisibility( AnimatedVisibility(
visible = videoPlayerViewModel.planeVisibility, visible = videoPlayerViewModel.planeVisibility,
enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }), enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }),

View File

@@ -1,5 +1,6 @@
package com.acitelight.aether.viewModel package com.acitelight.aether.viewModel
import android.app.Activity
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
@@ -40,7 +41,12 @@ class VideoPlayerViewModel() : ViewModel()
var playProcess by mutableFloatStateOf(0.0f) var playProcess by mutableFloatStateOf(0.0f)
var planeVisibility by mutableStateOf(true) var planeVisibility by mutableStateOf(true)
var isLongPressing by mutableStateOf(false) var isLongPressing by mutableStateOf(false)
var dragging by mutableStateOf(false)
// -1 : Not dragging
// 0 : Seek
// 1 : Volume
// 2 : Brightness
var draggingPurpose by mutableIntStateOf(-1)
var thumbUp by mutableIntStateOf(0) var thumbUp by mutableIntStateOf(0)
var thumbDown by mutableIntStateOf(0) var thumbDown by mutableIntStateOf(0)
@@ -53,6 +59,7 @@ class VideoPlayerViewModel() : ViewModel()
val dataSourceFactory = OkHttpDataSource.Factory(createOkHttp()) val dataSourceFactory = OkHttpDataSource.Factory(createOkHttp())
var imageLoader: ImageLoader? = null; var imageLoader: ImageLoader? = null;
var brit by mutableFloatStateOf(0.5f)
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@Composable @Composable