Implement source update
Refactor codes for testability
This commit is contained in:
@@ -86,8 +86,8 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1")
|
implementation(Kotlin.SERIALIZATION)
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
|
implementation(Kotlin.COROUTINE)
|
||||||
|
|
||||||
implementation("androidx.activity:activity-compose:1.4.0")
|
implementation("androidx.activity:activity-compose:1.4.0")
|
||||||
implementation("androidx.navigation:navigation-compose:2.4.2")
|
implementation("androidx.navigation:navigation-compose:2.4.2")
|
||||||
@@ -110,10 +110,10 @@ dependencies {
|
|||||||
|
|
||||||
implementation("io.coil-kt:coil-compose:1.4.0")
|
implementation("io.coil-kt:coil-compose:1.4.0")
|
||||||
|
|
||||||
implementation("io.ktor:ktor-client-core:2.0.0")
|
implementation(KtorClient.CORE)
|
||||||
implementation("io.ktor:ktor-client-okhttp:2.0.0")
|
implementation(KtorClient.OKHTTP)
|
||||||
implementation("io.ktor:ktor-client-content-negotiation:2.0.0")
|
implementation(KtorClient.CONTENT_NEGOTIATION)
|
||||||
implementation("io.ktor:ktor-serialization-kotlinx-json:2.0.0")
|
implementation(KtorClient.SERIALIZATION)
|
||||||
|
|
||||||
implementation("androidx.room:room-runtime:2.4.2")
|
implementation("androidx.room:room-runtime:2.4.2")
|
||||||
annotationProcessor("androidx.room:room-compiler:2.4.2")
|
annotationProcessor("androidx.room:room-compiler:2.4.2")
|
||||||
@@ -144,15 +144,18 @@ dependencies {
|
|||||||
implementation("xyz.quaver:subsampledimage:0.0.1-alpha19-SNAPSHOT")
|
implementation("xyz.quaver:subsampledimage:0.0.1-alpha19-SNAPSHOT")
|
||||||
|
|
||||||
implementation("org.kodein.log:kodein-log:0.12.0")
|
implementation("org.kodein.log:kodein-log:0.12.0")
|
||||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.8.1")
|
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.8.1")
|
||||||
|
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
testImplementation("org.mockito:mockito-inline:4.4.0")
|
testImplementation("org.mockito:mockito-inline:4.4.0")
|
||||||
|
testImplementation(KtorClient.TEST)
|
||||||
|
testImplementation(Kotlin.COROUTINE_TEST)
|
||||||
|
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||||
androidTestImplementation("androidx.test:rules:1.4.0")
|
androidTestImplementation("androidx.test:rules:1.4.0")
|
||||||
androidTestImplementation("androidx.test:runner:1.4.0")
|
androidTestImplementation("androidx.test:runner:1.4.0")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||||
|
androidTestImplementation(KtorClient.TEST)
|
||||||
|
|
||||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.1.1")
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.1.1")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,11 +30,12 @@ import com.google.android.gms.security.ProviderInstaller
|
|||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.engine.okhttp.*
|
import io.ktor.client.engine.okhttp.*
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import org.kodein.di.*
|
import org.kodein.di.*
|
||||||
import org.kodein.di.android.x.androidXModule
|
import org.kodein.di.android.x.androidXModule
|
||||||
import xyz.quaver.pupil.sources.core.NetworkCache
|
import xyz.quaver.pupil.sources.core.NetworkCache
|
||||||
import xyz.quaver.pupil.sources.core.settingsDataStore
|
import xyz.quaver.pupil.sources.core.settingsDataStore
|
||||||
import xyz.quaver.pupil.util.ApkDownloadManager
|
import xyz.quaver.pupil.util.PupilHttpClient
|
||||||
|
|
||||||
class Pupil : Application(), DIAware {
|
class Pupil : Application(), DIAware {
|
||||||
|
|
||||||
@@ -42,15 +43,10 @@ class Pupil : Application(), DIAware {
|
|||||||
import(androidXModule(this@Pupil))
|
import(androidXModule(this@Pupil))
|
||||||
|
|
||||||
bind { singleton { NetworkCache(this@Pupil) } }
|
bind { singleton { NetworkCache(this@Pupil) } }
|
||||||
bindSingleton { ApkDownloadManager(this@Pupil, instance()) }
|
|
||||||
|
|
||||||
bindSingleton { settingsDataStore }
|
bindSingleton { settingsDataStore }
|
||||||
|
|
||||||
bind { singleton {
|
bind { singleton { PupilHttpClient(OkHttp.create()) } }
|
||||||
HttpClient(OkHttp) {
|
|
||||||
install(ContentNegotiation)
|
|
||||||
}
|
|
||||||
} }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import android.app.Application
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import dalvik.system.PathClassLoader
|
import dalvik.system.PathClassLoader
|
||||||
@@ -31,26 +30,35 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import xyz.quaver.pupil.sources.core.Source
|
import xyz.quaver.pupil.sources.core.Source
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
@Composable
|
||||||
|
fun rememberLocalSourceList(context: Context = LocalContext.current): State<List<SourceEntry>> = produceState(emptyList()) {
|
||||||
|
while (true) {
|
||||||
|
value = loadSourceList(context)
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadSource(context: Context, sourceEntry: SourceEntry): Source = coroutineScope {
|
||||||
|
sourceCacheMutex.withLock {
|
||||||
|
sourceCache[sourceEntry.packageName] ?: run {
|
||||||
|
val classLoader = PathClassLoader(sourceEntry.sourceDir, null, context.classLoader)
|
||||||
|
|
||||||
|
Class.forName("${sourceEntry.packagePath}${sourceEntry.sourcePath}", false, classLoader)
|
||||||
|
.getConstructor(Application::class.java)
|
||||||
|
.newInstance(context.applicationContext) as Source
|
||||||
|
}.also { sourceCache[sourceEntry.packageName] = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private const val SOURCES_FEATURE = "pupil.sources"
|
private const val SOURCES_FEATURE = "pupil.sources"
|
||||||
private const val SOURCES_PACKAGE_PREFIX = "xyz.quaver.pupil.sources"
|
private const val SOURCES_PACKAGE_PREFIX = "xyz.quaver.pupil.sources"
|
||||||
private const val SOURCES_PATH = "pupil.sources.path"
|
private const val SOURCES_PATH = "pupil.sources.path"
|
||||||
|
|
||||||
data class SourceEntry(
|
private val PackageInfo.isSourceFeatureEnabled
|
||||||
val packageName: String,
|
|
||||||
val packagePath: String,
|
|
||||||
val sourceName: String,
|
|
||||||
val sourcePath: String,
|
|
||||||
val sourceDir: String,
|
|
||||||
val icon: Drawable,
|
|
||||||
val version: String
|
|
||||||
)
|
|
||||||
|
|
||||||
val PackageInfo.isSourceFeatureEnabled
|
|
||||||
get() = this.reqFeatures.orEmpty().any { it.name == SOURCES_FEATURE }
|
get() = this.reqFeatures.orEmpty().any { it.name == SOURCES_FEATURE }
|
||||||
|
|
||||||
fun loadSource(context: Context, packageInfo: PackageInfo): List<SourceEntry> {
|
private fun loadSource(context: Context, packageInfo: PackageInfo): List<SourceEntry> {
|
||||||
val packageManager = context.packageManager
|
val packageManager = context.packageManager
|
||||||
|
|
||||||
val applicationInfo = packageInfo.applicationInfo
|
val applicationInfo = packageInfo.applicationInfo
|
||||||
@@ -84,19 +92,7 @@ fun loadSource(context: Context, packageInfo: PackageInfo): List<SourceEntry> {
|
|||||||
private val sourceCacheMutex = Mutex()
|
private val sourceCacheMutex = Mutex()
|
||||||
private val sourceCache = mutableMapOf<String, Source>()
|
private val sourceCache = mutableMapOf<String, Source>()
|
||||||
|
|
||||||
suspend fun loadSource(context: Context, sourceEntry: SourceEntry): Source = coroutineScope {
|
private fun loadSourceList(context: Context): List<SourceEntry> {
|
||||||
sourceCacheMutex.withLock {
|
|
||||||
sourceCache[sourceEntry.packageName] ?: run {
|
|
||||||
val classLoader = PathClassLoader(sourceEntry.sourceDir, null, context.classLoader)
|
|
||||||
|
|
||||||
Class.forName("${sourceEntry.packagePath}${sourceEntry.sourcePath}", false, classLoader)
|
|
||||||
.getConstructor(Application::class.java)
|
|
||||||
.newInstance(context.applicationContext) as Source
|
|
||||||
}.also { sourceCache[sourceEntry.packageName] = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateSources(context: Context): List<SourceEntry> {
|
|
||||||
val packageManager = context.packageManager
|
val packageManager = context.packageManager
|
||||||
|
|
||||||
val packages = packageManager.getInstalledPackages(
|
val packages = packageManager.getInstalledPackages(
|
||||||
@@ -109,19 +105,4 @@ fun updateSources(context: Context): List<SourceEntry> {
|
|||||||
else
|
else
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun rememberSources(): State<List<SourceEntry>> {
|
|
||||||
val sources = remember { mutableStateOf<List<SourceEntry>>(emptyList()) }
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
while (true) {
|
|
||||||
sources.value = updateSources(context)
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sources
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Pupil, Hitomi.la viewer for Android
|
* Pupil, Hitomi.la viewer for Android
|
||||||
* Copyright (C) 2019 tom5079
|
* Copyright (C) 2022 tom5079
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -16,26 +16,21 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:Suppress("UNUSED_VARIABLE", "IncorrectScope")
|
package xyz.quaver.pupil.sources
|
||||||
|
|
||||||
package xyz.quaver.pupil
|
import androidx.compose.runtime.*
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import org.kodein.di.compose.localDI
|
||||||
|
import org.kodein.di.compose.rememberInstance
|
||||||
|
import org.kodein.di.direct
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import xyz.quaver.pupil.util.PupilHttpClient
|
||||||
|
import xyz.quaver.pupil.util.RemoteSourceInfo
|
||||||
|
|
||||||
import io.ktor.client.*
|
@Composable
|
||||||
import kotlinx.coroutines.runBlocking
|
fun rememberRemoteSourceList(client: PupilHttpClient = localDI().direct.instance()) = produceState<Map<String, RemoteSourceInfo>?>(null) {
|
||||||
import org.junit.Test
|
while (true) {
|
||||||
import xyz.quaver.pupil.sources.manatoki.getItem
|
value = client.getRemoteSourceList()
|
||||||
import org.junit.Assert.*;
|
delay(1000)
|
||||||
|
|
||||||
/**
|
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
|
|
||||||
class ExampleUnitTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test() {
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Pupil, Hitomi.la viewer for Android
|
* Pupil, Hitomi.la viewer for Android
|
||||||
* Copyright (C) 2021 tom5079
|
* Copyright (C) 2022 tom5079
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -16,12 +16,16 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package xyz.quaver.pupil.migrate
|
package xyz.quaver.pupil.sources
|
||||||
|
|
||||||
class Migrate {
|
import android.graphics.drawable.Drawable
|
||||||
|
|
||||||
fun migrate() {
|
data class SourceEntry(
|
||||||
|
val packageName: String,
|
||||||
}
|
val packagePath: String,
|
||||||
|
val sourceName: String,
|
||||||
}
|
val sourcePath: String,
|
||||||
|
val sourceDir: String,
|
||||||
|
val icon: Drawable,
|
||||||
|
val version: String
|
||||||
|
)
|
||||||
@@ -52,8 +52,6 @@ import xyz.quaver.pupil.ui.theme.PupilTheme
|
|||||||
class MainActivity : ComponentActivity(), DIAware {
|
class MainActivity : ComponentActivity(), DIAware {
|
||||||
override val di by closestDI()
|
override val di by closestDI()
|
||||||
|
|
||||||
private val logger = newLogger(LoggerFactory.default)
|
|
||||||
|
|
||||||
@SuppressLint("UnusedCrossfadeTargetStateParameter")
|
@SuppressLint("UnusedCrossfadeTargetStateParameter")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
@@ -34,7 +35,6 @@ import androidx.compose.material.icons.outlined.Info
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.capitalize
|
import androidx.compose.ui.text.capitalize
|
||||||
@@ -55,20 +55,23 @@ import com.google.accompanist.insets.systemBarsPadding
|
|||||||
import com.google.accompanist.insets.ui.BottomNavigation
|
import com.google.accompanist.insets.ui.BottomNavigation
|
||||||
import com.google.accompanist.insets.ui.Scaffold
|
import com.google.accompanist.insets.ui.Scaffold
|
||||||
import com.google.accompanist.insets.ui.TopAppBar
|
import com.google.accompanist.insets.ui.TopAppBar
|
||||||
import io.ktor.client.*
|
import kotlinx.coroutines.coroutineScope
|
||||||
import io.ktor.client.call.*
|
|
||||||
import io.ktor.client.request.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import org.kodein.di.*
|
||||||
import kotlinx.serialization.Serializable
|
import org.kodein.di.compose.localDI
|
||||||
import org.kodein.di.compose.rememberInstance
|
import org.kodein.di.compose.rememberInstance
|
||||||
import xyz.quaver.pupil.sources.SourceEntry
|
import xyz.quaver.pupil.sources.SourceEntry
|
||||||
import xyz.quaver.pupil.sources.rememberSources
|
import xyz.quaver.pupil.sources.rememberLocalSourceList
|
||||||
import xyz.quaver.pupil.util.ApkDownloadManager
|
import xyz.quaver.pupil.sources.rememberRemoteSourceList
|
||||||
|
import xyz.quaver.pupil.util.PupilHttpClient
|
||||||
|
import xyz.quaver.pupil.util.RemoteSourceInfo
|
||||||
|
import xyz.quaver.pupil.util.launchApkInstaller
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.collections.associateBy
|
||||||
|
import kotlin.collections.contains
|
||||||
|
import kotlin.collections.forEach
|
||||||
|
import kotlin.collections.listOf
|
||||||
|
import kotlin.collections.orEmpty
|
||||||
|
|
||||||
private sealed class SourceSelectorScreen(val route: String, val icon: ImageVector) {
|
private sealed class SourceSelectorScreen(val route: String, val icon: ImageVector) {
|
||||||
object Local: SourceSelectorScreen("local", Icons.Default.DownloadDone)
|
object Local: SourceSelectorScreen("local", Icons.Default.DownloadDone)
|
||||||
@@ -80,8 +83,53 @@ private val sourceSelectorScreens = listOf(
|
|||||||
SourceSelectorScreen.Explore
|
SourceSelectorScreen.Explore
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class DownloadApkActionState(override val di: DI) : DIAware {
|
||||||
|
private val app: Application by instance()
|
||||||
|
private val client: PupilHttpClient by instance()
|
||||||
|
|
||||||
|
var progress by mutableStateOf<Float?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
suspend fun download(sourceInfo: RemoteSourceInfo): File {
|
||||||
|
val file = File(app.cacheDir, "apks/${sourceInfo.name}-${sourceInfo.version}.apk").also {
|
||||||
|
it.parentFile?.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
client.downloadApk(sourceInfo, file).collect { progress = it }
|
||||||
|
|
||||||
|
require(progress == Float.POSITIVE_INFINITY)
|
||||||
|
|
||||||
|
progress = null
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceListItem(icon: Painter, name: String, version: String, actions: @Composable () -> Unit = { }) {
|
fun rememberDownloadApkActionState(di: DI = localDI()) = remember { DownloadApkActionState(di) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DownloadApkAction(
|
||||||
|
state: DownloadApkActionState = rememberDownloadApkActionState(),
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
state.progress?.let { progress ->
|
||||||
|
Box(
|
||||||
|
Modifier.padding(12.dp, 0.dp)
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
progress.isFinite() ->
|
||||||
|
CircularProgressIndicator(progress, modifier = Modifier.size(24.dp))
|
||||||
|
else ->
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
} ?: content()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SourceListItem(icon: @Composable (Modifier) -> Unit = { }, name: String, version: String, actions: @Composable () -> Unit = { }) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.padding(8.dp),
|
modifier = Modifier.padding(8.dp),
|
||||||
elevation = 4.dp
|
elevation = 4.dp
|
||||||
@@ -91,18 +139,12 @@ fun SourceListItem(icon: Painter, name: String, version: String, actions: @Compo
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Image(
|
icon(Modifier.size(48.dp))
|
||||||
icon,
|
|
||||||
contentDescription = "source icon",
|
|
||||||
modifier = Modifier.size(48.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier.weight(1f)
|
Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(name.capitalize(Locale.current))
|
||||||
name.capitalize(Locale.current)
|
|
||||||
)
|
|
||||||
|
|
||||||
CompositionLocalProvider(LocalContentAlpha provides 0.5f) {
|
CompositionLocalProvider(LocalContentAlpha provides 0.5f) {
|
||||||
Text(
|
Text(
|
||||||
@@ -119,9 +161,13 @@ fun SourceListItem(icon: Painter, name: String, version: String, actions: @Compo
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Local(onSource: (SourceEntry) -> Unit) {
|
fun Local(onSource: (SourceEntry) -> Unit) {
|
||||||
val sources by rememberSources()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
if (sources.isEmpty()) {
|
val localSourceList by rememberLocalSourceList()
|
||||||
|
val remoteSourceList by rememberRemoteSourceList()
|
||||||
|
|
||||||
|
if (localSourceList.isEmpty()) {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.align(Alignment.Center),
|
Modifier.align(Alignment.Center),
|
||||||
@@ -136,16 +182,38 @@ fun Local(onSource: (SourceEntry) -> Unit) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(sources) { source ->
|
items(localSourceList) { source ->
|
||||||
|
val actionState = rememberDownloadApkActionState()
|
||||||
|
|
||||||
SourceListItem(
|
SourceListItem(
|
||||||
rememberDrawablePainter(source.icon),
|
icon = { modifier ->
|
||||||
|
Image(
|
||||||
|
rememberDrawablePainter(source.icon),
|
||||||
|
contentDescription = "source icon",
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
},
|
||||||
source.sourceName,
|
source.sourceName,
|
||||||
source.version
|
source.version
|
||||||
) {
|
) {
|
||||||
TextButton(
|
DownloadApkAction(actionState) {
|
||||||
onClick = { onSource(source) }
|
val remoteSource = remoteSourceList?.get(source.packageName)
|
||||||
) {
|
if (remoteSource != null && remoteSource.version != source.version) {
|
||||||
Text("GO")
|
TextButton(onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val file = actionState.download(remoteSource)
|
||||||
|
context.launchApkInstaller(file)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("UPDATE")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TextButton(
|
||||||
|
onClick = { onSource(source) }
|
||||||
|
) {
|
||||||
|
Text("GO")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,39 +221,20 @@ fun Local(onSource: (SourceEntry) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class RemoteSourceInfo(
|
|
||||||
val projectName: String,
|
|
||||||
val name: String,
|
|
||||||
val version: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Explore() {
|
fun Explore() {
|
||||||
val sources by rememberSources()
|
val localSourceList by rememberLocalSourceList()
|
||||||
val localSources by derivedStateOf {
|
val localSources by derivedStateOf {
|
||||||
sources.map {
|
localSourceList.associateBy {
|
||||||
it.packageName to it
|
it.packageName
|
||||||
}.toMap()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val client: HttpClient by rememberInstance()
|
|
||||||
|
|
||||||
val downloadManager: ApkDownloadManager by rememberInstance()
|
|
||||||
val progresses = remember { mutableStateMapOf<String, Float>() }
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val remoteSources by produceState<Map<String, RemoteSourceInfo>?>(null) {
|
val remoteSources by rememberRemoteSourceList()
|
||||||
while (true) {
|
|
||||||
delay(1000)
|
|
||||||
value = withContext(Dispatchers.IO) {
|
|
||||||
client.get("https://raw.githubusercontent.com/tom5079/PupilSources/master/versions.json").body()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier.fillMaxSize()
|
Modifier.fillMaxSize()
|
||||||
@@ -194,50 +243,40 @@ fun Explore() {
|
|||||||
CircularProgressIndicator(Modifier.align(Alignment.Center))
|
CircularProgressIndicator(Modifier.align(Alignment.Center))
|
||||||
else
|
else
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(remoteSources?.values?.toList() ?: emptyList()) { source ->
|
items(remoteSources?.values?.toList().orEmpty()) { sourceInfo ->
|
||||||
|
val actionState = rememberDownloadApkActionState()
|
||||||
|
|
||||||
SourceListItem(
|
SourceListItem(
|
||||||
rememberImagePainter("https://raw.githubusercontent.com/tom5079/PupilSources/master/${source.projectName}/src/main/res/mipmap-xxxhdpi/ic_launcher.png"),
|
icon = { modifier ->
|
||||||
source.name,
|
Image(
|
||||||
source.version
|
rememberImagePainter("https://raw.githubusercontent.com/tom5079/PupilSources/master/${sourceInfo.projectName}/src/main/res/mipmap-xxxhdpi/ic_launcher.png"),
|
||||||
|
contentDescription = "source icon",
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
},
|
||||||
|
sourceInfo.name,
|
||||||
|
sourceInfo.version
|
||||||
) {
|
) {
|
||||||
if (source.name !in progresses)
|
DownloadApkAction(actionState) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (source.name in localSources) {
|
if (sourceInfo.name in localSources) {
|
||||||
context.startActivity(
|
context.startActivity(
|
||||||
Intent(
|
Intent(
|
||||||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||||
Uri.fromParts("package", localSources[source.name]!!.packagePath, null)
|
Uri.fromParts("package", localSources[sourceInfo.name]!!.packagePath, null)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else coroutineScope.launch {
|
} else coroutineScope.launch {
|
||||||
progresses[source.name] = 0f
|
val file = actionState.download(sourceInfo)
|
||||||
downloadManager.download(source.projectName, source.name, source.version)
|
context.launchApkInstaller(file)
|
||||||
.onCompletion {
|
|
||||||
progresses.remove(source.name)
|
|
||||||
}.collectLatest {
|
|
||||||
progresses[source.name] = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
if (source.name !in localSources) Icons.Default.Download
|
if (sourceInfo.name !in localSources) Icons.Default.Download
|
||||||
else Icons.Outlined.Info,
|
else Icons.Outlined.Info,
|
||||||
contentDescription = "download"
|
contentDescription = "download"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
val progress = progresses[source.name]
|
|
||||||
|
|
||||||
Box(
|
|
||||||
Modifier.padding(12.dp, 0.dp)
|
|
||||||
) {
|
|
||||||
when {
|
|
||||||
progress?.isFinite() == true ->
|
|
||||||
CircularProgressIndicator(progress, modifier = Modifier.size(24.dp))
|
|
||||||
else ->
|
|
||||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,7 +323,9 @@ fun SourceSelector(onSource: (SourceEntry) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
NavHost(bottomNavController, startDestination = "local", modifier = Modifier.systemBarsPadding(top = false, bottom = false).padding(contentPadding)) {
|
NavHost(bottomNavController, startDestination = "local", modifier = Modifier
|
||||||
|
.systemBarsPadding(top = false, bottom = false)
|
||||||
|
.padding(contentPadding)) {
|
||||||
composable(SourceSelectorScreen.Local.route) { Local(onSource) }
|
composable(SourceSelectorScreen.Local.route) { Local(onSource) }
|
||||||
composable(SourceSelectorScreen.Explore.route) { Explore() }
|
composable(SourceSelectorScreen.Explore.route) { Explore() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Pupil, Hitomi.la viewer for Android
|
* Pupil, Hitomi.la viewer for Android
|
||||||
* Copyright (C) 2021 tom5079
|
* Copyright (C) 2022 tom5079
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -13,41 +13,55 @@
|
|||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.engine.*
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.statement.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import io.ktor.utils.io.core.*
|
import io.ktor.utils.io.core.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.io.use
|
|
||||||
|
|
||||||
class ApkDownloadManager(private val context: Context, private val client: HttpClient) {
|
@Serializable
|
||||||
fun download(projectName: String, sourceName: String, version: String) = flow {
|
data class RemoteSourceInfo(
|
||||||
val url = "https://github.com/tom5079/PupilSources/releases/download/$sourceName-$version/$projectName-release.apk"
|
val projectName: String,
|
||||||
|
val name: String,
|
||||||
|
val version: String
|
||||||
|
)
|
||||||
|
|
||||||
val file = File(context.externalCacheDir, "apks/$sourceName-$version.apk").also {
|
class PupilHttpClient(engine: HttpClientEngine) {
|
||||||
it.parentFile?.mkdir()
|
private val httpClient = HttpClient(engine) {
|
||||||
it.delete()
|
install(ContentNegotiation) {
|
||||||
|
json()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
client.prepareGet(url).execute { response ->
|
suspend fun getRemoteSourceList(): Map<String, RemoteSourceInfo> = withContext(Dispatchers.IO) {
|
||||||
|
httpClient.get("https://tom5079.github.io/PupilSources/versions.json").body()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadApk(sourceInfo: RemoteSourceInfo, dest: File) = flow {
|
||||||
|
val url =
|
||||||
|
"https://github.com/tom5079/PupilSources/releases/download/${sourceInfo.name}-${sourceInfo.version}/${sourceInfo.projectName}-release.apk"
|
||||||
|
|
||||||
|
httpClient.prepareGet(url).execute { response ->
|
||||||
val channel = response.bodyAsChannel()
|
val channel = response.bodyAsChannel()
|
||||||
val contentLength = response.contentLength() ?: -1
|
val contentLength = response.contentLength() ?: -1
|
||||||
var readBytes = 0f
|
var readBytes = 0f
|
||||||
|
|
||||||
file.outputStream().use { outputStream ->
|
dest.outputStream().use { outputStream ->
|
||||||
while (!channel.isClosedForRead) {
|
while (!channel.isClosedForRead) {
|
||||||
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||||
while (!packet.isEmpty) {
|
while (!packet.isEmpty) {
|
||||||
@@ -62,20 +76,5 @@ class ApkDownloadManager(private val context: Context, private val client: HttpC
|
|||||||
}
|
}
|
||||||
|
|
||||||
emit(Float.POSITIVE_INFINITY)
|
emit(Float.POSITIVE_INFINITY)
|
||||||
|
|
||||||
val uri = FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
"${context.packageName}.fileprovider",
|
|
||||||
file
|
|
||||||
)
|
|
||||||
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
|
||||||
}
|
|
||||||
|
|
||||||
context.startActivity(intent)
|
|
||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Pupil, Hitomi.la viewer for Android
|
* Pupil, Hitomi.la viewer for Android
|
||||||
* Copyright (C) 2021 tom5079
|
* Copyright (C) 2022 tom5079
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -16,10 +16,23 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package xyz.quaver.pupil.migrate
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
class Migrate001 {
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
fun Context.launchApkInstaller(file: File) {
|
||||||
|
val uri = FileProvider.getUriForFile(this, "$packageName.fileprovider", file)
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||||
|
}
|
||||||
|
|
||||||
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
@@ -19,5 +19,5 @@
|
|||||||
|
|
||||||
<paths>
|
<paths>
|
||||||
<cache-path name="cached_image" path="networkcache/" />
|
<cache-path name="cached_image" path="networkcache/" />
|
||||||
<external-cache-path name="apks" path="apks/" />
|
<cache-path name="apks" path="apks/" />
|
||||||
</paths>
|
</paths>
|
||||||
76
app/src/test/java/xyz/quaver/pupil/PupilHttpClientTest.kt
Normal file
76
app/src/test/java/xyz/quaver/pupil/PupilHttpClientTest.kt
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2022 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
|
import io.ktor.client.engine.mock.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import xyz.quaver.pupil.util.PupilHttpClient
|
||||||
|
import xyz.quaver.pupil.util.RemoteSourceInfo
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.math.exp
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class PupilHttpClientTest {
|
||||||
|
|
||||||
|
val tempFile = File.createTempFile("pupilhttpclienttest", ".apk").also {
|
||||||
|
it.deleteOnExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getRemoteSourceList() = runTest {
|
||||||
|
val expected = buildMap {
|
||||||
|
put("hitomi.la", RemoteSourceInfo("hitomi", "hitomi.la", "0.0.1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val mockEngine = MockEngine { _ ->
|
||||||
|
respond(Json.encodeToString(expected), headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.contentType))
|
||||||
|
}
|
||||||
|
|
||||||
|
val client = PupilHttpClient(mockEngine)
|
||||||
|
|
||||||
|
assertEquals(expected, client.getRemoteSourceList())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun downloadApk() = runTest {
|
||||||
|
val expected = Random.Default.nextBytes(1000000) // 1MB
|
||||||
|
|
||||||
|
val mockEngine = MockEngine { _ ->
|
||||||
|
respond(expected, headers = headersOf(HttpHeaders.ContentType, "application/vnd.android.package-archive"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val client = PupilHttpClient(mockEngine)
|
||||||
|
|
||||||
|
client.downloadApk(RemoteSourceInfo("", "", ""), tempFile).collect()
|
||||||
|
|
||||||
|
assertArrayEquals(expected, tempFile.readBytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,9 +7,9 @@ buildscript {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:7.1.3")
|
classpath("com.android.tools.build:gradle:7.1.3")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN_VERSION}")
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN}")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-android-extensions:${Versions.KOTLIN_VERSION}")
|
classpath("org.jetbrains.kotlin:kotlin-android-extensions:${Versions.KOTLIN}")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-serialization:${Versions.KOTLIN_VERSION}")
|
classpath("org.jetbrains.kotlin:kotlin-serialization:${Versions.KOTLIN}")
|
||||||
classpath("com.google.gms:google-services:4.3.10")
|
classpath("com.google.gms:google-services:4.3.10")
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
@@ -20,10 +20,20 @@ const val GROUP_ID = "xyz.quaver"
|
|||||||
const val VERSION = "6.0.0-alpha02"
|
const val VERSION = "6.0.0-alpha02"
|
||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
const val KOTLIN_VERSION = "1.6.10"
|
const val KOTLIN = "1.6.10"
|
||||||
|
const val COROUTINE = "1.6.1"
|
||||||
|
const val SERIALIZATION = "1.3.2"
|
||||||
|
|
||||||
const val JETPACK_COMPOSE = "1.1.1"
|
const val JETPACK_COMPOSE = "1.1.1"
|
||||||
const val ACCOMPANIST = "0.23.1"
|
const val ACCOMPANIST = "0.23.1"
|
||||||
|
|
||||||
|
const val KTOR = "2.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
object Kotlin {
|
||||||
|
const val SERIALIZATION = "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.SERIALIZATION}"
|
||||||
|
const val COROUTINE = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.COROUTINE}"
|
||||||
|
const val COROUTINE_TEST = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.COROUTINE}"
|
||||||
}
|
}
|
||||||
|
|
||||||
object JetpackCompose {
|
object JetpackCompose {
|
||||||
@@ -44,4 +54,13 @@ object Accompanist {
|
|||||||
const val INSETS_UI = "com.google.accompanist:accompanist-insets-ui:${Versions.ACCOMPANIST}"
|
const val INSETS_UI = "com.google.accompanist:accompanist-insets-ui:${Versions.ACCOMPANIST}"
|
||||||
const val DRAWABLE_PAINTER = "com.google.accompanist:accompanist-drawablepainter:${Versions.ACCOMPANIST}"
|
const val DRAWABLE_PAINTER = "com.google.accompanist:accompanist-drawablepainter:${Versions.ACCOMPANIST}"
|
||||||
const val SYSTEM_UI_CONTROLLER = "com.google.accompanist:accompanist-systemuicontroller:${Versions.ACCOMPANIST}"
|
const val SYSTEM_UI_CONTROLLER = "com.google.accompanist:accompanist-systemuicontroller:${Versions.ACCOMPANIST}"
|
||||||
|
}
|
||||||
|
|
||||||
|
object KtorClient {
|
||||||
|
const val CORE = "io.ktor:ktor-client-core:${Versions.KTOR}"
|
||||||
|
const val OKHTTP = "io.ktor:ktor-client-okhttp:${Versions.KTOR}"
|
||||||
|
const val CONTENT_NEGOTIATION = "io.ktor:ktor-client-content-negotiation:${Versions.KTOR}"
|
||||||
|
const val SERIALIZATION = "io.ktor:ktor-serialization-kotlinx-json:${Versions.KTOR}"
|
||||||
|
|
||||||
|
const val TEST = "io.ktor:ktor-client-mock:${Versions.KTOR}"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user