[add] Implement Bilibili style

This commit is contained in:
acite
2025-08-25 04:26:34 +08:00
parent d0a6497dd6
commit 484f158e17
12 changed files with 766 additions and 424 deletions

View File

@@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
kotlin("plugin.serialization") version "1.9.0"
} }
android { android {

View File

@@ -6,8 +6,6 @@ import androidx.compose.runtime.setValue
import com.acitelight.aether.model.Video import com.acitelight.aether.model.Video
object Global { object Global {
var videoName: String = ""
var videoClass: String = ""
var loggedIn by mutableStateOf(false) var loggedIn by mutableStateOf(false)
var video: Video? = null var sameClassVideos: List<Video>? = null
} }

View File

@@ -116,7 +116,7 @@ fun AppNavigation() {
modifier = if(shouldShowBottomBar)Modifier.padding(innerPadding) else Modifier.padding(0.dp) modifier = if(shouldShowBottomBar)Modifier.padding(innerPadding) else Modifier.padding(0.dp)
) { ) {
composable(Screen.Home.route) { composable(Screen.Home.route) {
HomeScreen() HomeScreen(navController = navController)
} }
composable(Screen.Video.route) { composable(Screen.Video.route) {
VideoScreen(navController = navController) VideoScreen(navController = navController)
@@ -159,8 +159,6 @@ fun BottomNavigationBar(navController: NavController) {
Screen.Me Screen.Me
) else listOf( ) else listOf(
Screen.Home, Screen.Home,
Screen.Video,
Screen.Comic,
Screen.Transmission, Screen.Transmission,
Screen.Me Screen.Me
) )

View File

@@ -25,4 +25,5 @@ class Video constructor(
KeyImage(url = "${ApiClient.base}api/video/$klass/$id/gallery/$it?token=$token", key = "$klass/$id/gallery/$it") KeyImage(url = "${ApiClient.base}api/video/$klass/$id/gallery/$it?token=$token", key = "$klass/$id/gallery/$it")
} }
} }
} }

View File

@@ -0,0 +1,9 @@
package com.acitelight.aether.model
import kotlinx.serialization.Serializable
@Serializable
data class VideoQueryIndex(
val klass: String,
val id: String
)

View File

