[feat] New video UI& Basic Search feature

This commit is contained in:
acite
2025-09-16 18:23:45 +08:00
parent 829804abee
commit 2260f26d9a
4 changed files with 210 additions and 30 deletions

View File

@@ -24,14 +24,14 @@ _🚀This is the client of the multimedia server Abyss, which can also be extend
- [x] Optimize API call logic, do not create crashes - [x] Optimize API call logic, do not create crashes
- [x] Fix the issue of freezing when entering the client without configuring the private key - [x] Fix the issue of freezing when entering the client without configuring the private key
- [x] Replace Android robot icon with custom design - [x] Replace Android robot icon with custom design
- [ ] Configure server baseURL in client settings - [x] Configure server baseURL in client settings
- [ ] Implement proper access control for directory queries - [ ] Implement proper access control for directory queries
### Medium Priority ### Medium Priority
- [ ] Increase minHeight for video playback - [x] Increase minHeight for video playback
- [ ] Add top bar with title and back button in full-screen mode - [x] Add top bar with title and back button in full-screen mode
- [ ] Optimize data transfer system - [x] Optimize data transfer system
- [ ] Improve manga/comic page display - [x] Improve manga/comic page display
### Future ### Future
- [ ] (Prospective) Implement search functionality - [ ] (Prospective) Implement search functionality

View File

@@ -167,9 +167,7 @@ fun AppNavigation() {
} }
} }
composable(Screen.Video.route) { composable(Screen.Video.route) {
CardPage(title = "Videos") { VideoScreen(navController = navController)
VideoScreen(navController = navController)
}
} }
composable(Screen.Comic.route) { composable(Screen.Comic.route) {
CardPage(title = "Comic") { CardPage(title = "Comic") {

View File

@@ -1,6 +1,11 @@
package com.acitelight.aether.view package com.acitelight.aether.view
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
@@ -10,19 +15,35 @@ 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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
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.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CheckboxDefaults.colors
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -40,21 +61,33 @@ import coil3.compose.AsyncImage
import com.acitelight.aether.model.Video import com.acitelight.aether.model.Video
import com.acitelight.aether.viewModel.VideoScreenViewModel import com.acitelight.aether.viewModel.VideoScreenViewModel
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Surface
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.min
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil3.request.ImageRequest import coil3.request.ImageRequest
import com.acitelight.aether.CardPage
import com.acitelight.aether.Global import com.acitelight.aether.Global
import com.acitelight.aether.Global.updateRelate import com.acitelight.aether.Global.updateRelate
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.nio.charset.Charset import java.nio.charset.Charset
import java.security.KeyPair
fun String.toHex(): String { fun String.toHex(): String {
return this.toByteArray().joinToString("") { "%02x".format(it) } return this.toByteArray().joinToString("") { "%02x".format(it) }
@@ -76,32 +109,176 @@ fun VideoScreen(
videoScreenViewModel: VideoScreenViewModel = hiltViewModel<VideoScreenViewModel>(), videoScreenViewModel: VideoScreenViewModel = hiltViewModel<VideoScreenViewModel>(),
navController: NavHostController navController: NavHostController
) { ) {
val tabIndex by videoScreenViewModel.tabIndex; val colorScheme = MaterialTheme.colorScheme
val tabIndex by videoScreenViewModel.tabIndex
var menuVisibility by videoScreenViewModel.menuVisibility
var searchFilter by videoScreenViewModel.searchFilter
Column( CardPage(title = "Videos") {
modifier = Modifier.fillMaxSize() Box(Modifier.fillMaxSize())
) {
TopRow(videoScreenViewModel);
LazyVerticalGrid(
columns = GridCells.Adaptive(160.dp),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
)
{ {
if (videoScreenViewModel.videoLibrary.classes.isNotEmpty()) { Column(
items( modifier = Modifier.fillMaxSize()
videoScreenViewModel.videoLibrary.classesMap[videoScreenViewModel.videoLibrary.classes[tabIndex]] ) {
?: mutableStateListOf() // TopRow(videoScreenViewModel);
) { video -> Row(Modifier.padding(bottom = 4.dp))
VideoCard(video, navController, videoScreenViewModel) {
Card(
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(horizontal = 2.dp)
.size(36.dp),
onClick = {
menuVisibility = !menuVisibility
})
{
Box(Modifier.fillMaxSize())
{
Icon(
modifier = Modifier
.size(30.dp)
.align(Alignment.Center),
imageVector = Icons.Default.Menu,
contentDescription = "Catalogue"
)
}
}
Card(
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(horizontal = 2.dp)
.height(36.dp),
onClick = {
menuVisibility = !menuVisibility
})
{
Box(Modifier.fillMaxHeight())
{
Text(
text = videoScreenViewModel.videoLibrary.classes.getOrNull(tabIndex)
?: "",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier
.align(Alignment.CenterStart)
.padding(horizontal = 8.dp),
maxLines = 1
)
}
}
Box(
modifier = Modifier
.height(36.dp).widthIn(max = 240.dp)
.background(colorScheme.primary, RoundedCornerShape(8.dp))
.padding(horizontal = 6.dp),
contentAlignment = Alignment.CenterStart
) {
BasicTextField(
value = searchFilter,
onValueChange = { searchFilter = it },
textStyle = LocalTextStyle.current.copy(
fontSize = 18.sp,
color = Color.White,
textAlign = TextAlign.Start
),
singleLine = true,
modifier = Modifier
)
}
}
HorizontalDivider(Modifier.padding(bottom = 8.dp), 1.5.dp, DividerDefaults.color)
LazyVerticalGrid(
columns = GridCells.Adaptive(160.dp),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
)
{
items(
videoScreenViewModel.videoLibrary.classesMap.getOrDefault(
videoScreenViewModel.videoLibrary.classes.getOrNull(
tabIndex
), listOf()
).filter { it.video.name.contains(searchFilter) }
) { video ->
VideoCard(video, navController, videoScreenViewModel)
}
}
}
AnimatedVisibility(
visible = menuVisibility,
enter = slideInHorizontally(initialOffsetX = { full -> full }),
exit = slideOutHorizontally(targetOffsetX = { full -> full }),
modifier = Modifier.align(Alignment.CenterEnd)
) {
Card(
Modifier
.fillMaxHeight()
.width(200.dp)
.align(Alignment.CenterEnd),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = colorScheme.surface)
)
{
LazyColumn {
items(videoScreenViewModel.videoLibrary.classes) { item ->
CatalogueItemRow(
item = Pair(
videoScreenViewModel.videoLibrary.classes.indexOf(item),
item
),
onItemClick = {
menuVisibility = false
videoScreenViewModel.setTabIndex(
videoScreenViewModel.videoLibrary.classes.indexOf(
item
)
)
}
)
}
}
} }
} }
} }
} }
} }
@Composable
fun CatalogueItemRow(
item: Pair<Int, String>,
onItemClick: (Pair<Int, String>) -> Unit
) {
val colorScheme = MaterialTheme.colorScheme
Card(
modifier = Modifier
.clickable { onItemClick(item) }
.padding(4.dp)
.padding(horizontal = 4.dp)
.heightIn(min = 28.dp)
.width(200.dp),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = colorScheme.primary)
) {
Text(
text = item.second,
fontSize = 18.sp,
maxLines = 1,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TopRow(videoScreenViewModel: VideoScreenViewModel) { fun TopRow(videoScreenViewModel: VideoScreenViewModel) {
@@ -200,10 +377,12 @@ fun VideoCard(
) )
if (video.isLocal) if (video.isLocal)
Card(Modifier Card(
.align(Alignment.TopStart) Modifier
.padding(5.dp) .align(Alignment.TopStart)
.widthIn(max = 46.dp)) { .padding(5.dp)
.widthIn(max = 46.dp)
) {
Box(Modifier.fillMaxWidth()) Box(Modifier.fillMaxWidth())
{ {
Text( Text(

View File

@@ -7,6 +7,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -44,6 +45,8 @@ class VideoScreenViewModel @Inject constructor(
private val _tabIndex = mutableIntStateOf(0) private val _tabIndex = mutableIntStateOf(0)
val tabIndex: State<Int> = _tabIndex val tabIndex: State<Int> = _tabIndex
var imageLoader: ImageLoader? = null; var imageLoader: ImageLoader? = null;
var menuVisibility = mutableStateOf(false)
var searchFilter = mutableStateOf("")
suspend fun init() { suspend fun init() {
fetchManager.configured.filter { it }.first() fetchManager.configured.filter { it }.first()