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.content.negotiation)
|
||||||
implementation(libs.ktor.serialization)
|
implementation(libs.ktor.serialization)
|
||||||
|
|
||||||
// Markdown to HTML
|
// Markdown
|
||||||
implementation(libs.markdown)
|
implementation(libs.markdown.renderer)
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ val viewModelModule = module {
|
|||||||
viewModelOf(::VersionSelectorViewModel)
|
viewModelOf(::VersionSelectorViewModel)
|
||||||
viewModelOf(::InstallerViewModel)
|
viewModelOf(::InstallerViewModel)
|
||||||
viewModelOf(::UpdateProgressViewModel)
|
viewModelOf(::UpdateProgressViewModel)
|
||||||
viewModelOf(::ManagerUpdateChangelogViewModel)
|
viewModelOf(::ChangelogsViewModel)
|
||||||
viewModelOf(::ImportExportViewModel)
|
viewModelOf(::ImportExportViewModel)
|
||||||
viewModelOf(::ContributorViewModel)
|
viewModelOf(::ContributorViewModel)
|
||||||
viewModelOf(::DownloadsViewModel)
|
viewModelOf(::DownloadsViewModel)
|
||||||
|
@ -104,7 +104,7 @@ class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
|||||||
override suspend fun getLatestInfo() = coroutineScope {
|
override suspend fun getLatestInfo() = coroutineScope {
|
||||||
fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) {
|
fun getAssetAsync(repo: String, mime: String) = async(Dispatchers.IO) {
|
||||||
api
|
api
|
||||||
.getRelease(repo)
|
.getLatestRelease(repo)
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
.let {
|
.let {
|
||||||
BundleAsset(it.metadata.tag, it.findAssetByType(mime).downloadUrl)
|
BundleAsset(it.metadata.tag, it.findAssetByType(mime).downloadUrl)
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
package app.revanced.manager.network.api
|
package app.revanced.manager.network.api
|
||||||
|
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
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.dto.ReVancedRelease
|
||||||
import app.revanced.manager.network.service.ReVancedService
|
import app.revanced.manager.network.service.ReVancedService
|
||||||
import app.revanced.manager.network.utils.getOrThrow
|
|
||||||
import app.revanced.manager.network.utils.transform
|
import app.revanced.manager.network.utils.transform
|
||||||
|
|
||||||
class ReVancedAPI(
|
class ReVancedAPI(
|
||||||
@ -16,7 +13,9 @@ class ReVancedAPI(
|
|||||||
|
|
||||||
suspend fun getContributors() = service.getContributors(apiUrl()).transform { it.repositories }
|
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 {
|
companion object Extensions {
|
||||||
fun ReVancedRelease.findAssetByType(mime: String) = assets.singleOrNull { it.contentType == mime } ?: throw MissingAssetException(mime)
|
fun ReVancedRelease.findAssetByType(mime: String) = assets.singleOrNull { it.contentType == mime } ?: throw MissingAssetException(mime)
|
||||||
|
@ -8,6 +8,11 @@ data class ReVancedLatestRelease(
|
|||||||
val release: ReVancedRelease,
|
val release: ReVancedRelease,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ReVancedReleases(
|
||||||
|
val releases: List<ReVancedRelease>
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ReVancedRelease(
|
data class ReVancedRelease(
|
||||||
val metadata: ReVancedReleaseMeta,
|
val metadata: ReVancedReleaseMeta,
|
||||||
@ -28,6 +33,7 @@ data class ReVancedReleaseMeta(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class Asset(
|
data class Asset(
|
||||||
val name: String,
|
val name: String,
|
||||||
|
@SerialName("download_count") val downloadCount: Int,
|
||||||
@SerialName("browser_download_url") val downloadUrl: String,
|
@SerialName("browser_download_url") val downloadUrl: String,
|
||||||
@SerialName("content_type") val contentType: 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.ReVancedLatestRelease
|
||||||
import app.revanced.manager.network.dto.ReVancedGitRepositories
|
import app.revanced.manager.network.dto.ReVancedGitRepositories
|
||||||
|
import app.revanced.manager.network.dto.ReVancedReleases
|
||||||
import app.revanced.manager.network.utils.APIResponse
|
import app.revanced.manager.network.utils.APIResponse
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -10,13 +11,20 @@ import kotlinx.coroutines.withContext
|
|||||||
class ReVancedService(
|
class ReVancedService(
|
||||||
private val client: HttpService,
|
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) {
|
withContext(Dispatchers.IO) {
|
||||||
client.request {
|
client.request {
|
||||||
url("$api/v2/$repo/releases/latest")
|
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> =
|
suspend fun getContributors(api: String): APIResponse<ReVancedGitRepositories> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
client.request {
|
client.request {
|
||||||
|
@ -1,113 +1,32 @@
|
|||||||
package app.revanced.manager.ui.component
|
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.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.graphics.Color
|
import com.mikepenz.markdown.compose.Markdown
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import com.mikepenz.markdown.model.markdownColor
|
||||||
import app.revanced.manager.util.hexCode
|
import com.mikepenz.markdown.model.markdownTypography
|
||||||
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
|
@Composable
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
fun Markdown(
|
fun Markdown(
|
||||||
text: String,
|
text: String
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
) {
|
||||||
val ctx = LocalContext.current
|
val markdown = text.trimIndent()
|
||||||
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(
|
Markdown(
|
||||||
state,
|
content = markdown,
|
||||||
modifier = Modifier
|
colors = markdownColor(
|
||||||
.background(Color.Transparent)
|
text = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
.then(modifier),
|
codeBackground = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
client = client,
|
codeText = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
captureBackPresses = false,
|
),
|
||||||
onCreated = {
|
typography = markdownTypography(
|
||||||
it.setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
h1 = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold),
|
||||||
it.isVerticalScrollBarEnabled = false
|
h2 = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
|
||||||
it.isHorizontalScrollBarEnabled = false
|
h3 = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||||
it.setOnTouchListener { _, event -> event.action == MotionEvent.ACTION_MOVE }
|
text = MaterialTheme.typography.bodyMedium,
|
||||||
it.layoutParams = ViewGroup.LayoutParams(
|
list = MaterialTheme.typography.bodyMedium
|
||||||
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>"""
|
|
||||||
}
|
|
@ -30,7 +30,7 @@ sealed interface SettingsDestination : Parcelable {
|
|||||||
object UpdateProgress : SettingsDestination
|
object UpdateProgress : SettingsDestination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object UpdateChangelog : SettingsDestination
|
object Changelogs : SettingsDestination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Contributors: SettingsDestination
|
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.component.NotificationCard
|
||||||
import app.revanced.manager.ui.destination.SettingsDestination
|
import app.revanced.manager.ui.destination.SettingsDestination
|
||||||
import app.revanced.manager.ui.screen.settings.*
|
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.UpdateProgressScreen
|
||||||
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
|
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
|
||||||
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
||||||
@ -102,7 +102,7 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
is SettingsDestination.Updates -> UpdatesSettingsScreen(
|
is SettingsDestination.Updates -> UpdatesSettingsScreen(
|
||||||
onBackClick = { navController.pop() },
|
onBackClick = { navController.pop() },
|
||||||
onChangelogClick = { navController.navigate(SettingsDestination.UpdateChangelog) },
|
onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) },
|
||||||
onUpdateClick = { navController.navigate(SettingsDestination.UpdateProgress) }
|
onUpdateClick = { navController.navigate(SettingsDestination.UpdateProgress) }
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ fun SettingsScreen(
|
|||||||
onBackClick = { navController.pop() },
|
onBackClick = { navController.pop() },
|
||||||
)
|
)
|
||||||
|
|
||||||
is SettingsDestination.UpdateChangelog -> ManagerUpdateChangelog(
|
is SettingsDestination.Changelogs -> ChangelogsScreen(
|
||||||
onBackClick = { navController.pop() },
|
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 location = File.createTempFile("updater", ".apk", app.cacheDir)
|
||||||
private val job = viewModelScope.launch {
|
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) {
|
withContext(Dispatchers.IO) {
|
||||||
val asset = reVancedAPI
|
val asset = reVancedAPI
|
||||||
.getRelease("revanced-manager")
|
.getLatestRelease("revanced-manager")
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
.findAssetByType(APK_MIMETYPE)
|
.findAssetByType(APK_MIMETYPE)
|
||||||
|
|
||||||
|
@ -3,6 +3,11 @@ package app.revanced.manager.util
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ApplicationInfo
|
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.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
@ -12,6 +17,7 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import app.revanced.manager.R
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -21,6 +27,11 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.launch
|
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
|
import java.util.Locale
|
||||||
|
|
||||||
typealias PatchesSelection = Map<Int, Set<String>>
|
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="save">Save</string>
|
||||||
<string name="update">Update</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="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>
|
<string name="disable_battery_optimization">Disable battery optimization</string>
|
||||||
</resources>
|
</resources>
|
@ -17,7 +17,7 @@ koin-version = "3.4.3"
|
|||||||
koin-version-compose = "3.4.6"
|
koin-version-compose = "3.4.6"
|
||||||
reimagined-navigation = "1.5.0"
|
reimagined-navigation = "1.5.0"
|
||||||
ktor = "2.3.3"
|
ktor = "2.3.3"
|
||||||
markdown = "0.5.0"
|
markdown-renderer = "0.8.0"
|
||||||
androidGradlePlugin = "8.1.2"
|
androidGradlePlugin = "8.1.2"
|
||||||
kotlinGradlePlugin = "1.9.10"
|
kotlinGradlePlugin = "1.9.10"
|
||||||
devToolsGradlePlugin = "1.9.10-1.0.13"
|
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" }
|
skrapeit-parser = { group = "it.skrape", name = "skrapeit-html-parser", version.ref = "skrapeit" }
|
||||||
|
|
||||||
# Markdown
|
# 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
|
||||||
libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
|
libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user