diff --git a/app/build.gradle b/app/build.gradle index a084b63bf0a6f982e06fa57294caacc1b88ece1e..c7e27d5a8dd77c6e3e7d20b69c5c451da4238d05 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,6 +10,7 @@ plugins { id 'kotlin-allopen' id 'kotlin-parcelize' id 'jacoco' + alias libs.plugins.compose.compiler } jacoco { @@ -73,7 +74,7 @@ tasks.withType(Test).configureEach { } android { - compileSdk = 35 + compileSdk = 36 defaultConfig { applicationId = "foundation.e.apps" @@ -178,6 +179,7 @@ android { buildFeatures { buildConfig = true viewBinding = true + compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_21 @@ -343,6 +345,26 @@ dependencies { // JSoup implementation(libs.jsoup) + + // Compose + def composeBom = platform(libs.compose.bom) + implementation composeBom + androidTestImplementation composeBom + + implementation libs.compose.material3 + implementation libs.compose.material.icons.extended + + implementation libs.activity.compose + implementation libs.lifecycle.viewmodel.compose + implementation libs.runtime.livedata + + // Android Studio Preview support for Compose + implementation libs.compose.ui.tooling.preview + debugImplementation libs.compose.ui.tooling + + // UI Tests for Compose + androidTestImplementation libs.compose.ui.test.junit4 + debugImplementation libs.compose.ui.test.manifest } def retrieveKey(String keyName, String defaultValue) { diff --git a/app/src/main/java/foundation/e/apps/data/DownloadManager.kt b/app/src/main/java/foundation/e/apps/data/DownloadManager.kt index df1049b47e9ea1fdb4dde3b0d06cbb1050c950ac..b20650f4f9d1273ea3f1c3ce80f438ea3f936efd 100644 --- a/app/src/main/java/foundation/e/apps/data/DownloadManager.kt +++ b/app/src/main/java/foundation/e/apps/data/DownloadManager.kt @@ -21,6 +21,7 @@ import android.app.DownloadManager import android.content.Context import android.database.Cursor import android.net.Uri +import androidx.core.net.toUri import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.OpenForTesting import foundation.e.apps.R @@ -78,7 +79,7 @@ class DownloadManager @Inject constructor( ): Long { var downloadId = -1L try { - val request = DownloadManager.Request(Uri.parse(url)) + val request = DownloadManager.Request(url.toUri()) .setTitle(context.getString(R.string.downloading)) .setDestinationUri(Uri.fromFile(downloadFile)) if (appLoungePreference.isOnlyUnmeteredNetworkEnabled()) { diff --git a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt index 0ac131503fee56ba9aad38a7e1be9ce8d21e0db9..aa5767318247da566778e070b8590ff53b278667 100644 --- a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt +++ b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt @@ -19,6 +19,7 @@ package foundation.e.apps.data.application.data import android.net.Uri +import androidx.core.net.toUri import com.aurora.gplayapi.Constants.Restriction import com.aurora.gplayapi.data.models.ContentRating import com.google.gson.annotations.SerializedName @@ -109,10 +110,10 @@ data class Application( val Application.shareUri: Uri get() = when (type) { - PWA -> Uri.parse(url) + PWA -> url.toUri() NATIVE -> when { isFDroidApp -> buildFDroidUri(package_name) - else -> Uri.parse(shareUrl) + else -> shareUrl.toUri() } } diff --git a/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt b/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt index 2899e605b53b0fe705771ded1334e7e2be55dc9e..b7ff07e81d350879c90c5d16d7bbe5beb3659079 100644 --- a/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt @@ -24,6 +24,7 @@ import android.app.NotificationManager import android.content.Context import android.net.Uri import android.os.Environment +import androidx.core.net.toUri import androidx.lifecycle.LiveData import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R @@ -236,7 +237,7 @@ class AppManagerImpl @Inject constructor( } else { context.getString(R.string.additional_file_for, appInstall.name) } - val request = DownloadManager.Request(Uri.parse(it)) + val request = DownloadManager.Request(it.toUri()) .setTitle(requestTitle) .setDestinationUri(Uri.fromFile(packagePath)) diff --git a/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt b/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt index c1b63a1093cef351455130be3e819ca0600262bc..649318dcc7f5556e8095aab3c0842031cec74ec4 100644 --- a/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt +++ b/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt @@ -7,10 +7,10 @@ import android.content.Intent import android.database.Cursor import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.net.Uri import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat +import androidx.core.net.toUri import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.OpenForTesting import foundation.e.apps.data.application.data.Application @@ -64,7 +64,7 @@ class PwaManager @Inject constructor( */ fun getPwaStatus(application: Application): Status { context.contentResolver.query( - Uri.parse(PWA_PLAYER), + PWA_PLAYER.toUri(), null, null, null, @@ -106,7 +106,7 @@ class PwaManager @Inject constructor( */ fun launchPwa(application: Application) { val launchIntent = Intent(VIEW_PWA).apply { - data = Uri.parse(application.url) + data = application.url.toUri() putExtra(PWA_ID, application.pwaPlayerDbId) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS) @@ -137,7 +137,7 @@ class PwaManager @Inject constructor( put(ICON, iconByteArray) } - context.contentResolver.insert(Uri.parse(PWA_PLAYER), values)?.let { + context.contentResolver.insert(PWA_PLAYER.toUri(), values)?.let { val databaseID = ContentUris.parseId(it) publishShortcut(appInstall, iconBitmap, databaseID) } @@ -170,7 +170,7 @@ class PwaManager @Inject constructor( val intent = Intent().apply { action = VIEW_PWA - data = Uri.parse(appInstall.downloadURLList[0]) + data = appInstall.downloadURLList[0].toUri() putExtra(PWA_NAME, appInstall.name) putExtra(PWA_ID, databaseID) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) diff --git a/app/src/main/java/foundation/e/apps/ui/compose/theme/AppTheme.kt b/app/src/main/java/foundation/e/apps/ui/compose/theme/AppTheme.kt new file mode 100644 index 0000000000000000000000000000000000000000..f7e7dea080bbd61ca3fce5afac51ee260d692608 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/ui/compose/theme/AppTheme.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program 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 3 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see . + * + */ +package foundation.e.apps.ui.compose.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.colorResource +import foundation.e.elib.R as eR + +@Composable +fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { + val colorScheme = if (darkTheme) { + darkColorScheme( + primary = colorResource(eR.color.e_action_bar_dark), + secondary = colorResource(eR.color.e_action_bar_dark), + tertiary = colorResource(eR.color.e_accent_dark), + background = colorResource(eR.color.e_background_dark), + surface = colorResource(eR.color.e_floating_background_dark), + onPrimary = colorResource(eR.color.e_primary_text_color_dark), + onSecondary = colorResource(eR.color.e_primary_text_color_light), + onBackground = colorResource(eR.color.e_primary_text_color_dark), + onSurface = colorResource(eR.color.e_primary_text_color_dark), + ) + } else { + lightColorScheme( + primary = colorResource(eR.color.e_action_bar_light), + secondary = colorResource(eR.color.e_action_bar_light), + tertiary = colorResource(eR.color.e_accent_light), + background = colorResource(eR.color.e_background_light), + surface = colorResource(eR.color.e_floating_background_light), + onPrimary = colorResource(eR.color.e_primary_text_color_light), + onSecondary = colorResource(eR.color.e_primary_text_color_dark), + onBackground = colorResource(eR.color.e_primary_text_color_light), + onSurface = colorResource(eR.color.e_primary_text_color_light), + ) + } + + MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content) +} diff --git a/app/src/main/java/foundation/e/apps/ui/compose/theme/Typography.kt b/app/src/main/java/foundation/e/apps/ui/compose/theme/Typography.kt new file mode 100644 index 0000000000000000000000000000000000000000..363f5d87d04218e4a9421a6438d9776fe305143d --- /dev/null +++ b/app/src/main/java/foundation/e/apps/ui/compose/theme/Typography.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program 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 3 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see . + * + */ +package foundation.e.apps.ui.compose.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.Hyphens +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + ) +) + +val titleStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 20.sp, + lineHeight = 24.sp, + letterSpacing = 0.02.em, + hyphens = Hyphens.Auto, +) + +val summaryStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.01.em, + hyphens = Hyphens.Auto, +) + +val categoryStyle = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.01.em, +) diff --git a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt index 20876952480a6c8ac50015e24e9d01caf617df3d..9f5f31e68f4ef91014856d1954df4310756fbfe3 100644 --- a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt @@ -19,11 +19,11 @@ package foundation.e.apps.ui.parentFragment import android.content.Intent import android.graphics.Paint -import android.net.Uri import android.widget.TextView import androidx.annotation.LayoutRes import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog +import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner @@ -235,7 +235,7 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { private fun openTroubleshootingPage() { val troubleshootUrl = getString(R.string.troubleshootURL, Locale.getDefault().language) val openUrlIntent = Intent(Intent.ACTION_VIEW) - openUrlIntent.data = Uri.parse(troubleshootUrl) + openUrlIntent.data = troubleshootUrl.toUri() startActivity(openUrlIntent) } diff --git a/app/src/main/java/foundation/e/apps/ui/search/v2/SearchFragmentV2.kt b/app/src/main/java/foundation/e/apps/ui/search/v2/SearchFragmentV2.kt new file mode 100644 index 0000000000000000000000000000000000000000..c78d7ff04aa799952eae67b77274f81c91e85741 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/ui/search/v2/SearchFragmentV2.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program 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 3 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see . + * + */ + +package foundation.e.apps.ui.search.v2 + +import android.os.Bundle +import android.view.View +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import foundation.e.apps.R +import foundation.e.apps.ui.compose.theme.AppTheme + +class SearchFragmentV2 : Fragment(R.layout.fragment_search_v2) { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val composeView = view.findViewById(R.id.composeView) + composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + composeView.setContent { + AppTheme { } + } + } +} diff --git a/app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt b/app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt new file mode 100644 index 0000000000000000000000000000000000000000..3c178adec62aeb6f3c01d8b487a73044cb447d12 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program 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 3 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see . + * + */ + +package foundation.e.apps.ui.search.v2 + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class SearchViewModelV2 @Inject constructor() : ViewModel() diff --git a/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt b/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt index fb74c7f70e2a00351fc10a50315ccf5c06f10693..394d2824a782de1b3fe973c16e68fbaf37b67801 100644 --- a/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt @@ -21,10 +21,10 @@ package foundation.e.apps.ui.settings import android.content.ClipData import android.content.ClipboardManager import android.content.Intent -import android.net.Uri import android.os.Bundle import android.view.View import android.widget.Toast +import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.lifecycle.ViewModelProvider @@ -140,7 +140,7 @@ class SettingsFragment : PreferenceFragmentCompat() { } val troubleshootUrl = getString(R.string.troubleshootURL, Locale.getDefault().language) - troubleShootPreference?.intent = Intent(Intent.ACTION_VIEW, Uri.parse(troubleshootUrl)) + troubleShootPreference?.intent = Intent(Intent.ACTION_VIEW, troubleshootUrl.toUri()) } /** diff --git a/app/src/main/res/layout/fragment_search_v2.xml b/app/src/main/res/layout/fragment_search_v2.xml new file mode 100644 index 0000000000000000000000000000000000000000..eb9fbeab752a411de9fd7f15d8b286e826f7edb5 --- /dev/null +++ b/app/src/main/res/layout/fragment_search_v2.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/navigation/navigation_resource.xml b/app/src/main/res/navigation/navigation_resource.xml index 7746663611fc07819e5f3b0725cead47dba774e4..9de936fd205258cf7f3a176a347df1c47a7853ec 100644 --- a/app/src/main/res/navigation/navigation_resource.xml +++ b/app/src/main/res/navigation/navigation_resource.xml @@ -67,6 +67,11 @@ android:id="@+id/action_searchFragment_to_applicationFragment" app:destination="@id/applicationFragment" /> +