feat: updater changelogs (#48)

---------

Co-authored-by: Aunali321 <aunvakil.aa@gmail.com>
This commit is contained in:
Ax333l 2023-07-07 10:56:04 +02:00 committed by GitHub
parent 62e293763d
commit d611ae7e9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 528 additions and 155 deletions

View File

@ -82,6 +82,7 @@ dependencies {
//implementation("com.google.accompanist:accompanist-systemuicontroller:$accompanistVersion")
//implementation("com.google.accompanist:accompanist-placeholder-material:$accompanistVersion")
implementation("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
implementation("com.google.accompanist:accompanist-webview:$accompanistVersion")
//implementation("com.google.accompanist:accompanist-flowlayout:$accompanistVersion")
//implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion")
@ -128,4 +129,6 @@ dependencies {
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
// Markdown to HTML
implementation("org.jetbrains:markdown:0.4.1")
}

View File

@ -1,17 +1,15 @@
package app.revanced.manager.di
import app.revanced.manager.data.platform.FileSystem
import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.ReVancedRepository
import app.revanced.manager.domain.repository.*
import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.domain.repository.SourcePersistenceRepository
import app.revanced.manager.domain.repository.SourceRepository
import app.revanced.manager.domain.worker.WorkerRepository
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val repositoryModule = module {
singleOf(::ReVancedRepository)
singleOf(::GithubRepository)
singleOf(::ManagerAPI)
singleOf(::FileSystem)
singleOf(::SourcePersistenceRepository)

View File

@ -1,5 +1,6 @@
package app.revanced.manager.di
import app.revanced.manager.network.service.GithubService
import app.revanced.manager.network.service.HttpService
import app.revanced.manager.network.service.ReVancedService
import org.koin.core.module.dsl.singleOf
@ -16,4 +17,5 @@ val serviceModule = module {
single { provideReVancedService(get()) }
singleOf(::HttpService)
singleOf(::GithubService)
}

View File

@ -11,7 +11,8 @@ val viewModelModule = module {
viewModelOf(::AppSelectorViewModel)
viewModelOf(::SourcesViewModel)
viewModelOf(::InstallerViewModel)
viewModelOf(::UpdateSettingsViewModel)
viewModelOf(::UpdateProgressViewModel)
viewModelOf(::ManagerUpdateChangelogViewModel)
viewModelOf(::ImportExportViewModel)
viewModelOf(::ContributorViewModel)
}

View File

@ -0,0 +1,7 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.network.service.GithubService
class GithubRepository(private val service: GithubService) {
suspend fun getChangelog(repo: String) = service.getChangelog(repo)
}

View File

@ -17,7 +17,6 @@ import io.ktor.utils.io.*
import java.io.File
class ManagerAPI(
private val app: Application,
private val client: HttpClient,
private val revancedRepository: ReVancedRepository
) {
@ -27,7 +26,7 @@ class ManagerAPI(
private suspend fun downloadAsset(downloadUrl: String, saveLocation: File) {
client.get(downloadUrl) {
onDownload { bytesSentTotal, contentLength, ->
onDownload { bytesSentTotal, contentLength ->
downloadProgress = (bytesSentTotal.toFloat() / contentLength.toFloat())
downloadedSize = bytesSentTotal
totalSize = contentLength
@ -51,19 +50,9 @@ class ManagerAPI(
return patchBundleAsset.version to integrationsAsset.version
}
suspend fun downloadManager(): File? {
try {
val managerAsset = revancedRepository.findAsset(ghManager, ".apk")
val managerFile = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).also { it.mkdirs() }
.resolve("revanced-manager.apk")
downloadAsset(managerAsset.downloadUrl, managerFile)
println("Downloaded manager at ${managerFile.absolutePath}")
return managerFile
} catch (e: Exception) {
Log.e(tag, "Failed to download manager", e)
app.toast("Failed to download manager")
}
return null
suspend fun downloadManager(location: File) {
val managerAsset = revancedRepository.findAsset(ghManager, ".apk")
downloadAsset(managerAsset.downloadUrl, location)
}
}

View File

@ -0,0 +1,16 @@
package app.revanced.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GithubChangelog(
@SerialName("tag_name") val version: String,
@SerialName("body") val body: String,
@SerialName("assets") val assets: List<GithubAsset>
)
@Serializable
data class GithubAsset(
@SerialName("download_count") val downloadCount: Int,
)

View File

@ -5,11 +5,11 @@ import kotlinx.serialization.Serializable
@Serializable
class ReVancedReleases(
@SerialName("tools") val tools: List<Assets>,
@SerialName("tools") val tools: List<Asset>,
)
@Serializable
class Assets(
class Asset(
@SerialName("repository") val repository: String,
@SerialName("version") val version: String,
@SerialName("timestamp") val timestamp: String,

View File

@ -0,0 +1,15 @@
package app.revanced.manager.network.service
import app.revanced.manager.network.dto.GithubChangelog
import app.revanced.manager.network.utils.APIResponse
import io.ktor.client.request.url
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class GithubService(private val client: HttpService) {
suspend fun getChangelog(repo: String): APIResponse<GithubChangelog> = withContext(Dispatchers.IO) {
client.request {
url("https://api.github.com/repos/revanced/$repo/releases/latest")
}
}
}

View File

@ -1,7 +1,7 @@
package app.revanced.manager.network.service
import app.revanced.manager.network.api.MissingAssetException
import app.revanced.manager.network.dto.Assets
import app.revanced.manager.network.dto.Asset
import app.revanced.manager.network.dto.ReVancedReleases
import app.revanced.manager.network.dto.ReVancedRepositories
import app.revanced.manager.network.utils.APIResponse
@ -30,14 +30,14 @@ class ReVancedService(
}
}
suspend fun findAsset(repo: String, file: String): Assets {
suspend fun findAsset(repo: String, file: String): Asset {
val releases = getAssets().getOrThrow()
val asset = releases.tools.find { asset ->
(asset.name.contains(file) && asset.repository.contains(repo))
} ?: throw MissingAssetException()
return Assets(asset.repository, asset.version, asset.timestamp, asset.name,asset.size, asset.downloadUrl, asset.content_type)
return asset
}
private companion object {

View File

@ -0,0 +1,112 @@
package app.revanced.manager.ui.component
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.ViewGroup
import android.webkit.WebResourceRequest
import android.webkit.WebView
import androidx.compose.foundation.background
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import app.revanced.manager.util.hexCode
import app.revanced.manager.util.openUrl
import com.google.accompanist.web.AccompanistWebViewClient
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewStateWithHTMLData
@Composable
@SuppressLint("ClickableViewAccessibility")
fun Markdown(
text: String,
modifier: Modifier = Modifier
) {
val ctx = LocalContext.current
val state = rememberWebViewStateWithHTMLData(data = generateMdHtml(source = text))
val client = remember {
object : AccompanistWebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
if (request != null) ctx.openUrl(request.url.toString())
return true
}
}
}
WebView(
state,
modifier = Modifier
.background(Color.Transparent)
.then(modifier),
client = client,
onCreated = {
it.setBackgroundColor(android.graphics.Color.TRANSPARENT)
it.isVerticalScrollBarEnabled = false
it.isHorizontalScrollBarEnabled = false
it.setOnTouchListener { _, event -> event.action == MotionEvent.ACTION_MOVE }
it.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
)
}
@Composable
fun generateMdHtml(
source: String,
wrap: Boolean = false,
headingColor: Color = MaterialTheme.colorScheme.onSurface,
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
linkColor: Color = MaterialTheme.colorScheme.primary
) = remember(source, wrap, headingColor, textColor, linkColor) {
"""<html>
<head>
<meta charset="utf-8" />
<title>Markdown</title>
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;"/>
<style>
body {
color: #${textColor.hexCode};
}
a {
color: #${linkColor.hexCode}!important;
}
a.anchor {
display: none;
}
.highlight pre, pre {
word-wrap: ${if (wrap) "break-word" else "normal"};
white-space: ${if (wrap) "pre-wrap" else "pre"};
}
h2 {
color: #${headingColor.hexCode};
font-size: 18px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.15px;
}
ul {
margin-left: 0px;
padding-left: 18px;
}
li {
margin-left: 2px;
}
::marker {
font-size: 16px;
margin-right: 8px;
color: #${textColor.hexCode};
}
</style>
</head>
<body>
$source
</body>
</html>"""
}

View File

@ -26,6 +26,9 @@ sealed interface SettingsDestination : Parcelable {
@Parcelize
object UpdateProgress : SettingsDestination
@Parcelize
object UpdateChangelog : SettingsDestination
@Parcelize
object Contributors: SettingsDestination

View File

@ -39,6 +39,9 @@ import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.destination.SettingsDestination
import app.revanced.manager.ui.screen.settings.*
import app.revanced.manager.ui.screen.settings.update.ManagerUpdateChangelog
import app.revanced.manager.ui.screen.settings.update.UpdateProgressScreen
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
import app.revanced.manager.ui.viewmodel.SettingsViewModel
import dev.olshevski.navigation.reimagined.*
import org.koin.androidx.compose.getViewModel
@ -98,7 +101,8 @@ fun SettingsScreen(
is SettingsDestination.Updates -> UpdatesSettingsScreen(
onBackClick = { navController.pop() },
navController = navController
onChangelogClick = { navController.navigate(SettingsDestination.UpdateChangelog) },
onUpdateClick = { navController.navigate(SettingsDestination.UpdateProgress) }
)
is SettingsDestination.Downloads -> DownloadsSettingsScreen(
@ -119,6 +123,10 @@ fun SettingsScreen(
{ navController.pop() },
)
is SettingsDestination.UpdateChangelog -> ManagerUpdateChangelog(
onBackClick = { navController.pop() },
)
is SettingsDestination.Contributors -> ContributorScreen(
onBackClick = { navController.pop() },
)

View File

@ -0,0 +1,115 @@
package app.revanced.manager.ui.screen.settings.update
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Campaign
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.Sell
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.Markdown
import app.revanced.manager.ui.viewmodel.ManagerUpdateChangelogViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ManagerUpdateChangelog(
onBackClick: () -> Unit,
vm: ManagerUpdateChangelogViewModel = getViewModel()
) {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.changelog),
onBackClick = onBackClick
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(start = 16.dp, end = 16.dp, top = 16.dp)
.verticalScroll(rememberScrollState())
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Campaign,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.size(32.dp)
)
Text(
vm.changelog.version.removePrefix("v"),
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.primary,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Sell,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Text(
vm.changelog.version,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.FileDownload,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Text(
vm.formattedDownloadCount,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
)
}
}
Markdown(
vm.changelogHtml,
)
}
}
}

View File

@ -0,0 +1,98 @@
package app.revanced.manager.ui.screen.settings.update
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.viewmodel.UpdateProgressViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Stable
fun UpdateProgressScreen(
onBackClick: () -> Unit,
vm: UpdateProgressViewModel = getViewModel()
) {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.updates),
onBackClick = onBackClick
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(vertical = 16.dp, horizontal = 24.dp)
.verticalScroll(rememberScrollState()),
) {
Text(
text = if (vm.isInstalling) stringResource(R.string.installing_manager_update) else stringResource(
R.string.downloading_manager_update
), style = MaterialTheme.typography.headlineMedium
)
LinearProgressIndicator(
progress = vm.downloadProgress / 100f,
modifier = Modifier
.padding(vertical = 16.dp)
.fillMaxWidth()
)
Text(
text = if (!vm.isInstalling) "${vm.downloadedSize.div(1000000)} MB / ${
vm.totalSize.div(
1000000
)
} MB (${vm.downloadProgress.toInt()}%)" else stringResource(R.string.installing_message),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center
)
Text(
text = "This update adds many functionality and fixes many issues in Manager. New experiment toggles are also added, they can be found in Settings > Advanced. Please submit some feedback in Settings > About > Submit issues or feedback. Thank you, everyone!",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 32.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.weight(1f))
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
TextButton(
onClick = onBackClick,
) {
Text(text = stringResource(R.string.cancel))
}
Button(onClick = vm::installUpdate, enabled = vm.finished) {
Text(text = stringResource(R.string.update))
}
}
}
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.manager.ui.screen.settings
package app.revanced.manager.ui.screen.settings.update
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -15,53 +14,44 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.destination.SettingsDestination
import app.revanced.manager.ui.viewmodel.UpdateSettingsViewModel
import dev.olshevski.navigation.reimagined.NavController
import dev.olshevski.navigation.reimagined.navigate
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UpdatesSettingsScreen(
onBackClick: () -> Unit,
navController: NavController<SettingsDestination>,
onChangelogClick: () -> Unit,
onUpdateClick: () -> Unit,
) {
val listItems = listOf(
Triple(
stringResource(R.string.update_channel),
stringResource(R.string.update_channel_description),
third = { /*TODO*/ }),
third = { /*TODO*/ }
),
Triple(
stringResource(R.string.update_notifications),
stringResource(R.string.update_notifications_description),
third = { /*TODO*/ }),
third = { /*TODO*/ }
),
Triple(
stringResource(R.string.changelog),
stringResource(R.string.changelog_description),
third = { /*TODO*/ }),
third = onChangelogClick
),
)
@ -80,9 +70,7 @@ fun UpdatesSettingsScreen(
.verticalScroll(rememberScrollState())
) {
UpdateNotification(
onClick = {
navController.navigate(SettingsDestination.UpdateProgress)
}
onClick = onUpdateClick
)
listItems.forEach { (title, description, onClick) ->
@ -136,72 +124,4 @@ fun UpdateNotification(
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UpdateProgressScreen(
onBackClick: () -> Unit,
vm: UpdateSettingsViewModel = getViewModel()
) {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.updates),
onBackClick = onBackClick
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(vertical = 16.dp, horizontal = 24.dp),
) {
var isInstalling by remember { mutableStateOf(false) }
isInstalling = vm.downloadProgress >= 100
Text(
text = if (isInstalling) stringResource(R.string.installing_manager_update) else stringResource(
R.string.downloading_manager_update
), style = MaterialTheme.typography.headlineMedium
)
LinearProgressIndicator(
progress = vm.downloadProgress / 100f,
modifier = Modifier
.padding(vertical = 16.dp)
.fillMaxWidth()
)
Text(
text = if (!isInstalling) "${vm.downloadedSize.div(1000000)} MB / ${vm.totalSize.div(1000000)} MB (${vm.downloadProgress.toInt()}%)" else stringResource(R.string.installing_message),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center
)
Text(
text = "This update adds many functionality and fixes many issues in Manager. New experiment toggles are also added, they can be found in Settings > Advanced. Please submit some feedback in Settings > About > Submit issues or feedback. Thank you, everyone!",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 32.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.weight(1f))
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
TextButton(
onClick = { /*TODO*/ },
) {
Text(text = stringResource(R.string.cancel))
}
Button(onClick = {
vm.installUpdate()
}) {
Text(text = stringResource(R.string.update))
}
}
}
}
}

View File

@ -0,0 +1,58 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.domain.repository.GithubRepository
import app.revanced.manager.network.dto.GithubChangelog
import app.revanced.manager.network.utils.getOrThrow
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.launch
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import org.intellij.markdown.html.HtmlGenerator
import org.intellij.markdown.parser.MarkdownParser
class ManagerUpdateChangelogViewModel(
private val githubRepository: GithubRepository,
private val app: Application,
) : ViewModel() {
private val markdownFlavour = GFMFlavourDescriptor()
private val markdownParser = MarkdownParser(flavour = markdownFlavour)
var changelog by mutableStateOf(
GithubChangelog(
"...",
app.getString(R.string.changelog_loading),
emptyList()
)
)
private set
val formattedDownloadCount by derivedStateOf {
val downloadCount = changelog.assets.firstOrNull()?.downloadCount?.toDouble() ?: 0.0
if (downloadCount > 1000) {
val roundedValue =
(downloadCount / 100).toInt() / 10.0 // Divide by 100 and round to one decimal place
"${roundedValue}k"
} else {
downloadCount.toString()
}
}
val changelogHtml by derivedStateOf {
val markdown = changelog.body
val parsedTree = markdownParser.buildMarkdownTreeFromString(markdown)
HtmlGenerator(markdown, parsedTree, markdownFlavour).generateHtml()
}
init {
viewModelScope.launch {
uiSafe(app, R.string.changelog_download_fail, "Failed to download changelog") {
changelog = githubRepository.getChangelog("revanced-manager").getOrThrow()
}
}
}
}

View File

@ -0,0 +1,52 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.network.api.ManagerAPI
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import app.revanced.manager.util.PM
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.withContext
import java.io.File
class UpdateProgressViewModel(
app: Application,
private val managerAPI: ManagerAPI,
private val pm: PM
) : ViewModel() {
val downloadProgress by derivedStateOf { managerAPI.downloadProgress?.times(100) ?: 0f }
val downloadedSize by derivedStateOf { managerAPI.downloadedSize ?: 0L }
val totalSize by derivedStateOf { managerAPI.totalSize ?: 0L }
val isInstalling by derivedStateOf { downloadProgress >= 100 }
var finished by mutableStateOf(false)
private set
private val location = File.createTempFile("updater", ".apk", app.cacheDir)
private val job = viewModelScope.launch {
uiSafe(app, R.string.download_manager_failed, "Failed to download manager") {
withContext(Dispatchers.IO) {
managerAPI.downloadManager(location)
}
finished = true
}
}
fun installUpdate() {
pm.installApp(listOf(location))
}
override fun onCleared() {
super.onCleared()
job.cancel()
location.delete()
}
}

View File

@ -1,38 +0,0 @@
package app.revanced.manager.ui.viewmodel
import android.os.Environment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.util.PM
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
class UpdateSettingsViewModel(
private val managerAPI: ManagerAPI,
private val pm: PM
) : ViewModel() {
val downloadProgress get() = (managerAPI.downloadProgress?.times(100)) ?: 0f
val downloadedSize get() = managerAPI.downloadedSize ?: 0L
val totalSize get() = managerAPI.totalSize ?: 0L
private fun downloadLatestManager() {
viewModelScope.launch(Dispatchers.IO) {
managerAPI.downloadManager()
}
}
fun installUpdate() {
pm.installApp(
apks = listOf(
File(
(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS + "/revanced-manager.apk")
.toString())
),
)
)
}
init {
downloadLatestManager()
}
}

View File

@ -7,6 +7,7 @@ import android.graphics.drawable.Drawable
import android.util.Log
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.ui.graphics.Color
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
import java.util.Locale
typealias PatchesSelection = Map<Int, Set<String>>
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
@ -93,4 +95,13 @@ inline fun <T, reified R, C> Flow<Iterable<T>>.flatMapLatestAndCombine(
combine(iterable.map(transformer)) {
combiner(it)
}
}
}
val Color.hexCode: String
inline get() {
val a: Int = (alpha * 255).toInt()
val r: Int = (red * 255).toInt()
val g: Int = (green * 255).toInt()
val b: Int = (blue * 255).toInt()
return java.lang.String.format(Locale.getDefault(), "%02X%02X%02X%02X", r, g, b, a)
}

View File

@ -133,10 +133,13 @@
<string name="update_notifications">Update notifications</string>
<string name="update_notifications_description">Dialog on app launch + badges</string>
<string name="changelog">Changelog</string>
<string name="changelog_loading">Loading changelog</string>
<string name="changelog_download_fail">Failed to download changelog: %s</string>
<string name="changelog_description">Check out the latest changes in this update</string>
<string name="battery_optimization_notification">Battery optimization must be turned off in order for ReVanced Manager to work correctly in the background. Tap here to turn off.</string>
<string name="installing_manager_update">Installing update…</string>
<string name="downloading_manager_update">Downloading update…</string>
<string name="download_manager_failed">Failed to download update: %s</string>
<string name="cancel">Cancel</string>
<string name="update">Update</string>
<string name="installing_message">Tap on <b>Update</b> when prompted. \n ReVanced Manager will close when updating.</string>