diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 8cea94679..48db9c40f 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -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 { 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") { 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 { - ChangelogsSettingsScreen(onBackClick = navController::popBackStackSafe) + val source = it.getComplexArg() + ChangelogsSettingsScreen(source = source, onBackClick = navController::popBackStackSafe) } composable { diff --git a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt index 86c999758..952513cf9 100644 --- a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt +++ b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt @@ -27,4 +27,5 @@ val repositoryModule = module { singleOf(::WorkerRepository) singleOf(::DownloadedAppRepository) singleOf(::InstalledAppRepository) + singleOf(::ChangelogsRepository) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/repository/ChangelogsRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/ChangelogsRepository.kt new file mode 100644 index 000000000..aed2c04b7 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/domain/repository/ChangelogsRepository.kt @@ -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 = emptyList() + private var page = 0 + + suspend fun loadInitial( + source: ChangelogSource, + pageSize: Int + ): PageResult { + 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 { + 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( + val items: List, + val hasMore: Boolean +) \ No newline at end of file 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 8fc45f660..d4e61b0ba 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 @@ -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 request(api: String, route: String): APIResponse = + private suspend inline fun request(api: String, apiVersion: String, route: String): APIResponse = 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 request(route: String) = request(apiUrl(), route) + private suspend inline fun request(route: String, apiVersion: String = defaultApiVersion) = request(apiUrl(), apiVersion, route) suspend fun getAnnouncements() = request>("announcements") @@ -47,8 +49,13 @@ class ReVancedAPI( suspend fun getLatestAppInfo() = request("manager${prefs.useManagerPrereleases.prereleaseString()}") + suspend fun getAppHistory() = request>("manager/history") + suspend fun getPatchesUpdate() = request("patches${prefs.usePatchesPrereleases.prereleaseString()}") + suspend fun getPatchesHistory(apiUrl: String) = + request>(apiUrl, defaultApiVersion, "patches/history") + suspend fun getDownloaderUpdate() = request("manager/downloaders${prefs.useDownloaderPrerelease.prereleaseString()}") suspend fun getContributors() = request>("contributors") diff --git a/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt b/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt index 64c05f313..27ea5b4bc 100644 --- a/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt +++ b/app/src/main/java/app/revanced/manager/network/dto/ReVancedAsset.kt @@ -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, +) diff --git a/app/src/main/java/app/revanced/manager/ui/component/ChangelogList.kt b/app/src/main/java/app/revanced/manager/ui/component/ChangelogList.kt new file mode 100644 index 000000000..7f93620e2 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/ChangelogList.kt @@ -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, + 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() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt deleted file mode 100644 index 6a0f8d58f..000000000 --- a/app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt +++ /dev/null @@ -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, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt b/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt index 66fe87312..c19dd677c 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt @@ -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 @Serializable data object Contributors : Destination diff --git a/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt index 31f573a93..53247858d 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt @@ -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) } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt index c03c333be..7bc3a5044 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt @@ -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) - ) - } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsSettingsScreen.kt index e8f4c6249..cee726cdb 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsSettingsScreen.kt @@ -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) } } \ 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 index bfe1bbfa9..9825ee874 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt @@ -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 + ) + } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt index 15b1afaad..fb61e05f9 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt @@ -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") {