feat(Changelogs): overall improvement (#1429)

This commit is contained in:
Ushie 2023-11-03 21:03:14 +03:00 committed by GitHub
parent 3c5776214f
commit 1a83315424
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 336 additions and 272 deletions

View File

@ -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)
}

View File

@ -15,7 +15,7 @@ val viewModelModule = module {
viewModelOf(::VersionSelectorViewModel)
viewModelOf(::InstallerViewModel)
viewModelOf(::UpdateProgressViewModel)
viewModelOf(::ManagerUpdateChangelogViewModel)
viewModelOf(::ChangelogsViewModel)
viewModelOf(::ImportExportViewModel)
viewModelOf(::ContributorViewModel)
viewModelOf(::DownloadsViewModel)

View File

@ -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)

View File

@ -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)

View File

@ -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
)

View File

@ -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 {

View File

@ -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>"""
}

View File

@ -30,7 +30,7 @@ sealed interface SettingsDestination : Parcelable {
object UpdateProgress : SettingsDestination
@Parcelize
object UpdateChangelog : SettingsDestination
object Changelogs : SettingsDestination
@Parcelize
object Contributors: SettingsDestination

View File

@ -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() },
)

View File

@ -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,
)
}
}

View File

@ -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,
)
}
}
}

View File

@ -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,
)
}

View File

@ -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,
)
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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" }