compose: commons: Migrate stream browse fragment to compose
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import com.aurora.store.compose.ui.about.AboutScreen
|
|||||||
import com.aurora.store.compose.ui.accounts.AccountsScreen
|
import com.aurora.store.compose.ui.accounts.AccountsScreen
|
||||||
import com.aurora.store.compose.ui.blacklist.BlacklistScreen
|
import com.aurora.store.compose.ui.blacklist.BlacklistScreen
|
||||||
import com.aurora.store.compose.ui.commons.PermissionRationaleScreen
|
import com.aurora.store.compose.ui.commons.PermissionRationaleScreen
|
||||||
|
import com.aurora.store.compose.ui.commons.StreamBrowseScreen
|
||||||
import com.aurora.store.compose.ui.details.AppDetailsScreen
|
import com.aurora.store.compose.ui.details.AppDetailsScreen
|
||||||
import com.aurora.store.compose.ui.dev.DevProfileScreen
|
import com.aurora.store.compose.ui.dev.DevProfileScreen
|
||||||
import com.aurora.store.compose.ui.dispenser.DispenserScreen
|
import com.aurora.store.compose.ui.dispenser.DispenserScreen
|
||||||
@@ -155,6 +156,16 @@ fun NavDisplay(startDestination: NavKey) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry<Screen.StreamBrowse> { screen ->
|
||||||
|
StreamBrowseScreen(
|
||||||
|
streamCluster = screen.streamCluster,
|
||||||
|
onNavigateUp = ::onNavigateUp,
|
||||||
|
onNavigateToAppDetails = { packageName ->
|
||||||
|
backstack.add(Screen.AppDetails(packageName))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package com.aurora.store.compose.navigation
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
import com.aurora.store.data.model.PermissionType
|
import com.aurora.store.data.model.PermissionType
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -63,4 +64,7 @@ sealed class Screen : NavKey, Parcelable {
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Installed : Screen()
|
data object Installed : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class StreamBrowse(val streamCluster: StreamCluster) : Screen()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.commons
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import com.aurora.extensions.emptyPagingItems
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
||||||
|
import com.aurora.store.compose.composable.Error
|
||||||
|
import com.aurora.store.compose.composable.TopAppBar
|
||||||
|
import com.aurora.store.compose.composable.app.LargeAppListItem
|
||||||
|
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||||
|
import com.aurora.store.compose.preview.PreviewTemplate
|
||||||
|
import com.aurora.store.viewmodel.browse.StreamBrowseViewModel
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StreamBrowseScreen(
|
||||||
|
streamCluster: StreamCluster,
|
||||||
|
onNavigateUp: () -> Unit,
|
||||||
|
onNavigateToAppDetails: (packageName: String) -> Unit,
|
||||||
|
viewModel: StreamBrowseViewModel = hiltViewModel(
|
||||||
|
creationCallback = { factory: StreamBrowseViewModel.Factory ->
|
||||||
|
factory.create(streamCluster)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val apps = viewModel.apps.collectAsLazyPagingItems()
|
||||||
|
|
||||||
|
ScreenContent(
|
||||||
|
title = streamCluster.clusterTitle,
|
||||||
|
apps = apps,
|
||||||
|
onNavigateUp = onNavigateUp,
|
||||||
|
onNavigateToAppDetails = onNavigateToAppDetails
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ScreenContent(
|
||||||
|
title: String = String(),
|
||||||
|
apps: LazyPagingItems<App> = emptyPagingItems(),
|
||||||
|
onNavigateToAppDetails: (packageName: String) -> Unit = {},
|
||||||
|
onNavigateUp: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(title = title, onNavigateUp = onNavigateUp)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
when (apps.loadState.refresh) {
|
||||||
|
is LoadState.Loading -> ContainedLoadingIndicator()
|
||||||
|
|
||||||
|
is LoadState.Error -> {
|
||||||
|
Error(
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
painter = painterResource(R.drawable.ic_disclaimer),
|
||||||
|
message = stringResource(R.string.error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
if (apps.itemCount == 0) {
|
||||||
|
Error(
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
painter = painterResource(R.drawable.ic_disclaimer),
|
||||||
|
message = stringResource(R.string.no_apps_available)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
count = apps.itemCount,
|
||||||
|
key = { Uuid.random().toString() }
|
||||||
|
) { index ->
|
||||||
|
apps[index]?.let { app ->
|
||||||
|
LargeAppListItem(
|
||||||
|
app = app,
|
||||||
|
onClick = { onNavigateToAppDetails(app.packageName) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun StreamBrowseScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
|
PreviewTemplate {
|
||||||
|
val apps = List(10) { app.copy(id = Random.nextInt()) }
|
||||||
|
val pagedApps = MutableStateFlow(PagingData.from(apps)).collectAsLazyPagingItems()
|
||||||
|
ScreenContent(
|
||||||
|
apps = pagedApps
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -114,9 +114,7 @@ abstract class BaseFragment<ViewBindingType : ViewBinding> : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun openStreamBrowseFragment(streamCluster: StreamCluster) {
|
fun openStreamBrowseFragment(streamCluster: StreamCluster) {
|
||||||
findNavController().navigate(
|
requireContext().navigate(Screen.StreamBrowse(streamCluster))
|
||||||
MobileNavigationDirections.actionGlobalStreamBrowseFragment(streamCluster)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openAppMenuSheet(app: MinimalApp) {
|
fun openAppMenuSheet(app: MinimalApp) {
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.view.ui.commons
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import androidx.navigation.fragment.navArgs
|
|
||||||
import com.aurora.gplayapi.data.models.StreamCluster
|
|
||||||
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
|
|
||||||
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
|
|
||||||
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
|
|
||||||
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
|
|
||||||
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
|
|
||||||
import com.aurora.store.viewmodel.browse.StreamBrowseViewModel
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class StreamBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>() {
|
|
||||||
private val args: StreamBrowseFragmentArgs by navArgs()
|
|
||||||
private val viewModel: StreamBrowseViewModel by viewModels()
|
|
||||||
|
|
||||||
private lateinit var streamCluster: StreamCluster
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
streamCluster = args.cluster
|
|
||||||
|
|
||||||
// Toolbar
|
|
||||||
binding.toolbar.apply {
|
|
||||||
title = streamCluster.clusterTitle
|
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.recycler.addOnScrollListener(object :
|
|
||||||
EndlessRecyclerOnScrollListener(visibleThreshold = 4) {
|
|
||||||
override fun onLoadMore(currentPage: Int) {
|
|
||||||
viewModel.nextCluster()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
viewModel.seedCluster(streamCluster)
|
|
||||||
viewModel.liveData.observe(viewLifecycleOwner) {
|
|
||||||
updateController(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateController(streamCluster: StreamCluster?) {
|
|
||||||
binding.recycler.withModels {
|
|
||||||
setFilterDuplicates(true)
|
|
||||||
if (streamCluster == null) {
|
|
||||||
for (i in 1..6) {
|
|
||||||
add(
|
|
||||||
AppListViewShimmerModel_()
|
|
||||||
.id(i)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
streamCluster.clusterAppList.forEach {
|
|
||||||
add(
|
|
||||||
AppListViewModel_()
|
|
||||||
.id(it.packageName.hashCode())
|
|
||||||
.app(it)
|
|
||||||
.click { _ -> openDetailsFragment(it.packageName) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (streamCluster.hasNext()) {
|
|
||||||
add(
|
|
||||||
AppProgressViewModel_()
|
|
||||||
.id("progress")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,56 +20,68 @@
|
|||||||
package com.aurora.store.viewmodel.browse
|
package com.aurora.store.viewmodel.browse
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.cachedIn
|
||||||
import com.aurora.extensions.TAG
|
import com.aurora.extensions.TAG
|
||||||
|
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.web.WebStreamHelper
|
import com.aurora.gplayapi.helpers.web.WebStreamHelper
|
||||||
|
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel(assistedFactory = StreamBrowseViewModel.Factory::class)
|
||||||
class StreamBrowseViewModel @Inject constructor(
|
class StreamBrowseViewModel @AssistedInject constructor(
|
||||||
|
@Assisted private val streamCluster: StreamCluster,
|
||||||
private val streamHelper: WebStreamHelper
|
private val streamHelper: WebStreamHelper
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val liveData: MutableLiveData<StreamCluster> = MutableLiveData()
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
private lateinit var streamCluster: StreamCluster
|
fun create(streamCluster: StreamCluster): StreamBrowseViewModel
|
||||||
|
|
||||||
fun seedCluster(cluster: StreamCluster) {
|
|
||||||
streamCluster = cluster
|
|
||||||
liveData.postValue(streamCluster)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun nextCluster() {
|
private val _apps = MutableStateFlow<PagingData<App>>(PagingData.empty())
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
val apps = _apps.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
fetchApps()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchApps() {
|
||||||
|
var nextPageUrl: String = streamCluster.clusterNextPageUrl
|
||||||
|
|
||||||
|
manualPager { page ->
|
||||||
try {
|
try {
|
||||||
if (streamCluster.hasNext()) {
|
when (page) {
|
||||||
val next = streamHelper.nextStreamCluster(streamCluster.clusterNextPageUrl)
|
1 -> streamCluster.clusterAppList
|
||||||
|
|
||||||
streamCluster = streamCluster.copy(
|
else -> {
|
||||||
clusterNextPageUrl = next.clusterNextPageUrl,
|
if (nextPageUrl.isNotBlank()) {
|
||||||
clusterAppList = streamCluster.clusterAppList + next.clusterAppList
|
streamHelper.nextStreamCluster(nextPageUrl).also {
|
||||||
)
|
nextPageUrl = it.clusterNextPageUrl
|
||||||
|
}.clusterAppList
|
||||||
liveData.postValue(streamCluster)
|
} else {
|
||||||
} else {
|
emptyList()
|
||||||
Log.i(TAG, "End of Cluster")
|
}
|
||||||
postClusterEnd()
|
}
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (exception: Exception) {
|
||||||
|
Log.e(TAG, "Failed to fetch apps for $page: $nextPageUrl", exception)
|
||||||
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}.flow.distinctUntilChanged()
|
||||||
}
|
.cachedIn(viewModelScope)
|
||||||
|
.onEach { _apps.value = it }
|
||||||
fun postClusterEnd() {
|
.launchIn(viewModelScope)
|
||||||
streamCluster = streamCluster.copy(
|
|
||||||
clusterNextPageUrl = ""
|
|
||||||
)
|
|
||||||
liveData.postValue(streamCluster)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,6 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_global_devProfileFragment"
|
android:id="@+id/action_global_devProfileFragment"
|
||||||
app:destination="@id/devProfileFragment" />
|
app:destination="@id/devProfileFragment" />
|
||||||
<action
|
|
||||||
android:id="@+id/action_global_streamBrowseFragment"
|
|
||||||
app:destination="@id/streamBrowseFragment" />
|
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_global_appMenuSheet"
|
android:id="@+id/action_global_appMenuSheet"
|
||||||
app:destination="@id/appMenuSheet" />
|
app:destination="@id/appMenuSheet" />
|
||||||
@@ -120,14 +117,6 @@
|
|||||||
app:action="android.intent.action.VIEW"
|
app:action="android.intent.action.VIEW"
|
||||||
app:uri="play.google.com/store/apps/dev?id={devId}" />
|
app:uri="play.google.com/store/apps/dev?id={devId}" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
|
||||||
android:id="@+id/streamBrowseFragment"
|
|
||||||
android:name="com.aurora.store.view.ui.commons.StreamBrowseFragment"
|
|
||||||
tools:layout="@layout/fragment_generic_with_toolbar">
|
|
||||||
<argument
|
|
||||||
android:name="cluster"
|
|
||||||
app:argType="com.aurora.gplayapi.data.models.StreamCluster" />
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/splashFragment"
|
android:id="@+id/splashFragment"
|
||||||
android:name="com.aurora.store.view.ui.splash.SplashFragment"
|
android:name="com.aurora.store.view.ui.splash.SplashFragment"
|
||||||
|
|||||||
Reference in New Issue
Block a user