# 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