[feat] Comic Reader
This commit is contained in:
@@ -48,6 +48,8 @@ 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.ComicPageView
|
||||||
import com.acitelight.aether.view.ComicScreen
|
import com.acitelight.aether.view.ComicScreen
|
||||||
import com.acitelight.aether.view.HomeScreen
|
import com.acitelight.aether.view.HomeScreen
|
||||||
import com.acitelight.aether.view.MeScreen
|
import com.acitelight.aether.view.MeScreen
|
||||||
@@ -98,6 +100,8 @@ fun AppNavigation() {
|
|||||||
|
|
||||||
val hideBottomBarRoutes = listOf(
|
val hideBottomBarRoutes = listOf(
|
||||||
Screen.VideoPlayer.route,
|
Screen.VideoPlayer.route,
|
||||||
|
Screen.ComicGrid.route,
|
||||||
|
Screen.ComicPage.route
|
||||||
)
|
)
|
||||||
val shouldShowBottomBar = currentRoute !in hideBottomBarRoutes
|
val shouldShowBottomBar = currentRoute !in hideBottomBarRoutes
|
||||||
|
|
||||||
@@ -126,7 +130,7 @@ fun AppNavigation() {
|
|||||||
VideoScreen(navController = navController)
|
VideoScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable(Screen.Comic.route) {
|
composable(Screen.Comic.route) {
|
||||||
ComicScreen()
|
ComicScreen(navController = navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable(Screen.Transmission.route) {
|
composable(Screen.Transmission.route) {
|
||||||
@@ -146,6 +150,30 @@ fun AppNavigation() {
|
|||||||
VideoPlayer(videoId = videoId, navController = navController)
|
VideoPlayer(videoId = videoId, navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = Screen.ComicGrid.route,
|
||||||
|
arguments = listOf(navArgument("comicId") { type = NavType.StringType })
|
||||||
|
) {
|
||||||
|
backStackEntry ->
|
||||||
|
val comicId = backStackEntry.arguments?.getString("comicId")
|
||||||
|
if (comicId != null) {
|
||||||
|
ComicGridView(comicId = comicId, navController = navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = Screen.ComicPage.route,
|
||||||
|
arguments = listOf(navArgument("comicId") { type = NavType.StringType }, navArgument("page") { type = NavType.StringType })
|
||||||
|
) {
|
||||||
|
backStackEntry ->
|
||||||
|
val comicId = backStackEntry.arguments?.getString("comicId")
|
||||||
|
val page = backStackEntry.arguments?.getString("page")
|
||||||
|
if (comicId != null && page != null) {
|
||||||
|
ComicPageView(comicId = comicId, page = page, navController = navController)
|
||||||
|
ToggleFullScreen(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,4 +223,6 @@ sealed class Screen(val route: String, val icon: ImageVector, val title: String)
|
|||||||
Icons.AutoMirrored.Filled.CompareArrows, "Transmission")
|
Icons.AutoMirrored.Filled.CompareArrows, "Transmission")
|
||||||
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 ComicPage : Screen("comic_page_route/{comicId}/{page}", Icons.Filled.PlayArrow, "ComicPage")
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,50 @@
|
|||||||
package com.acitelight.aether.model
|
package com.acitelight.aether.model
|
||||||
|
|
||||||
data class Comic(
|
import com.acitelight.aether.service.ApiClient
|
||||||
val comic_name: String,
|
|
||||||
val page_count: Int,
|
class Comic(
|
||||||
val bookmarks: List<BookMark>,
|
val comic: ComicResponse,
|
||||||
val pages: List<String>
|
val id: String,
|
||||||
)
|
val token: String
|
||||||
|
)
|
||||||
|
{
|
||||||
|
fun getPage(pageNumber: Int): String
|
||||||
|
{
|
||||||
|
return "${ApiClient.base}api/image/$id/${comic.list[pageNumber]}?token=$token"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPage(pageName: String): String?
|
||||||
|
{
|
||||||
|
val v = comic.list.indexOf(pageName)
|
||||||
|
if(v >= 0)
|
||||||
|
{
|
||||||
|
return getPage(v)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPageIndex(pageName: String): Int
|
||||||
|
{
|
||||||
|
return comic.list.indexOf(pageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getChapterLength(pageName: String): Int
|
||||||
|
{
|
||||||
|
var v = comic.list.indexOf(pageName)
|
||||||
|
if(v >= 0)
|
||||||
|
{
|
||||||
|
var r: Int = 1
|
||||||
|
v+=1
|
||||||
|
while(v < comic.list.size && !comic.bookmarks.any{
|
||||||
|
x -> x.page == comic.list[v]
|
||||||
|
}){
|
||||||
|
r++
|
||||||
|
v+=1
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.acitelight.aether.model
|
||||||
|
|
||||||
|
data class ComicResponse(
|
||||||
|
val comic_name: String,
|
||||||
|
val page_count: Int,
|
||||||
|
val bookmarks: List<BookMark>,
|
||||||
|
val list: List<String>,
|
||||||
|
val author: String
|
||||||
|
)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.acitelight.aether.service
|
package com.acitelight.aether.service
|
||||||
|
|
||||||
import com.acitelight.aether.model.ChallengeResponse
|
import com.acitelight.aether.model.ChallengeResponse
|
||||||
import com.acitelight.aether.model.Comic
|
import com.acitelight.aether.model.ComicResponse
|
||||||
import com.acitelight.aether.model.VideoResponse
|
import com.acitelight.aether.model.VideoResponse
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
@@ -9,7 +9,6 @@ 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.Query
|
||||||
import retrofit2.http.Streaming
|
|
||||||
|
|
||||||
interface ApiInterface {
|
interface ApiInterface {
|
||||||
@GET("api/video")
|
@GET("api/video")
|
||||||
@@ -28,18 +27,10 @@ interface ApiInterface {
|
|||||||
@Query("token") token: String
|
@Query("token") token: String
|
||||||
): VideoResponse
|
): VideoResponse
|
||||||
|
|
||||||
@GET("api/video/{klass}/{id}/nv")
|
@GET("api/image")
|
||||||
@Streaming
|
suspend fun getComics(@Query("token") token: String): List<String>
|
||||||
suspend fun getNailVideo(
|
@GET("api/image/{id}")
|
||||||
@Path("klass") klass: String,
|
suspend fun queryComicInfo(@Path("id") id: String, @Query("token") token: String): ComicResponse
|
||||||
@Path("id") id: String,
|
|
||||||
@Query("token") token: String
|
|
||||||
): ResponseBody
|
|
||||||
|
|
||||||
@GET("api/image/collections")
|
|
||||||
suspend fun getComicCollections(): List<String>
|
|
||||||
@GET("api/image/meta")
|
|
||||||
suspend fun queryComicInfo(@Query("collection") collection: String): Comic
|
|
||||||
|
|
||||||
|
|
||||||
@GET("api/user/{user}")
|
@GET("api/user/{user}")
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package com.acitelight.aether.service
|
package com.acitelight.aether.service
|
||||||
|
|
||||||
import com.acitelight.aether.model.Comic
|
import com.acitelight.aether.model.Comic
|
||||||
|
import com.acitelight.aether.model.ComicResponse
|
||||||
import com.acitelight.aether.model.Video
|
import com.acitelight.aether.model.Video
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
|
|
||||||
object MediaManager
|
object MediaManager
|
||||||
@@ -52,13 +50,23 @@ object MediaManager
|
|||||||
|
|
||||||
suspend fun listComics() : List<String>
|
suspend fun listComics() : List<String>
|
||||||
{
|
{
|
||||||
// TODO: try
|
try{
|
||||||
return ApiClient.api!!.getComicCollections()
|
val j = ApiClient.api!!.getComics(token)
|
||||||
|
return j
|
||||||
|
}catch (e: Exception)
|
||||||
|
{
|
||||||
|
return listOf()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun queryComicInfo(c: String) : Comic
|
suspend fun queryComicInfo(id: String) : Comic?
|
||||||
{
|
{
|
||||||
// TODO: try
|
try{
|
||||||
return ApiClient.api!!.queryComicInfo(c)
|
val j = ApiClient.api!!.queryComicInfo(id, token)
|
||||||
|
return Comic(id = id, comic = j, token = token)
|
||||||
|
}catch (e: Exception)
|
||||||
|
{
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
154
app/src/main/java/com/acitelight/aether/view/ComicGridView.kt
Normal file
154
app/src/main/java/com/acitelight/aether/view/ComicGridView.kt
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package com.acitelight.aether.view
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.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.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import com.acitelight.aether.Global
|
||||||
|
import com.acitelight.aether.model.BookMark
|
||||||
|
import com.acitelight.aether.model.Comic
|
||||||
|
import com.acitelight.aether.viewModel.ComicGridViewModel
|
||||||
|
import java.util.EnumSet.range
|
||||||
|
import java.util.stream.IntStream.range
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ComicGridView(comicId: String, navController: NavHostController, comicGridViewModel: ComicGridViewModel = viewModel())
|
||||||
|
{
|
||||||
|
comicGridViewModel.SetupClient()
|
||||||
|
comicGridViewModel.Resolve(comicId.hexToString())
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxWidth())
|
||||||
|
{
|
||||||
|
items(comicGridViewModel.chapterList)
|
||||||
|
{
|
||||||
|
c ->
|
||||||
|
ChapterCard(comicGridViewModel.comic!!, navController, c, comicGridViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChapterCard(comic: Comic, navController: NavHostController, chapter: BookMark, comicGridViewModel: ComicGridViewModel = viewModel())
|
||||||
|
{
|
||||||
|
val c = chapter
|
||||||
|
val iv = comic.getPageIndex(c.page)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(16.dp),
|
||||||
|
onClick = {
|
||||||
|
val route = "comic_page_route/${"${comic.id}".toHex()}/${comic.getPageIndex(chapter.page)}"
|
||||||
|
navController.navigate(route)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(Modifier.fillMaxWidth())
|
||||||
|
{
|
||||||
|
Row(Modifier.padding(12.dp))
|
||||||
|
{
|
||||||
|
Box(Modifier
|
||||||
|
.height(260.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),
|
||||||
|
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(8.dp)) {
|
||||||
|
items(r)
|
||||||
|
{
|
||||||
|
r ->
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.height(200.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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
182
app/src/main/java/com/acitelight/aether/view/ComicPageView.kt
Normal file
182
app/src/main/java/com/acitelight/aether/view/ComicPageView.kt
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package com.acitelight.aether.view
|
||||||
|
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
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.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.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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.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.max
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
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.viewModel.ComicPageViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ComicPageView(comicId: String, page: String, navController: NavHostController, comicPageViewModel: ComicPageViewModel = viewModel())
|
||||||
|
{
|
||||||
|
comicPageViewModel.SetupClient()
|
||||||
|
comicPageViewModel.Resolve(comicId.hexToString(), page.toInt())
|
||||||
|
|
||||||
|
val title by comicPageViewModel.title
|
||||||
|
val pagerState = rememberPagerState(initialPage = page.toInt(), pageCount = { comicPageViewModel.pageList.size })
|
||||||
|
var showPlane by comicPageViewModel.showPlane
|
||||||
|
|
||||||
|
Box()
|
||||||
|
{
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.fillMaxSize().align(Alignment.Center).background(Color.Black).clickable(){
|
||||||
|
showPlane = !showPlane
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
page ->
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(comicPageViewModel.comic!!.getPage(page))
|
||||||
|
.memoryCacheKey("${comicPageViewModel.comic!!.id}/${page}")
|
||||||
|
.diskCacheKey("${comicPageViewModel.comic!!.id}/${page}")
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
imageLoader = comicPageViewModel.imageLoader!!,
|
||||||
|
modifier = Modifier.padding(8.dp).fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
androidx.compose.animation.AnimatedVisibility(
|
||||||
|
visible = showPlane,
|
||||||
|
enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }),
|
||||||
|
exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight }),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
){
|
||||||
|
Box()
|
||||||
|
{
|
||||||
|
Box(modifier = Modifier.height(180.dp).align(Alignment.TopCenter).fillMaxWidth().background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Black.copy(alpha = 0.75f),
|
||||||
|
Color.Transparent,
|
||||||
|
))))
|
||||||
|
|
||||||
|
|
||||||
|
Row(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 18.dp).padding(horizontal = 12.dp)
|
||||||
|
.height(60.dp)
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.background(Color(0x90FFFFFF), shape = RoundedCornerShape(12.dp)))
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.Black,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.padding(8.dp).padding(horizontal = 10.dp).weight(1f).align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${pagerState.currentPage + 1}/${pagerState.pageCount}",
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.Black,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.padding(8.dp).widthIn(120.dp).align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
androidx.compose.animation.AnimatedVisibility(
|
||||||
|
visible = showPlane,
|
||||||
|
enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }),
|
||||||
|
exit = slideOutVertically(targetOffsetY = { fullHeight -> fullHeight }),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Box{
|
||||||
|
Box(modifier = Modifier.height(360.dp).align(Alignment.BottomCenter).fillMaxWidth().background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.90f),
|
||||||
|
))))
|
||||||
|
|
||||||
|
LazyRow (state = comicPageViewModel.listState!!, modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 18.dp).padding(horizontal = 12.dp)
|
||||||
|
.height(240.dp)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.background(Color(0x90999999), shape = RoundedCornerShape(12.dp)))
|
||||||
|
{
|
||||||
|
items(comicPageViewModel.pageList.size)
|
||||||
|
{
|
||||||
|
r ->
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(horizontal = 6.dp).padding(vertical = 6.dp),
|
||||||
|
onClick = {
|
||||||
|
pagerState.requestScrollToPage(page = r)
|
||||||
|
}
|
||||||
|
){
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(comicPageViewModel.comic!!.getPage(r))
|
||||||
|
.memoryCacheKey("${comicPageViewModel.comic!!.id}/${r}")
|
||||||
|
.diskCacheKey("${comicPageViewModel.comic!!.id}/${r}")
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
imageLoader = comicPageViewModel.imageLoader!!,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(12.dp)),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,128 @@
|
|||||||
package com.acitelight.aether.view
|
package com.acitelight.aether.view
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.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.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
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.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import 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.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import com.acitelight.aether.Global
|
||||||
|
import com.acitelight.aether.model.Comic
|
||||||
import com.acitelight.aether.viewModel.ComicScreenViewModel
|
import com.acitelight.aether.viewModel.ComicScreenViewModel
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ComicScreen(comicScreenViewModel: ComicScreenViewModel = viewModel())
|
fun ComicScreen(navController: NavHostController, comicScreenViewModel: ComicScreenViewModel = viewModel())
|
||||||
{
|
{
|
||||||
|
comicScreenViewModel.SetupClient()
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Adaptive(128.dp),
|
||||||
|
contentPadding = PaddingValues(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
items(comicScreenViewModel.comics) { comic ->
|
||||||
|
ComicCard(comic, navController, comicScreenViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ComicCard(comic: Comic, navController: NavHostController, comicScreenViewModel: ComicScreenViewModel) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
onClick = {
|
||||||
|
val route = "comic_grid_route/${"${comic.id}".toHex() }"
|
||||||
|
navController.navigate(route)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()){
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(comic.getPage(0))
|
||||||
|
.memoryCacheKey("${comic.id}/${0}")
|
||||||
|
.diskCacheKey("${comic.id}/${0}")
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
imageLoader = comicScreenViewModel.imageLoader!!,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(24.dp)
|
||||||
|
.background( brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.45f)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
.align(Alignment.BottomCenter))
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.align(Alignment.BottomEnd).padding(2.dp),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
text = "${comic.comic.list.size} Pages",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = comic.comic.comic_name,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
maxLines = 2,
|
||||||
|
modifier = Modifier.padding(8.dp).background(Color.Transparent).heightIn(48.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ fun VideoScreen(videoScreenViewModel: VideoScreenViewModel = viewModel(), navCon
|
|||||||
TopRow(videoScreenViewModel);
|
TopRow(videoScreenViewModel);
|
||||||
|
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Fixed(2),
|
columns = GridCells.Adaptive(200.dp),
|
||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.acitelight.aether.viewModel
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
|
import com.acitelight.aether.model.BookMark
|
||||||
|
import com.acitelight.aether.model.Comic
|
||||||
|
import com.acitelight.aether.service.ApiClient.createOkHttp
|
||||||
|
import com.acitelight.aether.service.MediaManager
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class ComicGridViewModel : ViewModel()
|
||||||
|
{
|
||||||
|
var imageLoader: ImageLoader? = null
|
||||||
|
var comic: Comic? = null
|
||||||
|
val chapterList = mutableStateListOf<BookMark>()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SetupClient()
|
||||||
|
{
|
||||||
|
val context = LocalContext.current
|
||||||
|
imageLoader = ImageLoader.Builder(context)
|
||||||
|
.components {
|
||||||
|
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Resolve(id: String)
|
||||||
|
{
|
||||||
|
if(comic != null) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
comic = MediaManager.queryComicInfo(id)
|
||||||
|
val c = comic!!
|
||||||
|
for(i in c.comic.bookmarks)
|
||||||
|
{
|
||||||
|
chapterList.add(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.acitelight.aether.viewModel
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
|
import com.acitelight.aether.model.Comic
|
||||||
|
import com.acitelight.aether.service.ApiClient.createOkHttp
|
||||||
|
import com.acitelight.aether.service.MediaManager
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class ComicPageViewModel : ViewModel()
|
||||||
|
{
|
||||||
|
var imageLoader: ImageLoader? = null
|
||||||
|
var comic: Comic? = null
|
||||||
|
var pageList = mutableStateListOf<String>()
|
||||||
|
var title = mutableStateOf<String>("")
|
||||||
|
var listState: LazyListState? = null
|
||||||
|
var coroutineScope: CoroutineScope? = null
|
||||||
|
var showPlane = mutableStateOf(false)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SetupClient()
|
||||||
|
{
|
||||||
|
val context = LocalContext.current
|
||||||
|
imageLoader = ImageLoader.Builder(context)
|
||||||
|
.components {
|
||||||
|
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
listState = rememberLazyListState()
|
||||||
|
coroutineScope = rememberCoroutineScope()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Resolve(id: String, page: Int)
|
||||||
|
{
|
||||||
|
if(comic != null) return
|
||||||
|
LaunchedEffect(id, page) {
|
||||||
|
coroutineScope?.launch {
|
||||||
|
comic = MediaManager.queryComicInfo(id)
|
||||||
|
comic?.let {
|
||||||
|
pageList.addAll(it.comic.list)
|
||||||
|
title.value = it.comic.comic_name
|
||||||
|
listState?.scrollToItem(index = page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,46 @@
|
|||||||
package com.acitelight.aether.viewModel
|
package com.acitelight.aether.viewModel
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
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.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
import com.acitelight.aether.model.Comic
|
import com.acitelight.aether.model.Comic
|
||||||
import com.acitelight.aether.model.Video
|
import com.acitelight.aether.model.ComicResponse
|
||||||
|
import com.acitelight.aether.service.ApiClient.createOkHttp
|
||||||
import com.acitelight.aether.service.MediaManager
|
import com.acitelight.aether.service.MediaManager
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ComicScreenViewModel : ViewModel()
|
class ComicScreenViewModel : ViewModel() {
|
||||||
{
|
|
||||||
private val _comics = MutableStateFlow<List<Comic>>(emptyList())
|
|
||||||
val comics: StateFlow<List<Comic>> = _comics
|
|
||||||
|
|
||||||
init
|
var imageLoader: ImageLoader? = null;
|
||||||
|
|
||||||
|
val comics = mutableStateListOf<Comic>()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SetupClient()
|
||||||
{
|
{
|
||||||
// viewModelScope.launch {
|
val context = LocalContext.current
|
||||||
// val l = MediaManager.listComics()
|
imageLoader = ImageLoader.Builder(context)
|
||||||
// _comics.value = l.map { MediaManager.queryComicInfo(it) }
|
.components {
|
||||||
// }
|
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val l = MediaManager.listComics()
|
||||||
|
for(i in l)
|
||||||
|
{
|
||||||
|
val m = MediaManager.queryComicInfo(i)
|
||||||
|
if(m != null)
|
||||||
|
comics.add(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user