[feat] Image Recents
This commit is contained in:
		| @@ -2,11 +2,10 @@ package com.acitelight.aether.service | ||||
|  | ||||
| import android.content.Context | ||||
| import androidx.compose.runtime.mutableStateListOf | ||||
| import com.acitelight.aether.model.Comic | ||||
| import com.acitelight.aether.model.Video | ||||
| import com.acitelight.aether.model.VideoQueryIndex | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import kotlinx.coroutines.sync.Mutex | ||||
| import kotlinx.coroutines.sync.withLock | ||||
| import kotlinx.coroutines.withContext | ||||
| @@ -24,7 +23,7 @@ class RecentManager @Inject constructor( | ||||
| { | ||||
|     private val mutex = Mutex() | ||||
|  | ||||
|     suspend fun readFile(context: Context, filename: String): String { | ||||
|     private suspend fun readFile(context: Context, filename: String): String { | ||||
|         return withContext(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val file = File(context.filesDir, filename) | ||||
| @@ -38,7 +37,7 @@ class RecentManager @Inject constructor( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun writeFile(context: Context, filename: String, content: String) { | ||||
|     private suspend fun writeFile(context: Context, filename: String, content: String) { | ||||
|         withContext(Dispatchers.IO) { | ||||
|             try { | ||||
|                 val file = File(context.filesDir, filename) | ||||
| @@ -50,13 +49,85 @@ class RecentManager @Inject constructor( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun Query(context: Context): List<VideoQueryIndex> | ||||
|     suspend fun queryComic(context: Context): List<String> { | ||||
|         val content = readFile(context, "recent_comic.json") | ||||
|         try { | ||||
|             val ids = Json.decodeFromString<List<String>>(content) | ||||
|  | ||||
|  | ||||
|             recentComic.clear() | ||||
|  | ||||
|             try { | ||||
|                 val comics = mediaManager.queryComicInfoBulk(ids) | ||||
|                 if (comics != null) { | ||||
|                     for (c in comics) { | ||||
|                         recentComic.add(recentComic.size, c) | ||||
|                     } | ||||
|                 } else { | ||||
|                     for (id in ids) { | ||||
|                         val c = mediaManager.queryComicInfoSingle(id) | ||||
|                         if (c != null) recentComic.add(recentComic.size, c) | ||||
|                     } | ||||
|                 } | ||||
|             } catch (e: NoSuchMethodError) { | ||||
|                 for (id in ids) { | ||||
|                     val c = mediaManager.queryComicInfoSingle(id) | ||||
|                     if (c != null) recentComic.add(recentComic.size, c) | ||||
|                 } | ||||
|             } catch (e: Exception) { | ||||
|                 for (id in ids) { | ||||
|                     val c = mediaManager.queryComicInfoSingle(id) | ||||
|                     if (c != null) recentComic.add(recentComic.size, c) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|  | ||||
|             return ids | ||||
|         } catch (e: Exception) { | ||||
|             print(e.message) | ||||
|         } | ||||
|  | ||||
|  | ||||
|         return listOf() | ||||
|     } | ||||
|  | ||||
|     suspend fun pushComic(context: Context, comicId: String) { | ||||
|         mutex.withLock { | ||||
|             val c = readFile(context, "recent_comic.json") | ||||
|  | ||||
|  | ||||
|             val o = recentComic.map { it.id }.toMutableList() | ||||
|  | ||||
|  | ||||
|             if (o.contains(comicId)) { | ||||
|                 val index = o.indexOf(comicId) | ||||
|                 recentComic.removeAt(index) | ||||
|             } | ||||
|  | ||||
|  | ||||
|             val comic = mediaManager.queryComicInfoSingle(comicId) | ||||
|             if (comic != null) { | ||||
|                 recentComic.add(0, comic) | ||||
|             } else { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             if (recentComic.size > 21) { | ||||
|                 recentComic.removeAt(recentComic.size - 1) | ||||
|             } | ||||
|  | ||||
|  | ||||
|             writeFile(context, "recent_comic.json", Json.encodeToString(recentComic.map { it.id })) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun queryVideo(context: Context): List<VideoQueryIndex> | ||||
|     { | ||||
|         val content = readFile(context, "recent.json") | ||||
|         try{ | ||||
|             val r = Json.decodeFromString<List<VideoQueryIndex>>(content) | ||||
|  | ||||
|             recent.clear() | ||||
|             recentVideo.clear() | ||||
|             val gr = r.groupBy { it.klass } | ||||
|  | ||||
|             for(it in gr) | ||||
| @@ -65,7 +136,7 @@ class RecentManager @Inject constructor( | ||||
|                 if(v != null) | ||||
|                     for(j in v) | ||||
|                     { | ||||
|                         recent.add(recent.size, j) | ||||
|                         recentVideo.add(recentVideo.size, j) | ||||
|                     } | ||||
|             } | ||||
|  | ||||
| @@ -78,28 +149,29 @@ class RecentManager @Inject constructor( | ||||
|         return listOf() | ||||
|     } | ||||
|  | ||||
|     suspend fun Push(context: Context, video: VideoQueryIndex) | ||||
|     suspend fun pushVideo(context: Context, video: VideoQueryIndex) | ||||
|     { | ||||
|         mutex.withLock{ | ||||
|             val content = readFile(context, "recent.json") | ||||
|             val o = recent.map{ VideoQueryIndex(it.klass, it.id) }.toMutableList() | ||||
|             val o = recentVideo.map{ VideoQueryIndex(it.klass, it.id) }.toMutableList() | ||||
|  | ||||
|             if(o.contains(video)) | ||||
|             { | ||||
|                 val index = o.indexOf(video) | ||||
|                 val temp = recent[index] | ||||
|                 val temp = recentVideo[index] | ||||
|  | ||||
|                 recent.removeAt(index) | ||||
|                 recentVideo.removeAt(index) | ||||
|             } | ||||
|             recent.add(0, mediaManager.queryVideo(video.klass, video.id)!!) | ||||
|             recentVideo.add(0, mediaManager.queryVideo(video.klass, video.id)!!) | ||||
|  | ||||
|  | ||||
|             if(recent.size >= 21) | ||||
|                 recent.removeAt(o.size - 1) | ||||
|             if(recentVideo.size >= 21) | ||||
|                 recentVideo.removeAt(o.size - 1) | ||||
|  | ||||
|             writeFile(context, "recent.json", Json.encodeToString(recent.map{ VideoQueryIndex(it.klass, it.id) })) | ||||
|             writeFile(context, "recent.json", Json.encodeToString(recentVideo.map{ VideoQueryIndex(it.klass, it.id) })) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     val recent = mutableStateListOf<Video>() | ||||
|     val recentVideo = mutableStateListOf<Video>() | ||||
|     val recentComic = mutableStateListOf<Comic>() | ||||
| } | ||||
| @@ -10,6 +10,7 @@ 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 | ||||
| @@ -170,7 +171,7 @@ fun ChapterCard(comic: Comic, navController: NavHostController, chapter: BookMar | ||||
|             Row(Modifier.padding(6.dp)) | ||||
|             { | ||||
|                 Box(Modifier | ||||
|                     .height(170.dp) | ||||
|                     .heightIn(max = 170.dp) | ||||
|                     .clip(RoundedCornerShape(8.dp)) | ||||
|                     .background(Color(0x44FFFFFF))) | ||||
|                 { | ||||
| @@ -182,7 +183,7 @@ fun ChapterCard(comic: Comic, navController: NavHostController, chapter: BookMar | ||||
|                             .build(), | ||||
|                         contentDescription = null, | ||||
|                         imageLoader = comicGridViewModel.imageLoader!!, | ||||
|                         modifier = Modifier.padding(8.dp), | ||||
|                         modifier = Modifier.padding(8.dp).widthIn(max = 170.dp), | ||||
|                         contentScale = ContentScale.Fit, | ||||
|                     ) | ||||
|                 } | ||||
|   | ||||
| @@ -1,49 +1,67 @@ | ||||
| package com.acitelight.aether.view | ||||
|  | ||||
| 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.Spacer | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.fillMaxHeight | ||||
| import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.heightIn | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.wrapContentHeight | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.material3.Button | ||||
| 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.pager.HorizontalPager | ||||
| import androidx.compose.foundation.pager.rememberPagerState | ||||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||||
| import androidx.compose.material3.Card | ||||
| 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.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.draw.alpha | ||||
| import androidx.compose.ui.tooling.preview.Preview | ||||
| 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.hilt.navigation.compose.hiltViewModel | ||||
| import androidx.lifecycle.viewmodel.compose.viewModel | ||||
| import androidx.navigation.NavController | ||||
| import com.acitelight.aether.Global | ||||
| import androidx.navigation.NavHostController | ||||
| import coil3.compose.AsyncImage | ||||
| import coil3.request.ImageRequest | ||||
| import com.acitelight.aether.Global.updateRelate | ||||
| import com.acitelight.aether.service.MediaManager | ||||
| import com.acitelight.aether.service.RecentManager | ||||
| import com.acitelight.aether.model.Comic | ||||
| import com.acitelight.aether.viewModel.ComicScreenViewModel | ||||
| import com.acitelight.aether.viewModel.HomeScreenViewModel | ||||
| import kotlinx.coroutines.launch | ||||
|  | ||||
| @Composable | ||||
| fun HomeScreen( | ||||
|     homeScreenViewModel: HomeScreenViewModel = androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel<HomeScreenViewModel>(), | ||||
|     navController: NavController) | ||||
|     navController: NavHostController) | ||||
| { | ||||
|     if(Global.loggedIn) | ||||
|         homeScreenViewModel.Init() | ||||
|     val pagerState = rememberPagerState(initialPage = 0, pageCount = { 2 }) | ||||
|  | ||||
|     LazyColumn(modifier = Modifier.fillMaxWidth()) | ||||
|     { | ||||
|         item() | ||||
|     HorizontalPager( | ||||
|         state = pagerState, | ||||
|         modifier = Modifier.fillMaxSize().background(Color.Black) | ||||
|     ){ | ||||
|         p -> | ||||
|         if(p == 0) | ||||
|         { | ||||
|             Column { | ||||
|             Column(Modifier.fillMaxHeight()) { | ||||
|                 Text( | ||||
|                     text = "Videos", | ||||
|                     style = MaterialTheme.typography.headlineMedium, | ||||
| @@ -52,21 +70,123 @@ fun HomeScreen( | ||||
|  | ||||
|                 HorizontalDivider(Modifier.padding(8.dp), 2.dp, DividerDefaults.color) | ||||
|  | ||||
|                 for(i in homeScreenViewModel.recentManager.recent) | ||||
|                 LazyColumn(modifier = Modifier.fillMaxWidth()) | ||||
|                 { | ||||
|                     MiniVideoCard( | ||||
|                         modifier = Modifier | ||||
|                             .padding(horizontal = 12.dp), | ||||
|                         i, | ||||
|                         { | ||||
|                             updateRelate(homeScreenViewModel.recentManager.recent, i) | ||||
|                             val route = "video_player_route/${ "${i.klass}/${i.id}".toHex() }" | ||||
|                             navController.navigate(route) | ||||
|                         }, homeScreenViewModel.imageLoader!!) | ||||
|                     HorizontalDivider(Modifier.padding(vertical = 8.dp).alpha(0.25f), 1.dp, DividerDefaults.color) | ||||
|                     items(homeScreenViewModel.recentManager.recentVideo) | ||||
|                     { | ||||
|                             i -> | ||||
|                         MiniVideoCard( | ||||
|                             modifier = Modifier | ||||
|                                 .padding(horizontal = 12.dp), | ||||
|                             i, | ||||
|                             { | ||||
|                                 updateRelate(homeScreenViewModel.recentManager.recentVideo, i) | ||||
|                                 val route = "video_player_route/${ "${i.klass}/${i.id}".toHex() }" | ||||
|                                 navController.navigate(route) | ||||
|                             }, homeScreenViewModel.imageLoader!!) | ||||
|                         HorizontalDivider(Modifier.padding(vertical = 8.dp).alpha(0.4f), 1.dp, DividerDefaults.color) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             Column(Modifier.fillMaxHeight()) { | ||||
|                 Text( | ||||
|                     text = "Comics", | ||||
|                     style = MaterialTheme.typography.headlineMedium, | ||||
|                     modifier = Modifier.padding(8.dp).align(Alignment.Start) | ||||
|                 ) | ||||
|  | ||||
|                 HorizontalDivider(Modifier.padding(8.dp), 2.dp, DividerDefaults.color) | ||||
|  | ||||
|                 LazyVerticalGrid( | ||||
|                     columns = GridCells.Adaptive(128.dp), | ||||
|                     contentPadding = PaddingValues(8.dp), | ||||
|                     verticalArrangement = Arrangement.spacedBy(8.dp), | ||||
|                     horizontalArrangement = Arrangement.spacedBy(8.dp), | ||||
|                 ) | ||||
|                 { | ||||
|                     items(homeScreenViewModel.recentManager.recentComic) | ||||
|                     { | ||||
|                             comic -> | ||||
|                         ComicCardRecent(comic, navController, homeScreenViewModel) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @Composable | ||||
| fun ComicCardRecent( | ||||
|     comic: Comic, | ||||
|     navController: NavHostController, | ||||
|     homeScreenViewModel: HomeScreenViewModel | ||||
| ) { | ||||
|     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 = homeScreenViewModel.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) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -25,7 +25,8 @@ import javax.inject.Inject | ||||
| @HiltViewModel | ||||
| class ComicGridViewModel @Inject constructor( | ||||
|     @ApplicationContext val context: Context, | ||||
|     val mediaManager: MediaManager | ||||
|     val mediaManager: MediaManager, | ||||
|     val recentManager: RecentManager | ||||
| )  : ViewModel() | ||||
| { | ||||
|     var imageLoader: ImageLoader? = null | ||||
| @@ -52,6 +53,7 @@ class ComicGridViewModel @Inject constructor( | ||||
|         viewModelScope.launch { | ||||
|             if(comic.value == null) { | ||||
|                 comic.value = mediaManager.queryComicInfoSingle(id) | ||||
|                 recentManager.pushComic(context, id) | ||||
|                 val c = comic.value!! | ||||
|                 for (i in c.comic.bookmarks) { | ||||
|                     chapterList.add(i) | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| package com.acitelight.aether.viewModel | ||||
|  | ||||
| import androidx.compose.runtime.Composable | ||||
| import android.content.Context | ||||
| import androidx.lifecycle.ViewModel | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.lifecycle.viewModelScope | ||||
| import coil3.ImageLoader | ||||
| import coil3.network.okhttp.OkHttpNetworkFetcherFactory | ||||
| @@ -11,33 +9,27 @@ import com.acitelight.aether.service.ApiClient.createOkHttp | ||||
| import com.acitelight.aether.service.RecentManager | ||||
| import kotlinx.coroutines.launch | ||||
| import javax.inject.Inject | ||||
| import com.acitelight.aether.service.* | ||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||||
|  | ||||
|  | ||||
| @HiltViewModel | ||||
| class HomeScreenViewModel @Inject constructor( | ||||
|     val recentManager: RecentManager | ||||
|     val recentManager: RecentManager, | ||||
|     @ApplicationContext val context: Context | ||||
| ) : ViewModel() | ||||
| { | ||||
|     var _init = false | ||||
|     var imageLoader: ImageLoader? = null; | ||||
|     var imageLoader: ImageLoader? = null | ||||
|  | ||||
|     @Composable | ||||
|     fun Init(){ | ||||
|         if(_init) return | ||||
|         _init = true | ||||
|  | ||||
|         val context = LocalContext.current | ||||
|     init{ | ||||
|         imageLoader =  ImageLoader.Builder(context) | ||||
|             .components { | ||||
|                 add(OkHttpNetworkFetcherFactory(createOkHttp())) | ||||
|             } | ||||
|             .build() | ||||
|         remember { | ||||
|             viewModelScope.launch { | ||||
|                 recentManager.Query(context) | ||||
|             } | ||||
|         viewModelScope.launch { | ||||
|             recentManager.queryVideo(context) | ||||
|             recentManager.queryComic(context) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,10 +1,8 @@ | ||||
| package com.acitelight.aether.viewModel | ||||
|  | ||||
| import android.app.Activity | ||||
| import android.net.Uri | ||||
| import androidx.annotation.OptIn | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.State | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableFloatStateOf | ||||
| import androidx.compose.runtime.mutableIntStateOf | ||||
| @@ -25,7 +23,6 @@ import androidx.media3.exoplayer.ExoPlayer | ||||
| import androidx.media3.exoplayer.source.DefaultMediaSourceFactory | ||||
| import coil3.ImageLoader | ||||
| import coil3.network.okhttp.OkHttpNetworkFetcherFactory | ||||
| import com.acitelight.aether.Global | ||||
| import com.acitelight.aether.model.Video | ||||
| import com.acitelight.aether.model.VideoQueryIndex | ||||
| import com.acitelight.aether.service.ApiClient.createOkHttp | ||||
| @@ -86,7 +83,7 @@ class VideoPlayerViewModel @Inject constructor( | ||||
|         remember { | ||||
|             viewModelScope.launch { | ||||
|                 video = mediaManager.queryVideo(v.split("/")[0], v.split("/")[1])!! | ||||
|                 recentManager.Push(context, VideoQueryIndex(v.split("/")[0], v.split("/")[1])) | ||||
|                 recentManager.pushVideo(context, VideoQueryIndex(v.split("/")[0], v.split("/")[1])) | ||||
|                 _player = (if(video!!.isLocal) ExoPlayer.Builder(context) else ExoPlayer.Builder(context).setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))) | ||||
|                     .build().apply { | ||||
|                         val url = video?.getVideo() ?: "" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 acite
					acite