diff --git a/README.md b/README.md index b9d7146..124e29c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -_
+
# Aether (Client for Abyss) diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..556cfc9 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,76 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.acitelight.aether" + compileSdk = 36 + + defaultConfig { + applicationId = "com.acitelight.aether" + minSdk = 33 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.datastore.preferences) + implementation(libs.bcprov.jdk15on) + implementation(libs.converter.gson) + implementation(libs.gson) + implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.ui) + implementation(libs.androidx.media3.common) + + implementation(libs.coil.compose) + implementation(libs.coil.network.okhttp) + + implementation(libs.retrofit) + implementation(libs.retrofit2.kotlinx.serialization.converter) + implementation(libs.kotlinx.serialization.json) + implementation(libs.okhttp) + + implementation(libs.androidx.material.icons.extended) + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/acitelight/aether/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/acitelight/aether/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..c32d547 --- /dev/null +++ b/app/src/androidTest/java/com/acitelight/aether/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.acitelight.aether + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.acitelight.aether", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6d2ab95 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/AetherApp.kt b/app/src/main/java/com/acitelight/aether/AetherApp.kt new file mode 100644 index 0000000..73ca987 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/AetherApp.kt @@ -0,0 +1,15 @@ +package com.acitelight.aether + +import android.app.Application +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore + +val Context.dataStore: DataStore by preferencesDataStore(name = "configure") + +class AetherApp : Application() { + override fun onCreate() { + super.onCreate() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/Global.kt b/app/src/main/java/com/acitelight/aether/Global.kt new file mode 100644 index 0000000..8ce306d --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/Global.kt @@ -0,0 +1,13 @@ +package com.acitelight.aether + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.acitelight.aether.model.Video + +object Global { + var videoName: String = "" + var videoClass: String = "" + var loggedIn by mutableStateOf(false) + var video: Video? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/MainActivity.kt b/app/src/main/java/com/acitelight/aether/MainActivity.kt new file mode 100644 index 0000000..213371d --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/MainActivity.kt @@ -0,0 +1,196 @@ +package com.acitelight.aether + +import android.app.Activity +import androidx.compose.material.icons.Icons +import android.graphics.drawable.Icon +import android.net.http.SslCertificate.saveState +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.automirrored.filled.CompareArrows +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButtonDefaults.Icon +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.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavType +import com.acitelight.aether.ui.theme.AetherTheme +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.acitelight.aether.view.ComicScreen +import com.acitelight.aether.view.HomeScreen +import com.acitelight.aether.view.MeScreen +import com.acitelight.aether.view.VideoPlayer +import com.acitelight.aether.view.VideoScreen + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + WindowCompat.setDecorFitsSystemWindows(window, false) + setContent { + AetherTheme { + AppNavigation() + } + } + } +} + + +@Composable +fun ToggleFullScreen(isFullScreen: Boolean) +{ + val view = LocalView.current + + LaunchedEffect(isFullScreen) { + val window = (view.context as Activity).window + val insetsController = WindowCompat.getInsetsController(window, view) + + if (isFullScreen) { + insetsController.hide(WindowInsetsCompat.Type.systemBars()) + insetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } else { + insetsController.show(WindowInsetsCompat.Type.systemBars()) + } + } +} + +@Composable +fun AppNavigation() { + val navController = rememberNavController() + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route + + val hideBottomBarRoutes = listOf( + Screen.VideoPlayer.route, + ) + val shouldShowBottomBar = currentRoute !in hideBottomBarRoutes + + Scaffold( + bottomBar = { + AnimatedVisibility( + visible = shouldShowBottomBar, + enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }), + exit = slideOutVertically(targetOffsetY = { fullHeight -> fullHeight }) + ) { + BottomNavigationBar(navController = navController) + } + if(shouldShowBottomBar) + ToggleFullScreen(false) + } + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = Screen.Home.route, + modifier = if(shouldShowBottomBar)Modifier.padding(innerPadding) else Modifier.padding(0.dp) + ) { + composable(Screen.Home.route) { + HomeScreen() + } + composable(Screen.Video.route) { + VideoScreen(navController = navController) + } + composable(Screen.Comic.route) { + ComicScreen() + } + + composable(Screen.Transmission.route) { + // ComicScreen() + } + composable(Screen.Me.route) { + MeScreen(); + } + + composable( + route = Screen.VideoPlayer.route, + arguments = listOf(navArgument("videoId") { type = NavType.StringType }) + ) { + backStackEntry -> + val videoId = backStackEntry.arguments?.getString("videoId") + if (videoId != null) { + VideoPlayer(videoId = videoId, navController = navController) + } + } + } + } +} + +@Composable +fun BottomNavigationBar(navController: NavController) { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route + + val items = if(Global.loggedIn) listOf( + Screen.Home, + Screen.Video, + Screen.Comic, + Screen.Transmission, + Screen.Me + ) else listOf( + Screen.Home, + Screen.Video, + Screen.Comic, + Screen.Transmission, + Screen.Me + ) + + NavigationBar( modifier = Modifier.height(60.dp)) { + items.forEach { screen -> + NavigationBarItem( + icon = { Icon(imageVector = screen.icon, contentDescription = null) }, + selected = currentRoute == screen.route, + onClick = { + navController.navigate(screen.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + modifier = Modifier.padding(vertical = 2.dp).height(25.dp) + ) + } + } +} + +sealed class Screen(val route: String, val icon: ImageVector, val title: String) { + data object Home : Screen("home_route", Icons.Filled.Home, "Home") + data object Video : Screen("video_route", Icons.Filled.VideoLibrary, "Video") + data object Comic : Screen("comic_route", Icons.Filled.Image, "Comic") + data object Transmission : Screen("transmission_route", + Icons.AutoMirrored.Filled.CompareArrows, "Transmission") + data object Me : Screen("me_route", Icons.Filled.AccountCircle, "me") + data object VideoPlayer : Screen("video_player_route/{videoId}", Icons.Filled.PlayArrow, "VideoPlayer") +} \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/animation/scaleY.kt b/app/src/main/java/com/acitelight/aether/animation/scaleY.kt new file mode 100644 index 0000000..3bb4a15 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/animation/scaleY.kt @@ -0,0 +1,16 @@ +package com.acitelight.aether.animation + +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.TransformOrigin + diff --git a/app/src/main/java/com/acitelight/aether/helper/MemoryVideo.kt b/app/src/main/java/com/acitelight/aether/helper/MemoryVideo.kt new file mode 100644 index 0000000..c92f2ef --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/helper/MemoryVideo.kt @@ -0,0 +1,66 @@ +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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/model/BookMark.kt b/app/src/main/java/com/acitelight/aether/model/BookMark.kt new file mode 100644 index 0000000..5564ed6 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/model/BookMark.kt @@ -0,0 +1,6 @@ +package com.acitelight.aether.model + +data class BookMark( + val name: String, + val page: String +) \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/model/ChallengeResponse.kt b/app/src/main/java/com/acitelight/aether/model/ChallengeResponse.kt new file mode 100644 index 0000000..c597986 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/model/ChallengeResponse.kt @@ -0,0 +1,5 @@ +package com.acitelight.aether.model + +data class ChallengeResponse( + val response: String // 签名后的 challenge +) \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/model/Comic.kt b/app/src/main/java/com/acitelight/aether/model/Comic.kt new file mode 100644 index 0000000..930e687 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/model/Comic.kt @@ -0,0 +1,8 @@ +package com.acitelight.aether.model + +data class Comic( + val comic_name: String, + val page_count: Int, + val bookmarks: List, + val pages: List +) \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/model/Comment.kt b/app/src/main/java/com/acitelight/aether/model/Comment.kt new file mode 100644 index 0000000..16f6995 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/model/Comment.kt @@ -0,0 +1,7 @@ +package com.acitelight.aether.model + +data class Comment( + val content: String, + val username: String, + val time: String +) diff --git a/app/src/main/java/com/acitelight/aether/model/KeyImage.kt b/app/src/main/java/com/acitelight/aether/model/KeyImage.kt new file mode 100644 index 0000000..4754211 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/model/KeyImage.kt @@ -0,0 +1,6 @@ +package com.acitelight.aether.model + +data class KeyImage( + val url: String, + val key: String +) diff --git a/app/src/main/java/com/acitelight/aether/model/TokenResponse.kt b/app/src/main/java/com/acitelight/aether/model/TokenResponse.kt new file mode 100644 index 0000000..cb8f8b7 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/model/TokenResponse.kt @@ -0,0 +1,5 @@ +package com.acitelight.aether.model + +data class TokenResponse( + val token: String +) \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/model/Video.kt b/app/src/main/java/com/acitelight/aether/model/Video.kt new file mode 100644 index 0000000..5ab7ce4 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/model/Video.kt @@ -0,0 +1,28 @@ +package com.acitelight.aether.model + +import com.acitelight.aether.service.ApiClient +import java.security.KeyPair + +class Video constructor( + val klass: String, + val id: String, + val token: String, + val video: VideoResponse + ){ + fun getCover(): String + { + return "${ApiClient.base}api/video/$klass/$id/cover?token=$token" + } + + fun getVideo(): String + { + return "${ApiClient.base}api/video/$klass/$id/av?token=$token" + } + + fun getGallery(): List + { + return video.gallery.map{ + KeyImage(url = "${ApiClient.base}api/video/$klass/$id/gallery/$it?token=$token", key = "$klass/$id/gallery/$it") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/model/VideoResponse.kt b/app/src/main/java/com/acitelight/aether/model/VideoResponse.kt new file mode 100644 index 0000000..bc15631 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/model/VideoResponse.kt @@ -0,0 +1,11 @@ +package com.acitelight.aether.model + +data class VideoResponse( + val name: String, + val duration: Long, + val gallery: List, + val comment: List, + val star: Boolean, + val like: Int, + val author: String +) diff --git a/app/src/main/java/com/acitelight/aether/service/ApiClient.kt b/app/src/main/java/com/acitelight/aether/service/ApiClient.kt new file mode 100644 index 0000000..7bc5e02 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/service/ApiClient.kt @@ -0,0 +1,25 @@ + +package com.acitelight.aether.service + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object ApiClient { + const val base: String = "http://192.168.1.213/" + private val json = Json { + ignoreUnknownKeys = true + } + + private val retrofit = Retrofit.Builder() + .baseUrl(base) + .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .build() + + val api: ApiInterface by lazy { + retrofit.create(ApiInterface::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/service/ApiInterface.kt b/app/src/main/java/com/acitelight/aether/service/ApiInterface.kt new file mode 100644 index 0000000..d716807 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/service/ApiInterface.kt @@ -0,0 +1,55 @@ +package com.acitelight.aether.service + +import com.acitelight.aether.model.ChallengeResponse +import com.acitelight.aether.model.Comic +import com.acitelight.aether.model.VideoResponse +import okhttp3.ResponseBody +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query +import retrofit2.http.Streaming + +interface ApiInterface { + @GET("api/video") + suspend fun getVideoClasses( + @Query("token") token: String + ): List + @GET("api/video/{klass}") + suspend fun queryVideoClasses( + @Path("klass") klass: String, + @Query("token") token: String + ): List + @GET("api/video/{klass}/{id}") + suspend fun queryVideo( + @Path("klass") klass: String, + @Path("id") id: String, + @Query("token") token: String + ): VideoResponse + + @GET("api/video/{klass}/{id}/nv") + @Streaming + suspend fun getNailVideo( + @Path("klass") klass: String, + @Path("id") id: String, + @Query("token") token: String + ): ResponseBody + + @GET("api/image/collections") + suspend fun getComicCollections(): List + @GET("api/image/meta") + suspend fun queryComicInfo(@Query("collection") collection: String): Comic + + + @GET("api/user/{user}") + suspend fun getChallenge( + @Path("user") user: String + ): ResponseBody + + @POST("api/user/{user}") + suspend fun verifyChallenge( + @Path("user") user: String, + @Body challengeResponse: ChallengeResponse + ): ResponseBody +} diff --git a/app/src/main/java/com/acitelight/aether/service/AuthManager.kt b/app/src/main/java/com/acitelight/aether/service/AuthManager.kt new file mode 100644 index 0000000..1723f12 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/service/AuthManager.kt @@ -0,0 +1,46 @@ +package com.acitelight.aether.service + +import android.util.Base64 +import com.acitelight.aether.model.ChallengeResponse +import kotlinx.coroutines.runBlocking +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters +import org.bouncycastle.crypto.signers.Ed25519Signer +import java.security.PrivateKey +import java.security.Signature + +object AuthManager { + suspend fun fetchToken(baseUrl: String, username: String, privateKey: String): String? = runBlocking { + val api = ApiClient.api + var challengeBase64 = "" + try{ + challengeBase64 = api.getChallenge(username).string() + }catch (e: Exception) + { + print(e.message) + } + + val signedBase64 = signChallenge(db64(privateKey), db64(challengeBase64)) + + return@runBlocking try { + api.verifyChallenge(username, ChallengeResponse(response = signedBase64)).string() + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + fun db64(b64: String): ByteArray { + return Base64.decode(b64, Base64.DEFAULT) // 32 bytes + } + + fun signChallenge(privateKey: ByteArray, data: ByteArray): String + { + val privateKeyParams = Ed25519PrivateKeyParameters(privateKey, 0) + val signer = Ed25519Signer() + signer.init(true, privateKeyParams) + + signer.update(data, 0, data.size) + val signature = signer.generateSignature() + return Base64.encodeToString(signature, Base64.NO_WRAP) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acitelight/aether/service/MediaManager.kt b/app/src/main/java/com/acitelight/aether/service/MediaManager.kt new file mode 100644 index 0000000..e73a426 --- /dev/null +++ b/app/src/main/java/com/acitelight/aether/service/MediaManager.kt @@ -0,0 +1,43 @@ +package com.acitelight.aether.service + +import com.acitelight.aether.model.Comic +import com.acitelight.aether.model.Video +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.IOException + + +object MediaManager +{ + var token: String = "null" + + suspend fun listVideoKlasses(): List + { + val j = ApiClient.api.getVideoClasses(token) + return j.toList() + } + + suspend fun listVideos(klass: String): List