compose: installed: Migrate installed apps logic to compose
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
@@ -27,6 +27,7 @@ import com.aurora.store.compose.ui.dev.DevProfileScreen
|
|||||||
import com.aurora.store.compose.ui.dispenser.DispenserScreen
|
import com.aurora.store.compose.ui.dispenser.DispenserScreen
|
||||||
import com.aurora.store.compose.ui.downloads.DownloadsScreen
|
import com.aurora.store.compose.ui.downloads.DownloadsScreen
|
||||||
import com.aurora.store.compose.ui.favourite.FavouriteScreen
|
import com.aurora.store.compose.ui.favourite.FavouriteScreen
|
||||||
|
import com.aurora.store.compose.ui.installed.InstalledScreen
|
||||||
import com.aurora.store.compose.ui.onboarding.OnboardingScreen
|
import com.aurora.store.compose.ui.onboarding.OnboardingScreen
|
||||||
import com.aurora.store.compose.ui.preferences.installation.InstallerScreen
|
import com.aurora.store.compose.ui.preferences.installation.InstallerScreen
|
||||||
import com.aurora.store.compose.ui.search.SearchScreen
|
import com.aurora.store.compose.ui.search.SearchScreen
|
||||||
@@ -145,6 +146,15 @@ fun NavDisplay(startDestination: NavKey) {
|
|||||||
entry<Screen.Installer> {
|
entry<Screen.Installer> {
|
||||||
InstallerScreen(onNavigateUp = ::onNavigateUp)
|
InstallerScreen(onNavigateUp = ::onNavigateUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry<Screen.Installed> {
|
||||||
|
InstalledScreen(
|
||||||
|
onNavigateUp = ::onNavigateUp,
|
||||||
|
onNavigateToAppDetails = { packageName ->
|
||||||
|
backstack.add(Screen.AppDetails(packageName))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,4 +60,7 @@ sealed class Screen : NavKey, Parcelable {
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Installer : Screen()
|
data object Installer : Screen()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Installed : Screen()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.installed
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
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 androidx.paging.compose.itemKey
|
||||||
|
import com.aurora.extensions.emptyPagingItems
|
||||||
|
import com.aurora.gplayapi.data.models.App
|
||||||
|
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.all.InstalledViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InstalledScreen(
|
||||||
|
onNavigateUp: () -> Unit,
|
||||||
|
onNavigateToAppDetails: (packageName: String) -> Unit,
|
||||||
|
viewModel: InstalledViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
val apps = viewModel.apps.collectAsLazyPagingItems()
|
||||||
|
|
||||||
|
ScreenContent(
|
||||||
|
apps = apps,
|
||||||
|
onNavigateUp = onNavigateUp,
|
||||||
|
onNavigateToAppDetails = onNavigateToAppDetails
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ScreenContent(
|
||||||
|
onNavigateUp: () -> Unit = {},
|
||||||
|
apps: LazyPagingItems<App> = emptyPagingItems(),
|
||||||
|
onNavigateToAppDetails: (packageName: String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
/*
|
||||||
|
* For some reason paging3 frequently out-of-nowhere invalidates the list which causes
|
||||||
|
* the loading animation to play again even if the keys are same causing a glitching effect.
|
||||||
|
*
|
||||||
|
* Save the initial loading state to make sure we don't replay the loading animation again.
|
||||||
|
*/
|
||||||
|
var initialLoad by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = stringResource(R.string.title_apps_games),
|
||||||
|
onNavigateUp = onNavigateUp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
apps.loadState.refresh is LoadState.Loading && initialLoad -> {
|
||||||
|
ContainedLoadingIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
initialLoad = false
|
||||||
|
|
||||||
|
if (apps.itemCount == 0) {
|
||||||
|
Error(
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
painter = painterResource(R.drawable.ic_apps_outage),
|
||||||
|
message = stringResource(R.string.no_apps_available)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyColumn {
|
||||||
|
items(
|
||||||
|
count = apps.itemCount,
|
||||||
|
key = apps.itemKey { it.packageName }
|
||||||
|
) { index ->
|
||||||
|
apps[index]?.let { app ->
|
||||||
|
LargeAppListItem(
|
||||||
|
app = app,
|
||||||
|
onClick = { onNavigateToAppDetails(app.packageName) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun InstalledScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||||
|
PreviewTemplate {
|
||||||
|
val apps = List(15) { app.copy(packageName = Random.nextInt().toString()) }
|
||||||
|
val pagedApps = MutableStateFlow(PagingData.from(apps)).collectAsLazyPagingItems()
|
||||||
|
ScreenContent(apps = pagedApps)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,66 +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.epoxy.views
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import coil3.load
|
|
||||||
import coil3.request.placeholder
|
|
||||||
import coil3.request.transformations
|
|
||||||
import coil3.transform.RoundedCornersTransformation
|
|
||||||
import com.airbnb.epoxy.CallbackProp
|
|
||||||
import com.airbnb.epoxy.ModelProp
|
|
||||||
import com.airbnb.epoxy.ModelView
|
|
||||||
import com.aurora.store.R
|
|
||||||
import com.aurora.store.data.model.MinimalApp
|
|
||||||
import com.aurora.store.databinding.ViewPackageBinding
|
|
||||||
|
|
||||||
@ModelView(
|
|
||||||
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
|
|
||||||
baseModelClass = BaseModel::class
|
|
||||||
)
|
|
||||||
class InstalledAppView @JvmOverloads constructor(
|
|
||||||
context: Context?,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
) : BaseView<ViewPackageBinding>(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
@ModelProp(options = [ModelProp.Option.IgnoreRequireHashCode])
|
|
||||||
fun packageInfo(app: MinimalApp) {
|
|
||||||
binding.imgIcon.load(app.icon) {
|
|
||||||
placeholder(R.drawable.bg_placeholder)
|
|
||||||
transformations(RoundedCornersTransformation(25F))
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.txtLine1.text = app.displayName
|
|
||||||
binding.txtLine2.text = app.packageName
|
|
||||||
binding.txtLine3.text = ("${app.versionName} (${app.versionCode})")
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallbackProp
|
|
||||||
fun click(onClickListener: OnClickListener?) {
|
|
||||||
binding.root.setOnClickListener(onClickListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallbackProp
|
|
||||||
fun longClick(onClickListener: OnLongClickListener?) {
|
|
||||||
binding.root.setOnLongClickListener(onClickListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +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.all
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import com.aurora.Constants
|
|
||||||
import com.aurora.extensions.toast
|
|
||||||
import com.aurora.gplayapi.data.models.App
|
|
||||||
import com.aurora.store.AuroraApp
|
|
||||||
import com.aurora.store.R
|
|
||||||
import com.aurora.store.data.event.InstallerEvent
|
|
||||||
import com.aurora.store.data.model.MinimalApp
|
|
||||||
import com.aurora.store.databinding.FragmentGenericWithSearchBinding
|
|
||||||
import com.aurora.store.view.epoxy.views.HeaderViewModel_
|
|
||||||
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
|
|
||||||
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
|
|
||||||
import com.aurora.store.view.ui.commons.BaseFragment
|
|
||||||
import com.aurora.store.viewmodel.all.InstalledViewModel
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class AppsGamesFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
|
|
||||||
|
|
||||||
private val viewModel: InstalledViewModel by viewModels()
|
|
||||||
|
|
||||||
private val startForDocumentExport =
|
|
||||||
registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) {
|
|
||||||
if (it != null) exportInstalledApps(it) else toast(R.string.toast_fav_export_failed)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.apps.collect {
|
|
||||||
updateController(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
AuroraApp.events.installerEvent.collect {
|
|
||||||
when (it) {
|
|
||||||
is InstallerEvent.Installed,
|
|
||||||
is InstallerEvent.Uninstalled -> {
|
|
||||||
viewModel.fetchApps()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toolbar
|
|
||||||
binding.toolbar.apply {
|
|
||||||
inflateMenu(R.menu.menu_import_export)
|
|
||||||
|
|
||||||
// TODO: Add support for batch install
|
|
||||||
menu.findItem(R.id.action_import).isEnabled = false
|
|
||||||
|
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
|
||||||
setOnMenuItemClickListener {
|
|
||||||
when (it.itemId) {
|
|
||||||
R.id.action_export -> {
|
|
||||||
startForDocumentExport.launch(
|
|
||||||
"aurora_store_apps_${Calendar.getInstance().time.time}.json"
|
|
||||||
)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.searchBar.addTextChangedListener(object : TextWatcher {
|
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
|
||||||
if (s.isNullOrEmpty()) {
|
|
||||||
updateController(viewModel.apps.value)
|
|
||||||
} else {
|
|
||||||
val filteredPackages = viewModel.apps.value?.filter {
|
|
||||||
it.displayName.contains(s, true) || it.packageName.contains(s, true)
|
|
||||||
}
|
|
||||||
updateController(filteredPackages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun afterTextChanged(s: Editable?) {}
|
|
||||||
override fun beforeTextChanged(
|
|
||||||
s: CharSequence?,
|
|
||||||
start: Int,
|
|
||||||
count: Int,
|
|
||||||
after: Int
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateController(packages: List<App>?) {
|
|
||||||
binding.recycler.withModels {
|
|
||||||
setFilterDuplicates(true)
|
|
||||||
if (packages == null) {
|
|
||||||
for (i in 1..10) {
|
|
||||||
add(
|
|
||||||
AppListViewShimmerModel_()
|
|
||||||
.id(i)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
add(
|
|
||||||
HeaderViewModel_()
|
|
||||||
.id("header")
|
|
||||||
.title(getString(R.string.installed_apps_size, packages.size))
|
|
||||||
)
|
|
||||||
packages.forEach { app ->
|
|
||||||
add(
|
|
||||||
AppListViewModel_()
|
|
||||||
.id(app.packageName.hashCode())
|
|
||||||
.app(app)
|
|
||||||
.click { _ ->
|
|
||||||
openDetailsFragment(
|
|
||||||
app.packageName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.longClick { _ ->
|
|
||||||
openAppMenuSheet(
|
|
||||||
MinimalApp.fromApp(
|
|
||||||
app
|
|
||||||
)
|
|
||||||
)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun exportInstalledApps(uri: Uri) {
|
|
||||||
viewModel.exportApps(requireContext(), uri)
|
|
||||||
toast(R.string.toast_fav_export_success)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -423,10 +423,10 @@ class MoreDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private fun getOptions(): List<Option> {
|
private fun getOptions(): List<Option> {
|
||||||
return listOf(
|
return listOf(
|
||||||
ViewOption(
|
ComposeOption(
|
||||||
title = R.string.title_apps_games,
|
title = R.string.title_apps_games,
|
||||||
icon = R.drawable.ic_apps,
|
icon = R.drawable.ic_apps,
|
||||||
destinationID = R.id.appsGamesFragment
|
screen = Screen.Installed
|
||||||
),
|
),
|
||||||
ComposeOption(
|
ComposeOption(
|
||||||
title = R.string.title_blacklist_manager,
|
title = R.string.title_blacklist_manager,
|
||||||
|
|||||||
@@ -20,36 +20,38 @@
|
|||||||
package com.aurora.store.viewmodel.all
|
package com.aurora.store.viewmodel.all
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
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.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.gplayapi.helpers.web.WebAppDetailsHelper
|
import com.aurora.gplayapi.helpers.web.WebAppDetailsHelper
|
||||||
|
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
|
||||||
import com.aurora.store.data.providers.BlacklistProvider
|
import com.aurora.store.data.providers.BlacklistProvider
|
||||||
import com.aurora.store.data.room.favourite.Favourite
|
|
||||||
import com.aurora.store.data.room.favourite.ImportExport
|
|
||||||
import com.aurora.store.util.PackageUtil
|
import com.aurora.store.util.PackageUtil
|
||||||
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 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.flow.distinctUntilChanged
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class InstalledViewModel @Inject constructor(
|
class InstalledViewModel @Inject constructor(
|
||||||
|
blacklistProvider: BlacklistProvider,
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val blacklistProvider: BlacklistProvider,
|
|
||||||
private val json: Json,
|
|
||||||
private val webAppDetailsHelper: WebAppDetailsHelper
|
private val webAppDetailsHelper: WebAppDetailsHelper
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val TAG = InstalledViewModel::class.java.simpleName
|
private val TAG = InstalledViewModel::class.java.simpleName
|
||||||
|
|
||||||
private val _apps = MutableStateFlow<List<App>?>(null)
|
private val packages = PackageUtil.getAllValidPackages(context)
|
||||||
|
private val blacklist = blacklistProvider.blacklist
|
||||||
|
|
||||||
|
private val _apps = MutableStateFlow<PagingData<App>>(PagingData.empty())
|
||||||
val apps = _apps.asStateFlow()
|
val apps = _apps.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -57,37 +59,23 @@ class InstalledViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun fetchApps() {
|
fun fetchApps() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
val pagedPackages = packages
|
||||||
|
.filterNot { it.packageName in blacklist }
|
||||||
|
.chunked(20)
|
||||||
|
|
||||||
|
manualPager { page ->
|
||||||
try {
|
try {
|
||||||
val packages = PackageUtil.getAllValidPackages(context)
|
webAppDetailsHelper.getAppDetails(
|
||||||
.filterNot { blacklistProvider.isBlacklisted(it.packageName) }
|
pagedPackages[page].map { it.packageName }
|
||||||
|
)
|
||||||
// Divide the list of packages into chunks of 100 & fetch app details
|
|
||||||
// 50 is a safe number to avoid hitting the rate limit or package size limit
|
|
||||||
val chunkedPackages = packages.chunked(50)
|
|
||||||
val allApps = chunkedPackages.flatMap { chunk ->
|
|
||||||
webAppDetailsHelper.getAppDetails(chunk.map { it.packageName })
|
|
||||||
}
|
|
||||||
|
|
||||||
_apps.emit(allApps)
|
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.e(TAG, "Failed to fetch apps", exception)
|
Log.e(TAG, "Failed to fetch apps", exception)
|
||||||
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}.flow.distinctUntilChanged()
|
||||||
|
.cachedIn(viewModelScope)
|
||||||
|
.onEach { _apps.value = it }
|
||||||
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportApps(context: Context, uri: Uri) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val favourites: List<Favourite> = apps.value!!.map { app ->
|
|
||||||
Favourite.fromApp(app, Favourite.Mode.IMPORT)
|
|
||||||
}
|
|
||||||
context.contentResolver.openOutputStream(uri)?.use {
|
|
||||||
it.write(json.encodeToString(ImportExport(favourites)).encodeToByteArray())
|
|
||||||
}
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.e(TAG, "Failed to installed apps", exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ 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/>.
|
|
||||||
~
|
|
||||||
-->
|
|
||||||
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:paddingStart="@dimen/padding_normal"
|
|
||||||
android:paddingTop="@dimen/padding_xsmall"
|
|
||||||
android:paddingEnd="@dimen/padding_small"
|
|
||||||
android:paddingBottom="@dimen/padding_xsmall">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageView
|
|
||||||
android:id="@+id/img_icon"
|
|
||||||
android:layout_width="@dimen/icon_size_medium"
|
|
||||||
android:layout_height="@dimen/icon_size_medium"
|
|
||||||
android:layout_centerVertical="true" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
|
||||||
android:id="@+id/txt_line1"
|
|
||||||
style="@style/AuroraTextStyle.Line1"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/margin_normal"
|
|
||||||
android:layout_toEndOf="@id/img_icon" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
|
||||||
android:id="@+id/txt_line2"
|
|
||||||
style="@style/AuroraTextStyle.Line2"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@+id/txt_line1"
|
|
||||||
android:layout_alignStart="@id/txt_line1"
|
|
||||||
android:layout_alignEnd="@id/txt_line1"
|
|
||||||
android:layout_marginTop="@dimen/margin_xxsmall" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
|
||||||
android:id="@+id/txt_line3"
|
|
||||||
style="@style/AuroraTextStyle.Line3"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@+id/txt_line2"
|
|
||||||
android:layout_alignStart="@id/txt_line1"
|
|
||||||
android:layout_alignEnd="@id/txt_line1"
|
|
||||||
android:layout_marginTop="@dimen/margin_xxsmall" />
|
|
||||||
</RelativeLayout>
|
|
||||||
@@ -56,11 +56,6 @@
|
|||||||
android:name="com.aurora.store.view.ui.updates.UpdatesFragment"
|
android:name="com.aurora.store.view.ui.updates.UpdatesFragment"
|
||||||
android:label="@string/title_updates"
|
android:label="@string/title_updates"
|
||||||
tools:layout="@layout/fragment_updates" />
|
tools:layout="@layout/fragment_updates" />
|
||||||
<fragment
|
|
||||||
android:id="@+id/appsGamesFragment"
|
|
||||||
android:name="com.aurora.store.view.ui.all.AppsGamesFragment"
|
|
||||||
android:label="@string/title_apps_games"
|
|
||||||
tools:layout="@layout/fragment_generic_with_search" />
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/settingsFragment"
|
android:id="@+id/settingsFragment"
|
||||||
android:name="com.aurora.store.view.ui.preferences.SettingsFragment"
|
android:name="com.aurora.store.view.ui.preferences.SettingsFragment"
|
||||||
|
|||||||
Reference in New Issue
Block a user