Update viewmodel to reflect gplayapi changes
This commit is contained in:
@@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.compose.composition
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.neverEqualPolicy
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collects a [Flow] as [State] starting from [initial], always triggering recomposition on
|
|
||||||
* every emission regardless of structural equality, by using [neverEqualPolicy].
|
|
||||||
*
|
|
||||||
* Useful for non-state flows (e.g. [kotlinx.coroutines.flow.SharedFlow]) and for flows whose
|
|
||||||
* values have broken `equals` implementations that would otherwise be conflated upstream.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
internal fun <T> Flow<T>.collectForced(initial: T): State<T> {
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
|
||||||
val state: MutableState<T> = remember(this) { mutableStateOf(initial, neverEqualPolicy()) }
|
|
||||||
LaunchedEffect(this, lifecycleOwner) {
|
|
||||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
collect { state.value = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Observes a [LiveData] as [State], always triggering recomposition on every emission
|
|
||||||
* regardless of structural equality, by using [neverEqualPolicy].
|
|
||||||
*/
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
@Composable
|
|
||||||
internal fun <T> LiveData<T>.observeForced(): State<T?> {
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
|
||||||
val state: MutableState<T?> = remember { mutableStateOf(value, neverEqualPolicy()) }
|
|
||||||
DisposableEffect(this, lifecycleOwner) {
|
|
||||||
val observer = Observer<T> { state.value = it }
|
|
||||||
observe(lifecycleOwner, observer)
|
|
||||||
onDispose { removeObserver(observer) }
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
@@ -9,13 +9,13 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.gplayapi.data.models.StreamCluster
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
import com.aurora.gplayapi.helpers.contracts.StreamContract
|
import com.aurora.gplayapi.helpers.contracts.StreamContract
|
||||||
import com.aurora.store.HomeStash
|
import com.aurora.store.HomeStash
|
||||||
import com.aurora.store.compose.composable.StreamCarousel
|
import com.aurora.store.compose.composable.StreamCarousel
|
||||||
import com.aurora.store.compose.composition.observeForced
|
|
||||||
import com.aurora.store.data.model.ViewState
|
import com.aurora.store.data.model.ViewState
|
||||||
import com.aurora.store.viewmodel.homestream.StreamViewModel
|
import com.aurora.store.viewmodel.homestream.StreamViewModel
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ internal fun ForYouContent(
|
|||||||
onScrolledToEnd: () -> Unit
|
onScrolledToEnd: () -> Unit
|
||||||
) {
|
) {
|
||||||
val category = category(pageType)
|
val category = category(pageType)
|
||||||
val state by viewModel.liveData.observeForced()
|
val state by viewModel.liveData.observeAsState()
|
||||||
|
|
||||||
LaunchedEffect(category) {
|
LaunchedEffect(category) {
|
||||||
viewModel.getStreamBundle(category, StreamContract.Type.HOME)
|
viewModel.getStreamBundle(category, StreamContract.Type.HOME)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.gplayapi.data.models.StreamCluster
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
import com.aurora.gplayapi.helpers.contracts.TopChartsContract
|
import com.aurora.gplayapi.helpers.contracts.TopChartsContract
|
||||||
@@ -41,7 +42,6 @@ import com.aurora.store.R
|
|||||||
import com.aurora.store.compose.composable.Placeholder
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
import com.aurora.store.compose.composable.ShimmerAppRow
|
import com.aurora.store.compose.composable.ShimmerAppRow
|
||||||
import com.aurora.store.compose.composable.app.LargeAppListItem
|
import com.aurora.store.compose.composable.app.LargeAppListItem
|
||||||
import com.aurora.store.compose.composition.collectForced
|
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||||
import com.aurora.store.data.model.ViewState
|
import com.aurora.store.data.model.ViewState
|
||||||
@@ -72,7 +72,7 @@ internal fun TopChartsContent(
|
|||||||
if (pageType == 1) TopChartsContract.Type.GAME else TopChartsContract.Type.APPLICATION
|
if (pageType == 1) TopChartsContract.Type.GAME else TopChartsContract.Type.APPLICATION
|
||||||
var selectedIndex by rememberSaveable { mutableIntStateOf(0) }
|
var selectedIndex by rememberSaveable { mutableIntStateOf(0) }
|
||||||
val selectedChart = charts[selectedIndex]
|
val selectedChart = charts[selectedIndex]
|
||||||
val state by viewModel.state.collectForced(ViewState.Loading)
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val cluster = state.getDataAs<StreamCluster?>()
|
val cluster = state.getDataAs<StreamCluster?>()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ private fun TopChartsBodyLoadingPreview() {
|
|||||||
private fun TopChartsBodyLoadedPreview() {
|
private fun TopChartsBodyLoadedPreview() {
|
||||||
val app = AppPreviewProvider().values.first()
|
val app = AppPreviewProvider().values.first()
|
||||||
val apps = List(5) { i -> app.copy(id = i + 1, packageName = "com.preview.app$i") }
|
val apps = List(5) { i -> app.copy(id = i + 1, packageName = "com.preview.app$i") }
|
||||||
val cluster = StreamCluster(clusterAppList = apps)
|
val cluster = StreamCluster(id = 1, clusterAppList = apps)
|
||||||
TopChartsBody(
|
TopChartsBody(
|
||||||
selectedIndex = 0,
|
selectedIndex = 0,
|
||||||
chartTitles = previewChartTitles,
|
chartTitles = previewChartTitles,
|
||||||
@@ -236,7 +236,7 @@ private fun TopChartsBodyLoadedPreview() {
|
|||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
private fun TopChartsBodyEmptyPreview() {
|
private fun TopChartsBodyEmptyPreview() {
|
||||||
val cluster = StreamCluster(clusterAppList = emptyList())
|
val cluster = StreamCluster(id = 1, clusterAppList = emptyList())
|
||||||
TopChartsBody(
|
TopChartsBody(
|
||||||
selectedIndex = 1,
|
selectedIndex = 1,
|
||||||
chartTitles = previewChartTitles,
|
chartTitles = previewChartTitles,
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.aurora.gplayapi.data.models.StreamBundle
|
import com.aurora.gplayapi.data.models.StreamBundle
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.composable.Placeholder
|
import com.aurora.store.compose.composable.Placeholder
|
||||||
import com.aurora.store.compose.composable.StreamCarousel
|
import com.aurora.store.compose.composable.StreamCarousel
|
||||||
import com.aurora.store.compose.composable.TopAppBar
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
import com.aurora.store.compose.composition.collectForced
|
|
||||||
import com.aurora.store.compose.navigation.Destination
|
import com.aurora.store.compose.navigation.Destination
|
||||||
import com.aurora.store.data.model.ViewState
|
import com.aurora.store.data.model.ViewState
|
||||||
import com.aurora.store.viewmodel.subcategory.CategoryStreamViewModel
|
import com.aurora.store.viewmodel.subcategory.CategoryStreamViewModel
|
||||||
@@ -34,7 +34,7 @@ fun CategoryBrowseScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.viewState.collectForced(ViewState.Loading)
|
val uiState by viewModel.viewState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { TopAppBar(title = title) }
|
topBar = { TopAppBar(title = title) }
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ import com.aurora.store.compose.composable.SectionHeader
|
|||||||
import com.aurora.store.compose.composable.ShimmerCarouselSection
|
import com.aurora.store.compose.composable.ShimmerCarouselSection
|
||||||
import com.aurora.store.compose.composable.StreamCarousel
|
import com.aurora.store.compose.composable.StreamCarousel
|
||||||
import com.aurora.store.compose.composable.TopAppBar
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
import com.aurora.store.compose.composition.collectForced
|
|
||||||
import com.aurora.store.compose.navigation.Destination
|
import com.aurora.store.compose.navigation.Destination
|
||||||
import com.aurora.store.compose.navigation.Screen
|
import com.aurora.store.compose.navigation.Screen
|
||||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
@@ -126,7 +125,7 @@ fun AppDetailsScreen(
|
|||||||
val dataSafetyReport by viewModel.dataSafetyReport.collectAsStateWithLifecycle()
|
val dataSafetyReport by viewModel.dataSafetyReport.collectAsStateWithLifecycle()
|
||||||
val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle()
|
val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle()
|
||||||
val installError by viewModel.installError.collectAsStateWithLifecycle()
|
val installError by viewModel.installError.collectAsStateWithLifecycle()
|
||||||
val suggestionsBundle by viewModel.suggestionsBundle.collectForced(initial = null)
|
val suggestionsBundle by viewModel.suggestionsBundle.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(key1 = packageName) { viewModel.fetchAppDetails(packageName) }
|
LaunchedEffect(key1 = packageName) { viewModel.fetchAppDetails(packageName) }
|
||||||
|
|
||||||
@@ -694,6 +693,7 @@ private fun AppDetailsScreenPreview(@PreviewParameter(AppPreviewProvider::class)
|
|||||||
app = app,
|
app = app,
|
||||||
isAnonymous = false,
|
isAnonymous = false,
|
||||||
suggestionsBundle = StreamBundle(
|
suggestionsBundle = StreamBundle(
|
||||||
|
id = 1,
|
||||||
streamClusters = mapOf(
|
streamClusters = mapOf(
|
||||||
1 to StreamCluster(
|
1 to StreamCluster(
|
||||||
id = 1,
|
id = 1,
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val userMessage = exception.userReason()
|
val userMessage = exception.message?.takeIf { it.isNotBlank() }
|
||||||
?: context.getString(R.string.download_failed)
|
?: context.getString(R.string.download_failed)
|
||||||
notifyStatus(
|
notifyStatus(
|
||||||
status = DownloadStatus.FAILED,
|
status = DownloadStatus.FAILED,
|
||||||
@@ -261,22 +261,6 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts a user-readable message from an exception. Falls back to reading the
|
|
||||||
* `reason` property via reflection for GPlayApi's `InternalException` variants,
|
|
||||||
* which expose their detail through a data-class field rather than [Throwable.message].
|
|
||||||
*
|
|
||||||
* TODO: Remove reflection and use a proper exception hierarchy once GPlayApi supports it.
|
|
||||||
*/
|
|
||||||
private fun Throwable.userReason(): String? {
|
|
||||||
message?.takeIf { it.isNotBlank() }?.let { return it }
|
|
||||||
return runCatching {
|
|
||||||
javaClass.getDeclaredField("reason")
|
|
||||||
.apply { isAccessible = true }
|
|
||||||
.get(this) as? String
|
|
||||||
}.getOrNull()?.takeIf { it.isNotBlank() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Purchases the app to get the download URL of the required files
|
* Purchases the app to get the download URL of the required files
|
||||||
* @param packageName The packageName of the app
|
* @param packageName The packageName of the app
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ import com.aurora.store.util.PackageUtil
|
|||||||
import com.aurora.store.util.Preferences
|
import com.aurora.store.util.Preferences
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import java.net.ConnectException
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.net.ConnectException
|
|
||||||
import java.net.UnknownHostException
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class AuthViewModel @Inject constructor(
|
class AuthViewModel @Inject constructor(
|
||||||
@@ -194,8 +194,8 @@ class AuthViewModel @Inject constructor(
|
|||||||
context,
|
context,
|
||||||
Preferences.PREFERENCE_AUTH_VIA_MICROG,
|
Preferences.PREFERENCE_AUTH_VIA_MICROG,
|
||||||
accountType == AccountType.GOOGLE &&
|
accountType == AccountType.GOOGLE &&
|
||||||
tokenType == AuthHelper.Token.AUTH &&
|
tokenType == AuthHelper.Token.AUTH &&
|
||||||
PackageUtil.hasSupportedMicroGVariant(context)
|
PackageUtil.hasSupportedMicroGVariant(context)
|
||||||
)
|
)
|
||||||
_authState.value = AuthState.SignedIn
|
_authState.value = AuthState.SignedIn
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -61,7 +61,10 @@ class ExpandedStreamBrowseViewModel @AssistedInject constructor(
|
|||||||
val browseResponse = streamHelper.getBrowseStreamResponse(browseUrl)
|
val browseResponse = streamHelper.getBrowseStreamResponse(browseUrl)
|
||||||
if (browseResponse.hasBrowseTab()) {
|
if (browseResponse.hasBrowseTab()) {
|
||||||
listUrl = browseResponse.browseTab.listUrl
|
listUrl = browseResponse.browseTab.listUrl
|
||||||
val cluster = streamHelper.getExpandedBrowseClusters(listUrl)
|
val cluster = streamHelper.getExpandedBrowseClusters(
|
||||||
|
id = listUrl.hashCode(),
|
||||||
|
listUrl
|
||||||
|
)
|
||||||
_title.value = cluster.clusterTitle
|
_title.value = cluster.clusterTitle
|
||||||
nextPageUrl = cluster.clusterNextPageUrl
|
nextPageUrl = cluster.clusterNextPageUrl
|
||||||
cluster.clusterAppList
|
cluster.clusterAppList
|
||||||
@@ -72,7 +75,10 @@ class ExpandedStreamBrowseViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (nextPageUrl.isNotBlank()) {
|
if (nextPageUrl.isNotBlank()) {
|
||||||
val cluster = streamHelper.getExpandedBrowseClusters(nextPageUrl)
|
val cluster = streamHelper.getExpandedBrowseClusters(
|
||||||
|
nextPageUrl.hashCode(),
|
||||||
|
nextPageUrl
|
||||||
|
)
|
||||||
nextPageUrl = cluster.clusterNextPageUrl
|
nextPageUrl = cluster.clusterNextPageUrl
|
||||||
cluster.clusterAppList
|
cluster.clusterAppList
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -68,7 +68,10 @@ class StreamBrowseViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (nextPageUrl.isNotBlank()) {
|
if (nextPageUrl.isNotBlank()) {
|
||||||
streamHelper.nextStreamCluster(nextPageUrl).also {
|
streamHelper.nextStreamCluster(
|
||||||
|
nextPageUrl.hashCode(),
|
||||||
|
nextPageUrl
|
||||||
|
).also {
|
||||||
nextPageUrl = it.clusterNextPageUrl
|
nextPageUrl = it.clusterNextPageUrl
|
||||||
}.clusterAppList
|
}.clusterAppList
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import com.aurora.gplayapi.data.models.StreamBundle
|
|||||||
import com.aurora.gplayapi.data.models.StreamCluster
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
|
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
|
||||||
import com.aurora.gplayapi.data.models.details.TestingProgramStatus
|
import com.aurora.gplayapi.data.models.details.TestingProgramStatus
|
||||||
|
import com.aurora.gplayapi.exceptions.GooglePlayException
|
||||||
import com.aurora.gplayapi.helpers.AppDetailsHelper
|
import com.aurora.gplayapi.helpers.AppDetailsHelper
|
||||||
import com.aurora.gplayapi.helpers.ReviewsHelper
|
import com.aurora.gplayapi.helpers.ReviewsHelper
|
||||||
import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper
|
import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper
|
||||||
@@ -46,12 +47,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
@@ -82,13 +80,10 @@ class AppDetailsViewModel @Inject constructor(
|
|||||||
private val _state = MutableStateFlow<AppState>(AppState.Loading)
|
private val _state = MutableStateFlow<AppState>(AppState.Loading)
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
private val _suggestionsBundle = MutableSharedFlow<StreamBundle?>(
|
private val _suggestionsBundle = MutableStateFlow<StreamBundle?>(null)
|
||||||
replay = 1,
|
val suggestionsBundle: StateFlow<StreamBundle?> = _suggestionsBundle.asStateFlow()
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
|
||||||
)
|
|
||||||
val suggestionsBundle: SharedFlow<StreamBundle?> = _suggestionsBundle.asSharedFlow()
|
|
||||||
|
|
||||||
private var suggestionsState: StreamBundle = StreamBundle()
|
private var suggestionsState: StreamBundle = StreamBundle.EMPTY
|
||||||
|
|
||||||
private val _featuredReviews = MutableStateFlow<List<Review>>(emptyList())
|
private val _featuredReviews = MutableStateFlow<List<Review>>(emptyList())
|
||||||
val featuredReviews = _featuredReviews.asStateFlow()
|
val featuredReviews = _featuredReviews.asStateFlow()
|
||||||
@@ -167,31 +162,48 @@ class AppDetailsViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun fetchAppDetails(packageName: String) {
|
fun fetchAppDetails(packageName: String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
var retried = false
|
||||||
authProvider.awaitReady()
|
while (true) {
|
||||||
_app.value = appDetailsHelper.getAppByPackageName(packageName).copy(
|
try {
|
||||||
isInstalled = PackageUtil.isInstalled(context, packageName)
|
authProvider.awaitReady()
|
||||||
)
|
_app.value = appDetailsHelper.getAppByPackageName(packageName).copy(
|
||||||
val existingDownload = downloadHelper.getDownload(packageName)
|
isInstalled = PackageUtil.isInstalled(context, packageName)
|
||||||
|
)
|
||||||
|
val existingDownload = downloadHelper.getDownload(packageName)
|
||||||
|
|
||||||
// A COMPLETED record for an app that is no longer installed means the app was
|
// A COMPLETED record for an app that is no longer installed means the app was
|
||||||
// installed then removed while Aurora held a stale record.
|
// installed then removed while Aurora held a stale record.
|
||||||
// Remove it so the live download observer doesn't lock the UI in Installing state
|
// Remove it so the live download observer doesn't lock the UI in Installing state
|
||||||
// indefinitely.
|
// indefinitely.
|
||||||
if (existingDownload?.status == DownloadStatus.COMPLETED && !isInstalled) {
|
if (existingDownload?.status == DownloadStatus.COMPLETED && !isInstalled) {
|
||||||
downloadHelper.removeDownload(packageName)
|
downloadHelper.removeDownload(packageName)
|
||||||
_state.value = defaultAppState
|
_state.value = defaultAppState
|
||||||
} else {
|
} else {
|
||||||
// Seed state from any in-flight download for this package so reopening
|
// Seed state from any in-flight download for this package so reopening
|
||||||
// the screen doesn't briefly flash the default install action while the
|
// the screen doesn't briefly flash the default install action while the
|
||||||
// download flow catches up.
|
// download flow catches up.
|
||||||
_state.value =
|
_state.value =
|
||||||
existingDownload?.let { stateFromDownload(it) } ?: defaultAppState
|
existingDownload?.let { stateFromDownload(it) } ?: defaultAppState
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
// gplayapi throws AppNotFound(code=401) when the saved token is
|
||||||
|
// rejected mid-session; refresh anonymous auth once and retry.
|
||||||
|
if (!retried &&
|
||||||
|
authProvider.isAnonymous &&
|
||||||
|
exception is GooglePlayException.NotFound &&
|
||||||
|
exception.code == 401
|
||||||
|
) {
|
||||||
|
Log.w(TAG, "App details fetch returned 401, refreshing auth", exception)
|
||||||
|
retried = true
|
||||||
|
authProvider.refreshAnonymousAuth()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Log.e(TAG, "Failed to fetch app details", exception)
|
||||||
|
_app.value = null
|
||||||
|
_state.value = AppState.Error(exception.message)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.e(TAG, "Failed to fetch app details", exception)
|
|
||||||
_app.value = null
|
|
||||||
_state.value = AppState.Error(exception.message)
|
|
||||||
}
|
}
|
||||||
}.invokeOnCompletion { throwable ->
|
}.invokeOnCompletion { throwable ->
|
||||||
// Only proceed if there was no error while fetching the app details
|
// Only proceed if there was no error while fetching the app details
|
||||||
@@ -346,21 +358,21 @@ class AppDetailsViewModel @Inject constructor(
|
|||||||
|
|
||||||
// Bail out if we got no suggestions to offer
|
// Bail out if we got no suggestions to offer
|
||||||
if (streamUrl.isNullOrBlank()) {
|
if (streamUrl.isNullOrBlank()) {
|
||||||
_suggestionsBundle.tryEmit(StreamBundle.EMPTY)
|
_suggestionsBundle.value = StreamBundle.EMPTY
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val pageBundle = appDetailsHelper.getDetailsStream(streamUrl)
|
val pageBundle = appDetailsHelper.getDetailsStream(streamUrl.hashCode(), streamUrl)
|
||||||
val pageClusters = pageBundle.streamClusters.filterValues {
|
val pageClusters = pageBundle.streamClusters.filterValues {
|
||||||
it.clusterTitle.isNotBlank() && it.clusterAppList.isNotEmpty()
|
it.clusterTitle.isNotBlank() && it.clusterAppList.isNotEmpty()
|
||||||
}
|
}
|
||||||
suggestionsState = pageBundle.copy(streamClusters = pageClusters)
|
suggestionsState = pageBundle.copy(streamClusters = pageClusters)
|
||||||
_suggestionsBundle.tryEmit(suggestionsState)
|
_suggestionsBundle.value = suggestionsState
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.e(TAG, "Failed to fetch suggestions stream", exception)
|
Log.e(TAG, "Failed to fetch suggestions stream", exception)
|
||||||
_suggestionsBundle.tryEmit(StreamBundle.EMPTY)
|
_suggestionsBundle.value = StreamBundle.EMPTY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,7 +382,10 @@ class AppDetailsViewModel @Inject constructor(
|
|||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val nextPage = appDetailsHelper.getNextStreamCluster(cluster.clusterNextPageUrl)
|
val nextPage = appDetailsHelper.getNextStreamCluster(
|
||||||
|
cluster.id,
|
||||||
|
cluster.clusterNextPageUrl
|
||||||
|
)
|
||||||
val existing = suggestionsState.streamClusters[cluster.id] ?: return@launch
|
val existing = suggestionsState.streamClusters[cluster.id] ?: return@launch
|
||||||
val mergedCluster = existing.copy(
|
val mergedCluster = existing.copy(
|
||||||
clusterAppList = existing.clusterAppList + nextPage.clusterAppList,
|
clusterAppList = existing.clusterAppList + nextPage.clusterAppList,
|
||||||
@@ -379,7 +394,7 @@ class AppDetailsViewModel @Inject constructor(
|
|||||||
suggestionsState = suggestionsState.copy(
|
suggestionsState = suggestionsState.copy(
|
||||||
streamClusters = suggestionsState.streamClusters + (cluster.id to mergedCluster)
|
streamClusters = suggestionsState.streamClusters + (cluster.id to mergedCluster)
|
||||||
)
|
)
|
||||||
_suggestionsBundle.tryEmit(suggestionsState)
|
_suggestionsBundle.value = suggestionsState
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.e(TAG, "Failed to fetch next cluster page", exception)
|
Log.e(TAG, "Failed to fetch next cluster page", exception)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ class DevProfileViewModel @Inject constructor(
|
|||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val liveData: MutableLiveData<ViewState> = MutableLiveData()
|
val liveData: MutableLiveData<ViewState> = MutableLiveData()
|
||||||
var devStream: DevStream = DevStream()
|
var devStream: DevStream = DevStream.EMPTY
|
||||||
var streamBundle: StreamBundle = StreamBundle()
|
var streamBundle: StreamBundle = StreamBundle.EMPTY
|
||||||
|
|
||||||
lateinit var type: StreamContract.Type
|
lateinit var type: StreamContract.Type
|
||||||
lateinit var category: StreamContract.Category
|
lateinit var category: StreamContract.Category
|
||||||
@@ -74,6 +74,7 @@ class DevProfileViewModel @Inject constructor(
|
|||||||
if (streamCluster.hasNext()) {
|
if (streamCluster.hasNext()) {
|
||||||
authProvider.awaitReady()
|
authProvider.awaitReady()
|
||||||
val newCluster = streamHelper.getNextStreamCluster(
|
val newCluster = streamHelper.getNextStreamCluster(
|
||||||
|
streamCluster.id,
|
||||||
streamCluster.clusterNextPageUrl
|
streamCluster.clusterNextPageUrl
|
||||||
)
|
)
|
||||||
updateCluster(newCluster)
|
updateCluster(newCluster)
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class StreamViewModel @Inject constructor(
|
|||||||
// Fetch new stream bundle
|
// Fetch new stream bundle
|
||||||
val newBundle = if (bundle.hasCluster()) {
|
val newBundle = if (bundle.hasCluster()) {
|
||||||
streamContract.nextStreamBundle(
|
streamContract.nextStreamBundle(
|
||||||
|
bundle.id,
|
||||||
category,
|
category,
|
||||||
bundle.streamNextPageUrl
|
bundle.streamNextPageUrl
|
||||||
)
|
)
|
||||||
@@ -79,9 +80,19 @@ class StreamViewModel @Inject constructor(
|
|||||||
streamContract.fetch(type, category)
|
streamContract.fetch(type, category)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update old bundle
|
// gplayapi 3.6.1 stamps every cluster in a bundle with the bundle id,
|
||||||
|
// so naive Map.plus drops the existing page's clusters. Re-key the new
|
||||||
|
// clusters with synthetic ids past the current max so pagination appends.
|
||||||
|
val baseKey = (bundle.streamClusters.keys.maxOrNull() ?: 0) + 1
|
||||||
|
val rekeyedNewClusters = newBundle.streamClusters.values
|
||||||
|
.mapIndexed { index, cluster ->
|
||||||
|
val newId = baseKey + index
|
||||||
|
newId to cluster.copy(id = newId)
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
|
||||||
val mergedBundle = bundle.copy(
|
val mergedBundle = bundle.copy(
|
||||||
streamClusters = bundle.streamClusters + newBundle.streamClusters,
|
streamClusters = bundle.streamClusters + rekeyedNewClusters,
|
||||||
streamNextPageUrl = newBundle.streamNextPageUrl
|
streamNextPageUrl = newBundle.streamNextPageUrl
|
||||||
)
|
)
|
||||||
stash[category] = mergedBundle
|
stash[category] = mergedBundle
|
||||||
@@ -103,6 +114,7 @@ class StreamViewModel @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
if (streamCluster.hasNext()) {
|
if (streamCluster.hasNext()) {
|
||||||
val newCluster = streamContract.nextStreamCluster(
|
val newCluster = streamContract.nextStreamCluster(
|
||||||
|
streamCluster.id,
|
||||||
streamCluster.clusterNextPageUrl
|
streamCluster.clusterNextPageUrl
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -158,6 +170,6 @@ class StreamViewModel @Inject constructor(
|
|||||||
|
|
||||||
private fun targetBundle(category: StreamContract.Category): StreamBundle =
|
private fun targetBundle(category: StreamContract.Category): StreamBundle =
|
||||||
stash.getOrPut(category) {
|
stash.getOrPut(category) {
|
||||||
StreamBundle()
|
StreamBundle(id = category.value.hashCode())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import com.aurora.extensions.requiresGMS
|
|||||||
import com.aurora.gplayapi.SearchSuggestEntry
|
import com.aurora.gplayapi.SearchSuggestEntry
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.gplayapi.data.models.StreamCluster
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
import com.aurora.gplayapi.helpers.SearchHelper
|
|
||||||
import com.aurora.gplayapi.helpers.contracts.SearchContract
|
import com.aurora.gplayapi.helpers.contracts.SearchContract
|
||||||
import com.aurora.gplayapi.helpers.web.WebSearchHelper
|
import com.aurora.gplayapi.helpers.web.WebSearchHelper
|
||||||
import com.aurora.store.data.PageResult
|
import com.aurora.store.data.PageResult
|
||||||
@@ -41,12 +40,11 @@ import kotlinx.coroutines.launch
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SearchViewModel @Inject constructor(
|
class SearchViewModel @Inject constructor(
|
||||||
val authProvider: AuthProvider,
|
val authProvider: AuthProvider,
|
||||||
private val searchHelper: SearchHelper,
|
|
||||||
private val webSearchHelper: WebSearchHelper
|
private val webSearchHelper: WebSearchHelper
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val contract: SearchContract
|
private val contract: SearchContract
|
||||||
get() = if (authProvider.isAnonymous) webSearchHelper else searchHelper
|
get() = webSearchHelper
|
||||||
|
|
||||||
private val _suggestions = MutableStateFlow<List<SearchSuggestEntry>>(emptyList())
|
private val _suggestions = MutableStateFlow<List<SearchSuggestEntry>>(emptyList())
|
||||||
val suggestions = _suggestions.asStateFlow()
|
val suggestions = _suggestions.asStateFlow()
|
||||||
@@ -125,7 +123,7 @@ class SearchViewModel @Inject constructor(
|
|||||||
authProvider.awaitReady()
|
authProvider.awaitReady()
|
||||||
_suggestions.value = contract.searchSuggestions(query)
|
_suggestions.value = contract.searchSuggestions(query)
|
||||||
.filter { it.title.isNotBlank() }
|
.filter { it.title.isNotBlank() }
|
||||||
.take(3)
|
.take(5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,9 @@ import dagger.assisted.AssistedFactory
|
|||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
|
||||||
@@ -39,19 +38,13 @@ class CategoryStreamViewModel @AssistedInject constructor(
|
|||||||
fun create(browseUrl: String): CategoryStreamViewModel
|
fun create(browseUrl: String): CategoryStreamViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
// SharedFlow (instead of StateFlow) because StreamBundle/StreamCluster override equals
|
private val _viewState = MutableStateFlow<ViewState>(ViewState.Loading)
|
||||||
// to compare only id, which is preserved by copy(). StateFlow would conflate every update
|
val viewState: StateFlow<ViewState> = _viewState.asStateFlow()
|
||||||
// and break paginated scroll loading.
|
|
||||||
private val _viewState = MutableSharedFlow<ViewState>(
|
|
||||||
replay = 1,
|
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
|
||||||
)
|
|
||||||
val viewState: SharedFlow<ViewState> = _viewState.asSharedFlow()
|
|
||||||
|
|
||||||
private val categoryStreamContract: CategoryStreamContract
|
private val categoryStreamContract: CategoryStreamContract
|
||||||
get() = webCategoryStreamHelper
|
get() = webCategoryStreamHelper
|
||||||
|
|
||||||
private var streamBundle = StreamBundle()
|
private var streamBundle = StreamBundle.EMPTY
|
||||||
|
|
||||||
init {
|
init {
|
||||||
fetchNextPage()
|
fetchNextPage()
|
||||||
@@ -60,16 +53,13 @@ class CategoryStreamViewModel @AssistedInject constructor(
|
|||||||
fun fetchNextPage() {
|
fun fetchNextPage() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
supervisorScope {
|
supervisorScope {
|
||||||
if (streamBundle.streamClusters.isNotEmpty()) {
|
|
||||||
_viewState.tryEmit(ViewState.Success(streamBundle))
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!streamBundle.hasCluster() || streamBundle.hasNext()) {
|
if (!streamBundle.hasCluster() || streamBundle.hasNext()) {
|
||||||
val newBundle = if (streamBundle.streamClusters.isEmpty()) {
|
val newBundle = if (streamBundle.streamClusters.isEmpty()) {
|
||||||
categoryStreamContract.fetch(browseUrl)
|
categoryStreamContract.fetch(browseUrl)
|
||||||
} else {
|
} else {
|
||||||
categoryStreamContract.nextStreamBundle(
|
categoryStreamContract.nextStreamBundle(
|
||||||
|
streamBundle.id,
|
||||||
StreamContract.Category.NONE,
|
StreamContract.Category.NONE,
|
||||||
streamBundle.streamNextPageUrl
|
streamBundle.streamNextPageUrl
|
||||||
)
|
)
|
||||||
@@ -80,12 +70,12 @@ class CategoryStreamViewModel @AssistedInject constructor(
|
|||||||
streamNextPageUrl = newBundle.streamNextPageUrl
|
streamNextPageUrl = newBundle.streamNextPageUrl
|
||||||
)
|
)
|
||||||
|
|
||||||
_viewState.tryEmit(ViewState.Success(streamBundle))
|
_viewState.value = ViewState.Success(streamBundle)
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "End of Bundle")
|
Log.i(TAG, "End of Bundle")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_viewState.tryEmit(ViewState.Error(e.message))
|
_viewState.value = ViewState.Error(e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,6 +87,7 @@ class CategoryStreamViewModel @AssistedInject constructor(
|
|||||||
try {
|
try {
|
||||||
if (streamCluster.hasNext()) {
|
if (streamCluster.hasNext()) {
|
||||||
val newCluster = categoryStreamContract.nextStreamCluster(
|
val newCluster = categoryStreamContract.nextStreamCluster(
|
||||||
|
streamCluster.id,
|
||||||
streamCluster.clusterNextPageUrl
|
streamCluster.clusterNextPageUrl
|
||||||
)
|
)
|
||||||
val mergedCluster = streamCluster.copy(
|
val mergedCluster = streamCluster.copy(
|
||||||
@@ -109,7 +100,7 @@ class CategoryStreamViewModel @AssistedInject constructor(
|
|||||||
this[streamCluster.id] = mergedCluster
|
this[streamCluster.id] = mergedCluster
|
||||||
}
|
}
|
||||||
streamBundle = streamBundle.copy(streamClusters = newClusters)
|
streamBundle = streamBundle.copy(streamClusters = newClusters)
|
||||||
_viewState.tryEmit(ViewState.Success(streamBundle))
|
_viewState.value = ViewState.Success(streamBundle)
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "End of cluster")
|
Log.i(TAG, "End of cluster")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,9 @@ import com.aurora.store.data.model.ViewState
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
|
||||||
@@ -43,14 +42,8 @@ class TopChartViewModel @Inject constructor(
|
|||||||
|
|
||||||
private var stash: TopChartStash = mutableMapOf()
|
private var stash: TopChartStash = mutableMapOf()
|
||||||
|
|
||||||
// SharedFlow (instead of StateFlow) because StreamCluster overrides equals to compare
|
private val _state = MutableStateFlow<ViewState>(ViewState.Loading)
|
||||||
// only id, which is preserved by copy(). StateFlow would conflate paginated updates and
|
val state: StateFlow<ViewState> = _state.asStateFlow()
|
||||||
// break scroll loading. See CategoryStreamViewModel for the same fix.
|
|
||||||
private val _state = MutableSharedFlow<ViewState>(
|
|
||||||
replay = 1,
|
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
|
||||||
)
|
|
||||||
val state: SharedFlow<ViewState> = _state.asSharedFlow()
|
|
||||||
|
|
||||||
private val topChartsContract: TopChartsContract
|
private val topChartsContract: TopChartsContract
|
||||||
get() = webTopChartsHelper
|
get() = webTopChartsHelper
|
||||||
@@ -58,18 +51,18 @@ class TopChartViewModel @Inject constructor(
|
|||||||
fun getStreamCluster(type: TopChartsContract.Type, chart: TopChartsContract.Chart) {
|
fun getStreamCluster(type: TopChartsContract.Type, chart: TopChartsContract.Chart) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
if (targetCluster(type, chart).clusterAppList.isNotEmpty()) {
|
if (targetCluster(type, chart).clusterAppList.isNotEmpty()) {
|
||||||
_state.tryEmit(ViewState.Success(targetCluster(type, chart)))
|
_state.value = ViewState.Success(targetCluster(type, chart))
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
_state.tryEmit(ViewState.Loading)
|
_state.value = ViewState.Loading
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val cluster = topChartsContract.getCluster(type.value, chart.value)
|
val cluster = topChartsContract.getCluster(type.value, chart.value)
|
||||||
updateCluster(type, chart, cluster)
|
updateCluster(type, chart, cluster)
|
||||||
_state.tryEmit(ViewState.Success(targetCluster(type, chart)))
|
_state.value = ViewState.Success(targetCluster(type, chart))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_state.tryEmit(ViewState.Error(e.message))
|
_state.value = ViewState.Error(e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,12 +74,13 @@ class TopChartViewModel @Inject constructor(
|
|||||||
val target = targetCluster(type, chart)
|
val target = targetCluster(type, chart)
|
||||||
if (target.hasNext()) {
|
if (target.hasNext()) {
|
||||||
val newCluster = topChartsContract.getNextStreamCluster(
|
val newCluster = topChartsContract.getNextStreamCluster(
|
||||||
|
target.id,
|
||||||
target.clusterNextPageUrl
|
target.clusterNextPageUrl
|
||||||
)
|
)
|
||||||
|
|
||||||
updateCluster(type, chart, newCluster)
|
updateCluster(type, chart, newCluster)
|
||||||
|
|
||||||
_state.tryEmit(ViewState.Success(targetCluster(type, chart)))
|
_state.value = ViewState.Success(targetCluster(type, chart))
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
@@ -114,7 +108,7 @@ class TopChartViewModel @Inject constructor(
|
|||||||
): StreamCluster {
|
): StreamCluster {
|
||||||
val cluster = stash
|
val cluster = stash
|
||||||
.getOrPut(type) { mutableMapOf() }
|
.getOrPut(type) { mutableMapOf() }
|
||||||
.getOrPut(chart) { StreamCluster() }
|
.getOrPut(chart) { StreamCluster.EMPTY }
|
||||||
return cluster
|
return cluster
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,18 +6,18 @@
|
|||||||
|
|
||||||
[versions]
|
[versions]
|
||||||
activity = "1.13.0"
|
activity = "1.13.0"
|
||||||
adaptive = "1.3.0-beta01"
|
adaptive = "1.3.0-beta02"
|
||||||
agCoreservice = "13.3.1.300"
|
agCoreservice = "13.3.1.300"
|
||||||
androidGradlePlugin = "9.2.1"
|
androidGradlePlugin = "9.2.1"
|
||||||
androidx-hilt = "1.3.0"
|
androidx-hilt = "1.3.0"
|
||||||
androidx-junit = "1.3.0"
|
androidx-junit = "1.3.0"
|
||||||
browser = "1.10.0"
|
browser = "1.10.0"
|
||||||
coil = "3.4.0"
|
coil = "3.4.0"
|
||||||
composeBom = "2026.05.00"
|
composeBom = "2026.05.01"
|
||||||
composeMaterial = "1.5.0-alpha19"
|
composeMaterial = "1.5.0-alpha20"
|
||||||
core = "1.18.0"
|
core = "1.18.0"
|
||||||
espresso = "3.7.0"
|
espresso = "3.7.0"
|
||||||
gplayapi = "3.5.9"
|
gplayapi = "3.6.2"
|
||||||
hiddenapibypass = "6.1"
|
hiddenapibypass = "6.1"
|
||||||
hilt = "2.59.2"
|
hilt = "2.59.2"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
@@ -29,12 +29,12 @@ libsu = "6.0.0"
|
|||||||
lifecycle = "2.10.0"
|
lifecycle = "2.10.0"
|
||||||
material = "1.14.0"
|
material = "1.14.0"
|
||||||
navigation = "2.9.8"
|
navigation = "2.9.8"
|
||||||
navigation3 = "1.1.1"
|
navigation3 = "1.1.2"
|
||||||
okhttp = "5.3.2"
|
okhttp = "5.3.2"
|
||||||
paging = "3.5.0"
|
paging = "3.5.0"
|
||||||
preference = "1.2.1"
|
preference = "1.2.1"
|
||||||
processPhoenix = "3.0.0"
|
processPhoenix = "3.0.0"
|
||||||
protobufJavalite = "4.34.1"
|
protobufJavalite = "4.35.0"
|
||||||
rikkaHiddenAPI = "4.4.0"
|
rikkaHiddenAPI = "4.4.0"
|
||||||
rikkaTools = "4.4.0"
|
rikkaTools = "4.4.0"
|
||||||
room = "2.8.4"
|
room = "2.8.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user