[feat] Video position remember& New Icon and Theme
BIN
aether.png
Normal file → Executable file
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 616 KiB |
BIN
aether_clip.png
|
Before Width: | Height: | Size: 508 KiB After Width: | Height: | Size: 348 KiB |
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 163 KiB |
12
app/src/main/java/com/acitelight/aether/model/VideoRecord.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.acitelight.aether.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class VideoRecord (
|
||||
@PrimaryKey(autoGenerate = false) val id: String = "",
|
||||
@ColumnInfo(name = "name") val klass: String = "",
|
||||
@ColumnInfo(name = "position") val position: Long
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.acitelight.aether.model
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface VideoRecordDao {
|
||||
@Query("SELECT * FROM videorecord")
|
||||
fun getAll(): Flow<List<VideoRecord>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(rec: VideoRecord)
|
||||
|
||||
@Update
|
||||
suspend fun update(rec: VideoRecord)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(rec: VideoRecord)
|
||||
|
||||
@Query("SELECT * FROM videorecord WHERE id = :id")
|
||||
suspend fun getById(id: String): VideoRecord?
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.acitelight.aether.model
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(entities = [VideoRecord::class], version = 1)
|
||||
abstract class VideoRecordDatabase : RoomDatabase() {
|
||||
abstract fun userDao(): VideoRecordDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: VideoRecordDatabase? = null
|
||||
|
||||
fun getDatabase(context: Context): VideoRecordDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
VideoRecordDatabase::class.java,
|
||||
"videorecord_database"
|
||||
).build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,8 +130,8 @@ fun generateColorScheme(primaryColor: Color, isDarkMode: Boolean): ColorScheme {
|
||||
}
|
||||
}
|
||||
|
||||
private val DarkColorScheme = generateColorScheme(Color(0xFF2F4F8F), isDarkMode = true)
|
||||
private val LightColorScheme = generateColorScheme(Color(0xFF2F4F8F), isDarkMode = false)
|
||||
private val DarkColorScheme = generateColorScheme(Color(0xFF4A6F9F), isDarkMode = true)
|
||||
private val LightColorScheme = generateColorScheme(Color(0xFF4A6F9F), isDarkMode = false)
|
||||
|
||||
@Composable
|
||||
fun AetherTheme(
|
||||
|
||||
@@ -8,58 +8,44 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
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.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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bookmarks
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.ComicRecord
|
||||
import com.acitelight.aether.service.MediaManager
|
||||
import com.acitelight.aether.viewModel.ComicPageViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -71,7 +57,6 @@ fun ComicPageView(
|
||||
comicPageViewModel: ComicPageViewModel = hiltViewModel<ComicPageViewModel>()
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
comicPageViewModel.SetupClient()
|
||||
comicPageViewModel.Resolve(comicId.hexToString(), page.toInt())
|
||||
|
||||
val title by comicPageViewModel.title
|
||||
@@ -81,7 +66,7 @@ fun ComicPageView(
|
||||
var showPlane by comicPageViewModel.showPlane
|
||||
var showBookMarkPop by remember { mutableStateOf(false) }
|
||||
|
||||
comicPageViewModel.UpdateProcess(pagerState.currentPage)
|
||||
comicPageViewModel.updateProcess(pagerState.currentPage)
|
||||
|
||||
val comic by comicPageViewModel.comic
|
||||
comic?.let {
|
||||
@@ -96,7 +81,7 @@ fun ComicPageView(
|
||||
.clickable {
|
||||
showPlane = !showPlane
|
||||
if (showPlane) {
|
||||
comicPageViewModel.coroutineScope?.launch {
|
||||
comicPageViewModel.viewModelScope.launch {
|
||||
comicPageViewModel.listState?.scrollToItem(index = pagerState.currentPage)
|
||||
}
|
||||
}
|
||||
@@ -329,7 +314,7 @@ fun ComicPageView(
|
||||
showBookMarkPop = false
|
||||
}, { s ->
|
||||
showBookMarkPop = false
|
||||
comicPageViewModel.coroutineScope?.launch {
|
||||
comicPageViewModel.viewModelScope.launch {
|
||||
comicPageViewModel.mediaManager.postBookmark(
|
||||
comicId.hexToString(),
|
||||
BookMark(name = s, page = comicPageViewModel.pageList[pagerState.currentPage])
|
||||
|
||||
@@ -213,7 +213,7 @@ fun VideoPlayer(
|
||||
videoId: String,
|
||||
navController: NavHostController
|
||||
) {
|
||||
videoPlayerViewModel.Init(videoId)
|
||||
videoPlayerViewModel.init(videoId)
|
||||
|
||||
if(videoPlayerViewModel.startPlaying)
|
||||
{
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package com.acitelight.aether.viewModel
|
||||
|
||||
import android.content.Context
|
||||
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
|
||||
@@ -22,8 +19,6 @@ import com.acitelight.aether.model.ComicRecord
|
||||
import com.acitelight.aether.model.ComicRecordDatabase
|
||||
import com.acitelight.aether.service.ApiClient.createOkHttp
|
||||
import com.acitelight.aether.service.MediaManager
|
||||
import com.acitelight.aether.service.SettingsDataStoreManager
|
||||
import com.acitelight.aether.view.hexToString
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -32,7 +27,8 @@ import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ComicPageViewModel @Inject constructor(
|
||||
val mediaManager: MediaManager
|
||||
val mediaManager: MediaManager,
|
||||
@ApplicationContext private val context: Context
|
||||
) : ViewModel()
|
||||
{
|
||||
var imageLoader: ImageLoader? = null
|
||||
@@ -40,29 +36,18 @@ class ComicPageViewModel @Inject constructor(
|
||||
var pageList = mutableStateListOf<String>()
|
||||
var title = mutableStateOf<String>("")
|
||||
var listState: LazyListState? = null
|
||||
var coroutineScope: CoroutineScope? = null
|
||||
var showPlane = mutableStateOf(true)
|
||||
var db: ComicRecordDatabase? = null
|
||||
var db: ComicRecordDatabase
|
||||
|
||||
@Composable
|
||||
fun SetupClient()
|
||||
{
|
||||
val context = LocalContext.current
|
||||
|
||||
init{
|
||||
imageLoader = ImageLoader.Builder(context)
|
||||
.components {
|
||||
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
||||
}
|
||||
.build()
|
||||
listState = rememberLazyListState()
|
||||
coroutineScope = rememberCoroutineScope()
|
||||
|
||||
db = remember {
|
||||
try{
|
||||
ComicRecordDatabase.getDatabase(context)
|
||||
}catch (e: Exception) {
|
||||
print(e.message)
|
||||
} as ComicRecordDatabase?
|
||||
}
|
||||
listState = LazyListState(0, 0)
|
||||
db = ComicRecordDatabase.getDatabase(context)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -70,23 +55,23 @@ class ComicPageViewModel @Inject constructor(
|
||||
{
|
||||
if(comic.value != null) return
|
||||
LaunchedEffect(id, page) {
|
||||
coroutineScope?.launch {
|
||||
viewModelScope.launch {
|
||||
comic.value = mediaManager.queryComicInfoSingle(id)
|
||||
comic.value?.let {
|
||||
pageList.addAll(it.comic.list)
|
||||
title.value = it.comic.comic_name
|
||||
listState?.scrollToItem(index = page)
|
||||
UpdateProcess(page)
|
||||
updateProcess(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun UpdateProcess(page: Int)
|
||||
fun updateProcess(page: Int)
|
||||
{
|
||||
if(comic.value == null) return
|
||||
coroutineScope?.launch {
|
||||
db?.userDao()?.insert(ComicRecord(id = comic.value!!.id.toInt(), name = comic.value!!.comic.comic_name, position = page))
|
||||
viewModelScope.launch {
|
||||
db.userDao().insert(ComicRecord(id = comic.value!!.id.toInt(), name = comic.value!!.comic.comic_name, position = page))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.acitelight.aether.viewModel
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -25,11 +27,15 @@ import coil3.ImageLoader
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import com.acitelight.aether.model.Video
|
||||
import com.acitelight.aether.model.VideoQueryIndex
|
||||
import com.acitelight.aether.model.VideoRecord
|
||||
import com.acitelight.aether.model.VideoRecordDatabase
|
||||
import com.acitelight.aether.service.ApiClient.createOkHttp
|
||||
import com.acitelight.aether.service.MediaManager
|
||||
import com.acitelight.aether.service.RecentManager
|
||||
import com.acitelight.aether.view.formatTime
|
||||
import com.acitelight.aether.view.hexToString
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -39,6 +45,7 @@ import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class VideoPlayerViewModel @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
val mediaManager: MediaManager,
|
||||
val recentManager: RecentManager
|
||||
) : ViewModel() {
|
||||
@@ -66,25 +73,24 @@ class VideoPlayerViewModel @Inject constructor(
|
||||
val dataSourceFactory = OkHttpDataSource.Factory(createOkHttp())
|
||||
var imageLoader: ImageLoader? = null;
|
||||
var brit by mutableFloatStateOf(0.5f)
|
||||
val database: VideoRecordDatabase = VideoRecordDatabase.getDatabase(context)
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
fun Init(videoId: String) {
|
||||
fun init(videoId: String) {
|
||||
if (_init) return;
|
||||
val context = LocalContext.current
|
||||
val v = videoId.hexToString()
|
||||
|
||||
imageLoader = ImageLoader.Builder(context)
|
||||
.components {
|
||||
add(OkHttpNetworkFetcherFactory(createOkHttp()))
|
||||
}
|
||||
.build()
|
||||
|
||||
remember {
|
||||
viewModelScope.launch {
|
||||
video = mediaManager.queryVideo(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)))
|
||||
viewModelScope.launch {
|
||||
video = mediaManager.queryVideo(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() ?: ""
|
||||
val mediaItem = if (video!!.isLocal)
|
||||
@@ -105,18 +111,27 @@ class VideoPlayerViewModel @Inject constructor(
|
||||
|
||||
override fun onRenderedFirstFrame() {
|
||||
super.onRenderedFirstFrame()
|
||||
if(!renderedFirst)
|
||||
{
|
||||
viewModelScope.launch {
|
||||
val ii = database.userDao().getById(video!!.id)
|
||||
if(ii != null)
|
||||
{
|
||||
_player!!.seekTo(ii.position)
|
||||
Toast.makeText(context, "Recover from ${formatTime(ii.position)} ", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
renderedFirst = true
|
||||
}
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
Log.e("ExoPlayer", "Playback error: ", error)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
startListen()
|
||||
}
|
||||
startListen()
|
||||
}
|
||||
|
||||
_init = true;
|
||||
}
|
||||
|
||||
@@ -135,6 +150,10 @@ class VideoPlayerViewModel @Inject constructor(
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
val p = _player!!.currentPosition
|
||||
_player?.release()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
database.userDao().insert(VideoRecord(video!!.id, video!!.klass, p))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 17 KiB |