diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 32f146c1d..211f40cd9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,6 +26,7 @@ plugins { alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.jetbrains.kotlin.compose) alias(libs.plugins.jetbrains.kotlin.parcelize) + alias(libs.plugins.jetbrains.kotlin.serialization) alias(libs.plugins.google.ksp) alias(libs.plugins.androidx.navigation) alias(libs.plugins.ktlint) @@ -147,15 +148,19 @@ dependencies { implementation(libs.androidx.browser) implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.process) - implementation(libs.androidx.navigation.fragment.ktx) - implementation(libs.androidx.navigation.ui.ktx) implementation(libs.androidx.preference.ktx) implementation(libs.androidx.swiperefreshlayout) implementation(libs.androidx.viewpager2) implementation(libs.androidx.work.runtime.ktx) + implementation(libs.androidx.navigation.compose) + implementation(libs.kotlinx.serialization.json) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.material3) implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) @@ -205,6 +210,7 @@ dependencies { //Hilt ksp(libs.hilt.android.compiler) ksp(libs.hilt.androidx.compiler) + implementation(libs.androidx.hilt.navigation) implementation(libs.hilt.android.core) implementation(libs.hilt.androidx.work) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5873e2185..48d4e3c35 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -104,6 +104,11 @@ + + + Unit = {} ) { Row( modifier = Modifier .fillMaxWidth() - .clickable { onClick() } + .clickable(enabled = isEnabled) { onClick() } .padding( horizontal = dimensionResource(R.dimen.padding_medium), vertical = dimensionResource(R.dimen.padding_xsmall) @@ -87,11 +89,13 @@ fun BlackListComposable( ) Text( text = stringResource(R.string.version, versionName, versionCode), - style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp) + style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp), + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } } - Checkbox(checked = isChecked, onCheckedChange = { onClick() }) + Checkbox(checked = isChecked, enabled = isEnabled, onCheckedChange = { onClick() }) } } @@ -104,6 +108,7 @@ private fun BlackListComposablePreview() { packageName = BuildConfig.APPLICATION_ID, versionName = BuildConfig.VERSION_NAME, versionCode = BuildConfig.VERSION_CODE.toLong(), - isChecked = true + isChecked = true, + isEnabled = false ) } diff --git a/app/src/main/java/com/aurora/store/compose/composables/InstalledAppComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/InstalledAppComposable.kt index 7b0f1ad31..ab6c3975b 100644 --- a/app/src/main/java/com/aurora/store/compose/composables/InstalledAppComposable.kt +++ b/app/src/main/java/com/aurora/store/compose/composables/InstalledAppComposable.kt @@ -84,7 +84,9 @@ fun InstalledAppComposable( ) Text( text = stringResource(R.string.version, versionName, versionCode), - style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp) + style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp), + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } } diff --git a/app/src/main/java/com/aurora/store/compose/composables/SearchAppBarComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/SearchAppBarComposable.kt new file mode 100644 index 000000000..74fea776c --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/SearchAppBarComposable.kt @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composables + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar +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.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction + +/** + * A top app bar composable to be used with Scaffold in different Screen + * @param searchHint Hint to show to the user in search bar + * @param onNavigateUp Action when user clicks the navigation icon + * @param onSearch Callback for a search + * @param actions Actions to display on the top app bar (for e.g. menu) + */ +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun SearchAppBarComposable( + @StringRes searchHint: Int? = null, + onNavigateUp: () -> Unit, + onSearch: (query: String) -> Unit, + actions: @Composable (RowScope.() -> Unit) = {} +) { + val focusManager = LocalFocusManager.current + var query by rememberSaveable { mutableStateOf("") } + + TopAppBar( + title = { + TextField( + modifier = Modifier.fillMaxWidth(), + value = query, + onValueChange = { newQuery -> + query = newQuery.trim() + onSearch(query) + }, + maxLines = 1, + placeholder = { if (searchHint != null) Text(text = stringResource(searchHint)) }, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { focusManager.clearFocus() }) + ) + }, + navigationIcon = { + IconButton(onClick = onNavigateUp) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null + ) + } + }, + actions = { + if (query.isNotBlank()) { + IconButton( + onClick = { + query = "" + onSearch(query) + focusManager.clearFocus(force = true) + } + ) { + Icon(imageVector = Icons.Default.Clear, contentDescription = null) + } + } + actions() + } + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/TopAppBarComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/TopAppBarComposable.kt new file mode 100644 index 000000000..98249a516 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/TopAppBarComposable.kt @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composables + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource + +/** + * A top app bar composable to be used with Scaffold in different Screen + * @param title Title of the screen + * @param onNavigateUp Action when user clicks the navigation icon + * @param actions Actions to display on the top app bar (for e.g. menu) + */ +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun TopAppBarComposable( + @StringRes title: Int? = null, + onNavigateUp: () -> Unit, + actions: @Composable (RowScope.() -> Unit) = {} +) { + TopAppBar( + title = { + if (title != null) Text(text = stringResource(title)) + }, + navigationIcon = { + IconButton(onClick = onNavigateUp) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null + ) + } + }, + actions = actions + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/TransparentIconComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/TransparentIconComposable.kt new file mode 100644 index 000000000..d9fbad770 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/TransparentIconComposable.kt @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.composables + +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.aurora.store.R + +/** + * A transparent icon for occupying space + * + * This is useful for occupying spaces in composable where alignment is not respected such as + * DropDownMenu. + */ +@Composable +fun TransparentIconComposable() { + Icon( + painter = painterResource(R.drawable.ic_transparent), + contentDescription = null + ) +} + +@Preview(showBackground = true) +@Composable +private fun TransparentIconComposablePreview() { + TransparentIconComposable() +} diff --git a/app/src/main/java/com/aurora/store/compose/menu/BlacklistMenu.kt b/app/src/main/java/com/aurora/store/compose/menu/BlacklistMenu.kt new file mode 100644 index 000000000..2c8b23c87 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/menu/BlacklistMenu.kt @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.menu + +import androidx.compose.foundation.layout.Box +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.aurora.store.R +import com.aurora.store.compose.composables.TransparentIconComposable +import com.aurora.store.compose.menu.items.BlacklistMenuItem + +/** + * Menu for the blacklist screen + * @param onMenuItemClicked Callback when a menu item has been clicked + * @see BlacklistMenuItem + */ +@Composable +fun BlacklistMenu(onMenuItemClicked: (blacklistMenuItem: BlacklistMenuItem) -> Unit = {}) { + var expanded by remember { mutableStateOf(false) } + fun onClick(blacklistMenuItem: BlacklistMenuItem) { + onMenuItemClicked(blacklistMenuItem) + expanded = false + } + + Box { + IconButton(onClick = { expanded = true }) { + Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.menu)) + } + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + DropdownMenuItem( + text = { Text(text = stringResource(R.string.action_select_all)) }, + onClick = { onClick(BlacklistMenuItem.SELECT_ALL) }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.ic_select_all), + contentDescription = null + ) + } + ) + DropdownMenuItem( + text = { Text(text = stringResource(R.string.action_remove_all)) }, + onClick = { onClick(BlacklistMenuItem.REMOVE_ALL) }, + leadingIcon = { TransparentIconComposable() } + ) + HorizontalDivider() + DropdownMenuItem( + text = { Text(text = stringResource(R.string.action_import)) }, + onClick = { onClick(BlacklistMenuItem.IMPORT) }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.ic_file_import), + contentDescription = null + ) + } + ) + DropdownMenuItem( + text = { Text(text = stringResource(R.string.action_export)) }, + onClick = { onClick(BlacklistMenuItem.EXPORT) }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.ic_file_export), + contentDescription = null + ) + } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun BlacklistMenuPreview() { + BlacklistMenu() +} diff --git a/app/src/main/java/com/aurora/store/compose/menu/items/BlacklistMenuItem.kt b/app/src/main/java/com/aurora/store/compose/menu/items/BlacklistMenuItem.kt new file mode 100644 index 000000000..59f8b79bd --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/menu/items/BlacklistMenuItem.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.menu.items + +/** + * Valid menu items for the purpose of handling clicks + */ +enum class BlacklistMenuItem { + SELECT_ALL, + REMOVE_ALL, + IMPORT, + EXPORT +} diff --git a/app/src/main/java/com/aurora/store/compose/navigation/NavGraph.kt b/app/src/main/java/com/aurora/store/compose/navigation/NavGraph.kt new file mode 100644 index 000000000..5ba69343e --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/navigation/NavGraph.kt @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.navigation + +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.aurora.store.compose.ui.commons.BlacklistScreen + +/** + * Navigation graph for compose screens + * @param navHostController [NavHostController] to navigate with compose + * @param startDestination Starting destination for the activity/app + */ +@Composable +fun NavGraph(navHostController: NavHostController, startDestination: Screen) { + // TODO: Drop this logic once everything is in compose + val activity = LocalActivity.current + fun onNavigateUp() { + if (navHostController.previousBackStackEntry != null) { + navHostController.navigateUp() + } else { + activity?.finish() + } + } + + NavHost(navController = navHostController, startDestination = startDestination) { + composable { + BlacklistScreen(onNavigateUp = { onNavigateUp() }) + } + } +} 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 new file mode 100644 index 000000000..927502aed --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.navigation + +import android.os.Parcelable +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.aurora.store.R +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +/** + * Destination (Screen) for navigation in compose + * @param label Label of the screen + * @param icon Optional icon for the screen; Must not be null if screen is a top-level destination + */ +@Parcelize +@Serializable +sealed class Screen(@StringRes val label: Int, @DrawableRes val icon: Int? = null): Parcelable { + + companion object { + const val PARCEL_KEY = "SCREEN" + } + + @Serializable + data object Blacklist : Screen(label = R.string.title_blacklist_manager) +} diff --git a/app/src/main/java/com/aurora/store/compose/ui/commons/BlacklistScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/commons/BlacklistScreen.kt new file mode 100644 index 000000000..c8e6e4e6b --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/ui/commons/BlacklistScreen.kt @@ -0,0 +1,156 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.compose.ui.commons + +import android.content.pm.PackageInfo +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.content.pm.PackageInfoCompat +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.aurora.Constants +import com.aurora.extensions.toast +import com.aurora.store.R +import com.aurora.store.compose.composables.BlackListComposable +import com.aurora.store.compose.composables.SearchAppBarComposable +import com.aurora.store.compose.menu.BlacklistMenu +import com.aurora.store.compose.menu.items.BlacklistMenuItem +import com.aurora.store.util.PackageUtil +import com.aurora.store.viewmodel.all.BlacklistViewModel +import java.util.Calendar + +@Composable +fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hiltViewModel()) { + val ctx = LocalContext.current + val packages by viewModel.filteredPackages.collectAsStateWithLifecycle() + + ScreenContent( + packages = packages, + onNavigateUp = onNavigateUp, + isPackageBlacklisted = { pkgName -> pkgName in viewModel.blacklist }, + isPackageFiltered = { pkgInfo -> viewModel.isFiltered(pkgInfo) }, + onBlacklistImport = { uri -> + viewModel.importBlacklist(ctx, uri) + ctx.toast(R.string.toast_black_import_success) + }, + onBlacklistExport = { uri -> + viewModel.exportBlacklist(ctx, uri) + ctx.toast(R.string.toast_black_export_success) + }, + onBlacklist = { packageName -> viewModel.blacklist(packageName) }, + onBlacklistAll = { viewModel.blacklistAll() }, + onWhitelist = { packageName -> viewModel.whitelist(packageName) }, + onWhitelistAll = { viewModel.whitelistAll() }, + onSearch = { query -> viewModel.search(query) } + ) +} + +@Composable +private fun ScreenContent( + packages: List? = null, + onNavigateUp: () -> Unit = {}, + isPackageBlacklisted: (packageName: String) -> Boolean = { false }, + isPackageFiltered: (packageInfo: PackageInfo) -> Boolean = { false }, + onBlacklistImport: (uri: Uri) -> Unit = {}, + onBlacklistExport: (uri: Uri) -> Unit = {}, + onBlacklist: (packageName: String) -> Unit = {}, + onBlacklistAll: () -> Unit = {}, + onWhitelist: (packageName: String) -> Unit = {}, + onWhitelistAll: () -> Unit = {}, + onSearch: (query: String) -> Unit = {} +) { + + val ctx = LocalContext.current + val docImportLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument(), + onResult = { + if (it != null) onBlacklistImport(it) else ctx.toast(R.string.toast_black_import_failed) + } + ) + val docExportLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE), + onResult = { + if (it != null) onBlacklistExport(it) else ctx.toast(R.string.toast_black_export_failed) + } + ) + + @Composable + fun SetupMenu() { + BlacklistMenu { blacklistMenuItem -> + when (blacklistMenuItem) { + BlacklistMenuItem.SELECT_ALL -> onBlacklistAll() + BlacklistMenuItem.REMOVE_ALL -> onWhitelistAll() + BlacklistMenuItem.IMPORT -> { + docImportLauncher.launch(arrayOf(Constants.JSON_MIME_TYPE)) + } + + BlacklistMenuItem.EXPORT -> { + docExportLauncher.launch( + "aurora_store_apps_${Calendar.getInstance().time.time}.json" + ) + } + } + } + } + + Scaffold( + topBar = { + SearchAppBarComposable( + onNavigateUp = onNavigateUp, + actions = { if (!packages.isNullOrEmpty()) SetupMenu() }, + searchHint = R.string.search_hint, + onSearch = { query -> onSearch(query) } + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues), + verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall)) + ) { + items(items = packages ?: emptyList(), key = { p -> p.packageName.hashCode() }) { pkg -> + val isBlacklisted = isPackageBlacklisted(pkg.packageName) + val isFiltered = isPackageFiltered(pkg) + BlackListComposable( + icon = PackageUtil.getIconForPackage(ctx, pkg.packageName)!!, + displayName = pkg.applicationInfo!!.loadLabel(ctx.packageManager).toString(), + packageName = pkg.packageName, + versionName = pkg.versionName!!, + versionCode = PackageInfoCompat.getLongVersionCode(pkg), + isChecked = isBlacklisted || isFiltered, + isEnabled = !isFiltered, + onClick = { + if (isBlacklisted) { + onWhitelist(pkg.packageName) + } else { + onBlacklist(pkg.packageName) + } + } + ) + } + } + } +} + +@Preview +@Composable +private fun BlacklistScreenPreview() { + ScreenContent() +} diff --git a/app/src/main/java/com/aurora/store/data/helper/UpdateHelper.kt b/app/src/main/java/com/aurora/store/data/helper/UpdateHelper.kt index c63a6e288..802ad64d8 100644 --- a/app/src/main/java/com/aurora/store/data/helper/UpdateHelper.kt +++ b/app/src/main/java/com/aurora/store/data/helper/UpdateHelper.kt @@ -113,6 +113,13 @@ class UpdateHelper @Inject constructor( updateDao.delete(packageName) } + /** + * Delete all updates from the database + */ + suspend fun deleteAllUpdates() { + updateDao.deleteAll() + } + /** * Cancels the automated updates check * @see [UpdateWorker] diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt deleted file mode 100644 index 35a251b74..000000000 --- a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt +++ /dev/null @@ -1,179 +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.commons - -import android.content.pm.PackageInfo -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.store.AuroraApp -import com.aurora.store.R -import com.aurora.store.data.event.BusEvent -import com.aurora.store.databinding.FragmentGenericWithSearchBinding -import com.aurora.store.view.epoxy.views.BlackListViewModel_ -import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ -import com.aurora.store.viewmodel.all.BlacklistViewModel -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import java.util.Calendar - -@AndroidEntryPoint -class BlacklistFragment : BaseFragment() { - - private val viewModel: BlacklistViewModel by viewModels() - - private val startForDocumentImport = - registerForActivityResult(ActivityResultContracts.OpenDocument()) { - if (it != null) importBlacklist(it) else toast(R.string.toast_black_import_failed) - } - private val startForDocumentExport = - registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) { - if (it != null) exportBlacklist(it) else toast(R.string.toast_black_export_failed) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleOwner.lifecycleScope.launch { - viewModel.packages.collect { - updateController(it) - } - } - - // Toolbar - binding.toolbar.apply { - inflateMenu(R.menu.menu_blacklist) - setNavigationOnClickListener { - viewModel.blacklistProvider.blacklist = viewModel.selected - findNavController().navigateUp() - } - setOnMenuItemClickListener { - when (it.itemId) { - R.id.action_import -> { - startForDocumentImport.launch(arrayOf(Constants.JSON_MIME_TYPE)) - } - - R.id.action_export -> { - startForDocumentExport.launch( - "aurora_store_apps_${Calendar.getInstance().time.time}.json" - ) - } - - R.id.action_select_all -> { - viewModel.selectAll() - binding.recycler.requestModelBuild() - true - } - - R.id.action_remove_all -> { - viewModel.removeAll() - binding.recycler.requestModelBuild() - true - } - } - true - } - } - - binding.searchBar.addTextChangedListener(object : TextWatcher { - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - if (s.isNullOrEmpty()) { - updateController(viewModel.packages.value) - } else { - val filteredPackages = viewModel.packages.value?.filter { - it.applicationInfo!!.loadLabel(requireContext().packageManager) - .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 - ) { - } - }) - } - - override fun onPause() { - super.onPause() - viewModel.blacklistProvider.blacklist = viewModel.selected - } - - private fun updateController(packages: List?) { - binding.recycler.withModels { - setFilterDuplicates(true) - if (packages == null) { - for (i in 1..10) { - add( - AppListViewShimmerModel_() - .id(i) - ) - } - } else { - packages - .sortedByDescending { app -> - viewModel.blacklistProvider.isBlacklisted(app.packageName) - } - .forEach { - add( - BlackListViewModel_() - .id(it.packageName.hashCode()) - .packageInfo(it) - .markChecked(viewModel.selected.contains(it.packageName)) - .checked { _, isChecked -> - if (isChecked) { - viewModel.selected.add(it.packageName) - AuroraApp.events.send(BusEvent.Blacklisted(it.packageName)) - } else { - viewModel.selected.remove(it.packageName) - } - - requestModelBuild() - } - ) - } - } - } - } - - private fun importBlacklist(uri: Uri) { - viewModel.importBlacklist(requireContext(), uri) - binding.recycler.requestModelBuild() - toast(R.string.toast_black_import_success) - } - - private fun exportBlacklist(uri: Uri) { - viewModel.exportBlacklist(requireContext(), uri) - toast(R.string.toast_black_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 f466eb969..79db6a636 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 @@ -57,9 +57,11 @@ import com.aurora.Constants import com.aurora.Constants.URL_TOS import com.aurora.extensions.browse import com.aurora.extensions.getStyledAttributeColor +import com.aurora.extensions.navigate import com.aurora.extensions.setAppTheme import com.aurora.store.MR import com.aurora.store.R +import com.aurora.store.compose.navigation.Screen import com.aurora.store.util.Preferences import com.aurora.store.compose.theme.AuroraTheme import com.aurora.store.viewmodel.commons.MoreViewModel @@ -76,12 +78,23 @@ class MoreDialogFragment : DialogFragment() { private var secondaryColor: Color = Color.White private var onSecondaryColor: Color = Color.Black - private data class Option( - @StringRes val title: Int, - @DrawableRes val icon: Int, - @IdRes val destinationID: Int + private abstract class Option( + @StringRes open val title: Int, + @DrawableRes open val icon: Int, ) + private data class ViewOption( + override val title: Int, + override val icon: Int, + @IdRes val destinationID: Int + ) : Option(title, icon) + + private data class ComposeOption( + override val title: Int, + override val icon: Int, + val screen: Screen + ) : Option(title, icon) + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return MaterialAlertDialogBuilder(requireContext()) .setView(customDialogView(requireContext())) @@ -134,7 +147,16 @@ class MoreDialogFragment : DialogFragment() { OptionItem( option = option, tintColor = onPrimaryColor, - textColor = onSecondaryColor + textColor = onSecondaryColor, + onClick = { + when (option) { + is ViewOption -> { + findNavController().navigate(option.destinationID) + } + + is ComposeOption -> context.navigate(option.screen) + } + } ) } } @@ -142,7 +164,16 @@ class MoreDialogFragment : DialogFragment() { OptionItem( option = option, tintColor = onPrimaryColor, - textColor = onPrimaryColor + textColor = onPrimaryColor, + onClick = { + when (option) { + is ViewOption -> { + findNavController().navigate(option.destinationID) + } + + is ComposeOption -> context.navigate(option.screen) + } + } ) } Footer(onPrimaryColor) @@ -310,12 +341,13 @@ class MoreDialogFragment : DialogFragment() { private fun OptionItem( option: Option, tintColor: Color = Color.Blue, - textColor: Color = Color.Black + textColor: Color = Color.Black, + onClick: () -> Unit ) { Row( modifier = Modifier .fillMaxWidth() - .clickable { findNavController().navigate(option.destinationID) } + .clickable { onClick() } .padding(12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start) @@ -391,22 +423,22 @@ class MoreDialogFragment : DialogFragment() { private fun getOptions(): List