diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 4e3b5f8f..a8b1ab97 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -101,6 +101,8 @@ dependencies {
implementation(JetpackCompose.MATERIAL_ICONS)
implementation(JetpackCompose.RUNTIME_LIVEDATA)
+// implementation(JetpackCompose.MARKDOWN)
+
implementation(Accompanist.INSETS)
implementation(Accompanist.INSETS_UI)
implementation(Accompanist.FLOW_LAYOUT)
@@ -108,7 +110,7 @@ dependencies {
implementation(Accompanist.DRAWABLE_PAINTER)
implementation(Accompanist.APPCOMPAT_THEME)
- implementation("io.coil-kt:coil-compose:1.4.0")
+ implementation("io.coil-kt:coil-compose:2.0.0-rc03")
implementation(KtorClient.CORE)
implementation(KtorClient.OKHTTP)
@@ -136,8 +138,6 @@ dependencies {
implementation("org.jsoup:jsoup:1.14.3")
- implementation("ru.noties.markwon:core:3.1.0")
-
implementation("xyz.quaver.pupil.sources:core:0.0.1-alpha01-DEV26")
implementation("xyz.quaver:documentfilex:0.7.2")
diff --git a/app/src/androidTest/java/xyz/quaver/pupil/SourceLoaderInstrumentedTest.kt b/app/src/androidTest/java/xyz/quaver/pupil/SourceLoaderInstrumentedTest.kt
deleted file mode 100644
index 094248a0..00000000
--- a/app/src/androidTest/java/xyz/quaver/pupil/SourceLoaderInstrumentedTest.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Pupil, Hitomi.la viewer for Android
- * Copyright (C) 2021 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 .
- */
-
-package xyz.quaver.pupil
-
-import android.app.Application
-import android.content.pm.PackageManager
-import android.util.Log
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.Assert.*
-import xyz.quaver.pupil.sources.isSourceFeatureEnabled
-import xyz.quaver.pupil.sources.loadSource
-
-@RunWith(AndroidJUnit4::class)
-class SourceLoaderInstrumentedTest {
-
- @Test
- fun getPackages() {
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- val application: Application = appContext.applicationContext as Application
-
- val packageManager = appContext.packageManager
-
- val packages = packageManager.getInstalledPackages(
- PackageManager.GET_CONFIGURATIONS or
- PackageManager.GET_META_DATA
- )
-
- val sources = packages.filter { it.isSourceFeatureEnabled }
-
- assertEquals(1, sources.size)
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
index d3265711..7f4a437d 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
@@ -35,9 +35,13 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
+import org.kodein.di.compose.rememberInstance
+import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.sources.core.Source
import xyz.quaver.pupil.sources.loadSource
import xyz.quaver.pupil.ui.theme.PupilTheme
+import xyz.quaver.pupil.util.PupilHttpClient
+import xyz.quaver.pupil.util.Release
class MainActivity : ComponentActivity(), DIAware {
override val di by closestDI()
@@ -57,6 +61,14 @@ class MainActivity : ComponentActivity(), DIAware {
val coroutineScope = rememberCoroutineScope()
+ val client: PupilHttpClient by rememberInstance()
+
+ val latestRelease by produceState(null) {
+ value = client.latestRelease()
+ }
+
+ var dismissUpdate by remember { mutableStateOf(false) }
+
SideEffect {
systemUiController.setSystemBarsColor(
color = Color.Transparent,
@@ -64,6 +76,14 @@ class MainActivity : ComponentActivity(), DIAware {
)
}
+ latestRelease?.let { release ->
+ UpdateAlertDialog(
+ show = !dismissUpdate && release.version != BuildConfig.VERSION_NAME,
+ release = release,
+ onDismiss = { dismissUpdate = true }
+ )
+ }
+
NavHost(navController, "main") {
composable("main") {
var source by remember { mutableStateOf(null) }
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/SourceSelector.kt b/app/src/main/java/xyz/quaver/pupil/ui/SourceSelector.kt
index 0afc1b2e..2016857a 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/SourceSelector.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/SourceSelector.kt
@@ -55,11 +55,11 @@ import com.google.accompanist.insets.systemBarsPadding
import com.google.accompanist.insets.ui.BottomNavigation
import com.google.accompanist.insets.ui.Scaffold
import com.google.accompanist.insets.ui.TopAppBar
-import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.kodein.di.*
import org.kodein.di.compose.localDI
-import org.kodein.di.compose.rememberInstance
import xyz.quaver.pupil.sources.SourceEntry
import xyz.quaver.pupil.sources.rememberLocalSourceList
import xyz.quaver.pupil.sources.rememberRemoteSourceList
@@ -83,6 +83,10 @@ private val sourceSelectorScreens = listOf(
SourceSelectorScreen.Explore
)
+private val RemoteSourceInfo.apkUrl: String
+ get() = "https://github.com/tom5079/PupilSources/releases/download/$name-$version/$projectName-release.apk"
+
+
class DownloadApkActionState(override val di: DI) : DIAware {
private val app: Application by instance()
private val client: PupilHttpClient by instance()
@@ -90,19 +94,20 @@ class DownloadApkActionState(override val di: DI) : DIAware {
var progress by mutableStateOf(null)
private set
- suspend fun download(sourceInfo: RemoteSourceInfo): File {
+ suspend fun download(url: String): File? = withContext(Dispatchers.IO) {
progress = 0f
- val file = File(app.cacheDir, "apks/${sourceInfo.name}-${sourceInfo.version}.apk").also {
+ val file = File.createTempFile("pupil", ".apk", File(app.cacheDir, "apks")).also {
it.parentFile?.mkdirs()
}
- client.downloadApk(sourceInfo, file).collect { progress = it }
+ client.downloadFile(url, file).collect { progress = it }
- require(progress == Float.POSITIVE_INFINITY)
+ if (progress == Float.POSITIVE_INFINITY) file else null
+ }
+ fun reset() {
progress = null
- return file
}
}
@@ -111,7 +116,7 @@ fun rememberDownloadApkActionState(di: DI = localDI()) = remember { DownloadApkA
@Composable
fun DownloadApkAction(
- state: DownloadApkActionState = rememberDownloadApkActionState(),
+ state: DownloadApkActionState,
content: @Composable () -> Unit
) {
state.progress?.let { progress ->
@@ -203,8 +208,9 @@ fun Local(onSource: (SourceEntry) -> Unit) {
if (remoteSource != null && remoteSource.version != source.version) {
TextButton(onClick = {
coroutineScope.launch {
- val file = actionState.download(remoteSource)
+ val file = actionState.download(remoteSource.apkUrl)!! // TODO("Handle error")
context.launchApkInstaller(file)
+ actionState.reset()
}
}) {
Text("UPDATE")
@@ -263,8 +269,9 @@ fun Explore() {
if (localSources[sourceInfo.name]?.version != sourceInfo.version) {
TextButton(onClick = {
coroutineScope.launch {
- val file = actionState.download(sourceInfo)
+ val file = actionState.download(sourceInfo.apkUrl)!! // TODO("Handle exception")
context.launchApkInstaller(file)
+ actionState.reset()
}
}) {
Text("UPDATE")
@@ -279,8 +286,9 @@ fun Explore() {
)
)
} else coroutineScope.launch {
- val file = actionState.download(sourceInfo)
+ val file = actionState.download(sourceInfo.apkUrl)!! // TODO("Handle exception")
context.launchApkInstaller(file)
+ actionState.reset()
}
}) {
Icon(
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/UpdateDialog.kt b/app/src/main/java/xyz/quaver/pupil/ui/UpdateDialog.kt
new file mode 100644
index 00000000..be55ecdd
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/ui/UpdateDialog.kt
@@ -0,0 +1,95 @@
+/*
+ * 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 .
+ */
+
+package xyz.quaver.pupil.ui
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import kotlinx.coroutines.launch
+import org.kodein.di.compose.onDIContext
+import xyz.quaver.pupil.util.Release
+import xyz.quaver.pupil.util.launchApkInstaller
+import java.util.*
+
+@Composable
+fun UpdateAlertDialog(
+ show: Boolean,
+ release: Release,
+ onDismiss: () -> Unit
+) {
+ val state = rememberDownloadApkActionState()
+
+ val coroutineScope = rememberCoroutineScope()
+ val context = LocalContext.current
+
+ if (show) {
+ Dialog(onDismissRequest = { if (state.progress == null) onDismiss() }) {
+ Card {
+ val progress = state.progress
+
+ if (progress != null) {
+ if (progress.isFinite() && progress > 0)
+ LinearProgressIndicator(progress)
+ else
+ LinearProgressIndicator()
+ }
+
+ Column(
+ Modifier.padding(start = 8.dp, top = 8.dp, end = 8.dp, bottom = 0.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ "Update Available",
+ style = MaterialTheme.typography.h6
+ )
+
+ Text(release.releaseNotes.getOrElse(Locale.getDefault()) { release.releaseNotes[Locale.ENGLISH]!! })
+
+ Row(
+ Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End
+ ) {
+ TextButton(onClick = onDismiss, enabled = progress == null) {
+ Text("DISMISS")
+ }
+
+ TextButton(
+ onClick = {
+ coroutineScope.launch {
+ val file = state.download(release.apkUrl)!! // TODO("Handle exception")
+ context.launchApkInstaller(file)
+ state.reset()
+ onDismiss()
+ }
+ },
+ enabled = progress == null
+ ) {
+ Text("UPDATE")
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/PupilHttpClient.kt b/app/src/main/java/xyz/quaver/pupil/util/PupilHttpClient.kt
index e063e14a..a6a2a4be 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/PupilHttpClient.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/PupilHttpClient.kt
@@ -18,6 +18,7 @@
package xyz.quaver.pupil.util
+import androidx.compose.ui.res.stringArrayResource
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.*
@@ -32,7 +33,9 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.*
import java.io.File
+import java.util.*
@Serializable
data class RemoteSourceInfo(
@@ -41,6 +44,18 @@ data class RemoteSourceInfo(
val version: String
)
+class Release(
+ val version: String,
+ val apkUrl: String,
+ val releaseNotes: Map
+)
+
+private val localeMap = mapOf(
+ "한국어" to Locale.KOREAN,
+ "日本語" to Locale.JAPANESE,
+ "English" to Locale.ENGLISH
+)
+
class PupilHttpClient(engine: HttpClientEngine) {
private val httpClient = HttpClient(engine) {
install(ContentNegotiation) {
@@ -48,33 +63,94 @@ class PupilHttpClient(engine: HttpClientEngine) {
}
}
+ /**
+ * Fetch a list of available sources from PupilSources repository.
+ * Returns empty map when exception occurs
+ */
suspend fun getRemoteSourceList(): Map = withContext(Dispatchers.IO) {
- httpClient.get("https://tom5079.github.io/PupilSources/versions.json").body()
+ runCatching {
+ httpClient.get("https://tom5079.github.io/PupilSources/versions.json").body