From 15ad121fc8e5b07cde8fd7d3bbce7717a10b48d9 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 13 Feb 2025 11:52:30 -0300 Subject: [PATCH] next --- basic/.gitignore | 1 + basic/build.gradle.kts | 66 ++++++ basic/proguard-rules.pro | 21 ++ basic/src/main/AndroidManifest.xml | 23 ++ .../java/org/fdroid/basic/MainActivity.kt | 17 ++ .../fdroid/basic/ui/icons/PackageVariant.kt | 94 ++++++++ .../java/org/fdroid/basic/ui/main/Apps.kt | 134 +++++++++++ .../java/org/fdroid/basic/ui/main/Main.kt | 97 ++++++++ .../fdroid/basic/ui/main/MainOverflowMenu.kt | 51 +++++ .../fdroid/basic/ui/main/apps/AppDetails.kt | 55 +++++ .../org/fdroid/basic/ui/main/apps/AppList.kt | 111 +++++++++ .../basic/ui/main/apps/AppNavigationItem.kt | 11 + .../basic/ui/main/apps/AppSearchInputField.kt | 90 ++++++++ .../fdroid/basic/ui/main/apps/AppsFilter.kt | 212 ++++++++++++++++++ .../fdroid/basic/ui/main/apps/AppsSearch.kt | 89 ++++++++ .../java/org/fdroid/basic/ui/theme/Color.kt | 1 + .../java/org/fdroid/basic/ui/theme/Theme.kt | 1 + .../src/main/java/org/fdroid/fdroid/Compat.kt | 9 + .../drawable-v24/ic_launcher_foreground.xml | 30 +++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + basic/src/main/res/values/colors.xml | 10 + basic/src/main/res/values/strings-next.xml | 6 + basic/src/main/res/values/strings.xml | 1 + basic/src/main/res/values/themes.xml | 5 + gradle/libs.versions.toml | 13 +- settings.gradle | 2 + 28 files changed, 1331 insertions(+), 1 deletion(-) create mode 100644 basic/.gitignore create mode 100644 basic/build.gradle.kts create mode 100644 basic/proguard-rules.pro create mode 100644 basic/src/main/AndroidManifest.xml create mode 100644 basic/src/main/java/org/fdroid/basic/MainActivity.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/icons/PackageVariant.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/Apps.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/Main.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/MainOverflowMenu.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/apps/AppDetails.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/apps/AppList.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/apps/AppNavigationItem.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/apps/AppSearchInputField.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsFilter.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsSearch.kt create mode 120000 basic/src/main/java/org/fdroid/basic/ui/theme/Color.kt create mode 120000 basic/src/main/java/org/fdroid/basic/ui/theme/Theme.kt create mode 100644 basic/src/main/java/org/fdroid/fdroid/Compat.kt create mode 100644 basic/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 basic/src/main/res/drawable/ic_launcher_background.xml create mode 100644 basic/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 basic/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 basic/src/main/res/values/colors.xml create mode 100644 basic/src/main/res/values/strings-next.xml create mode 120000 basic/src/main/res/values/strings.xml create mode 100644 basic/src/main/res/values/themes.xml diff --git a/basic/.gitignore b/basic/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/basic/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/basic/build.gradle.kts b/basic/build.gradle.kts new file mode 100644 index 000000000..0df56bbb3 --- /dev/null +++ b/basic/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.jetbrains.kotlin.parcelize) + alias(libs.plugins.jetbrains.compose.compiler) +} + +android { + namespace = "org.fdroid.basic" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + applicationId = "org.fdroid.next" + minSdk = 23 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material3.adaptive) + implementation(libs.androidx.compose.material3.adaptive.layout) + implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.compose.material3.adaptive.navigation.suite.android) + + testImplementation(libs.junit) + + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} diff --git a/basic/proguard-rules.pro b/basic/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/basic/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/basic/src/main/AndroidManifest.xml b/basic/src/main/AndroidManifest.xml new file mode 100644 index 000000000..dde1136bb --- /dev/null +++ b/basic/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/basic/src/main/java/org/fdroid/basic/MainActivity.kt b/basic/src/main/java/org/fdroid/basic/MainActivity.kt new file mode 100644 index 000000000..97e4d9c75 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/MainActivity.kt @@ -0,0 +1,17 @@ +package org.fdroid.basic + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import org.fdroid.basic.ui.main.Main + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + Main() + } + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/icons/PackageVariant.kt b/basic/src/main/java/org/fdroid/basic/ui/icons/PackageVariant.kt new file mode 100644 index 000000000..d05c34437 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/icons/PackageVariant.kt @@ -0,0 +1,94 @@ +package org.fdroid.basic.ui.icons + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.fdroid.fdroid.ui.theme.FDroidContent + +val PackageVariant: ImageVector + get() { + if (_PackageVariant != null) { + return _PackageVariant!! + } + _PackageVariant = Builder( + name = "packageVariant", defaultWidth = 24.0.dp, defaultHeight + = 24.0.dp, viewportWidth = 24.0f, viewportHeight = 24.0f + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero + ) { + moveTo(2.0f, 10.96f) + curveTo(1.5f, 10.68f, 1.35f, 10.07f, 1.63f, 9.59f) + lineTo(3.13f, 7.0f) + curveTo(3.24f, 6.8f, 3.41f, 6.66f, 3.6f, 6.58f) + lineTo(11.43f, 2.18f) + curveTo(11.59f, 2.06f, 11.79f, 2.0f, 12.0f, 2.0f) + curveTo(12.21f, 2.0f, 12.41f, 2.06f, 12.57f, 2.18f) + lineTo(20.47f, 6.62f) + curveTo(20.66f, 6.72f, 20.82f, 6.88f, 20.91f, 7.08f) + lineTo(22.36f, 9.6f) + curveTo(22.64f, 10.08f, 22.47f, 10.69f, 22.0f, 10.96f) + lineTo(21.0f, 11.54f) + verticalLineTo(16.5f) + curveTo(21.0f, 16.88f, 20.79f, 17.21f, 20.47f, 17.38f) + lineTo(12.57f, 21.82f) + curveTo(12.41f, 21.94f, 12.21f, 22.0f, 12.0f, 22.0f) + curveTo(11.79f, 22.0f, 11.59f, 21.94f, 11.43f, 21.82f) + lineTo(3.53f, 17.38f) + curveTo(3.21f, 17.21f, 3.0f, 16.88f, 3.0f, 16.5f) + verticalLineTo(10.96f) + curveTo(2.7f, 11.13f, 2.32f, 11.14f, 2.0f, 10.96f) + moveTo(12.0f, 4.15f) + verticalLineTo(4.15f) + lineTo(12.0f, 10.85f) + verticalLineTo(10.85f) + lineTo(17.96f, 7.5f) + lineTo(12.0f, 4.15f) + moveTo(5.0f, 15.91f) + lineTo(11.0f, 19.29f) + verticalLineTo(12.58f) + lineTo(5.0f, 9.21f) + verticalLineTo(15.91f) + moveTo(19.0f, 15.91f) + verticalLineTo(12.69f) + lineTo(14.0f, 15.59f) + curveTo(13.67f, 15.77f, 13.3f, 15.76f, 13.0f, 15.6f) + verticalLineTo(19.29f) + lineTo(19.0f, 15.91f) + moveTo(13.85f, 13.36f) + lineTo(20.13f, 9.73f) + lineTo(19.55f, 8.72f) + lineTo(13.27f, 12.35f) + lineTo(13.85f, 13.36f) + close() + } + } + .build() + return _PackageVariant!! + } + +private var _PackageVariant: ImageVector? = null + +@Preview +@Composable +private fun Preview() { + FDroidContent { + Box(modifier = Modifier.padding(12.dp)) { + Image(imageVector = PackageVariant, contentDescription = "") + } + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/Apps.kt b/basic/src/main/java/org/fdroid/basic/ui/main/Apps.kt new file mode 100644 index 000000000..730710ccb --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/Apps.kt @@ -0,0 +1,134 @@ +package org.fdroid.basic.ui.main + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole.Detail +import androidx.compose.material3.adaptive.layout.PaneAdaptedValue +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import kotlinx.coroutines.launch +import org.fdroid.basic.R +import org.fdroid.basic.ui.main.apps.AppDetails +import org.fdroid.basic.ui.main.apps.AppList +import org.fdroid.basic.ui.main.apps.AppNavigationItem +import org.fdroid.basic.ui.main.apps.AppsFilter +import org.fdroid.basic.ui.main.apps.AppsSearch +import org.fdroid.fdroid.ui.theme.FDroidContent + +enum class Sort { + NAME, + LATEST, +} + +const val NUM_ITEMS = 42 + +@Composable +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +fun Apps(modifier: Modifier) { + val navigator = rememberListDetailPaneScaffoldNavigator() + val scope = rememberCoroutineScope() + BackHandler(enabled = navigator.canNavigateBack()) { + scope.launch { + navigator.navigateBack() + } + } + val isDetailVisible = navigator.scaffoldValue[Detail] == PaneAdaptedValue.Expanded + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + Column( + modifier.fillMaxSize() + ) { + var filterExpanded by rememberSaveable { mutableStateOf(true) } + var sortBy by rememberSaveable { mutableStateOf(Sort.NAME) } + var onlyInstalledApps by rememberSaveable { mutableStateOf(false) } + val addedCategories = remember { mutableStateListOf() } + val addedRepos = remember { mutableStateListOf() } + val categories = listOf( + stringResource(R.string.category_Time), + stringResource(R.string.category_Games), + stringResource(R.string.category_Money), + stringResource(R.string.category_Reading), + stringResource(R.string.category_Theming), + stringResource(R.string.category_Connectivity), + stringResource(R.string.category_Internet), + stringResource(R.string.category_Navigation), + stringResource(R.string.category_Multimedia), + stringResource(R.string.category_Phone_SMS), + stringResource(R.string.category_Science_Education), + stringResource(R.string.category_Security), + stringResource(R.string.category_Sports_Health), + stringResource(R.string.category_System), + stringResource(R.string.category_Writing), + ) + AppsSearch( + onlyInstalledApps = onlyInstalledApps, + addedCategories = addedCategories, + addedRepos = addedRepos, + toggleFilter = { filterExpanded = !filterExpanded }, + ) + AppsFilter( + filterExpanded = filterExpanded, + sortBy = sortBy, + onlyInstalledApps = onlyInstalledApps, + addedCategories = addedCategories, + addedRepos = addedRepos, + categories = categories, + onSortByChanged = { sortBy = it }, + toggleOnlyInstalledApps = { + onlyInstalledApps = !onlyInstalledApps + }, + ) + AppList( + onlyInstalledApps = onlyInstalledApps, + sortBy = sortBy, + addedCategories = addedCategories, + categories = categories, + currentItem = if (isDetailVisible) { + navigator.currentDestination?.contentKey + } else { + null + }, + ) { + scope.launch { navigator.navigateTo(Detail, it) } + } + } + } + }, + detailPane = { + AnimatedPane { + navigator.currentDestination?.contentKey?.let { + AppDetails( + appItem = it, + ) + } + } + }, + ) +} + +@Preview +@PreviewScreenSizes +@Composable +fun AppsPreview() { + FDroidContent { + Apps(Modifier) + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/Main.kt b/basic/src/main/java/org/fdroid/basic/ui/main/Main.kt new file mode 100644 index 000000000..4f5673b99 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/Main.kt @@ -0,0 +1,97 @@ +package org.fdroid.basic.ui.main + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Apps +import androidx.compose.material.icons.filled.Update +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType +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.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import androidx.window.core.layout.WindowWidthSizeClass +import org.fdroid.basic.R +import org.fdroid.fdroid.ui.theme.FDroidContent + +enum class AppDestinations( + @StringRes val label: Int, + val icon: ImageVector, +) { + APPS(R.string.apps, Icons.Filled.Apps), + UPDATES(R.string.updates, Icons.Filled.Update), +} + +@Composable +fun Main() { + var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.APPS) } + FDroidContent { + val adaptiveInfo = currentWindowAdaptiveInfo() + val customNavSuiteType = with(adaptiveInfo) { + when (windowSizeClass.windowWidthSizeClass) { + WindowWidthSizeClass.COMPACT -> NavigationSuiteType.NavigationBar + else -> NavigationSuiteType.NavigationRail + } + } + NavigationSuiteScaffold( + modifier = Modifier.fillMaxSize(), + layoutType = customNavSuiteType, + navigationSuiteColors = NavigationSuiteDefaults.colors( + navigationBarContainerColor = Color.Transparent, + ), + navigationSuiteItems = { + AppDestinations.entries.forEach { dest -> + item( + icon = { + BadgedBox( + badge = { + if (dest == AppDestinations.UPDATES) { + Badge { + Text(text = "13") + } + } + } + ) { + Icon( + dest.icon, + contentDescription = stringResource(dest.label) + ) + } + }, + label = { Text(stringResource(dest.label)) }, + selected = dest == currentDestination, + onClick = { currentDestination = dest } + ) + } + } + ) { + if (currentDestination == AppDestinations.APPS) Apps(Modifier) + else Text( + text = "TODO", + modifier = Modifier.safeDrawingPadding(), + ) + } + } +} + +@Preview +@PreviewScreenSizes +@Composable +fun MainPreview() { + Main() +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/MainOverflowMenu.kt b/basic/src/main/java/org/fdroid/basic/ui/main/MainOverflowMenu.kt new file mode 100644 index 000000000..ad24ba461 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/MainOverflowMenu.kt @@ -0,0 +1,51 @@ +package org.fdroid.basic.ui.main + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import org.fdroid.basic.R +import org.fdroid.basic.ui.icons.PackageVariant + +@Composable +fun MainOverFlowMenu(menuExpanded: Boolean, onDismissRequest: () -> Unit) { + DropdownMenu( + expanded = menuExpanded, + onDismissRequest = onDismissRequest) { + DropdownMenuItem( + text = { Text(stringResource(R.string.app_details_repositories)) }, + onClick = { }, + leadingIcon = { + Icon( + PackageVariant, + contentDescription = null + ) + } + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.menu_settings)) }, + onClick = { }, + leadingIcon = { + Icon( + Icons.Filled.Settings, + contentDescription = null + ) + } + ) + DropdownMenuItem( + text = { Text("About") }, + onClick = { }, + leadingIcon = { + Icon( + Icons.Filled.Info, + contentDescription = null + ) + } + ) + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppDetails.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppDetails.kt new file mode 100644 index 000000000..1671dc9ac --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppDetails.kt @@ -0,0 +1,55 @@ +package org.fdroid.basic.ui.main.apps + +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Android +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.fdroid.fdroid.ui.theme.FDroidContent + +@Composable +fun AppDetails( + appItem: AppNavigationItem, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .safeDrawingPadding() + .padding(16.dp), + horizontalArrangement = spacedBy(8.dp), + ) { + Icon( + Icons.Filled.Android, + tint = MaterialTheme.colorScheme.secondary, + contentDescription = null, + modifier = Modifier.size(48.dp), + ) + Column { + Text(appItem.name, style = MaterialTheme.typography.headlineMedium) + Text(appItem.summary, style = MaterialTheme.typography.bodyMedium) + } + } +} + +@Preview +@Composable +fun AppDetailsPreview() { + FDroidContent { + val item = AppNavigationItem( + packageName = "foo", + name = "bar", + summary = "This is a nice app!", + ) + AppDetails(item) + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppList.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppList.kt new file mode 100644 index 000000000..3d74d8ab4 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppList.kt @@ -0,0 +1,111 @@ +package org.fdroid.basic.ui.main.apps + +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Android +import androidx.compose.material.icons.filled.NewReleases +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import org.fdroid.basic.ui.main.NUM_ITEMS +import org.fdroid.basic.ui.main.Sort + +@Composable +fun AppList( + onlyInstalledApps: Boolean, + sortBy: Sort, + addedCategories: List, + categories: List, + currentItem: AppNavigationItem?, + onItemClick: (AppNavigationItem) -> Unit, +) { + LazyColumn( + contentPadding = PaddingValues(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.then( + if (currentItem == null) Modifier + else Modifier.selectableGroup() + ), + ) { + repeat(NUM_ITEMS) { idx -> + if (onlyInstalledApps && idx % 2 > 0) return@repeat + val i = if (sortBy == Sort.NAME) idx else NUM_ITEMS - idx + val category = categories.getOrElse(i) { categories.random() } + if (addedCategories.isNotEmpty() && category !in addedCategories) return@repeat + item { + val navItem = AppNavigationItem( + packageName = "$i", + name = "App $i", + summary = "Summary of the app • $category", + ) + val isSelected = currentItem?.packageName == navItem.packageName + val interactionModifier = if (currentItem == null) { + Modifier.clickable( + onClick = { onItemClick(navItem) } + ) + } else { + Modifier.selectable( + selected = isSelected, + onClick = { onItemClick(navItem) } + ) + } + ListItem( + headlineContent = { Text(navItem.name) }, + supportingContent = { Text(navItem.summary) }, + leadingContent = { + BadgedBox(badge = { + if (i <= 3) Icon( + imageVector = Icons.Filled.NewReleases, + tint = MaterialTheme.colorScheme.primary, + contentDescription = null, modifier = Modifier.size(24.dp), + ) + }) { + Icon( + Icons.Filled.Android, + tint = MaterialTheme.colorScheme.secondary, + contentDescription = null, + ) + } + }, + colors = ListItemDefaults.colors( + containerColor = if (isSelected) { + MaterialTheme.colorScheme.surfaceVariant + } else { + Color.Transparent + } + ), + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 8.dp, + vertical = 4.dp + ) + .then(interactionModifier) + ) + } + } + item { + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) + } + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppNavigationItem.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppNavigationItem.kt new file mode 100644 index 000000000..7a7e9f0f9 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppNavigationItem.kt @@ -0,0 +1,11 @@ +package org.fdroid.basic.ui.main.apps + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +class AppNavigationItem( + val packageName: String, + val name: String, + val summary: String, +): Parcelable diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppSearchInputField.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppSearchInputField.kt new file mode 100644 index 000000000..3cd638025 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppSearchInputField.kt @@ -0,0 +1,90 @@ +package org.fdroid.basic.ui.main.apps + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.FilterList +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.SearchBarState +import androidx.compose.material3.SearchBarValue +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import kotlinx.coroutines.launch +import org.fdroid.basic.ui.main.MainOverFlowMenu + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun AppSearchInputField( + searchBarState: SearchBarState, + textFieldState: TextFieldState, + toggleFilter: () -> Unit, + showFilterBadge: Boolean +) { + val scope = rememberCoroutineScope() + var menuExpanded by remember { mutableStateOf(false) } + SearchBarDefaults.InputField( + modifier = Modifier, + searchBarState = searchBarState, + textFieldState = textFieldState, + onSearch = { + scope.launch { searchBarState.animateToCollapsed() } + }, + placeholder = { Text("Search...") }, + leadingIcon = { + if (searchBarState.currentValue == SearchBarValue.Expanded) { + IconButton( + onClick = { scope.launch { searchBarState.animateToCollapsed() } } + ) { + Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back") + } + } else { + Icon(Icons.Default.Search, contentDescription = null) + } + }, + trailingIcon = { + if (searchBarState.currentValue == SearchBarValue.Expanded) { + IconButton(onClick = { textFieldState.setTextAndPlaceCursorAtEnd("") }) { + Icon( + Icons.Filled.Clear, + contentDescription = null, + ) + } + } else Row { + IconButton(onClick = toggleFilter) { + BadgedBox(badge = { + if (showFilterBadge) Badge(containerColor = MaterialTheme.colorScheme.secondary) + }) { + Icon( + Icons.Filled.FilterList, + contentDescription = null, + ) + } + } + IconButton(onClick = { menuExpanded = true }) { + Icon( + Icons.Default.MoreVert, + contentDescription = null, + ) + } + MainOverFlowMenu(menuExpanded) { menuExpanded = false } + } + } + ) +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsFilter.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsFilter.kt new file mode 100644 index 000000000..267c8c29a --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsFilter.kt @@ -0,0 +1,212 @@ +package org.fdroid.basic.ui.main.apps + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccessTime +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Done +import androidx.compose.material.icons.filled.SortByAlpha +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +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.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.fdroid.basic.R +import org.fdroid.basic.ui.main.Sort + +@Composable +fun ColumnScope.AppsFilter( + filterExpanded: Boolean, + sortBy: Sort, + onlyInstalledApps: Boolean, + addedCategories: MutableList, + addedRepos: MutableList, + categories: List, + onSortByChanged: (Sort) -> Unit, + toggleOnlyInstalledApps: () -> Unit, +) { + AnimatedVisibility(filterExpanded) { + FlowRow( + modifier = Modifier + .padding(horizontal = 16.dp), + horizontalArrangement = spacedBy(16.dp), + ) { + var sortByMenuExpanded by remember { mutableStateOf(false) } + var repoMenuExpanded by remember { mutableStateOf(false) } + var categoryMenuExpanded by remember { mutableStateOf(false) } + addedCategories.forEach { category -> + FilterChip( + selected = true, + trailingIcon = { + Icon( + Icons.Filled.Clear, + null, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + }, + label = { + Text(category) + }, + onClick = { + addedCategories.remove(category) + } + ) + } + addedRepos.forEach { repo -> + FilterChip( + selected = true, + trailingIcon = { + Icon( + Icons.Filled.Clear, + null, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + }, + label = { + Text(repo) + }, + onClick = { + addedRepos.remove(repo) + } + ) + } + FilterChip( + selected = false, + leadingIcon = { + val vector = when (sortBy) { + Sort.NAME -> Icons.Filled.SortByAlpha + Sort.LATEST -> Icons.Filled.AccessTime + } + Icon(vector, null, modifier = Modifier.size(FilterChipDefaults.IconSize)) + }, + trailingIcon = { + Icon(Icons.Filled.ArrowDropDown, null) + }, + label = { + val s = when (sortBy) { + Sort.NAME -> "Sort by name" + Sort.LATEST -> "Sort by latest" + } + Text(s) + DropdownMenu( + expanded = sortByMenuExpanded, + onDismissRequest = { sortByMenuExpanded = false }, + ) { + DropdownMenuItem( + text = { Text("Sort by name") }, + leadingIcon = { + Icon(Icons.Filled.SortByAlpha, null) + }, + onClick = { + onSortByChanged(Sort.NAME) + sortByMenuExpanded = false + }, + ) + DropdownMenuItem( + text = { Text("Sort by latest") }, + leadingIcon = { + Icon(Icons.Filled.AccessTime, null) + }, + onClick = { + onSortByChanged(Sort.LATEST) + sortByMenuExpanded = false + }, + ) + } + }, + onClick = { sortByMenuExpanded = !sortByMenuExpanded }, + ) + FilterChip( + selected = onlyInstalledApps, + leadingIcon = if (onlyInstalledApps) { + { + Icon( + imageVector = Icons.Filled.Done, + contentDescription = null, + modifier = Modifier.size(FilterChipDefaults.IconSize), + ) + } + } else null, + label = { Text(stringResource(R.string.app_installed)) }, + onClick = toggleOnlyInstalledApps, + ) + FilterChip( + selected = false, + leadingIcon = { + Icon( + Icons.Filled.Add, + null, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + }, + label = { + Text("Category") + DropdownMenu( + expanded = categoryMenuExpanded, + onDismissRequest = { categoryMenuExpanded = false }, + ) { + categories.forEach { category -> + DropdownMenuItem( + text = { Text(category) }, + onClick = { + addedCategories.add(category) + categoryMenuExpanded = false + }, + ) + } + } + }, + onClick = { categoryMenuExpanded = !categoryMenuExpanded }, + ) + FilterChip( + selected = false, + leadingIcon = { + Icon( + Icons.Filled.Add, + null, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + }, + label = { + Text("Repository") + DropdownMenu( + expanded = repoMenuExpanded, + onDismissRequest = { repoMenuExpanded = false }, + ) { + val repos = listOf( + "F-Droid", + "Guardian Project", + "IzzyOnDroid", + ) + repos.forEach { category -> + DropdownMenuItem( + text = { Text(category) }, + onClick = { + addedRepos.add(category) + repoMenuExpanded = false + }, + ) + } + } + }, + onClick = { repoMenuExpanded = !repoMenuExpanded }, + ) + } + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsSearch.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsSearch.kt new file mode 100644 index 000000000..78cfdeaff --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/apps/AppsSearch.kt @@ -0,0 +1,89 @@ +package org.fdroid.basic.ui.main.apps + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.ExpandedFullScreenSearchBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TopSearchBar +import androidx.compose.material3.rememberSearchBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun AppsSearch( + onlyInstalledApps: Boolean, + addedCategories: List, + addedRepos: List, + toggleFilter: () -> Unit, +) { + val textFieldState = rememberTextFieldState() + val scrollBehavior = SearchBarDefaults.enterAlwaysSearchBarScrollBehavior() + val searchBarState = rememberSearchBarState() + val scope = rememberCoroutineScope() + val inputField = @Composable { + AppSearchInputField( + searchBarState = searchBarState, + textFieldState = textFieldState, + toggleFilter = toggleFilter, + showFilterBadge = addedRepos.isNotEmpty() || addedCategories.isNotEmpty() || + onlyInstalledApps, + ) + } + TopSearchBar( + modifier = Modifier, + state = searchBarState, + scrollBehavior = scrollBehavior, + windowInsets = WindowInsets.systemBars, + inputField = inputField, + ) + ExpandedFullScreenSearchBar( + state = searchBarState, + inputField = inputField, + ) { + Column(Modifier.verticalScroll(rememberScrollState())) { + repeat(4) { idx -> + val resultText = "Suggestion $idx" + ListItem(headlineContent = { Text(resultText) }, + supportingContent = { Text("Additional info") }, + leadingContent = { + Icon( + Icons.Filled.Star, + contentDescription = null + ) + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + modifier = Modifier + .clickable { + textFieldState.setTextAndPlaceCursorAtEnd(resultText) + scope.launch { searchBarState.animateToCollapsed() } + } + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 4.dp + ) + ) + } + } + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/theme/Color.kt b/basic/src/main/java/org/fdroid/basic/ui/theme/Color.kt new file mode 120000 index 000000000..9d0fa521d --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/theme/Color.kt @@ -0,0 +1 @@ +../../../../../../../../../app/src/main/java/org/fdroid/fdroid/ui/theme/Color.kt \ No newline at end of file diff --git a/basic/src/main/java/org/fdroid/basic/ui/theme/Theme.kt b/basic/src/main/java/org/fdroid/basic/ui/theme/Theme.kt new file mode 120000 index 000000000..895916611 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/theme/Theme.kt @@ -0,0 +1 @@ +../../../../../../../../../app/src/main/java/org/fdroid/fdroid/ui/theme/Theme.kt \ No newline at end of file diff --git a/basic/src/main/java/org/fdroid/fdroid/Compat.kt b/basic/src/main/java/org/fdroid/fdroid/Compat.kt new file mode 100644 index 000000000..dc4e8d31d --- /dev/null +++ b/basic/src/main/java/org/fdroid/fdroid/Compat.kt @@ -0,0 +1,9 @@ +package org.fdroid.fdroid + +object Preferences { + + fun get(): Preferences = Preferences + + val isPureBlack: Boolean = true + +} diff --git a/basic/src/main/res/drawable-v24/ic_launcher_foreground.xml b/basic/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..7706ab9e6 --- /dev/null +++ b/basic/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/basic/src/main/res/drawable/ic_launcher_background.xml b/basic/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/basic/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/basic/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/basic/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..b3e26b4c6 --- /dev/null +++ b/basic/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/basic/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/basic/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..b3e26b4c6 --- /dev/null +++ b/basic/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/basic/src/main/res/values/colors.xml b/basic/src/main/res/values/colors.xml new file mode 100644 index 000000000..ca1931bca --- /dev/null +++ b/basic/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + diff --git a/basic/src/main/res/values/strings-next.xml b/basic/src/main/res/values/strings-next.xml new file mode 100644 index 000000000..c5faad631 --- /dev/null +++ b/basic/src/main/res/values/strings-next.xml @@ -0,0 +1,6 @@ + + + + F-Droid Next + + diff --git a/basic/src/main/res/values/strings.xml b/basic/src/main/res/values/strings.xml new file mode 120000 index 000000000..83c4d8695 --- /dev/null +++ b/basic/src/main/res/values/strings.xml @@ -0,0 +1 @@ +../../../../../app/src/main/res/values/strings.xml \ No newline at end of file diff --git a/basic/src/main/res/values/themes.xml b/basic/src/main/res/values/themes.xml new file mode 100644 index 000000000..859ec354d --- /dev/null +++ b/basic/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +