8 Commits

17 changed files with 707 additions and 231 deletions

2
.gitignore vendored
View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import androidx.compose.material.icons.Icons
import android.graphics.drawable.Icon
import android.net.http.SslCertificate.saveState
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
@@ -56,6 +57,9 @@ import com.acitelight.aether.view.VideoScreen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.attributes = window.attributes.apply {
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
}
enableEdgeToEdge()
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {

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

@@ -1,25 +1,106 @@
package com.acitelight.aether.service
import android.content.Context
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.CertificatePinner
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.ByteArrayInputStream
import java.security.KeyStore
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
object ApiClient {
const val base: String = "http://192.168.1.213/"
var base: String = ""
var domain: String = ""
var cert: String = ""
private val json = Json {
ignoreUnknownKeys = true
}
private val retrofit = Retrofit.Builder()
.baseUrl(base)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
fun loadCertificateFromString(pemString: String): X509Certificate {
val certificateFactory = CertificateFactory.getInstance("X.509")
val decodedPem = pemString
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replace("\\s+".toRegex(), "")
val api: ApiInterface by lazy {
retrofit.create(ApiInterface::class.java)
val decodedBytes = android.util.Base64.decode(decodedPem, android.util.Base64.DEFAULT)
ByteArrayInputStream(decodedBytes).use { inputStream ->
return certificateFactory.generateCertificate(inputStream) as X509Certificate
}
}
fun createOkHttpClientWithDynamicCert(trustedCert: X509Certificate): OkHttpClient {
try {
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
load(null, null)
setCertificateEntry("ca", trustedCert)
}
val tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
val tmf = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
init(keyStore)
}
val trustManager = tmf.trustManagers.first { it is X509TrustManager } as X509TrustManager
val sslContext = SSLContext.getInstance("TLS").apply {
init(null, arrayOf(trustManager), null)
}
return OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustManager)
.build()
} catch (e: Exception) {
throw RuntimeException("Failed to create OkHttpClient with dynamic certificate", e)
}
}
fun createOkHttp(): OkHttpClient
{
return createOkHttpClientWithDynamicCert(loadCertificateFromString(cert))
}
private fun createRetrofit(): Retrofit {
val okHttpClient = createOkHttpClientWithDynamicCert(loadCertificateFromString(cert))
return Retrofit.Builder()
.baseUrl(base)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
}
var api: ApiInterface? = null
fun apply(url: String, crt: String)
{
try{
domain = url.toHttpUrlOrNull()?.host !!
cert = crt
base = url
api = createRetrofit().create(ApiInterface::class.java)
}catch (e: Exception)
{
api = null
base = ""
domain = ""
cert = ""
}
}
}

View File