@@ -0,0 +1,94 @@
package com.acitelight.aether.service
import android.content.Context
import com.acitelight.aether.model.Video
import com.acitelight.aether.model.VideoQueryIndex
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.*
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
object RecentManager
{
private val mutex = Mutex()
suspend fun readFile(context: Context, filename: String): String {
return withContext(Dispatchers.IO) {
try {
val file = File(context.filesDir, filename)
val content = file.readText()
content
} catch (e: FileNotFoundException) {
"[]"
} catch (e: IOException) {
"[]"
}
}
}
suspend fun writeFile(context: Context, filename: String, content: String) {
withContext(Dispatchers.IO) {
try {
val file = File(context.filesDir, filename)
file.writeText(content)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
suspend fun Query(context: Context): List<VideoQueryIndex>
{
val content = readFile(context, "recent.json")
try{
val r = Json.decodeFromString<List<VideoQueryIndex>>(content)
_recent.value = r.map{
MediaManager.queryVideo(it.klass, it.id)
}
return r
}catch (e: Exception)
{
print(e.message)
}
return listOf()
}
suspend fun Push(context: Context, video: VideoQueryIndex)
{
mutex.withLock{
val content = readFile(context, "recent.json")
var o = Json.decodeFromString<List<VideoQueryIndex>>(content).toMutableList();
if(o.contains(video))
{
val temp = o[0]
val index = o.indexOf(video)
o[0] = o[index]
o[index] = temp
}
else
{
o.add(0, video)
}
if(o.size >= 21)
o.removeAt(o.size - 1)
_recent.value = o.map{
MediaManager.queryVideo(it.klass, it.id)
}
writeFile(context, "recent.json", Json.encodeToString(o))
}
}
private val _recent = MutableStateFlow<List<Video>>(emptyList())
val recent: StateFlow<List<Video>> = _recent
}

View File

@@ -4,20 +4,64 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.acitelight.aether.Global
import com.acitelight.aether.service.RecentManager
import com.acitelight.aether.viewModel.HomeScreenViewModel import com.acitelight.aether.viewModel.HomeScreenViewModel
@Composable @Composable
fun HomeScreen(homeScreenViewModel: HomeScreenViewModel = viewModel()) fun HomeScreen(homeScreenViewModel: HomeScreenViewModel = viewModel(), navController: NavController)
{ {
if(Global.loggedIn)
homeScreenViewModel.Init()
val recent by RecentManager.recent.collectAsState()
LazyColumn(modifier = Modifier.fillMaxWidth())
{
item()
{
Column {
Text(
text = "Recent",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(16.dp).align(Alignment.Start)
)
HorizontalDivider(Modifier.padding(8.dp), 2.dp, DividerDefaults.color)
for(i in recent)
{
MiniVideoCard(
modifier = Modifier
.padding(horizontal = 12.dp),
i,
{
val route = "video_player_route/${ "${i.klass}/${i.id}".toHex() }"
navController.navigate(route)
})
HorizontalDivider(Modifier.padding(vertical = 8.dp).alpha(0.25f), 1.dp, DividerDefaults.color)
}
}
}
}
} }

View File

@@ -1,91 +1,56 @@
package com.acitelight.aether.view package com.acitelight.aether.view
import android.R
import android.app.Activity import android.app.Activity
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.text.Layout
import android.widget.Toast
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
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically 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.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.material.icons.Icons 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
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.SegmentedButtonDefaults.Icon
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.acitelight.aether.service.MediaManager
import com.acitelight.aether.viewModel.VideoPlayerViewModel import com.acitelight.aether.viewModel.VideoPlayerViewModel
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack
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
@@ -93,44 +58,42 @@ 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.material3.Divider import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.DividerDefaults import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
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.getValue
import androidx.compose.runtime.mutableFloatStateOf 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.Alignment
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.TransformOrigin 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.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coil3.request.ImageRequest import coil3.request.ImageRequest
import com.acitelight.aether.BottomNavigationBar
import com.acitelight.aether.Global 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.ui.theme.AetherTheme import com.acitelight.aether.model.Video
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import org.jetbrains.annotations.Async
import java.nio.file.WatchEvent
fun formatTime(ms: Long): String { fun formatTime(ms: Long): String {
if (ms <= 0) return "00:00:00" if (ms <= 0) return "00:00:00"
@@ -238,9 +201,10 @@ fun VideoPlayer(
videoId: String, videoId: String,
navController: NavHostController navController: NavHostController
) { ) {
videoPlayerViewModel.Init(videoId); videoPlayerViewModel.Init(videoId)
videoPlayerViewModel.startListen()
if(videoPlayerViewModel.startPlaying)
{
if (isLandscape()) { if (isLandscape()) {
VideoPlayerLandscape(videoPlayerViewModel) VideoPlayerLandscape(videoPlayerViewModel)
} }
@@ -249,19 +213,16 @@ fun VideoPlayer(
VideoPlayerPortal(videoPlayerViewModel, navController) VideoPlayerPortal(videoPlayerViewModel, navController)
} }
} }
}
@Composable @Composable
fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController: NavHostController?) { fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel, cover: Float)
{
val exoPlayer: ExoPlayer = videoPlayerViewModel._player!!;
val context = LocalContext.current val context = LocalContext.current
val activity = context as? Activity val activity = context as? Activity
val exoPlayer: ExoPlayer = videoPlayerViewModel._player!!;
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp;
ToggleFullScreen(false) Box(modifier)
Column()
{
Box(modifier = Modifier.padding(top = 42.dp).heightIn(max = screenHeight * 0.65f))
{ {
AndroidView( AndroidView(
factory = { factory = {
@@ -349,19 +310,6 @@ fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController:
} }
} }
IconButton(
onClick = { navController?.popBackStack() },
modifier = Modifier
.padding(8.dp)
.align(Alignment.TopStart)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = Color.White
)
}
androidx.compose.animation.AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.dragging, visible = videoPlayerViewModel.dragging,
enter = fadeIn( enter = fadeIn(
@@ -385,6 +333,9 @@ fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController:
) )
} }
if(cover > 0.0f)
Spacer(Modifier.background(Color(0x00FF6699 - 0x00222222 + ((0x000000FF * cover).toLong() shl 24) )).fillMaxSize())
androidx.compose.animation.AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
visible = !videoPlayerViewModel.planeVisibility, visible = !videoPlayerViewModel.planeVisibility,
enter = fadeIn( enter = fadeIn(
@@ -412,7 +363,7 @@ fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController:
exit = fadeOut( exit = fadeOut(
targetAlpha = 0f targetAlpha = 0f
), ),
modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter) modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter).height(42.dp)
) )
{ {
Row( Row(
@@ -480,6 +431,72 @@ fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController:
} }
} }
} }
}
@Composable
fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController: NavHostController)
{
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
}
}
}
ToggleFullScreen(false)
Column(Modifier.nestedScroll(nestedScrollConnection).fillMaxHeight())
{
PortalCorePlayer(
Modifier
.padding(top = 42.dp)
.heightIn(max = playerHeight)
.onGloballyPositioned { layoutCoordinates ->
if(!posed && videoPlayerViewModel.renderedFirst)
{
maxHeight = with(dens) {layoutCoordinates.size.height.toDp()}
playerHeight = maxHeight
posed = true
}
},
videoPlayerViewModel = videoPlayerViewModel, coverAlpha)
Row() Row()
{ {
@@ -503,13 +520,13 @@ fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController:
} }
} }
LazyColumn( modifier = Modifier.fillMaxWidth()) { LazyColumn(state = listState, modifier = Modifier.fillMaxWidth()) {
item{ item{
HorizontalDivider(Modifier, 2.dp, DividerDefaults.color) HorizontalDivider(Modifier, 2.dp, DividerDefaults.color)
Text( Text(
modifier = Modifier.align(Alignment.Start).padding(horizontal = 12.dp).padding(top = 12.dp), modifier = Modifier.align(Alignment.Start).padding(horizontal = 12.dp).padding(top = 12.dp),
text = Global.videoName, text = videoPlayerViewModel.video?.video?.name ?: "",
fontSize = 16.sp, fontSize = 16.sp,
maxLines = 2, maxLines = 2,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -518,7 +535,7 @@ fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController:
Row(Modifier.align(Alignment.Start).padding(horizontal = 4.dp).alpha(0.5f)) { Row(Modifier.align(Alignment.Start).padding(horizontal = 4.dp).alpha(0.5f)) {
Text( Text(
modifier = Modifier.padding(horizontal = 8.dp), modifier = Modifier.padding(horizontal = 8.dp),
text = Global.videoClass, text = videoPlayerViewModel.video?.klass ?: "",
fontSize = 14.sp, fontSize = 14.sp,
maxLines = 1, maxLines = 1,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -526,7 +543,7 @@ fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController:
Text( Text(
modifier = Modifier.padding(horizontal = 8.dp), modifier = Modifier.padding(horizontal = 8.dp),
text = formatTime(Global.video?.video?.duration ?: 0), text = formatTime(videoPlayerViewModel.video?.video?.duration ?: 0),
fontSize = 14.sp, fontSize = 14.sp,
maxLines = 1, maxLines = 1,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -535,9 +552,40 @@ fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController:
HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color) HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color)
SocialPanel(Modifier.align(Alignment.CenterHorizontally).fillMaxWidth(), 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 == videoPlayerViewModel.video?.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)
})
HorizontalDivider(Modifier.padding(vertical = 8.dp).alpha(0.25f), 1.dp, DividerDefaults.color)
}
}
}
}
}
@Composable
fun SocialPanel(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel)
{
Row( Row(
horizontalArrangement = Arrangement.Center, modifier,
modifier = Modifier.align(Alignment.CenterHorizontally) horizontalArrangement = Arrangement.Center
) )
{ {
Column(modifier = Modifier.padding(horizontal = 12.dp)) { Column(modifier = Modifier.padding(horizontal = 12.dp)) {
@@ -624,25 +672,17 @@ fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController:
} }
} }
} }
Spacer(modifier = Modifier.height(16.dp))
HorizontalGallery()
HorizontalDivider(Modifier.padding(vertical = 8.dp), 1.dp, DividerDefaults.color)
}
}
}
} }
@Composable @Composable
fun HorizontalGallery() fun HorizontalGallery(videoPlayerViewModel: VideoPlayerViewModel)
{ {
LazyRow( LazyRow(
modifier = Modifier.fillMaxWidth().height(100.dp), modifier = Modifier.fillMaxWidth().height(120.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(horizontal = 24.dp) contentPadding = PaddingValues(horizontal = 24.dp)
) { ) {
items(Global.video?.getGallery() ?: listOf()) { it -> items(videoPlayerViewModel.video?.getGallery() ?: listOf()) { it ->
SingleImageItem(img = it) SingleImageItem(img = it)
} }
} }
@@ -659,7 +699,7 @@ fun SingleImageItem(img: KeyImage) {
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(8.dp)), .clip(RoundedCornerShape(12.dp)),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} }
@@ -867,3 +907,67 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
} }
} }
} }
@Composable
fun MiniVideoCard(modifier: Modifier, video: Video, onClick: () -> Unit)
{
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")
.build(),
contentDescription = null,
modifier = Modifier
.width(128.dp).fillMaxHeight()
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
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,
)
}
}
}
}

