4 Commits

14 changed files with 119 additions and 145 deletions

2
.gitignore vendored
View File

@@ -15,7 +15,7 @@ captures/
*.aab *.aab
*.apk *.apk
output-metadata.json output-metadata.json
release/
# IntelliJ # IntelliJ
*.iml *.iml
.idea/ .idea/

View File

@@ -26,6 +26,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
) )
signingConfig = signingConfigs.getByName("debug")
} }
} }
compileOptions { compileOptions {
@@ -67,6 +68,7 @@ dependencies {
implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.media3.datasource.okhttp)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -1,66 +0,0 @@
import android.content.Context
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.ByteArrayDataSource
import androidx.media3.datasource.DataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.ui.PlayerView
@androidx.annotation.OptIn(UnstableApi::class)
@Composable
private fun InMemoryVideoPlayer(
modifier: Modifier = Modifier,
videoData: ByteArray
) {
val context = LocalContext.current
val exoPlayer = remember(context, videoData) {
createExoPlayer(context, videoData)
}
DisposableEffect(Unit) {
onDispose {
exoPlayer.release()
}
}
AndroidView(
modifier = modifier,
factory = {
PlayerView(it).apply {
player = exoPlayer
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}
)
}
@androidx.annotation.OptIn(UnstableApi::class)
private fun createExoPlayer(context: Context, videoData: ByteArray): ExoPlayer {
val byteArrayDataSource = ByteArrayDataSource(videoData)
val factory = DataSource.Factory {
byteArrayDataSource
}
val mediaSource = ProgressiveMediaSource.Factory(factory)
.createMediaSource(MediaItem.fromUri("data://local/video.mp4"))
return ExoPlayer.Builder(context).build().apply {
setMediaSource(mediaSource)
prepare()
playWhenReady = false
}
}

View File

@@ -69,6 +69,11 @@ object ApiClient {
} }
} }
fun createOkHttp(): OkHttpClient
{
return createOkHttpClientWithDynamicCert(loadCertificateFromString(cert))
}
private fun createRetrofit(): Retrofit { private fun createRetrofit(): Retrofit {
val okHttpClient = createOkHttpClientWithDynamicCert(loadCertificateFromString(cert)) val okHttpClient = createOkHttpClientWithDynamicCert(loadCertificateFromString(cert))

View File

@@ -23,16 +23,17 @@ object MediaManager
} }
} }
suspend fun listVideos(klass: String): List<Video> suspend fun listVideos(klass: String, callback: (Video) -> Unit)
{ {
try { val j = ApiClient.api!!.queryVideoClasses(klass, token)
val j = ApiClient.api!!.queryVideoClasses(klass, token) for(it in j)
return j.map{
queryVideo(klass, it)!!
}.toList()
}catch (e: Exception)
{ {
return listOf() try {
callback(queryVideo(klass, it)!!)
}catch (e: Exception)
{
}
} }
} }

View File

@@ -58,7 +58,7 @@ fun HomeScreen(homeScreenViewModel: HomeScreenViewModel = viewModel(), navContro
Global.sameClassVideos = RecentManager.recent Global.sameClassVideos = RecentManager.recent
val route = "video_player_route/${ "${i.klass}/${i.id}".toHex() }" val route = "video_player_route/${ "${i.klass}/${i.id}".toHex() }"
navController.navigate(route) navController.navigate(route)
}) }, homeScreenViewModel.imageLoader!!)
HorizontalDivider(Modifier.padding(vertical = 8.dp).alpha(0.25f), 1.dp, DividerDefaults.color) HorizontalDivider(Modifier.padding(vertical = 8.dp).alpha(0.25f), 1.dp, DividerDefaults.color)
} }
} }

View File

@@ -106,7 +106,8 @@ fun MeScreen(meScreenViewModel: MeScreenViewModel = viewModel()) {
onClick = { onClick = {
meScreenViewModel.updateAccount(username, privateKey, context) meScreenViewModel.updateAccount(username, privateKey, context)
}, },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
enabled = privateKey != "******"
) { ) {
Text("Save") Text("Save")
} }

