20 Commits

Author SHA1 Message Date
rootacite
c3e0a23ed1 [update] Comic sort policy 2025-10-29 20:14:07 +08:00
acite
7be18dd517 [feat] Playlist remember 2025-10-09 11:33:34 +08:00
acite
a13ddbdd87 [feat] Live framework. 2025-10-06 22:54:08 +08:00
acite
390094b8b0 [fix&optimize] Fix the issue of element teleportation in ComicScreen pages 2025-10-02 12:24:26 +08:00
acite
b360724dca [fix] Chapter progress 2025-10-02 01:34:09 +08:00
acite
db8d5ef4d5 [fix] Cover height meansure 2025-10-02 01:20:13 +08:00
acite
2c4d5d2366 [fix] Too big cover 2025-10-01 23:04:16 +08:00
acite
200cf33e5a [feat] Adaptive coverHeight 2025-10-01 19:56:42 +08:00
acite
603c2c38aa [feat] UI optimization 3 2025-10-01 19:47:00 +08:00
acite
7c99ea394b [feat] Player Logic 2025-10-01 02:37:40 +08:00
acite
614a0d591d [fix] Playlist load logic 2025-10-01 02:07:19 +08:00
acite
24dda0eb2c [feat] Better UI 2 2025-10-01 02:04:37 +08:00
acite
c5a5826321 [fix] Fix the issue where the delete task button is invalid 2025-10-01 00:48:14 +08:00
acite
ba4811e65f [doc] Doc Update 2025-09-29 20:49:44 +08:00
acite
8b5adfd6b7 [feat] Better Transmission UI 2025-09-29 20:34:56 +08:00
acite
02d8d30da7 [fix] Download Logic 2025-09-29 04:02:15 +08:00
acite
422da51a74 [update] Remove token from Query param, move to cookies 2025-09-29 01:19:12 +08:00
acite
393419afd7 [optimize] Refactoring API client injection architecture 2025-09-28 14:31:03 +08:00
acite
88392444a4 [feat] Task Statistics 2025-09-28 02:03:20 +08:00
acite
2166229923 [fix] Offline mode display exception 2025-09-28 01:54:02 +08:00
50 changed files with 2133 additions and 1389 deletions

View File

@@ -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.

View File

@@ -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)

View File

@@ -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)) {

View File

@@ -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")

View File

@@ -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

View File

@@ -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]

View File

@@ -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
) )

View File

@@ -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
) )
} }

View File

@@ -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()

View File

@@ -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 */ }
} }

View File

@@ -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
} }
} }

View File

@@ -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(

View File

@@ -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{

View File

@@ -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),

View File

@@ -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)
{ {

View File

@@ -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,
)
}
}
}
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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))
) )
} }
} }

View File

@@ -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

View File

@@ -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)
)
}
}
}

View File

@@ -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

View File

@@ -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(

View File

@@ -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(

View File

@@ -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

View File

@@ -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,

View File

@@ -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
} }

View File

@@ -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)
}
}
}
}

View File

@@ -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 }
}

View File

@@ -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)
)
}
)
}
}
}
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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,
)
}
}
}
}
}
}

View File

@@ -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))
} }
} }
} }

View File

@@ -1,6 +1,5 @@
package com.acitelight.aether.view package com.acitelight.aether.view.pages
import android.nfc.Tag
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -14,56 +13,42 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.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.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card import androidx.compose.material.icons.Icons
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab
import androidx.compose.material3.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.runtime.setValue
import androidx.compose.ui.Alignment
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.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.Layout
import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.modifier.modifierLocalOf import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp 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.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil3.request.ImageRequest import com.acitelight.aether.view.components.ComicCard
import com.acitelight.aether.Global
import com.acitelight.aether.model.Comic
import com.acitelight.aether.viewModel.ComicScreenViewModel import com.acitelight.aether.viewModel.ComicScreenViewModel
import java.nio.charset.Charset
@Composable @Composable
fun VariableGrid( fun VariableGrid(
@@ -145,14 +130,57 @@ fun ComicScreen(
val included = comicScreenViewModel.included val included = comicScreenViewModel.included
val state = rememberLazyStaggeredGridState() val state = rememberLazyStaggeredGridState()
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
var searchFilter by comicScreenViewModel.searchFilter
Column { Column {
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.primary, 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)
)
}
}
VariableGrid( VariableGrid(
modifier = Modifier modifier = Modifier
.heightIn(max = 120.dp) .heightIn(max = 72.dp)
.padding(8.dp), .padding(4.dp),
rowHeight = 32.dp rowHeight = 30.dp
) )
{ {
for (i in comicScreenViewModel.tags) { for (i in comicScreenViewModel.tags) {
@@ -160,10 +188,11 @@ fun ComicScreen(
Box( Box(
Modifier Modifier
.background( .background(
if (included.contains(i)) Color.Green.copy(alpha = 0.65f) else colorScheme.primary, if (included.contains(i)) Color.Green.copy(alpha = 0.65f) else colorScheme.surface,
shape = RoundedCornerShape(4.dp) shape = RoundedCornerShape(4.dp)
) )
.height(32.dp).widthIn(max = 72.dp) .height(32.dp)
.widthIn(max = 72.dp)
.clickable { .clickable {
if (included.contains(i)) if (included.contains(i))
included.remove(i) included.remove(i)
@@ -184,25 +213,28 @@ fun ComicScreen(
} }
} }
HorizontalDivider(thickness = 1.5.dp) HorizontalDivider(Modifier.padding(1.dp), thickness = 1.5.dp)
LazyVerticalStaggeredGrid( LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Adaptive(136.dp), columns = StaggeredGridCells.Adaptive(120.dp),
contentPadding = PaddingValues(8.dp), contentPadding = PaddingValues(4.dp),
verticalItemSpacing = 8.dp, verticalItemSpacing = 6.dp,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
state = state, state = state,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
items( items(
items = comicScreenViewModel.comics.filter { x -> items = comicScreenViewModel.comics
included.all { y -> y in x.comic.tags } || included.isEmpty() .filter { searchFilter.isEmpty() || searchFilter in it.comic.comic_name }
}, .filter { x ->
included.all { y -> y in x.comic.tags } || included.isEmpty()
},
key = { it.id } key = { it.id }
) { comic -> ) { comic ->
Box(modifier = Modifier Box(
.fillMaxWidth() modifier = Modifier
.wrapContentHeight() .fillMaxWidth()
.wrapContentHeight()
) { ) {
ComicCard(comic, navController, comicScreenViewModel) ComicCard(comic, navController, comicScreenViewModel)
} }
@@ -210,86 +242,3 @@ fun ComicScreen(
} }
} }
} }
@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)
)
}
}
}

View File

@@ -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(),

View File

@@ -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>()
)
{
}

View File

@@ -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")
} }
} }
} }

View File

@@ -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 }
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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{

View File

@@ -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)

View File

@@ -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 })

View File

@@ -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 {

View File

@@ -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(){
}

View File

@@ -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)
}
}
}
} }

View File

@@ -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(
} }
} }
} }
}
} }
} }
} }

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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" }