View File

@@ -3,6 +3,7 @@ package com.acitelight.aether.view
import android.R.id.tabs import android.R.id.tabs
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -43,11 +44,14 @@ import com.acitelight.aether.viewModel.VideoScreenViewModel
import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.ScrollableTabRow
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil3.request.ImageRequest import coil3.request.ImageRequest
import com.acitelight.aether.Global import com.acitelight.aether.Global
import kotlinx.coroutines.flow.first
import java.nio.charset.Charset import java.nio.charset.Charset
fun String.toHex(): String { fun String.toHex(): String {
@@ -83,7 +87,7 @@ fun VideoScreen(videoScreenViewModel: VideoScreenViewModel = viewModel(), navCon
) )
{ {
items(videoList) { video -> items(videoList) { video ->
VideoCard(video, navController) VideoCard(video, navController, videoScreenViewModel)
} }
} }
} }
@@ -110,17 +114,17 @@ fun TopRow(videoScreenViewModel: VideoScreenViewModel)
} }
@Composable @Composable
fun VideoCard(video: Video, navController: NavHostController) { fun VideoCard(video: Video, navController: NavHostController, videoScreenViewModel: VideoScreenViewModel) {
val videoList by videoScreenViewModel.videos.collectAsState()
Card( Card(
shape = RoundedCornerShape(6.dp), shape = RoundedCornerShape(6.dp),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight(), .wrapContentHeight(),
onClick = { onClick = {
Global.videoName = video.video.name Global.sameClassVideos = videoList
Global.videoClass = video.klass val route = "video_player_route/${ "${video.klass}/${video.id}".toHex() }"
Global.video = video
val route = "video_player_route/${ video.getVideo().toHex() }"
navController.navigate(route) navController.navigate(route)
} }
) { ) {
@@ -128,6 +132,7 @@ fun VideoCard(video: Video, navController: NavHostController) {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
) { ) {
Box(modifier = Modifier.fillMaxSize()){
AsyncImage( AsyncImage(
model = ImageRequest.Builder(LocalContext.current) model = ImageRequest.Builder(LocalContext.current)
.data(video.getCover()) .data(video.getCover())
@@ -139,6 +144,23 @@ fun VideoCard(video: Video, navController: NavHostController) {
.fillMaxSize(), .fillMaxSize(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
Text(
modifier = Modifier.align(Alignment.BottomEnd).padding(2.dp),
text = formatTime(video.video.duration), fontSize = 12.sp, color = Color.White, fontWeight = FontWeight.Bold)
Box(
Modifier
.fillMaxWidth()
.height(24.dp)
.background( brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.45f)
)
))
.align(Alignment.BottomCenter))
}
Text( Text(
text = video.video.name, text = video.video.name,
fontSize = 14.sp, fontSize = 14.sp,
@@ -151,7 +173,8 @@ fun VideoCard(video: Video, navController: NavHostController) {
modifier = Modifier.padding(horizontal = 8.dp), modifier = Modifier.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text("class: ${video.klass}", fontSize = 12.sp) Text("Class", fontSize = 12.sp)
Text("${video.klass}", fontSize = 12.sp)
} }
} }
} }

View File

@@ -1,21 +1,81 @@
package com.acitelight.aether.viewModel package com.acitelight.aether.viewModel
import android.app.Application import android.app.Application
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.acitelight.aether.Global import com.acitelight.aether.Global
import com.acitelight.aether.dataStore import com.acitelight.aether.dataStore
import com.acitelight.aether.model.Video
import com.acitelight.aether.model.VideoQueryIndex
import com.acitelight.aether.service.ApiClient import com.acitelight.aether.service.ApiClient
import com.acitelight.aether.service.AuthManager import com.acitelight.aether.service.AuthManager
import com.acitelight.aether.service.MediaManager import com.acitelight.aether.service.MediaManager
import com.acitelight.aether.service.MediaManager.token import com.acitelight.aether.service.MediaManager.token
import com.acitelight.aether.service.RecentManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class HomeScreenViewModel() : ViewModel() class HomeScreenViewModel(application: Application) : AndroidViewModel(application)
{ {
private val dataStore = application.dataStore
private val USER_NAME_KEY = stringPreferencesKey("user_name")
private val PRIVATE_KEY = stringPreferencesKey("private_key")
val userNameFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[USER_NAME_KEY] ?: ""
}
val privateKeyFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[PRIVATE_KEY] ?: ""
}
var _init = false
@Composable
fun Init(){
if(_init) return
_init = true
val context = LocalContext.current
remember {
viewModelScope.launch {
RecentManager.Query(context)
}
}
}
init {
viewModelScope.launch {
val u = userNameFlow.first()
val p = privateKeyFlow.first()
if(u=="" || p=="") return@launch
try{
if (MediaManager.token == "null")
MediaManager.token = AuthManager.fetchToken(
ApiClient.base,
u,
p
)!!
}catch(e: Exception)
{
print(e.message)
}finally {
Global.loggedIn = true
}
}
}
} }

View File

@@ -13,10 +13,15 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Player.STATE_READY
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import com.acitelight.aether.Global import com.acitelight.aether.Global
import com.acitelight.aether.model.Video
import com.acitelight.aether.model.VideoQueryIndex
import com.acitelight.aether.service.MediaManager import com.acitelight.aether.service.MediaManager
import com.acitelight.aether.service.RecentManager
import com.acitelight.aether.view.hexToString import com.acitelight.aether.view.hexToString
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -32,25 +37,47 @@ class VideoPlayerViewModel() : ViewModel()
var isLongPressing by mutableStateOf(false) var isLongPressing by mutableStateOf(false)
var dragging by mutableStateOf(false) var dragging by mutableStateOf(false)
var thumbUp by mutableIntStateOf(0) var thumbUp by mutableIntStateOf(0)
var thumbDown by mutableIntStateOf(0) var thumbDown by mutableIntStateOf(0)
var star by mutableStateOf(false) var star by mutableStateOf(false)
private var _init: Boolean = false; private var _init: Boolean = false;
var startPlaying by mutableStateOf(false)
var renderedFirst = false
var video: Video? = null
@Composable @Composable
fun Init(videoId: String) fun Init(videoId: String)
{ {
if(_init) return; if(_init) return;
val context = LocalContext.current val context = LocalContext.current
_player = remember { val v = videoId.hexToString()
ExoPlayer.Builder(context).build().apply {
val url = videoId.hexToString() remember {
viewModelScope.launch {
video = MediaManager.queryVideo(v.split("/")[0], v.split("/")[1])
RecentManager.Push(context, VideoQueryIndex(v.split("/")[0], v.split("/")[1]))
_player = ExoPlayer.Builder(context).build().apply {
val url = video?.getVideo() ?: ""
val mediaItem = MediaItem.fromUri(url) val mediaItem = MediaItem.fromUri(url)
setMediaItem(mediaItem) setMediaItem(mediaItem)
prepare() prepare()
playWhenReady = true playWhenReady = true
addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == STATE_READY) {
startPlaying = true
}
}
override fun onRenderedFirstFrame() {
super.onRenderedFirstFrame()
renderedFirst = true
}
})
}
startListen()
} }
} }

View File

@@ -62,24 +62,7 @@ class VideoScreenViewModel(application: Application) : AndroidViewModel(applicat
init { init {
viewModelScope.launch { viewModelScope.launch {
val u = userNameFlow.first()
val p = privateKeyFlow.first()
if(u=="" || p=="") return@launch
try{
if (MediaManager.token == "null")
MediaManager.token = AuthManager.fetchToken(
ApiClient.base,
u,
p
)!!
init() init()
}catch(e: Exception)
{
print(e.message)
}
} }
} }
} }