Compare commits
21 Commits
9bad0dcbc2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9efbcdfe8a | ||
|
|
c3e0a23ed1 | ||
|
|
7be18dd517 | ||
|
|
a13ddbdd87 | ||
|
|
390094b8b0 | ||
|
|
b360724dca | ||
|
|
db8d5ef4d5 | ||
|
|
2c4d5d2366 | ||
|
|
200cf33e5a | ||
|
|
603c2c38aa | ||
|
|
7c99ea394b | ||
|
|
614a0d591d | ||
|
|
24dda0eb2c | ||
|
|
c5a5826321 | ||
|
|
ba4811e65f | ||
|
|
8b5adfd6b7 | ||
|
|
02d8d30da7 | ||
|
|
422da51a74 | ||
|
|
393419afd7 | ||
|
|
88392444a4 | ||
|
|
2166229923 |
25
README.md
25
README.md
@@ -9,6 +9,31 @@ _🚀This is the client of the multimedia server Abyss, which can also be extend
|
|||||||
<img src="aether_clip.png" width="25%" alt="Logo">
|
<img src="aether_clip.png" width="25%" alt="Logo">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## 🎯 Target
|
||||||
|
|
||||||
|
The ultimate goal of this software project is to enable anyone to easily build a smooth media library that they can fully manage and control,
|
||||||
|
contribute to with trusted individuals, and securely access from any location without worrying about unauthorized use of their data by third parties.
|
||||||
|
Undoubtedly, this is a distant goal, but in any case,
|
||||||
|
I hope this project can make a modest contribution to the advancement of cybersecurity and the protection of user privacy.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **Media Management**: Organize and serve images, videos, and live streams with structured directory support.
|
||||||
|
- **User Authentication**: Challenge-response authentication using **Ed25519** signatures. No private keys are ever transmitted.
|
||||||
|
- **Role-Based Access Control (RBAC)**: Hierarchical user privileges with configurable permissions for resources.
|
||||||
|
- **Secure Proxy**: Built-in HTTP/S proxy with end-to-end encrypted tunneling using **X25519** key exchange and **ChaCha20-Poly1305** encryption.
|
||||||
|
- **Resource-Level Permissions**: Fine-grained control over files and directories using a SQLite-based attribute system.
|
||||||
|
- **Task System**: Support for background tasks such as media uploads and processing with chunk-based integrity validation.
|
||||||
|
- **RESTful API**: Fully documented API endpoints for media access, user management, and task control.
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Backend**: ASP.NET Core 9, MVC, Dependency Injection
|
||||||
|
- **Database**: SQLite with async ORM support
|
||||||
|
- **Cryptography**: NSec.Cryptography (Ed25519, X25519), ChaCha20Poly1305
|
||||||
|
- **Media Handling**: Range requests, MIME type detection, chunked uploads
|
||||||
|
- **Security**: Rate limiting, IP binding, token expiration, secure headers
|
||||||
|
|
||||||
## Development background
|
## Development background
|
||||||
|
|
||||||
- Operating System: Voidraw OS v1.1 (based on Ubuntu) or any compatible Linux distribution.
|
- Operating System: Voidraw OS v1.1 (based on Ubuntu) or any compatible Linux distribution.
|
||||||
|
|||||||
@@ -46,12 +46,16 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(libs.persistentcookiejar)
|
||||||
|
|
||||||
implementation(libs.fetch2)
|
implementation(libs.fetch2)
|
||||||
implementation(libs.fetch2okhttp)
|
implementation(libs.fetch2okhttp)
|
||||||
|
|
||||||
implementation(libs.hilt.android)
|
implementation(libs.hilt.android)
|
||||||
implementation(libs.hilt.navigation.compose)
|
implementation(libs.hilt.navigation.compose)
|
||||||
implementation(libs.androidx.compose.material.core)
|
implementation(libs.androidx.compose.material.core)
|
||||||
|
implementation(libs.androidx.constraintlayout)
|
||||||
|
implementation(libs.androidx.compose.animation)
|
||||||
ksp(libs.hilt.android.compiler)
|
ksp(libs.hilt.android.compiler)
|
||||||
|
|
||||||
implementation(libs.androidx.room.runtime)
|
implementation(libs.androidx.room.runtime)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ object Global {
|
|||||||
var sameClassVideos: List<Video>? = null
|
var sameClassVideos: List<Video>? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var isFullScreen by mutableStateOf(false)
|
||||||
fun updateRelate(v: List<Video>, s: Video)
|
fun updateRelate(v: List<Video>, s: Video)
|
||||||
{
|
{
|
||||||
sameClassVideos = if (v.contains(s)) {
|
sameClassVideos = if (v.contains(s)) {
|
||||||
|
|||||||
@@ -3,49 +3,41 @@ package com.acitelight.aether
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import android.graphics.drawable.Icon
|
|
||||||
import android.net.http.SslCertificate.saveState
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.automirrored.filled.CompareArrows
|
import androidx.compose.material.icons.automirrored.filled.CompareArrows
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardColors
|
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SegmentedButtonDefaults.Icon
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
@@ -60,22 +52,19 @@ import androidx.navigation.compose.composable
|
|||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import com.acitelight.aether.view.ComicGridView
|
import com.acitelight.aether.view.pages.ComicGridView
|
||||||
import com.acitelight.aether.view.ComicPageView
|
import com.acitelight.aether.view.pages.ComicPageView
|
||||||
import com.acitelight.aether.view.ComicScreen
|
import com.acitelight.aether.view.pages.ComicScreen
|
||||||
import com.acitelight.aether.view.HomeScreen
|
import com.acitelight.aether.view.pages.HomeScreen
|
||||||
import com.acitelight.aether.view.MeScreen
|
import com.acitelight.aether.view.pages.LiveScreen
|
||||||
import com.acitelight.aether.view.TransmissionScreen
|
import com.acitelight.aether.view.pages.MeScreen
|
||||||
import com.acitelight.aether.view.VideoPlayer
|
import com.acitelight.aether.view.pages.TransmissionScreen
|
||||||
import com.acitelight.aether.view.VideoScreen
|
import com.acitelight.aether.view.pages.VideoPlayer
|
||||||
|
import com.acitelight.aether.view.pages.VideoScreen
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -111,22 +100,17 @@ class MainScreenActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
fun setFullScreen(view: View, isFullScreen: Boolean) {
|
||||||
fun ToggleFullScreen(isFullScreen: Boolean)
|
Global.isFullScreen = isFullScreen
|
||||||
{
|
val window = (view.context as Activity).window
|
||||||
val view = LocalView.current
|
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||||
|
|
||||||
LaunchedEffect(isFullScreen) {
|
if (isFullScreen) {
|
||||||
val window = (view.context as Activity).window
|
insetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||||
val insetsController = WindowCompat.getInsetsController(window, view)
|
insetsController.systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
if (isFullScreen) {
|
} else {
|
||||||
insetsController.hide(WindowInsetsCompat.Type.systemBars())
|
insetsController.show(WindowInsetsCompat.Type.systemBars())
|
||||||
insetsController.systemBarsBehavior =
|
|
||||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
||||||
} else {
|
|
||||||
insetsController.show(WindowInsetsCompat.Type.systemBars())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,40 +136,73 @@ fun AppNavigation() {
|
|||||||
) {
|
) {
|
||||||
BottomNavigationBar(navController = navController)
|
BottomNavigationBar(navController = navController)
|
||||||
}
|
}
|
||||||
if(shouldShowBottomBar)
|
|
||||||
ToggleFullScreen(false)
|
|
||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Screen.Me.route,
|
startDestination = Screen.Me.route,
|
||||||
modifier = if(shouldShowBottomBar)Modifier.padding(innerPadding) else Modifier.padding(0.dp)
|
modifier = if(!Global.isFullScreen) Modifier.padding(innerPadding) else Modifier.padding(0.dp)
|
||||||
) {
|
) {
|
||||||
composable(Screen.Home.route) {
|
composable(
|
||||||
|
Screen.Home.route,
|
||||||
|
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
|
||||||
|
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) }
|
||||||
|
) {
|
||||||
CardPage(title = "Home") {
|
CardPage(title = "Home") {
|
||||||
HomeScreen(navController = navController)
|
HomeScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable(Screen.Video.route) {
|
composable(Screen.Video.route,
|
||||||
|
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
|
||||||
|
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) }) {
|
||||||
VideoScreen(navController = navController)
|
VideoScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable(Screen.Comic.route) {
|
composable(Screen.Comic.route,
|
||||||
|
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
|
||||||
|
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) }) {
|
||||||
CardPage(title = "Comic") {
|
CardPage(title = "Comic") {
|
||||||
ComicScreen(navController = navController)
|
ComicScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(Screen.Transmission.route) {
|
composable(Screen.Transmission.route,
|
||||||
|
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
|
||||||
|
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) }) {
|
||||||
CardPage(title = "Tasks") {
|
CardPage(title = "Tasks") {
|
||||||
TransmissionScreen(navigator = navController)
|
TransmissionScreen(navigator = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable(Screen.Me.route) {
|
|
||||||
MeScreen();
|
composable(Screen.Live.route,
|
||||||
|
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
|
||||||
|
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) }) {
|
||||||
|
LiveScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(Screen.Me.route,
|
||||||
|
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
|
||||||
|
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) }) {
|
||||||
|
MeScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(
|
composable(
|
||||||
route = Screen.VideoPlayer.route,
|
route = Screen.VideoPlayer.route,
|
||||||
|
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
|
||||||
|
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
|
||||||
arguments = listOf(navArgument("videoId") { type = NavType.StringType })
|
arguments = listOf(navArgument("videoId") { type = NavType.StringType })
|
||||||
) {
|
) {
|
||||||
backStackEntry ->
|
backStackEntry ->
|
||||||
@@ -197,6 +214,10 @@ fun AppNavigation() {
|
|||||||
|
|
||||||
composable(
|
composable(
|
||||||
route = Screen.ComicGrid.route,
|
route = Screen.ComicGrid.route,
|
||||||
|
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
|
||||||
|
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
|
||||||
arguments = listOf(navArgument("comicId") { type = NavType.StringType })
|
arguments = listOf(navArgument("comicId") { type = NavType.StringType })
|
||||||
) {
|
) {
|
||||||
backStackEntry ->
|
backStackEntry ->
|
||||||
@@ -208,6 +229,10 @@ fun AppNavigation() {
|
|||||||
|
|
||||||
composable(
|
composable(
|
||||||
route = Screen.ComicPage.route,
|
route = Screen.ComicPage.route,
|
||||||
|
enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, animationSpec = tween(200)) },
|
||||||
|
popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
|
||||||
|
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, animationSpec = tween(200)) },
|
||||||
arguments = listOf(navArgument("comicId") { type = NavType.StringType }, navArgument("page") { type = NavType.StringType })
|
arguments = listOf(navArgument("comicId") { type = NavType.StringType }, navArgument("page") { type = NavType.StringType })
|
||||||
) {
|
) {
|
||||||
backStackEntry ->
|
backStackEntry ->
|
||||||
@@ -215,7 +240,6 @@ fun AppNavigation() {
|
|||||||
val page = backStackEntry.arguments?.getString("page")
|
val page = backStackEntry.arguments?.getString("page")
|
||||||
if (comicId != null && page != null) {
|
if (comicId != null && page != null) {
|
||||||
ComicPageView(comicId = comicId, page = page, navController = navController)
|
ComicPageView(comicId = comicId, page = page, navController = navController)
|
||||||
ToggleFullScreen(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,6 +256,7 @@ fun BottomNavigationBar(navController: NavController) {
|
|||||||
Screen.Video,
|
Screen.Video,
|
||||||
Screen.Comic,
|
Screen.Comic,
|
||||||
Screen.Transmission,
|
Screen.Transmission,
|
||||||
|
Screen.Live,
|
||||||
Screen.Me
|
Screen.Me
|
||||||
) else listOf(
|
) else listOf(
|
||||||
Screen.Video,
|
Screen.Video,
|
||||||
@@ -296,6 +321,8 @@ sealed class Screen(val route: String, val icon: ImageVector, val title: String)
|
|||||||
data object Comic : Screen("comic_route", Icons.Filled.Image, "Comic")
|
data object Comic : Screen("comic_route", Icons.Filled.Image, "Comic")
|
||||||
data object Transmission : Screen("transmission_route",
|
data object Transmission : Screen("transmission_route",
|
||||||
Icons.AutoMirrored.Filled.CompareArrows, "Transmission")
|
Icons.AutoMirrored.Filled.CompareArrows, "Transmission")
|
||||||
|
data object Live : Screen("live_route",
|
||||||
|
Icons.Filled.LiveTv, "Live")
|
||||||
data object Me : Screen("me_route", Icons.Filled.AccountCircle, "me")
|
data object Me : Screen("me_route", Icons.Filled.AccountCircle, "me")
|
||||||
data object VideoPlayer : Screen("video_player_route/{videoId}", Icons.Filled.PlayArrow, "VideoPlayer")
|
data object VideoPlayer : Screen("video_player_route/{videoId}", Icons.Filled.PlayArrow, "VideoPlayer")
|
||||||
data object ComicGrid : Screen("comic_grid_route/{comicId}", Icons.Filled.PlayArrow, "ComicGrid")
|
data object ComicGrid : Screen("comic_grid_route/{comicId}", Icons.Filled.PlayArrow, "ComicGrid")
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.acitelight.aether.model
|
package com.acitelight.aether.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class BookMark(
|
data class BookMark(
|
||||||
val name: String,
|
val name: String,
|
||||||
val page: String
|
val page: String
|
||||||
|
|||||||
@@ -4,21 +4,33 @@ import com.acitelight.aether.service.ApiClient
|
|||||||
|
|
||||||
class Comic(
|
class Comic(
|
||||||
val comic: ComicResponse,
|
val comic: ComicResponse,
|
||||||
val id: String,
|
val id: String
|
||||||
val token: String
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
fun getPage(pageNumber: Int): String
|
fun getCover(api: ApiClient): String
|
||||||
{
|
{
|
||||||
return "${ApiClient.getBase()}api/image/$id/${comic.list[pageNumber]}?token=$token"
|
if(id == "101")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
if(comic.cover != "")
|
||||||
|
{
|
||||||
|
return "${api.getBase()}api/image/$id/${comic.cover}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "${api.getBase()}api/image/$id/${comic.list[0]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPage(pageName: String): String?
|
fun getPage(pageNumber: Int, api: ApiClient): String
|
||||||
|
{
|
||||||
|
return "${api.getBase()}api/image/$id/${comic.list[pageNumber]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPage(pageName: String, api: ApiClient): String?
|
||||||
{
|
{
|
||||||
val v = comic.list.indexOf(pageName)
|
val v = comic.list.indexOf(pageName)
|
||||||
if(v >= 0)
|
if(v >= 0)
|
||||||
{
|
{
|
||||||
return getPage(v)
|
return getPage(v, api)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -33,7 +45,7 @@ class Comic(
|
|||||||
var v = comic.list.indexOf(pageName)
|
var v = comic.list.indexOf(pageName)
|
||||||
if(v >= 0)
|
if(v >= 0)
|
||||||
{
|
{
|
||||||
var r: Int = 1
|
var r = 1
|
||||||
v+=1
|
v+=1
|
||||||
while(v < comic.list.size && !comic.bookmarks.any{
|
while(v < comic.list.size && !comic.bookmarks.any{
|
||||||
x -> x.page == comic.list[v]
|
x -> x.page == comic.list[v]
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package com.acitelight.aether.model
|
package com.acitelight.aether.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class ComicResponse(
|
data class ComicResponse(
|
||||||
val comic_name: String,
|
val comic_name: String,
|
||||||
val page_count: Int,
|
val page_count: Int,
|
||||||
val bookmarks: List<BookMark>,
|
val bookmarks: List<BookMark>,
|
||||||
val list: List<String>,
|
val list: List<String>,
|
||||||
val tags: List<String>,
|
val tags: List<String>,
|
||||||
val author: String
|
val author: String,
|
||||||
|
val cover: String
|
||||||
)
|
)
|
||||||
@@ -11,31 +11,30 @@ class Video(
|
|||||||
val localBase: String,
|
val localBase: String,
|
||||||
val klass: String,
|
val klass: String,
|
||||||
val id: String,
|
val id: String,
|
||||||
val token: String,
|
|
||||||
val video: VideoResponse
|
val video: VideoResponse
|
||||||
) {
|
) {
|
||||||
fun getCover(): String {
|
fun getCover(api: ApiClient): String {
|
||||||
return if (isLocal)
|
return if (isLocal)
|
||||||
"$localBase/videos/$klass/$id/cover.jpg"
|
"$localBase/videos/$klass/$id/cover.jpg"
|
||||||
else
|
else
|
||||||
"${ApiClient.getBase()}api/video/$klass/$id/cover?token=$token"
|
"${api.getBase()}api/video/$klass/$id/cover"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getVideo(): String {
|
fun getVideo(api: ApiClient): String {
|
||||||
return if (isLocal)
|
return if (isLocal)
|
||||||
"$localBase/videos/$klass/$id/video.mp4"
|
"$localBase/videos/$klass/$id/video.mp4"
|
||||||
else
|
else
|
||||||
"${ApiClient.getBase()}api/video/$klass/$id/av?token=$token"
|
"${api.getBase()}api/video/$klass/$id/av"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSubtitle(): String {
|
fun getSubtitle(api: ApiClient): String {
|
||||||
return if (isLocal)
|
return if (isLocal)
|
||||||
"$localBase/videos/$klass/$id/subtitle.vtt"
|
"$localBase/videos/$klass/$id/subtitle.vtt"
|
||||||
else
|
else
|
||||||
"${ApiClient.getBase()}api/video/$klass/$id/subtitle?token=$token"
|
"${api.getBase()}api/video/$klass/$id/subtitle"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGallery(): List<KeyImage> {
|
fun getGallery(api: ApiClient): List<KeyImage> {
|
||||||
return if (isLocal)
|
return if (isLocal)
|
||||||
video.gallery.map {
|
video.gallery.map {
|
||||||
KeyImage(
|
KeyImage(
|
||||||
@@ -46,7 +45,7 @@ class Video(
|
|||||||
} else video.gallery.map {
|
} else video.gallery.map {
|
||||||
KeyImage(
|
KeyImage(
|
||||||
name = it,
|
name = it,
|
||||||
url = "${ApiClient.getBase()}api/video/$klass/$id/gallery/$it?token=$token",
|
url = "${api.getBase()}api/video/$klass/$id/gallery/$it",
|
||||||
key = "$klass/$id/gallery/$it"
|
key = "$klass/$id/gallery/$it"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -59,7 +58,6 @@ class Video(
|
|||||||
localBase = localBase,
|
localBase = localBase,
|
||||||
klass = klass,
|
klass = klass,
|
||||||
id = id,
|
id = id,
|
||||||
token = "",
|
|
||||||
video = video
|
video = video
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package com.acitelight.aether.service
|
package com.acitelight.aether.service
|
||||||
|
|
||||||
import com.acitelight.aether.service.AuthManager.db64
|
|
||||||
import com.acitelight.aether.service.AuthManager.signChallenge
|
|
||||||
import com.acitelight.aether.service.AuthManager.signChallengeByte
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
@@ -44,7 +41,7 @@ class AbyssStream private constructor(
|
|||||||
* Create and perform handshake on an already-connected socket.
|
* Create and perform handshake on an already-connected socket.
|
||||||
* If privateKeyRaw is provided, it must be 32 bytes.
|
* If privateKeyRaw is provided, it must be 32 bytes.
|
||||||
*/
|
*/
|
||||||
suspend fun create(socket: Socket, privateKeyRaw: ByteArray? = null): AbyssStream = withContext(Dispatchers.IO) {
|
suspend fun create(authManager: AuthManager, socket: Socket, privateKeyRaw: ByteArray? = null): AbyssStream = withContext(Dispatchers.IO) {
|
||||||
if (!socket.isConnected) throw IllegalArgumentException("socket is not connected")
|
if (!socket.isConnected) throw IllegalArgumentException("socket is not connected")
|
||||||
val inStream = socket.getInputStream()
|
val inStream = socket.getInputStream()
|
||||||
val outStream = socket.getOutputStream()
|
val outStream = socket.getOutputStream()
|
||||||
@@ -69,7 +66,7 @@ class AbyssStream private constructor(
|
|||||||
|
|
||||||
val ch = ByteArray(32)
|
val ch = ByteArray(32)
|
||||||
readExact(inStream, ch, 0, 32)
|
readExact(inStream, ch, 0, 32)
|
||||||
val signed = signChallengeByte(localPriv, ch)
|
val signed = authManager.signChallengeByte(localPriv, ch)
|
||||||
writeExact(outStream, signed, 0, signed.size)
|
writeExact(outStream, signed, 0, signed.size)
|
||||||
readExact(inStream, ch, 0, 16)
|
readExact(inStream, ch, 0, 16)
|
||||||
|
|
||||||
@@ -222,7 +219,7 @@ class AbyssStream private constructor(
|
|||||||
val header = ByteArray(4)
|
val header = ByteArray(4)
|
||||||
try {
|
try {
|
||||||
readExact(input, header, 0, 4)
|
readExact(input, header, 0, 4)
|
||||||
} catch (e: EOFException) {
|
} catch (_: EOFException) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val payloadLen = ByteBuffer.wrap(header).int and 0xffffffff.toInt()
|
val payloadLen = ByteBuffer.wrap(header).int and 0xffffffff.toInt()
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package com.acitelight.aether.service
|
package com.acitelight.aether.service
|
||||||
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.acitelight.aether.service.AuthManager.db64
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.selects.select
|
import kotlinx.coroutines.selects.select
|
||||||
@@ -17,7 +14,8 @@ import kotlin.coroutines.CoroutineContext
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class AbyssTunnelProxy @Inject constructor(
|
class AbyssTunnelProxy @Inject constructor(
|
||||||
private val settingsDataStoreManager: SettingsDataStoreManager
|
private val settingsDataStoreManager: SettingsDataStoreManager,
|
||||||
|
private val authManager: AuthManager
|
||||||
) {
|
) {
|
||||||
private val coroutineContext: CoroutineContext = Dispatchers.IO
|
private val coroutineContext: CoroutineContext = Dispatchers.IO
|
||||||
private var serverHost: String = ""
|
private var serverHost: String = ""
|
||||||
@@ -48,7 +46,7 @@ class AbyssTunnelProxy @Inject constructor(
|
|||||||
|
|
||||||
launch {
|
launch {
|
||||||
try { handleLocalConnection(client) }
|
try { handleLocalConnection(client) }
|
||||||
catch (ex: Exception) { /* ignore */ }
|
catch (_: Exception) { /* ignore */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
@@ -72,14 +70,14 @@ class AbyssTunnelProxy @Inject constructor(
|
|||||||
var abyssStream: AbyssStream? = null
|
var abyssStream: AbyssStream? = null
|
||||||
try {
|
try {
|
||||||
abyssSocket = Socket(serverHost, serverPort)
|
abyssSocket = Socket(serverHost, serverPort)
|
||||||
abyssStream = AbyssStream.create(abyssSocket, db64(settingsDataStoreManager.privateKeyFlow.first()))
|
abyssStream = AbyssStream.create(authManager, abyssSocket, authManager.db64(settingsDataStoreManager.privateKeyFlow.first()))
|
||||||
|
|
||||||
// concurrently copy in both directions
|
// concurrently copy in both directions
|
||||||
val job1 = launch { copyExactSuspend(localIn, abyssStream) } // local -> abyss
|
val job1 = launch { copyExactSuspend(localIn, abyssStream) } // local -> abyss
|
||||||
val job2 = launch { copyFromAbyssToLocal(abyssStream, localOut) } // abyss -> local
|
val job2 = launch { copyFromAbyssToLocal(abyssStream, localOut) } // abyss -> local
|
||||||
|
|
||||||
// wait for either direction to finish
|
// wait for either direction to finish
|
||||||
select<Unit> {
|
select {
|
||||||
job1.onJoin { /* completed */ }
|
job1.onJoin { /* completed */ }
|
||||||
job2.onJoin { /* completed */ }
|
job2.onJoin { /* completed */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,27 @@ import android.content.Context
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.acitelight.aether.AetherApp
|
import com.acitelight.aether.AetherApp
|
||||||
|
import com.franmontiel.persistentcookiejar.PersistentCookieJar
|
||||||
|
import com.franmontiel.persistentcookiejar.cache.SetCookieCache
|
||||||
|
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
|
||||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.ConnectionSpec
|
import okhttp3.ConnectionSpec
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.CookieJar
|
||||||
import okhttp3.EventListener
|
import okhttp3.EventListener
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.JavaNetCookieJar
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.net.CookieManager
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
@@ -24,26 +33,31 @@ import java.security.KeyStore
|
|||||||
import java.security.cert.CertificateException
|
import java.security.cert.CertificateException
|
||||||
import java.security.cert.CertificateFactory
|
import java.security.cert.CertificateFactory
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.TrustManagerFactory
|
import javax.net.ssl.TrustManagerFactory
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
|
@Singleton
|
||||||
object ApiClient {
|
class ApiClient @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) {
|
||||||
fun getBase(): String{
|
fun getBase(): String{
|
||||||
return replaceAbyssProtocol(base)
|
return replaceAbyssProtocol(base)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getDomain(): String = domain
|
||||||
|
|
||||||
private var base: String = ""
|
private var base: String = ""
|
||||||
var domain: String = ""
|
private var domain: String = ""
|
||||||
var cert: String = ""
|
private var cert: String = ""
|
||||||
private val json = Json {
|
private val json = Json {
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
}
|
}
|
||||||
|
private fun replaceAbyssProtocol(uri: String): String {
|
||||||
fun replaceAbyssProtocol(uri: String): String {
|
|
||||||
return uri.replaceFirst("^abyss://".toRegex(), "https://")
|
return uri.replaceFirst("^abyss://".toRegex(), "https://")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val dnsEventListener = object : EventListener() {
|
private val dnsEventListener = object : EventListener() {
|
||||||
override fun dnsEnd(call: okhttp3.Call, domainName: String, inetAddressList: List<InetAddress>) {
|
override fun dnsEnd(call: okhttp3.Call, domainName: String, inetAddressList: List<InetAddress>) {
|
||||||
super.dnsEnd(call, domainName, inetAddressList)
|
super.dnsEnd(call, domainName, inetAddressList)
|
||||||
@@ -51,8 +65,7 @@ object ApiClient {
|
|||||||
Log.d("OkHttp_DNS", "Domain '$domainName' resolved to IPs: [$ipAddresses]")
|
Log.d("OkHttp_DNS", "Domain '$domainName' resolved to IPs: [$ipAddresses]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private fun loadCertificateFromString(pemString: String): X509Certificate {
|
||||||
fun loadCertificateFromString(pemString: String): X509Certificate {
|
|
||||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||||
val decodedPem = pemString
|
val decodedPem = pemString
|
||||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
.replace("-----BEGIN CERTIFICATE-----", "")
|
||||||
@@ -65,8 +78,7 @@ object ApiClient {
|
|||||||
return certificateFactory.generateCertificate(inputStream) as X509Certificate
|
return certificateFactory.generateCertificate(inputStream) as X509Certificate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private fun createOkHttpClientWithDynamicCert(trustedCert: X509Certificate?): OkHttpClient {
|
||||||
fun createOkHttpClientWithDynamicCert(trustedCert: X509Certificate?): OkHttpClient {
|
|
||||||
try {
|
try {
|
||||||
val defaultTmFactory = TrustManagerFactory.getInstance(
|
val defaultTmFactory = TrustManagerFactory.getInstance(
|
||||||
TrustManagerFactory.getDefaultAlgorithm()
|
TrustManagerFactory.getDefaultAlgorithm()
|
||||||
@@ -86,7 +98,7 @@ object ApiClient {
|
|||||||
).apply {
|
).apply {
|
||||||
init(keyStore)
|
init(keyStore)
|
||||||
}
|
}
|
||||||
tmf.trustManagers.first { it is X509TrustManager } as X509TrustManager
|
tmf.trustManagers.first { i -> i is X509TrustManager } as X509TrustManager
|
||||||
}
|
}
|
||||||
|
|
||||||
val combinedTm = object : X509TrustManager {
|
val combinedTm = object : X509TrustManager {
|
||||||
@@ -156,12 +168,17 @@ object ApiClient {
|
|||||||
throw RuntimeException("Failed to create OkHttpClient with dynamic certificate", e)
|
throw RuntimeException("Failed to create OkHttpClient with dynamic certificate", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private fun createOkHttp(): OkHttpClient {
|
||||||
fun createOkHttp(): OkHttpClient {
|
|
||||||
return if (cert == "")
|
return if (cert == "")
|
||||||
if (base.startsWith("abyss://"))
|
if (base.startsWith("abyss://"))
|
||||||
OkHttpClient
|
OkHttpClient
|
||||||
.Builder()
|
.Builder()
|
||||||
|
.cookieJar(
|
||||||
|
PersistentCookieJar(
|
||||||
|
SetCookieCache(),
|
||||||
|
SharedPrefsCookiePersistor(context)
|
||||||
|
)
|
||||||
|
)
|
||||||
.proxy(
|
.proxy(
|
||||||
Proxy(
|
Proxy(
|
||||||
Proxy.Type.HTTP,
|
Proxy.Type.HTTP,
|
||||||
@@ -174,6 +191,12 @@ object ApiClient {
|
|||||||
else
|
else
|
||||||
OkHttpClient
|
OkHttpClient
|
||||||
.Builder()
|
.Builder()
|
||||||
|
.cookieJar(
|
||||||
|
PersistentCookieJar(
|
||||||
|
SetCookieCache(),
|
||||||
|
SharedPrefsCookiePersistor(context)
|
||||||
|
)
|
||||||
|
)
|
||||||
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
|
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
|
||||||
.eventListener(dnsEventListener)
|
.eventListener(dnsEventListener)
|
||||||
.build()
|
.build()
|
||||||
@@ -181,24 +204,26 @@ object ApiClient {
|
|||||||
createOkHttpClientWithDynamicCert(loadCertificateFromString(cert))
|
createOkHttpClientWithDynamicCert(loadCertificateFromString(cert))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRetrofit(): Retrofit {
|
private fun createRetrofit(): Retrofit {
|
||||||
val okHttpClient = createOkHttp()
|
client = createOkHttp()
|
||||||
val b = replaceAbyssProtocol(base)
|
val b = replaceAbyssProtocol(base)
|
||||||
|
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
.baseUrl(b)
|
.baseUrl(b)
|
||||||
.client(okHttpClient)
|
.client(client!!)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
private var client: OkHttpClient? = null
|
||||||
|
|
||||||
var api: ApiInterface? = null
|
var api: ApiInterface? = null
|
||||||
|
|
||||||
|
fun getClient() = client!!
|
||||||
|
|
||||||
suspend fun apply(context: Context, urls: String, crt: String): String? {
|
suspend fun apply(context: Context, urls: String, crt: String): String? {
|
||||||
try {
|
try {
|
||||||
|
client = createOkHttp()
|
||||||
|
|
||||||
val urlList = urls.split(";").map { it.trim() }
|
val urlList = urls.split(";").map { it.trim() }
|
||||||
|
|
||||||
var selectedUrl: String? = null
|
var selectedUrl: String? = null
|
||||||
@@ -214,21 +239,16 @@ object ApiClient {
|
|||||||
throw Exception("No reachable URL found")
|
throw Exception("No reachable URL found")
|
||||||
}
|
}
|
||||||
|
|
||||||
domain = selectedUrl.toHttpUrlOrNull()?.host ?: ""
|
domain = replaceAbyssProtocol(selectedUrl).toHttpUrlOrNull()?.host ?: ""
|
||||||
cert = crt
|
cert = crt
|
||||||
base = selectedUrl
|
base = selectedUrl
|
||||||
withContext(Dispatchers.IO)
|
withContext(Dispatchers.IO)
|
||||||
{
|
{
|
||||||
(context as AetherApp).abyssService?.proxy?.config(ApiClient.getBase().toUri().host!!, 4096)
|
(context as AetherApp).abyssService?.proxy?.config(getBase().toUri().host!!, 4096)
|
||||||
}
|
}
|
||||||
api = createRetrofit().create(ApiInterface::class.java)
|
api = createRetrofit().create(ApiInterface::class.java)
|
||||||
|
|
||||||
Log.i("Delay Analyze", "Start Abyss Hello")
|
|
||||||
val h = api!!.hello()
|
|
||||||
Log.i("Delay Analyze", "Abyss Hello: ${h.string()}")
|
|
||||||
|
|
||||||
return base
|
return base
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
api = null
|
api = null
|
||||||
base = ""
|
base = ""
|
||||||
domain = ""
|
domain = ""
|
||||||
@@ -241,7 +261,7 @@ object ApiClient {
|
|||||||
return@withContext try {
|
return@withContext try {
|
||||||
val address = InetAddress.getByName(host)
|
val address = InetAddress.getByName(host)
|
||||||
address.isReachable(200)
|
address.isReachable(200)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,49 +4,43 @@ import com.acitelight.aether.model.BookMark
|
|||||||
import com.acitelight.aether.model.ChallengeResponse
|
import com.acitelight.aether.model.ChallengeResponse
|
||||||
import com.acitelight.aether.model.ComicResponse
|
import com.acitelight.aether.model.ComicResponse
|
||||||
import com.acitelight.aether.model.VideoResponse
|
import com.acitelight.aether.model.VideoResponse
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
|
||||||
import retrofit2.http.Streaming
|
|
||||||
|
|
||||||
interface ApiInterface {
|
interface ApiInterface {
|
||||||
@GET("api/video")
|
@GET("api/video")
|
||||||
suspend fun getVideoClasses(
|
suspend fun getVideoClasses(
|
||||||
@Query("token") token: String
|
|
||||||
): List<String>
|
): List<String>
|
||||||
@GET("api/video/{klass}")
|
@GET("api/video/{klass}")
|
||||||
suspend fun queryVideoClasses(
|
suspend fun queryVideoClasses(
|
||||||
@Path("klass") klass: String,
|
@Path("klass") klass: String
|
||||||
@Query("token") token: String
|
|
||||||
): List<String>
|
): List<String>
|
||||||
@GET("api/video/{klass}/{id}")
|
@GET("api/video/{klass}/{id}")
|
||||||
suspend fun queryVideo(
|
suspend fun queryVideo(
|
||||||
@Path("klass") klass: String,
|
@Path("klass") klass: String,
|
||||||
@Path("id") id: String,
|
@Path("id") id: String
|
||||||
@Query("token") token: String
|
|
||||||
): VideoResponse
|
): VideoResponse
|
||||||
|
|
||||||
@POST("api/video/{klass}/bulkquery")
|
@POST("api/video/{klass}/bulkquery")
|
||||||
suspend fun queryVideoBulk(
|
suspend fun queryVideoBulk(
|
||||||
@Path("klass") klass: String,
|
@Path("klass") klass: String,
|
||||||
@Body() id: List<String>,
|
@Body() id: List<String>
|
||||||
@Query("token") token: String
|
|
||||||
): List<VideoResponse>
|
): List<VideoResponse>
|
||||||
|
|
||||||
@GET("api/image")
|
@GET("api/image")
|
||||||
suspend fun getComics(@Query("token") token: String): List<String>
|
suspend fun getComics(): List<String>
|
||||||
@GET("api/image/{id}")
|
@GET("api/image/{id}")
|
||||||
suspend fun queryComicInfo(@Path("id") id: String, @Query("token") token: String): ComicResponse
|
suspend fun queryComicInfo(@Path("id") id: String): ComicResponse
|
||||||
|
|
||||||
@POST("api/image/bulkquery")
|
@POST("api/image/bulkquery")
|
||||||
suspend fun queryComicInfoBulk(@Body() id: List<String>, @Query("token") token: String): List<ComicResponse>
|
suspend fun queryComicInfoBulk(@Body() id: List<String>): List<ComicResponse>
|
||||||
|
|
||||||
@POST("api/image/{id}/bookmark")
|
@POST("api/image/{id}/bookmark")
|
||||||
suspend fun postBookmark(@Path("id") id: String, @Query("token") token: String, @Body bookmark: BookMark)
|
suspend fun postBookmark(@Path("id") id: String, @Body bookmark: BookMark)
|
||||||
|
|
||||||
@GET("api/user/{user}")
|
@GET("api/user/{user}")
|
||||||
suspend fun getChallenge(
|
suspend fun getChallenge(
|
||||||
|
|||||||
@@ -18,10 +18,15 @@ import java.lang.reflect.Proxy
|
|||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.Signature
|
import java.security.Signature
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
object AuthManager {
|
@Singleton
|
||||||
|
class AuthManager @Inject constructor(
|
||||||
|
private val apiClient: ApiClient
|
||||||
|
) {
|
||||||
suspend fun fetchToken(username: String, privateKey: String): String? {
|
suspend fun fetchToken(username: String, privateKey: String): String? {
|
||||||
val api = ApiClient.api
|
val api = apiClient.api
|
||||||
var challengeBase64 = ""
|
var challengeBase64 = ""
|
||||||
|
|
||||||
try{
|
try{
|
||||||
|
|||||||
@@ -1,48 +1,37 @@
|
|||||||
package com.acitelight.aether.service
|
package com.acitelight.aether.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import com.acitelight.aether.Screen
|
|
||||||
import com.acitelight.aether.model.Video
|
import com.acitelight.aether.model.Video
|
||||||
import com.acitelight.aether.service.ApiClient.createOkHttp
|
|
||||||
import com.tonyodev.fetch2.Download
|
import com.tonyodev.fetch2.Download
|
||||||
import com.tonyodev.fetch2.Fetch
|
import com.tonyodev.fetch2.Fetch
|
||||||
import com.tonyodev.fetch2.FetchConfiguration
|
import com.tonyodev.fetch2.FetchConfiguration
|
||||||
import com.tonyodev.fetch2.FetchListener
|
import com.tonyodev.fetch2.FetchListener
|
||||||
import com.tonyodev.fetch2.Request
|
import com.tonyodev.fetch2.Request
|
||||||
import com.tonyodev.fetch2.Status
|
|
||||||
import com.tonyodev.fetch2core.Extras
|
import com.tonyodev.fetch2core.Extras
|
||||||
import com.tonyodev.fetch2okhttp.OkHttpDownloader
|
import com.tonyodev.fetch2okhttp.OkHttpDownloader
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class FetchManager @Inject constructor(
|
class FetchManager @Inject constructor(
|
||||||
@ApplicationContext private val context: Context
|
@ApplicationContext private val context: Context,
|
||||||
|
private val apiClient: ApiClient
|
||||||
) {
|
) {
|
||||||
private var fetch: Fetch? = null
|
private var fetch: Fetch? = null
|
||||||
private var listener: FetchListener? = null
|
private var listener: FetchListener? = null
|
||||||
private var client: OkHttpClient? = null
|
|
||||||
val configured = MutableStateFlow(false)
|
val configured = MutableStateFlow(false)
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
client = createOkHttp()
|
|
||||||
val fetchConfiguration = FetchConfiguration.Builder(context)
|
val fetchConfiguration = FetchConfiguration.Builder(context)
|
||||||
.setDownloadConcurrentLimit(8)
|
.setDownloadConcurrentLimit(8)
|
||||||
.setHttpDownloader(OkHttpDownloader(client))
|
.setHttpDownloader(OkHttpDownloader(apiClient.getClient()))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fetch = Fetch.Impl.getInstance(fetchConfiguration)
|
fetch = Fetch.Impl.getInstance(fetchConfiguration)
|
||||||
@@ -65,12 +54,6 @@ class FetchManager @Inject constructor(
|
|||||||
listener = null
|
listener = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// query downloads
|
|
||||||
suspend fun getAllDownloads(callback: (List<Download>) -> Unit) {
|
|
||||||
configured.filter { it }.first()
|
|
||||||
fetch?.getDownloads { list -> callback(list) } ?: callback(emptyList())
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getAllDownloadsAsync(): List<Download> {
|
suspend fun getAllDownloadsAsync(): List<Download> {
|
||||||
configured.filter { it }.first()
|
configured.filter { it }.first()
|
||||||
val completed = MutableStateFlow(false)
|
val completed = MutableStateFlow(false)
|
||||||
@@ -94,6 +77,10 @@ class FetchManager @Inject constructor(
|
|||||||
fetch?.resume(id)
|
fetch?.resume(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun retry(id: Int) {
|
||||||
|
fetch?.retry(id)
|
||||||
|
}
|
||||||
|
|
||||||
fun cancel(id: Int) {
|
fun cancel(id: Int) {
|
||||||
fetch?.cancel(id)
|
fetch?.cancel(id)
|
||||||
}
|
}
|
||||||
@@ -120,6 +107,10 @@ class FetchManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun startVideoDownload(video: Video) {
|
suspend fun startVideoDownload(video: Video) {
|
||||||
|
if(getAllDownloadsAsync().any{
|
||||||
|
it.extras.getString("class", "") == video.klass && it.extras.getString("id", "") == video.id })
|
||||||
|
return
|
||||||
|
|
||||||
makeFolder(video)
|
makeFolder(video)
|
||||||
File(
|
File(
|
||||||
context.getExternalFilesDir(null),
|
context.getExternalFilesDir(null),
|
||||||
@@ -136,7 +127,7 @@ class FetchManager @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val requests = mutableListOf(
|
val requests = mutableListOf(
|
||||||
Request(video.getVideo(), videoPath.path).apply {
|
Request(video.getVideo(apiClient), videoPath.path).apply {
|
||||||
extras = Extras(
|
extras = Extras(
|
||||||
mapOf(
|
mapOf(
|
||||||
"name" to video.video.name,
|
"name" to video.video.name,
|
||||||
@@ -146,7 +137,7 @@ class FetchManager @Inject constructor(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
Request(video.getCover(), coverPath.path).apply {
|
Request(video.getCover(apiClient), coverPath.path).apply {
|
||||||
extras = Extras(
|
extras = Extras(
|
||||||
mapOf(
|
mapOf(
|
||||||
"name" to video.video.name,
|
"name" to video.video.name,
|
||||||
@@ -156,7 +147,7 @@ class FetchManager @Inject constructor(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
Request(video.getSubtitle(), subtitlePath.path).apply {
|
Request(video.getSubtitle(apiClient), subtitlePath.path).apply {
|
||||||
extras = Extras(
|
extras = Extras(
|
||||||
mapOf(
|
mapOf(
|
||||||
"name" to video.video.name,
|
"name" to video.video.name,
|
||||||
@@ -167,7 +158,7 @@ class FetchManager @Inject constructor(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
for (p in video.getGallery()) {
|
for (p in video.getGallery(apiClient)) {
|
||||||
requests.add(
|
requests.add(
|
||||||
Request(p.url, File(
|
Request(p.url, File(
|
||||||
context.getExternalFilesDir(null),
|
context.getExternalFilesDir(null),
|
||||||
|
|||||||
@@ -16,16 +16,15 @@ import javax.inject.Singleton
|
|||||||
@Singleton
|
@Singleton
|
||||||
class MediaManager @Inject constructor(
|
class MediaManager @Inject constructor(
|
||||||
val fetchManager: FetchManager,
|
val fetchManager: FetchManager,
|
||||||
@ApplicationContext val context: Context
|
@ApplicationContext val context: Context,
|
||||||
|
private val apiClient: ApiClient
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var token: String = "null"
|
|
||||||
|
|
||||||
suspend fun listVideoKlasses(): List<String>
|
suspend fun listVideoKlasses(): List<String>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
val j = ApiClient.api!!.getVideoClasses(token)
|
val j = apiClient.api!!.getVideoClasses()
|
||||||
return j.toList()
|
return j.toList()
|
||||||
}catch(_: Exception)
|
}catch(_: Exception)
|
||||||
{
|
{
|
||||||
@@ -37,7 +36,7 @@ class MediaManager @Inject constructor(
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
val j = ApiClient.api!!.queryVideoClasses(klass, token)
|
val j = apiClient.api!!.queryVideoClasses(klass)
|
||||||
return j.toList()
|
return j.toList()
|
||||||
}catch(_: Exception)
|
}catch(_: Exception)
|
||||||
{
|
{
|
||||||
@@ -57,8 +56,8 @@ class MediaManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val j = ApiClient.api!!.queryVideo(klass, id, token)
|
val j = apiClient.api!!.queryVideo(klass, id)
|
||||||
return Video(klass = klass, id = id, token=token, isLocal = false, localBase = "", video = j)
|
return Video(klass = klass, id = id, isLocal = false, localBase = "", video = j)
|
||||||
}catch (_: Exception)
|
}catch (_: Exception)
|
||||||
{
|
{
|
||||||
return null
|
return null
|
||||||
@@ -83,8 +82,8 @@ class MediaManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val j = ApiClient.api!!.queryVideo(klass, id, token)
|
val j = apiClient.api!!.queryVideo(klass, id)
|
||||||
return Video(klass = klass, id = id, token=token, isLocal = false, localBase = "", video = j)
|
return Video(klass = klass, id = id, isLocal = false, localBase = "", video = j)
|
||||||
}catch (_: Exception)
|
}catch (_: Exception)
|
||||||
{
|
{
|
||||||
return null
|
return null
|
||||||
@@ -133,12 +132,11 @@ class MediaManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val remoteVideos = if (remoteIds.isNotEmpty()) {
|
val remoteVideos = if (remoteIds.isNotEmpty()) {
|
||||||
val j = ApiClient.api!!.queryVideoBulk(klass, remoteIds, token)
|
val j = apiClient.api!!.queryVideoBulk(klass, remoteIds)
|
||||||
j.zip(remoteIds).map {
|
j.zip(remoteIds).map {
|
||||||
Video(
|
Video(
|
||||||
klass = klass,
|
klass = klass,
|
||||||
id = it.second,
|
id = it.second,
|
||||||
token = token,
|
|
||||||
isLocal = false,
|
isLocal = false,
|
||||||
localBase = "",
|
localBase = "",
|
||||||
video = it.first
|
video = it.first
|
||||||
@@ -157,8 +155,8 @@ class MediaManager @Inject constructor(
|
|||||||
suspend fun listComics() : List<String>
|
suspend fun listComics() : List<String>
|
||||||
{
|
{
|
||||||
try{
|
try{
|
||||||
val j = ApiClient.api!!.getComics(token)
|
val j = apiClient.api!!.getComics()
|
||||||
return j
|
return j.sorted()
|
||||||
}catch (_: Exception)
|
}catch (_: Exception)
|
||||||
{
|
{
|
||||||
return listOf()
|
return listOf()
|
||||||
@@ -168,8 +166,8 @@ class MediaManager @Inject constructor(
|
|||||||
suspend fun queryComicInfoSingle(id: String) : Comic?
|
suspend fun queryComicInfoSingle(id: String) : Comic?
|
||||||
{
|
{
|
||||||
try{
|
try{
|
||||||
val j = ApiClient.api!!.queryComicInfo(id, token)
|
val j = apiClient.api!!.queryComicInfo(id)
|
||||||
return Comic(id = id, comic = j, token = token)
|
return Comic(id = id, comic = j)
|
||||||
}catch (_: Exception)
|
}catch (_: Exception)
|
||||||
{
|
{
|
||||||
return null
|
return null
|
||||||
@@ -179,8 +177,8 @@ class MediaManager @Inject constructor(
|
|||||||
suspend fun queryComicInfoBulk(id: List<String>) : List<Comic>?
|
suspend fun queryComicInfoBulk(id: List<String>) : List<Comic>?
|
||||||
{
|
{
|
||||||
try{
|
try{
|
||||||
val j = ApiClient.api!!.queryComicInfoBulk(id, token)
|
val j = apiClient.api!!.queryComicInfoBulk(id)
|
||||||
return j.zip(id).map { Comic(id = it.second, comic = it.first, token = token) }
|
return j.zip(id).map { Comic(id = it.second, comic = it.first) }
|
||||||
}catch (_: Exception)
|
}catch (_: Exception)
|
||||||
{
|
{
|
||||||
return null
|
return null
|
||||||
@@ -190,7 +188,7 @@ class MediaManager @Inject constructor(
|
|||||||
suspend fun postBookmark(id: String, bookMark: BookMark): Boolean
|
suspend fun postBookmark(id: String, bookMark: BookMark): Boolean
|
||||||
{
|
{
|
||||||
try{
|
try{
|
||||||
ApiClient.api!!.postBookmark(id, token, bookMark)
|
apiClient.api!!.postBookmark(id, bookMark)
|
||||||
return true
|
return true
|
||||||
}catch (_: Exception)
|
}catch (_: Exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,325 +0,0 @@
|
|||||||
package com.acitelight.aether.view
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
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.widthIn
|
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import coil3.compose.AsyncImage
|
|
||||||
import coil3.request.ImageRequest
|
|
||||||
import com.acitelight.aether.ToggleFullScreen
|
|
||||||
import com.acitelight.aether.model.BookMark
|
|
||||||
import com.acitelight.aether.model.Comic
|
|
||||||
import com.acitelight.aether.viewModel.ComicGridViewModel
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ComicGridView(
|
|
||||||
comicId: String,
|
|
||||||
navController: NavHostController,
|
|
||||||
comicGridViewModel: ComicGridViewModel = hiltViewModel<ComicGridViewModel>()
|
|
||||||
) {
|
|
||||||
comicGridViewModel.resolve(comicId.hexToString())
|
|
||||||
comicGridViewModel.updateProcess(comicId.hexToString()) {}
|
|
||||||
ToggleFullScreen(false)
|
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
val comic by comicGridViewModel.comic
|
|
||||||
val record by comicGridViewModel.record
|
|
||||||
|
|
||||||
if (comic != null) {
|
|
||||||
Column {
|
|
||||||
Card(
|
|
||||||
Modifier
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(top = 36.dp)
|
|
||||||
.heightIn(min = 42.dp),
|
|
||||||
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.heightIn(min = 42.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Text(
|
|
||||||
text = comic!!.comic.comic_name,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier.padding(4.dp).align(Alignment.CenterStart)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Card(
|
|
||||||
Modifier
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(top = 4.dp)
|
|
||||||
.heightIn(min = 42.dp),
|
|
||||||
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.heightIn(min = 42.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Text(
|
|
||||||
text = comic!!.comic.author,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(4.dp)
|
|
||||||
.align(Alignment.CenterStart)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Card(
|
|
||||||
Modifier
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(top = 4.dp)
|
|
||||||
.heightIn(min = 42.dp),
|
|
||||||
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.heightIn(min = 42.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Text(
|
|
||||||
text = "Tags : ${comic!!.comic.tags.joinToString(", ")}",
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 5,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(4.dp)
|
|
||||||
.align(Alignment.CenterStart)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f)
|
|
||||||
.padding(top = 6.dp)
|
|
||||||
.clip(RoundedCornerShape(6.dp))
|
|
||||||
)
|
|
||||||
{
|
|
||||||
items(comicGridViewModel.chapterList)
|
|
||||||
{ c ->
|
|
||||||
ChapterCard(comic!!, navController, c, comicGridViewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Card(
|
|
||||||
Modifier
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(top = 6.dp)
|
|
||||||
.padding(bottom = 20.dp)
|
|
||||||
.height(42.dp)
|
|
||||||
.clickable {
|
|
||||||
comicGridViewModel.updateProcess(comicId.hexToString())
|
|
||||||
{
|
|
||||||
if (record != null) {
|
|
||||||
val k = comic!!.getPageChapterIndex(record!!.position)
|
|
||||||
val route = "comic_page_route/${comic!!.id.toHex()}/${
|
|
||||||
record!!.position
|
|
||||||
}"
|
|
||||||
navController.navigate(route)
|
|
||||||
} else {
|
|
||||||
val route = "comic_page_route/${comic!!.id.toHex()}/${0}"
|
|
||||||
navController.navigate(route)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Box(Modifier.fillMaxSize()) {
|
|
||||||
Row(
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
) {
|
|
||||||
if (record != null) {
|
|
||||||
val k = comic!!.getPageChapterIndex(record!!.position)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "Last Read Position: ${k.first.name} ${k.second}/${
|
|
||||||
comic!!.getChapterLength(
|
|
||||||
k.first.page
|
|
||||||
)
|
|
||||||
}",
|
|
||||||
fontSize = 20.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(4.dp)
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = "Read from scratch",
|
|
||||||
fontSize = 20.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(4.dp)
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ChapterCard(
|
|
||||||
comic: Comic,
|
|
||||||
navController: NavHostController,
|
|
||||||
chapter: BookMark,
|
|
||||||
comicGridViewModel: ComicGridViewModel = hiltViewModel<ComicGridViewModel>()
|
|
||||||
) {
|
|
||||||
val c = chapter
|
|
||||||
val iv = comic.getPageIndex(c.page)
|
|
||||||
|
|
||||||
Card(
|
|
||||||
shape = RoundedCornerShape(6.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(vertical = 6.dp),
|
|
||||||
onClick = {
|
|
||||||
val route = "comic_page_route/${comic.id.toHex()}/${comic.getPageIndex(chapter.page)}"
|
|
||||||
navController.navigate(route)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Column(Modifier.fillMaxWidth())
|
|
||||||
{
|
|
||||||
Row(Modifier.padding(6.dp))
|
|
||||||
{
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.heightIn(max = 170.dp)
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.background(Color(0x44FFFFFF))
|
|
||||||
)
|
|
||||||
{
|
|
||||||
AsyncImage(
|
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
|
||||||
.data(comic.getPage(c.page))
|
|
||||||
.memoryCacheKey("${comic.id}/${c.page}")
|
|
||||||
.diskCacheKey("${comic.id}/${c.page}")
|
|
||||||
.build(),
|
|
||||||
contentDescription = null,
|
|
||||||
imageLoader = comicGridViewModel.imageLoader!!,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.widthIn(max = 170.dp),
|
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(modifier = Modifier.padding(horizontal = 12.dp)) {
|
|
||||||
Text(
|
|
||||||
text = chapter.name,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 5,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.background(Color.Transparent)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "${comic.getChapterLength(chapter.page)} Pages",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.background(Color.Transparent)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val r = comic.comic.list.subList(iv, iv + comic.getChapterLength(c.page))
|
|
||||||
LazyRow(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(6.dp)
|
|
||||||
) {
|
|
||||||
items(r)
|
|
||||||
{ r ->
|
|
||||||
Card(
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
.height(140.dp)
|
|
||||||
.padding(horizontal = 6.dp),
|
|
||||||
onClick = {
|
|
||||||
val route =
|
|
||||||
"comic_page_route/${"${comic.id}".toHex()}/${comic.getPageIndex(r)}"
|
|
||||||
navController.navigate(route)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
|
||||||
.data(comic.getPage(r))
|
|
||||||
.memoryCacheKey("${comic.id}/${r}")
|
|
||||||
.diskCacheKey("${comic.id}/${r}")
|
|
||||||
.build(),
|
|
||||||
contentDescription = null,
|
|
||||||
imageLoader = comicGridViewModel.imageLoader!!,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.clip(RoundedCornerShape(12.dp)),
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
package com.acitelight.aether.view
|
|
||||||
|
|
||||||
import android.nfc.Tag
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
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.widthIn
|
|
||||||
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.lazy.grid.rememberLazyGridState
|
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.items
|
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Tab
|
|
||||||
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.viewModel.VideoScreenViewModel
|
|
||||||
import androidx.compose.material3.ScrollableTabRow
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.layout.Layout
|
|
||||||
import androidx.compose.ui.layout.Placeable
|
|
||||||
import androidx.compose.ui.modifier.modifierLocalOf
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import coil3.request.ImageRequest
|
|
||||||
import com.acitelight.aether.Global
|
|
||||||
import com.acitelight.aether.model.Comic
|
|
||||||
import com.acitelight.aether.viewModel.ComicScreenViewModel
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun VariableGrid(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
rowHeight: Dp,
|
|
||||||
horizontalSpacing: Dp = 4.dp,
|
|
||||||
verticalSpacing: Dp = 4.dp,
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
|
|
||||||
Layout(
|
|
||||||
modifier = modifier
|
|
||||||
.verticalScroll(scrollState),
|
|
||||||
content = content
|
|
||||||
) { measurables, constraints ->
|
|
||||||
|
|
||||||
val rowHeightPx = rowHeight.roundToPx()
|
|
||||||
val hSpacePx = horizontalSpacing.roundToPx()
|
|
||||||
val vSpacePx = verticalSpacing.roundToPx()
|
|
||||||
|
|
||||||
val placeables = measurables.map { measurable ->
|
|
||||||
measurable.measure(
|
|
||||||
constraints.copy(
|
|
||||||
minWidth = 0,
|
|
||||||
minHeight = rowHeightPx,
|
|
||||||
maxHeight = rowHeightPx
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val rows = mutableListOf<List<Placeable>>()
|
|
||||||
var currentRow = mutableListOf<Placeable>()
|
|
||||||
var currentWidth = 0
|
|
||||||
val maxWidth = constraints.maxWidth
|
|
||||||
|
|
||||||
for (placeable in placeables) {
|
|
||||||
if (currentRow.isNotEmpty() && currentWidth + placeable.width + hSpacePx > maxWidth) {
|
|
||||||
rows.add(currentRow)
|
|
||||||
currentRow = mutableListOf()
|
|
||||||
currentWidth = 0
|
|
||||||
}
|
|
||||||
currentRow.add(placeable)
|
|
||||||
currentWidth += placeable.width + hSpacePx
|
|
||||||
}
|
|
||||||
if (currentRow.isNotEmpty()) {
|
|
||||||
rows.add(currentRow)
|
|
||||||
}
|
|
||||||
|
|
||||||
val layoutHeight = if (rows.isEmpty()) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
rows.size * rowHeightPx + (rows.size - 1) * vSpacePx
|
|
||||||
}
|
|
||||||
|
|
||||||
layout(
|
|
||||||
width = constraints.maxWidth.coerceAtLeast(constraints.minWidth),
|
|
||||||
height = layoutHeight.coerceAtLeast(constraints.minHeight)
|
|
||||||
) {
|
|
||||||
var y = 0
|
|
||||||
for (row in rows) {
|
|
||||||
var x = 0
|
|
||||||
for (placeable in row) {
|
|
||||||
placeable.placeRelative(x, y)
|
|
||||||
x += placeable.width + hSpacePx
|
|
||||||
}
|
|
||||||
y += rowHeightPx + vSpacePx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ComicScreen(
|
|
||||||
navController: NavHostController,
|
|
||||||
comicScreenViewModel: ComicScreenViewModel = hiltViewModel<ComicScreenViewModel>()
|
|
||||||
) {
|
|
||||||
val included = comicScreenViewModel.included
|
|
||||||
val state = rememberLazyStaggeredGridState()
|
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
Column {
|
|
||||||
|
|
||||||
VariableGrid(
|
|
||||||
modifier = Modifier
|
|
||||||
.heightIn(max = 120.dp)
|
|
||||||
.padding(8.dp),
|
|
||||||
rowHeight = 32.dp
|
|
||||||
)
|
|
||||||
{
|
|
||||||
for (i in comicScreenViewModel.tags) {
|
|
||||||
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.background(
|
|
||||||
if (included.contains(i)) Color.Green.copy(alpha = 0.65f) else colorScheme.primary,
|
|
||||||
shape = RoundedCornerShape(4.dp)
|
|
||||||
)
|
|
||||||
.height(32.dp).widthIn(max = 72.dp)
|
|
||||||
.clickable {
|
|
||||||
if (included.contains(i))
|
|
||||||
included.remove(i)
|
|
||||||
else
|
|
||||||
included.add(i)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = i,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(2.dp)
|
|
||||||
.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider(thickness = 1.5.dp)
|
|
||||||
|
|
||||||
LazyVerticalStaggeredGrid(
|
|
||||||
columns = StaggeredGridCells.Adaptive(136.dp),
|
|
||||||
contentPadding = PaddingValues(8.dp),
|
|
||||||
verticalItemSpacing = 8.dp,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
state = state,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
items = comicScreenViewModel.comics.filter { x ->
|
|
||||||
included.all { y -> y in x.comic.tags } || included.isEmpty()
|
|
||||||
},
|
|
||||||
key = { it.id }
|
|
||||||
) { comic ->
|
|
||||||
Box(modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
) {
|
|
||||||
ComicCard(comic, navController, comicScreenViewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ComicCard(
|
|
||||||
comic: Comic,
|
|
||||||
navController: NavHostController,
|
|
||||||
comicScreenViewModel: ComicScreenViewModel
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
shape = RoundedCornerShape(6.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight(),
|
|
||||||
onClick = {
|
|
||||||
val route = "comic_grid_route/${"${comic.id}".toHex()}"
|
|
||||||
navController.navigate(route)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
|
||||||
AsyncImage(
|
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
|
||||||
.data(comic.getPage(0))
|
|
||||||
.memoryCacheKey("${comic.id}/${0}")
|
|
||||||
.diskCacheKey("${comic.id}/${0}")
|
|
||||||
.build(),
|
|
||||||
contentDescription = null,
|
|
||||||
imageLoader = comicScreenViewModel.imageLoader!!,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize(),
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(24.dp)
|
|
||||||
.background(
|
|
||||||
brush = Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.Transparent,
|
|
||||||
Color.Black.copy(alpha = 0.45f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.padding(2.dp),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
text = "${comic.comic.list.size} Pages",
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = Color.White
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = comic.comic.comic_name,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 2,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(4.dp)
|
|
||||||
.background(Color.Transparent)
|
|
||||||
.heightIn(max = 48.dp)
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = "Id: ${comic.id}",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
maxLines = 2,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(bottom = 4.dp).padding(horizontal = 4.dp)
|
|
||||||
.background(Color.Transparent)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
package com.acitelight.aether.view
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.ActivityInfo
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.media.AudioManager
|
|
||||||
import android.view.View
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.slideInVertically
|
|
||||||
import androidx.compose.animation.slideOutVertically
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.material.icons.filled.Pause
|
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Slider
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
|
||||||
import androidx.media3.ui.PlayerView
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import com.acitelight.aether.viewModel.VideoPlayerViewModel
|
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
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.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.wrapContentWidth
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
|
||||||
import androidx.compose.material.icons.filled.Brightness4
|
|
||||||
import androidx.compose.material.icons.filled.FastForward
|
|
||||||
import androidx.compose.material.icons.filled.Info
|
|
||||||
import androidx.compose.material.icons.filled.Lock
|
|
||||||
import androidx.compose.material.icons.filled.LockOpen
|
|
||||||
import androidx.compose.material.icons.filled.Share
|
|
||||||
import androidx.compose.material.icons.filled.Star
|
|
||||||
import androidx.compose.material.icons.filled.ThumbDown
|
|
||||||
import androidx.compose.material.icons.filled.ThumbUp
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardColors
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.DividerDefaults
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.SliderDefaults
|
|
||||||
import androidx.compose.material3.Tab
|
|
||||||
import androidx.compose.material3.TabRow
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Shadow
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.TextUnit
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.media3.common.text.Cue
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
|
||||||
import coil3.ImageLoader
|
|
||||||
import coil3.compose.AsyncImage
|
|
||||||
import coil3.request.ImageRequest
|
|
||||||
import com.acitelight.aether.Global
|
|
||||||
import com.acitelight.aether.ToggleFullScreen
|
|
||||||
import com.acitelight.aether.model.KeyImage
|
|
||||||
import com.acitelight.aether.model.Video
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.exp
|
|
||||||
import kotlin.math.ln
|
|
||||||
import kotlin.math.pow
|
|
||||||
|
|
||||||
fun formatTime(ms: Long): String {
|
|
||||||
if (ms <= 0) return "00:00:00"
|
|
||||||
val totalSeconds = ms / 1000
|
|
||||||
val hours = totalSeconds / 3600
|
|
||||||
val minutes = (totalSeconds % 3600) / 60
|
|
||||||
val seconds = totalSeconds % 60
|
|
||||||
return String.format("%02d:%02d:%02d", hours, minutes, seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun moveBrit(db: Float, activity: Activity, videoPlayerViewModel: VideoPlayerViewModel) {
|
|
||||||
val attr = activity.window.attributes
|
|
||||||
|
|
||||||
val britUi = (videoPlayerViewModel.brit - db * 0.002f).coerceIn(0f, 1f)
|
|
||||||
videoPlayerViewModel.brit = britUi
|
|
||||||
|
|
||||||
val gamma = 2.2f
|
|
||||||
val britSystem = britUi.pow(gamma).coerceIn(0.001f, 1f)
|
|
||||||
|
|
||||||
attr.screenBrightness = britSystem
|
|
||||||
activity.window.attributes = attr
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun VideoPlayer(
|
|
||||||
videoPlayerViewModel: VideoPlayerViewModel = hiltViewModel<VideoPlayerViewModel>(),
|
|
||||||
videoId: String,
|
|
||||||
navController: NavHostController
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val activity = (context as? Activity)!!
|
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
videoPlayerViewModel.init(videoId)
|
|
||||||
|
|
||||||
activity.requestedOrientation =
|
|
||||||
if(videoPlayerViewModel.isLandscape)
|
|
||||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
|
||||||
else
|
|
||||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
||||||
|
|
||||||
if (videoPlayerViewModel.startPlaying) {
|
|
||||||
if (videoPlayerViewModel.isLandscape) {
|
|
||||||
VideoPlayerLandscape(videoPlayerViewModel)
|
|
||||||
} else {
|
|
||||||
VideoPlayerPortal(videoPlayerViewModel, navController)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SliderColors
|
||||||
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -63,9 +64,13 @@ fun BiliMiniSlider(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
value: Float,
|
value: Float,
|
||||||
onValueChange: (Float) -> Unit,
|
onValueChange: (Float) -> Unit,
|
||||||
valueRange: ClosedFloatingPointRange<Float> = 0f..1f
|
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
|
||||||
|
colors: SliderColors = SliderDefaults.colors(
|
||||||
|
thumbColor = Color(0xFFFFFFFF),
|
||||||
|
activeTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
|
||||||
val trackHeight = 3.dp
|
val trackHeight = 3.dp
|
||||||
|
|
||||||
Slider(
|
Slider(
|
||||||
@@ -73,11 +78,8 @@ fun BiliMiniSlider(
|
|||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
valueRange = valueRange,
|
valueRange = valueRange,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
colors = SliderDefaults.colors(
|
colors = colors,
|
||||||
thumbColor = Color(0xFFFFFFFF),
|
enabled = false,
|
||||||
activeTrackColor = colorScheme.primary,
|
|
||||||
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
|
|
||||||
),
|
|
||||||
thumb = {
|
thumb = {
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -86,14 +88,14 @@ fun BiliMiniSlider(
|
|||||||
Modifier
|
Modifier
|
||||||
.height(trackHeight)
|
.height(trackHeight)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color.LightGray.copy(alpha = 0.3f), RoundedCornerShape(50))
|
.background(colors.inactiveTrackColor, RoundedCornerShape(50))
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.CenterStart)
|
.align(Alignment.CenterStart)
|
||||||
.fillMaxWidth(value)
|
.fillMaxWidth(value)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.background(colorScheme.primary, RoundedCornerShape(50))
|
.background(colors.activeTrackColor, RoundedCornerShape(50))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import com.acitelight.aether.model.Comic
|
||||||
|
import com.acitelight.aether.view.pages.toHex
|
||||||
|
import com.acitelight.aether.viewModel.ComicScreenViewModel
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ComicCard(
|
||||||
|
comic: Comic,
|
||||||
|
navController: NavHostController,
|
||||||
|
comicScreenViewModel: ComicScreenViewModel
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface.copy(0.65f)),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
onClick = {
|
||||||
|
val route = "comic_grid_route/${comic.id.toHex()}"
|
||||||
|
navController.navigate(route)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(comic.getCover(comicScreenViewModel.apiClient))
|
||||||
|
.memoryCacheKey("${comic.id}/cover")
|
||||||
|
.diskCacheKey("${comic.id}/cover")
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
imageLoader = comicScreenViewModel.imageLoader!!,
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(24.dp)
|
||||||
|
.background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.45f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(2.dp),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
text = "${comic.comic.list.size} Pages",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.White,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = comic.comic.comic_name,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 2,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.heightIn(min = 14.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Id: ${comic.id}",
|
||||||
|
fontSize = 10.sp,
|
||||||
|
lineHeight = 12.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.padding(4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
import android.R
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -20,10 +18,8 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.drawBehind
|
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.PaintingStyle.Companion.Stroke
|
|
||||||
import androidx.compose.ui.graphics.drawOutline
|
import androidx.compose.ui.graphics.drawOutline
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
@@ -35,12 +31,13 @@ import coil3.ImageLoader
|
|||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import com.acitelight.aether.model.Video
|
import com.acitelight.aether.model.Video
|
||||||
|
import com.acitelight.aether.service.ApiClient
|
||||||
|
import com.acitelight.aether.view.pages.formatTime
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MiniPlaylistCard(modifier: Modifier, video: Video, imageLoader: ImageLoader, selected: Boolean, onClick: () -> Unit) {
|
fun MiniPlaylistCard(modifier: Modifier, video: Video, imageLoader: ImageLoader, selected: Boolean, apiClient: ApiClient, onClick: () -> Unit) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.height(80.dp)
|
.height(80.dp)
|
||||||
@@ -58,7 +55,7 @@ fun MiniPlaylistCard(modifier: Modifier, video: Video, imageLoader: ImageLoader,
|
|||||||
{
|
{
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
.data(video.getCover())
|
.data(video.getCover(apiClient))
|
||||||
.memoryCacheKey("${video.klass}/${video.id}/cover")
|
.memoryCacheKey("${video.klass}/${video.id}/cover")
|
||||||
.diskCacheKey("${video.klass}/${video.id}/cover")
|
.diskCacheKey("${video.klass}/${video.id}/cover")
|
||||||
.listener(
|
.listener(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -28,10 +28,12 @@ import coil3.ImageLoader
|
|||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import com.acitelight.aether.model.Video
|
import com.acitelight.aether.model.Video
|
||||||
|
import com.acitelight.aether.service.ApiClient
|
||||||
|
import com.acitelight.aether.view.pages.formatTime
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MiniVideoCard(modifier: Modifier, video: Video, onClick: () -> Unit, imageLoader: ImageLoader) {
|
fun MiniVideoCard(modifier: Modifier, video: Video, imageLoader: ImageLoader, apiClient: ApiClient, onClick: () -> Unit) {
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.height(80.dp)
|
.height(80.dp)
|
||||||
@@ -49,7 +51,7 @@ fun MiniVideoCard(modifier: Modifier, video: Video, onClick: () -> Unit, imageLo
|
|||||||
{
|
{
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
.data(video.getCover())
|
.data(video.getCover(apiClient))
|
||||||
.memoryCacheKey("${video.klass}/${video.id}/cover")
|
.memoryCacheKey("${video.klass}/${video.id}/cover")
|
||||||
.diskCacheKey("${video.klass}/${video.id}/cover")
|
.diskCacheKey("${video.klass}/${video.id}/cover")
|
||||||
.listener(
|
.listener(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
@@ -51,11 +53,13 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
|
import com.acitelight.aether.view.pages.formatTime
|
||||||
|
import com.acitelight.aether.view.pages.moveBrit
|
||||||
import com.acitelight.aether.viewModel.VideoPlayerViewModel
|
import com.acitelight.aether.viewModel.VideoPlayerViewModel
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
|
||||||
@androidx.annotation.OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel, cover: Float) {
|
fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewModel, cover: Float) {
|
||||||
val exoPlayer: ExoPlayer = videoPlayerViewModel.player!!
|
val exoPlayer: ExoPlayer = videoPlayerViewModel.player!!
|
||||||
@@ -173,7 +177,7 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = videoPlayerViewModel.isLongPressing,
|
visible = videoPlayerViewModel.isLongPressing,
|
||||||
enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }),
|
enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }),
|
||||||
exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight }),
|
exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight }),
|
||||||
@@ -212,7 +216,7 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = videoPlayerViewModel.draggingPurpose == 0,
|
visible = videoPlayerViewModel.draggingPurpose == 0,
|
||||||
enter = fadeIn(
|
enter = fadeIn(
|
||||||
initialAlpha = 0f,
|
initialAlpha = 0f,
|
||||||
@@ -235,7 +239,7 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = videoPlayerViewModel.draggingPurpose == 2,
|
visible = videoPlayerViewModel.draggingPurpose == 2,
|
||||||
enter = fadeIn(
|
enter = fadeIn(
|
||||||
initialAlpha = 0f,
|
initialAlpha = 0f,
|
||||||
@@ -270,7 +274,7 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = videoPlayerViewModel.draggingPurpose == 1,
|
visible = videoPlayerViewModel.draggingPurpose == 1,
|
||||||
enter = fadeIn(
|
enter = fadeIn(
|
||||||
initialAlpha = 0f,
|
initialAlpha = 0f,
|
||||||
@@ -310,7 +314,7 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
|
|||||||
.background(MaterialTheme.colorScheme.primary.copy(cover))
|
.background(MaterialTheme.colorScheme.primary.copy(cover))
|
||||||
.fillMaxSize())
|
.fillMaxSize())
|
||||||
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = (!videoPlayerViewModel.planeVisibility) || videoPlayerViewModel.locked,
|
visible = (!videoPlayerViewModel.planeVisibility) || videoPlayerViewModel.locked,
|
||||||
enter = fadeIn(
|
enter = fadeIn(
|
||||||
initialAlpha = 0f,
|
initialAlpha = 0f,
|
||||||
@@ -331,7 +335,7 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = videoPlayerViewModel.planeVisibility && (!videoPlayerViewModel.locked),
|
visible = videoPlayerViewModel.planeVisibility && (!videoPlayerViewModel.locked),
|
||||||
enter = fadeIn(
|
enter = fadeIn(
|
||||||
initialAlpha = 0f,
|
initialAlpha = 0f,
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
|
import android.text.Layout
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -42,9 +43,9 @@ fun SubtitleOverlay(
|
|||||||
if (raw.isEmpty()) return
|
if (raw.isEmpty()) return
|
||||||
|
|
||||||
val textAlign = when (cues.firstOrNull()?.textAlignment) {
|
val textAlign = when (cues.firstOrNull()?.textAlignment) {
|
||||||
android.text.Layout.Alignment.ALIGN_CENTER -> TextAlign.Center
|
Layout.Alignment.ALIGN_CENTER -> TextAlign.Center
|
||||||
android.text.Layout.Alignment.ALIGN_OPPOSITE -> TextAlign.End
|
Layout.Alignment.ALIGN_OPPOSITE -> TextAlign.End
|
||||||
android.text.Layout.Alignment.ALIGN_NORMAL -> TextAlign.Start
|
Layout.Alignment.ALIGN_NORMAL -> TextAlign.Start
|
||||||
else -> TextAlign.Center
|
else -> TextAlign.Center
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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.widthIn
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import com.acitelight.aether.Global.updateRelate
|
||||||
|
import com.acitelight.aether.model.Video
|
||||||
|
import com.acitelight.aether.view.pages.formatTime
|
||||||
|
import com.acitelight.aether.view.pages.toHex
|
||||||
|
import com.acitelight.aether.viewModel.VideoScreenViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VideoCard(
|
||||||
|
videos: List<Video>,
|
||||||
|
navController: NavHostController,
|
||||||
|
videoScreenViewModel: VideoScreenViewModel
|
||||||
|
) {
|
||||||
|
val tabIndex by videoScreenViewModel.tabIndex;
|
||||||
|
val video = videos.first()
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface.copy(0.65f)),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
updateRelate(
|
||||||
|
videoScreenViewModel.videoLibrary.classesMap[videoScreenViewModel.videoLibrary.classes[tabIndex]]
|
||||||
|
?: mutableStateListOf(), video
|
||||||
|
)
|
||||||
|
val vg = videos.joinToString(",") { "${it.klass}/${it.id}" }.toHex()
|
||||||
|
val route = "video_player_route/$vg"
|
||||||
|
navController.navigate(route)
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
videoScreenViewModel.viewModelScope.launch {
|
||||||
|
for(i in videos)
|
||||||
|
{
|
||||||
|
videoScreenViewModel.download(i)
|
||||||
|
}
|
||||||
|
Toast.makeText(
|
||||||
|
videoScreenViewModel.context,
|
||||||
|
"Start downloading ${video.video.group}",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(video.getCover(videoScreenViewModel.apiClient))
|
||||||
|
.memoryCacheKey("${video.klass}/${video.id}/cover")
|
||||||
|
.diskCacheKey("${video.klass}/${video.id}/cover")
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
imageLoader = videoScreenViewModel.imageLoader!!
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(24.dp)
|
||||||
|
.background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.padding(horizontal = 2.dp),
|
||||||
|
text = "${videos.size} Videos",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
lineHeight = 13.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(horizontal = 2.dp),
|
||||||
|
text = formatTime(video.video.duration),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
lineHeight = 13.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
if (videos.all{ it.isLocal })
|
||||||
|
Card(
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.padding(5.dp)
|
||||||
|
.widthIn(max = 46.dp)
|
||||||
|
) {
|
||||||
|
Box(Modifier.fillMaxWidth())
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
text = "Local",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = video.video.group ?: video.video.name,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 2,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.background(Color.Transparent)
|
||||||
|
.heightIn(min = 24.dp),
|
||||||
|
lineHeight = 14.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp).fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(modifier = Modifier.align(Alignment.CenterStart), text = "Class: ${video.klass}", fontSize = 10.sp, maxLines = 1)
|
||||||
|
Text(modifier = Modifier.align(Alignment.CenterEnd), text = "Id: ${
|
||||||
|
videos.take(5).joinToString(
|
||||||
|
","
|
||||||
|
) { it.id }
|
||||||
|
}", fontSize = 10.sp, maxLines = 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -8,16 +8,13 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Pause
|
|
||||||
import androidx.compose.material.icons.filled.Stop
|
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Pause
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.Stop
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
@@ -35,14 +32,14 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
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.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import com.acitelight.aether.Global.updateRelate
|
import com.acitelight.aether.Global.updateRelate
|
||||||
import com.acitelight.aether.model.VideoDownloadItemState
|
|
||||||
import com.acitelight.aether.model.Video
|
import com.acitelight.aether.model.Video
|
||||||
|
import com.acitelight.aether.model.VideoDownloadItemState
|
||||||
|
import com.acitelight.aether.view.pages.toHex
|
||||||
import com.acitelight.aether.viewModel.TransmissionScreenViewModel
|
import com.acitelight.aether.viewModel.TransmissionScreenViewModel
|
||||||
import com.tonyodev.fetch2.Status
|
import com.tonyodev.fetch2.Status
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -50,63 +47,18 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.collections.sortedWith
|
import kotlin.math.abs
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TransmissionScreen(
|
fun VideoDownloadCard(
|
||||||
navigator: NavHostController,
|
|
||||||
transmissionScreenViewModel: TransmissionScreenViewModel = hiltViewModel<TransmissionScreenViewModel>()
|
|
||||||
) {
|
|
||||||
val downloads = transmissionScreenViewModel.downloads
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
items(downloads.filter { it.type == "main" }, key = { it.id }) { item ->
|
|
||||||
VideoDownloadCard(
|
|
||||||
navigator = navigator,
|
|
||||||
viewModel = transmissionScreenViewModel,
|
|
||||||
model = item,
|
|
||||||
onPause = {
|
|
||||||
for (i in downloadToGroup(
|
|
||||||
item,
|
|
||||||
downloads
|
|
||||||
)) transmissionScreenViewModel.pause(i.id)
|
|
||||||
},
|
|
||||||
onResume = {
|
|
||||||
for (i in downloadToGroup(
|
|
||||||
item,
|
|
||||||
downloads
|
|
||||||
)) transmissionScreenViewModel.resume(i.id)
|
|
||||||
},
|
|
||||||
onCancel = {
|
|
||||||
for (i in downloadToGroup(
|
|
||||||
item,
|
|
||||||
downloads
|
|
||||||
)) transmissionScreenViewModel.cancel(i.id)
|
|
||||||
},
|
|
||||||
onDelete = {
|
|
||||||
for (i in downloadToGroup(
|
|
||||||
item,
|
|
||||||
downloads
|
|
||||||
)) transmissionScreenViewModel.delete(i.id, true)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun VideoDownloadCard(
|
|
||||||
navigator: NavHostController,
|
navigator: NavHostController,
|
||||||
viewModel: TransmissionScreenViewModel,
|
viewModel: TransmissionScreenViewModel,
|
||||||
model: VideoDownloadItemState,
|
model: VideoDownloadItemState,
|
||||||
onPause: () -> Unit,
|
onPause: () -> Unit,
|
||||||
onResume: () -> Unit,
|
onResume: () -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
onDelete: () -> Unit
|
onDelete: () -> Unit,
|
||||||
|
onRetry: () -> Unit
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
@@ -179,7 +131,7 @@ private fun VideoDownloadCard(
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(text = model.fileName, style = MaterialTheme.typography.titleMedium)
|
Text(text = model.fileName, style = MaterialTheme.typography.titleMedium, maxLines = 2)
|
||||||
// Text(text = model.filePath, style = MaterialTheme.typography.titleSmall)
|
// Text(text = model.filePath, style = MaterialTheme.typography.titleSmall)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +168,7 @@ private fun VideoDownloadCard(
|
|||||||
else {
|
else {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
.data(video.getCover())
|
.data(video.getCover(viewModel.apiClient))
|
||||||
.memoryCacheKey("${model.klass}/${model.vid}/cover")
|
.memoryCacheKey("${model.klass}/${model.vid}/cover")
|
||||||
.diskCacheKey("${model.klass}/${model.vid}/cover")
|
.diskCacheKey("${model.klass}/${model.vid}/cover")
|
||||||
.build(),
|
.build(),
|
||||||
@@ -230,7 +182,7 @@ private fun VideoDownloadCard(
|
|||||||
|
|
||||||
Column(Modifier.align(Alignment.BottomEnd)) {
|
Column(Modifier.align(Alignment.BottomEnd)) {
|
||||||
Text(
|
Text(
|
||||||
text = "${model.progress}%",
|
text = "${model.progress.coerceIn(0, 100)}%",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 8.dp)
|
.padding(start = 8.dp)
|
||||||
.align(Alignment.End)
|
.align(Alignment.End)
|
||||||
@@ -254,7 +206,7 @@ private fun VideoDownloadCard(
|
|||||||
|
|
||||||
// progress bar
|
// progress bar
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
progress = { model.progress.coerceIn(0, 100) / 100f },
|
progress = { abs(model.progress).coerceIn(0, 100) / 100f },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 8.dp, bottom = 8.dp),
|
.padding(top = 8.dp, bottom = 8.dp),
|
||||||
@@ -304,7 +256,7 @@ private fun VideoDownloadCard(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
// for FAILED, CANCELLED, REMOVED etc.
|
// for FAILED, CANCELLED, REMOVED etc.
|
||||||
Button(onClick = onResume) {
|
Button(onClick = onRetry) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.PlayArrow,
|
imageVector = Icons.Default.PlayArrow,
|
||||||
contentDescription = "Retry"
|
contentDescription = "Retry"
|
||||||
@@ -320,11 +272,4 @@ private fun VideoDownloadCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadToGroup(
|
|
||||||
i: VideoDownloadItemState,
|
|
||||||
downloads: List<VideoDownloadItemState>
|
|
||||||
): List<VideoDownloadItemState> {
|
|
||||||
return downloads.filter { it.vid == i.vid && it.klass == i.klass }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SliderDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import com.acitelight.aether.model.VideoDownloadItemState
|
||||||
|
import com.acitelight.aether.viewModel.TransmissionScreenViewModel
|
||||||
|
import com.tonyodev.fetch2.Status
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VideoDownloadCardMini(
|
||||||
|
navigator: NavHostController,
|
||||||
|
viewModel: TransmissionScreenViewModel,
|
||||||
|
model: VideoDownloadItemState,
|
||||||
|
onPause: () -> Unit,
|
||||||
|
onResume: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
onRetry: () -> Unit
|
||||||
|
) {
|
||||||
|
val video = viewModel.modelToVideo(model)
|
||||||
|
val imageModel =
|
||||||
|
if (video == null)
|
||||||
|
ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(
|
||||||
|
File(
|
||||||
|
viewModel.context.getExternalFilesDir(null),
|
||||||
|
"videos/${model.klass}/${model.vid}/cover.jpg"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.memoryCacheKey("${model.klass}/${model.vid}/cover")
|
||||||
|
.diskCacheKey("${model.klass}/${model.vid}/cover")
|
||||||
|
.build()
|
||||||
|
else
|
||||||
|
ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(video.getCover(viewModel.apiClient))
|
||||||
|
.memoryCacheKey("${model.klass}/${model.vid}/cover")
|
||||||
|
.diskCacheKey("${model.klass}/${model.vid}/cover")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 4.dp)
|
||||||
|
.background(Color.Transparent)
|
||||||
|
.clickable(onClick = {
|
||||||
|
when (model.status) {
|
||||||
|
Status.COMPLETED -> viewModel.viewModelScope.launch(Dispatchers.IO)
|
||||||
|
{
|
||||||
|
viewModel.playStart(model, navigator)
|
||||||
|
}
|
||||||
|
Status.DOWNLOADING -> onPause()
|
||||||
|
Status.PAUSED -> onResume()
|
||||||
|
Status.ADDED, Status.FAILED, Status.CANCELLED -> onRetry()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.height(100.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Box(Modifier
|
||||||
|
.fillMaxHeight())
|
||||||
|
{
|
||||||
|
AsyncImage(
|
||||||
|
model = imageModel,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(100.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.widthIn(max = 150.dp)
|
||||||
|
.background(Color.Black),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = onDelete,
|
||||||
|
Modifier
|
||||||
|
.padding(2.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.background(MaterialTheme.colorScheme.error, RoundedCornerShape(4.dp))
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = "Delete",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = model.fileName,
|
||||||
|
lineHeight = 14.sp,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 2,
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(Modifier.align(Alignment.BottomEnd)) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.align(Alignment.End),
|
||||||
|
text = when (model.status) {
|
||||||
|
Status.COMPLETED -> "Completed"
|
||||||
|
Status.PAUSED, Status.QUEUED -> "Paused"
|
||||||
|
Status.DOWNLOADING -> "Downloading"
|
||||||
|
else -> "Error"
|
||||||
|
},
|
||||||
|
fontSize = 10.sp,
|
||||||
|
lineHeight = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(Modifier
|
||||||
|
.align(Alignment.End)
|
||||||
|
.padding(vertical = 2.dp)) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier,
|
||||||
|
text = "%.2f MB/%.2f MB".format(
|
||||||
|
model.downloadedBytes / (1024.0 * 1024.0),
|
||||||
|
model.totalBytes / (1024.0 * 1024.0)
|
||||||
|
),
|
||||||
|
fontSize = 10.sp,
|
||||||
|
lineHeight = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = "${model.progress.coerceIn(0, 100)}%",
|
||||||
|
modifier = Modifier,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
lineHeight = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BiliMiniSlider(
|
||||||
|
value = abs(model.progress).coerceIn(0, 100) / 100f,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(6.dp)
|
||||||
|
.align(Alignment.End)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
onValueChange = {
|
||||||
|
|
||||||
|
},
|
||||||
|
colors = when(model.status)
|
||||||
|
{
|
||||||
|
Status.DOWNLOADING, Status.QUEUED, Status.ADDED -> SliderDefaults.colors(
|
||||||
|
thumbColor = Color(0xFFFFFFFF),
|
||||||
|
activeTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
Status.PAUSED -> SliderDefaults.colors(
|
||||||
|
thumbColor = Color(0xFFFFFFFF),
|
||||||
|
activeTrackColor = Color(0xFFFFA500),
|
||||||
|
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
Status.COMPLETED -> SliderDefaults.colors(
|
||||||
|
thumbColor = Color(0xFFFFFFFF),
|
||||||
|
activeTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
else -> SliderDefaults.colors(
|
||||||
|
thumbColor = Color(0xFFFFFFFF),
|
||||||
|
activeTrackColor = MaterialTheme.colorScheme.error,
|
||||||
|
inactiveTrackColor = Color.LightGray.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
@@ -30,6 +31,7 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
@@ -42,6 +44,7 @@ import androidx.compose.material.icons.filled.Lock
|
|||||||
import androidx.compose.material.icons.filled.LockOpen
|
import androidx.compose.material.icons.filled.LockOpen
|
||||||
import androidx.compose.material.icons.filled.Pause
|
import androidx.compose.material.icons.filled.Pause
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.SkipNext
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -49,6 +52,8 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -59,6 +64,7 @@ import androidx.compose.ui.graphics.Brush
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -67,13 +73,15 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import com.acitelight.aether.ToggleFullScreen
|
import com.acitelight.aether.setFullScreen
|
||||||
|
import com.acitelight.aether.view.pages.formatTime
|
||||||
|
import com.acitelight.aether.view.pages.moveBrit
|
||||||
import com.acitelight.aether.viewModel.VideoPlayerViewModel
|
import com.acitelight.aether.viewModel.VideoPlayerViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
|
||||||
@androidx.annotation.OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
|
fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
@@ -81,6 +89,18 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
|
|||||||
val activity = (context as? Activity)!!
|
val activity = (context as? Activity)!!
|
||||||
val exoPlayer: ExoPlayer = videoPlayerViewModel.player!!
|
val exoPlayer: ExoPlayer = videoPlayerViewModel.player!!
|
||||||
|
|
||||||
|
val name by videoPlayerViewModel.currentName
|
||||||
|
val id by videoPlayerViewModel.currentId
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val videos = videoPlayerViewModel.videos
|
||||||
|
|
||||||
|
LaunchedEffect(id, videos) {
|
||||||
|
val targetIndex = videos.indexOfFirst { it.id == id }
|
||||||
|
if (targetIndex >= 0) {
|
||||||
|
listState.scrollToItem(targetIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager }
|
val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager }
|
||||||
val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) }
|
val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) }
|
||||||
var volFactor by remember {
|
var volFactor by remember {
|
||||||
@@ -89,9 +109,6 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val name by videoPlayerViewModel.currentName
|
|
||||||
val id by videoPlayerViewModel.currentId
|
|
||||||
|
|
||||||
fun setVolume(value: Int) {
|
fun setVolume(value: Int) {
|
||||||
audioManager.setStreamVolume(
|
audioManager.setStreamVolume(
|
||||||
AudioManager.STREAM_MUSIC,
|
AudioManager.STREAM_MUSIC,
|
||||||
@@ -104,7 +121,14 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
|
|||||||
videoPlayerViewModel.isLandscape = false
|
videoPlayerViewModel.isLandscape = false
|
||||||
}
|
}
|
||||||
|
|
||||||
ToggleFullScreen(true)
|
val view = LocalView.current
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
setFullScreen(view, true)
|
||||||
|
onDispose {
|
||||||
|
setFullScreen(view, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(Modifier.fillMaxSize())
|
Box(Modifier.fillMaxSize())
|
||||||
{
|
{
|
||||||
Box(
|
Box(
|
||||||
@@ -216,12 +240,14 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = videoPlayerViewModel.draggingPurpose == 0,
|
visible = videoPlayerViewModel.draggingPurpose == 0,
|
||||||
enter = fadeIn(
|
enter = fadeIn(
|
||||||
initialAlpha = 0f,
|
initialAlpha = 0f,
|
||||||
@@ -242,7 +268,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = videoPlayerViewModel.draggingPurpose == 2,
|
visible = videoPlayerViewModel.draggingPurpose == 2,
|
||||||
enter = fadeIn(
|
enter = fadeIn(
|
||||||
initialAlpha = 0f,
|
initialAlpha = 0f,
|
||||||
@@ -279,7 +305,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = videoPlayerViewModel.draggingPurpose == 1,
|
visible = videoPlayerViewModel.draggingPurpose == 1,
|
||||||
enter = fadeIn(
|
enter = fadeIn(
|
||||||
initialAlpha = 0f,
|
initialAlpha = 0f,
|
||||||
@@ -469,6 +495,27 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
videoPlayerViewModel.viewModelScope.launch {
|
||||||
|
videoPlayerViewModel.startPlay(
|
||||||
|
videoPlayerViewModel.videos.getOrNull(videoPlayerViewModel.videos.indexOf(
|
||||||
|
videoPlayerViewModel.videos.first {
|
||||||
|
it.id == videoPlayerViewModel.currentId.value
|
||||||
|
}) + 1) ?: videoPlayerViewModel.videos.first()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Modifier.size(42.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.SkipNext,
|
||||||
|
contentDescription = "Next",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(42.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -554,10 +601,10 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel) {
|
|||||||
colors = CardDefaults.cardColors(containerColor = colorScheme.surface.copy(0.75f))
|
colors = CardDefaults.cardColors(containerColor = colorScheme.surface.copy(0.75f))
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
LazyColumn(contentPadding = PaddingValues(vertical = 4.dp)) {
|
LazyColumn(state = listState, contentPadding = PaddingValues(vertical = 4.dp)) {
|
||||||
items(videoPlayerViewModel.videos) { item ->
|
items(videoPlayerViewModel.videos) { item ->
|
||||||
MiniPlaylistCard(Modifier.padding(4.dp), video = item, imageLoader = videoPlayerViewModel.imageLoader!!,
|
MiniPlaylistCard(Modifier.padding(4.dp), video = item, imageLoader = videoPlayerViewModel.imageLoader!!,
|
||||||
selected = id == item.id)
|
selected = id == item.id, apiClient = videoPlayerViewModel.apiClient)
|
||||||
{
|
{
|
||||||
if (name == item.video.name)
|
if (name == item.video.name)
|
||||||
return@MiniPlaylistCard
|
return@MiniPlaylistCard
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -47,8 +48,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import com.acitelight.aether.Global
|
import com.acitelight.aether.Global
|
||||||
import com.acitelight.aether.Global.updateRelate
|
import com.acitelight.aether.view.pages.formatTime
|
||||||
import com.acitelight.aether.ToggleFullScreen
|
import com.acitelight.aether.view.pages.toHex
|
||||||
import com.acitelight.aether.viewModel.VideoPlayerViewModel
|
import com.acitelight.aether.viewModel.VideoPlayerViewModel
|
||||||
|
|
||||||
|
|
||||||
@@ -108,7 +109,6 @@ fun VideoPlayerPortal(
|
|||||||
val name by videoPlayerViewModel.currentName
|
val name by videoPlayerViewModel.currentName
|
||||||
val duration by videoPlayerViewModel.currentDuration
|
val duration by videoPlayerViewModel.currentDuration
|
||||||
|
|
||||||
ToggleFullScreen(false)
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.nestedScroll(nestedScrollConnection)
|
.nestedScroll(nestedScrollConnection)
|
||||||
@@ -118,7 +118,6 @@ fun VideoPlayerPortal(
|
|||||||
Box {
|
Box {
|
||||||
PortalCorePlayer(
|
PortalCorePlayer(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(top = 32.dp)
|
|
||||||
.heightIn(max = playerHeight)
|
.heightIn(max = playerHeight)
|
||||||
.onGloballyPositioned { layoutCoordinates ->
|
.onGloballyPositioned { layoutCoordinates ->
|
||||||
if (!posed && videoPlayerViewModel.renderedFirst) {
|
if (!posed && videoPlayerViewModel.renderedFirst) {
|
||||||
@@ -245,22 +244,25 @@ fun VideoPlayerPortal(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 12.dp),
|
.padding(horizontal = 12.dp),
|
||||||
i,
|
i,
|
||||||
{
|
apiClient = videoPlayerViewModel.apiClient,
|
||||||
videoPlayerViewModel.isPlaying = false
|
imageLoader = videoPlayerViewModel.imageLoader!!
|
||||||
videoPlayerViewModel.player?.pause()
|
) {
|
||||||
|
videoPlayerViewModel.isPlaying = false
|
||||||
|
videoPlayerViewModel.player?.pause()
|
||||||
|
|
||||||
val playList = mutableListOf<String>()
|
val playList = mutableListOf<String>()
|
||||||
val fv = videoPlayerViewModel.videoLibrary.classesMap.map { it.value }.flatten()
|
val fv =
|
||||||
|
videoPlayerViewModel.videoLibrary.classesMap.map { it.value }.flatten()
|
||||||
|
|
||||||
val group = fv.filter { it.klass == i.klass && it.video.group == i.video.group }
|
val group =
|
||||||
for (i in group) {
|
fv.filter { it.klass == i.klass && it.video.group == i.video.group }
|
||||||
playList.add("${i.klass}/${i.id}")
|
for (i in group) {
|
||||||
}
|
playList.add("${i.klass}/${i.id}")
|
||||||
|
}
|
||||||
|
|
||||||
val route = "video_player_route/${playList.joinToString(",").toHex()}"
|
val route = "video_player_route/${(playList.joinToString(",") + "|${i.id}").toHex()}"
|
||||||
navController.navigate(route)
|
navController.navigate(route)
|
||||||
}, videoPlayerViewModel.imageLoader!!
|
}
|
||||||
)
|
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
package com.acitelight.aether.view.pages
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyHorizontalStaggeredGrid
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.min
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import com.acitelight.aether.model.BookMark
|
||||||
|
import com.acitelight.aether.model.Comic
|
||||||
|
import com.acitelight.aether.setFullScreen
|
||||||
|
import com.acitelight.aether.view.components.BiliMiniSlider
|
||||||
|
import com.acitelight.aether.viewModel.ComicGridViewModel
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ComicGridView(
|
||||||
|
comicId: String,
|
||||||
|
navController: NavHostController,
|
||||||
|
comicGridViewModel: ComicGridViewModel = hiltViewModel<ComicGridViewModel>()
|
||||||
|
) {
|
||||||
|
comicGridViewModel.resolve(comicId.hexToString())
|
||||||
|
comicGridViewModel.updateProcess(comicId.hexToString()) {}
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val screenHeight = configuration.screenHeightDp.dp
|
||||||
|
val screenWidth = configuration.screenWidthDp.dp
|
||||||
|
val record by comicGridViewModel.record
|
||||||
|
val comic by comicGridViewModel.comic
|
||||||
|
|
||||||
|
val view = LocalView.current
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
setFullScreen(view, true)
|
||||||
|
onDispose {
|
||||||
|
val nextRoute = navController.currentBackStackEntry?.destination?.route
|
||||||
|
if (nextRoute?.startsWith("comic_page_route") != true) {
|
||||||
|
setFullScreen(view, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(comicGridViewModel) {
|
||||||
|
comicGridViewModel.coverHeight = screenHeight * 0.3f
|
||||||
|
if(comicGridViewModel.maxHeight == 0.dp)
|
||||||
|
comicGridViewModel.maxHeight = screenHeight * 0.8f
|
||||||
|
}
|
||||||
|
|
||||||
|
val dens = LocalDensity.current
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
val nestedScrollConnection = remember {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
val deltaY = available.y // px
|
||||||
|
val deltaDp = with(dens) { deltaY.toDp() }
|
||||||
|
|
||||||
|
val r = if (deltaY < 0 && comicGridViewModel.coverHeight > 0.dp) {
|
||||||
|
val newHeight = (comicGridViewModel.coverHeight + deltaDp).coerceIn(0.dp, comicGridViewModel.maxHeight)
|
||||||
|
val consumedDp = newHeight - comicGridViewModel.coverHeight
|
||||||
|
comicGridViewModel.coverHeight = newHeight
|
||||||
|
val consumedPx = with(dens) { consumedDp.toPx() }
|
||||||
|
Offset(0f, consumedPx)
|
||||||
|
} else if (
|
||||||
|
deltaY > 0
|
||||||
|
&& comicGridViewModel.coverHeight < comicGridViewModel.maxHeight
|
||||||
|
&& listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0
|
||||||
|
) {
|
||||||
|
val newHeight = (comicGridViewModel.coverHeight + deltaDp).coerceIn(0.dp, comicGridViewModel.maxHeight)
|
||||||
|
val consumedDp = newHeight - comicGridViewModel.coverHeight
|
||||||
|
comicGridViewModel.coverHeight = newHeight
|
||||||
|
val consumedPx = with(dens) { consumedDp.toPx() }
|
||||||
|
Offset(0f, consumedPx)
|
||||||
|
} else {
|
||||||
|
Offset.Zero
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comic != null) {
|
||||||
|
val comic = comic!!
|
||||||
|
val pagerState = rememberPagerState(
|
||||||
|
initialPage = 0,
|
||||||
|
pageCount = { comic.comic.bookmarks.size })
|
||||||
|
|
||||||
|
Column(Modifier
|
||||||
|
.nestedScroll(nestedScrollConnection).fillMaxSize()) {
|
||||||
|
Box(Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(comicGridViewModel.coverHeight))
|
||||||
|
{
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
{ page ->
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(comic.getPage(comic.comic.bookmarks[page].page, comicGridViewModel.apiClient))
|
||||||
|
.memoryCacheKey("${comic.id}/${comic.comic.bookmarks[page].page}")
|
||||||
|
.diskCacheKey("${comic.id}/${comic.comic.bookmarks[page].page}")
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
imageLoader = comicGridViewModel.imageLoader!!,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
onSuccess = { success ->
|
||||||
|
val drawable = success.result.image
|
||||||
|
val width = drawable.width
|
||||||
|
val height = drawable.height
|
||||||
|
val aspectRatio = width.toFloat() / height.toFloat()
|
||||||
|
comicGridViewModel.maxHeight = min(screenWidth / aspectRatio, screenHeight * 0.8f)
|
||||||
|
|
||||||
|
if(comicGridViewModel.coverHeight > comicGridViewModel.maxHeight)
|
||||||
|
comicGridViewModel.coverHeight = comicGridViewModel.maxHeight
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.height(50.dp)
|
||||||
|
.background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.5f),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
BiliMiniSlider(
|
||||||
|
value = (pagerState.currentPage + 1) / pagerState.pageCount.toFloat(),
|
||||||
|
modifier = Modifier
|
||||||
|
.height(6.dp)
|
||||||
|
.width(100.dp)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
onValueChange = {
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 6.dp)
|
||||||
|
.clip(RoundedCornerShape(6.dp))
|
||||||
|
)
|
||||||
|
{
|
||||||
|
item()
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = comic.comic.comic_name,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
lineHeight = 22.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp).padding(bottom = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 4.dp)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
comic.comic.tags.take(15).forEach()
|
||||||
|
{
|
||||||
|
ic ->
|
||||||
|
Card(
|
||||||
|
Modifier.padding(1.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = ic,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
lineHeight = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 2,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxWidth())
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = "Author: ${comic.comic.author} \n${comic.comic.list.size} Pages",
|
||||||
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 15.sp,
|
||||||
|
maxLines = 3,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 4.dp).align(Alignment.CenterStart)
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(onClick = {
|
||||||
|
comicGridViewModel.updateProcess(comicId.hexToString())
|
||||||
|
{
|
||||||
|
if (record != null) {
|
||||||
|
val route = "comic_page_route/${comic.id.toHex()}/${
|
||||||
|
record!!.position
|
||||||
|
}"
|
||||||
|
navController.navigate(route)
|
||||||
|
} else {
|
||||||
|
val route = "comic_page_route/${comic.id.toHex()}/${0}"
|
||||||
|
navController.navigate(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, modifier = Modifier.align(Alignment.CenterEnd))
|
||||||
|
{
|
||||||
|
Text(text = "Continue", fontSize = 16.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(Modifier.padding(horizontal = 12.dp).padding(bottom = 4.dp), thickness = 1.5.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(comicGridViewModel.chapterList)
|
||||||
|
{ c ->
|
||||||
|
ChapterCard(comic, navController, c, comicGridViewModel)
|
||||||
|
HorizontalDivider(Modifier.padding(horizontal = 26.dp), thickness = 1.5.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Card(
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(top = 6.dp)
|
||||||
|
.padding(bottom = 20.dp)
|
||||||
|
.height(42.dp)
|
||||||
|
.clickable {
|
||||||
|
comicGridViewModel.updateProcess(comicId.hexToString())
|
||||||
|
{
|
||||||
|
if (record != null) {
|
||||||
|
val route = "comic_page_route/${comic.id.toHex()}/${
|
||||||
|
record!!.position
|
||||||
|
}"
|
||||||
|
navController.navigate(route)
|
||||||
|
} else {
|
||||||
|
val route = "comic_page_route/${comic.id.toHex()}/${0}"
|
||||||
|
navController.navigate(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
if (record != null) {
|
||||||
|
val k = comic.getPageChapterIndex(record!!.position)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Last Read Position: ${k.first.name} ${k.second}/${
|
||||||
|
comic.getChapterLength(
|
||||||
|
k.first.page
|
||||||
|
)
|
||||||
|
}",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "Read from scratch",
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChapterCard(
|
||||||
|
comic: Comic,
|
||||||
|
navController: NavHostController,
|
||||||
|
chapter: BookMark,
|
||||||
|
comicGridViewModel: ComicGridViewModel = hiltViewModel<ComicGridViewModel>()
|
||||||
|
) {
|
||||||
|
val c = chapter
|
||||||
|
val iv = comic.getPageIndex(c.page)
|
||||||
|
val r = comic.comic.list.subList(iv, iv + comic.getChapterLength(c.page))
|
||||||
|
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface.copy(0.65f)),
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(vertical = 6.dp),
|
||||||
|
onClick = {
|
||||||
|
val route = "comic_page_route/${comic.id.toHex()}/${comic.getPageIndex(chapter.page)}"
|
||||||
|
navController.navigate(route)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(Modifier.fillMaxWidth())
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = chapter.name,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 2,
|
||||||
|
lineHeight = 18.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp).padding(vertical = 4.dp)
|
||||||
|
.background(Color.Transparent)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${comic.getChapterLength(chapter.page)} Pages",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.background(Color.Transparent)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp).padding(vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
items(r)
|
||||||
|
{ r ->
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.height(120.dp)
|
||||||
|
.padding(horizontal = 2.dp),
|
||||||
|
onClick = {
|
||||||
|
val route =
|
||||||
|
"comic_page_route/${comic.id.toHex()}/${comic.getPageIndex(r)}"
|
||||||
|
navController.navigate(route)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(comic.getPage(r, comicGridViewModel.apiClient))
|
||||||
|
.memoryCacheKey("${comic.id}/${r}")
|
||||||
|
.diskCacheKey("${comic.id}/${r}")
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
imageLoader = comicGridViewModel.imageLoader!!,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(12.dp)),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.pages
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
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.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
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.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
@@ -27,6 +31,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -34,9 +39,12 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -46,6 +54,9 @@ import androidx.navigation.NavHostController
|
|||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import com.acitelight.aether.model.BookMark
|
import com.acitelight.aether.model.BookMark
|
||||||
|
import com.acitelight.aether.setFullScreen
|
||||||
|
import com.acitelight.aether.view.components.BiliMiniSlider
|
||||||
|
import com.acitelight.aether.view.components.BookmarkPop
|
||||||
import com.acitelight.aether.viewModel.ComicPageViewModel
|
import com.acitelight.aether.viewModel.ComicPageViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -69,6 +80,15 @@ fun ComicPageView(
|
|||||||
comicPageViewModel.updateProcess(pagerState.currentPage)
|
comicPageViewModel.updateProcess(pagerState.currentPage)
|
||||||
|
|
||||||
val comic by comicPageViewModel.comic
|
val comic by comicPageViewModel.comic
|
||||||
|
|
||||||
|
val view = LocalView.current
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
setFullScreen(view, true)
|
||||||
|
onDispose {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
comic?.let {
|
comic?.let {
|
||||||
Box()
|
Box()
|
||||||
{
|
{
|
||||||
@@ -78,18 +98,22 @@ fun ComicPageView(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
.background(Color.Black)
|
.background(Color.Black)
|
||||||
.clickable {
|
.pointerInput(Unit) {
|
||||||
showPlane = !showPlane
|
detectTapGestures(
|
||||||
if (showPlane) {
|
onTap = {
|
||||||
comicPageViewModel.viewModelScope.launch {
|
showPlane = !showPlane
|
||||||
comicPageViewModel.listState?.scrollToItem(index = pagerState.currentPage)
|
if (showPlane) {
|
||||||
|
comicPageViewModel.viewModelScope.launch {
|
||||||
|
comicPageViewModel.listState?.scrollToItem(index = pagerState.currentPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
) { page ->
|
) { page ->
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
.data(it.getPage(page))
|
.data(it.getPage(page, comicPageViewModel.apiClient))
|
||||||
.memoryCacheKey("${it.id}/${page}")
|
.memoryCacheKey("${it.id}/${page}")
|
||||||
.diskCacheKey("${it.id}/${page}")
|
.diskCacheKey("${it.id}/${page}")
|
||||||
.build(),
|
.build(),
|
||||||
@@ -102,121 +126,114 @@ fun ComicPageView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showPlane,
|
visible = showPlane,
|
||||||
enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }),
|
enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }),
|
||||||
exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight }),
|
exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight }),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
) {
|
) {
|
||||||
Box()
|
Column(Modifier
|
||||||
{
|
.align(Alignment.TopCenter)
|
||||||
Column(Modifier
|
.fillMaxWidth()
|
||||||
.align(Alignment.TopCenter)
|
.background(
|
||||||
.fillMaxWidth())
|
brush = Brush.verticalGradient(
|
||||||
{
|
colors = listOf(
|
||||||
Card(
|
Color.Black.copy(alpha = 0.9f),
|
||||||
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
|
Color.Transparent,
|
||||||
shape = RoundedCornerShape(12.dp),
|
)
|
||||||
modifier = Modifier
|
)
|
||||||
.fillMaxWidth()
|
))
|
||||||
.padding(top = 18.dp)
|
{
|
||||||
.padding(horizontal = 12.dp)
|
Row(modifier = Modifier
|
||||||
.height(42.dp)
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp).padding(top = 16.dp))
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 19.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.padding(horizontal = 10.dp)
|
||||||
|
.weight(1f)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
)
|
)
|
||||||
{
|
|
||||||
Row(modifier = Modifier.fillMaxSize())
|
|
||||||
{
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.padding(horizontal = 10.dp)
|
|
||||||
.weight(1f)
|
|
||||||
.align(Alignment.CenterVertically)
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "${pagerState.currentPage + 1}/${pagerState.pageCount}",
|
text = "${pagerState.currentPage + 1}/${pagerState.pageCount}",
|
||||||
fontSize = 18.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
lineHeight = 19.sp,
|
||||||
maxLines = 1,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier
|
maxLines = 1,
|
||||||
.padding(8.dp)
|
color = Color.White,
|
||||||
.widthIn(min = 60.dp)
|
modifier = Modifier
|
||||||
.align(Alignment.CenterVertically)
|
.padding(8.dp)
|
||||||
)
|
.widthIn(min = 60.dp)
|
||||||
}
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(Modifier.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp))
|
||||||
|
{
|
||||||
|
Row {
|
||||||
|
val k = it.getPageChapterIndex(pagerState.currentPage)
|
||||||
|
Text(
|
||||||
|
text = k.first.name,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.padding(horizontal = 10.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${k.second}/${it.getChapterLength(k.first.page)}",
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 1,
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.widthIn(min = 60.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(Modifier.fillMaxWidth()) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterStart)
|
|
||||||
.padding(top = 6.dp)
|
|
||||||
.padding(horizontal = 12.dp)
|
|
||||||
.height(42.dp),
|
|
||||||
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Row {
|
|
||||||
val k = it.getPageChapterIndex(pagerState.currentPage)
|
|
||||||
Text(
|
|
||||||
text = k.first.name,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.padding(horizontal = 10.dp)
|
|
||||||
.align(Alignment.CenterVertically)
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
Card(
|
||||||
text = "${k.second}/${it.getChapterLength(k.first.page)}",
|
modifier = Modifier
|
||||||
fontSize = 18.sp,
|
.align(Alignment.CenterEnd)
|
||||||
fontWeight = FontWeight.Bold,
|
.padding(top = 6.dp)
|
||||||
maxLines = 1,
|
.padding(horizontal = 12.dp)
|
||||||
modifier = Modifier
|
.height(42.dp),
|
||||||
.padding(8.dp)
|
colors = CardDefaults.cardColors(containerColor = colorScheme.surface),
|
||||||
.widthIn(min = 60.dp)
|
shape = RoundedCornerShape(12.dp)
|
||||||
.align(Alignment.CenterVertically)
|
)
|
||||||
)
|
{
|
||||||
}
|
Box(Modifier.clickable {
|
||||||
}
|
showBookMarkPop = true
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
Card(
|
Icons.Filled.Bookmarks,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterEnd)
|
.padding(8.dp),
|
||||||
.padding(top = 6.dp)
|
contentDescription = "Bookmark"
|
||||||
.padding(horizontal = 12.dp)
|
)
|
||||||
.height(42.dp),
|
|
||||||
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Box(Modifier.clickable {
|
|
||||||
showBookMarkPop = true
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Bookmarks,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp),
|
|
||||||
contentDescription = "Bookmark"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(64.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showPlane,
|
visible = showPlane,
|
||||||
enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }),
|
enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }),
|
||||||
exit = slideOutVertically(targetOffsetY = { fullHeight -> fullHeight }),
|
exit = slideOutVertically(targetOffsetY = { fullHeight -> fullHeight }),
|
||||||
@@ -224,22 +241,31 @@ fun ComicPageView(
|
|||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Box {
|
val k = it.getPageChapterIndex(pagerState.currentPage)
|
||||||
|
Column(Modifier
|
||||||
|
.background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.9f),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)) {
|
||||||
|
Spacer(Modifier.height(42.dp))
|
||||||
LazyRow(
|
LazyRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
state = comicPageViewModel.listState!!, modifier = Modifier
|
state = comicPageViewModel.listState!!, modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(bottom = 18.dp)
|
.padding(bottom = 1.dp)
|
||||||
.padding(horizontal = 12.dp)
|
.padding(horizontal = 12.dp)
|
||||||
.height(240.dp)
|
.height(180.dp)
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
items(comicPageViewModel.pageList.size)
|
items(comicPageViewModel.pageList.size)
|
||||||
{ r ->
|
{ r ->
|
||||||
Card(
|
Card(
|
||||||
colors = CardDefaults.cardColors(containerColor = colorScheme.primary.copy(0.8f)),
|
colors = CardDefaults.cardColors(containerColor = colorScheme.primary.copy(0.8f)),
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
@@ -248,11 +274,11 @@ fun ComicPageView(
|
|||||||
pagerState.requestScrollToPage(page = r)
|
pagerState.requestScrollToPage(page = r)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Box(Modifier.padding(4.dp))
|
Box(Modifier.padding(0.dp))
|
||||||
{
|
{
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
.data(it.getPage(r))
|
.data(it.getPage(r, comicPageViewModel.apiClient))
|
||||||
.memoryCacheKey("${it.id}/${r}")
|
.memoryCacheKey("${it.id}/${r}")
|
||||||
.diskCacheKey("${it.id}/${r}")
|
.diskCacheKey("${it.id}/${r}")
|
||||||
.build(),
|
.build(),
|
||||||
@@ -260,7 +286,7 @@ fun ComicPageView(
|
|||||||
imageLoader = comicPageViewModel.imageLoader!!,
|
imageLoader = comicPageViewModel.imageLoader!!,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.align(Alignment.Center),
|
.align(Alignment.Center),
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
)
|
)
|
||||||
@@ -276,18 +302,6 @@ fun ComicPageView(
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
Row {
|
Row {
|
||||||
Text(
|
|
||||||
text = k.first.name,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = Color.White,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(2.dp)
|
|
||||||
.widthIn(max = 200.dp)
|
|
||||||
.align(Alignment.CenterVertically)
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "${k.second}/${it.getChapterLength(k.first.page)}",
|
text = "${k.second}/${it.getChapterLength(k.first.page)}",
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
@@ -304,6 +318,20 @@ fun ComicPageView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BiliMiniSlider(
|
||||||
|
value = (k.second.toInt()) / it.getChapterLength(k.first.page).toFloat(),
|
||||||
|
modifier = Modifier
|
||||||
|
.height(6.dp)
|
||||||
|
.fillMaxWidth().padding(horizontal = 24.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
onValueChange = {
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
package com.acitelight.aether.view.pages
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
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.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.items
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.Placeable
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import com.acitelight.aether.view.components.ComicCard
|
||||||
|
import com.acitelight.aether.viewModel.ComicScreenViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VariableGrid(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
rowHeight: Dp,
|
||||||
|
horizontalSpacing: Dp = 4.dp,
|
||||||
|
verticalSpacing: Dp = 4.dp,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
Layout(
|
||||||
|
modifier = modifier
|
||||||
|
.verticalScroll(scrollState),
|
||||||
|
content = content
|
||||||
|
) { measurables, constraints ->
|
||||||
|
|
||||||
|
val rowHeightPx = rowHeight.roundToPx()
|
||||||
|
val hSpacePx = horizontalSpacing.roundToPx()
|
||||||
|
val vSpacePx = verticalSpacing.roundToPx()
|
||||||
|
|
||||||
|
val placeables = measurables.map { measurable ->
|
||||||
|
measurable.measure(
|
||||||
|
constraints.copy(
|
||||||
|
minWidth = 0,
|
||||||
|
minHeight = rowHeightPx,
|
||||||
|
maxHeight = rowHeightPx
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val rows = mutableListOf<List<Placeable>>()
|
||||||
|
var currentRow = mutableListOf<Placeable>()
|
||||||
|
var currentWidth = 0
|
||||||
|
val maxWidth = constraints.maxWidth
|
||||||
|
|
||||||
|
for (placeable in placeables) {
|
||||||
|
if (currentRow.isNotEmpty() && currentWidth + placeable.width + hSpacePx > maxWidth) {
|
||||||
|
rows.add(currentRow)
|
||||||
|
currentRow = mutableListOf()
|
||||||
|
currentWidth = 0
|
||||||
|
}
|
||||||
|
currentRow.add(placeable)
|
||||||
|
currentWidth += placeable.width + hSpacePx
|
||||||
|
}
|
||||||
|
if (currentRow.isNotEmpty()) {
|
||||||
|
rows.add(currentRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
val layoutHeight = if (rows.isEmpty()) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
rows.size * rowHeightPx + (rows.size - 1) * vSpacePx
|
||||||
|
}
|
||||||
|
|
||||||
|
layout(
|
||||||
|
width = constraints.maxWidth.coerceAtLeast(constraints.minWidth),
|
||||||
|
height = layoutHeight.coerceAtLeast(constraints.minHeight)
|
||||||
|
) {
|
||||||
|
var y = 0
|
||||||
|
for (row in rows) {
|
||||||
|
var x = 0
|
||||||
|
for (placeable in row) {
|
||||||
|
placeable.placeRelative(x, y)
|
||||||
|
x += placeable.width + hSpacePx
|
||||||
|
}
|
||||||
|
y += rowHeightPx + vSpacePx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ComicScreen(
|
||||||
|
navController: NavHostController,
|
||||||
|
comicScreenViewModel: ComicScreenViewModel = hiltViewModel<ComicScreenViewModel>()
|
||||||
|
) {
|
||||||
|
val included = comicScreenViewModel.included
|
||||||
|
val state = rememberLazyStaggeredGridState()
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
var searchFilter by comicScreenViewModel.searchFilter
|
||||||
|
var isTagsVisible by remember { mutableStateOf(false) }
|
||||||
|
var sortType by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.animateContentSize()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = "Comics",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.height(36.dp)
|
||||||
|
.widthIn(max = 240.dp)
|
||||||
|
.background(colorScheme.surface, RoundedCornerShape(8.dp))
|
||||||
|
.padding(horizontal = 6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(30.dp)
|
||||||
|
.align(Alignment.CenterVertically),
|
||||||
|
imageVector = Icons.Default.Search,
|
||||||
|
contentDescription = "Catalogue"
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
BasicTextField(
|
||||||
|
value = searchFilter,
|
||||||
|
onValueChange = { searchFilter = it },
|
||||||
|
textStyle = LocalTextStyle.current.copy(
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
Text(
|
||||||
|
text = "Sorted by: ",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier = Modifier.padding(horizontal = 6.dp).align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
|
||||||
|
RadioButton(
|
||||||
|
selected = (sortType == 0),
|
||||||
|
onClick = { sortType = 0 },
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically).size(24.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Id",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically).padding(3.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
RadioButton(
|
||||||
|
selected = (sortType == 1),
|
||||||
|
onClick = { sortType = 1 },
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically).size(24.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Name",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically).padding(3.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = colorScheme.surface),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
.padding(horizontal = 4.dp)
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.height(32.dp)
|
||||||
|
.width(64.dp),
|
||||||
|
onClick = {
|
||||||
|
isTagsVisible = !isTagsVisible
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Row(Modifier.fillMaxSize())
|
||||||
|
{
|
||||||
|
Text(text = "Tags", fontWeight = FontWeight.Bold, fontSize = 16.sp, modifier = Modifier.align(Alignment.CenterVertically).padding(start = 5.dp))
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = isTagsVisible, modifier = Modifier.align(Alignment.CenterVertically).padding(end = 5.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(Modifier.padding(1.dp), thickness = 1.5.dp)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isTagsVisible,
|
||||||
|
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
|
||||||
|
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut()
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
VariableGrid(
|
||||||
|
modifier = Modifier
|
||||||
|
.heightIn(max = 80.dp)
|
||||||
|
.padding(4.dp),
|
||||||
|
rowHeight = 30.dp
|
||||||
|
)
|
||||||
|
{
|
||||||
|
for (i in comicScreenViewModel.tags) {
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.background(
|
||||||
|
if (included.contains(i)) Color.Green.copy(alpha = 0.65f) else colorScheme.surface,
|
||||||
|
shape = RoundedCornerShape(4.dp)
|
||||||
|
)
|
||||||
|
.height(32.dp)
|
||||||
|
.widthIn(max = 72.dp)
|
||||||
|
.clickable {
|
||||||
|
if (included.contains(i))
|
||||||
|
included.remove(i)
|
||||||
|
else
|
||||||
|
included.add(i)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = i,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(2.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(Modifier.padding(1.dp), thickness = 1.5.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyVerticalStaggeredGrid(
|
||||||
|
columns = StaggeredGridCells.Adaptive(120.dp),
|
||||||
|
contentPadding = PaddingValues(4.dp),
|
||||||
|
verticalItemSpacing = 6.dp,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
state = state,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = comicScreenViewModel.comics
|
||||||
|
.filter { searchFilter.isEmpty() || searchFilter in it.comic.comic_name }
|
||||||
|
.filter { x ->
|
||||||
|
included.all { y -> y in x.comic.tags } || included.isEmpty()
|
||||||
|
}
|
||||||
|
.sortedByDescending {
|
||||||
|
when(sortType)
|
||||||
|
{
|
||||||
|
0 -> it.id.toInt().toString().padStart(10, '0')
|
||||||
|
1 -> it.comic.comic_name
|
||||||
|
else -> it.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key = { it.id }
|
||||||
|
) { comic ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
) {
|
||||||
|
ComicCard(comic, navController, comicScreenViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.pages
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -37,35 +36,35 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import com.acitelight.aether.Global.updateRelate
|
import com.acitelight.aether.Global.updateRelate
|
||||||
import com.acitelight.aether.model.Comic
|
import com.acitelight.aether.model.Comic
|
||||||
import com.acitelight.aether.viewModel.ComicScreenViewModel
|
import com.acitelight.aether.view.components.MiniVideoCard
|
||||||
import com.acitelight.aether.viewModel.HomeScreenViewModel
|
import com.acitelight.aether.viewModel.HomeScreenViewModel
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
homeScreenViewModel: HomeScreenViewModel = androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel<HomeScreenViewModel>(),
|
homeScreenViewModel: HomeScreenViewModel = hiltViewModel<HomeScreenViewModel>(),
|
||||||
navController: NavHostController)
|
navController: NavHostController
|
||||||
{
|
) {
|
||||||
val pagerState = rememberPagerState(initialPage = 0, pageCount = { 2 })
|
val pagerState = rememberPagerState(initialPage = 0, pageCount = { 2 })
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier.fillMaxSize().background(Color.Black)
|
modifier = Modifier
|
||||||
){
|
.fillMaxSize()
|
||||||
p ->
|
) { p ->
|
||||||
if(p == 0)
|
if (p == 0) {
|
||||||
{
|
|
||||||
Column(Modifier.fillMaxHeight()) {
|
Column(Modifier.fillMaxHeight()) {
|
||||||
Text(
|
Text(
|
||||||
text = "Videos",
|
text = "Videos",
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
modifier = Modifier.padding(8.dp).align(Alignment.Start)
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.align(Alignment.Start)
|
||||||
)
|
)
|
||||||
|
|
||||||
HorizontalDivider(Modifier.padding(8.dp), 2.dp, DividerDefaults.color)
|
HorizontalDivider(Modifier.padding(8.dp), 2.dp, DividerDefaults.color)
|
||||||
@@ -73,27 +72,41 @@ fun HomeScreen(
|
|||||||
LazyColumn(modifier = Modifier.fillMaxWidth())
|
LazyColumn(modifier = Modifier.fillMaxWidth())
|
||||||
{
|
{
|
||||||
items(homeScreenViewModel.recentManager.recentVideo)
|
items(homeScreenViewModel.recentManager.recentVideo)
|
||||||
{
|
{ i ->
|
||||||
i ->
|
|
||||||
MiniVideoCard(
|
MiniVideoCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 12.dp),
|
.padding(horizontal = 12.dp),
|
||||||
i,
|
i,
|
||||||
{
|
apiClient = homeScreenViewModel.apiClient,
|
||||||
updateRelate(homeScreenViewModel.recentManager.recentVideo, i)
|
imageLoader = homeScreenViewModel.imageLoader!!
|
||||||
|
)
|
||||||
|
{
|
||||||
|
updateRelate(homeScreenViewModel.recentManager.recentVideo, i)
|
||||||
|
|
||||||
val playList = mutableListOf<String>()
|
val playList = mutableListOf<String>()
|
||||||
val fv = homeScreenViewModel.videoLibrary.classesMap.map { it.value }.flatten()
|
val fv = homeScreenViewModel.videoLibrary.classesMap.map { it.value }
|
||||||
|
.flatten()
|
||||||
|
|
||||||
val group = fv.filter { it.klass == i.klass && it.video.group == i.video.group }
|
val group =
|
||||||
for (i in group) {
|
fv.filter { it.klass == i.klass && it.video.group == i.video.group && it.video.group != "null" }
|
||||||
playList.add("${i.klass}/${i.id}")
|
for (ix in group) {
|
||||||
}
|
playList.add("${ix.klass}/${ix.id}")
|
||||||
|
}
|
||||||
|
|
||||||
val route = "video_player_route/${(playList.joinToString(",") + "|${i.id}").toHex()}"
|
if(!playList.contains("${i.klass}/${i.id}"))
|
||||||
navController.navigate(route)
|
playList.add("${i.klass}/${i.id}")
|
||||||
}, homeScreenViewModel.imageLoader!!)
|
|
||||||
HorizontalDivider(Modifier.padding(vertical = 8.dp).alpha(0.4f), 1.dp, DividerDefaults.color)
|
val route =
|
||||||
|
"video_player_route/${(playList.joinToString(",") + "|${i.id}").toHex()}"
|
||||||
|
navController.navigate(route)
|
||||||
|
}
|
||||||
|
HorizontalDivider(
|
||||||
|
Modifier
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.alpha(0.4f),
|
||||||
|
1.dp,
|
||||||
|
DividerDefaults.color
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +115,9 @@ fun HomeScreen(
|
|||||||
Text(
|
Text(
|
||||||
text = "Comics",
|
text = "Comics",
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
modifier = Modifier.padding(8.dp).align(Alignment.Start)
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.align(Alignment.Start)
|
||||||
)
|
)
|
||||||
|
|
||||||
HorizontalDivider(Modifier.padding(8.dp), 2.dp, DividerDefaults.color)
|
HorizontalDivider(Modifier.padding(8.dp), 2.dp, DividerDefaults.color)
|
||||||
@@ -115,8 +130,7 @@ fun HomeScreen(
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
items(homeScreenViewModel.recentManager.recentComic)
|
items(homeScreenViewModel.recentManager.recentComic)
|
||||||
{
|
{ comic ->
|
||||||
comic ->
|
|
||||||
ComicCardRecent(comic, navController, homeScreenViewModel)
|
ComicCardRecent(comic, navController, homeScreenViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +152,7 @@ fun ComicCardRecent(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight(),
|
.wrapContentHeight(),
|
||||||
onClick = {
|
onClick = {
|
||||||
val route = "comic_grid_route/${"${comic.id}".toHex()}"
|
val route = "comic_grid_route/${comic.id.toHex()}"
|
||||||
navController.navigate(route)
|
navController.navigate(route)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -149,7 +163,7 @@ fun ComicCardRecent(
|
|||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
.data(comic.getPage(0))
|
.data(comic.getPage(0, homeScreenViewModel.apiClient))
|
||||||
.memoryCacheKey("${comic.id}/${0}")
|
.memoryCacheKey("${comic.id}/${0}")
|
||||||
.diskCacheKey("${comic.id}/${0}")
|
.diskCacheKey("${comic.id}/${0}")
|
||||||
.build(),
|
.build(),
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.acitelight.aether.view.pages
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import com.acitelight.aether.viewModel.LiveScreenViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LiveScreen(
|
||||||
|
liveScreenViewModel: LiveScreenViewModel = hiltViewModel<LiveScreenViewModel>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.pages
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -17,7 +15,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Key
|
import androidx.compose.material.icons.filled.Key
|
||||||
import androidx.compose.material.icons.filled.Link
|
import androidx.compose.material.icons.filled.Link
|
||||||
import androidx.compose.material.icons.filled.Person
|
import androidx.compose.material.icons.filled.Person
|
||||||
import androidx.compose.material.icons.filled.Security
|
import androidx.compose.material.icons.filled.Textsms
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
@@ -31,27 +29,20 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.acitelight.aether.service.ApiClient.api
|
|
||||||
import com.acitelight.aether.viewModel.MeScreenViewModel
|
import com.acitelight.aether.viewModel.MeScreenViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MeScreen(meScreenViewModel: MeScreenViewModel = androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel<MeScreenViewModel>()) {
|
fun MeScreen(meScreenViewModel: MeScreenViewModel = hiltViewModel<MeScreenViewModel>()) {
|
||||||
val context = LocalContext.current
|
var username by meScreenViewModel.username
|
||||||
var username by meScreenViewModel.username;
|
var privateKey by meScreenViewModel.privateKey
|
||||||
var privateKey by meScreenViewModel.privateKey;
|
|
||||||
var url by meScreenViewModel.url
|
var url by meScreenViewModel.url
|
||||||
var cert by meScreenViewModel.cert
|
var cert by meScreenViewModel.cert
|
||||||
|
var pak by meScreenViewModel.pak
|
||||||
|
|
||||||
val uss by meScreenViewModel.uss.collectAsState(initial = false)
|
val uss by meScreenViewModel.uss.collectAsState(initial = false)
|
||||||
|
|
||||||
@@ -61,7 +52,8 @@ fun MeScreen(meScreenViewModel: MeScreenViewModel = androidx.hilt.lifecycle.view
|
|||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Top
|
verticalArrangement = Arrangement.Top
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
// Card component for a clean, contained UI block
|
// Card component for a clean, contained UI block
|
||||||
item{
|
item{
|
||||||
Card(
|
Card(
|
||||||
@@ -204,18 +196,53 @@ fun MeScreen(meScreenViewModel: MeScreenViewModel = androidx.hilt.lifecycle.view
|
|||||||
) {
|
) {
|
||||||
Text("Save")
|
Text("Save")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Toolbox",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.align(Alignment.Start)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = pak,
|
||||||
|
onValueChange = { pak = it },
|
||||||
|
label = { Text("Packet") },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.Textsms, contentDescription = "Packet")
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Row{
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
meScreenViewModel.viewModelScope.launch {
|
meScreenViewModel.sendPacket(pak)
|
||||||
Log.i("Delay Analyze", "Start Abyss Hello")
|
|
||||||
val h = api!!.hello()
|
|
||||||
Log.i("Delay Analyze", "Abyss Hello: ${h.string()}")
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(0.5f).padding(8.dp)
|
modifier = Modifier.weight(0.5f).padding(8.dp)
|
||||||
) {
|
) {
|
||||||
Text("Ping")
|
Text("Send")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.acitelight.aether.view.pages
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.DividerDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import com.acitelight.aether.model.VideoDownloadItemState
|
||||||
|
import com.acitelight.aether.view.components.BiliMiniSlider
|
||||||
|
import com.acitelight.aether.view.components.VideoDownloadCardMini
|
||||||
|
import com.acitelight.aether.viewModel.TransmissionScreenViewModel
|
||||||
|
import com.tonyodev.fetch2.Status
|
||||||
|
import kotlin.collections.sortedWith
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TransmissionScreen(
|
||||||
|
navigator: NavHostController,
|
||||||
|
transmissionScreenViewModel: TransmissionScreenViewModel = hiltViewModel<TransmissionScreenViewModel>()
|
||||||
|
) {
|
||||||
|
val downloads = transmissionScreenViewModel.downloads
|
||||||
|
Column()
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = "Video Tasks",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.align(Alignment.Start)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "All: ${downloads.count { it.type == "main" }}",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.align(Alignment.Start),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 13.sp,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Completed: ${downloads.count { it.type == "main" && it.status == Status.COMPLETED }}",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.align(Alignment.Start),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 13.sp,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
val downloading = downloads.filter { it.status == Status.DOWNLOADING }
|
||||||
|
BiliMiniSlider(
|
||||||
|
value = if (downloading.sumOf { it.totalBytes } == 0L) 1f else downloading.sumOf { it.downloadedBytes } / downloading.sumOf { it.totalBytes }
|
||||||
|
.toFloat(),
|
||||||
|
modifier = Modifier
|
||||||
|
.height(6.dp)
|
||||||
|
.align(Alignment.End)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
onValueChange = {
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(Modifier.padding(8.dp), 2.dp, DividerDefaults.color)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
items(
|
||||||
|
downloads
|
||||||
|
.filter { it.type == "main" }
|
||||||
|
.sortedWith(compareBy(naturalOrder()) { it.fileName })
|
||||||
|
.sortedBy { it.status == Status.COMPLETED }, key = { it.id })
|
||||||
|
{ item ->
|
||||||
|
VideoDownloadCardMini(
|
||||||
|
navigator = navigator,
|
||||||
|
viewModel = transmissionScreenViewModel,
|
||||||
|
model = item,
|
||||||
|
onPause = {
|
||||||
|
for (i in downloadToGroup(
|
||||||
|
item,
|
||||||
|
downloads
|
||||||
|
)) transmissionScreenViewModel.pause(i.id)
|
||||||
|
},
|
||||||
|
onResume = {
|
||||||
|
for (i in downloadToGroup(
|
||||||
|
item,
|
||||||
|
downloads
|
||||||
|
)) transmissionScreenViewModel.resume(i.id)
|
||||||
|
},
|
||||||
|
onCancel = {
|
||||||
|
for (i in downloadToGroup(
|
||||||
|
item,
|
||||||
|
downloads
|
||||||
|
)) transmissionScreenViewModel.delete(i.id)
|
||||||
|
},
|
||||||
|
onDelete = {
|
||||||
|
for (i in downloadToGroup(
|
||||||
|
item,
|
||||||
|
downloads
|
||||||
|
)) transmissionScreenViewModel.delete(i.id)
|
||||||
|
},
|
||||||
|
onRetry = {
|
||||||
|
for (i in downloadToGroup(
|
||||||
|
item,
|
||||||
|
downloads
|
||||||
|
)) transmissionScreenViewModel.retry(i.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider(
|
||||||
|
Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||||
|
2.dp,
|
||||||
|
DividerDefaults.color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadToGroup(
|
||||||
|
i: VideoDownloadItemState,
|
||||||
|
downloads: List<VideoDownloadItemState>
|
||||||
|
): List<VideoDownloadItemState> {
|
||||||
|
return downloads.filter { it.vid == i.vid && it.klass == i.klass }
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.acitelight.aether.view.pages
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import com.acitelight.aether.viewModel.VideoPlayerViewModel
|
||||||
|
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import com.acitelight.aether.view.components.VideoPlayerLandscape
|
||||||
|
import com.acitelight.aether.view.components.VideoPlayerPortal
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
fun formatTime(ms: Long): String {
|
||||||
|
if (ms <= 0) return "00:00:00"
|
||||||
|
val totalSeconds = ms / 1000
|
||||||
|
val hours = totalSeconds / 3600
|
||||||
|
val minutes = (totalSeconds % 3600) / 60
|
||||||
|
val seconds = totalSeconds % 60
|
||||||
|
return String.format("%02d:%02d:%02d", hours, minutes, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveBrit(db: Float, activity: Activity, videoPlayerViewModel: VideoPlayerViewModel) {
|
||||||
|
val attr = activity.window.attributes
|
||||||
|
|
||||||
|
val britUi = (videoPlayerViewModel.brit - db * 0.002f).coerceIn(0f, 1f)
|
||||||
|
videoPlayerViewModel.brit = britUi
|
||||||
|
|
||||||
|
val gamma = 2.2f
|
||||||
|
val britSystem = britUi.pow(gamma).coerceIn(0.001f, 1f)
|
||||||
|
|
||||||
|
attr.screenBrightness = britSystem
|
||||||
|
activity.window.attributes = attr
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VideoPlayer(
|
||||||
|
videoPlayerViewModel: VideoPlayerViewModel = hiltViewModel<VideoPlayerViewModel>(),
|
||||||
|
videoId: String,
|
||||||
|
navController: NavHostController
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val activity = (context as? Activity)!!
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
videoPlayerViewModel.init(videoId)
|
||||||
|
|
||||||
|
activity.requestedOrientation =
|
||||||
|
if(videoPlayerViewModel.isLandscape)
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
else
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
|
||||||
|
if (videoPlayerViewModel.startPlaying) {
|
||||||
|
if (videoPlayerViewModel.isLandscape) {
|
||||||
|
VideoPlayerLandscape(videoPlayerViewModel)
|
||||||
|
} else {
|
||||||
|
VideoPlayerPortal(videoPlayerViewModel, navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view.pages
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideInVertically
|
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
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
|
||||||
@@ -26,9 +24,6 @@ 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.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.lazy.items
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||||
@@ -39,20 +34,15 @@ import androidx.compose.foundation.text.BasicTextField
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Menu
|
import androidx.compose.material.icons.filled.Menu
|
||||||
import androidx.compose.material.icons.filled.Search
|
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.CardDefaults
|
||||||
import androidx.compose.material3.CheckboxDefaults.colors
|
|
||||||
import androidx.compose.material3.DividerDefaults
|
import androidx.compose.material3.DividerDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Tab
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -60,44 +50,30 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import coil3.compose.AsyncImage
|
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.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.setValue
|
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.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.CardPage
|
||||||
import com.acitelight.aether.Global
|
|
||||||
import com.acitelight.aether.Global.updateRelate
|
import com.acitelight.aether.Global.updateRelate
|
||||||
import kotlinx.coroutines.Dispatchers
|
import com.acitelight.aether.view.components.VideoCard
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.security.KeyPair
|
|
||||||
import kotlin.collections.sortedWith
|
import kotlin.collections.sortedWith
|
||||||
|
|
||||||
fun videoTOView(v: List<Video>): Map<String?, List<Video>>
|
fun videoToView(v: List<Video>): Map<String?, List<Video>>
|
||||||
{
|
{
|
||||||
return v.map { if(it.video.group != null) it else Video(id=it.id, isLocal = it.isLocal, localBase = it.localBase,
|
return v.map { if(it.video.group != null) it else Video(id=it.id, isLocal = it.isLocal, localBase = it.localBase,
|
||||||
klass = it.klass, token = it.token, video = it.video.copy(group = it.video.name)) }.groupBy { it.video.group }
|
klass = it.klass, video = it.video.copy(group = it.video.name)) }.groupBy { it.video.group }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.toHex(): String {
|
fun String.toHex(): String {
|
||||||
@@ -126,7 +102,7 @@ fun VideoScreen(
|
|||||||
var menuVisibility by videoScreenViewModel.menuVisibility
|
var menuVisibility by videoScreenViewModel.menuVisibility
|
||||||
var searchFilter by videoScreenViewModel.searchFilter
|
var searchFilter by videoScreenViewModel.searchFilter
|
||||||
var doneInit by videoScreenViewModel.doneInit
|
var doneInit by videoScreenViewModel.doneInit
|
||||||
val vb = videoTOView(videoScreenViewModel.videoLibrary.classesMap.getOrDefault(
|
val vb = videoToView(videoScreenViewModel.videoLibrary.classesMap.getOrDefault(
|
||||||
videoScreenViewModel.videoLibrary.classes.getOrNull(
|
videoScreenViewModel.videoLibrary.classes.getOrNull(
|
||||||
tabIndex
|
tabIndex
|
||||||
), listOf()
|
), listOf()
|
||||||
@@ -141,15 +117,23 @@ fun VideoScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Videos",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.align(Alignment.Start)
|
||||||
|
)
|
||||||
|
|
||||||
// TopRow(videoScreenViewModel);
|
// TopRow(videoScreenViewModel);
|
||||||
Row(Modifier.padding(bottom = 4.dp))
|
Row(Modifier.padding(bottom = 4.dp).padding(start = 8.dp))
|
||||||
{
|
{
|
||||||
Card(
|
Card(
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
|
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterVertically)
|
.align(Alignment.CenterVertically)
|
||||||
.padding(horizontal = 2.dp)
|
.padding(horizontal = 1.dp)
|
||||||
.size(36.dp),
|
.size(36.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
menuVisibility = !menuVisibility
|
menuVisibility = !menuVisibility
|
||||||
@@ -172,7 +156,7 @@ fun VideoScreen(
|
|||||||
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
|
colors = CardDefaults.cardColors(containerColor = colorScheme.primary),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterVertically)
|
.align(Alignment.CenterVertically)
|
||||||
.padding(horizontal = 2.dp)
|
.padding(horizontal = 1.dp)
|
||||||
.height(36.dp),
|
.height(36.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
menuVisibility = !menuVisibility
|
menuVisibility = !menuVisibility
|
||||||
@@ -224,15 +208,15 @@ fun VideoScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
Modifier.padding(bottom = 8.dp),
|
Modifier.padding(4.dp),
|
||||||
1.5.dp,
|
2.dp,
|
||||||
DividerDefaults.color
|
DividerDefaults.color
|
||||||
)
|
)
|
||||||
LazyVerticalStaggeredGrid(
|
LazyVerticalStaggeredGrid(
|
||||||
columns = StaggeredGridCells.Adaptive(160.dp),
|
columns = StaggeredGridCells.Adaptive(160.dp),
|
||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(8.dp),
|
||||||
verticalItemSpacing = 8.dp,
|
verticalItemSpacing = 8.dp,
|
||||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
8.dp
|
8.dp
|
||||||
),
|
),
|
||||||
state = state,
|
state = state,
|
||||||
@@ -242,7 +226,7 @@ fun VideoScreen(
|
|||||||
items = vb,
|
items = vb,
|
||||||
key = { "${it.first}/${it.second}" }
|
key = { "${it.first}/${it.second}" }
|
||||||
) { video ->
|
) { video ->
|
||||||
androidx.compose.foundation.layout.Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
@@ -320,139 +304,3 @@ fun CatalogueItemRow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun VideoCard(
|
|
||||||
videos: List<Video>,
|
|
||||||
navController: NavHostController,
|
|
||||||
videoScreenViewModel: VideoScreenViewModel
|
|
||||||
) {
|
|
||||||
val tabIndex by videoScreenViewModel.tabIndex;
|
|
||||||
val video = videos.first()
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = {
|
|
||||||
updateRelate(
|
|
||||||
videoScreenViewModel.videoLibrary.classesMap[videoScreenViewModel.videoLibrary.classes[tabIndex]]
|
|
||||||
?: mutableStateListOf(), video
|
|
||||||
)
|
|
||||||
val vg = videos.joinToString(",") { "${it.klass}/${it.id}" }.toHex()
|
|
||||||
val route = "video_player_route/$vg"
|
|
||||||
navController.navigate(route)
|
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
videoScreenViewModel.viewModelScope.launch {
|
|
||||||
for(i in videos)
|
|
||||||
{
|
|
||||||
videoScreenViewModel.download(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Toast.makeText(
|
|
||||||
videoScreenViewModel.context,
|
|
||||||
"Start downloading ${video.video.group}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(6.dp),
|
|
||||||
) {
|
|
||||||
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.Fit,
|
|
||||||
imageLoader = videoScreenViewModel.imageLoader!!
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(24.dp)
|
|
||||||
.background(
|
|
||||||
brush = Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.Transparent,
|
|
||||||
Color.Black.copy(alpha = 0.6f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomStart)
|
|
||||||
.padding(horizontal = 2.dp),
|
|
||||||
text = "${videos.size} Videos",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
lineHeight = 13.sp,
|
|
||||||
color = Color.White
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.padding(horizontal = 2.dp),
|
|
||||||
text = formatTime(video.video.duration),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
lineHeight = 13.sp,
|
|
||||||
color = Color.White
|
|
||||||
)
|
|
||||||
|
|
||||||
if (videos.all{ it.isLocal })
|
|
||||||
Card(
|
|
||||||
Modifier
|
|
||||||
.align(Alignment.TopStart)
|
|
||||||
.padding(5.dp)
|
|
||||||
.widthIn(max = 46.dp)
|
|
||||||
) {
|
|
||||||
Box(Modifier.fillMaxWidth())
|
|
||||||
{
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.align(Alignment.Center),
|
|
||||||
text = "Local",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = video.video.group ?: video.video.name,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
maxLines = 4,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.background(Color.Transparent)
|
|
||||||
.heightIn(min = 24.dp),
|
|
||||||
lineHeight = 14.sp
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
) {
|
|
||||||
Text("Class: ", fontSize = 10.sp, maxLines = 1)
|
|
||||||
Text(video.klass, fontSize = 10.sp, maxLines = 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
package com.acitelight.aether.viewModel
|
package com.acitelight.aether.viewModel
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
@@ -14,7 +15,7 @@ import com.acitelight.aether.model.BookMark
|
|||||||
import com.acitelight.aether.model.Comic
|
import com.acitelight.aether.model.Comic
|
||||||
import com.acitelight.aether.model.ComicRecord
|
import com.acitelight.aether.model.ComicRecord
|
||||||
import com.acitelight.aether.model.ComicRecordDatabase
|
import com.acitelight.aether.model.ComicRecordDatabase
|
||||||
import com.acitelight.aether.service.ApiClient.createOkHttp
|
import com.acitelight.aether.service.ApiClient
|
||||||
import com.acitelight.aether.service.MediaManager
|
import com.acitelight.aether.service.MediaManager
|
||||||
import com.acitelight.aether.service.RecentManager
|
import com.acitelight.aether.service.RecentManager
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@@ -26,9 +27,13 @@ import javax.inject.Inject
|
|||||||
class ComicGridViewModel @Inject constructor(
|
class ComicGridViewModel @Inject constructor(
|
||||||
@ApplicationContext val context: Context,
|
@ApplicationContext val context: Context,
|
||||||
val mediaManager: MediaManager,
|
val mediaManager: MediaManager,
|
||||||
val recentManager: RecentManager
|
val recentManager: RecentManager,
|
||||||
|
val apiClient: ApiClient
|
||||||
) : ViewModel()
|
) : ViewModel()
|
||||||
{
|
{
|
||||||
|
var coverHeight by mutableStateOf(220.dp)
|
||||||
|
var maxHeight = 0.dp
|
||||||
|
|
||||||
var imageLoader: ImageLoader? = null
|
var imageLoader: ImageLoader? = null
|
||||||
var comic = mutableStateOf<Comic?>(null)
|
var comic = mutableStateOf<Comic?>(null)
|
||||||
val chapterList = mutableStateListOf<BookMark>()
|
val chapterList = mutableStateListOf<BookMark>()
|
||||||
@@ -38,7 +43,7 @@ class ComicGridViewModel @Inject constructor(
|
|||||||
init {
|
init {
|
||||||
imageLoader = ImageLoader.Builder(context)
|
imageLoader = ImageLoader.Builder(context)
|
||||||
.components {
|
.components {
|
||||||
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
add(OkHttpNetworkFetcherFactory(apiClient.getClient()))
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
db = try{
|
db = try{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
|||||||
import com.acitelight.aether.model.Comic
|
import com.acitelight.aether.model.Comic
|
||||||
import com.acitelight.aether.model.ComicRecord
|
import com.acitelight.aether.model.ComicRecord
|
||||||
import com.acitelight.aether.model.ComicRecordDatabase
|
import com.acitelight.aether.model.ComicRecordDatabase
|
||||||
import com.acitelight.aether.service.ApiClient.createOkHttp
|
import com.acitelight.aether.service.ApiClient
|
||||||
import com.acitelight.aether.service.MediaManager
|
import com.acitelight.aether.service.MediaManager
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
@@ -28,7 +28,8 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ComicPageViewModel @Inject constructor(
|
class ComicPageViewModel @Inject constructor(
|
||||||
val mediaManager: MediaManager,
|
val mediaManager: MediaManager,
|
||||||
@ApplicationContext private val context: Context
|
@ApplicationContext private val context: Context,
|
||||||
|
val apiClient: ApiClient
|
||||||
) : ViewModel()
|
) : ViewModel()
|
||||||
{
|
{
|
||||||
var imageLoader: ImageLoader? = null
|
var imageLoader: ImageLoader? = null
|
||||||
@@ -43,7 +44,7 @@ class ComicPageViewModel @Inject constructor(
|
|||||||
init{
|
init{
|
||||||
imageLoader = ImageLoader.Builder(context)
|
imageLoader = ImageLoader.Builder(context)
|
||||||
.components {
|
.components {
|
||||||
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
add(OkHttpNetworkFetcherFactory(apiClient.getClient()))
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
listState = LazyListState(0, 0)
|
listState = LazyListState(0, 0)
|
||||||
|
|||||||
@@ -1,32 +1,30 @@
|
|||||||
package com.acitelight.aether.viewModel
|
package com.acitelight.aether.viewModel
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
import com.acitelight.aether.model.Comic
|
import com.acitelight.aether.model.Comic
|
||||||
import com.acitelight.aether.model.ComicResponse
|
import com.acitelight.aether.service.ApiClient
|
||||||
import com.acitelight.aether.service.ApiClient.createOkHttp
|
|
||||||
import com.acitelight.aether.service.MediaManager
|
import com.acitelight.aether.service.MediaManager
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ComicScreenViewModel @Inject constructor(
|
class ComicScreenViewModel @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
val mediaManager: MediaManager
|
val mediaManager: MediaManager,
|
||||||
|
val apiClient: ApiClient
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
var imageLoader: ImageLoader? = null;
|
var imageLoader: ImageLoader? = null;
|
||||||
|
|
||||||
|
val searchFilter = mutableStateOf("")
|
||||||
val comics = mutableStateListOf<Comic>()
|
val comics = mutableStateListOf<Comic>()
|
||||||
val excluded = mutableStateListOf<String>()
|
val excluded = mutableStateListOf<String>()
|
||||||
val included = mutableStateListOf<String>()
|
val included = mutableStateListOf<String>()
|
||||||
@@ -54,7 +52,7 @@ class ComicScreenViewModel @Inject constructor(
|
|||||||
init {
|
init {
|
||||||
imageLoader = ImageLoader.Builder(context)
|
imageLoader = ImageLoader.Builder(context)
|
||||||
.components {
|
.components {
|
||||||
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
add(OkHttpNetworkFetcherFactory(apiClient.getClient()))
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -63,7 +61,7 @@ class ComicScreenViewModel @Inject constructor(
|
|||||||
val m = mediaManager.queryComicInfoBulk(l)
|
val m = mediaManager.queryComicInfoBulk(l)
|
||||||
|
|
||||||
if(m != null) {
|
if(m != null) {
|
||||||
comics.addAll(m.sortedWith(compareBy(naturalOrder()) { it.comic.comic_name }))
|
comics.addAll(m.sortedBy { it.id.toInt() }.reversed())
|
||||||
tags.addAll(m.flatMap { it.comic.tags }.groupingBy { it }.eachCount()
|
tags.addAll(m.flatMap { it.comic.tags }.groupingBy { it }.eachCount()
|
||||||
.entries.sortedByDescending { it.value }
|
.entries.sortedByDescending { it.value }
|
||||||
.map { it.key })
|
.map { it.key })
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
import com.acitelight.aether.service.ApiClient.createOkHttp
|
import com.acitelight.aether.service.ApiClient
|
||||||
import com.acitelight.aether.service.RecentManager
|
import com.acitelight.aether.service.RecentManager
|
||||||
import com.acitelight.aether.service.VideoLibrary
|
import com.acitelight.aether.service.VideoLibrary
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -19,6 +19,7 @@ class HomeScreenViewModel @Inject constructor(
|
|||||||
val recentManager: RecentManager,
|
val recentManager: RecentManager,
|
||||||
@ApplicationContext val context: Context,
|
@ApplicationContext val context: Context,
|
||||||
val videoLibrary: VideoLibrary,
|
val videoLibrary: VideoLibrary,
|
||||||
|
val apiClient: ApiClient
|
||||||
) : ViewModel()
|
) : ViewModel()
|
||||||
{
|
{
|
||||||
var imageLoader: ImageLoader? = null
|
var imageLoader: ImageLoader? = null
|
||||||
@@ -26,7 +27,7 @@ class HomeScreenViewModel @Inject constructor(
|
|||||||
init{
|
init{
|
||||||
imageLoader = ImageLoader.Builder(context)
|
imageLoader = ImageLoader.Builder(context)
|
||||||
.components {
|
.components {
|
||||||
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
add(OkHttpNetworkFetcherFactory(apiClient.getClient()))
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.acitelight.aether.viewModel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.acitelight.aether.service.ApiClient
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class LiveScreenViewModel @Inject constructor(
|
||||||
|
val apiClient: ApiClient
|
||||||
|
) : ViewModel(){
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,46 +1,42 @@
|
|||||||
package com.acitelight.aether.viewModel
|
package com.acitelight.aether.viewModel
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.datastore.preferences.core.edit
|
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.acitelight.aether.AetherApp
|
import com.acitelight.aether.AetherApp
|
||||||
import com.acitelight.aether.Global
|
import com.acitelight.aether.Global
|
||||||
import com.acitelight.aether.dataStore
|
|
||||||
import com.acitelight.aether.model.Video
|
|
||||||
import com.acitelight.aether.service.ApiClient
|
import com.acitelight.aether.service.ApiClient
|
||||||
import com.acitelight.aether.service.AuthManager
|
import com.acitelight.aether.service.AuthManager
|
||||||
import com.acitelight.aether.service.MediaManager
|
import com.acitelight.aether.service.MediaManager
|
||||||
import kotlinx.coroutines.flow.Flow
|
import com.acitelight.aether.service.SettingsDataStoreManager
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
|
||||||
import com.acitelight.aether.service.*
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.net.DatagramPacket
|
||||||
|
import java.net.DatagramSocket
|
||||||
|
import java.net.InetAddress
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MeScreenViewModel @Inject constructor(
|
class MeScreenViewModel @Inject constructor(
|
||||||
private val settingsDataStoreManager: SettingsDataStoreManager,
|
private val settingsDataStoreManager: SettingsDataStoreManager,
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
val mediaManager: MediaManager
|
val mediaManager: MediaManager,
|
||||||
|
private val apiClient: ApiClient,
|
||||||
|
private val authManager: AuthManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val username = mutableStateOf("");
|
val username = mutableStateOf("")
|
||||||
val privateKey = mutableStateOf("")
|
val privateKey = mutableStateOf("")
|
||||||
val url = mutableStateOf("");
|
val url = mutableStateOf("")
|
||||||
val cert = mutableStateOf("")
|
val cert = mutableStateOf("")
|
||||||
|
val pak = mutableStateOf("")
|
||||||
|
|
||||||
val uss = settingsDataStoreManager.useSelfSignedFlow
|
val uss = settingsDataStoreManager.useSelfSignedFlow
|
||||||
|
|
||||||
@@ -54,18 +50,17 @@ class MeScreenViewModel @Inject constructor(
|
|||||||
if(username.value=="" || privateKey.value=="" || url.value=="") return@launch
|
if(username.value=="" || privateKey.value=="" || url.value=="") return@launch
|
||||||
|
|
||||||
try{
|
try{
|
||||||
val usedUrl = ApiClient.apply(context, url.value, if(uss.first()) cert.value else "")
|
apiClient.apply(context, url.value, if(uss.first()) cert.value else "")
|
||||||
|
|
||||||
if (mediaManager.token == "null")
|
authManager.fetchToken(
|
||||||
mediaManager.token = AuthManager.fetchToken(
|
username.value,
|
||||||
username.value,
|
settingsDataStoreManager.privateKeyFlow.first()
|
||||||
settingsDataStoreManager.privateKeyFlow.first()
|
)!!
|
||||||
)!!
|
|
||||||
|
|
||||||
Global.loggedIn = true
|
Global.loggedIn = true
|
||||||
withContext(Dispatchers.IO)
|
withContext(Dispatchers.IO)
|
||||||
{
|
{
|
||||||
(context as AetherApp).abyssService?.proxy?.config(ApiClient.getBase().toUri().host!!, 4096)
|
(context as AetherApp).abyssService?.proxy?.config(apiClient.getBase().toUri().host!!, 4096)
|
||||||
context.abyssService?.downloader?.init()
|
context.abyssService?.downloader?.init()
|
||||||
}
|
}
|
||||||
}catch(e: Exception)
|
}catch(e: Exception)
|
||||||
@@ -100,10 +95,10 @@ class MeScreenViewModel @Inject constructor(
|
|||||||
if (u == "" || p == "" || us == "") return@launch
|
if (u == "" || p == "" || us == "") return@launch
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val usedUrl = ApiClient.apply(context, u, if(uss.first()) c else "")
|
val usedUrl = apiClient.apply(context, u, if(uss.first()) c else "")
|
||||||
(context as AetherApp).abyssService?.proxy?.config(ApiClient.getBase().toUri().host!!, 4096)
|
(context as AetherApp).abyssService?.proxy?.config(apiClient.getBase().toUri().host!!, 4096)
|
||||||
context.abyssService?.downloader?.init()
|
context.abyssService?.downloader?.init()
|
||||||
mediaManager.token = AuthManager.fetchToken(
|
authManager.fetchToken(
|
||||||
us,
|
us,
|
||||||
p
|
p
|
||||||
)!!
|
)!!
|
||||||
@@ -117,7 +112,8 @@ class MeScreenViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAccount(u: String, p: String) {
|
fun updateAccount(u: String, p: String)
|
||||||
|
{
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsDataStoreManager.saveUserName(u)
|
settingsDataStoreManager.saveUserName(u)
|
||||||
settingsDataStoreManager.savePrivateKey(p)
|
settingsDataStoreManager.savePrivateKey(p)
|
||||||
@@ -133,7 +129,7 @@ class MeScreenViewModel @Inject constructor(
|
|||||||
if (u == "" || p == "" || ur == "") return@launch
|
if (u == "" || p == "" || ur == "") return@launch
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mediaManager.token = AuthManager.fetchToken(
|
authManager.fetchToken(
|
||||||
u,
|
u,
|
||||||
p
|
p
|
||||||
)!!
|
)!!
|
||||||
@@ -141,7 +137,7 @@ class MeScreenViewModel @Inject constructor(
|
|||||||
Global.loggedIn = true
|
Global.loggedIn = true
|
||||||
withContext(Dispatchers.IO)
|
withContext(Dispatchers.IO)
|
||||||
{
|
{
|
||||||
(context as AetherApp).abyssService?.proxy?.config(ApiClient.getBase().toUri().host!!, 4096)
|
(context as AetherApp).abyssService?.proxy?.config(apiClient.getBase().toUri().host!!, 4096)
|
||||||
context.abyssService?.downloader?.init()
|
context.abyssService?.downloader?.init()
|
||||||
}
|
}
|
||||||
Toast.makeText(context, "Account Updated", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Account Updated", Toast.LENGTH_SHORT).show()
|
||||||
@@ -151,4 +147,22 @@ class MeScreenViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendPacket(p: String)
|
||||||
|
{
|
||||||
|
val b = (p + "\r\n").toByteArray(Charsets.UTF_8)
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val addr = InetAddress.getByName(apiClient.getDomain())
|
||||||
|
|
||||||
|
val socket = DatagramSocket()
|
||||||
|
val packet = DatagramPacket(
|
||||||
|
b, b.size, addr, 4096
|
||||||
|
)
|
||||||
|
|
||||||
|
socket.send(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,22 +5,28 @@ import androidx.compose.runtime.mutableStateListOf
|
|||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
|
import com.acitelight.aether.Global.updateRelate
|
||||||
import com.acitelight.aether.model.Video
|
import com.acitelight.aether.model.Video
|
||||||
import com.acitelight.aether.model.VideoDownloadItemState
|
import com.acitelight.aether.model.VideoDownloadItemState
|
||||||
import com.acitelight.aether.service.ApiClient.createOkHttp
|
import com.acitelight.aether.service.ApiClient
|
||||||
import com.acitelight.aether.service.FetchManager
|
import com.acitelight.aether.service.FetchManager
|
||||||
import com.acitelight.aether.service.MediaManager
|
import com.acitelight.aether.service.MediaManager
|
||||||
import com.acitelight.aether.service.VideoLibrary
|
import com.acitelight.aether.service.VideoLibrary
|
||||||
|
import com.acitelight.aether.view.pages.toHex
|
||||||
import com.tonyodev.fetch2.Download
|
import com.tonyodev.fetch2.Download
|
||||||
import com.tonyodev.fetch2.FetchListener
|
import com.tonyodev.fetch2.FetchListener
|
||||||
|
import com.tonyodev.fetch2.Status
|
||||||
import com.tonyodev.fetch2core.DownloadBlock
|
import com.tonyodev.fetch2core.DownloadBlock
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
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 kotlinx.serialization.json.Json
|
||||||
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -29,6 +35,7 @@ class TransmissionScreenViewModel @Inject constructor(
|
|||||||
@ApplicationContext val context: Context,
|
@ApplicationContext val context: Context,
|
||||||
val videoLibrary: VideoLibrary,
|
val videoLibrary: VideoLibrary,
|
||||||
val mediaManager: MediaManager,
|
val mediaManager: MediaManager,
|
||||||
|
val apiClient: ApiClient
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
var imageLoader: ImageLoader? = null
|
var imageLoader: ImageLoader? = null
|
||||||
val downloads: SnapshotStateList<VideoDownloadItemState> = mutableStateListOf()
|
val downloads: SnapshotStateList<VideoDownloadItemState> = mutableStateListOf()
|
||||||
@@ -128,7 +135,7 @@ class TransmissionScreenViewModel @Inject constructor(
|
|||||||
|
|
||||||
val state = downloadToState(download)
|
val state = downloadToState(download)
|
||||||
|
|
||||||
if (videoLibrary.classes.contains(state.klass)) videoLibrary.classes.add(state.klass)
|
if (!videoLibrary.classes.contains(state.klass)) videoLibrary.classes.add(state.klass)
|
||||||
|
|
||||||
if (!videoLibrary.classesMap.containsKey(state.klass)) videoLibrary.classesMap[state.klass] =
|
if (!videoLibrary.classesMap.containsKey(state.klass)) videoLibrary.classesMap[state.klass] =
|
||||||
mutableStateListOf()
|
mutableStateListOf()
|
||||||
@@ -204,8 +211,8 @@ class TransmissionScreenViewModel @Inject constructor(
|
|||||||
// UI actions delegated to FetchManager
|
// UI actions delegated to FetchManager
|
||||||
fun pause(id: Int) = fetchManager.pause(id)
|
fun pause(id: Int) = fetchManager.pause(id)
|
||||||
fun resume(id: Int) = fetchManager.resume(id)
|
fun resume(id: Int) = fetchManager.resume(id)
|
||||||
fun cancel(id: Int) = fetchManager.cancel(id)
|
fun retry(id: Int) = fetchManager.retry(id)
|
||||||
fun delete(id: Int, deleteFile: Boolean = true) {
|
fun delete(id: Int) {
|
||||||
fetchManager.delete(id) {
|
fetchManager.delete(id) {
|
||||||
viewModelScope.launch(Dispatchers.Main) { removeOnMain(id) }
|
viewModelScope.launch(Dispatchers.Main) { removeOnMain(id) }
|
||||||
}
|
}
|
||||||
@@ -216,9 +223,59 @@ class TransmissionScreenViewModel @Inject constructor(
|
|||||||
fetchManager.removeListener()
|
fetchManager.removeListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun playStart(model: VideoDownloadItemState, navigator: NavHostController)
|
||||||
|
{
|
||||||
|
val downloaded = fetchManager.getAllDownloadsAsync().filter {
|
||||||
|
it.status == Status.COMPLETED && it.extras.getString(
|
||||||
|
"class",
|
||||||
|
""
|
||||||
|
) != "comic" && it.extras.getString(
|
||||||
|
"type",
|
||||||
|
""
|
||||||
|
) == "main"
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsonQuery = downloaded.map {
|
||||||
|
File(
|
||||||
|
context.getExternalFilesDir(null),
|
||||||
|
"videos/${
|
||||||
|
it.extras.getString(
|
||||||
|
"class",
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}/${it.extras.getString("id", "")}/summary.json"
|
||||||
|
).readText()
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
Json.decodeFromString<Video>(it)
|
||||||
|
.toLocal(context.getExternalFilesDir(null)!!.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRelate(
|
||||||
|
jsonQuery,
|
||||||
|
jsonQuery.first { it.id == model.vid && it.klass == model.klass }
|
||||||
|
)
|
||||||
|
|
||||||
|
val playList = mutableListOf<String>()
|
||||||
|
val fv = videoLibrary.classesMap.map { it.value }.flatten()
|
||||||
|
val video = fv.firstOrNull { it.klass == model.klass && it.id == model.vid }
|
||||||
|
|
||||||
|
if (video != null) {
|
||||||
|
val group = fv.filter { it.klass == video.klass && it.video.group == video.video.group && it.video.group != "null" }
|
||||||
|
for (i in group.sortedWith(compareBy(naturalOrder()) { it.video.name })) {
|
||||||
|
playList.add("${i.klass}/${i.id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val route = "video_player_route/${(playList.joinToString(",") + "|${model.vid}").toHex()}"
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
navigator.navigate(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
imageLoader = ImageLoader.Builder(context).components {
|
imageLoader = ImageLoader.Builder(context).components {
|
||||||
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
add(OkHttpNetworkFetcherFactory(apiClient.getClient()))
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -227,12 +284,12 @@ class TransmissionScreenViewModel @Inject constructor(
|
|||||||
|
|
||||||
downloads.clear()
|
downloads.clear()
|
||||||
idToState.clear()
|
idToState.clear()
|
||||||
downloaded.filter { it.extras.getString("type", "") == "main" }.sortedWith(compareBy(naturalOrder()) { it.extras.getString("name", "") })
|
downloaded.forEach { d ->
|
||||||
.forEach { d ->
|
val s = downloadToState(d)
|
||||||
val s = downloadToState(d)
|
downloads.add(s)
|
||||||
downloads.add(s)
|
idToState[s.id] = s
|
||||||
idToState[s.id] = s
|
|
||||||
|
|
||||||
|
if (d.extras.getString("type", "") == "main") {
|
||||||
if (!videoLibrary.classes.contains(s.klass))
|
if (!videoLibrary.classes.contains(s.klass))
|
||||||
videoLibrary.classes.add(s.klass)
|
videoLibrary.classes.add(s.klass)
|
||||||
|
|
||||||
@@ -246,6 +303,7 @@ class TransmissionScreenViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,50 +8,48 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.Player.STATE_ENDED
|
||||||
import androidx.media3.common.Player.STATE_READY
|
import androidx.media3.common.Player.STATE_READY
|
||||||
|
import androidx.media3.common.Tracks
|
||||||
import androidx.media3.common.text.Cue
|
import androidx.media3.common.text.Cue
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.datasource.DefaultDataSource
|
||||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||||
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
|
import com.acitelight.aether.model.KeyImage
|
||||||
import com.acitelight.aether.model.Video
|
import com.acitelight.aether.model.Video
|
||||||
import com.acitelight.aether.model.VideoQueryIndex
|
import com.acitelight.aether.model.VideoQueryIndex
|
||||||
import com.acitelight.aether.model.VideoRecord
|
import com.acitelight.aether.model.VideoRecord
|
||||||
import com.acitelight.aether.model.VideoRecordDatabase
|
import com.acitelight.aether.model.VideoRecordDatabase
|
||||||
import com.acitelight.aether.service.ApiClient.createOkHttp
|
import com.acitelight.aether.service.ApiClient
|
||||||
import com.acitelight.aether.service.MediaManager
|
import com.acitelight.aether.service.MediaManager
|
||||||
import com.acitelight.aether.service.RecentManager
|
import com.acitelight.aether.service.RecentManager
|
||||||
import com.acitelight.aether.view.formatTime
|
import com.acitelight.aether.service.VideoLibrary
|
||||||
import com.acitelight.aether.view.hexToString
|
import com.acitelight.aether.view.pages.formatTime
|
||||||
|
import com.acitelight.aether.view.pages.hexToString
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.media3.common.Tracks
|
|
||||||
import androidx.media3.datasource.DefaultDataSource
|
|
||||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
|
||||||
import com.acitelight.aether.Global
|
|
||||||
import com.acitelight.aether.model.KeyImage
|
|
||||||
import com.acitelight.aether.service.VideoLibrary
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class VideoPlayerViewModel @Inject constructor(
|
class VideoPlayerViewModel @Inject constructor(
|
||||||
@@ -59,6 +57,7 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
val mediaManager: MediaManager,
|
val mediaManager: MediaManager,
|
||||||
val recentManager: RecentManager,
|
val recentManager: RecentManager,
|
||||||
val videoLibrary: VideoLibrary,
|
val videoLibrary: VideoLibrary,
|
||||||
|
val apiClient: ApiClient
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
var showPlaylist by mutableStateOf(false)
|
var showPlaylist by mutableStateOf(false)
|
||||||
var isLandscape by mutableStateOf(false)
|
var isLandscape by mutableStateOf(false)
|
||||||
@@ -79,7 +78,7 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
var renderedFirst = false
|
var renderedFirst = false
|
||||||
var videos: List<Video> = listOf()
|
var videos: List<Video> = listOf()
|
||||||
|
|
||||||
private val httpDataSourceFactory = OkHttpDataSource.Factory(createOkHttp())
|
private val httpDataSourceFactory = OkHttpDataSource.Factory(apiClient.getClient())
|
||||||
private val defaultDataSourceFactory by lazy {
|
private val defaultDataSourceFactory by lazy {
|
||||||
DefaultDataSource.Factory(
|
DefaultDataSource.Factory(
|
||||||
context,
|
context,
|
||||||
@@ -117,7 +116,7 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
|
|
||||||
imageLoader = ImageLoader.Builder(context)
|
imageLoader = ImageLoader.Builder(context)
|
||||||
.components {
|
.components {
|
||||||
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
add(OkHttpNetworkFetcherFactory(apiClient.getClient()))
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -156,7 +155,7 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val client = createOkHttp()
|
val client = apiClient.getClient()
|
||||||
|
|
||||||
val headReq = Request.Builder().url(trimmed).head().build()
|
val headReq = Request.Builder().url(trimmed).head().build()
|
||||||
val headResp = try {
|
val headResp = try {
|
||||||
@@ -240,7 +239,7 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
currentKlass.value = video.klass
|
currentKlass.value = video.klass
|
||||||
currentName.value = video.video.name
|
currentName.value = video.video.name
|
||||||
currentDuration.longValue = video.video.duration
|
currentDuration.longValue = video.video.duration
|
||||||
currentGallery.value = video.getGallery()
|
currentGallery.value = video.getGallery(apiClient)
|
||||||
|
|
||||||
player?.apply {
|
player?.apply {
|
||||||
stop()
|
stop()
|
||||||
@@ -249,7 +248,7 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
|
|
||||||
recentManager.pushVideo(context, VideoQueryIndex(video.klass, video.id))
|
recentManager.pushVideo(context, VideoQueryIndex(video.klass, video.id))
|
||||||
|
|
||||||
val subtitleCandidate = video.getSubtitle().trim()
|
val subtitleCandidate = video.getSubtitle(apiClient).trim()
|
||||||
val subtitleUri = tryResolveSubtitleUri(subtitleCandidate)
|
val subtitleUri = tryResolveSubtitleUri(subtitleCandidate)
|
||||||
|
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
@@ -270,8 +269,18 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
if (playbackState == STATE_READY) {
|
when(playbackState)
|
||||||
startPlaying = true
|
{
|
||||||
|
STATE_READY -> {
|
||||||
|
startPlaying = true
|
||||||
|
}
|
||||||
|
STATE_ENDED -> {
|
||||||
|
player?.seekTo(0)
|
||||||
|
player?.pause()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +312,7 @@ class VideoPlayerViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val url = video.getVideo()
|
val url = video.getVideo(apiClient)
|
||||||
val videoUri = if (video.isLocal) Uri.fromFile(File(url)) else url.toUri()
|
val videoUri = if (video.isLocal) Uri.fromFile(File(url)) else url.toUri()
|
||||||
|
|
||||||
val mediaItem: MediaItem = if (subtitleUri != null) {
|
val mediaItem: MediaItem = if (subtitleUri != null) {
|
||||||
|
|||||||
@@ -1,25 +1,19 @@
|
|||||||
package com.acitelight.aether.viewModel
|
package com.acitelight.aether.viewModel
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.State
|
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.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
import com.acitelight.aether.Global
|
import com.acitelight.aether.Global
|
||||||
import com.acitelight.aether.model.Video
|
import com.acitelight.aether.model.Video
|
||||||
import com.acitelight.aether.service.ApiClient.createOkHttp
|
import com.acitelight.aether.service.ApiClient
|
||||||
import com.acitelight.aether.service.FetchManager
|
import com.acitelight.aether.service.FetchManager
|
||||||
import com.acitelight.aether.service.MediaManager
|
import com.acitelight.aether.service.MediaManager
|
||||||
import com.acitelight.aether.service.RecentManager
|
|
||||||
import com.acitelight.aether.service.VideoLibrary
|
import com.acitelight.aether.service.VideoLibrary
|
||||||
import com.tonyodev.fetch2.Status
|
import com.tonyodev.fetch2.Status
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
@@ -31,20 +25,19 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class VideoScreenViewModel @Inject constructor(
|
class VideoScreenViewModel @Inject constructor(
|
||||||
private val fetchManager: FetchManager,
|
val fetchManager: FetchManager,
|
||||||
@ApplicationContext val context: Context,
|
@ApplicationContext val context: Context,
|
||||||
val mediaManager: MediaManager,
|
val mediaManager: MediaManager,
|
||||||
val recentManager: RecentManager,
|
val videoLibrary: VideoLibrary,
|
||||||
val videoLibrary: VideoLibrary
|
val apiClient: ApiClient
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
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 menuVisibility = mutableStateOf(false)
|
||||||
var searchFilter = mutableStateOf("")
|
var searchFilter = mutableStateOf("")
|
||||||
var doneInit = mutableStateOf(false)
|
var doneInit = mutableStateOf(false)
|
||||||
@@ -53,8 +46,9 @@ class VideoScreenViewModel @Inject constructor(
|
|||||||
fetchManager.configured.filter { it }.first()
|
fetchManager.configured.filter { it }.first()
|
||||||
|
|
||||||
if (Global.loggedIn) {
|
if (Global.loggedIn) {
|
||||||
videoLibrary.classes.addAll(mediaManager.listVideoKlasses())
|
videoLibrary.classes.addAll(
|
||||||
videoLibrary.classes.distinct()
|
mediaManager.listVideoKlasses().filter { it !in videoLibrary.classes }
|
||||||
|
)
|
||||||
|
|
||||||
if(videoLibrary.classes.isEmpty())
|
if(videoLibrary.classes.isEmpty())
|
||||||
return
|
return
|
||||||
@@ -79,10 +73,12 @@ class VideoScreenViewModel @Inject constructor(
|
|||||||
else {
|
else {
|
||||||
videoLibrary.classes.add("Offline")
|
videoLibrary.classes.add("Offline")
|
||||||
videoLibrary.updatingMap[0] = true
|
videoLibrary.updatingMap[0] = true
|
||||||
videoLibrary.classesMap["Offline"] = mutableStateListOf<Video>()
|
videoLibrary.classesMap["Offline"] = mutableStateListOf()
|
||||||
|
|
||||||
val downloaded = fetchManager.getAllDownloadsAsync().filter {
|
val downloaded = fetchManager.getAllDownloadsAsync().filter {
|
||||||
it.status == Status.COMPLETED && it.extras.getString("class", "") != "comic"
|
it.status == Status.COMPLETED &&
|
||||||
|
it.extras.getString("class", "") != "comic" &&
|
||||||
|
it.extras.getString("type", "") == "main"
|
||||||
}
|
}
|
||||||
|
|
||||||
val jsonQuery = downloaded.map{ File(
|
val jsonQuery = downloaded.map{ File(
|
||||||
@@ -99,7 +95,7 @@ class VideoScreenViewModel @Inject constructor(
|
|||||||
fun setTabIndex(index: Int) {
|
fun setTabIndex(index: Int) {
|
||||||
viewModelScope.launch()
|
viewModelScope.launch()
|
||||||
{
|
{
|
||||||
_tabIndex.intValue = index;
|
_tabIndex.intValue = index
|
||||||
if (videoLibrary.updatingMap[index] == true) return@launch
|
if (videoLibrary.updatingMap[index] == true) return@launch
|
||||||
|
|
||||||
videoLibrary.updatingMap[index] = true
|
videoLibrary.updatingMap[index] = true
|
||||||
@@ -124,7 +120,7 @@ class VideoScreenViewModel @Inject constructor(
|
|||||||
init {
|
init {
|
||||||
imageLoader = ImageLoader.Builder(context)
|
imageLoader = ImageLoader.Builder(context)
|
||||||
.components {
|
.components {
|
||||||
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
add(OkHttpNetworkFetcherFactory(apiClient.getClient()))
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
accompanistNavigationAnimation = "0.37.3"
|
||||||
agp = "8.13.0"
|
agp = "8.13.0"
|
||||||
ariaCompiler = "latest"
|
ariaCompiler = "latest"
|
||||||
bcprovJdk15on = "1.70"
|
bcprovJdk15on = "1.70"
|
||||||
@@ -10,35 +11,40 @@ datastorePreferences = "1.1.7"
|
|||||||
exoplayerplus = "0.2.0"
|
exoplayerplus = "0.2.0"
|
||||||
fetch2 = "3.4.1"
|
fetch2 = "3.4.1"
|
||||||
fetch2okhttp = "3.4.1"
|
fetch2okhttp = "3.4.1"
|
||||||
gson = "2.13.1"
|
gson = "2.13.2"
|
||||||
kotlin = "2.2.20"
|
kotlin = "2.2.20"
|
||||||
coreKtx = "1.17.0"
|
coreKtx = "1.17.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.3.0"
|
junitVersion = "1.3.0"
|
||||||
espressoCore = "3.7.0"
|
espressoCore = "3.7.0"
|
||||||
kotlinxSerializationJson = "1.9.0"
|
kotlinxSerializationJson = "1.9.0"
|
||||||
lifecycleRuntimeKtx = "2.9.3"
|
lifecycleRuntimeKtx = "2.9.4"
|
||||||
activityCompose = "1.11.0"
|
activityCompose = "1.11.0"
|
||||||
composeBom = "2025.09.00"
|
composeBom = "2025.09.01"
|
||||||
media3Common = "1.8.0"
|
media3Common = "1.8.0"
|
||||||
media3Exoplayer = "1.8.0"
|
media3Exoplayer = "1.8.0"
|
||||||
media3ExoplayerFfmpeg = "1.8.0"
|
media3ExoplayerFfmpeg = "1.8.0"
|
||||||
media3Ui = "1.8.0"
|
media3Ui = "1.8.0"
|
||||||
navigationCompose = "2.9.4"
|
navigationCompose = "2.9.5"
|
||||||
okhttp = "5.1.0"
|
okhttp = "5.1.0"
|
||||||
|
persistentcookiejar = "1.0.1"
|
||||||
|
repo = "Tag"
|
||||||
retrofit = "3.0.0"
|
retrofit = "3.0.0"
|
||||||
retrofit2KotlinxSerializationConverter = "1.0.0"
|
retrofit2KotlinxSerializationConverter = "1.0.0"
|
||||||
media3DatasourceOkhttp = "1.8.0"
|
media3DatasourceOkhttp = "1.8.0"
|
||||||
roomCompiler = "2.8.0"
|
roomCompiler = "2.8.1"
|
||||||
roomKtx = "2.8.0"
|
roomKtx = "2.8.1"
|
||||||
roomRuntime = "2.8.0"
|
roomRuntime = "2.8.1"
|
||||||
|
|
||||||
ksp = "2.1.21-2.0.2"
|
ksp = "2.1.21-2.0.2"
|
||||||
hilt = "2.57.1"
|
hilt = "2.57.2"
|
||||||
hilt-navigation-compose = "1.3.0"
|
hilt-navigation-compose = "1.3.0"
|
||||||
composeMaterialCore = "1.5.1"
|
composeMaterialCore = "1.5.2"
|
||||||
|
constraintlayout = "2.2.1"
|
||||||
|
animation = "1.9.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
accompanist-navigation-animation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanistNavigationAnimation" }
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
|
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
|
||||||
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
|
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
|
||||||
@@ -72,6 +78,7 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
|
|||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
|
persistentcookiejar = { module = "com.github.franmontiel:PersistentCookieJar", version.ref = "persistentcookiejar" }
|
||||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||||
retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" }
|
retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" }
|
||||||
androidx-media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3DatasourceOkhttp" }
|
androidx-media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3DatasourceOkhttp" }
|
||||||
@@ -80,6 +87,8 @@ hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref
|
|||||||
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
||||||
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
|
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
|
||||||
androidx-compose-material-core = { group = "androidx.wear.compose", name = "compose-material-core", version.ref = "composeMaterialCore" }
|
androidx-compose-material-core = { group = "androidx.wear.compose", name = "compose-material-core", version.ref = "composeMaterialCore" }
|
||||||
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
|
androidx-compose-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "animation" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user