181 lines
6.7 KiB
Kotlin
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)
|
|
}
|
|
}
|
|
}
|
|
} |