Pupil-129 [Source] Implement in-app update
This commit is contained in:
@@ -101,6 +101,8 @@ dependencies {
|
|||||||
implementation(JetpackCompose.MATERIAL_ICONS)
|
implementation(JetpackCompose.MATERIAL_ICONS)
|
||||||
implementation(JetpackCompose.RUNTIME_LIVEDATA)
|
implementation(JetpackCompose.RUNTIME_LIVEDATA)
|
||||||
|
|
||||||
|
implementation(JetpackCompose.MARKDOWN)
|
||||||
|
|
||||||
implementation(Accompanist.INSETS)
|
implementation(Accompanist.INSETS)
|
||||||
implementation(Accompanist.INSETS_UI)
|
implementation(Accompanist.INSETS_UI)
|
||||||
implementation(Accompanist.FLOW_LAYOUT)
|
implementation(Accompanist.FLOW_LAYOUT)
|
||||||
|
|||||||
@@ -35,9 +35,13 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.kodein.di.DIAware
|
import org.kodein.di.DIAware
|
||||||
import org.kodein.di.android.closestDI
|
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.core.Source
|
||||||
import xyz.quaver.pupil.sources.loadSource
|
import xyz.quaver.pupil.sources.loadSource
|
||||||
import xyz.quaver.pupil.ui.theme.PupilTheme
|
import xyz.quaver.pupil.ui.theme.PupilTheme
|
||||||
|
import xyz.quaver.pupil.util.PupilHttpClient
|
||||||
|
import xyz.quaver.pupil.util.Release
|
||||||
|
|
||||||
class MainActivity : ComponentActivity(), DIAware {
|
class MainActivity : ComponentActivity(), DIAware {
|
||||||
override val di by closestDI()
|
override val di by closestDI()
|
||||||
@@ -57,6 +61,14 @@ class MainActivity : ComponentActivity(), DIAware {
|
|||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val client: PupilHttpClient by rememberInstance()
|
||||||
|
|
||||||
|
val latestRelease by produceState<Release?>(null) {
|
||||||
|
value = client.latestRelease()
|
||||||
|
}
|
||||||
|
|
||||||
|
var dismissUpdate by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
SideEffect {
|
SideEffect {
|
||||||
systemUiController.setSystemBarsColor(
|
systemUiController.setSystemBarsColor(
|
||||||
color = Color.Transparent,
|
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") {
|
NavHost(navController, "main") {
|
||||||
composable("main") {
|
composable("main") {
|
||||||
var source by remember { mutableStateOf<Source?>(null) }
|
var source by remember { mutableStateOf<Source?>(null) }
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ 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 kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.kodein.di.*
|
import org.kodein.di.*
|
||||||
import org.kodein.di.compose.localDI
|
import org.kodein.di.compose.localDI
|
||||||
import org.kodein.di.compose.rememberInstance
|
|
||||||
import xyz.quaver.pupil.sources.SourceEntry
|
import xyz.quaver.pupil.sources.SourceEntry
|
||||||
import xyz.quaver.pupil.sources.rememberLocalSourceList
|
import xyz.quaver.pupil.sources.rememberLocalSourceList
|
||||||
import xyz.quaver.pupil.sources.rememberRemoteSourceList
|
import xyz.quaver.pupil.sources.rememberRemoteSourceList
|
||||||
@@ -94,19 +94,20 @@ class DownloadApkActionState(override val di: DI) : DIAware {
|
|||||||
var progress by mutableStateOf<Float?>(null)
|
var progress by mutableStateOf<Float?>(null)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
suspend fun download(sourceInfo: RemoteSourceInfo): File {
|
suspend fun download(url: String): File? = withContext(Dispatchers.IO) {
|
||||||
progress = 0f
|
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()
|
it.parentFile?.mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
client.downloadApk(sourceInfo.apkUrl, 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
|
progress = null
|
||||||
return file
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ fun rememberDownloadApkActionState(di: DI = localDI()) = remember { DownloadApkA
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadApkAction(
|
fun DownloadApkAction(
|
||||||
state: DownloadApkActionState = rememberDownloadApkActionState(),
|
state: DownloadApkActionState,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
state.progress?.let { progress ->
|
state.progress?.let { progress ->
|
||||||
@@ -207,8 +208,9 @@ fun Local(onSource: (SourceEntry) -> Unit) {
|
|||||||
if (remoteSource != null && remoteSource.version != source.version) {
|
if (remoteSource != null && remoteSource.version != source.version) {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val file = actionState.download(remoteSource)
|
val file = actionState.download(remoteSource.apkUrl)!! // TODO("Handle error")
|
||||||
context.launchApkInstaller(file)
|
context.launchApkInstaller(file)
|
||||||
|
actionState.reset()
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Text("UPDATE")
|
Text("UPDATE")
|
||||||
@@ -267,8 +269,9 @@ fun Explore() {
|
|||||||
if (localSources[sourceInfo.name]?.version != sourceInfo.version) {
|
if (localSources[sourceInfo.name]?.version != sourceInfo.version) {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val file = actionState.download(sourceInfo)
|
val file = actionState.download(sourceInfo.apkUrl)!! // TODO("Handle exception")
|
||||||
context.launchApkInstaller(file)
|
context.launchApkInstaller(file)
|
||||||
|
actionState.reset()
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Text("UPDATE")
|
Text("UPDATE")
|
||||||
@@ -283,8 +286,9 @@ fun Explore() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else coroutineScope.launch {
|
} else coroutineScope.launch {
|
||||||
val file = actionState.download(sourceInfo)
|
val file = actionState.download(sourceInfo.apkUrl)!! // TODO("Handle exception")
|
||||||
context.launchApkInstaller(file)
|
context.launchApkInstaller(file)
|
||||||
|
actionState.reset()
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
96
app/src/main/java/xyz/quaver/pupil/ui/UpdateDialog.kt
Normal file
96
app/src/main/java/xyz/quaver/pupil/ui/UpdateDialog.kt
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* 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.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 dev.jeziellago.compose.markdowntext.MarkdownText
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
MarkdownText(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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.engine.*
|
import io.ktor.client.engine.*
|
||||||
@@ -46,7 +47,7 @@ data class RemoteSourceInfo(
|
|||||||
class Release(
|
class Release(
|
||||||
val version: String,
|
val version: String,
|
||||||
val apkUrl: String,
|
val apkUrl: String,
|
||||||
val updateNotes: Map<Locale, String>
|
val releaseNotes: Map<Locale, String>
|
||||||
)
|
)
|
||||||
|
|
||||||
private val localeMap = mapOf(
|
private val localeMap = mapOf(
|
||||||
@@ -62,69 +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<String, RemoteSourceInfo> = withContext(Dispatchers.IO) {
|
suspend fun getRemoteSourceList(): Map<String, RemoteSourceInfo> = withContext(Dispatchers.IO) {
|
||||||
httpClient.get("https://tom5079.github.io/PupilSources/versions.json").body()
|
runCatching {
|
||||||
|
httpClient.get("https://tom5079.github.io/PupilSources/versions.json").body<Map<String, RemoteSourceInfo>>()
|
||||||
|
}.getOrDefault(emptyMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadApk(url: String, dest: File) = flow {
|
/**
|
||||||
httpClient.prepareGet(url).execute { response ->
|
* Downloads specific file from :url to :dest.
|
||||||
val channel = response.bodyAsChannel()
|
* Returns flow that emits progress.
|
||||||
val contentLength = response.contentLength() ?: -1
|
* when value emitted by flow {
|
||||||
var readBytes = 0f
|
* in 0f .. 1f -> downloading
|
||||||
|
* POSITIVE_INFINITY -> download finised
|
||||||
|
* NEGATIVE_INFINITY -> exception occured
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
fun downloadFile(url: String, dest: File) = flow {
|
||||||
|
runCatching {
|
||||||
|
httpClient.prepareGet(url).execute { response ->
|
||||||
|
val channel = response.bodyAsChannel()
|
||||||
|
val contentLength = response.contentLength() ?: -1
|
||||||
|
var readBytes = 0f
|
||||||
|
|
||||||
dest.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) {
|
||||||
val bytes = packet.readBytes()
|
val bytes = packet.readBytes()
|
||||||
outputStream.write(bytes)
|
outputStream.write(bytes)
|
||||||
|
|
||||||
readBytes += bytes.size
|
readBytes += bytes.size
|
||||||
emit(readBytes / contentLength)
|
emit(readBytes / contentLength)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
emit(Float.POSITIVE_INFINITY)
|
emit(Float.POSITIVE_INFINITY)
|
||||||
|
}.onFailure {
|
||||||
|
emit(Float.NEGATIVE_INFINITY)
|
||||||
|
}
|
||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
suspend fun latestRelease(beta: Boolean = true): Release = withContext(Dispatchers.IO) {
|
/**
|
||||||
val releases = Json.parseToJsonElement(
|
* Latest application release info from Github API.
|
||||||
httpClient.get("https://api.github.com/repos/tom5079/Pupil/releases").bodyAsText()
|
* Returns null when exception occurs.
|
||||||
).jsonArray
|
*/
|
||||||
|
suspend fun latestRelease(beta: Boolean = true): Release? = withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
val releases = Json.parseToJsonElement(
|
||||||
|
httpClient.get("https://api.github.com/repos/tom5079/Pupil/releases").bodyAsText()
|
||||||
|
).jsonArray
|
||||||
|
|
||||||
val latestRelease = releases.first { release ->
|
val latestRelease = releases.first { release ->
|
||||||
beta || !release.jsonObject["prerelease"]!!.jsonPrimitive.boolean
|
beta || !release.jsonObject["prerelease"]!!.jsonPrimitive.boolean
|
||||||
}.jsonObject
|
}.jsonObject
|
||||||
|
|
||||||
val version = latestRelease["tag_name"]!!.jsonPrimitive.content
|
val version = latestRelease["tag_name"]!!.jsonPrimitive.content
|
||||||
|
|
||||||
val apkUrl = latestRelease["assets"]!!.jsonArray.first { asset ->
|
val apkUrl = latestRelease["assets"]!!.jsonArray.first { asset ->
|
||||||
val name = asset.jsonObject["name"]!!.jsonPrimitive.content
|
val name = asset.jsonObject["name"]!!.jsonPrimitive.content
|
||||||
name.startsWith("Pupil-v") && name.endsWith(".apk")
|
name.startsWith("Pupil-v") && name.endsWith(".apk")
|
||||||
}.jsonObject["browser_download_url"]!!.jsonPrimitive.content
|
}.jsonObject["browser_download_url"]!!.jsonPrimitive.content
|
||||||
|
|
||||||
val updateNotes: Map<Locale, String> = buildMap {
|
val releaseNotes: Map<Locale, String> = buildMap {
|
||||||
val body = latestRelease["body"]!!.jsonPrimitive.content
|
val body = latestRelease["body"]!!.jsonPrimitive.content
|
||||||
|
|
||||||
var locale: Locale? = null
|
var locale: Locale? = null
|
||||||
val stringBuilder = StringBuilder()
|
val stringBuilder = StringBuilder()
|
||||||
body.lineSequence().forEach { line ->
|
body.lineSequence().forEach { line ->
|
||||||
localeMap[line.drop(3)]?.let { newLocale ->
|
localeMap[line.drop(3)]?.let { newLocale ->
|
||||||
if (locale != null) {
|
if (locale != null) {
|
||||||
put(locale!!, stringBuilder.deleteCharAt(stringBuilder.length-1).toString())
|
put(locale!!, stringBuilder.deleteCharAt(stringBuilder.length-1).toString())
|
||||||
stringBuilder.clear()
|
stringBuilder.clear()
|
||||||
|
}
|
||||||
|
locale = newLocale
|
||||||
|
return@forEach
|
||||||
}
|
}
|
||||||
locale = newLocale
|
|
||||||
return@forEach
|
if (locale != null) stringBuilder.appendLine(line)
|
||||||
}
|
}
|
||||||
|
put(locale!!, stringBuilder.deleteCharAt(stringBuilder.length-1).toString())
|
||||||
if (locale != null) stringBuilder.appendLine(line)
|
|
||||||
}
|
}
|
||||||
put(locale!!, stringBuilder.deleteCharAt(stringBuilder.length-1).toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
Release(version, apkUrl, updateNotes)
|
Release(version, apkUrl, releaseNotes)
|
||||||
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ class PupilHttpClientTest {
|
|||||||
|
|
||||||
val client = PupilHttpClient(mockEngine)
|
val client = PupilHttpClient(mockEngine)
|
||||||
|
|
||||||
client.downloadApk("http://a/", tempFile).collect()
|
client.downloadFile("http://a/", tempFile).collect()
|
||||||
|
|
||||||
assertArrayEquals(expected, tempFile.readBytes())
|
assertArrayEquals(expected, tempFile.readBytes())
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ class PupilHttpClientTest {
|
|||||||
fun latestRelease() = runTest {
|
fun latestRelease() = runTest {
|
||||||
val expectedVersion = "5.3.7"
|
val expectedVersion = "5.3.7"
|
||||||
val expectedApkUrl = "https://github.com/tom5079/Pupil/releases/download/5.3.7/Pupil-v5.3.7.apk"
|
val expectedApkUrl = "https://github.com/tom5079/Pupil/releases/download/5.3.7/Pupil-v5.3.7.apk"
|
||||||
val expectedUpdateNotes = mapOf(
|
val expectedReleaseNotes = mapOf(
|
||||||
Locale.KOREAN to """
|
Locale.KOREAN to """
|
||||||
* 가끔씩 무한로딩 걸리는 현상 수정
|
* 가끔씩 무한로딩 걸리는 현상 수정
|
||||||
* 백업시 즐겨찾기 태그도 백업되게 수정
|
* 백업시 즐겨찾기 태그도 백업되게 수정
|
||||||
@@ -100,14 +100,14 @@ class PupilHttpClientTest {
|
|||||||
|
|
||||||
val client = PupilHttpClient(mockEngine)
|
val client = PupilHttpClient(mockEngine)
|
||||||
|
|
||||||
val release = client.latestRelease()
|
val release = client.latestRelease()!!
|
||||||
|
|
||||||
assertEquals(expectedVersion, release.version)
|
assertEquals(expectedVersion, release.version)
|
||||||
assertEquals(expectedApkUrl, release.apkUrl)
|
assertEquals(expectedApkUrl, release.apkUrl)
|
||||||
|
|
||||||
println(expectedUpdateNotes)
|
println(expectedReleaseNotes)
|
||||||
println(release.updateNotes)
|
println(release.releaseNotes)
|
||||||
assertEquals(expectedUpdateNotes, release.updateNotes)
|
assertEquals(expectedReleaseNotes, release.releaseNotes)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,8 @@ object JetpackCompose {
|
|||||||
const val RUNTIME_LIVEDATA = "androidx.compose.runtime:runtime-livedata:${Versions.JETPACK_COMPOSE}"
|
const val RUNTIME_LIVEDATA = "androidx.compose.runtime:runtime-livedata:${Versions.JETPACK_COMPOSE}"
|
||||||
const val UI_UTIL = "androidx.compose.ui:ui-util:${Versions.JETPACK_COMPOSE}"
|
const val UI_UTIL = "androidx.compose.ui:ui-util:${Versions.JETPACK_COMPOSE}"
|
||||||
const val ANIMATION = "androidx.compose.animation:animation:${Versions.JETPACK_COMPOSE}"
|
const val ANIMATION = "androidx.compose.animation:animation:${Versions.JETPACK_COMPOSE}"
|
||||||
|
|
||||||
|
const val MARKDOWN = "com.github.jeziellago:compose-markdown:0.2.9"
|
||||||
}
|
}
|
||||||
|
|
||||||
object Accompanist {
|
object Accompanist {
|
||||||
|
|||||||
Reference in New Issue
Block a user