mirror of
https://github.com/ReVanced/revanced-manager
synced 2026-04-25 17:15:36 +02:00
pref: Use Paging3 for changelogs instead of custom pagination
This commit is contained in:
@@ -38,6 +38,7 @@ dependencies {
|
||||
implementation(libs.compose.material.icons.extended)
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.navigation.compose)
|
||||
implementation(libs.paging3)
|
||||
|
||||
// Accompanist
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
|
||||
@@ -2,6 +2,8 @@ package app.revanced.manager.domain.repository
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.core.net.toUri
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.dto.ReVancedAssetHistory
|
||||
import app.revanced.manager.network.utils.getOrThrow
|
||||
@@ -18,50 +20,29 @@ sealed interface ChangelogSource : Parcelable {
|
||||
}
|
||||
|
||||
class ChangelogsRepository(
|
||||
private val api: ReVancedAPI
|
||||
) {
|
||||
private var all: List<ReVancedAssetHistory> = emptyList()
|
||||
private var page = 0
|
||||
private val api: ReVancedAPI,
|
||||
private val source: ChangelogSource,
|
||||
) : PagingSource<Int, ReVancedAssetHistory>() {
|
||||
|
||||
suspend fun loadInitial(
|
||||
source: ChangelogSource,
|
||||
pageSize: Int
|
||||
): PageResult<ReVancedAssetHistory> {
|
||||
all = when (source) {
|
||||
is ChangelogSource.Manager ->
|
||||
api.getAppHistory().getOrThrow()
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ReVancedAssetHistory> {
|
||||
return try {
|
||||
val items = when (source) {
|
||||
is ChangelogSource.Manager ->
|
||||
api.getAppHistory().getOrThrow()
|
||||
|
||||
is ChangelogSource.Patches ->
|
||||
api.getPatchesHistory(source.baseUrl, source.prerelease).getOrThrow()
|
||||
is ChangelogSource.Patches ->
|
||||
api.getPatchesHistory(source.baseUrl, source.prerelease).getOrThrow()
|
||||
}
|
||||
|
||||
LoadResult.Page(
|
||||
data = items,
|
||||
prevKey = null,
|
||||
nextKey = null
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LoadResult.Error(e)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
override fun getRefreshKey(state: PagingState<Int, ReVancedAssetHistory>): Int? = null
|
||||
}
|
||||
@@ -31,85 +31,65 @@ 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 androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
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,
|
||||
changelogs: LazyPagingItems<ReVancedAssetHistory>,
|
||||
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()),
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (state) {
|
||||
is ChangelogUiState.Loading -> LoadingIndicator()
|
||||
when {
|
||||
changelogs.loadState.refresh is LoadState.Loading -> LoadingIndicator()
|
||||
|
||||
is ChangelogUiState.Error -> Text(
|
||||
text = state.error,
|
||||
changelogs.loadState.refresh is LoadState.Error -> {
|
||||
val error = changelogs.loadState.refresh as LoadState.Error
|
||||
Text(
|
||||
text = error.error.message ?: stringResource(R.string.changelog_download_fail),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
|
||||
changelogs.itemCount == 0 -> Text(
|
||||
text = stringResource(R.string.no_changelogs_found),
|
||||
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 ->
|
||||
else -> {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState
|
||||
) {
|
||||
items(
|
||||
count = changelogs.itemCount,
|
||||
key = { changelogs.peek(it)?.version ?: it }
|
||||
) { index ->
|
||||
changelogs[index]?.let { changelog ->
|
||||
ChangelogItem(
|
||||
changelog = changelog,
|
||||
showDivider = changelog != state.changelogs.last()
|
||||
showDivider = index < changelogs.itemCount - 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isLoadingMore) {
|
||||
item(key = "loading_more") {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
if (changelogs.loadState.append is LoadState.Loading) {
|
||||
item(key = "loading_more") {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.BottomContentBar
|
||||
@@ -48,6 +49,7 @@ fun UpdateScreen(
|
||||
vm: UpdateViewModel = koinViewModel()
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val changelogs = vm.changelogs.collectAsLazyPagingItems()
|
||||
|
||||
val buttonConfig = when (vm.state) {
|
||||
State.CAN_DOWNLOAD -> Triple(
|
||||
@@ -123,10 +125,7 @@ fun UpdateScreen(
|
||||
)
|
||||
}
|
||||
|
||||
ChangelogList(
|
||||
state = vm.changelogsState,
|
||||
onLoadMore = vm::loadNextPage,
|
||||
)
|
||||
ChangelogList(changelogs = changelogs, modifier = Modifier.padding(paddingValues))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import app.revanced.manager.domain.repository.ChangelogSource
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ChangelogList
|
||||
@@ -23,6 +24,7 @@ fun ChangelogsSettingsScreen(
|
||||
vm: ChangelogsViewModel = koinViewModel { parametersOf(source) }
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val changelogs = vm.changelogs.collectAsLazyPagingItems()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -34,6 +36,6 @@ fun ChangelogsSettingsScreen(
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
ChangelogList(modifier = Modifier.padding(paddingValues), state = vm.state, onLoadMore =vm::loadNextPage)
|
||||
ChangelogList(changelogs = changelogs, modifier = Modifier.padding(paddingValues))
|
||||
}
|
||||
}
|
||||
@@ -1,60 +1,26 @@
|
||||
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 androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
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
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.dto.ReVancedAssetHistory
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class ChangelogsViewModel(
|
||||
private val repository: ChangelogsRepository,
|
||||
private val app: Application,
|
||||
private val api: ReVancedAPI,
|
||||
private val source: ChangelogSource,
|
||||
) : ViewModel() {
|
||||
|
||||
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") {
|
||||
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
|
||||
)
|
||||
}
|
||||
val changelogs: Flow<PagingData<ReVancedAssetHistory>> = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = 10,
|
||||
enablePlaceholders = false
|
||||
),
|
||||
pagingSourceFactory = { ChangelogsRepository(api, source) }
|
||||
).flow.cachedIn(viewModelScope)
|
||||
}
|
||||
@@ -10,6 +10,10 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
@@ -17,13 +21,14 @@ 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.dto.ReVancedAssetHistory
|
||||
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
|
||||
import io.ktor.client.request.url
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
@@ -37,7 +42,8 @@ import ru.solrudev.ackpine.session.await
|
||||
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||
|
||||
class UpdateViewModel(
|
||||
private val changelogsRepository: ChangelogsRepository,
|
||||
private val api: ReVancedAPI,
|
||||
private val source: ChangelogSource,
|
||||
private val downloadOnScreenEntry: Boolean
|
||||
) : ViewModel(), KoinComponent {
|
||||
private val app: Application by inject()
|
||||
@@ -66,8 +72,13 @@ class UpdateViewModel(
|
||||
var releaseInfo: ReVancedAsset? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
var changelogsState: ChangelogUiState by mutableStateOf(ChangelogUiState.Loading)
|
||||
private val changelogsPageSize = 2
|
||||
val changelogs: Flow<PagingData<ReVancedAssetHistory>> = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = 10,
|
||||
enablePlaceholders = false
|
||||
),
|
||||
pagingSourceFactory = { ChangelogsRepository(api, source) }
|
||||
).flow.cachedIn(viewModelScope)
|
||||
|
||||
private val location = fs.tempDir.resolve("updater.apk")
|
||||
|
||||
@@ -77,44 +88,14 @@ class UpdateViewModel(
|
||||
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()
|
||||
} else {
|
||||
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") {
|
||||
|
||||
@@ -40,6 +40,7 @@ binary-compatibility-validator = "0.18.1"
|
||||
semver-parser = "3.0.0"
|
||||
ackpine = "0.20.6"
|
||||
foundation-layout = "1.10.5"
|
||||
paging3 = "3.4.2"
|
||||
|
||||
[libraries]
|
||||
# AndroidX Core
|
||||
@@ -62,6 +63,7 @@ compose-livedata = { group = "androidx.compose.runtime", name = "runtime-livedat
|
||||
compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
|
||||
compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
|
||||
paging3 = { group = "androidx.paging", name = "paging-compose", version.ref = "paging3" }
|
||||
|
||||
# Coil
|
||||
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
||||
|
||||
Reference in New Issue
Block a user