diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0c7aa133..a0404b32 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) } diff --git a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt index 1729f546..baf66330 100644 --- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -15,7 +15,7 @@ val viewModelModule = module { viewModelOf(::VersionSelectorViewModel) viewModelOf(::InstallerViewModel) viewModelOf(::UpdateProgressViewModel) - viewModelOf(::ManagerUpdateChangelogViewModel) + viewModelOf(::ChangelogsViewModel) viewModelOf(::ImportExportViewModel) viewModelOf(::ContributorViewModel) viewModelOf(::DownloadsViewModel) diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt index 240347af..295cc2bd 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/RemotePatchBundle.kt @@ -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) diff --git a/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt b/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt index 3367bcf2..1bc5fdd6 100644 --- a/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt +++ b/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt @@ -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) diff --git a/app/src/main/java/app/revanced/manager/network/dto/ReVancedRelease.kt b/app/src/main/java/app/revanced/manager/network/dto/ReVancedRelease.kt index 416e0629..d7fe2bbf 100644 --- a/app/src/main/java/app/revanced/manager/network/dto/ReVancedRelease.kt +++ b/app/src/main/java/app/revanced/manager/network/dto/ReVancedRelease.kt @@ -8,6 +8,11 @@ data class ReVancedLatestRelease( val release: ReVancedRelease, ) +@Serializable +data class ReVancedReleases( + val releases: List +) + @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 ) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/service/ReVancedService.kt b/app/src/main/java/app/revanced/manager/network/service/ReVancedService.kt index b5681afe..54516b4d 100644 --- a/app/src/main/java/app/revanced/manager/network/service/ReVancedService.kt +++ b/app/src/main/java/app/revanced/manager/network/service/ReVancedService.kt @@ -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 = + suspend fun getLatestRelease(api: String, repo: String): APIResponse = withContext(Dispatchers.IO) { client.request { url("$api/v2/$repo/releases/latest") } } + suspend fun getReleases(api: String, repo: String): APIResponse = + withContext(Dispatchers.IO) { + client.request { + url("$api/v2/$repo/releases") + } + } + suspend fun getContributors(api: String): APIResponse = withContext(Dispatchers.IO) { client.request { diff --git a/app/src/main/java/app/revanced/manager/ui/component/Markdown.kt b/app/src/main/java/app/revanced/manager/ui/component/Markdown.kt index 6773b15a..1b79d8f8 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/Markdown.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/Markdown.kt @@ -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) { - """ - - - Markdown - - - - - $source - - """ } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/destination/SettingsDestination.kt b/app/src/main/java/app/revanced/manager/ui/destination/SettingsDestination.kt index ffdf20bc..ac3374c8 100644 --- a/app/src/main/java/app/revanced/manager/ui/destination/SettingsDestination.kt +++ b/app/src/main/java/app/revanced/manager/ui/destination/SettingsDestination.kt @@ -30,7 +30,7 @@ sealed interface SettingsDestination : Parcelable { object UpdateProgress : SettingsDestination @Parcelize - object UpdateChangelog : SettingsDestination + object Changelogs : SettingsDestination @Parcelize object Contributors: SettingsDestination diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt index 0c2dd3d3..eb3464df 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt @@ -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() }, ) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt new file mode 100644 index 00000000..16a9f802 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ManagerUpdateChangelog.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ManagerUpdateChangelog.kt deleted file mode 100644 index 5ba085e6..00000000 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ManagerUpdateChangelog.kt +++ /dev/null @@ -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, - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt new file mode 100644 index 00000000..61466ed2 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt @@ -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? 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, + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ManagerUpdateChangelogViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ManagerUpdateChangelogViewModel.kt deleted file mode 100644 index 02d187ea..00000000 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/ManagerUpdateChangelogViewModel.kt +++ /dev/null @@ -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, - ) -} diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateProgressViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateProgressViewModel.kt index 2ee6ed33..a7d7969d 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateProgressViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateProgressViewModel.kt @@ -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) diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt index 908aa0f6..760d396a 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -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> @@ -108,4 +119,50 @@ suspend fun Flow>.collectEach(block: suspend (T) -> Unit) { block(it) } } +} + +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) + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63f53250..ea55f13c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -290,5 +290,11 @@ Save Update Tap on Update when prompted. \n ReVanced Manager will close when updating. + No changelogs found + Just now + %sm ago + %sh ago + %sd ago + Invalid date Disable battery optimization \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5dcc6c08..dc60f15a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }