mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-05 08:14:25 +02:00
feat(Changelogs): overall improvement (#1429)
This commit is contained in:
parent
3c5776214f
commit
1a83315424
@ -156,6 +156,6 @@ dependencies {
|
||||
implementation(libs.ktor.content.negotiation)
|
||||
implementation(libs.ktor.serialization)
|
||||
|
||||
// Markdown to HTML
|
||||
implementation(libs.markdown)
|
||||
// Markdown
|
||||
implementation(libs.markdown.renderer)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ val viewModelModule = module {
|
||||
viewModelOf(::VersionSelectorViewModel)
|
||||
viewModelOf(::InstallerViewModel)
|
||||
viewModelOf(::UpdateProgressViewModel)
|
||||
viewModelOf(::ManagerUpdateChangelogViewModel)
|
||||
viewModelOf(::ChangelogsViewModel)
|
||||
viewModelOf(::ImportExportViewModel)
|
||||
viewModelOf(::ContributorViewModel)
|
||||
viewModelOf(::DownloadsViewModel)
|
||||
|
@ -104,7 +104,7 @@ class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
||||
override suspend fun getLatestInfo() = coroutineScope {
|
||||
fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) {
|
||||
api
|
||||
.getRelease(repo)
|
||||
.getLatestRelease(repo)
|
||||
.getOrThrow()
|
||||
.let {
|
||||
BundleAsset(it.metadata.tag, it.findAssetByType(mime).downloadUrl)
|
||||
|
@ -1,11 +1,8 @@
|
||||
package app.revanced.manager.network.api
|
||||
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.network.dto.Asset
|
||||
import app.revanced.manager.network.dto.ReVancedLatestRelease
|
||||
import app.revanced.manager.network.dto.ReVancedRelease
|
||||
import app.revanced.manager.network.service.ReVancedService
|
||||
import app.revanced.manager.network.utils.getOrThrow
|
||||
import app.revanced.manager.network.utils.transform
|
||||
|
||||
class ReVancedAPI(
|
||||
@ -16,7 +13,9 @@ class ReVancedAPI(
|
||||
|
||||
suspend fun getContributors() = service.getContributors(apiUrl()).transform { it.repositories }
|
||||
|
||||
suspend fun getRelease(name: String) = service.getRelease(apiUrl(), name).transform { it.release }
|
||||
suspend fun getLatestRelease(name: String) = service.getLatestRelease(apiUrl(), name).transform { it.release }
|
||||
|
||||
suspend fun getReleases(name: String) = service.getReleases(apiUrl(), name).transform { it.releases }
|
||||
|
||||
companion object Extensions {
|
||||
fun ReVancedRelease.findAssetByType(mime: String) = assets.singleOrNull { it.contentType == mime } ?: throw MissingAssetException(mime)
|
||||
|
@ -8,6 +8,11 @@ data class ReVancedLatestRelease(
|
||||
val release: ReVancedRelease,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedReleases(
|
||||
val releases: List<ReVancedRelease>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedRelease(
|
||||
val metadata: ReVancedReleaseMeta,
|
||||
@ -28,6 +33,7 @@ data class ReVancedReleaseMeta(
|
||||
@Serializable
|
||||
data class Asset(
|
||||
val name: String,
|
||||
@SerialName("download_count") val downloadCount: Int,
|
||||
@SerialName("browser_download_url") val downloadUrl: String,
|
||||
@SerialName("content_type") val contentType: String
|
||||
)
|
@ -2,6 +2,7 @@ package app.revanced.manager.network.service
|
||||
|
||||
import app.revanced.manager.network.dto.ReVancedLatestRelease
|
||||
import app.revanced.manager.network.dto.ReVancedGitRepositories
|
||||
import app.revanced.manager.network.dto.ReVancedReleases
|
||||
import app.revanced.manager.network.utils.APIResponse
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -10,13 +11,20 @@ import kotlinx.coroutines.withContext
|
||||
class ReVancedService(
|
||||
private val client: HttpService,
|
||||
) {
|
||||
suspend fun getRelease(api: String, repo: String): APIResponse<ReVancedLatestRelease> =
|
||||
suspend fun getLatestRelease(api: String, repo: String): APIResponse<ReVancedLatestRelease> =
|
||||
withContext(Dispatchers.IO) {
|
||||
client.request {
|
||||
url("$api/v2/$repo/releases/latest")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getReleases(api: String, repo: String): APIResponse<ReVancedReleases> =
|
||||
withContext(Dispatchers.IO) {
|
||||
client.request {
|
||||
url("$api/v2/$repo/releases")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getContributors(api: String): APIResponse<ReVancedGitRepositories> =
|
||||
withContext(Dispatchers.IO) {
|
||||
client.request {
|
||||
|
@ -1,113 +1,32 @@
|
||||
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
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.mikepenz.markdown.compose.Markdown
|
||||
import com.mikepenz.markdown.model.markdownColor
|
||||
import com.mikepenz.markdown.model.markdownTypography
|
||||
|
||||
@Composable
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun Markdown(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
text: String
|
||||
) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
val markdown = text.trimIndent()
|
||||
|
||||
WebView(
|
||||
state,
|
||||
modifier = Modifier
|
||||
.background(Color.Transparent)
|
||||
.then(modifier),
|
||||
client = client,
|
||||
captureBackPresses = false,
|
||||
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
|
||||
)
|
||||
}
|
||||
Markdown(
|
||||
content = markdown,
|
||||
colors = markdownColor(
|
||||
text = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
codeBackground = MaterialTheme.colorScheme.secondaryContainer,
|
||||
codeText = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
),
|
||||
typography = markdownTypography(
|
||||
h1 = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold),
|
||||
h2 = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
|
||||
h3 = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||
text = MaterialTheme.typography.bodyMedium,
|
||||
list = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@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>"""
|
||||
}
|
@ -30,7 +30,7 @@ sealed interface SettingsDestination : Parcelable {
|
||||
object UpdateProgress : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
object UpdateChangelog : SettingsDestination
|
||||
object Changelogs : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
object Contributors: SettingsDestination
|
||||
|
@ -30,7 +30,7 @@ import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.NotificationCard
|
||||
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.ChangelogsScreen
|
||||
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
|
||||
@ -102,7 +102,7 @@ fun SettingsScreen(
|
||||
|
||||
is SettingsDestination.Updates -> UpdatesSettingsScreen(
|
||||
onBackClick = { navController.pop() },
|
||||
onChangelogClick = { navController.navigate(SettingsDestination.UpdateChangelog) },
|
||||
onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) },
|
||||
onUpdateClick = { navController.navigate(SettingsDestination.UpdateProgress) }
|
||||
)
|
||||
|
||||
@ -124,7 +124,7 @@ fun SettingsScreen(
|
||||
onBackClick = { navController.pop() },
|
||||
)
|
||||
|
||||
is SettingsDestination.UpdateChangelog -> ManagerUpdateChangelog(
|
||||
is SettingsDestination.Changelogs -> ChangelogsScreen(
|
||||
onBackClick = { navController.pop() },
|
||||
)
|
||||
|
||||
|
@ -0,0 +1,178 @@
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CalendarToday
|
||||
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.Divider
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.component.Markdown
|
||||
import app.revanced.manager.ui.viewmodel.ChangelogsViewModel
|
||||
import app.revanced.manager.util.formatNumber
|
||||
import app.revanced.manager.util.relativeTime
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChangelogsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: ChangelogsViewModel = getViewModel()
|
||||
) {
|
||||
val changelogs = vm.changelogs
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.changelog),
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = if (changelogs.isNullOrEmpty()) Arrangement.Center else Arrangement.Top
|
||||
) {
|
||||
if (changelogs == null) {
|
||||
item {
|
||||
LoadingIndicator()
|
||||
}
|
||||
} else if (changelogs.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.no_changelogs_found),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val lastChangelog = changelogs.last()
|
||||
items(
|
||||
changelogs,
|
||||
key = { it.version }
|
||||
) { changelog ->
|
||||
ChangelogItem(changelog, lastChangelog)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChangelogItem(
|
||||
changelog: ChangelogsViewModel.Changelog,
|
||||
lastChangelog: ChangelogsViewModel.Changelog
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Changelog(
|
||||
markdown = changelog.body.replace("`", ""),
|
||||
version = changelog.version,
|
||||
downloadCount = changelog.downloadCount.formatNumber(),
|
||||
publishDate = changelog.publishDate.relativeTime(LocalContext.current)
|
||||
)
|
||||
if (changelog != lastChangelog) {
|
||||
Divider(
|
||||
modifier = Modifier.padding(top = 32.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Changelog(
|
||||
markdown: String,
|
||||
version: String,
|
||||
downloadCount: String,
|
||||
publishDate: String
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 0.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(
|
||||
version.removePrefix("v"),
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Tag(
|
||||
Icons.Outlined.Sell,
|
||||
version
|
||||
)
|
||||
Tag(
|
||||
Icons.Outlined.FileDownload,
|
||||
downloadCount
|
||||
)
|
||||
Tag(
|
||||
Icons.Outlined.CalendarToday,
|
||||
publishDate
|
||||
)
|
||||
}
|
||||
}
|
||||
Markdown(
|
||||
markdown,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Tag(icon: ImageVector, text: String) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Markdown(
|
||||
vm.changelogHtml,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
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.ReVancedAPI
|
||||
import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType
|
||||
import app.revanced.manager.network.utils.getOrNull
|
||||
import app.revanced.manager.util.APK_MIMETYPE
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ChangelogsViewModel(
|
||||
private val api: ReVancedAPI,
|
||||
private val app: Application,
|
||||
) : ViewModel() {
|
||||
var changelogs: List<Changelog>? by mutableStateOf(null)
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
uiSafe(app, R.string.changelog_download_fail, "Failed to download changelog") {
|
||||
changelogs = api.getReleases("revanced-manager").getOrNull().orEmpty().map { release ->
|
||||
Changelog(
|
||||
release.metadata.tag,
|
||||
release.findAssetByType(APK_MIMETYPE).downloadCount,
|
||||
release.metadata.publishedAt,
|
||||
release.metadata.body
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Changelog(
|
||||
val version: String,
|
||||
val downloadCount: Int,
|
||||
val publishDate: String,
|
||||
val body: String,
|
||||
)
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
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.ReVancedAPI
|
||||
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 api: ReVancedAPI,
|
||||
private val app: Application,
|
||||
) : ViewModel() {
|
||||
private val markdownFlavour = GFMFlavourDescriptor()
|
||||
private val markdownParser = MarkdownParser(flavour = markdownFlavour)
|
||||
|
||||
var changelog by mutableStateOf(
|
||||
Changelog(
|
||||
"...",
|
||||
app.getString(R.string.changelog_loading),
|
||||
)
|
||||
)
|
||||
private set
|
||||
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 = api.getRelease("revanced-manager").getOrThrow().let {
|
||||
Changelog(it.metadata.tag, it.metadata.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Changelog(
|
||||
val version: String,
|
||||
val body: String,
|
||||
)
|
||||
}
|
@ -43,10 +43,10 @@ class UpdateProgressViewModel(
|
||||
|
||||
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") {
|
||||
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||
withContext(Dispatchers.IO) {
|
||||
val asset = reVancedAPI
|
||||
.getRelease("revanced-manager")
|
||||
.getLatestRelease("revanced-manager")
|
||||
.getOrThrow()
|
||||
.findAssetByType(APK_MIMETYPE)
|
||||
|
||||
|
@ -3,6 +3,11 @@ package app.revanced.manager.util
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.icu.number.Notation
|
||||
import android.icu.number.NumberFormatter
|
||||
import android.icu.number.Precision
|
||||
import android.icu.text.CompactDecimalFormat
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
@ -12,6 +17,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import app.revanced.manager.R
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -21,6 +27,11 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Duration
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.util.Locale
|
||||
|
||||
typealias PatchesSelection = Map<Int, Set<String>>
|
||||
@ -109,3 +120,49 @@ suspend fun <T> Flow<Iterable<T>>.collectEach(block: suspend (T) -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Int.formatNumber(): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
NumberFormatter.with()
|
||||
.notation(Notation.compactShort())
|
||||
.decimal(NumberFormatter.DecimalSeparatorDisplay.ALWAYS)
|
||||
.precision(Precision.fixedFraction(1))
|
||||
.locale(Locale.getDefault())
|
||||
.format(this)
|
||||
.toString()
|
||||
} else {
|
||||
val compact = CompactDecimalFormat.getInstance(
|
||||
Locale.getDefault(), CompactDecimalFormat.CompactStyle.SHORT
|
||||
)
|
||||
compact.maximumFractionDigits = 1
|
||||
compact.format(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.relativeTime(context: Context): String {
|
||||
try {
|
||||
val currentTime = ZonedDateTime.now(ZoneId.of("UTC"))
|
||||
val inputDateTime = ZonedDateTime.parse(this)
|
||||
val duration = Duration.between(inputDateTime, currentTime)
|
||||
|
||||
return when {
|
||||
duration.toMinutes() < 1 -> context.getString(R.string.just_now)
|
||||
duration.toMinutes() < 60 -> context.getString(R.string.minutes_ago, duration.toMinutes().toString())
|
||||
duration.toHours() < 24 -> context.getString(R.string.hours_ago, duration.toHours().toString())
|
||||
duration.toDays() < 30 -> context.getString(R.string.days_ago, duration.toDays().toString())
|
||||
else -> {
|
||||
val formatter = DateTimeFormatter.ofPattern("MMM d")
|
||||
val formattedDate = inputDateTime.format(formatter)
|
||||
if (inputDateTime.year != currentTime.year) {
|
||||
val yearFormatter = DateTimeFormatter.ofPattern(", yyyy")
|
||||
val formattedYear = inputDateTime.format(yearFormatter)
|
||||
"$formattedDate$formattedYear"
|
||||
} else {
|
||||
formattedDate
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: DateTimeParseException) {
|
||||
return context.getString(R.string.invalid_date)
|
||||
}
|
||||
}
|
@ -290,5 +290,11 @@
|
||||
<string name="save">Save</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>
|
||||
<string name="no_changelogs_found">No changelogs found</string>
|
||||
<string name="just_now">Just now</string>
|
||||
<string name="minutes_ago">%sm ago</string>
|
||||
<string name="hours_ago">%sh ago</string>
|
||||
<string name="days_ago">%sd ago</string>
|
||||
<string name="invalid_date">Invalid date</string>
|
||||
<string name="disable_battery_optimization">Disable battery optimization</string>
|
||||
</resources>
|
@ -17,7 +17,7 @@ koin-version = "3.4.3"
|
||||
koin-version-compose = "3.4.6"
|
||||
reimagined-navigation = "1.5.0"
|
||||
ktor = "2.3.3"
|
||||
markdown = "0.5.0"
|
||||
markdown-renderer = "0.8.0"
|
||||
androidGradlePlugin = "8.1.2"
|
||||
kotlinGradlePlugin = "1.9.10"
|
||||
devToolsGradlePlugin = "1.9.10-1.0.13"
|
||||
@ -93,7 +93,7 @@ skrapeit-dsl = { group = "it.skrape", name = "skrapeit-dsl", version.ref = "skra
|
||||
skrapeit-parser = { group = "it.skrape", name = "skrapeit-html-parser", version.ref = "skrapeit" }
|
||||
|
||||
# Markdown
|
||||
markdown = { group = "org.jetbrains", name = "markdown", version.ref = "markdown" }
|
||||
markdown-renderer = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-android", version.ref = "markdown-renderer" }
|
||||
|
||||
# LibSU
|
||||
libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
|
||||
|
Loading…
x
Reference in New Issue
Block a user