[feat&optimize] Video grouping recording, large-scale reconstruction
This commit is contained in:
@@ -8,5 +8,7 @@ import androidx.room.PrimaryKey
|
|||||||
data class VideoRecord (
|
data class VideoRecord (
|
||||||
@PrimaryKey(autoGenerate = false) val id: String = "",
|
@PrimaryKey(autoGenerate = false) val id: String = "",
|
||||||
@ColumnInfo(name = "name") val klass: 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
|
||||||
)
|
)
|
||||||
@@ -18,7 +18,7 @@ abstract class VideoRecordDatabase : RoomDatabase() {
|
|||||||
val instance = Room.databaseBuilder(
|
val instance = Room.databaseBuilder(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
VideoRecordDatabase::class.java,
|
VideoRecordDatabase::class.java,
|
||||||
"videorecord_database"
|
"videorecords_database"
|
||||||
).build()
|
).build()
|
||||||
INSTANCE = instance
|
INSTANCE = instance
|
||||||
instance
|
instance
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ class RecentManager @Inject constructor(
|
|||||||
val file = File(context.filesDir, filename)
|
val file = File(context.filesDir, filename)
|
||||||
val content = file.readText()
|
val content = file.readText()
|
||||||
content
|
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)
|
if (c != null) recentComic.add(recentComic.size, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: NoSuchMethodError) {
|
} catch (_: NoSuchMethodError) {
|
||||||
for (id in ids) {
|
for (id in ids) {
|
||||||
val c = mediaManager.queryComicInfoSingle(id)
|
val c = mediaManager.queryComicInfoSingle(id)
|
||||||
if (c != null) recentComic.add(recentComic.size, c)
|
if (c != null) recentComic.add(recentComic.size, c)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
for (id in ids) {
|
for (id in ids) {
|
||||||
val c = mediaManager.queryComicInfoSingle(id)
|
val c = mediaManager.queryComicInfoSingle(id)
|
||||||
if (c != null) recentComic.add(recentComic.size, c)
|
if (c != null) recentComic.add(recentComic.size, c)
|
||||||
@@ -93,9 +93,6 @@ class RecentManager @Inject constructor(
|
|||||||
|
|
||||||
suspend fun pushComic(context: Context, comicId: String) {
|
suspend fun pushComic(context: Context, comicId: String) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val c = readFile(context, "recent_comic.json")
|
|
||||||
|
|
||||||
|
|
||||||
val o = recentComic.map { it.id }.toMutableList()
|
val o = recentComic.map { it.id }.toMutableList()
|
||||||
|
|
||||||
|
|
||||||
@@ -152,14 +149,11 @@ class RecentManager @Inject constructor(
|
|||||||
suspend fun pushVideo(context: Context, video: VideoQueryIndex)
|
suspend fun pushVideo(context: Context, video: VideoQueryIndex)
|
||||||
{
|
{
|
||||||
mutex.withLock{
|
mutex.withLock{
|
||||||
val content = readFile(context, "recent.json")
|
|
||||||
val o = recentVideo.map{ VideoQueryIndex(it.klass, it.id) }.toMutableList()
|
val o = recentVideo.map{ VideoQueryIndex(it.klass, it.id) }.toMutableList()
|
||||||
|
|
||||||
if(o.contains(video))
|
if(o.contains(video))
|
||||||
{
|
{
|
||||||
val index = o.indexOf(video)
|
val index = o.indexOf(video)
|
||||||
val temp = recentVideo[index]
|
|
||||||
|
|
||||||
recentVideo.removeAt(index)
|
recentVideo.removeAt(index)
|
||||||
}
|
}
|
||||||
recentVideo.add(0, mediaManager.queryVideo(video.klass, video.id)!!)
|
recentVideo.add(0, mediaManager.queryVideo(video.klass, video.id)!!)
|
||||||
|
|||||||
101
app/src/main/java/com/acitelight/aether/view/BiliStyle.kt
Normal file
101
app/src/main/java/com/acitelight/aether/view/BiliStyle.kt
Normal file
@@ -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<Float> = 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<Float> = 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))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
106
app/src/main/java/com/acitelight/aether/view/MiniVideoCard.kt
Normal file
106
app/src/main/java/com/acitelight/aether/view/MiniVideoCard.kt
Normal file
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
409
app/src/main/java/com/acitelight/aether/view/PortalCorePlayer.kt
Normal file
409
app/src/main/java/com/acitelight/aether/view/PortalCorePlayer.kt
Normal file
@@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<Cue>,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -345,11 +345,14 @@ fun VideoCard(
|
|||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
videoScreenViewModel.viewModelScope.launch {
|
videoScreenViewModel.viewModelScope.launch {
|
||||||
videoScreenViewModel.download(video)
|
for(i in videos)
|
||||||
|
{
|
||||||
|
videoScreenViewModel.download(i)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
videoScreenViewModel.context,
|
videoScreenViewModel.context,
|
||||||
"Start downloading ${video.video.name}",
|
"Start downloading ${video.video.group}",
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
@@ -413,7 +416,7 @@ fun VideoCard(
|
|||||||
color = Color.White
|
color = Color.White
|
||||||
)
|
)
|
||||||
|
|
||||||
if (video.isLocal)
|
if (videos.all{ it.isLocal })
|
||||||
Card(
|
Card(
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.TopStart)
|
.align(Alignment.TopStart)
|
||||||
|
|||||||
@@ -45,8 +45,11 @@ import java.io.File
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.media3.common.Tracks
|
import androidx.media3.common.Tracks
|
||||||
|
import androidx.media3.datasource.DefaultDataSource
|
||||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||||
import com.acitelight.aether.model.KeyImage
|
import com.acitelight.aether.model.KeyImage
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class VideoPlayerViewModel @Inject constructor(
|
class VideoPlayerViewModel @Inject constructor(
|
||||||
@@ -65,18 +68,22 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
// 1 : Volume
|
// 1 : Volume
|
||||||
// 2 : Brightness
|
// 2 : Brightness
|
||||||
var draggingPurpose by mutableIntStateOf(-1)
|
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)
|
var locked by mutableStateOf(false)
|
||||||
private var _init: Boolean = false
|
private var _init: Boolean = false
|
||||||
var startPlaying by mutableStateOf(false)
|
var startPlaying by mutableStateOf(false)
|
||||||
var renderedFirst = false
|
var renderedFirst = false
|
||||||
var videos: List<Video> = listOf()
|
var videos: List<Video> = listOf()
|
||||||
|
|
||||||
val dataSourceFactory = OkHttpDataSource.Factory(createOkHttp())
|
private val httpDataSourceFactory = OkHttpDataSource.Factory(createOkHttp())
|
||||||
|
private val defaultDataSourceFactory by lazy {
|
||||||
|
DefaultDataSource.Factory(
|
||||||
|
context,
|
||||||
|
httpDataSourceFactory
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var imageLoader: ImageLoader? = null
|
var imageLoader: ImageLoader? = null
|
||||||
var brit by mutableFloatStateOf(0.5f)
|
var brit by mutableFloatStateOf(0.0f)
|
||||||
val database: VideoRecordDatabase = VideoRecordDatabase.getDatabase(context)
|
val database: VideoRecordDatabase = VideoRecordDatabase.getDatabase(context)
|
||||||
var cues by mutableStateOf(listOf<Cue>())
|
var cues by mutableStateOf(listOf<Cue>())
|
||||||
|
|
||||||
@@ -101,7 +108,15 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
videos = mediaManager.queryVideoBulk(vs.first()[0], vs.map { it[1] })!!
|
videos = mediaManager.queryVideoBulk(vs.first()[0], vs.map { it[1] })!!
|
||||||
startPlay(videos.first())
|
|
||||||
|
val ii = database.userDao().getAll().first()
|
||||||
|
val ix = ii.filter { it.id in videos.map{ m -> m.id } }.maxByOrNull { it.time }
|
||||||
|
|
||||||
|
startPlay(
|
||||||
|
if (ix != null)
|
||||||
|
videos.first { it.id == ix.id }
|
||||||
|
else videos.first()
|
||||||
|
)
|
||||||
startListen()
|
startListen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,59 +127,72 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
* - If it's a http(s) URL -> try HEAD; if HEAD unsupported, try GET with Range: bytes=0-1
|
* - If it's a http(s) URL -> try HEAD; if HEAD unsupported, try GET with Range: bytes=0-1
|
||||||
* - Return null when unreachable / 404 / not exist
|
* - Return null when unreachable / 404 / not exist
|
||||||
*/
|
*/
|
||||||
private suspend fun tryResolveSubtitleUri(pathOrUrl: String?): Uri? = withContext(Dispatchers.IO) {
|
private suspend fun tryResolveSubtitleUri(pathOrUrl: String?): Uri? =
|
||||||
if (pathOrUrl.isNullOrBlank()) return@withContext null
|
withContext(Dispatchers.IO) {
|
||||||
val trimmed = pathOrUrl.trim()
|
if (pathOrUrl.isNullOrBlank()) return@withContext null
|
||||||
|
val trimmed = pathOrUrl.trim()
|
||||||
|
|
||||||
// Remote URL case (http/https)
|
// Remote URL case (http/https)
|
||||||
if (trimmed.startsWith("http://", ignoreCase = true) || trimmed.startsWith("https://", ignoreCase = true)) {
|
if (trimmed.startsWith("http://", ignoreCase = true) || trimmed.startsWith(
|
||||||
try {
|
"https://",
|
||||||
val client = createOkHttp()
|
ignoreCase = true
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val client = createOkHttp()
|
||||||
|
|
||||||
val headReq = Request.Builder().url(trimmed).head().build()
|
val headReq = Request.Builder().url(trimmed).head().build()
|
||||||
val headResp = try { client.newCall(headReq).execute() } catch (_: Exception) { null }
|
val headResp = try {
|
||||||
|
client.newCall(headReq).execute()
|
||||||
headResp?.use { resp ->
|
} catch (_: Exception) {
|
||||||
val code = resp.code
|
null
|
||||||
if (code == 200 || code == 206) {
|
|
||||||
return@withContext trimmed.toUri()
|
|
||||||
}
|
}
|
||||||
if (code == 404) {
|
|
||||||
return@withContext null
|
headResp?.use { resp ->
|
||||||
|
val code = resp.code
|
||||||
|
if (code == 200 || code == 206) {
|
||||||
|
return@withContext trimmed.toUri()
|
||||||
|
}
|
||||||
|
if (code == 404) {
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
val rangeReq = Request.Builder()
|
||||||
|
.url(trimmed)
|
||||||
|
.addHeader("Range", "bytes=0-1")
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val rangeResp = try {
|
||||||
|
client.newCall(rangeReq).execute()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
rangeResp?.use { resp ->
|
||||||
|
val code = resp.code
|
||||||
|
if (code == 206) {
|
||||||
|
return@withContext trimmed.toUri()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == 200) {
|
||||||
|
return@withContext trimmed.toUri()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == 404) {
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return@withContext null
|
||||||
}
|
}
|
||||||
val rangeReq = Request.Builder()
|
|
||||||
.url(trimmed)
|
|
||||||
.addHeader("Range", "bytes=0-1")
|
|
||||||
.get()
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val rangeResp = try { client.newCall(rangeReq).execute() } catch (_: Exception) { null }
|
|
||||||
|
|
||||||
rangeResp?.use { resp ->
|
|
||||||
val code = resp.code
|
|
||||||
if (code == 206) {
|
|
||||||
return@withContext trimmed.toUri()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code == 200) {
|
|
||||||
return@withContext trimmed.toUri()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code == 404) {
|
|
||||||
return@withContext null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@withContext null
|
return@withContext null
|
||||||
|
} else {
|
||||||
|
// Local path
|
||||||
|
val f = File(trimmed)
|
||||||
|
return@withContext if (f.exists() && f.isFile) Uri.fromFile(f) else null
|
||||||
}
|
}
|
||||||
return@withContext null
|
|
||||||
} else {
|
|
||||||
// Local path
|
|
||||||
val f = File(trimmed)
|
|
||||||
return@withContext if (f.exists() && f.isFile) Uri.fromFile(f) else null
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
fun startListen() {
|
fun startListen() {
|
||||||
@@ -178,6 +206,19 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
suspend fun startPlay(video: Video) {
|
suspend fun startPlay(video: Video) {
|
||||||
|
if (currentId.value.isNotEmpty() && currentKlass.value.isNotEmpty()) {
|
||||||
|
val pos = player?.currentPosition ?: 0L
|
||||||
|
database.userDao().insert(
|
||||||
|
VideoRecord(
|
||||||
|
currentId.value,
|
||||||
|
currentKlass.value,
|
||||||
|
pos,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
videos.joinToString(",") { it.id })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderedFirst = false
|
||||||
currentId.value = video.id
|
currentId.value = video.id
|
||||||
currentKlass.value = video.klass
|
currentKlass.value = video.klass
|
||||||
currentName.value = video.video.name
|
currentName.value = video.video.name
|
||||||
@@ -196,7 +237,8 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
|
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
val trackSelector = DefaultTrackSelector(context)
|
val trackSelector = DefaultTrackSelector(context)
|
||||||
val builder = ExoPlayer.Builder(context).setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
|
val builder = ExoPlayer.Builder(context)
|
||||||
|
.setMediaSourceFactory(DefaultMediaSourceFactory(defaultDataSourceFactory))
|
||||||
|
|
||||||
player = builder.setTrackSelector(trackSelector).build().apply {
|
player = builder.setTrackSelector(trackSelector).build().apply {
|
||||||
addListener(object : Player.Listener {
|
addListener(object : Player.Listener {
|
||||||
@@ -219,7 +261,7 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
override fun onRenderedFirstFrame() {
|
override fun onRenderedFirstFrame() {
|
||||||
if (!renderedFirst) {
|
if (!renderedFirst) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val ii = database.userDao().get(video.id, video.klass)
|
val ii = database.userDao().get(currentId.value, currentKlass.value)
|
||||||
if (ii != null) {
|
if (ii != null) {
|
||||||
player?.seekTo(ii.position)
|
player?.seekTo(ii.position)
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
@@ -271,12 +313,22 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
|
|
||||||
_init = false
|
_init = false
|
||||||
val p = player!!.currentPosition
|
val pos = player?.currentPosition ?: 0L
|
||||||
player?.release()
|
player?.release()
|
||||||
|
player = null
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
if(currentId.value.isNotEmpty() && currentKlass.value.isNotEmpty())
|
if (currentId.value.isNotEmpty() && currentKlass.value.isNotEmpty())
|
||||||
database.userDao().insert(VideoRecord(currentId.value, currentKlass.value, p))
|
database.userDao().insert(
|
||||||
|
VideoRecord(
|
||||||
|
currentId.value,
|
||||||
|
currentKlass.value,
|
||||||
|
pos,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
videos.joinToString(",") { it.id })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user