Files
Aether/app/src/main/java/com/acitelight/aether/view/VideoScreen.kt
2025-08-25 04:26:34 +08:00

181 lines
6.7 KiB
Kotlin

package com.acitelight.aether.view
import android.R.id.tabs
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
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.wrapContentHeight
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
import com.acitelight.aether.model.Video
import com.acitelight.aether.service.MediaManager
import com.acitelight.aether.viewModel.VideoScreenViewModel
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.ScrollableTabRow
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.text.style.TextOverflow
import androidx.navigation.NavHostController
import coil3.request.ImageRequest
import com.acitelight.aether.Global
import kotlinx.coroutines.flow.first
import java.nio.charset.Charset
fun String.toHex(): String {
return this.toByteArray().joinToString("") { "%02x".format(it) }
}
fun String.hexToString(charset: Charset = Charsets.UTF_8): String {
require(length % 2 == 0) { "Hex string must have even length" }
val bytes = ByteArray(length / 2)
for (i in bytes.indices) {
val hexByte = substring(i * 2, i * 2 + 2)
bytes[i] = hexByte.toInt(16).toByte()
}
return String(bytes, charset)
}
@Composable
fun VideoScreen(videoScreenViewModel: VideoScreenViewModel = viewModel(), navController: NavHostController)
{
val videoList by videoScreenViewModel.videos.collectAsState()
Column(
modifier = Modifier.fillMaxSize() // 或至少 fillMaxWidth()
){
TopRow(videoScreenViewModel);
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
)
{
items(videoList) { video ->
VideoCard(video, navController, videoScreenViewModel)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopRow(videoScreenViewModel: VideoScreenViewModel)
{
val tabIndex by videoScreenViewModel.tabIndex;
val klasses by videoScreenViewModel.klasses.collectAsState();
if(klasses.isEmpty()) return
ScrollableTabRow (selectedTabIndex = tabIndex) {
klasses.forEachIndexed { index, title ->
Tab(
selected = tabIndex == index,
onClick = { videoScreenViewModel.setTabIndex(index) },
text = { Text(text = title, maxLines = 1) },
)
}
}
}
@Composable
fun VideoCard(video: Video, navController: NavHostController, videoScreenViewModel: VideoScreenViewModel) {
val videoList by videoScreenViewModel.videos.collectAsState()
Card(
shape = RoundedCornerShape(6.dp),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
onClick = {
Global.sameClassVideos = videoList
val route = "video_player_route/${ "${video.klass}/${video.id}".toHex() }"
navController.navigate(route)
}
) {
Column(
modifier = Modifier
.fillMaxWidth()
) {
Box(modifier = Modifier.fillMaxSize()){
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
.fillMaxSize(),
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 = video.video.name,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
maxLines = 2,
modifier = Modifier.padding(8.dp).background(Color.Transparent).heightIn(48.dp)
)
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text("Class", fontSize = 12.sp)
Text("${video.klass}", fontSize = 12.sp)
}
}
}
}