@@ -9,12 +9,12 @@ import java.security.PrivateKey
import java.security.Signature
object AuthManager {
suspend fun fetchToken(baseUrl: String, username: String, privateKey: String): String? {
suspend fun fetchToken(username: String, privateKey: String): String? {
val api = ApiClient.api
var challengeBase64 = ""
try{
challengeBase64 = api.getChallenge(username).string()
challengeBase64 = api!!.getChallenge(username).string()
}catch (e: Exception)
{
print(e.message)
@@ -23,7 +23,7 @@ object AuthManager {
val signedBase64 = signChallenge(db64(privateKey), db64(challengeBase64))
return try {
api.verifyChallenge(username, ChallengeResponse(response = signedBase64)).string()
api!!.verifyChallenge(username, ChallengeResponse(response = signedBase64)).string()
} catch (e: Exception) {
e.printStackTrace()
null

View File

@@ -15,7 +15,7 @@ object MediaManager
{
try
{
val j = ApiClient.api.getVideoClasses(token)
val j = ApiClient.api!!.getVideoClasses(token)
return j.toList()
}catch(e: Exception)
{
@@ -23,23 +23,24 @@ 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)
return j.map{
queryVideo(klass, it)!!
}.toList()
}catch (e: Exception)
val j = ApiClient.api!!.queryVideoClasses(klass, token)
for(it in j)
{
return listOf()
try {
callback(queryVideo(klass, it)!!)
}catch (e: Exception)
{
}
}
}
suspend fun queryVideo(klass: String, id: String): Video?
{
try {
val j = ApiClient.api.queryVideo(klass, id, token)
val j = ApiClient.api!!.queryVideo(klass, id, token)
return Video(klass = klass, id = id, token=token, j)
}catch (e: Exception)
{
@@ -49,11 +50,13 @@ object MediaManager
suspend fun listComics() : List<String>
{
return ApiClient.api.getComicCollections()
// TODO: try
return ApiClient.api!!.getComicCollections()
}
suspend fun queryComicInfo(c: String) : Comic
{
return ApiClient.api.queryComicInfo(c)
// TODO: try
return ApiClient.api!!.queryComicInfo(c)
}
}

View File

@@ -1,6 +1,7 @@
package com.acitelight.aether.service
import android.content.Context
import androidx.compose.runtime.mutableStateListOf
import com.acitelight.aether.model.Video
import com.acitelight.aether.model.VideoQueryIndex
import kotlinx.coroutines.Dispatchers
@@ -50,11 +51,15 @@ object RecentManager
try{
val r = Json.decodeFromString<List<VideoQueryIndex>>(content)
val vn = r.map{
MediaManager.queryVideo(it.klass, it.id)
}.filter { it != null }
recent.clear()
for(it in r)
{
val v = MediaManager.queryVideo(it.klass, it.id)
if(v != null)
recent.add(recent.size, v)
}
_recent.value = vn.map { it!! }
return r
}catch (e: Exception)
{
@@ -74,6 +79,7 @@ object RecentManager
{
val temp = o[0]
val index = o.indexOf(video)
recent.removeAt(index)
o[0] = o[index]
o[index] = temp
}
@@ -85,15 +91,10 @@ object RecentManager
if(o.size >= 21)
o.removeAt(o.size - 1)
val vn = o.map{
MediaManager.queryVideo(it.klass, it.id)
}.filter { it != null }
_recent.value = vn.map { it!! }
recent.add(0, MediaManager.queryVideo(video.klass, video.id)!!)
writeFile(context, "recent.json", Json.encodeToString(o))
}
}
private val _recent = MutableStateFlow<List<Video>>(emptyList())
val recent: StateFlow<List<Video>> = _recent
val recent = mutableStateListOf<Video>()
}

View File

@@ -34,7 +34,6 @@ fun HomeScreen(homeScreenViewModel: HomeScreenViewModel = viewModel(), navContro
{
if(Global.loggedIn)
homeScreenViewModel.Init()
val recent by RecentManager.recent.collectAsState()
LazyColumn(modifier = Modifier.fillMaxWidth())
{
@@ -49,17 +48,17 @@ fun HomeScreen(homeScreenViewModel: HomeScreenViewModel = viewModel(), navContro
HorizontalDivider(Modifier.padding(8.dp), 2.dp, DividerDefaults.color)
for(i in recent)
for(i in RecentManager.recent)
{
MiniVideoCard(
modifier = Modifier
.padding(horizontal = 12.dp),
i,
{
Global.sameClassVideos = recent
Global.sameClassVideos = RecentManager.recent
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)
}
}

View File

@@ -8,10 +8,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
@@ -25,19 +28,22 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.acitelight.aether.viewModel.MeScreenViewModel
@Composable
fun MeScreen(meScreenViewModel: MeScreenViewModel = viewModel())
{
fun MeScreen(meScreenViewModel: MeScreenViewModel = viewModel()) {
val context = LocalContext.current
var username by meScreenViewModel.username;
var privateKey by meScreenViewModel.privateKey;
var url by meScreenViewModel.url
var cert by meScreenViewModel.cert
Column(
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
@@ -45,62 +51,131 @@ fun MeScreen(meScreenViewModel: MeScreenViewModel = viewModel())
verticalArrangement = Arrangement.Top
) {
// Card component for a clean, contained UI block
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Column(
item{
Card(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Account Setting",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 16.dp).align(Alignment.Start)
)
// Username input field
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("Username") },
leadingIcon = {
Icon(Icons.Default.Person, contentDescription = "Username")
},
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Private key input field
OutlinedTextField(
value = privateKey,
onValueChange = { privateKey = it },
label = { Text("Key") },
leadingIcon = {
Icon(Icons.Default.Key, contentDescription = "Key")
},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
// Save Button
Button(
onClick = {
meScreenViewModel.updateAccount(username, privateKey, context)
},
modifier = Modifier.fillMaxWidth()
.fillMaxWidth()
.padding(8.dp)
)
{
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Save")
Text(
text = "Account Setting",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.padding(bottom = 16.dp)
.align(Alignment.Start)
)
// Username input field
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("Username") },
leadingIcon = {
Icon(Icons.Default.Person, contentDescription = "Username")
},
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Private key input field
OutlinedTextField(
value = privateKey,
onValueChange = { privateKey = it },
label = { Text("Key") },
leadingIcon = {
Icon(Icons.Default.Key, contentDescription = "Key")
},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
// Save Button
Button(
onClick = {
meScreenViewModel.updateAccount(username, privateKey, context)
},
modifier = Modifier.fillMaxWidth(),
enabled = privateKey != "******"
) {
Text("Save")
}
}
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
{
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Server Setting",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.padding(bottom = 16.dp)
.align(Alignment.Start)
)
// Username input field
OutlinedTextField(
value = url,
onValueChange = { url = it },
label = { Text("Url") },
leadingIcon = {
Icon(Icons.Default.Link, contentDescription = "Url")
},
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Private key input field
OutlinedTextField(
value = cert,
onValueChange = { cert = it },
label = { Text("Cert") },
singleLine = false,
maxLines = 40,
minLines = 20,
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(
fontSize = 8.sp
)
)
Spacer(modifier = Modifier.height(24.dp))
// Save Button
Button(
onClick = {
meScreenViewModel.updateServer(url, cert, context)
},
modifier = Modifier.fillMaxWidth()
) {
Text("Save")
}
}
}
}
}
}

View File

@@ -1,8 +1,10 @@
package com.acitelight.aether.view
import android.app.Activity
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.media.AudioManager
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
@@ -12,7 +14,6 @@ import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
@@ -50,7 +51,10 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.Brightness4
import androidx.compose.material.icons.filled.FastForward
import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material.icons.filled.Info
@@ -58,6 +62,7 @@ import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.ThumbDown
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material.icons.filled.VolumeUp
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.material3.DividerDefaults
@@ -68,6 +73,8 @@ import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
@@ -88,12 +95,14 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import coil3.ImageLoader
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import com.acitelight.aether.Global
import com.acitelight.aether.ToggleFullScreen
import com.acitelight.aether.model.KeyImage
import com.acitelight.aether.model.Video
import kotlin.math.abs
fun formatTime(ms: Long): String {
if (ms <= 0) return "00:00:00"
@@ -222,6 +231,18 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
val context = LocalContext.current
val activity = context as? Activity
val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager }
val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) }
var volFactor by remember { mutableFloatStateOf(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() / maxVolume.toFloat()) }
fun setVolume(value: Int) {
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,
value.coerceIn(0, maxVolume),
AudioManager.FLAG_PLAY_SOUND
)
}
Box(modifier)
{
AndroidView(
@@ -238,19 +259,53 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
.fillMaxWidth()
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
videoPlayerViewModel.dragging = true
videoPlayerViewModel.planeVisibility = true
exoPlayer.pause()
onDragStart = { offset ->
if(offset.x < size.width / 2)
{
videoPlayerViewModel.draggingPurpose = -1;
}else{
videoPlayerViewModel.draggingPurpose = -2;
}
},
onDragEnd = {
videoPlayerViewModel.dragging = false
if (videoPlayerViewModel.isPlaying)
if (videoPlayerViewModel.isPlaying && videoPlayerViewModel.draggingPurpose == 0)
exoPlayer.play()
videoPlayerViewModel.draggingPurpose = -1;
},
onDrag = { change, dragAmount ->
exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong())
videoPlayerViewModel.playProcess = exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat()
if(abs(dragAmount.x) > abs(dragAmount.y) &&
(videoPlayerViewModel.draggingPurpose == -1 || videoPlayerViewModel.draggingPurpose == -2))
{
videoPlayerViewModel.draggingPurpose = 0
videoPlayerViewModel.planeVisibility = true
exoPlayer.pause()
}
else if(videoPlayerViewModel.draggingPurpose == -1) videoPlayerViewModel.draggingPurpose = 1
else if(videoPlayerViewModel.draggingPurpose == -2) videoPlayerViewModel.draggingPurpose = 2
if(videoPlayerViewModel.draggingPurpose == 0)
{
exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong())
videoPlayerViewModel.playProcess = exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat()
}else if(videoPlayerViewModel.draggingPurpose == 2)
{
val cu = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
volFactor = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() / maxVolume.toFloat()
if(dragAmount.y < 0)
setVolume( cu + 1);
else if(dragAmount.y > 0)
setVolume( cu - 1);
}else if(videoPlayerViewModel.draggingPurpose == 1)
{
videoPlayerViewModel.brit = (videoPlayerViewModel.brit - dragAmount.y * 0.002f).coerceIn(0f, 1f);
activity?.window?.attributes = activity.window.attributes.apply {
screenBrightness = videoPlayerViewModel.brit.coerceIn(0f, 1f)
}
activity?.window?.setAttributes(activity.window.attributes)
}
}
)
}
@@ -311,7 +366,7 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
}
androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.dragging,
visible = videoPlayerViewModel.draggingPurpose == 0,
enter = fadeIn(
initialAlpha = 0f,
),
@@ -319,7 +374,8 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.Center)
) {
)
{
Text(
text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${
formatTime(
@@ -327,12 +383,74 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
)
}",
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(bottom = 12.dp),
modifier = Modifier.padding(bottom = 12.dp),
fontSize = 18.sp
)
}
androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.draggingPurpose == 2,
enter = fadeIn(
initialAlpha = 0f,
),
exit = fadeOut(
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.Center)
)
{
Row(Modifier.background(Color(0x88000000), RoundedCornerShape(18)).width(200.dp))
{
Icon(
imageVector = Icons.AutoMirrored.Filled.VolumeUp,
contentDescription = "Vol",
tint = Color.White,
modifier = Modifier.size(48.dp).padding(8.dp)
.align(Alignment.CenterVertically)
)
BiliMiniSlider(
value = volFactor,
onValueChange = {},
modifier = Modifier
.height(4.dp)
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.draggingPurpose == 1,
enter = fadeIn(
initialAlpha = 0f,
),
exit = fadeOut(
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.Center)
)
{
Row(Modifier.background(Color(0x88000000), RoundedCornerShape(18)).width(200.dp))
{
Icon(
imageVector = Icons.Default.Brightness4,
contentDescription = "Brightness",
tint = Color.White,
modifier = Modifier.size(48.dp).padding(8.dp)
.align(Alignment.CenterVertically)
)
BiliMiniSlider(
value = videoPlayerViewModel.brit,
onValueChange = {},
modifier = Modifier
.height(4.dp)
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
)
}
}
if(cover > 0.0f)
Spacer(Modifier.background(Color(0x00FF6699 - 0x00222222 + ((0x000000FF * cover).toLong() shl 24) )).fillMaxSize())
@@ -370,7 +488,13 @@ fun PortalCorePlayer(modifier: Modifier, videoPlayerViewModel: VideoPlayerViewMo
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 2.dp)
.align(Alignment.BottomCenter),
.align(Alignment.BottomCenter).background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.4f),
)
)),
horizontalArrangement = Arrangement.SpaceBetween,
) {
IconButton(
@@ -572,7 +696,7 @@ fun VideoPlayerPortal(videoPlayerViewModel: VideoPlayerViewModel, navController:
videoPlayerViewModel._player?.pause()
val route = "video_player_route/${ "${i.klass}/${i.id}".toHex() }"
navController.navigate(route)
})
}, videoPlayerViewModel.imageLoader!!)
HorizontalDivider(Modifier.padding(vertical = 8.dp).alpha(0.25f), 1.dp, DividerDefaults.color)
}
}
@@ -683,13 +807,13 @@ fun HorizontalGallery(videoPlayerViewModel: VideoPlayerViewModel)
contentPadding = PaddingValues(horizontal = 24.dp)
) {
items(videoPlayerViewModel.video?.getGallery() ?: listOf()) { it ->
SingleImageItem(img = it)
SingleImageItem(img = it, videoPlayerViewModel.imageLoader!!)
}
}
}
@Composable
fun SingleImageItem(img: KeyImage) {
fun SingleImageItem(img: KeyImage, imageLoader: ImageLoader) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(img.url)
@@ -700,7 +824,8 @@ fun SingleImageItem(img: KeyImage) {
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp)),
contentScale = ContentScale.Crop
contentScale = ContentScale.Crop,
imageLoader = imageLoader
)
}
@@ -711,6 +836,18 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
val activity = context as? Activity
val exoPlayer: ExoPlayer = videoPlayerViewModel._player!!;
val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager }
val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) }
var volFactor by remember { mutableFloatStateOf(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() / maxVolume.toFloat()) }
fun setVolume(value: Int) {
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,
value.coerceIn(0, maxVolume),
AudioManager.FLAG_PLAY_SOUND
)
}
BackHandler {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
@@ -736,19 +873,53 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
.fillMaxWidth()
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
videoPlayerViewModel.planeVisibility = true
videoPlayerViewModel.dragging = true;
exoPlayer.pause()
onDragStart = { offset ->
if(offset.x < size.width / 2)
{
videoPlayerViewModel.draggingPurpose = -1;
}else{
videoPlayerViewModel.draggingPurpose = -2;
}
},
onDragEnd = {
videoPlayerViewModel.dragging = false;
if (videoPlayerViewModel.isPlaying)
if (videoPlayerViewModel.isPlaying && videoPlayerViewModel.draggingPurpose == 0)
exoPlayer.play()
videoPlayerViewModel.draggingPurpose = -1;
},
onDrag = { change, dragAmount ->
exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong())
videoPlayerViewModel.playProcess = exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat()
if(abs(dragAmount.x) > abs(dragAmount.y) &&
(videoPlayerViewModel.draggingPurpose == -1 || videoPlayerViewModel.draggingPurpose == -2))
{
videoPlayerViewModel.draggingPurpose = 0
videoPlayerViewModel.planeVisibility = true
exoPlayer.pause()
}
else if(videoPlayerViewModel.draggingPurpose == -1) videoPlayerViewModel.draggingPurpose = 1
else if(videoPlayerViewModel.draggingPurpose == -2) videoPlayerViewModel.draggingPurpose = 2
if(videoPlayerViewModel.draggingPurpose == 0)
{
exoPlayer.seekTo((exoPlayer.currentPosition + dragAmount.x * 200.0f).toLong())
videoPlayerViewModel.playProcess = exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat()
}else if(videoPlayerViewModel.draggingPurpose == 2)
{
val cu = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
volFactor = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat() / maxVolume.toFloat()
if(dragAmount.y < 0)
setVolume( cu + 1);
else if(dragAmount.y > 0)
setVolume( cu - 1);
}else if(videoPlayerViewModel.draggingPurpose == 1)
{
videoPlayerViewModel.brit = (videoPlayerViewModel.brit - dragAmount.y * 0.002f).coerceIn(0f, 1f);
activity?.window?.attributes = activity.window.attributes.apply {
screenBrightness = videoPlayerViewModel.brit.coerceIn(0f, 1f)
}
activity?.window?.setAttributes(activity.window.attributes)
}
}
)
}
@@ -780,7 +951,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
)
androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.dragging,
visible = videoPlayerViewModel.draggingPurpose == 0,
enter = fadeIn(
initialAlpha = 0f,
),
@@ -788,7 +959,8 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.Center)
) {
)
{
Text(
text = "${formatTime((exoPlayer.duration * videoPlayerViewModel.playProcess).toLong())}/${
formatTime(
@@ -801,6 +973,68 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
)
}
androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.draggingPurpose == 2,
enter = fadeIn(
initialAlpha = 0f,
),
exit = fadeOut(
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.Center)
)
{
Row(Modifier.background(Color(0x88000000), RoundedCornerShape(18)).width(200.dp))
{
Icon(
imageVector = Icons.AutoMirrored.Filled.VolumeUp,
contentDescription = "Vol",
tint = Color.White,
modifier = Modifier.size(48.dp).padding(8.dp)
.align(Alignment.CenterVertically)
)
BiliMiniSlider(
value = volFactor,
onValueChange = {},
modifier = Modifier
.height(4.dp)
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = videoPlayerViewModel.draggingPurpose == 1,
enter = fadeIn(
initialAlpha = 0f,
),
exit = fadeOut(
targetAlpha = 0f
),
modifier = Modifier.align(Alignment.Center)
)
{
Row(Modifier.background(Color(0x88000000), RoundedCornerShape(18)).width(200.dp))
{
Icon(
imageVector = Icons.Default.Brightness4,
contentDescription = "Brightness",
tint = Color.White,
modifier = Modifier.size(48.dp).padding(8.dp)
.align(Alignment.CenterVertically)
)
BiliMiniSlider(
value = videoPlayerViewModel.brit,
onValueChange = {},
modifier = Modifier
.height(4.dp)
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
)
}
}
AnimatedVisibility(
visible = videoPlayerViewModel.isLongPressing,
enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }),
@@ -830,21 +1064,50 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
}
}
IconButton(
onClick = {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
},
AnimatedVisibility(
visible = videoPlayerViewModel.planeVisibility,
enter = slideInVertically(initialOffsetY = { fullHeight -> -fullHeight }),
exit = slideOutVertically(targetOffsetY = { fullHeight -> -fullHeight }),
modifier = Modifier
.align(Alignment.TopCenter)
.fillMaxWidth()
)
{
Row(Modifier
.align(Alignment.TopStart)
.padding(8.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = Color.White
)
.padding(horizontal = 32.dp).background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Black.copy(alpha = 0.4f),
Color.Transparent,
)
)))
{
IconButton(
onClick = {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
},
modifier = Modifier.size(36.dp).align(Alignment.CenterVertically)
) {
Icon(
modifier = Modifier.size(36.dp),
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = Color.White
)
}
Text(
text = "${videoPlayerViewModel.video?.video?.name}",
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 12.dp).align(Alignment.CenterVertically),
fontSize = 18.sp
)
}
}
AnimatedVisibility(
visible = videoPlayerViewModel.planeVisibility,
enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }),
@@ -912,7 +1175,7 @@ fun VideoPlayerLandscape(videoPlayerViewModel: VideoPlayerViewModel)
}
@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) }
Card(
@@ -943,7 +1206,8 @@ fun MiniVideoCard(modifier: Modifier, video: Video, onClick: () -> Unit)
modifier = Modifier
.width(128.dp).fillMaxHeight()
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
contentScale = ContentScale.Crop,
imageLoader = imageLoader
)
Column (

View File

@@ -72,7 +72,7 @@ fun String.hexToString(charset: Charset = Charsets.UTF_8): String {
@Composable
fun VideoScreen(videoScreenViewModel: VideoScreenViewModel = viewModel(), navController: NavHostController)
{
val videoList by videoScreenViewModel.videos.collectAsState()
videoScreenViewModel.SetupClient()
Column(
modifier = Modifier.fillMaxSize() // 或至少 fillMaxWidth()
@@ -86,7 +86,7 @@ fun VideoScreen(videoScreenViewModel: VideoScreenViewModel = viewModel(), navCon
horizontalArrangement = Arrangement.spacedBy(8.dp)
)
{
items(videoList) { video ->
items(videoScreenViewModel.videos) { video ->
VideoCard(video, navController, videoScreenViewModel)
}
}
@@ -115,15 +115,13 @@ fun TopRow(videoScreenViewModel: VideoScreenViewModel)
@Composable
fun VideoCard(video: Video, navController: NavHostController, videoScreenViewModel: VideoScreenViewModel) {
val videoList by videoScreenViewModel.videos.collectAsState()
Card(
shape = RoundedCornerShape(6.dp),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
onClick = {
Global.sameClassVideos = videoList
Global.sameClassVideos = videoScreenViewModel.videos
val route = "video_player_route/${ "${video.klass}/${video.id}".toHex() }"
navController.navigate(route)
}
@@ -142,7 +140,8 @@ fun VideoCard(video: Video, navController: NavHostController, videoScreenViewMod
contentDescription = null,
modifier = Modifier
.fillMaxSize(),
contentScale = ContentScale.Crop
contentScale = ContentScale.Crop,
imageLoader = videoScreenViewModel.imageLoader!!
)
Text(

View File

@@ -11,11 +11,15 @@ import androidx.compose.ui.platform.LocalContext
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import coil3.ImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import okhttp3.OkHttpClient
import com.acitelight.aether.Global
import com.acitelight.aether.dataStore
import com.acitelight.aether.model.Video
import com.acitelight.aether.model.VideoQueryIndex
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.token
@@ -32,6 +36,8 @@ class HomeScreenViewModel(application: Application) : AndroidViewModel(applicati
private val dataStore = application.dataStore
private val USER_NAME_KEY = stringPreferencesKey("user_name")
private val PRIVATE_KEY = stringPreferencesKey("private_key")
private val URL_KEY = stringPreferencesKey("url")
private val CERT_KEY = stringPreferencesKey("cert")
val userNameFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[USER_NAME_KEY] ?: ""
@@ -41,7 +47,16 @@ class HomeScreenViewModel(application: Application) : AndroidViewModel(applicati
preferences[PRIVATE_KEY] ?: ""
}
val urlFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[URL_KEY] ?: ""
}
val certFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[CERT_KEY] ?: ""
}
var _init = false
var imageLoader: ImageLoader? = null;
@Composable
fun Init(){
@@ -49,6 +64,11 @@ class HomeScreenViewModel(application: Application) : AndroidViewModel(applicati
_init = true
val context = LocalContext.current
imageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(createOkHttp()))
}
.build()
remember {
viewModelScope.launch {
RecentManager.Query(context)
@@ -60,13 +80,16 @@ class HomeScreenViewModel(application: Application) : AndroidViewModel(applicati
viewModelScope.launch {
val u = userNameFlow.first()
val p = privateKeyFlow.first()
val ur = urlFlow.first()
val c = certFlow.first()
if(u=="" || p=="") return@launch
if(u=="" || p=="" || ur=="" || c=="") return@launch
try{
ApiClient.apply(ur, c)
if (MediaManager.token == "null")
MediaManager.token = AuthManager.fetchToken(
ApiClient.base,
u,
p
)!!
@@ -74,6 +97,7 @@ class HomeScreenViewModel(application: Application) : AndroidViewModel(applicati
Global.loggedIn = true
}catch(e: Exception)
{
Global.loggedIn = false
print(e.message)
}
}

View File

@@ -24,23 +24,70 @@ import kotlinx.coroutines.launch
class MeScreenViewModel(application: Application) : AndroidViewModel(application) {
private val dataStore = application.dataStore
private val USER_NAME_KEY = stringPreferencesKey("user_name")
private val PRIVATE_KEY = stringPreferencesKey("private_key")
private val PRIVATE_KEY = stringPreferencesKey("private_key")
private val URL_KEY = stringPreferencesKey("url")
private val CERT_KEY = stringPreferencesKey("cert")
val userNameFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[USER_NAME_KEY] ?: ""
}
val privateKeyFlow: Flow<String> = dataStore.data.map { preferences ->
val privateKeyFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[PRIVATE_KEY] ?: ""
}
val urlFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[URL_KEY] ?: ""
}
val certFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[CERT_KEY] ?: ""
}
val username = mutableStateOf("");
val privateKey = mutableStateOf("")
val url = mutableStateOf("");
val cert = mutableStateOf("")
init {
viewModelScope.launch {
username.value = userNameFlow.first()
privateKey.value = if (privateKeyFlow.first() == "") "" else "******"
url.value = urlFlow.first()
cert.value = certFlow.first()
}
}
fun updateServer(u: String, c: String, context: Context)
{
viewModelScope.launch {
dataStore.edit { preferences ->
preferences[URL_KEY] = u
preferences[CERT_KEY] = c
}
Global.loggedIn = false
val us = userNameFlow.first()
val u = urlFlow.first()
val c = certFlow.first()
val p = privateKeyFlow.first()
if (u == "" || c == "" || p == "" || us == "") return@launch
try {
ApiClient.apply(u, c)
MediaManager.token = AuthManager.fetchToken(
us,
p
)!!
Global.loggedIn = true
Toast.makeText(context, "Server Updated", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
print(e.message)
Toast.makeText(context, "Invalid Account or Server Information", Toast.LENGTH_SHORT).show()
}
}
}
@@ -58,11 +105,13 @@ class MeScreenViewModel(application: Application) : AndroidViewModel(application
val u = userNameFlow.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 {
MediaManager.token = AuthManager.fetchToken(
ApiClient.base,
u,
p
)!!

View File

@@ -1,5 +1,6 @@
package com.acitelight.aether.viewModel
import android.app.Activity
import androidx.annotation.OptIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@@ -16,10 +17,15 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Player.STATE_READY
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.okhttp.OkHttpDataSource
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
import com.acitelight.aether.service.MediaManager
import com.acitelight.aether.service.RecentManager
import com.acitelight.aether.view.hexToString
@@ -35,7 +41,12 @@ class VideoPlayerViewModel() : ViewModel()
var playProcess by mutableFloatStateOf(0.0f)
var planeVisibility by mutableStateOf(true)
var isLongPressing by mutableStateOf(false)
var dragging by mutableStateOf(false)
// -1 : Not dragging
// 0 : Seek
// 1 : Volume
// 2 : Brightness
var draggingPurpose by mutableIntStateOf(-1)
var thumbUp by mutableIntStateOf(0)
var thumbDown by mutableIntStateOf(0)
@@ -46,6 +57,11 @@ class VideoPlayerViewModel() : ViewModel()
var renderedFirst = false
var video: Video? = null
val dataSourceFactory = OkHttpDataSource.Factory(createOkHttp())
var imageLoader: ImageLoader? = null;
var brit by mutableFloatStateOf(0.5f)
@OptIn(UnstableApi::class)
@Composable
fun Init(videoId: String)
{
@@ -53,11 +69,20 @@ class VideoPlayerViewModel() : ViewModel()
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.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 mediaItem = MediaItem.fromUri(url)
setMediaItem(mediaItem)

View File

@@ -1,18 +1,24 @@
package com.acitelight.aether.viewModel
import android.app.Application
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.ui.platform.LocalContext
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import coil3.ImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import com.acitelight.aether.Global
import com.acitelight.aether.dataStore
import com.acitelight.aether.model.Video
import com.acitelight.aether.service.ApiClient
import com.acitelight.aether.service.ApiClient.createOkHttp
import com.acitelight.aether.service.AuthManager
import com.acitelight.aether.service.MediaManager
import kotlinx.coroutines.flow.Flow
@@ -39,15 +45,28 @@ class VideoScreenViewModel(application: Application) : AndroidViewModel(applicat
private val _tabIndex = mutableIntStateOf(0)
val tabIndex: State<Int> = _tabIndex
private val _videos = MutableStateFlow<List<Video>>(emptyList())
val videos: StateFlow<List<Video>> = _videos
val videos = mutableStateListOf<Video>()
private val _klasses = MutableStateFlow<List<String>>(emptyList())
val klasses: StateFlow<List<String>> = _klasses;
var imageLoader: ImageLoader? = null;
@Composable
fun SetupClient()
{
val context = LocalContext.current
imageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(createOkHttp()))
}
.build()
}
suspend fun init() {
_klasses.value = MediaManager.listVideoKlasses()
val p = MediaManager.listVideos(_klasses.value.first())
_videos.value = p
MediaManager.listVideos(_klasses.value.first()){
v -> if(0 == tabIndex.value && !videos.contains(v)) videos.add(videos.size, v)
}
}
fun setTabIndex(index: Int)
@@ -55,8 +74,12 @@ class VideoScreenViewModel(application: Application) : AndroidViewModel(applicat
viewModelScope.launch()
{
_tabIndex.intValue = index;
val p = MediaManager.listVideos(_klasses.value[index])
_videos.value = p
videos.clear()
MediaManager.listVideos(_klasses.value[index])
{
v -> if(index == tabIndex.value) videos.add(videos.size, v)
}
}
}

View File

@@ -14,7 +14,6 @@ junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
kotlinxSerializationJson = "1.9.0"
kotlinxSerializationJsonVersion = "1.9.0"
lifecycleRuntimeKtx = "2.9.2"
activityCompose = "1.10.1"
composeBom = "2025.08.00"
@@ -25,9 +24,7 @@ navigationCompose = "2.9.3"
okhttp = "5.1.0"
retrofit = "3.0.0"
retrofit2KotlinxSerializationConverter = "1.0.0"
retrofitVersion = "3.0.0"
tinkAndroid = "1.18.0"
tweetnaclJava = "1.0.0"
media3DatasourceOkhttp = "1.8.0"
[libraries]
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-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
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-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilNetworkOkhttp" }
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-material3 = { group = "androidx.compose.material3", name = "material3" }
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" }
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" }
tink-android = { module = "com.google.crypto.tink:tink-android", version.ref = "tinkAndroid" }
tweetnacl-java = { module = "com.github.InstantWebP2P:tweetnacl-java", version.ref = "tweetnaclJava" }
androidx-media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3DatasourceOkhttp" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }