diff --git a/README.md b/README.md index 490451a..9ddef6f 100644 --- a/README.md +++ b/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] Fix the issue of freezing when entering the client without configuring the private key - [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 ### Medium Priority -- [ ] Increase minHeight for video playback -- [ ] Add top bar with title and back button in full-screen mode -- [ ] Optimize data transfer system -- [ ] Improve manga/comic page display +- [x] Increase minHeight for video playback +- [x] Add top bar with title and back button in full-screen mode +- [x] Optimize data transfer system +- [x] Improve manga/comic page display ### Future - [ ] (Prospective) Implement search functionality \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/MainActivity.kt b/app/src/main/java/com/acitelight/aether/MainActivity.kt index 8975207..abb09cb 100644 --- a/app/src/main/java/com/acitelight/aether/MainActivity.kt +++ b/app/src/main/java/com/acitelight/aether/MainActivity.kt @@ -167,9 +167,7 @@ fun AppNavigation() { } } composable(Screen.Video.route) { - CardPage(title = "Videos") { - VideoScreen(navController = navController) - } + VideoScreen(navController = navController) } composable(Screen.Comic.route) { CardPage(title = "Comic") { diff --git a/app/src/main/java/com/acitelight/aether/view/VideoScreen.kt b/app/src/main/java/com/acitelight/aether/view/VideoScreen.kt index 6363b4e..273737c 100644 --- a/app/src/main/java/com/acitelight/aether/view/VideoScreen.kt +++ b/app/src/main/java/com/acitelight/aether/view/VideoScreen.kt @@ -1,6 +1,11 @@ package com.acitelight.aether.view 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.clickable 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.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight 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.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn 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.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items 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.CardDefaults +import androidx.compose.material3.CheckboxDefaults.colors +import androidx.compose.material3.DividerDefaults 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.Tab import androidx.compose.material3.Text @@ -40,21 +61,33 @@ import coil3.compose.AsyncImage import com.acitelight.aether.model.Video import com.acitelight.aether.viewModel.VideoScreenViewModel 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.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.Brush 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.lifecycle.viewModelScope import androidx.navigation.NavHostController import coil3.request.ImageRequest +import com.acitelight.aether.CardPage import com.acitelight.aether.Global import com.acitelight.aether.Global.updateRelate import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.nio.charset.Charset +import java.security.KeyPair fun String.toHex(): String { return this.toByteArray().joinToString("") { "%02x".format(it) } @@ -76,32 +109,176 @@ fun VideoScreen( videoScreenViewModel: VideoScreenViewModel = hiltViewModel(), 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( - modifier = Modifier.fillMaxSize() - ) { - TopRow(videoScreenViewModel); - - LazyVerticalGrid( - columns = GridCells.Adaptive(160.dp), - contentPadding = PaddingValues(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) + CardPage(title = "Videos") { + Box(Modifier.fillMaxSize()) { - if (videoScreenViewModel.videoLibrary.classes.isNotEmpty()) { - items( - videoScreenViewModel.videoLibrary.classesMap[videoScreenViewModel.videoLibrary.classes[tabIndex]] - ?: mutableStateListOf() - ) { video -> - VideoCard(video, navController, videoScreenViewModel) + Column( + modifier = Modifier.fillMaxSize() + ) { + // 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( + 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, + onItemClick: (Pair) -> 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) @Composable fun TopRow(videoScreenViewModel: VideoScreenViewModel) { @@ -200,10 +377,12 @@ fun VideoCard( ) if (video.isLocal) - Card(Modifier - .align(Alignment.TopStart) - .padding(5.dp) - .widthIn(max = 46.dp)) { + Card( + Modifier + .align(Alignment.TopStart) + .padding(5.dp) + .widthIn(max = 46.dp) + ) { Box(Modifier.fillMaxWidth()) { Text( diff --git a/app/src/main/java/com/acitelight/aether/viewModel/VideoScreenViewModel.kt b/app/src/main/java/com/acitelight/aether/viewModel/VideoScreenViewModel.kt index 2271f98..ae1b0df 100644 --- a/app/src/main/java/com/acitelight/aether/viewModel/VideoScreenViewModel.kt +++ b/app/src/main/java/com/acitelight/aether/viewModel/VideoScreenViewModel.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.ViewModel @@ -44,6 +45,8 @@ class VideoScreenViewModel @Inject constructor( private val _tabIndex = mutableIntStateOf(0) val tabIndex: State = _tabIndex var imageLoader: ImageLoader? = null; + var menuVisibility = mutableStateOf(false) + var searchFilter = mutableStateOf("") suspend fun init() { fetchManager.configured.filter { it }.first()