mirror of
https://github.com/ReVanced/revanced-manager
synced 2026-04-25 17:15:36 +02:00
feat: Improve changelogs (#2968)
This commit is contained in:
@@ -7,7 +7,6 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.EaseInOutQuad
|
||||
import androidx.compose.animation.core.EaseInOutQuart
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
@@ -28,6 +27,7 @@ import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.toRoute
|
||||
import app.revanced.manager.domain.repository.ChangelogSource
|
||||
import app.revanced.manager.ui.model.navigation.Announcement
|
||||
import app.revanced.manager.ui.model.navigation.Announcements
|
||||
import app.revanced.manager.ui.model.navigation.AppSelector
|
||||
@@ -129,10 +129,28 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = startDestination,
|
||||
enterTransition = { slideInHorizontally(animationSpec = tween(300, easing = EaseInOutQuad), initialOffsetX = { it }) },
|
||||
exitTransition = { slideOutHorizontally(animationSpec = tween(300, easing = EaseOut), targetOffsetX = { -it / 3 }) },
|
||||
popEnterTransition = { slideInHorizontally(animationSpec = tween(300, easing = EaseInOutQuad), initialOffsetX = { -it / 3 }) },
|
||||
popExitTransition = { slideOutHorizontally(animationSpec = tween(300, easing = EaseOut), targetOffsetX = { it }) }
|
||||
enterTransition = {
|
||||
slideInHorizontally(
|
||||
animationSpec = tween(300, easing = EaseInOutQuad),
|
||||
initialOffsetX = { it })
|
||||
},
|
||||
exitTransition = {
|
||||
slideOutHorizontally(
|
||||
animationSpec = tween(300, easing = EaseOut),
|
||||
targetOffsetX = { -it / 3 })
|
||||
},
|
||||
popEnterTransition = {
|
||||
slideInHorizontally(
|
||||
animationSpec = tween(
|
||||
300,
|
||||
easing = EaseInOutQuad
|
||||
), initialOffsetX = { -it / 3 })
|
||||
},
|
||||
popExitTransition = {
|
||||
slideOutHorizontally(
|
||||
animationSpec = tween(300, easing = EaseOut),
|
||||
targetOffsetX = { it })
|
||||
}
|
||||
) {
|
||||
composable<Onboarding> {
|
||||
OnboardingScreen(
|
||||
@@ -178,6 +196,11 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
|
||||
BundleInformationScreen(
|
||||
onBackClick = navController::popBackStackSafe,
|
||||
onChangelogClick = { source ->
|
||||
navController.navigateComplex(
|
||||
Settings.Changelogs, source
|
||||
)
|
||||
},
|
||||
viewModel = koinViewModel { parametersOf(data.uid) }
|
||||
)
|
||||
}
|
||||
@@ -349,7 +372,7 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
deepLinkedComposable<Settings.Updates>("settings/updates") {
|
||||
UpdatesSettingsScreen(
|
||||
onBackClick = navController::popBackStackSafe,
|
||||
onChangelogClick = { navController.navigateSafe(Settings.Changelogs) },
|
||||
onChangelogClick = { navController.navigateComplex(Settings.Changelogs, ChangelogSource.Manager) },
|
||||
onUpdateClick = { navController.navigateSafe(Update()) }
|
||||
)
|
||||
}
|
||||
@@ -383,7 +406,8 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
}
|
||||
|
||||
composable<Settings.Changelogs> {
|
||||
ChangelogsSettingsScreen(onBackClick = navController::popBackStackSafe)
|
||||
val source = it.getComplexArg<ChangelogSource>()
|
||||
ChangelogsSettingsScreen(source = source, onBackClick = navController::popBackStackSafe)
|
||||
}
|
||||
|
||||
composable<Settings.Contributors> {
|
||||
|
||||
@@ -27,4 +27,5 @@ val repositoryModule = module {
|
||||
singleOf(::WorkerRepository)
|
||||
singleOf(::DownloadedAppRepository)
|
||||
singleOf(::InstalledAppRepository)
|
||||
singleOf(::ChangelogsRepository)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package app.revanced.manager.domain.repository
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.core.net.toUri
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.dto.ReVancedAssetHistory
|
||||
import app.revanced.manager.network.utils.getOrThrow
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
sealed interface ChangelogSource : Parcelable {
|
||||
data object Manager : ChangelogSource {
|
||||
}
|
||||
data class Patches(val url: String) : ChangelogSource {
|
||||
val baseUrl get() = url.toUri().let { "${it.scheme}://${it.host}" }
|
||||
}
|
||||
}
|
||||
|
||||
class ChangelogsRepository(
|
||||
private val api: ReVancedAPI
|
||||
) {
|
||||
private var all: List<ReVancedAssetHistory> = emptyList()
|
||||
private var page = 0
|
||||
|
||||
suspend fun loadInitial(
|
||||
source: ChangelogSource,
|
||||
pageSize: Int
|
||||
): PageResult<ReVancedAssetHistory> {
|
||||
all = when (source) {
|
||||
is ChangelogSource.Manager ->
|
||||
api.getAppHistory().getOrThrow()
|
||||
|
||||
is ChangelogSource.Patches ->
|
||||
api.getPatchesHistory(source.baseUrl).getOrThrow()
|
||||
}
|
||||
|
||||
page = 1
|
||||
|
||||
val items = all.take(pageSize)
|
||||
return PageResult(
|
||||
items = items,
|
||||
hasMore = hasMore(pageSize)
|
||||
)
|
||||
}
|
||||
|
||||
fun loadNext(pageSize: Int): PageResult<ReVancedAssetHistory> {
|
||||
val items = all
|
||||
.drop(page * pageSize)
|
||||
.take(pageSize)
|
||||
|
||||
page++
|
||||
|
||||
return PageResult(
|
||||
items = items,
|
||||
hasMore = hasMore(pageSize)
|
||||
)
|
||||
}
|
||||
|
||||
private fun hasMore(pageSize: Int) =
|
||||
page * pageSize < all.size
|
||||
}
|
||||
|
||||
data class PageResult<T>(
|
||||
val items: List<T>,
|
||||
val hasMore: Boolean
|
||||
)
|
||||
@@ -6,6 +6,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.manager.base.Preference
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncement
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.network.dto.ReVancedAssetHistory
|
||||
import app.revanced.manager.network.dto.ReVancedGitRepository
|
||||
import app.revanced.manager.network.dto.ReVancedInfo
|
||||
import app.revanced.manager.network.service.HttpService
|
||||
@@ -20,10 +21,11 @@ class ReVancedAPI(
|
||||
private val prefs: PreferencesManager
|
||||
) {
|
||||
private suspend fun apiUrl() = prefs.api.get()
|
||||
private val defaultApiVersion = "v5"
|
||||
|
||||
private suspend inline fun <reified T> request(api: String, route: String): APIResponse<T> =
|
||||
private suspend inline fun <reified T> request(api: String, apiVersion: String, route: String): APIResponse<T> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val fullUrl = "$api/v5/$route"
|
||||
val fullUrl = "$api/$apiVersion/$route"
|
||||
try {
|
||||
Log.d("API", "Requesting: $fullUrl")
|
||||
|
||||
@@ -37,7 +39,7 @@ class ReVancedAPI(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <reified T> request(route: String) = request<T>(apiUrl(), route)
|
||||
private suspend inline fun <reified T> request(route: String, apiVersion: String = defaultApiVersion) = request<T>(apiUrl(), apiVersion, route)
|
||||
|
||||
suspend fun getAnnouncements() = request<List<ReVancedAnnouncement>>("announcements")
|
||||
|
||||
@@ -47,8 +49,13 @@ class ReVancedAPI(
|
||||
suspend fun getLatestAppInfo() =
|
||||
request<ReVancedAsset>("manager${prefs.useManagerPrereleases.prereleaseString()}")
|
||||
|
||||
suspend fun getAppHistory() = request<List<ReVancedAssetHistory>>("manager/history")
|
||||
|
||||
suspend fun getPatchesUpdate() = request<ReVancedAsset>("patches${prefs.usePatchesPrereleases.prereleaseString()}")
|
||||
|
||||
suspend fun getPatchesHistory(apiUrl: String) =
|
||||
request<List<ReVancedAssetHistory>>(apiUrl, defaultApiVersion, "patches/history")
|
||||
|
||||
suspend fun getDownloaderUpdate() = request<ReVancedAsset>("manager/downloaders${prefs.useDownloaderPrerelease.prereleaseString()}")
|
||||
|
||||
suspend fun getContributors() = request<List<ReVancedGitRepository>>("contributors")
|
||||
|
||||
@@ -16,3 +16,10 @@ data class ReVancedAsset (
|
||||
val version: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReVancedAssetHistory(
|
||||
val version: String,
|
||||
@SerialName("created_at")
|
||||
val createdAt: LocalDateTime,
|
||||
val description: String,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Campaign
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
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.network.dto.ReVancedAssetHistory
|
||||
import app.revanced.manager.util.relativeTime
|
||||
|
||||
sealed interface ChangelogUiState {
|
||||
data object Loading : ChangelogUiState
|
||||
data class Error(val error: String) : ChangelogUiState
|
||||
data class Success(
|
||||
val changelogs: List<ReVancedAssetHistory>,
|
||||
val hasMore: Boolean = false,
|
||||
val isLoadingMore: Boolean = false,
|
||||
) : ChangelogUiState
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChangelogList(
|
||||
state: ChangelogUiState,
|
||||
onLoadMore: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val shouldLoadMore by remember {
|
||||
derivedStateOf {
|
||||
val lastVisible = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
||||
val total = listState.layoutInfo.totalItemsCount
|
||||
val canScroll = listState.canScrollForward || listState.canScrollBackward
|
||||
|
||||
(lastVisible >= total - 2 || !canScroll) && total > 0
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(shouldLoadMore, state) {
|
||||
if (shouldLoadMore) onLoadMore()
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier.then(Modifier.fillMaxSize()),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (state) {
|
||||
is ChangelogUiState.Loading -> LoadingIndicator()
|
||||
|
||||
is ChangelogUiState.Error -> Text(
|
||||
text = state.error,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
|
||||
is ChangelogUiState.Success -> {
|
||||
if (state.changelogs.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_changelogs_found),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
} else {
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState
|
||||
) {
|
||||
items(
|
||||
items = state.changelogs,
|
||||
key = { it.version }
|
||||
) { changelog ->
|
||||
ChangelogItem(
|
||||
changelog = changelog,
|
||||
showDivider = changelog != state.changelogs.last()
|
||||
)
|
||||
}
|
||||
|
||||
if (state.isLoadingMore) {
|
||||
item(key = "loading_more") {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChangelogItem(
|
||||
changelog: ReVancedAssetHistory,
|
||||
showDivider: Boolean
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Changelog(
|
||||
description = changelog.description,
|
||||
version = changelog.version,
|
||||
publishDate = changelog.createdAt.relativeTime(LocalContext.current)
|
||||
)
|
||||
if (showDivider) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(top = 32.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Changelog(
|
||||
description: String,
|
||||
version: String,
|
||||
publishDate: String
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Campaign,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
version.removePrefix("v"),
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight(800)
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier)
|
||||
|
||||
Text(
|
||||
"•",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
|
||||
Text(
|
||||
publishDate,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
)
|
||||
}
|
||||
}
|
||||
Markdown(
|
||||
description.removeVersionHeaderIfMatches(version),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.removeVersionHeaderIfMatches(version: String): String {
|
||||
val firstNewlineIndex = indexOf('\n')
|
||||
if (firstNewlineIndex == -1) return this
|
||||
|
||||
val firstLine = substring(0, firstNewlineIndex).trim()
|
||||
val versionWithoutPrefix = version.removePrefix("v")
|
||||
|
||||
if (!firstLine.contains(versionWithoutPrefix)) return this
|
||||
|
||||
return substring(firstNewlineIndex + 1).trimStart()
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package app.revanced.manager.ui.component.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.ui.component.Markdown
|
||||
|
||||
@Composable
|
||||
fun Changelog(
|
||||
markdown: String,
|
||||
version: 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.NewReleases,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
)
|
||||
Text(
|
||||
"${version.removePrefix("v")} ($publishDate)",
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
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,6 +1,7 @@
|
||||
package app.revanced.manager.ui.model.navigation
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.domain.repository.ChangelogSource
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncement
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.Options
|
||||
@@ -101,7 +102,7 @@ object Settings {
|
||||
data object About : Destination
|
||||
|
||||
@Serializable
|
||||
data object Changelogs : Destination
|
||||
data object Changelogs : ComplexParameter<ChangelogSource>
|
||||
|
||||
@Serializable
|
||||
data object Contributors : Destination
|
||||
|
||||
@@ -55,6 +55,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.repository.ChangelogSource
|
||||
import app.revanced.manager.domain.sources.Extensions.asRemoteOrNull
|
||||
import app.revanced.manager.domain.sources.LocalSource
|
||||
import app.revanced.manager.domain.sources.Source
|
||||
@@ -73,6 +74,7 @@ import app.revanced.manager.ui.viewmodel.BundleInformationViewModel
|
||||
@Composable
|
||||
fun BundleInformationScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onChangelogClick: (ChangelogSource.Patches) -> Unit,
|
||||
viewModel: BundleInformationViewModel
|
||||
) {
|
||||
val srcState = viewModel.bundle.collectAsStateWithLifecycle(null)
|
||||
@@ -309,6 +311,15 @@ fun BundleInformationScreen(
|
||||
trailingContent = null
|
||||
)
|
||||
|
||||
endpoint?.let {
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.changelog),
|
||||
onClick = {
|
||||
onChangelogClick(ChangelogSource.Patches(if (src.isDefault) viewModel.prefs.api.getBlocking() else endpoint))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
src.error?.let {
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -32,18 +30,14 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.BottomContentBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbarEdgeShadow
|
||||
import app.revanced.manager.ui.component.settings.Changelog
|
||||
import app.revanced.manager.ui.component.ChangelogList
|
||||
import app.revanced.manager.ui.viewmodel.UpdateViewModel
|
||||
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
|
||||
import app.revanced.manager.util.relativeTime
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@@ -61,12 +55,14 @@ fun UpdateScreen(
|
||||
R.string.download,
|
||||
Icons.Outlined.InstallMobile
|
||||
)
|
||||
|
||||
State.DOWNLOADING -> Triple(onBackClick, R.string.cancel, Icons.Outlined.Cancel)
|
||||
State.CAN_INSTALL -> Triple(
|
||||
{ vm.installUpdate() },
|
||||
R.string.install_app,
|
||||
Icons.Outlined.InstallMobile
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -112,7 +108,7 @@ fun UpdateScreen(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
modifier = Modifier.padding(paddingValues).fillMaxSize(),
|
||||
) {
|
||||
if (vm.state == State.DOWNLOADING)
|
||||
LinearWavyProgressIndicator(
|
||||
@@ -127,9 +123,10 @@ fun UpdateScreen(
|
||||
)
|
||||
}
|
||||
|
||||
vm.releaseInfo?.let { changelog ->
|
||||
Changelog(changelog)
|
||||
}
|
||||
ChangelogList(
|
||||
state = vm.changelogsState,
|
||||
onLoadMore = vm::loadNextPage,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,20 +159,4 @@ private fun MeteredDownloadConfirmationDialog(
|
||||
icon = { Icon(Icons.Outlined.Update, null) },
|
||||
text = { Text(stringResource(R.string.download_confirmation_metered)) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
|
||||
ColumnWithScrollbarEdgeShadow(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Changelog(
|
||||
markdown = releaseInfo.description.replace("`", ""),
|
||||
version = releaseInfo.version,
|
||||
publishDate = releaseInfo.createdAt.relativeTime(LocalContext.current)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,26 @@
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.repository.ChangelogSource
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.component.settings.Changelog
|
||||
import app.revanced.manager.ui.component.ChangelogList
|
||||
import app.revanced.manager.ui.viewmodel.ChangelogsViewModel
|
||||
import app.revanced.manager.util.relativeTime
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChangelogsSettingsScreen(
|
||||
source: ChangelogSource,
|
||||
onBackClick: () -> Unit,
|
||||
vm: ChangelogsViewModel = koinViewModel()
|
||||
vm: ChangelogsViewModel = koinViewModel { parametersOf(source) }
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
@@ -44,22 +34,6 @@ fun ChangelogsSettingsScreen(
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = if (vm.releaseInfo == null) Arrangement.Center else Arrangement.Top
|
||||
) {
|
||||
vm.releaseInfo?.let { info ->
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Changelog(
|
||||
markdown = info.description.replace("`", ""),
|
||||
version = info.version,
|
||||
publishDate = info.createdAt.relativeTime(LocalContext.current)
|
||||
)
|
||||
}
|
||||
} ?: LoadingIndicator()
|
||||
}
|
||||
ChangelogList(modifier = Modifier.padding(paddingValues), state = vm.state, onLoadMore =vm::loadNextPage)
|
||||
}
|
||||
}
|
||||
@@ -7,24 +7,54 @@ 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.dto.ReVancedAsset
|
||||
import app.revanced.manager.network.utils.getOrThrow
|
||||
import app.revanced.manager.domain.repository.ChangelogSource
|
||||
import app.revanced.manager.domain.repository.ChangelogsRepository
|
||||
import app.revanced.manager.ui.component.ChangelogUiState
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ChangelogsViewModel(
|
||||
private val api: ReVancedAPI,
|
||||
private val repository: ChangelogsRepository,
|
||||
private val app: Application,
|
||||
private val source: ChangelogSource,
|
||||
) : ViewModel() {
|
||||
var releaseInfo: ReVancedAsset? by mutableStateOf(null)
|
||||
|
||||
var state: ChangelogUiState by mutableStateOf(ChangelogUiState.Loading)
|
||||
private set
|
||||
|
||||
private val pageSize = 2
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
uiSafe(app, R.string.changelog_download_fail, "Failed to download changelog") {
|
||||
releaseInfo = api.getLatestAppInfo().getOrThrow()
|
||||
val result = repository.loadInitial(source, pageSize)
|
||||
|
||||
state = ChangelogUiState.Success(
|
||||
changelogs = result.items,
|
||||
hasMore = result.hasMore
|
||||
)
|
||||
}
|
||||
|
||||
if (state is ChangelogUiState.Loading) {
|
||||
state = ChangelogUiState.Error(
|
||||
app.getString(R.string.changelog_download_fail)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadNextPage() {
|
||||
val current = state as? ChangelogUiState.Success ?: return
|
||||
if (current.isLoadingMore || !current.hasMore) return
|
||||
|
||||
state = current.copy(isLoadingMore = true)
|
||||
|
||||
val result = repository.loadNext(pageSize)
|
||||
|
||||
state = current.copy(
|
||||
changelogs = current.changelogs + result.items,
|
||||
isLoadingMore = false,
|
||||
hasMore = result.hasMore
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,12 @@ import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.domain.repository.ChangelogSource
|
||||
import app.revanced.manager.domain.repository.ChangelogsRepository
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.network.service.HttpService
|
||||
import app.revanced.manager.ui.component.ChangelogUiState
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import io.ktor.client.plugins.onDownload
|
||||
@@ -34,6 +37,7 @@ import ru.solrudev.ackpine.session.await
|
||||
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||
|
||||
class UpdateViewModel(
|
||||
private val changelogsRepository: ChangelogsRepository,
|
||||
private val downloadOnScreenEntry: Boolean
|
||||
) : ViewModel(), KoinComponent {
|
||||
private val app: Application by inject()
|
||||
@@ -62,12 +66,26 @@ class UpdateViewModel(
|
||||
var releaseInfo: ReVancedAsset? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
var changelogsState: ChangelogUiState by mutableStateOf(ChangelogUiState.Loading)
|
||||
private val changelogsPageSize = 2
|
||||
|
||||
private val location = fs.tempDir.resolve("updater.apk")
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
|
||||
releaseInfo = reVancedAPI.getAppUpdate()
|
||||
?: throw Exception("No update available")
|
||||
|
||||
val result = changelogsRepository.loadInitial(
|
||||
ChangelogSource.Manager,
|
||||
changelogsPageSize
|
||||
)
|
||||
|
||||
changelogsState = ChangelogUiState.Success(
|
||||
changelogs = result.items,
|
||||
hasMore = result.hasMore
|
||||
)
|
||||
|
||||
if (downloadOnScreenEntry) {
|
||||
downloadUpdate()
|
||||
@@ -75,8 +93,28 @@ class UpdateViewModel(
|
||||
state = State.CAN_DOWNLOAD
|
||||
}
|
||||
}
|
||||
|
||||
if (changelogsState is ChangelogUiState.Loading) {
|
||||
changelogsState = ChangelogUiState.Error(
|
||||
app.getString(R.string.changelog_download_fail)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun loadNextPage() {
|
||||
val current = changelogsState as? ChangelogUiState.Success ?: return
|
||||
if (current.isLoadingMore || !current.hasMore) return
|
||||
|
||||
changelogsState = current.copy(isLoadingMore = true)
|
||||
|
||||
val result = changelogsRepository.loadNext(changelogsPageSize)
|
||||
|
||||
changelogsState = current.copy(
|
||||
changelogs = current.changelogs + result.items,
|
||||
isLoadingMore = false,
|
||||
hasMore = result.hasMore
|
||||
)
|
||||
}
|
||||
|
||||
fun downloadUpdate(ignoreInternetCheck: Boolean = false) = viewModelScope.launch {
|
||||
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
|
||||
|
||||
Reference in New Issue
Block a user