View File

@@ -88,6 +88,7 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil3.ImageLoader
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import coil3.request.ImageRequest import coil3.request.ImageRequest
import com.acitelight.aether.Global import com.acitelight.aether.Global
@@ -572,7 +573,7 @@ fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController:
videoPlayerViewModel._player?.pause() videoPlayerViewModel._player?.pause()
val route = "video_player_route/${ "${i.klass}/${i.id}".toHex() }" val route = "video_player_route/${ "${i.klass}/${i.id}".toHex() }"
navController.navigate(route) navController.navigate(route)
}) }, videoPlayerViewModel.imageLoader!!)
HorizontalDivider(Modifier.padding(vertical = 8.dp).alpha(0.25f), 1.dp, DividerDefaults.color) HorizontalDivider(Modifier.padding(vertical = 8.dp).alpha(0.25f), 1.dp, DividerDefaults.color)
} }
} }
@@ -683,13 +684,13 @@ fun HorizontalGallery(videoPlayerViewModel: VideoPlayerViewModel)
contentPadding = PaddingValues(horizontal = 24.dp) contentPadding = PaddingValues(horizontal = 24.dp)
) { ) {
items(videoPlayerViewModel.video?.getGallery() ?: listOf()) { it -> items(videoPlayerViewModel.video?.getGallery() ?: listOf()) { it ->
SingleImageItem(img = it) SingleImageItem(img = it, videoPlayerViewModel.imageLoader!!)
} }
} }
} }
@Composable @Composable
fun SingleImageItem(img: KeyImage) { fun SingleImageItem(img: KeyImage, imageLoader: ImageLoader) {
AsyncImage( AsyncImage(
model = ImageRequest.Builder(LocalContext.current) model = ImageRequest.Builder(LocalContext.current)
.data(img.url) .data(img.url)
@@ -700,7 +701,8 @@ fun SingleImageItem(img: KeyImage) {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(12.dp)), .clip(RoundedCornerShape(12.dp)),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop,
imageLoader = imageLoader
) )
} }
@@ -912,7 +914,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
} }
@Composable @Composable
fun MiniVideoCard(modifier: Modifier, video: Video, onClick: () -> Unit) fun MiniVideoCard(modifier: Modifier, video: Video, onClick: () -> Unit, imageLoader: ImageLoader)
{ {
var isImageLoaded by remember { mutableStateOf(false) } var isImageLoaded by remember { mutableStateOf(false) }
Card( Card(
@@ -943,7 +945,8 @@ fun MiniVideoCard(modifier: Modifier, video: Video, onClick: () -> Unit)
modifier = Modifier modifier = Modifier
.width(128.dp).fillMaxHeight() .width(128.dp).fillMaxHeight()
.clip(RoundedCornerShape(8.dp)), .clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop,
imageLoader = imageLoader
) )
Column ( Column (

View File

@@ -1,6 +1,5 @@
package com.acitelight.aether.view package com.acitelight.aether.view
import android.R.id.tabs
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -20,12 +19,7 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
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.collectAsState
@@ -39,19 +33,16 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel 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.service.MediaManager
import com.acitelight.aether.viewModel.VideoScreenViewModel import com.acitelight.aether.viewModel.VideoScreenViewModel
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.ScrollableTabRow
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
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.text.style.TextOverflow
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil3.request.ImageRequest import coil3.request.ImageRequest
import com.acitelight.aether.Global import com.acitelight.aether.Global
import kotlinx.coroutines.flow.first
import java.nio.charset.Charset import java.nio.charset.Charset
fun String.toHex(): String { fun String.toHex(): String {
@@ -72,7 +63,8 @@ fun String.hexToString(charset: Charset = Charsets.UTF_8): String {
@Composable @Composable
fun VideoScreen(videoScreenViewModel: VideoScreenViewModel = viewModel(), navController: NavHostController) fun VideoScreen(videoScreenViewModel: VideoScreenViewModel = viewModel(), navController: NavHostController)
{ {
val videoList by videoScreenViewModel.videos.collectAsState() val tabIndex by videoScreenViewModel.tabIndex;
videoScreenViewModel.SetupClient()
Column( Column(
modifier = Modifier.fillMaxSize() // 或至少 fillMaxWidth() modifier = Modifier.fillMaxSize() // 或至少 fillMaxWidth()
@@ -86,8 +78,11 @@ fun VideoScreen(videoScreenViewModel: VideoScreenViewModel = viewModel(), navCon
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) )
{ {
items(videoList) { video -> if(videoScreenViewModel.classes.isNotEmpty())
VideoCard(video, navController, videoScreenViewModel) {
items(videoScreenViewModel.classesMap[videoScreenViewModel.classes[tabIndex]] ?: mutableStateListOf()) { video ->
VideoCard(video, navController, videoScreenViewModel)
}
} }
} }
} }
@@ -98,12 +93,11 @@ fun VideoScreen(videoScreenViewModel: VideoScreenViewModel = viewModel(), navCon
fun TopRow(videoScreenViewModel: VideoScreenViewModel) fun TopRow(videoScreenViewModel: VideoScreenViewModel)
{ {
val tabIndex by videoScreenViewModel.tabIndex; val tabIndex by videoScreenViewModel.tabIndex;
val klasses by videoScreenViewModel.klasses.collectAsState();
if(klasses.isEmpty()) return if(videoScreenViewModel.classes.isEmpty()) return
ScrollableTabRow (selectedTabIndex = tabIndex) { ScrollableTabRow (selectedTabIndex = tabIndex) {
klasses.forEachIndexed { index, title -> videoScreenViewModel.classes.forEachIndexed { index, title ->
Tab( Tab(
selected = tabIndex == index, selected = tabIndex == index,
onClick = { videoScreenViewModel.setTabIndex(index) }, onClick = { videoScreenViewModel.setTabIndex(index) },
@@ -115,15 +109,14 @@ fun TopRow(videoScreenViewModel: VideoScreenViewModel)
@Composable @Composable
fun VideoCard(video: Video, navController: NavHostController, videoScreenViewModel: VideoScreenViewModel) { fun VideoCard(video: Video, navController: NavHostController, videoScreenViewModel: VideoScreenViewModel) {
val videoList by videoScreenViewModel.videos.collectAsState() val tabIndex by videoScreenViewModel.tabIndex;
Card( Card(
shape = RoundedCornerShape(6.dp), shape = RoundedCornerShape(6.dp),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight(), .wrapContentHeight(),
onClick = { onClick = {
Global.sameClassVideos = videoList Global.sameClassVideos = videoScreenViewModel.classesMap[videoScreenViewModel.classes[tabIndex]] ?: mutableStateListOf()
val route = "video_player_route/${ "${video.klass}/${video.id}".toHex() }" val route = "video_player_route/${ "${video.klass}/${video.id}".toHex() }"
navController.navigate(route) navController.navigate(route)
} }
@@ -142,7 +135,8 @@ fun VideoCard(video: Video, navController: NavHostController, videoScreenViewMod
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop,
imageLoader = videoScreenViewModel.imageLoader!!
) )
Text( Text(

View File

@@ -11,11 +11,15 @@ import androidx.compose.ui.platform.LocalContext
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import coil3.ImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import okhttp3.OkHttpClient
import com.acitelight.aether.Global import com.acitelight.aether.Global
import com.acitelight.aether.dataStore import com.acitelight.aether.dataStore
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.service.ApiClient import com.acitelight.aether.service.ApiClient
import com.acitelight.aether.service.ApiClient.createOkHttp
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 com.acitelight.aether.service.MediaManager.token import com.acitelight.aether.service.MediaManager.token
@@ -52,6 +56,7 @@ class HomeScreenViewModel(application: Application) : AndroidViewModel(applicati
} }
var _init = false var _init = false
var imageLoader: ImageLoader? = null;
@Composable @Composable
fun Init(){ fun Init(){
@@ -59,6 +64,11 @@ class HomeScreenViewModel(application: Application) : AndroidViewModel(applicati
_init = true _init = true
val context = LocalContext.current val context = LocalContext.current
imageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(createOkHttp()))
}
.build()
remember { remember {
viewModelScope.launch { viewModelScope.launch {
RecentManager.Query(context) RecentManager.Query(context)

View File

@@ -73,7 +73,7 @@ class MeScreenViewModel(application: Application) : AndroidViewModel(application
val c = certFlow.first() val c = certFlow.first()
val p = privateKeyFlow.first() val p = privateKeyFlow.first()
if (u == "" || c == "" || p == "") return@launch if (u == "" || c == "" || p == "" || us == "") return@launch
try { try {
ApiClient.apply(u, c) ApiClient.apply(u, c)
@@ -105,7 +105,10 @@ class MeScreenViewModel(application: Application) : AndroidViewModel(application
val u = userNameFlow.first() val u = userNameFlow.first()
val p = privateKeyFlow.first() val p = privateKeyFlow.first()
if (u == "" || p == "") return@launch val ur = urlFlow.first()
val c = certFlow.first()
if (u == "" || p == "" || ur == "" || c == "") return@launch
try { try {
MediaManager.token = AuthManager.fetchToken( MediaManager.token = AuthManager.fetchToken(

View File

@@ -16,10 +16,15 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Player.STATE_READY import androidx.media3.common.Player.STATE_READY
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.ExoPlayer 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.Global
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.service.ApiClient.createOkHttp
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.hexToString import com.acitelight.aether.view.hexToString
@@ -46,6 +51,10 @@ class VideoPlayerViewModel() : ViewModel()
var renderedFirst = false var renderedFirst = false
var video: Video? = null var video: Video? = null
val dataSourceFactory = OkHttpDataSource.Factory(createOkHttp())
var imageLoader: ImageLoader? = null;
@OptIn(UnstableApi::class)
@Composable @Composable
fun Init(videoId: String) fun Init(videoId: String)
{ {
@@ -53,11 +62,20 @@ class VideoPlayerViewModel() : ViewModel()
val context = LocalContext.current val context = LocalContext.current
val v = videoId.hexToString() val v = videoId.hexToString()
imageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(createOkHttp()))
}
.build()
remember { remember {
viewModelScope.launch { viewModelScope.launch {
video = MediaManager.queryVideo(v.split("/")[0], v.split("/")[1])!! video = MediaManager.queryVideo(v.split("/")[0], v.split("/")[1])!!
RecentManager.Push(context, VideoQueryIndex(v.split("/")[0], v.split("/")[1])) RecentManager.Push(context, VideoQueryIndex(v.split("/")[0], v.split("/")[1]))
_player = ExoPlayer.Builder(context).build().apply { _player = ExoPlayer
.Builder(context)
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
.build().apply {
val url = video?.getVideo() ?: "" val url = video?.getVideo() ?: ""
val mediaItem = MediaItem.fromUri(url) val mediaItem = MediaItem.fromUri(url)
setMediaItem(mediaItem) setMediaItem(mediaItem)

View File

@@ -1,53 +1,60 @@
package com.acitelight.aether.viewModel package com.acitelight.aether.viewModel
import android.app.Application import android.app.Application
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.platform.LocalContext
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.acitelight.aether.Global import coil3.ImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import com.acitelight.aether.dataStore import com.acitelight.aether.dataStore
import com.acitelight.aether.model.Video import com.acitelight.aether.model.Video
import com.acitelight.aether.service.ApiClient import com.acitelight.aether.service.ApiClient.createOkHttp
import com.acitelight.aether.service.AuthManager
import com.acitelight.aether.service.MediaManager import com.acitelight.aether.service.MediaManager
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class VideoScreenViewModel(application: Application) : AndroidViewModel(application) class VideoScreenViewModel(application: Application) : AndroidViewModel(application)
{ {
private val dataStore = application.dataStore
private val USER_NAME_KEY = stringPreferencesKey("user_name")
private val PRIVATE_KEY = stringPreferencesKey("private_key")
val userNameFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[USER_NAME_KEY] ?: ""
}
val privateKeyFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[PRIVATE_KEY] ?: ""
}
private val _tabIndex = mutableIntStateOf(0) private val _tabIndex = mutableIntStateOf(0)
val tabIndex: State<Int> = _tabIndex val tabIndex: State<Int> = _tabIndex
// val videos = mutableStateListOf<Video>()
// private val _klasses = MutableStateFlow<List<String>>(emptyList())
var classes = mutableStateListOf<String>()
val classesMap = mutableStateMapOf<String, SnapshotStateList<Video>>()
private val _videos = MutableStateFlow<List<Video>>(emptyList()) var imageLoader: ImageLoader? = null;
val videos: StateFlow<List<Video>> = _videos
private val _klasses = MutableStateFlow<List<String>>(emptyList()) @Composable
val klasses: StateFlow<List<String>> = _klasses; fun SetupClient()
{
val context = LocalContext.current
imageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(createOkHttp()))
}
.build()
}
suspend fun init() { suspend fun init() {
_klasses.value = MediaManager.listVideoKlasses() classes.addAll(MediaManager.listVideoKlasses())
val p = MediaManager.listVideos(_klasses.value.first()) for(it in classes)
_videos.value = p {
classesMap[it] = mutableStateListOf<Video>()
}
MediaManager.listVideos(classes[0]){
v -> classesMap[classes[0]]?.add(v)
}
} }
fun setTabIndex(index: Int) fun setTabIndex(index: Int)
@@ -55,8 +62,11 @@ class VideoScreenViewModel(application: Application) : AndroidViewModel(applicat
viewModelScope.launch() viewModelScope.launch()
{ {
_tabIndex.intValue = index; _tabIndex.intValue = index;
val p = MediaManager.listVideos(_klasses.value[index])
_videos.value = p MediaManager.listVideos(classes[index])
{
v -> classesMap[classes[index]]?.add(v)
}
} }
} }

View File

@@ -14,7 +14,6 @@ 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"
kotlinxSerializationJsonVersion = "1.9.0"
lifecycleRuntimeKtx = "2.9.2" lifecycleRuntimeKtx = "2.9.2"
activityCompose = "1.10.1" activityCompose = "1.10.1"
composeBom = "2025.08.00" composeBom = "2025.08.00"
@@ -25,9 +24,7 @@ navigationCompose = "2.9.3"
okhttp = "5.1.0" okhttp = "5.1.0"
retrofit = "3.0.0" retrofit = "3.0.0"
retrofit2KotlinxSerializationConverter = "1.0.0" retrofit2KotlinxSerializationConverter = "1.0.0"
retrofitVersion = "3.0.0" media3DatasourceOkhttp = "1.8.0"
tinkAndroid = "1.18.0"
tweetnaclJava = "1.0.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -38,7 +35,6 @@ androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", versi
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Ui" } androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Ui" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "bcprovJdk15on" } bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "bcprovJdk15on" }
bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcprovJdk18on" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilNetworkOkhttp" } coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilNetworkOkhttp" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" } converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
@@ -57,13 +53,10 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
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" }
kotlinx-serialization-json-v163 = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJsonVersion" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-v2110 = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofitVersion" }
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" }
tink-android = { module = "com.google.crypto.tink:tink-android", version.ref = "tinkAndroid" } androidx-media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3DatasourceOkhttp" }
tweetnacl-java = { module = "com.github.InstantWebP2P:tweetnacl-java", version.ref = "tweetnaclJava" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }