diff --git a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt index ab5a563f3..a40419114 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt @@ -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.downloads.DownloadsScreen 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.preferences.installation.InstallerScreen import com.aurora.store.compose.ui.search.SearchScreen @@ -145,6 +146,15 @@ fun NavDisplay(startDestination: NavKey) { entry { InstallerScreen(onNavigateUp = ::onNavigateUp) } + + entry { + InstalledScreen( + onNavigateUp = ::onNavigateUp, + onNavigateToAppDetails = { packageName -> + backstack.add(Screen.AppDetails(packageName)) + } + ) + } } ) } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt index 116d1a267..3489056bb 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt @@ -60,4 +60,7 @@ sealed class Screen : NavKey, Parcelable { @Serializable data object Installer : Screen() + + @Serializable + data object Installed : Screen() } diff --git a/app/src/main/java/com/aurora/store/compose/ui/installed/InstalledScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/installed/InstalledScreen.kt new file mode 100644 index 000000000..98ccdae7c --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/installed/InstalledScreen.kt @@ -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 = 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) + } +} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/InstalledAppView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/InstalledAppView.kt deleted file mode 100644 index efaa492c4..000000000 --- a/app/src/main/java/com/aurora/store/view/epoxy/views/InstalledAppView.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * 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 . - * - */ - -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(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) - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt b/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt deleted file mode 100644 index ff2229cac..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * 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 . - * - */ - -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() { - - 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?) { - 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) - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt index 3112c0456..4668933d3 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt @@ -423,10 +423,10 @@ class MoreDialogFragment : DialogFragment() { private fun getOptions(): List