This commit is contained in:
Torsten Grote
2025-02-13 11:52:30 -03:00
parent 29e2d15386
commit 15ad121fc8
28 changed files with 1331 additions and 1 deletions

1
basic/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

66
basic/build.gradle.kts Normal file
View File

@@ -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)
}

21
basic/proguard-rules.pro vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Fdroidclient">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.Fdroidclient">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -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()
}
}
}

View File

@@ -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 = "")
}
}
}

View File

@@ -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<AppNavigationItem>()
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<String>() }
val addedRepos = remember { mutableStateListOf<String>() }
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)
}
}

View File

@@ -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()
}

View File

@@ -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
)
}
)
}
}

View File

@@ -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)
}
}

View File

@@ -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<String>,
categories: List<String>,
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))
}
}
}

View File

@@ -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

View File

@@ -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 }
}
}
)
}

View File

@@ -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<String>,
addedRepos: MutableList<String>,
categories: List<String>,
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 },
)
}
}
}

View File

@@ -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<String>,
addedRepos: List<String>,
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
)
)
}
}
}
}

View File

@@ -0,0 +1 @@
../../../../../../../../../app/src/main/java/org/fdroid/fdroid/ui/theme/Color.kt

View File

@@ -0,0 +1 @@
../../../../../../../../../app/src/main/java/org/fdroid/fdroid/ui/theme/Theme.kt

View File

@@ -0,0 +1,9 @@
package org.fdroid.fdroid
object Preferences {
fun get(): Preferences = Preferences
val isPureBlack: Boolean = true
}

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">F-Droid Next</string>
</resources>

View File

@@ -0,0 +1 @@
../../../../../app/src/main/res/values/strings.xml

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Fdroidclient" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@@ -82,6 +82,7 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCoreKtx" }
androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "androidxLifecycleLivedataKtx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidxLifecycleLivedataKtx" }
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidxConstraintlayout" }
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidxSwipeRefreshLayout" }
androidx-gridlayout = { module = "androidx.gridlayout:gridlayout", version.ref = "androidxGridlayout" }
@@ -96,12 +97,19 @@ androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom-alpha", version.ref = "androidxComposeBom" } # TODO remove -alpha
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose" }
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive" }
androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation" }
androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout" }
androidx-compose-material3-adaptive-navigation-suite-android = { module = "androidx.compose.material3:material3-adaptive-navigation-suite-android" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanistDrawablepainter" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivityCompose" }
@@ -162,6 +170,8 @@ turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" }
json = { module = "org.json:json", version.ref = "json" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
@@ -170,6 +180,7 @@ android-ksp = { id = "com.google.devtools.ksp", version.ref = "androidKspPlugin"
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
jetbrains-kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
jetbrains-kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
jetbrains-kotlin-parcelize = { id = "kotlin-parcelize" }
jetbrains-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
jetbrains-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }

View File

@@ -12,3 +12,5 @@ include ':libs:sharedTest'
include ':libs:download'
include ':libs:index'
include ':libs:database'
include ':basic'