[feat] New video UI& Basic Search feature
This commit is contained in:
10
README.md
10
README.md
@@ -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
|
||||||
@@ -167,10 +167,8 @@ 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") {
|
||||||
ComicScreen(navController = navController)
|
ComicScreen(navController = navController)
|
||||||
|
|||||||
@@ -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,13 +109,90 @@ 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
|
||||||
|
|
||||||
|
CardPage(title = "Videos") {
|
||||||
|
Box(Modifier.fillMaxSize())
|
||||||
|
{
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
TopRow(videoScreenViewModel);
|
// TopRow(videoScreenViewModel);
|
||||||
|
Row(Modifier.padding(bottom = 4.dp))
|
||||||
|
{
|
||||||
|
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(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(160.dp),
|
columns = GridCells.Adaptive(160.dp),
|
||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(8.dp),
|
||||||
@@ -90,15 +200,82 @@ fun VideoScreen(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (videoScreenViewModel.videoLibrary.classes.isNotEmpty()) {
|
|
||||||
items(
|
items(
|
||||||
videoScreenViewModel.videoLibrary.classesMap[videoScreenViewModel.videoLibrary.classes[tabIndex]]
|
videoScreenViewModel.videoLibrary.classesMap.getOrDefault(
|
||||||
?: mutableStateListOf()
|
videoScreenViewModel.videoLibrary.classes.getOrNull(
|
||||||
|
tabIndex
|
||||||
|
), listOf()
|
||||||
|
).filter { it.video.name.contains(searchFilter) }
|
||||||
) { video ->
|
) { video ->
|
||||||
VideoCard(video, navController, videoScreenViewModel)
|
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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,10 +377,12 @@ fun VideoCard(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (video.isLocal)
|
if (video.isLocal)
|
||||||
Card(Modifier
|
Card(
|
||||||
|
Modifier
|
||||||
.align(Alignment.TopStart)
|
.align(Alignment.TopStart)
|
||||||
.padding(5.dp)
|
.padding(5.dp)
|
||||||
.widthIn(max = 46.dp)) {
|
.widthIn(max = 46.dp)
|
||||||
|
) {
|
||||||
Box(Modifier.fillMaxWidth())
|
Box(Modifier.fillMaxWidth())
|
||||||
{
|
{
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user