mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-06-17 12:19:52 -04:00
Merge branch '2.0' into 'master'
Release 2.0-alpha10 Closes #3278, #3276, #3273, #3269, #3254, and #3284 See merge request fdroid/fdroidclient!1673
This commit is contained in:
142
.gitlab-ci.yml
142
.gitlab-ci.yml
@@ -20,20 +20,17 @@ workflow:
|
||||
variables:
|
||||
JAVA_HOME: /usr/lib/jvm/java-21-openjdk-amd64
|
||||
before_script:
|
||||
- echo "org.gradle.caching=true" >> gradle.properties
|
||||
- test -e /etc/apt/sources.list.d/trixie-backports.list
|
||||
|| echo "deb http://deb.debian.org/debian trixie-backports main" >> /etc/apt/sources.list
|
||||
- apt update
|
||||
- apt-get -qy install -t trixie-backports --no-install-recommends git git-lfs sdkmanager openjdk-21-jdk-headless
|
||||
|
||||
- test -n "$ANDROID_HOME" || source /etc/profile.d/bsenv.sh
|
||||
|
||||
- export cmdline_tools_latest="$ANDROID_HOME/cmdline-tools/latest/bin"
|
||||
- test -e $cmdline_tools_latest && export PATH="$cmdline_tools_latest:$PATH"
|
||||
|
||||
- export GRADLE_USER_HOME=$PWD/.gradle
|
||||
- export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdk = "\([0-9][0-9]*\)".*,\1,p' gradle/libs.versions.toml`
|
||||
- echo y | sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null
|
||||
- sdkmanager --install
|
||||
"build-tools;${ANDROID_COMPILE_SDK}.0.0"
|
||||
"platform-tools"
|
||||
"platforms;android-${ANDROID_COMPILE_SDK}.0"
|
||||
# index-v1.jar tests need SHA1 support still, TODO use apksig to validate JAR sigs
|
||||
- sed -i 's,SHA1 denyAfter 20[0-9][0-9],SHA1 denyAfter 2027,'
|
||||
/usr/lib/jvm/java-21-openjdk-amd64/conf/security/java.security
|
||||
@@ -61,17 +58,11 @@ workflow:
|
||||
- app/build/reports
|
||||
- app/build/outputs/*ml
|
||||
- app/build/outputs/apk
|
||||
- legacy/core*
|
||||
- legacy/*.log
|
||||
- legacy/build/reports
|
||||
- legacy/build/outputs/*ml
|
||||
- legacy/build/outputs/apk
|
||||
- libs/*/build/reports
|
||||
- build/reports
|
||||
reports:
|
||||
junit:
|
||||
- app/build/**/TEST-*.xml
|
||||
- legacy/build/**/TEST-*.xml
|
||||
- libs/*/build/**/TEST-*.xml
|
||||
expire_in: 1 week
|
||||
when: on_failure
|
||||
@@ -95,9 +86,8 @@ app assembleRelease test:
|
||||
- changes:
|
||||
- app/**/*
|
||||
- libs/**/*
|
||||
- legacy/**/*
|
||||
script:
|
||||
- ./gradlew :app:assembleDefault :app:testFullDebugUnitTest :legacy:assemble :legacy:testFullDebugUnitTest
|
||||
- ./gradlew :app:assembleDefault :app:testFullDebugUnitTest
|
||||
artifacts:
|
||||
name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
|
||||
paths:
|
||||
@@ -105,12 +95,9 @@ app assembleRelease test:
|
||||
- app/build/outputs/apk
|
||||
- app/build/outputs/mapping
|
||||
- libs/*/build/reports
|
||||
- legacy/build/reports
|
||||
- legacy/build/outputs/apk
|
||||
reports:
|
||||
junit:
|
||||
- app/build/test-results/*/TEST-*.xml
|
||||
- legacy/build/test-results/*/TEST-*.xml
|
||||
expire_in: 1 week
|
||||
when: always
|
||||
|
||||
@@ -148,26 +135,9 @@ app lint:
|
||||
- <<: *always-on-these-changes
|
||||
- changes:
|
||||
- app/**/*
|
||||
- legacy/**/*
|
||||
script:
|
||||
# always report on lint errors to the build log
|
||||
- sed -i -e 's,textReport .*,textReport true,' legacy/build.gradle
|
||||
# the tasks "lint", "test", etc don't always include everything
|
||||
- ./gradlew :app:lint :app:ktfmtCheck :legacy:lint
|
||||
|
||||
legacy checkstyle:
|
||||
<<: *test-template
|
||||
stage: lint
|
||||
rules:
|
||||
- <<: *always-on-these-changes
|
||||
- changes:
|
||||
- legacy/**/*
|
||||
script:
|
||||
- ./gradlew :legacy:checkstyle
|
||||
- python3 tools/checkstyle-to-codeclimate.py --input legacy/build/reports/checkstyle/checkstyle.xml --output gl-checkstyle.json
|
||||
artifacts:
|
||||
reports:
|
||||
codequality: gl-checkstyle.json
|
||||
- ./gradlew :app:lint :app:ktfmtCheck
|
||||
|
||||
libs lint:
|
||||
<<: *test-template
|
||||
@@ -181,55 +151,6 @@ libs lint:
|
||||
- ./gradlew :libs:core:ktfmtCheck :libs:database:ktfmtCheck :libs:download:ktfmtCheck :libs:index:ktfmtCheck :libs:sharedTest:ktfmtCheck
|
||||
- ./gradlew checkLegacyAbi
|
||||
|
||||
# Reference: https://gitlab.com/components/code-quality-oss/codequality-os-scanners-integration/-/blob/4121970daed111dda84cab4547e1f2951684653c/templates/pmd.yml#L52-92
|
||||
legacy lint pmd:
|
||||
stage: lint
|
||||
image:
|
||||
name: registry.gitlab.com/gitlab-ci-utils/gitlab-pmd-cpd:latest
|
||||
entrypoint: [ "" ]
|
||||
rules:
|
||||
- <<: *always-on-these-changes
|
||||
- changes:
|
||||
- legacy/**/*
|
||||
parallel:
|
||||
matrix:
|
||||
- PMD_VARIANT: main
|
||||
PMD_RULESETS: "config/pmd/rules.xml,config/pmd/rules-main.xml"
|
||||
PMD_FILE_PATHS:
|
||||
- "legacy/src/main/java"
|
||||
- PMD_VARIANT: test
|
||||
PMD_RULESETS: "config/pmd/rules.xml,config/pmd/rules-test.xml"
|
||||
PMD_FILE_PATHS:
|
||||
- "legacy/src/test/java"
|
||||
- "legacy/src/androidTest/java"
|
||||
before_script:
|
||||
- apt-get update
|
||||
- apt-get -qy install --no-install-recommends jq
|
||||
script:
|
||||
- find ${PMD_FILE_PATHS[@]} -type f -name '*.java' ! -path '/vendored/*' > .pmd-files.txt
|
||||
- pmd check --file-list .pmd-files.txt -R ${PMD_RULESETS} -f codeclimate -r gl-code-quality-not-formatted.json --no-fail-on-violation
|
||||
after_script:
|
||||
## Fingerprint is required for reading Codequality: https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
|
||||
## PMD output does not contain fingerprint. Following snippet generates a fingerprint with md5sum for each code quality item and adds a comma at end of each line to format as JSON array
|
||||
## This returns true always. PMD rarely outputs codequality items which are not JSON conformant, because they are not properly escaped.
|
||||
## For example \w+ instead of \\w+ Therefore following snippet ignores those issues, so that all other lines are represented in the report
|
||||
- |
|
||||
sed 's/\\/\\\\/g' gl-code-quality-not-formatted.json | while IFS= read -r line; do
|
||||
fingerprint=$(echo -n "$line" | md5sum | awk '{print $1}');
|
||||
echo "$line" | jq -c --arg fp "$fingerprint" '. + {fingerprint: $fp}' | sed 's/$/,/';
|
||||
done > gl-pmd-${PMD_VARIANT}-report.json || true
|
||||
|
||||
# adds square bracket at the beginning for JSON array
|
||||
- sed -i '1s/^/[/' gl-pmd-${PMD_VARIANT}-report.json
|
||||
# adds square bracket at the end for JSON array
|
||||
- sed -i '$s/,$/]/' gl-pmd-${PMD_VARIANT}-report.json
|
||||
artifacts:
|
||||
paths:
|
||||
- gl-pmd-${PMD_VARIANT}-report.json
|
||||
reports:
|
||||
codequality: gl-pmd-${PMD_VARIANT}-report.json
|
||||
when: always
|
||||
|
||||
app screenshots:
|
||||
<<: *test-template
|
||||
stage: lint
|
||||
@@ -265,12 +186,9 @@ app tools scripts:
|
||||
- app/**/*
|
||||
script:
|
||||
- apt-get update
|
||||
- apt-get -qy install --no-install-recommends git python3
|
||||
- apt-get -qy install --no-install-recommends python3
|
||||
- ./tools/check-format-strings.py
|
||||
- ./tools/check-fastlane-whitespace.py
|
||||
- ./tools/remove-unused-and-blank-translations.py
|
||||
- echo "These are unused or blank translations that should be removed:"
|
||||
- git --no-pager diff --ignore-all-space --name-only --exit-code app/src/*/res/values*/strings.xml
|
||||
|
||||
app weblate merge conflict:
|
||||
stage: lint
|
||||
@@ -297,40 +215,18 @@ app weblate merge conflict:
|
||||
- git diff --exit-code
|
||||
- exit $EXITVALUE
|
||||
|
||||
legacy errorprone:
|
||||
extends: .base
|
||||
stage: lint
|
||||
rules:
|
||||
- <<: *always-on-these-changes
|
||||
- changes:
|
||||
- legacy/**/*
|
||||
script:
|
||||
- sed -i "s@plugins {@plugins{\nid 'net.ltgt.errorprone' version '3.1.0'@" legacy/build.gradle
|
||||
- cat config/errorprone.gradle >> legacy/build.gradle
|
||||
- ./gradlew -Dorg.gradle.dependency.verification=lenient :legacy:assembleDebug
|
||||
|
||||
libs database schema:
|
||||
<<: *test-template
|
||||
stage: lint
|
||||
image: debian:trixie-backports
|
||||
rules:
|
||||
- <<: *always-on-these-changes
|
||||
- changes:
|
||||
- libs/database/**/*
|
||||
variables:
|
||||
JAVA_HOME: /usr/lib/jvm/java-21-openjdk-amd64
|
||||
before_script:
|
||||
- apt-get update
|
||||
- apt-get -qy --no-install-recommends install openjdk-21-jdk-headless git git-lfs sdkmanager
|
||||
- export ANDROID_HOME=/opt/android-sdk
|
||||
- export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdk = "\([0-9][0-9]*\)".*,\1,p' gradle/libs.versions.toml`
|
||||
- sdkmanager "platforms;android-$ANDROID_COMPILE_SDK" "build-tools;$ANDROID_COMPILE_SDK.0.0"
|
||||
- sdkmanager "build-tools;34.0.0" # something (AGP?) still pulls in old build-tools
|
||||
- sdkmanager "build-tools;35.0.0" # something (AGP?) still pulls in old build-tools
|
||||
- git lfs install
|
||||
script:
|
||||
- ./gradlew :libs:database:kspDebugKotlin
|
||||
- git status
|
||||
- git --no-pager diff --exit-code
|
||||
cache: []
|
||||
|
||||
# Run the tests in the emulator. Each step is broken out to run on
|
||||
# its own since the CI runner can have limited RAM, and the emulator
|
||||
@@ -348,7 +244,6 @@ libs database schema:
|
||||
# Cache hits the storage limits in kvm runners quickly
|
||||
cache: []
|
||||
script:
|
||||
- ./gradlew assembleFullDebug
|
||||
- export AVD_SDK=`echo $CI_JOB_NAME | awk '{print $2}'`
|
||||
- export AVD_TAG=`echo $CI_JOB_NAME | awk '{print $3}'`
|
||||
- export AVD_ARCH=`echo $CI_JOB_NAME | awk '{print $4}'`
|
||||
@@ -357,15 +252,14 @@ libs database schema:
|
||||
|
||||
- $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager --verbose delete avd --name "$NAME_AVD"
|
||||
- export AVD="$AVD_PACKAGE"
|
||||
- echo y | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "$AVD"
|
||||
- $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "$AVD"
|
||||
- echo no | $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager --verbose create avd --name "$NAME_AVD" --package "$AVD" --device "pixel"
|
||||
- df -h
|
||||
- start-emulator.sh
|
||||
- adb shell am start -n org.fdroid.fdroid.debug/org.fdroid.fdroid.views.main.MainActivity
|
||||
- export FLAG="-Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest,androidx.test.filters.FlakyTest"
|
||||
- ./gradlew $FLAG :app:connectedBasicDebugAndroidTest :legacy:connectedFullDebugAndroidTest :libs:database:connectedCheck :libs:download:connectedAndroidTest :libs:index:connectedAndroidTest
|
||||
- ./gradlew $FLAG :app:connectedAndroidTest :libs:database:connectedCheck :libs:download:connectedAndroidTest :libs:index:connectedAndroidTest
|
||||
- export FLAG="-Pandroid.testInstrumentationRunnerArguments.annotation=androidx.test.filters.FlakyTest"
|
||||
- for i in {1..5}; do echo "$i" && ./gradlew $FLAG :app:connectedBasicDebugAndroidTest :legacy:connectedFullDebugAndroidTest :libs:database:connectedCheck :libs:download:connectedAndroidTest :libs:index:connectedAndroidTest && break; done || exit 137
|
||||
- for i in {1..5}; do echo "$i" && ./gradlew $FLAG :app:connectedAndroidTest :libs:database:connectedCheck :libs:download:connectedAndroidTest :libs:index:connectedAndroidTest && break; done || exit 137
|
||||
allow_failure:
|
||||
exit_codes: 137
|
||||
|
||||
@@ -424,15 +318,9 @@ deploy_nightly:
|
||||
app/src/main/res/values*/strings.xml
|
||||
# add this nightly repo as a enabled repo
|
||||
- jq --slurpfile new_dict config/nightly-repo/repo.json "[(\$new_dict[0] | .address = \"${CI_PROJECT_URL}-nightly/-/raw/master/fdroid/repo\")] + ." app/src/main/assets/default_repos.json | sponge app/src/main/assets/default_repos.json
|
||||
- sed -i -e '/<\/string-array>/d' -e '/<\/resources>/d' legacy/src/main/res/values/default_repos.xml
|
||||
- echo "<item>${CI_PROJECT_PATH}-nightly</item>" >> legacy/src/main/res/values/default_repos.xml
|
||||
- echo "<item>${CI_PROJECT_URL}-nightly/-/raw/master/fdroid/repo</item>" >> legacy/src/main/res/values/default_repos.xml
|
||||
- cat config/nightly-repo/repo.xml >> legacy/src/main/res/values/default_repos.xml
|
||||
- export DB=`sed -n 's,.*version *= *\([0-9][0-9]*\).*,\1,p' libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt`
|
||||
- export versionCode=`printf '%d%05d' $DB $(date '+%s'| cut -b1-8)`
|
||||
- sed -i "s,^\(\s*versionCode\) *[0-9].*,\1 $versionCode," legacy/build.gradle
|
||||
# build the APKs!
|
||||
- rm -rf app/build/outputs/apk legacy/build/outputs/apk
|
||||
- ./gradlew :app:assembleBasicNightly :legacy:assembleFullDebug
|
||||
- mv app/build/outputs/apk/basicNightly/release/app-basic-nightly-release-unsigned.apk app/build/outputs/apk/basicNightly/release/app-debug.apk
|
||||
- ./gradlew :app:assembleBasicNightly :app:assembleFullNightly
|
||||
- mv app/build/outputs/apk/basicNightly/release/app-basic-nightly-release-unsigned.apk app/build/outputs/apk/basicNightly/release/app-basic-debug.apk
|
||||
- mv app/build/outputs/apk/fullNightly/release/app-full-nightly-release-unsigned.apk app/build/outputs/apk/fullNightly/release/app-full-debug.apk
|
||||
- fdroid nightly -v
|
||||
|
||||
@@ -10,6 +10,8 @@ plugins {
|
||||
alias(libs.plugins.ktfmt)
|
||||
}
|
||||
|
||||
val nightlyVersionCode = (System.currentTimeMillis() / 1000 / 60).toInt()
|
||||
|
||||
android {
|
||||
namespace = "org.fdroid"
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
@@ -17,9 +19,9 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "org.fdroid"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 2000009
|
||||
versionName = "2.0-alpha9"
|
||||
targetSdk = 37
|
||||
versionCode = 2000010
|
||||
versionName = "2.0-alpha10"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -28,6 +30,7 @@ android {
|
||||
all { buildConfigField("String", "ACRA_REPORT_EMAIL", "\"reports@f-droid.org\"") }
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
getByName("debug") {
|
||||
@@ -49,7 +52,7 @@ android {
|
||||
create("default") { dimension = "release" }
|
||||
create("nightly") {
|
||||
dimension = "release"
|
||||
versionCode = (System.currentTimeMillis() / 1000 / 60).toInt()
|
||||
versionCode = nightlyVersionCode
|
||||
versionNameSuffix = "-$gitHash"
|
||||
applicationIdSuffix = ".nightly"
|
||||
}
|
||||
@@ -84,6 +87,10 @@ androidComponents {
|
||||
variantBuilder.enable = false
|
||||
}
|
||||
}
|
||||
// only needed while basic flavor has its own version code
|
||||
onVariants(selector().withFlavor("release" to "nightly")) { variant ->
|
||||
variant.outputs.forEach { output -> output.versionCode.set(nightlyVersionCode) }
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -102,6 +109,7 @@ dependencies {
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.compose.material3.adaptive.navigation)
|
||||
implementation(libs.androidx.compose.material3.adaptive.navigation3)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
@@ -132,7 +140,7 @@ dependencies {
|
||||
ksp(libs.hilt.android.compiler)
|
||||
ksp(libs.androidx.hilt.compiler)
|
||||
// https://github.com/google/dagger/issues/5001
|
||||
ksp("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0")
|
||||
ksp("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.21")
|
||||
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
@@ -4,7 +4,7 @@ import org.fdroid.LocaleChooser.getBestLocale
|
||||
import org.fdroid.download.NetworkState
|
||||
import org.fdroid.install.InstallState
|
||||
import org.fdroid.ui.details.AppDetails
|
||||
import org.fdroid.ui.details.AppDetailsItem
|
||||
import org.fdroid.ui.details.LoadedAppDetailsItem
|
||||
import org.fdroid.ui.details.VersionItem
|
||||
import org.fdroid.ui.utils.getAppDetailsActions
|
||||
import org.fdroid.ui.utils.testVersion1
|
||||
@@ -23,7 +23,7 @@ class DetailsScreenshotTest(localeName: String) : LocalizedScreenshotTest(locale
|
||||
fun appDetails() =
|
||||
screenshotTest("4_Details", showBottomBar = false, dark = true) { localeList ->
|
||||
val item =
|
||||
AppDetailsItem(
|
||||
LoadedAppDetailsItem(
|
||||
app = appMetadata,
|
||||
actions = getAppDetailsActions(),
|
||||
installState = InstallState.Unknown,
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -36,6 +36,7 @@
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
|
||||
|
||||
<uses-permission-sdk-23
|
||||
android:name="android.permission.ACCESS_COARSE_LOCATION"
|
||||
|
||||
@@ -78,7 +78,7 @@ public class BluetoothManager {
|
||||
* so make sure {@link BroadcastReceiver}s handle duplicates.
|
||||
*/
|
||||
public static void start(final Context context) {
|
||||
if (checkSelfPermission(context, BLUETOOTH_CONNECT) != PERMISSION_GRANTED &&
|
||||
if (checkSelfPermission(context, BLUETOOTH_CONNECT) != PERMISSION_GRANTED ||
|
||||
checkSelfPermission(context, BLUETOOTH_SCAN) != PERMISSION_GRANTED) {
|
||||
// TODO we either throw away that Bluetooth code or properly request permissions
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.fdroid.fdroid.nearby;
|
||||
|
||||
import static android.Manifest.permission.ACCESS_LOCAL_NETWORK;
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
|
||||
import static org.fdroid.fdroid.nearby.SwapService.ACTION_REQUEST_SWAP;
|
||||
|
||||
@@ -149,6 +152,11 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
if (isGranted) sendFDroidBluetooth();
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<String> requestLocalAccessLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
|
||||
if (isGranted) sendFDroidBluetooth();
|
||||
});
|
||||
|
||||
public static void requestSwap(Context context, String repo) {
|
||||
requestSwap(context, Uri.parse(repo));
|
||||
}
|
||||
@@ -1092,18 +1100,25 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
|
||||
wifiSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
Context context = getApplicationContext();
|
||||
if (isChecked) {
|
||||
boolean granted = SDK_INT < 37 ||
|
||||
ContextCompat.checkSelfPermission(context, ACCESS_LOCAL_NETWORK) == PERMISSION_GRANTED;
|
||||
if (isChecked && !granted) {
|
||||
requestLocalAccessLauncher.launch(ACCESS_LOCAL_NETWORK);
|
||||
Toast.makeText(this, R.string.swap_bluetooth_permissions, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
boolean activate = isChecked && granted;
|
||||
if (activate) {
|
||||
if (wifiApControl != null && wifiApControl.isEnabled()) {
|
||||
setupWifiAP();
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT <= 28) {
|
||||
if (SDK_INT <= 28) {
|
||||
wifiManager.setWifiEnabled(true);
|
||||
}
|
||||
}
|
||||
BonjourManager.start(context);
|
||||
}
|
||||
BonjourManager.setVisible(context, isChecked);
|
||||
SwapService.putWifiVisibleUserPreference(isChecked);
|
||||
BonjourManager.setVisible(context, activate);
|
||||
SwapService.putWifiVisibleUserPreference(activate);
|
||||
});
|
||||
|
||||
scanQrButton.setOnClickListener(v -> inflateSwapView(R.layout.swap_wifi_qr));
|
||||
@@ -1143,7 +1158,12 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
break;
|
||||
case BonjourManager.STATUS_STARTED:
|
||||
textWifiVisible.setText(R.string.swap_not_visible_wifi);
|
||||
peopleNearbyText.setText(R.string.swap_scanning_for_peers);
|
||||
if (SDK_INT < 37 ||
|
||||
ContextCompat.checkSelfPermission(context, ACCESS_LOCAL_NETWORK) == PERMISSION_GRANTED) {
|
||||
peopleNearbyText.setText(R.string.swap_scanning_for_peers);
|
||||
} else {
|
||||
peopleNearbyText.setText(R.string.swap_bluetooth_permissions);
|
||||
}
|
||||
peopleNearbyText.setVisibility(View.VISIBLE);
|
||||
peopleNearbyProgress.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
@@ -1152,7 +1172,12 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
break;
|
||||
case BonjourManager.STATUS_NOT_VISIBLE:
|
||||
textWifiVisible.setText(R.string.swap_not_visible_wifi);
|
||||
peopleNearbyText.setText(R.string.swap_scanning_for_peers);
|
||||
if (SDK_INT < 37 ||
|
||||
ContextCompat.checkSelfPermission(context, ACCESS_LOCAL_NETWORK) == PERMISSION_GRANTED) {
|
||||
peopleNearbyText.setText(R.string.swap_scanning_for_peers);
|
||||
} else {
|
||||
peopleNearbyText.setText(R.string.swap_bluetooth_permissions);
|
||||
}
|
||||
peopleNearbyText.setVisibility(View.VISIBLE);
|
||||
peopleNearbyProgress.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
@@ -1162,7 +1187,12 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
||||
} else {
|
||||
textWifiVisible.setText(R.string.swap_visible_wifi);
|
||||
}
|
||||
peopleNearbyText.setText(R.string.swap_scanning_for_peers);
|
||||
if (SDK_INT < 37 ||
|
||||
ContextCompat.checkSelfPermission(context, ACCESS_LOCAL_NETWORK) == PERMISSION_GRANTED) {
|
||||
peopleNearbyText.setText(R.string.swap_scanning_for_peers);
|
||||
} else {
|
||||
peopleNearbyText.setText(R.string.swap_bluetooth_permissions);
|
||||
}
|
||||
peopleNearbyText.setVisibility(View.VISIBLE);
|
||||
peopleNearbyProgress.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
BIN
app/src/full/res/mipmap-xhdpi/ic_banner.png
Normal file
BIN
app/src/full/res/mipmap-xhdpi/ic_banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -161,6 +161,14 @@
|
||||
<action android:name="android.intent.action.UNARCHIVE_PACKAGE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name="org.fdroid.install.AppUpdateReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -56,7 +56,10 @@
|
||||
"https://opencolo.mm.fcix.net/fdroid/archive",
|
||||
"https://plug-mirror.rcac.purdue.edu/fdroid/archive",
|
||||
"https://mirror.init7.net/fdroid/archive",
|
||||
"https://mirror.freedif.org/fdroid/archive"
|
||||
"https://mirror.freedif.org/fdroid/archive",
|
||||
"https://mirrors.hostico.ro/fdroid/archive",
|
||||
"https://ftp.lug.ro/fdroid/archive",
|
||||
"https://mirrors.chroot.ro/fdroid/archive"
|
||||
],
|
||||
"description": "The archive repository of the F-Droid client. This contains older versions of applications from the main repository.",
|
||||
"certificate": "3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef",
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
[
|
||||
"app.comaps.fdroid",
|
||||
"app.organicmaps",
|
||||
"at.bitfire.davdroid",
|
||||
"ch.protonvpn.android",
|
||||
"com.artifex.mupdf.mini.app",
|
||||
"com.artifex.mupdf.viewer.app",
|
||||
"com.aurora.store",
|
||||
"com.duckduckgo.mobile.android",
|
||||
"com.foobnix.pro.pdf.reader",
|
||||
"com.fsck.k9",
|
||||
"com.github.andreyasadchy.xtra",
|
||||
"com.github.libretube",
|
||||
"com.inspiredandroid.linuxcommandbibliotheca",
|
||||
"com.junkfood.seal",
|
||||
"com.kunzisoft.keepass.libre",
|
||||
@@ -17,31 +16,32 @@
|
||||
"com.machiav3lli.fdroid",
|
||||
"com.maxrave.simpmusic",
|
||||
"com.nextcloud.client",
|
||||
"com.nononsenseapps.feeder",
|
||||
"com.termux",
|
||||
"com.termux.api",
|
||||
"com.unciv.app",
|
||||
"de.danoeh.antennapod",
|
||||
"de.markusfisch.android.binaryeye",
|
||||
"de.schildbach.oeffi",
|
||||
"de.tutao.tutanota",
|
||||
"deckers.thibault.aves.libre",
|
||||
"dev.imranr.obtainium.fdroid",
|
||||
"eu.faircode.email",
|
||||
"eu.siacs.conversations",
|
||||
"helium314.keyboard",
|
||||
"InfinityLoop1309.NewPipeEnhanced",
|
||||
"io.element.android.x",
|
||||
"jp.nonbili.noutube",
|
||||
"net.cozic.joplin",
|
||||
"net.osmand.plus",
|
||||
"net.thunderbird.android",
|
||||
"net.typeblog.shelter",
|
||||
"org.adaway",
|
||||
"org.breezyweather",
|
||||
"org.documentfoundation.libreoffice",
|
||||
"org.fairscan.app",
|
||||
"org.fdroid.fdroid",
|
||||
"org.fossify.calendar",
|
||||
"org.fossify.filemanager",
|
||||
"org.fossify.gallery",
|
||||
"org.fossify.messages",
|
||||
"org.fossify.phone",
|
||||
"org.kde.kdeconnect_tp",
|
||||
"org.mozilla.fennec_fdroid",
|
||||
"org.schabi.newpipe",
|
||||
|
||||
@@ -31,6 +31,7 @@ class MainActivity : AppCompatActivity() {
|
||||
val requestPermissionLauncher = registerForActivityResult(RequestPermission()) {}
|
||||
|
||||
@Inject lateinit var settingsManager: SettingsManager
|
||||
@Inject lateinit var notificationManager: NotificationManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -78,4 +79,9 @@ class MainActivity : AppCompatActivity() {
|
||||
// calling super seems to be needed, so the IntentListener gets informed
|
||||
super.onNewIntent(intent, caller)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
notificationManager.cancelSelfUpdateNotification()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,14 @@ import androidx.annotation.StringRes
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationCompat.BigTextStyle
|
||||
import androidx.core.app.NotificationCompat.CATEGORY_REMINDER
|
||||
import androidx.core.app.NotificationCompat.CATEGORY_SERVICE
|
||||
import androidx.core.app.NotificationCompat.PRIORITY_HIGH
|
||||
import androidx.core.app.NotificationCompat.PRIORITY_MAX
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT
|
||||
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW
|
||||
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_MAX
|
||||
import androidx.core.content.ContextCompat.checkSelfPermission
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
@@ -40,10 +43,12 @@ constructor(@param:ApplicationContext private val context: Context) {
|
||||
const val NOTIFICATION_ID_APP_INSTALLS: Int = 1
|
||||
const val NOTIFICATION_ID_APP_INSTALL_SUCCESS: Int = 2
|
||||
const val NOTIFICATION_ID_APP_UPDATES_AVAILABLE: Int = 3
|
||||
const val NOTIFICATION_ID_SELF_UPDATE: Int = 4
|
||||
private const val CHANNEL_UPDATES = "update-channel"
|
||||
private const val CHANNEL_INSTALLS = "install-channel"
|
||||
private const val CHANNEL_INSTALL_SUCCESS = "install-success-channel"
|
||||
private const val CHANNEL_UPDATES_AVAILABLE = "updates-available-channel"
|
||||
private const val CHANNEL_SELF_UPDATE = "self-update-channel"
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -69,6 +74,10 @@ constructor(@param:ApplicationContext private val context: Context) {
|
||||
.setName(s(R.string.notification_channel_updates_available_title))
|
||||
.setDescription(s(R.string.notification_channel_updates_available_description))
|
||||
.build(),
|
||||
NotificationChannelCompat.Builder(CHANNEL_SELF_UPDATE, IMPORTANCE_MAX)
|
||||
.setName(s(R.string.notification_channel_self_update_title))
|
||||
.setDescription(s(R.string.notification_channel_self_update_description))
|
||||
.build(),
|
||||
)
|
||||
nm.createNotificationChannelsCompat(channels)
|
||||
}
|
||||
@@ -181,6 +190,28 @@ constructor(@param:ApplicationContext private val context: Context) {
|
||||
return builder
|
||||
}
|
||||
|
||||
fun showSelfUpdateNotification() {
|
||||
val pi = getMyAppsPendingIntent(context)
|
||||
val app = context.getString(R.string.app_name)
|
||||
val title = context.getString(R.string.notification_self_update_title, app)
|
||||
val builder =
|
||||
NotificationCompat.Builder(context, CHANNEL_SELF_UPDATE)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setCategory(CATEGORY_REMINDER)
|
||||
.setContentTitle(title)
|
||||
.setPriority(PRIORITY_MAX)
|
||||
.setContentIntent(pi)
|
||||
.setAutoCancel(true)
|
||||
val n = builder.build()
|
||||
if (checkSelfPermission(context, POST_NOTIFICATIONS) == PERMISSION_GRANTED) {
|
||||
nm.notify(NOTIFICATION_ID_SELF_UPDATE, n)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelSelfUpdateNotification() {
|
||||
nm.cancel(NOTIFICATION_ID_SELF_UPDATE)
|
||||
}
|
||||
|
||||
private fun getMainActivityPendingIntent(context: Context): PendingIntent {
|
||||
val i = Intent(ACTION_MAIN).apply { setClass(context, MainActivity::class.java) }
|
||||
val flags = FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package org.fdroid.download
|
||||
|
||||
import android.content.Context
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import org.fdroid.settings.SettingsConstants
|
||||
import org.fdroid.settings.SettingsManager
|
||||
import org.fdroid.utils.getCurrentLocation
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
@Singleton
|
||||
@@ -24,7 +22,7 @@ constructor(
|
||||
override fun cacheMirrorIpAddresses(
|
||||
mirrorUrl: String,
|
||||
ipv4Addresses: List<String>,
|
||||
ipv6Addresses: List<String>
|
||||
ipv6Addresses: List<String>,
|
||||
) {
|
||||
dnsWithCache.populateCacheWithStrings(mirrorUrl, ipv4Addresses, ipv6Addresses)
|
||||
}
|
||||
@@ -42,13 +40,5 @@ constructor(
|
||||
return settingsManager.mirrorChooser == SettingsConstants.MirrorChooserValues.PreferForeign
|
||||
}
|
||||
|
||||
override fun getCurrentLocation(): String {
|
||||
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
|
||||
return tm.simCountryIso
|
||||
?: tm.networkCountryIso
|
||||
?: run {
|
||||
val localeList = LocaleListCompat.getDefault()
|
||||
localeList.get(0)?.country ?: Locale.getDefault().country
|
||||
}
|
||||
}
|
||||
override fun getCurrentLocation(): String = getCurrentLocation(context)
|
||||
}
|
||||
|
||||
@@ -488,6 +488,23 @@ constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun clearInstallingApps() {
|
||||
apps.update { oldApps ->
|
||||
oldApps.toMutableMap().apply {
|
||||
val iterator = entries.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val app = iterator.next()
|
||||
if (!app.value.showProgress) {
|
||||
val packageName = app.key
|
||||
jobs.remove(packageName)?.cancel()
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun cleanUp(packageName: String) {
|
||||
val state = apps.value[packageName] ?: return
|
||||
|
||||
44
app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt
Normal file
44
app/src/main/kotlin/org/fdroid/install/AppUpdateReceiver.kt
Normal file
@@ -0,0 +1,44 @@
|
||||
package org.fdroid.install
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.ACTION_MY_PACKAGE_REPLACED
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import mu.KotlinLogging
|
||||
import org.fdroid.NotificationManager
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AppUpdateReceiver : BroadcastReceiver() {
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Inject lateinit var notificationManager: NotificationManager
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != ACTION_MY_PACKAGE_REPLACED) {
|
||||
log.warn { "Unknown action: ${intent.action}" }
|
||||
return
|
||||
}
|
||||
log.info { "Intent received, we just updated ourselves!" }
|
||||
val intent =
|
||||
context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
|
||||
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
if (intent == null) {
|
||||
log.error { "Could not get launch intent for ourselves" }
|
||||
} else {
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
log.error(e) { "Failed to start activity after update" }
|
||||
}
|
||||
}
|
||||
// show notification on Android 10+, because we aren't allowed to start activity from background
|
||||
// see: https://developer.android.com/guide/components/activities/background-starts
|
||||
if (SDK_INT >= 29) notificationManager.showSelfUpdateNotification()
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@ package org.fdroid.install
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.EXTRA_ARCHIVAL
|
||||
import android.content.Intent.EXTRA_REPLACING
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.GET_SIGNATURES
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
import androidx.core.content.ContextCompat.registerReceiver
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
@@ -88,10 +90,14 @@ constructor(
|
||||
|
||||
private fun onPackageRemoved(intent: Intent) {
|
||||
val replacing = intent.getBooleanExtra(EXTRA_REPLACING, false)
|
||||
log.info { "onPackageRemoved($intent) ${intent.data} replacing: $replacing" }
|
||||
val archival = if (SDK_INT >= 35) intent.getBooleanExtra(EXTRA_ARCHIVAL, false) else false
|
||||
log.info {
|
||||
"onPackageRemoved($intent) ${intent.data} replacing: $replacing ${intent.extras?.keySet()?.toList()}"
|
||||
}
|
||||
val packageName =
|
||||
intent.data?.schemeSpecificPart ?: error("No package name in ACTION_PACKAGE_REMOVED")
|
||||
if (!replacing)
|
||||
if (!replacing || archival) {
|
||||
_installedApps.update { apps -> apps.toMutableMap().apply { remove(packageName) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import android.content.pm.PackageInstaller.SessionParams
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.icu.util.ULocale
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.WorkerThread
|
||||
@@ -25,6 +26,7 @@ import androidx.core.content.ContextCompat.registerReceiver
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -38,6 +40,7 @@ import org.fdroid.database.AppMetadata
|
||||
import org.fdroid.index.v2.PackageVersion
|
||||
import org.fdroid.ui.utils.isAppInForeground
|
||||
import org.fdroid.utils.IoDispatcher
|
||||
import org.fdroid.utils.isChina
|
||||
|
||||
@Singleton
|
||||
class SessionInstallManager
|
||||
@@ -70,8 +73,24 @@ constructor(
|
||||
// https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setRequireUserAction(int)
|
||||
// https://cs.android.com/android/platform/superproject/+/android-16.0.0_r2:frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java;l=329;drc=73caa0299d9196ddeefe4f659f557fb880f6536d
|
||||
// current code requires targetSdk 34 on SDK 36+
|
||||
// SDK 37 beta 4 also auto updates targetSdk 34 apps
|
||||
return SDK_INT >= 36 && targetSdk >= 34
|
||||
}
|
||||
|
||||
/**
|
||||
* Many Chinese ROMs have as of 2026 not properly implemented pre-approval. They either return
|
||||
* `3: INSTALL_FAILED_ABORTED: User rejected permissions` or not return anything at all. Since
|
||||
* it is hard to handle this and Chinese ROMs can't easily be detected, we turn pre-approval off
|
||||
* for all devices that are likely affected by this.
|
||||
*
|
||||
* See: https://gitlab.com/fdroid/fdroidclient/-/work_items/3254
|
||||
*/
|
||||
fun isPreApprovalLikelyBroken(context: Context): Boolean {
|
||||
val localeList = LocaleListCompat.getDefault()
|
||||
val country = localeList.get(0)?.country ?: Locale.getDefault().country
|
||||
return (isChina(context) || country.equals("cn", ignoreCase = true)) &&
|
||||
Build.TYPE != "userdebug"
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -104,6 +123,9 @@ constructor(
|
||||
// should not be needed, so we say not supported
|
||||
log.info { "Can do auto-update pre-approval for ${app.packageName} not needed." }
|
||||
PreApprovalResult.NotSupported
|
||||
} else if (isPreApprovalLikelyBroken(context)) {
|
||||
log.info { "Device is in China and not a userdebug build, so pre-approval is likely broken." }
|
||||
PreApprovalResult.NotSupported
|
||||
} else if (SDK_INT >= 34) {
|
||||
log.info { "Requesting pre-approval for ${app.packageName}..." }
|
||||
try {
|
||||
|
||||
@@ -23,10 +23,12 @@ import org.fdroid.download.DownloaderFactory
|
||||
import org.fdroid.index.IndexUpdateResult
|
||||
import org.fdroid.index.RepoManager
|
||||
import org.fdroid.index.RepoUpdater
|
||||
import org.fdroid.settings.SettingsConstants
|
||||
import org.fdroid.settings.SettingsManager
|
||||
import org.fdroid.updates.UpdatesManager
|
||||
|
||||
private const val MIN_UPDATE_INTERVAL_MILLIS = 15_000
|
||||
private const val MAX_UPDATE_INTERVAL_MILLIS = 12 * 60 * 60 * 1000L // 12 hours
|
||||
|
||||
@Singleton
|
||||
class RepoUpdateManager
|
||||
@@ -87,6 +89,17 @@ internal constructor(
|
||||
workInfo?.nextScheduleTimeMillis ?: Long.MAX_VALUE
|
||||
}
|
||||
|
||||
init {
|
||||
log.info { "RepoUpdateManager initialized" }
|
||||
if (settingsManager.repoUpdates == SettingsConstants.AutoUpdateValues.OnlyWhenOpenApp) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - settingsManager.lastRepoUpdate > MAX_UPDATE_INTERVAL_MILLIS) {
|
||||
log.info { "Last repo update was more than 12h ago, triggering update..." }
|
||||
RepoUpdateWorker.updateNow(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all enabled repositories.
|
||||
*
|
||||
|
||||
@@ -74,7 +74,7 @@ constructor(
|
||||
@JvmStatic
|
||||
fun scheduleOrCancel(context: Context, autoUpdate: AutoUpdateValues) {
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
if (autoUpdate != AutoUpdateValues.Never) {
|
||||
if (autoUpdate.workerEnabled) {
|
||||
Log.i(TAG, "scheduleOrCancel: enqueueUniquePeriodicWork")
|
||||
val networkType =
|
||||
if (autoUpdate == AutoUpdateValues.Always) {
|
||||
|
||||
@@ -17,10 +17,11 @@ object SettingsConstants {
|
||||
const val PREF_KEY_DYNAMIC_COLORS = "dynamicColors"
|
||||
const val PREF_DEFAULT_DYNAMIC_COLORS = false
|
||||
|
||||
enum class AutoUpdateValues {
|
||||
OnlyWifi,
|
||||
Always,
|
||||
Never,
|
||||
enum class AutoUpdateValues(val workerEnabled: Boolean) {
|
||||
OnlyWifi(true),
|
||||
Always(true),
|
||||
OnlyWhenOpenApp(false),
|
||||
Never(false),
|
||||
}
|
||||
|
||||
const val PREF_KEY_REPO_UPDATES = "repoAutoUpdates"
|
||||
|
||||
@@ -158,7 +158,6 @@ class SettingsManager @Inject constructor(@param:ApplicationContext private val
|
||||
prefs.getString(PREF_KEY_MIRROR_CHOOSER, PREF_DEFAULT_MIRROR_CHOOSER).toMirrorChooserValue()
|
||||
|
||||
val proxyConfig: ProxyConfig?
|
||||
@UiThread
|
||||
get() {
|
||||
val proxyStr = prefs.getString(PREF_KEY_PROXY, PREF_DEFAULT_PROXY)
|
||||
return if (proxyStr.isNullOrBlank()) null
|
||||
|
||||
@@ -21,7 +21,9 @@ data class MyAppsModel(
|
||||
val networkState: NetworkState,
|
||||
val isSearching: Boolean = false,
|
||||
val appUpdatesBytes: Long? = null,
|
||||
)
|
||||
) {
|
||||
val showClearInstallingAppsButton: Boolean = installingApps.all { !it.installState.showProgress }
|
||||
}
|
||||
|
||||
interface MyAppsActions {
|
||||
fun updateAll()
|
||||
@@ -32,6 +34,8 @@ interface MyAppsActions {
|
||||
|
||||
fun confirmAppInstall(packageName: String, state: InstallConfirmationState)
|
||||
|
||||
fun clearInstallingApps()
|
||||
|
||||
fun ignoreAppIssue(item: AppWithIssueItem)
|
||||
|
||||
fun onUpdatesHintSeen()
|
||||
|
||||
@@ -34,6 +34,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.fdroid.R
|
||||
import org.fdroid.database.NotAvailable
|
||||
import org.fdroid.install.InstallState
|
||||
import org.fdroid.ui.FDroidContent
|
||||
import org.fdroid.ui.utils.MeteredConnectionDialog
|
||||
import org.fdroid.ui.utils.OfflineBar
|
||||
@@ -139,11 +140,26 @@ fun MyAppsList(
|
||||
// Apps currently installing header
|
||||
if (installingApps.isNotEmpty()) {
|
||||
item(key = "B", contentType = "header") {
|
||||
Text(
|
||||
text = stringResource(R.string.notification_title_summary_installing),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text =
|
||||
if (myAppsInfo.model.showClearInstallingAppsButton) {
|
||||
stringResource(R.string.my_apps_header_recently_installed_apps)
|
||||
} else {
|
||||
stringResource(R.string.notification_title_summary_installing)
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(16.dp).weight(1f),
|
||||
)
|
||||
if (myAppsInfo.model.showClearInstallingAppsButton) {
|
||||
TextButton(
|
||||
onClick = myAppsInfo.actions::clearInstallingApps,
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
) {
|
||||
Text(stringResource(R.string.clear_all))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// List of currently installing apps
|
||||
items(items = installingApps, key = { it.packageName }, contentType = { "B" }) { app ->
|
||||
@@ -157,7 +173,7 @@ fun MyAppsList(
|
||||
onClick = { onAppItemClick(app.packageName) },
|
||||
)
|
||||
}
|
||||
val modifier = Modifier.Companion.animateItem().then(interactionModifier)
|
||||
val modifier = Modifier.animateItem().then(interactionModifier)
|
||||
InstallingAppRow(app, isSelected, modifier)
|
||||
}
|
||||
}
|
||||
@@ -288,7 +304,12 @@ private fun Preview() {
|
||||
getMyAppsInfo(
|
||||
myAppsModel.copy(
|
||||
appUpdates = emptyList(),
|
||||
installingApps = emptyList(),
|
||||
installingApps =
|
||||
listOf(
|
||||
myAppsModel.installingApps[0].copy(
|
||||
installState = InstallState.Installed("Installed App", "0.5.2", null, 1337L, null)
|
||||
)
|
||||
),
|
||||
appsWithIssue = emptyList(),
|
||||
)
|
||||
),
|
||||
|
||||
@@ -113,11 +113,22 @@ fun MyAppsPresenter(
|
||||
private fun <T : MyAppItem> List<T>.sort(sortOrder: AppListSortOrder): List<T> {
|
||||
val collator = Collator.getInstance(Locale.getDefault())
|
||||
return when (sortOrder) {
|
||||
AppListSortOrder.NAME ->
|
||||
AppListSortOrder.NAME -> {
|
||||
sortedWith { a1, a2 ->
|
||||
// storing collator.getCollationKey() and using that could be an optimization
|
||||
collator.compare(a1.name, a2.name)
|
||||
}
|
||||
AppListSortOrder.LAST_UPDATED -> sortedByDescending { it.lastUpdated }
|
||||
}
|
||||
AppListSortOrder.LAST_UPDATED -> {
|
||||
sortedWith { a1, a2 ->
|
||||
val lastAddedCompare = a2.lastUpdated.compareTo(a1.lastUpdated)
|
||||
if (lastAddedCompare == 0) {
|
||||
// fall-back to name if last updated is the same, to ensure stable sorting
|
||||
collator.compare(a1.name, a2.name)
|
||||
} else {
|
||||
lastAddedCompare
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.fdroid.ui.apps
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
@@ -135,6 +136,11 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
override fun clearInstallingApps() {
|
||||
appInstallManager.clearInstallingApps()
|
||||
}
|
||||
|
||||
override fun ignoreAppIssue(item: AppWithIssueItem) {
|
||||
settingsManager.ignoreAppIssue(item.packageName, item.installedVersionCode)
|
||||
updatesManager.loadUpdates()
|
||||
|
||||
@@ -52,7 +52,11 @@ fun CategoryChip(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryChip(categoryItem: CategoryItem, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
fun CategoryChip(
|
||||
categoryItem: CategoryItem,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AssistChip(
|
||||
onClick = onClick,
|
||||
leadingIcon = {
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.material.icons.filled.Forum
|
||||
import androidx.compose.material.icons.filled.NetworkCheck
|
||||
import androidx.compose.material.icons.filled.PhoneAndroid
|
||||
import androidx.compose.material.icons.filled.SdStorage
|
||||
import androidx.compose.material.icons.filled.SportsEsports
|
||||
import androidx.compose.material.icons.filled.VideogameAsset
|
||||
import androidx.compose.material.icons.filled.Wallet
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
@@ -23,6 +24,54 @@ data class CategoryGroup(
|
||||
)
|
||||
|
||||
object CategoryGroups {
|
||||
val communication =
|
||||
CategoryGroup(
|
||||
id = "communication",
|
||||
name = R.string.category_group_communication,
|
||||
summary = R.string.category_group_summary_communication,
|
||||
imageVector = Icons.Default.Forum,
|
||||
)
|
||||
val device =
|
||||
CategoryGroup(
|
||||
id = "device",
|
||||
name = R.string.category_group_device,
|
||||
summary = R.string.category_group_summary_device,
|
||||
imageVector = Icons.Default.PhoneAndroid,
|
||||
)
|
||||
val games = CategoryGroup(
|
||||
id = "games",
|
||||
name = R.string.category_group_games,
|
||||
summary = R.string.category_group_summary_games,
|
||||
imageVector = Icons.Default.SportsEsports,
|
||||
)
|
||||
val interests =
|
||||
CategoryGroup(
|
||||
id = "interests",
|
||||
name = R.string.category_group_interests,
|
||||
summary = R.string.category_group_summary_interests,
|
||||
imageVector = Icons.Default.FavoriteBorder,
|
||||
)
|
||||
val media =
|
||||
CategoryGroup(
|
||||
id = "media",
|
||||
name = R.string.category_group_media,
|
||||
summary = R.string.category_group_summary_media,
|
||||
imageVector = Icons.Default.VideogameAsset,
|
||||
)
|
||||
val misc =
|
||||
CategoryGroup(
|
||||
id = "misc",
|
||||
name = R.string.category_group_misc,
|
||||
summary = R.string.category_group_summary_misc,
|
||||
imageVector = Icons.Default.Category,
|
||||
)
|
||||
val network =
|
||||
CategoryGroup(
|
||||
id = "network",
|
||||
name = R.string.category_group_network,
|
||||
summary = R.string.category_group_summary_network,
|
||||
imageVector = Icons.Default.NetworkCheck,
|
||||
)
|
||||
val productivity =
|
||||
CategoryGroup(
|
||||
id = "productivity",
|
||||
@@ -30,6 +79,13 @@ object CategoryGroups {
|
||||
summary = R.string.category_group_summary_productivity,
|
||||
imageVector = Icons.Default.Factory,
|
||||
)
|
||||
val storage =
|
||||
CategoryGroup(
|
||||
id = "storage",
|
||||
name = R.string.category_group_storage,
|
||||
summary = R.string.category_group_summary_storage,
|
||||
imageVector = Icons.Default.SdStorage,
|
||||
)
|
||||
val tools =
|
||||
CategoryGroup(
|
||||
id = "tools",
|
||||
@@ -44,53 +100,4 @@ object CategoryGroups {
|
||||
summary = R.string.category_group_summary_wallets,
|
||||
imageVector = Icons.Default.Wallet,
|
||||
)
|
||||
val media =
|
||||
CategoryGroup(
|
||||
id = "media",
|
||||
name = R.string.category_group_media,
|
||||
summary = R.string.category_group_summary_media,
|
||||
imageVector = Icons.Default.VideogameAsset,
|
||||
)
|
||||
val communication =
|
||||
CategoryGroup(
|
||||
id = "communication",
|
||||
name = R.string.category_group_communication,
|
||||
summary = R.string.category_group_summary_communication,
|
||||
imageVector = Icons.Default.Forum,
|
||||
)
|
||||
val device =
|
||||
CategoryGroup(
|
||||
id = "device",
|
||||
name = R.string.category_group_device,
|
||||
summary = R.string.category_group_summary_device,
|
||||
imageVector = Icons.Default.PhoneAndroid,
|
||||
)
|
||||
val network =
|
||||
CategoryGroup(
|
||||
id = "network",
|
||||
name = R.string.category_group_network,
|
||||
summary = R.string.category_group_summary_network,
|
||||
imageVector = Icons.Default.NetworkCheck,
|
||||
)
|
||||
val storage =
|
||||
CategoryGroup(
|
||||
id = "storage",
|
||||
name = R.string.category_group_storage,
|
||||
summary = R.string.category_group_summary_storage,
|
||||
imageVector = Icons.Default.SdStorage,
|
||||
)
|
||||
val interests =
|
||||
CategoryGroup(
|
||||
id = "interests",
|
||||
name = R.string.category_group_interests,
|
||||
summary = R.string.category_group_summary_interests,
|
||||
imageVector = Icons.Default.FavoriteBorder,
|
||||
)
|
||||
val misc =
|
||||
CategoryGroup(
|
||||
id = "misc",
|
||||
name = R.string.category_group_misc,
|
||||
summary = R.string.category_group_summary_misc,
|
||||
imageVector = Icons.Default.Category,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,28 +17,41 @@ import androidx.compose.material.icons.filled.BrowserUpdated
|
||||
import androidx.compose.material.icons.filled.Brush
|
||||
import androidx.compose.material.icons.filled.Calculate
|
||||
import androidx.compose.material.icons.filled.CalendarMonth
|
||||
import androidx.compose.material.icons.filled.Casino
|
||||
import androidx.compose.material.icons.filled.Castle
|
||||
import androidx.compose.material.icons.filled.Category
|
||||
import androidx.compose.material.icons.filled.Celebration
|
||||
import androidx.compose.material.icons.filled.CenterFocusWeak
|
||||
import androidx.compose.material.icons.filled.Church
|
||||
import androidx.compose.material.icons.filled.Cloud
|
||||
import androidx.compose.material.icons.filled.Collections
|
||||
import androidx.compose.material.icons.filled.Contacts
|
||||
import androidx.compose.material.icons.filled.CrueltyFree
|
||||
import androidx.compose.material.icons.filled.CurrencyExchange
|
||||
import androidx.compose.material.icons.filled.DeveloperBoard
|
||||
import androidx.compose.material.icons.filled.DeveloperMode
|
||||
import androidx.compose.material.icons.filled.DirectionsBus
|
||||
import androidx.compose.material.icons.filled.Diversity3
|
||||
import androidx.compose.material.icons.filled.Dns
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.Draw
|
||||
import androidx.compose.material.icons.filled.EditNote
|
||||
import androidx.compose.material.icons.filled.EnhancedEncryption
|
||||
import androidx.compose.material.icons.filled.Extension
|
||||
import androidx.compose.material.icons.filled.Fastfood
|
||||
import androidx.compose.material.icons.filled.FileCopy
|
||||
import androidx.compose.material.icons.filled.FitnessCenter
|
||||
import androidx.compose.material.icons.filled.FlashlightOn
|
||||
import androidx.compose.material.icons.filled.Games
|
||||
import androidx.compose.material.icons.filled.Gamepad
|
||||
import androidx.compose.material.icons.filled.Groups
|
||||
import androidx.compose.material.icons.filled.Handyman
|
||||
import androidx.compose.material.icons.filled.HealthAndSafety
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material.icons.filled.Keyboard
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material.icons.filled.LocalPlay
|
||||
import androidx.compose.material.icons.filled.ModeComment
|
||||
import androidx.compose.material.icons.filled.MonetizationOn
|
||||
import androidx.compose.material.icons.filled.Money
|
||||
import androidx.compose.material.icons.filled.MusicNote
|
||||
@@ -54,20 +67,29 @@ import androidx.compose.material.icons.filled.PermPhoneMsg
|
||||
import androidx.compose.material.icons.filled.PhotoSizeSelectActual
|
||||
import androidx.compose.material.icons.filled.Podcasts
|
||||
import androidx.compose.material.icons.filled.Radio
|
||||
import androidx.compose.material.icons.filled.RecordVoiceOver
|
||||
import androidx.compose.material.icons.filled.RestaurantMenu
|
||||
import androidx.compose.material.icons.filled.School
|
||||
import androidx.compose.material.icons.filled.Science
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material.icons.filled.SelfImprovement
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.SettingsRemote
|
||||
import androidx.compose.material.icons.filled.ShoppingCart
|
||||
import androidx.compose.material.icons.filled.SignalCellularAlt
|
||||
import androidx.compose.material.icons.filled.Sos
|
||||
import androidx.compose.material.icons.filled.SportsMartialArts
|
||||
import androidx.compose.material.icons.filled.SportsSoccer
|
||||
import androidx.compose.material.icons.filled.Storefront
|
||||
import androidx.compose.material.icons.filled.Style
|
||||
import androidx.compose.material.icons.filled.TaskAlt
|
||||
import androidx.compose.material.icons.filled.Timelapse
|
||||
import androidx.compose.material.icons.filled.Timer
|
||||
import androidx.compose.material.icons.filled.TrackChanges
|
||||
import androidx.compose.material.icons.filled.Translate
|
||||
import androidx.compose.material.icons.filled.UploadFile
|
||||
import androidx.compose.material.icons.filled.VideoChat
|
||||
import androidx.compose.material.icons.filled.VideogameAsset
|
||||
import androidx.compose.material.icons.filled.VoiceChat
|
||||
import androidx.compose.material.icons.filled.VpnLock
|
||||
import androidx.compose.material.icons.filled.Wallet
|
||||
@@ -82,30 +104,41 @@ data class CategoryItem(val id: String, val name: String, val description: Strin
|
||||
"AI Chat" -> Icons.Default.VoiceChat
|
||||
"App Manager" -> Icons.Default.Apps
|
||||
"App Store & Updater" -> Icons.Default.Storefront
|
||||
"Action Game" -> Icons.Default.SportsMartialArts
|
||||
"Battery" -> Icons.Default.BatteryChargingFull
|
||||
"Board Game" -> Icons.Default.DeveloperBoard
|
||||
"Bookmark" -> Icons.Default.Bookmarks
|
||||
"Browser" -> Icons.Default.OpenInBrowser
|
||||
"Calculator" -> Icons.Default.Calculate
|
||||
"Calendar & Agenda" -> Icons.Default.CalendarMonth
|
||||
"Card Game" -> Icons.Default.Style
|
||||
"Casual Game" -> Icons.Default.Gamepad
|
||||
"Clock" -> Icons.Default.AccessTime
|
||||
"Cloud Storage & File Sync" -> Icons.Default.Cloud
|
||||
"Connectivity" -> Icons.Default.SignalCellularAlt
|
||||
"Contact" -> Icons.Default.Contacts
|
||||
"Development" -> Icons.Default.DeveloperMode
|
||||
"Dice" -> Icons.Default.Casino
|
||||
"Diet" -> Icons.Default.Fastfood
|
||||
"DNS & Hosts" -> Icons.Default.Dns
|
||||
"Download" -> Icons.Default.Download
|
||||
"Draw" -> Icons.Default.Draw
|
||||
"Ebook Reader" -> AutoMirrored.Default.MenuBook
|
||||
"Educational Game" -> Icons.Default.School
|
||||
"Email" -> Icons.Default.AlternateEmail
|
||||
"Emulator" -> Icons.Default.VideogameAsset
|
||||
"File Encryption & Vault" -> Icons.Default.EnhancedEncryption
|
||||
"File Manager" -> Icons.Default.FileCopy
|
||||
"File Transfer" -> Icons.Default.UploadFile
|
||||
"Finance Manager" -> Icons.Default.MonetizationOn
|
||||
"Firewall" -> Icons.Default.AppBlocking
|
||||
"Flashlight" -> Icons.Default.FlashlightOn
|
||||
"Forum" -> Icons.Default.Image
|
||||
"Gallery" -> Icons.Default.PhotoSizeSelectActual
|
||||
"Games" -> Icons.Default.Games
|
||||
"Game Helper" -> Icons.Default.Handyman
|
||||
"Graphics" -> Icons.Default.Brush
|
||||
"Habit Tracker" -> Icons.Default.TrackChanges
|
||||
"Health Manager" -> Icons.Default.HealthAndSafety
|
||||
"Icon Pack" -> Icons.Default.Collections
|
||||
"Internet" -> Icons.Default.Language
|
||||
"Inventory" -> Icons.Default.AllInbox
|
||||
@@ -113,6 +146,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin
|
||||
"Launcher" -> Icons.Default.Home
|
||||
"Local Media Player" -> Icons.Default.LocalPlay
|
||||
"Location Tracker & Sharer" -> Icons.Default.MyLocation
|
||||
"Meditation" -> Icons.Default.SelfImprovement
|
||||
"Messaging" -> AutoMirrored.Default.Message
|
||||
"Money" -> Icons.Default.Money
|
||||
"Multimedia" -> Icons.Default.MusicVideo
|
||||
@@ -122,34 +156,47 @@ data class CategoryItem(val id: String, val name: String, val description: Strin
|
||||
"News" -> Icons.Default.Newspaper
|
||||
"Note" -> Icons.Default.NoteAlt
|
||||
"Online Media Player" -> Icons.Default.Airplay
|
||||
"Party Game" -> Icons.Default.Celebration
|
||||
"Pass Wallet" -> Icons.Default.AccountBalanceWallet
|
||||
"Password & 2FA" -> Icons.Default.Password
|
||||
"Phone & SMS" -> Icons.Default.PermPhoneMsg
|
||||
"Platformer Game" -> Icons.Default.CrueltyFree
|
||||
"Podcast" -> Icons.Default.Podcasts
|
||||
"Public Transport" -> Icons.Default.DirectionsBus
|
||||
"Puzzle Game" -> Icons.Default.Extension
|
||||
"Radio" -> Icons.Default.Radio
|
||||
"Reading" -> AutoMirrored.Default.MenuBook
|
||||
"Recipe Manager" -> Icons.Default.RestaurantMenu
|
||||
"Religion" -> Icons.Default.Church
|
||||
"Role-Playing Game" -> Icons.Default.Diversity3
|
||||
"Remote Access" -> Icons.Default.BrowserUpdated
|
||||
"Remote Controller" -> Icons.Default.SettingsRemote
|
||||
"Schedule" -> Icons.Default.CalendarMonth
|
||||
"Science & Education" -> Icons.Default.Science
|
||||
"Security" -> Icons.Default.Security
|
||||
"Shooter Game" -> Icons.Default.CenterFocusWeak
|
||||
"Strategy Game" -> Icons.Default.Castle
|
||||
"Shopping List" -> Icons.Default.ShoppingCart
|
||||
"Social Network" -> Icons.Default.Groups
|
||||
"Sport Game" -> Icons.Default.SportsSoccer
|
||||
"Sports & Health" -> Icons.Default.HealthAndSafety
|
||||
"System" -> Icons.Default.Settings
|
||||
"Task" -> Icons.Default.TaskAlt
|
||||
"Text Editor" -> Icons.Default.EditNote
|
||||
"Text to Speech" -> Icons.Default.RecordVoiceOver
|
||||
"Theming" -> Icons.Default.Style
|
||||
"Time" -> Icons.Default.AccessTime
|
||||
"Time Tracker" -> Icons.Default.Timelapse
|
||||
"Timer" -> Icons.Default.Timer
|
||||
"Translation & Dictionary" -> Icons.Default.Translate
|
||||
"Visual Novel" -> Icons.Default.ModeComment
|
||||
"Voice & Video Chat" -> Icons.Default.VideoChat
|
||||
"Unit Convertor" -> Icons.Default.CurrencyExchange
|
||||
"VPN & Proxy" -> Icons.Default.VpnLock
|
||||
"Wallet" -> Icons.Default.Wallet
|
||||
"Wallpaper" -> Icons.Default.Wallpaper
|
||||
"Weather" -> Icons.Default.WbSunny
|
||||
"Word Game" -> Icons.Default.Sos
|
||||
"Workout" -> Icons.Default.FitnessCenter
|
||||
"Writing" -> Icons.Default.EditNote
|
||||
else -> Icons.Default.Category
|
||||
@@ -161,30 +208,41 @@ data class CategoryItem(val id: String, val name: String, val description: Strin
|
||||
"AI Chat" -> CategoryGroups.tools
|
||||
"App Manager" -> CategoryGroups.device
|
||||
"App Store & Updater" -> CategoryGroups.device
|
||||
"Action Game" -> CategoryGroups.games
|
||||
"Battery" -> CategoryGroups.device
|
||||
"Board Game" -> CategoryGroups.games
|
||||
"Bookmark" -> CategoryGroups.storage
|
||||
"Browser" -> CategoryGroups.network
|
||||
"Calculator" -> CategoryGroups.tools
|
||||
"Calendar & Agenda" -> CategoryGroups.productivity
|
||||
"Card Game" -> CategoryGroups.games
|
||||
"Casual Game" -> CategoryGroups.games
|
||||
"Clock" -> CategoryGroups.productivity
|
||||
"Cloud Storage & File Sync" -> CategoryGroups.storage
|
||||
"Connectivity" -> CategoryGroups.network
|
||||
"Contact" -> CategoryGroups.communication
|
||||
"Development" -> CategoryGroups.interests
|
||||
"Dice" -> CategoryGroups.games
|
||||
"Diet" -> CategoryGroups.interests
|
||||
"DNS & Hosts" -> CategoryGroups.network
|
||||
"Download" -> CategoryGroups.network
|
||||
"Draw" -> CategoryGroups.interests
|
||||
"Ebook Reader" -> CategoryGroups.media
|
||||
"Educational Game" -> CategoryGroups.games
|
||||
"Email" -> CategoryGroups.communication
|
||||
"Emulator" -> CategoryGroups.games
|
||||
"File Encryption & Vault" -> CategoryGroups.storage
|
||||
"File Manager" -> CategoryGroups.storage
|
||||
"File Transfer" -> CategoryGroups.storage
|
||||
"Finance Manager" -> CategoryGroups.wallets
|
||||
"Firewall" -> CategoryGroups.network
|
||||
"Flashlight" -> CategoryGroups.tools
|
||||
"Forum" -> CategoryGroups.communication
|
||||
"Gallery" -> CategoryGroups.storage
|
||||
"Games" -> CategoryGroups.media
|
||||
"Game Helper" -> CategoryGroups.games
|
||||
"Graphics" -> CategoryGroups.interests
|
||||
"Habit Tracker" -> CategoryGroups.productivity
|
||||
"Health Manager" -> CategoryGroups.productivity
|
||||
"Icon Pack" -> CategoryGroups.device
|
||||
"Internet" -> CategoryGroups.network
|
||||
"Inventory" -> CategoryGroups.tools
|
||||
@@ -192,6 +250,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin
|
||||
"Launcher" -> CategoryGroups.device
|
||||
"Local Media Player" -> CategoryGroups.media
|
||||
"Location Tracker & Sharer" -> CategoryGroups.tools
|
||||
"Meditation" -> CategoryGroups.interests
|
||||
"Messaging" -> CategoryGroups.communication
|
||||
"Money" -> CategoryGroups.wallets
|
||||
"Multimedia" -> CategoryGroups.media
|
||||
@@ -201,34 +260,47 @@ data class CategoryItem(val id: String, val name: String, val description: Strin
|
||||
"News" -> CategoryGroups.interests
|
||||
"Note" -> CategoryGroups.storage
|
||||
"Online Media Player" -> CategoryGroups.media
|
||||
"Party Game" -> CategoryGroups.games
|
||||
"Pass Wallet" -> CategoryGroups.wallets
|
||||
"Password & 2FA" -> CategoryGroups.device
|
||||
"Phone & SMS" -> CategoryGroups.communication
|
||||
"Platformer Game" -> CategoryGroups.games
|
||||
"Podcast" -> CategoryGroups.media
|
||||
"Public Transport" -> CategoryGroups.tools
|
||||
"Puzzle Game" -> CategoryGroups.games
|
||||
"Radio" -> CategoryGroups.media
|
||||
"Reading" -> CategoryGroups.media
|
||||
"Recipe Manager" -> CategoryGroups.interests
|
||||
"Religion" -> CategoryGroups.interests
|
||||
"Role-Playing Game" -> CategoryGroups.games
|
||||
"Remote Access" -> CategoryGroups.network
|
||||
"Remote Controller" -> CategoryGroups.tools
|
||||
"Schedule" -> CategoryGroups.productivity
|
||||
"Science & Education" -> CategoryGroups.interests
|
||||
"Security" -> CategoryGroups.device
|
||||
"Shooter Game" -> CategoryGroups.games
|
||||
"Strategy Game" -> CategoryGroups.games
|
||||
"Shopping List" -> CategoryGroups.tools
|
||||
"Social Network" -> CategoryGroups.communication
|
||||
"Sport Game" -> CategoryGroups.games
|
||||
"Sports & Health" -> CategoryGroups.interests
|
||||
"System" -> CategoryGroups.device
|
||||
"Task" -> CategoryGroups.productivity
|
||||
"Text Editor" -> CategoryGroups.productivity
|
||||
"Text to Speech" -> CategoryGroups.device
|
||||
"Theming" -> CategoryGroups.device
|
||||
"Time" -> CategoryGroups.productivity
|
||||
"Time Tracker" -> CategoryGroups.productivity
|
||||
"Timer" -> CategoryGroups.productivity
|
||||
"Translation & Dictionary" -> CategoryGroups.tools
|
||||
"Visual Novel" -> CategoryGroups.games
|
||||
"Voice & Video Chat" -> CategoryGroups.communication
|
||||
"Unit Convertor" -> CategoryGroups.tools
|
||||
"VPN & Proxy" -> CategoryGroups.network
|
||||
"Wallet" -> CategoryGroups.wallets
|
||||
"Wallpaper" -> CategoryGroups.device
|
||||
"Weather" -> CategoryGroups.tools
|
||||
"Word Game" -> CategoryGroups.games
|
||||
"Workout" -> CategoryGroups.interests
|
||||
"Writing" -> CategoryGroups.productivity
|
||||
else -> CategoryGroups.misc
|
||||
|
||||
@@ -76,7 +76,7 @@ fun AntiFeatures(
|
||||
ExpandableSection(
|
||||
icon = rememberVectorPainter(Icons.Default.WarningAmber),
|
||||
title = stringResource(R.string.anti_features_title),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
) {
|
||||
Column {
|
||||
antiFeatures.forEach { antiFeature ->
|
||||
|
||||
@@ -2,9 +2,11 @@ package org.fdroid.ui.details
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@@ -12,7 +14,6 @@ import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AppSettingsAlt
|
||||
import androidx.compose.material.icons.filled.Category
|
||||
import androidx.compose.material.icons.filled.ChangeHistory
|
||||
import androidx.compose.material.icons.filled.Code
|
||||
import androidx.compose.material.icons.filled.CurrencyBitcoin
|
||||
@@ -43,6 +44,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
@@ -66,7 +68,9 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import com.viktormykhailiv.compose.hints.HintHost
|
||||
import com.viktormykhailiv.compose.hints.HintProperties
|
||||
import com.viktormykhailiv.compose.hints.rememberHint
|
||||
import com.viktormykhailiv.compose.hints.rememberHintAnchorState
|
||||
@@ -76,8 +80,6 @@ import org.fdroid.LocaleChooser.getBestLocale
|
||||
import org.fdroid.R
|
||||
import org.fdroid.install.InstallState
|
||||
import org.fdroid.ui.FDroidContent
|
||||
import org.fdroid.ui.categories.CategoryChip
|
||||
import org.fdroid.ui.categories.ChipFlowRow
|
||||
import org.fdroid.ui.icons.License
|
||||
import org.fdroid.ui.icons.Litecoin
|
||||
import org.fdroid.ui.lists.AppListType
|
||||
@@ -100,11 +102,21 @@ fun AppDetails(
|
||||
var showInstallError by remember { mutableStateOf(false) }
|
||||
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState)
|
||||
if (item == null) BigLoadingIndicator()
|
||||
else
|
||||
else {
|
||||
Scaffold(
|
||||
topBar = { AppDetailsTopAppBar(item, topAppBarState, scrollBehavior, onBackNav) },
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { innerPadding ->
|
||||
if (item is NotFoundAppDetailsItem) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxSize().padding(innerPadding),
|
||||
) {
|
||||
Text(stringResource(R.string.no_such_app))
|
||||
}
|
||||
return@Scaffold
|
||||
}
|
||||
item as LoadedAppDetailsItem
|
||||
// react to install state changes
|
||||
LaunchedEffect(item.installState) {
|
||||
val state = item.installState
|
||||
@@ -146,7 +158,7 @@ fun AppDetails(
|
||||
.onGloballyPositioned { coordinates -> size = coordinates.size }
|
||||
) {
|
||||
// Header is taking care of top innerPadding
|
||||
AppDetailsHeader(item, innerPadding)
|
||||
AppDetailsHeader(item, onNav, innerPadding)
|
||||
AnimatedVisibility(item.showWarnings) {
|
||||
AppDetailsWarnings(item, Modifier.padding(horizontal = 16.dp))
|
||||
}
|
||||
@@ -186,6 +198,20 @@ fun AppDetails(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Screenshots
|
||||
if (item.phoneScreenshots.isNotEmpty()) {
|
||||
Screenshots(item.networkState.showWarningDialog, item.phoneScreenshots)
|
||||
}
|
||||
// Anti-features
|
||||
if (!item.antiFeatures.isNullOrEmpty()) {
|
||||
AntiFeatures(
|
||||
antiFeatures = item.antiFeatures,
|
||||
hintAnchor = hintAnchor,
|
||||
showOnboarding = item.showAntiFeaturesOnboarding,
|
||||
) {
|
||||
coroutineScope.launch { hintController.show(hintAnchor) }
|
||||
}
|
||||
}
|
||||
// Description
|
||||
item.description?.let { description ->
|
||||
val maxLines = 3
|
||||
@@ -210,6 +236,7 @@ fun AppDetails(
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = htmlDescription,
|
||||
lineHeight = 22.sp,
|
||||
modifier = Modifier.padding(horizontal = 16.dp).padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
@@ -218,6 +245,7 @@ fun AppDetails(
|
||||
AnimatedVisibility(!descriptionExpanded) {
|
||||
Text(
|
||||
text = htmlDescription,
|
||||
lineHeight = 22.sp,
|
||||
maxLines = maxLines,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).padding(top = 8.dp),
|
||||
@@ -239,20 +267,6 @@ fun AppDetails(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Anti-features
|
||||
if (!item.antiFeatures.isNullOrEmpty()) {
|
||||
AntiFeatures(
|
||||
antiFeatures = item.antiFeatures,
|
||||
hintAnchor = hintAnchor,
|
||||
showOnboarding = item.showAntiFeaturesOnboarding,
|
||||
) {
|
||||
coroutineScope.launch { hintController.show(hintAnchor) }
|
||||
}
|
||||
}
|
||||
// Screenshots
|
||||
if (item.phoneScreenshots.isNotEmpty()) {
|
||||
Screenshots(item.networkState.showWarningDialog, item.phoneScreenshots)
|
||||
}
|
||||
// Donate card
|
||||
if (item.showDonate)
|
||||
ElevatedCard(
|
||||
@@ -391,25 +405,6 @@ fun AppDetails(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!item.categories.isNullOrEmpty())
|
||||
ExpandableSection(
|
||||
icon = rememberVectorPainter(Icons.Default.Category),
|
||||
title = stringResource(R.string.main_menu__categories),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
initiallyExpanded = true,
|
||||
) {
|
||||
ChipFlowRow(modifier = Modifier.padding(start = 8.dp)) {
|
||||
item.categories.forEach { item ->
|
||||
CategoryChip(
|
||||
item,
|
||||
onClick = {
|
||||
val categoryNav = AppListType.Category(item.name, item.id)
|
||||
onNav(NavigationKey.AppList(categoryNav))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
ExpandableSection(
|
||||
icon = rememberVectorPainter(Icons.Default.AppSettingsAlt),
|
||||
title = stringResource(R.string.technical_info),
|
||||
@@ -431,33 +426,34 @@ fun AppDetails(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showInstallError && item != null && item.installState is InstallState.Error)
|
||||
AlertDialog(
|
||||
onDismissRequest = { showInstallError = false },
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
title = { Text(stringResource(R.string.install_error_notify_title, item.name)) },
|
||||
text = {
|
||||
if (item.installState.msg == null) {
|
||||
Text(stringResource(R.string.app_details_install_error_text))
|
||||
} else {
|
||||
ExpandableSection(
|
||||
icon = null,
|
||||
title = stringResource(R.string.app_details_install_error_text),
|
||||
) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = item.installState.msg,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
if (item is LoadedAppDetailsItem && showInstallError && item.installState is InstallState.Error)
|
||||
AlertDialog(
|
||||
onDismissRequest = { showInstallError = false },
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
title = { Text(stringResource(R.string.install_error_notify_title, item.name)) },
|
||||
text = {
|
||||
if (item.installState.msg == null) {
|
||||
Text(stringResource(R.string.app_details_install_error_text))
|
||||
} else {
|
||||
ExpandableSection(
|
||||
icon = null,
|
||||
title = stringResource(R.string.app_details_install_error_text),
|
||||
) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = item.installState.msg,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showInstallError = false }) { Text(stringResource(R.string.ok)) }
|
||||
},
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showInstallError = false }) { Text(stringResource(R.string.ok)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@@ -468,6 +464,12 @@ fun AppDetailsLoadingPreview() {
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AppDetailsPreview() {
|
||||
FDroidContent { AppDetails(testApp, {}, {}) }
|
||||
fun AppDetailsNotFoundPreview() {
|
||||
FDroidContent { AppDetails(NotFoundAppDetailsItem, {}, {}) }
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AppDetailsPreview() {
|
||||
HintHost { FDroidContent { AppDetails(testApp, {}, {}) } }
|
||||
}
|
||||
|
||||
@@ -21,8 +21,10 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -55,9 +57,16 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.hideFromAccessibility
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
@@ -67,11 +76,16 @@ import org.fdroid.R
|
||||
import org.fdroid.download.NetworkState
|
||||
import org.fdroid.install.InstallState
|
||||
import org.fdroid.ui.FDroidContent
|
||||
import org.fdroid.ui.categories.ChipFlowRow
|
||||
import org.fdroid.ui.categories.chipHeight
|
||||
import org.fdroid.ui.lists.AppListType
|
||||
import org.fdroid.ui.navigation.NavigationKey
|
||||
import org.fdroid.ui.utils.AsyncShimmerImage
|
||||
import org.fdroid.ui.utils.InstalledBadge
|
||||
import org.fdroid.ui.utils.MeteredConnectionDialog
|
||||
import org.fdroid.ui.utils.OfflineBar
|
||||
import org.fdroid.ui.utils.asRelativeTimeString
|
||||
import org.fdroid.ui.utils.getRepository
|
||||
import org.fdroid.ui.utils.startActivitySafe
|
||||
import org.fdroid.ui.utils.testApp
|
||||
|
||||
@@ -79,7 +93,8 @@ import org.fdroid.ui.utils.testApp
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
/** Timestamp [now] gets passed in for screenshot tests to have a stable download speed. */
|
||||
fun AppDetailsHeader(
|
||||
item: AppDetailsItem,
|
||||
item: LoadedAppDetailsItem,
|
||||
onNav: (NavigationKey) -> Unit,
|
||||
innerPadding: PaddingValues,
|
||||
now: Long = System.currentTimeMillis(),
|
||||
) {
|
||||
@@ -136,14 +151,48 @@ fun AppDetailsHeader(
|
||||
}
|
||||
Column {
|
||||
SelectionContainer {
|
||||
Text(item.name, style = MaterialTheme.typography.headlineMediumEmphasized)
|
||||
Text(
|
||||
item.name,
|
||||
style = MaterialTheme.typography.headlineSmallEmphasized,
|
||||
fontWeight = FontWeight.Medium,
|
||||
lineHeight = 26.sp,
|
||||
)
|
||||
}
|
||||
item.app.authorName?.let { authorName ->
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = stringResource(R.string.author_by, authorName),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
if (item.authorHasMoreThanOneApp) {
|
||||
val title = stringResource(R.string.app_list_author, authorName)
|
||||
val authorString = stringResource(R.string.by_author_format, authorName)
|
||||
val startIndex = authorString.indexOf(authorName)
|
||||
val annotatedString = buildAnnotatedString {
|
||||
append(authorString)
|
||||
addLink(
|
||||
clickable =
|
||||
LinkAnnotation.Clickable(
|
||||
tag = "author",
|
||||
linkInteractionListener = {
|
||||
onNav(NavigationKey.AppList(AppListType.Author(title, authorName)))
|
||||
},
|
||||
styles =
|
||||
TextLinkStyles(
|
||||
style =
|
||||
SpanStyle(
|
||||
color = ButtonDefaults.textButtonColors().contentColor,
|
||||
textDecoration = TextDecoration.None,
|
||||
)
|
||||
),
|
||||
),
|
||||
start = startIndex,
|
||||
end = startIndex + authorName.length,
|
||||
)
|
||||
}
|
||||
Text(text = annotatedString, style = MaterialTheme.typography.bodyMedium)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.by_author_format, authorName),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val lastUpdated = item.app.lastUpdated.asRelativeTimeString()
|
||||
@@ -171,6 +220,22 @@ fun AppDetailsHeader(
|
||||
)
|
||||
}
|
||||
}
|
||||
// Category chips
|
||||
if (!item.categories.isNullOrEmpty())
|
||||
ChipFlowRow(modifier = Modifier.padding(start = 8.dp)) {
|
||||
item.categories.forEach { categoryItem ->
|
||||
AssistChip(
|
||||
label = {
|
||||
Text(text = categoryItem.name, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
},
|
||||
onClick = {
|
||||
val categoryNav = AppListType.Category(categoryItem.name, categoryItem.id)
|
||||
onNav(NavigationKey.AppList(categoryNav))
|
||||
},
|
||||
modifier = Modifier.height(chipHeight),
|
||||
)
|
||||
}
|
||||
}
|
||||
// Repo Chooser
|
||||
RepoChooser(
|
||||
repos = item.repositories,
|
||||
@@ -340,7 +405,7 @@ fun AppDetailsHeader(
|
||||
@Preview
|
||||
@Composable
|
||||
fun AppDetailsHeaderPreview() {
|
||||
FDroidContent { Column { AppDetailsHeader(testApp, PaddingValues(top = 8.dp)) } }
|
||||
FDroidContent { Column { AppDetailsHeader(testApp, {}, PaddingValues(top = 8.dp)) } }
|
||||
}
|
||||
|
||||
@Preview
|
||||
@@ -353,9 +418,12 @@ private fun InstallPreview() {
|
||||
installedVersion = null,
|
||||
installedVersionCode = null,
|
||||
installedVersionName = null,
|
||||
repositories = listOf(getRepository(testApp.app.repoId), getRepository()),
|
||||
preferredRepoId = 42L,
|
||||
suggestedVersion = testApp.versions?.first()?.version,
|
||||
networkState = NetworkState(true, isMetered = true),
|
||||
),
|
||||
{},
|
||||
PaddingValues(top = 8.dp),
|
||||
)
|
||||
}
|
||||
@@ -372,6 +440,7 @@ private fun UpdatePreview() {
|
||||
suggestedVersion = testApp.versions?.first()?.version,
|
||||
networkState = NetworkState(true, isMetered = true),
|
||||
),
|
||||
{},
|
||||
PaddingValues(top = 8.dp),
|
||||
)
|
||||
}
|
||||
@@ -384,7 +453,7 @@ private fun PreviewLoading() {
|
||||
FDroidContent {
|
||||
Column {
|
||||
val app = testApp.copy(versions = null)
|
||||
AppDetailsHeader(app, PaddingValues(top = 8.dp))
|
||||
AppDetailsHeader(app, {}, PaddingValues(top = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,7 +478,7 @@ private fun PreviewDownloading() {
|
||||
),
|
||||
networkState = NetworkState(true, isMetered = true),
|
||||
)
|
||||
AppDetailsHeader(app, PaddingValues(top = 8.dp))
|
||||
AppDetailsHeader(app, {}, PaddingValues(top = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,7 +503,7 @@ private fun PreviewDownloadingNight() {
|
||||
),
|
||||
networkState = NetworkState(true, isMetered = true),
|
||||
)
|
||||
AppDetailsHeader(app, PaddingValues(top = 8.dp))
|
||||
AppDetailsHeader(app, {}, PaddingValues(top = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,7 +518,7 @@ private fun PreviewProgress() {
|
||||
installState = InstallState.Starting("", "", "", 23),
|
||||
networkState = NetworkState(true, isMetered = true),
|
||||
)
|
||||
AppDetailsHeader(app, PaddingValues(top = 16.dp))
|
||||
AppDetailsHeader(app, {}, PaddingValues(top = 16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,11 @@ import org.fdroid.install.SessionInstallManager
|
||||
import org.fdroid.search.SearchHelper.removeZeroWhiteSpace
|
||||
import org.fdroid.ui.categories.CategoryItem
|
||||
|
||||
data class AppDetailsItem(
|
||||
sealed class AppDetailsItem
|
||||
|
||||
data object NotFoundAppDetailsItem : AppDetailsItem()
|
||||
|
||||
data class LoadedAppDetailsItem(
|
||||
val app: AppMetadata,
|
||||
val actions: AppDetailsActions,
|
||||
val installState: InstallState,
|
||||
@@ -66,7 +70,7 @@ data class AppDetailsItem(
|
||||
val issue: AppIssue? = null,
|
||||
val authorHasMoreThanOneApp: Boolean = false,
|
||||
val proxy: ProxyConfig? = null,
|
||||
) {
|
||||
) : AppDetailsItem() {
|
||||
constructor(
|
||||
repository: Repository,
|
||||
preferredRepoId: Long,
|
||||
@@ -181,7 +185,9 @@ data class AppDetailsItem(
|
||||
if (versions == null) {
|
||||
MainButtonState.LOADING
|
||||
} else if (
|
||||
suggestedVersion == null || suggestedVersion.versionCode <= installedVersionCode
|
||||
suggestedVersion == null ||
|
||||
suggestedVersion.versionCode <= installedVersionCode ||
|
||||
suggestedVersion.versionCode <= (appPrefs?.ignoreVersionCodeUpdate ?: 0)
|
||||
) {
|
||||
MainButtonState.NONE
|
||||
} else {
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.fdroid.ui.utils.testApp
|
||||
|
||||
@Composable
|
||||
fun AppDetailsMenu(
|
||||
item: AppDetailsItem,
|
||||
item: LoadedAppDetailsItem,
|
||||
uninstallLauncher: ActivityResultLauncher<Intent>,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
|
||||
@@ -32,28 +32,30 @@ fun AppDetailsTopAppBar(
|
||||
TopAppBar(
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
|
||||
title = {
|
||||
if (topAppBarState.overlappedFraction == 1f) {
|
||||
if (item is LoadedAppDetailsItem && topAppBarState.overlappedFraction == 1f) {
|
||||
Text(item.name, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
},
|
||||
navigationIcon = { if (onBackNav != null) BackButton(onClick = onBackNav) },
|
||||
actions = {
|
||||
val context = LocalContext.current
|
||||
item.actions.shareIntent?.let { shareIntent ->
|
||||
TopAppBarButton(
|
||||
imageVector = Icons.Filled.Share,
|
||||
contentDescription = stringResource(R.string.menu_share),
|
||||
onClick = { context.startActivitySafe(shareIntent) },
|
||||
)
|
||||
}
|
||||
// the launcher needs to be at least here,
|
||||
// because if the menu is dismissed we don't get the result
|
||||
val uninstallLauncher =
|
||||
rememberLauncherForActivityResult(StartActivityForResult()) {
|
||||
item.actions.onUninstallResult(it)
|
||||
if (item is LoadedAppDetailsItem) {
|
||||
val context = LocalContext.current
|
||||
item.actions.shareIntent?.let { shareIntent ->
|
||||
TopAppBarButton(
|
||||
imageVector = Icons.Filled.Share,
|
||||
contentDescription = stringResource(R.string.menu_share),
|
||||
onClick = { context.startActivitySafe(shareIntent) },
|
||||
)
|
||||
}
|
||||
// the launcher needs to be at least here,
|
||||
// because if the menu is dismissed we don't get the result
|
||||
val uninstallLauncher =
|
||||
rememberLauncherForActivityResult(StartActivityForResult()) {
|
||||
item.actions.onUninstallResult(it)
|
||||
}
|
||||
TopAppBarOverflowButton { onDismissRequest ->
|
||||
AppDetailsMenu(item, uninstallLauncher, onDismissRequest)
|
||||
}
|
||||
TopAppBarOverflowButton { onDismissRequest ->
|
||||
AppDetailsMenu(item, uninstallLauncher, onDismissRequest)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
|
||||
@@ -165,7 +165,7 @@ constructor(
|
||||
|
||||
@UiThread
|
||||
fun onUninstallResult(activityResult: ActivityResult) {
|
||||
val name = appDetails.value?.name
|
||||
val name = (appDetails.value as? LoadedAppDetailsItem)?.name
|
||||
val result = appInstallManager.onUninstallResult(packageName, name, activityResult)
|
||||
log.info { "Uninstall result was: $result" }
|
||||
}
|
||||
@@ -195,7 +195,7 @@ constructor(
|
||||
val diskCache = SingletonImageLoader.get(application).diskCache
|
||||
if (diskCache != null)
|
||||
scope.launch {
|
||||
appDetails.value?.phoneScreenshots?.forEach { screenshot ->
|
||||
(appDetails.value as? LoadedAppDetailsItem)?.phoneScreenshots?.forEach { screenshot ->
|
||||
if (screenshot is DownloadRequest) {
|
||||
try {
|
||||
diskCache.remove(screenshot.getCacheKey())
|
||||
@@ -209,7 +209,7 @@ constructor(
|
||||
|
||||
@UiThread
|
||||
fun allowBetaUpdates() {
|
||||
val appPrefs = appDetails.value?.appPrefs ?: return
|
||||
val appPrefs = (appDetails.value as? LoadedAppDetailsItem)?.appPrefs ?: return
|
||||
scope.launch {
|
||||
db.getAppPrefsDao().update(appPrefs.toggleReleaseChannel(RELEASE_CHANNEL_BETA))
|
||||
updatesManager.loadUpdates()
|
||||
@@ -218,7 +218,7 @@ constructor(
|
||||
|
||||
@UiThread
|
||||
fun ignoreAllUpdates() {
|
||||
val appPrefs = appDetails.value?.appPrefs ?: return
|
||||
val appPrefs = (appDetails.value as? LoadedAppDetailsItem)?.appPrefs ?: return
|
||||
scope.launch {
|
||||
db.getAppPrefsDao().update(appPrefs.toggleIgnoreAllUpdates())
|
||||
updatesManager.loadUpdates()
|
||||
@@ -227,8 +227,9 @@ constructor(
|
||||
|
||||
@UiThread
|
||||
fun ignoreThisUpdate() {
|
||||
val appPrefs = appDetails.value?.appPrefs ?: return
|
||||
val versionCode = appDetails.value?.possibleUpdate?.versionCode ?: return
|
||||
val appPrefs = (appDetails.value as? LoadedAppDetailsItem)?.appPrefs ?: return
|
||||
val versionCode =
|
||||
(appDetails.value as? LoadedAppDetailsItem)?.possibleUpdate?.versionCode ?: return
|
||||
scope.launch {
|
||||
db.getAppPrefsDao().update(appPrefs.toggleIgnoreVersionCodeUpdate(versionCode))
|
||||
updatesManager.loadUpdates()
|
||||
|
||||
@@ -30,7 +30,7 @@ import org.fdroid.ui.FDroidContent
|
||||
import org.fdroid.ui.utils.testApp
|
||||
|
||||
@Composable
|
||||
fun AppDetailsWarnings(item: AppDetailsItem, modifier: Modifier = Modifier) {
|
||||
fun AppDetailsWarnings(item: LoadedAppDetailsItem, modifier: Modifier = Modifier) {
|
||||
val (color, string) =
|
||||
when {
|
||||
// app issues take priority
|
||||
|
||||
@@ -16,7 +16,6 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.fdroid.LocaleChooser.getBestLocale
|
||||
import org.fdroid.UpdateChecker
|
||||
import org.fdroid.database.App
|
||||
import org.fdroid.database.AppPrefs
|
||||
import org.fdroid.database.AppVersion
|
||||
import org.fdroid.database.FDroidDatabase
|
||||
@@ -29,12 +28,12 @@ import org.fdroid.install.InstallState
|
||||
import org.fdroid.repo.RepoPreLoader
|
||||
import org.fdroid.settings.SettingsManager
|
||||
import org.fdroid.ui.apps.AppWithIssueItem
|
||||
import org.fdroid.utils.Loaded
|
||||
import org.fdroid.utils.Loading
|
||||
import org.fdroid.utils.sha256
|
||||
|
||||
private const val TAG = "DetailsPresenter"
|
||||
|
||||
// TODO write tests for this function
|
||||
// see: https://github.com/cashapp/molecule?tab=readme-ov-file#testing
|
||||
@Composable
|
||||
fun DetailsPresenter(
|
||||
db: FDroidDatabase,
|
||||
@@ -57,18 +56,24 @@ fun DetailsPresenter(
|
||||
val currentRepoId = currentRepoIdFlow.collectAsState().value
|
||||
val appsWithIssues = appsWithIssuesFlow.collectAsState().value
|
||||
val appDao = db.getAppDao()
|
||||
val app =
|
||||
produceState<App?>(null, currentRepoId) {
|
||||
val loadableApp =
|
||||
produceState(Loading(), currentRepoId) {
|
||||
withContext(dispatcher) {
|
||||
if (currentRepoId == null) {
|
||||
val flow = appDao.getApp(packageName).asFlow()
|
||||
flow.collect { value = it }
|
||||
flow.collect { value = Loaded(it) }
|
||||
} else {
|
||||
value = appDao.getApp(currentRepoId, packageName)
|
||||
value = Loaded(appDao.getApp(currentRepoId, packageName))
|
||||
}
|
||||
}
|
||||
}
|
||||
.value ?: return null
|
||||
.value
|
||||
val app =
|
||||
when (loadableApp) {
|
||||
is Loading -> return null
|
||||
is Loaded if loadableApp.value != null -> loadableApp.value
|
||||
else -> return NotFoundAppDetailsItem
|
||||
}
|
||||
val versions =
|
||||
produceState<List<AppVersion>?>(null, currentRepoId) {
|
||||
withContext(dispatcher) {
|
||||
@@ -100,6 +105,7 @@ fun DetailsPresenter(
|
||||
@Suppress("DEPRECATION") // so far we had issues with the new way of getting sigs
|
||||
packageInfo?.signatures?.get(0)?.let { sha256(it.toByteArray()) }
|
||||
}
|
||||
val installedVersionCode = packageInfo?.let { getLongVersionCode(packageInfo) }
|
||||
val suggestedVersion =
|
||||
remember(versions, appPrefs, installedSigner) {
|
||||
if (versions == null || appPrefs == null) {
|
||||
@@ -109,15 +115,23 @@ fun DetailsPresenter(
|
||||
versions = versions,
|
||||
preferredSigner = installedSigner ?: app.metadata.preferredSigner,
|
||||
releaseChannels = appPrefs.releaseChannels,
|
||||
preferencesGetter = { appPrefs },
|
||||
preferencesGetter = {
|
||||
// the suggested version shouldn't be affected by ignored versions
|
||||
appPrefs.copy(ignoreVersionCodeUpdate = 0)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
val repo =
|
||||
produceState<Repository?>(null, app) {
|
||||
withContext(dispatcher) { value = repoManager.getRepository(app.repoId) }
|
||||
val loadableRepo =
|
||||
produceState(Loading(), app) {
|
||||
withContext(dispatcher) { value = Loaded(repoManager.getRepository(app.repoId)) }
|
||||
}
|
||||
.value ?: return null
|
||||
.value
|
||||
val repo =
|
||||
when (loadableRepo) {
|
||||
is Loading -> return null
|
||||
is Loaded -> loadableRepo.value ?: return NotFoundAppDetailsItem
|
||||
}
|
||||
val repositories =
|
||||
produceState(emptyList(), packageName) {
|
||||
withContext(dispatcher) {
|
||||
@@ -151,27 +165,25 @@ fun DetailsPresenter(
|
||||
)
|
||||
}
|
||||
}
|
||||
val installedVersionCode = packageInfo?.let { getLongVersionCode(packageInfo) }
|
||||
val installedVersion =
|
||||
packageInfo?.let {
|
||||
val installedVersions = versions?.filter { it.versionCode == installedVersionCode }
|
||||
when (installedVersions?.size) {
|
||||
null -> null
|
||||
0 -> null
|
||||
1 -> installedVersions.first()
|
||||
// more than version with the same version code, find a matching signer
|
||||
else ->
|
||||
installedVersions.find {
|
||||
val versionSigners = it.signer?.sha256?.toSet()
|
||||
// F-Droid allows versions without a signer entry, allow those
|
||||
if (versionSigners != null && installedSigner != null) {
|
||||
versionSigners.intersect(setOf(installedSigner)).isNotEmpty()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
val installedVersion = packageInfo?.let {
|
||||
val installedVersions = versions?.filter { it.versionCode == installedVersionCode }
|
||||
when (installedVersions?.size) {
|
||||
null -> null
|
||||
0 -> null
|
||||
1 -> installedVersions.first()
|
||||
// more than version with the same version code, find a matching signer
|
||||
else ->
|
||||
installedVersions.find {
|
||||
val versionSigners = it.signer?.sha256?.toSet()
|
||||
// F-Droid allows versions without a signer entry, allow those
|
||||
if (versionSigners != null && installedSigner != null) {
|
||||
versionSigners.intersect(setOf(installedSigner)).isNotEmpty()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val authorName = app.authorName
|
||||
val authorHasMoreThanOneApp =
|
||||
if (authorName == null) false
|
||||
@@ -192,7 +204,7 @@ fun DetailsPresenter(
|
||||
Log.d(TAG, " versions: ${versions?.size}")
|
||||
Log.d(TAG, " appPrefs: $appPrefs")
|
||||
Log.d(TAG, " installState: $installState")
|
||||
return AppDetailsItem(
|
||||
return LoadedAppDetailsItem(
|
||||
repository = repo,
|
||||
preferredRepoId = preferredRepoId,
|
||||
repositories = repositories,
|
||||
@@ -210,7 +222,7 @@ fun DetailsPresenter(
|
||||
allowBetaVersions = viewModel::allowBetaUpdates,
|
||||
onAntiFeaturesOnboardingSeen = viewModel::onAntiFeaturesOnboardingSeen,
|
||||
ignoreAllUpdates =
|
||||
if (installedVersionCode == null) {
|
||||
if (installedVersionCode == null && (appPrefs == null || !appPrefs.ignoreAllUpdates)) {
|
||||
null
|
||||
} else {
|
||||
viewModel::ignoreAllUpdates
|
||||
@@ -257,7 +269,7 @@ fun DetailsPresenter(
|
||||
if (!signerCompatible || installState.showProgress) {
|
||||
false
|
||||
} else {
|
||||
(installedVersion?.versionCode ?: 0) < version.versionCode
|
||||
(installedVersionCode ?: 0) < version.versionCode
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.fdroid.ui.FDroidContent
|
||||
import org.fdroid.ui.utils.testApp
|
||||
|
||||
@Composable
|
||||
fun TechnicalInfo(item: AppDetailsItem) {
|
||||
fun TechnicalInfo(item: LoadedAppDetailsItem) {
|
||||
val items = mutableMapOf(stringResource(R.string.package_name) to item.app.packageName)
|
||||
if (item.installedVersionCode != null) {
|
||||
items[stringResource(R.string.installed_version)] =
|
||||
|
||||
@@ -47,7 +47,7 @@ import org.fdroid.ui.utils.testApp
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
fun Versions(item: AppDetailsItem, scrollUp: suspend () -> Unit) {
|
||||
fun Versions(item: LoadedAppDetailsItem, scrollUp: suspend () -> Unit) {
|
||||
ExpandableSection(
|
||||
icon = rememberVectorPainter(Icons.Default.AccessTime),
|
||||
title = stringResource(R.string.versions),
|
||||
|
||||
@@ -105,7 +105,7 @@ fun AddRepoPreviewScreen(
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
val address = "https://example.org"
|
||||
val repo = getRepository(address)
|
||||
val repo = getRepository(address = address)
|
||||
val app1 =
|
||||
object : MinimalApp {
|
||||
override val repoId = 0L
|
||||
|
||||
@@ -140,7 +140,7 @@ fun RepoPreviewHeader(
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 720, heightDp = 360)
|
||||
fun RepoPreviewScreenNewMirrorPreview() {
|
||||
val repo = getRepository("https://example.org")
|
||||
val repo = getRepository(address = "https://example.org")
|
||||
FDroidContent {
|
||||
RepoPreviewHeader(
|
||||
Fetching("https://mirror.example.org", repo, emptyList(), IsNewMirror(0L)),
|
||||
@@ -155,7 +155,7 @@ fun RepoPreviewScreenNewMirrorPreview() {
|
||||
@Composable
|
||||
@Preview
|
||||
fun RepoPreviewScreenNewRepoAndNewMirrorPreview() {
|
||||
val repo = getRepository("https://example.org")
|
||||
val repo = getRepository(address = "https://example.org")
|
||||
FDroidContent {
|
||||
RepoPreviewHeader(
|
||||
state =
|
||||
@@ -177,7 +177,7 @@ fun RepoPreviewScreenNewRepoAndNewMirrorPreview() {
|
||||
@Composable
|
||||
fun RepoPreviewScreenExistingRepoPreview() {
|
||||
val address = "https://example.org"
|
||||
val repo = getRepository(address)
|
||||
val repo = getRepository(address = address)
|
||||
FDroidContent {
|
||||
RepoPreviewHeader(
|
||||
Fetching(address, repo, emptyList(), IsExistingRepository(0L)),
|
||||
@@ -192,7 +192,7 @@ fun RepoPreviewScreenExistingRepoPreview() {
|
||||
@Preview
|
||||
@Composable
|
||||
fun RepoPreviewScreenExistingMirrorPreview() {
|
||||
val repo = getRepository("https://example.org")
|
||||
val repo = getRepository(address = "https://example.org")
|
||||
FDroidContent {
|
||||
RepoPreviewHeader(
|
||||
Fetching("https://mirror.example.org", repo, emptyList(), IsExistingMirror(0L)),
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@@ -49,12 +50,18 @@ fun AppSearchInputField(
|
||||
}
|
||||
}
|
||||
}
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
SearchBarDefaults.InputField(
|
||||
modifier = modifier,
|
||||
searchBarState = searchBarState,
|
||||
textFieldState = textFieldState,
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
onSearch = { if (it.isSearchable()) scope.launch { onSearch(it) } },
|
||||
onSearch = {
|
||||
if (it.isSearchable()) {
|
||||
keyboardController?.hide()
|
||||
scope.launch { onSearch(it) }
|
||||
}
|
||||
},
|
||||
placeholder = { Text(stringResource(R.string.search_placeholder)) },
|
||||
trailingIcon = {
|
||||
if (textFieldState.text.isNotEmpty()) {
|
||||
|
||||
@@ -59,6 +59,7 @@ import org.fdroid.R
|
||||
import org.fdroid.settings.SettingsConstants.AutoUpdateValues
|
||||
import org.fdroid.settings.SettingsConstants.AutoUpdateValues.Always
|
||||
import org.fdroid.settings.SettingsConstants.AutoUpdateValues.Never
|
||||
import org.fdroid.settings.SettingsConstants.AutoUpdateValues.OnlyWhenOpenApp
|
||||
import org.fdroid.settings.SettingsConstants.AutoUpdateValues.OnlyWifi
|
||||
import org.fdroid.settings.SettingsConstants.MirrorChooserValues
|
||||
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_AUTO_UPDATES
|
||||
@@ -214,7 +215,8 @@ fun Settings(model: SettingsModel, onSaveLogcat: (Uri?) -> Unit, onBackClicked:
|
||||
)
|
||||
},
|
||||
summary = { strValue ->
|
||||
if (strValue != Never.name) {
|
||||
val value = strValue.toAutoUpdateValue()
|
||||
if (value.workerEnabled) {
|
||||
val nextUpdate = model.nextRepoUpdateFlow.collectAsState(Long.MAX_VALUE).value
|
||||
val nextUpdateStr =
|
||||
if (nextUpdate == Long.MAX_VALUE) {
|
||||
@@ -228,17 +230,20 @@ fun Settings(model: SettingsModel, onSaveLogcat: (Uri?) -> Unit, onBackClicked:
|
||||
stringResource(R.string.auto_update_time, nextUpdate.asRelativeTimeString())
|
||||
}
|
||||
val s =
|
||||
if (strValue == OnlyWifi.name) {
|
||||
if (value == OnlyWifi) {
|
||||
stringResource(R.string.pref_repo_updates_summary_only_wifi)
|
||||
} else if (strValue == Always.name) {
|
||||
} else if (value == Always) {
|
||||
stringResource(R.string.pref_repo_updates_summary_always)
|
||||
} else error("Unknown value: $strValue")
|
||||
Text(s + "\n" + nextUpdateStr)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.pref_repo_updates_summary_never),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
val s =
|
||||
if (value == OnlyWhenOpenApp) {
|
||||
stringResource(R.string.pref_repo_updates_summary_only_when_open_app)
|
||||
} else {
|
||||
stringResource(R.string.pref_repo_updates_summary_never)
|
||||
}
|
||||
Text(text = s, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
values = AutoUpdateValues.entries.map { it.name },
|
||||
@@ -247,6 +252,7 @@ fun Settings(model: SettingsModel, onSaveLogcat: (Uri?) -> Unit, onBackClicked:
|
||||
when (value.toAutoUpdateValue()) {
|
||||
OnlyWifi -> res.getString(R.string.pref_auto_updates_only_wifi)
|
||||
Always -> res.getString(R.string.pref_auto_updates_only_always)
|
||||
OnlyWhenOpenApp -> res.getString(R.string.pref_auto_updates_only_only_when_open_app)
|
||||
Never -> res.getString(R.string.pref_auto_updates_only_never)
|
||||
}
|
||||
)
|
||||
@@ -295,12 +301,15 @@ fun Settings(model: SettingsModel, onSaveLogcat: (Uri?) -> Unit, onBackClicked:
|
||||
}
|
||||
Text(s)
|
||||
},
|
||||
values = AutoUpdateValues.entries.map { it.name },
|
||||
// Exclude the OnlyWhenOpenApp option here
|
||||
values =
|
||||
AutoUpdateValues.entries.mapNotNull { if (it == OnlyWhenOpenApp) null else it.name },
|
||||
valueToText = { value: String ->
|
||||
AnnotatedString(
|
||||
when (value.toAutoUpdateValue()) {
|
||||
OnlyWifi -> res.getString(R.string.pref_auto_updates_only_wifi)
|
||||
Always -> res.getString(R.string.pref_auto_updates_only_always)
|
||||
OnlyWhenOpenApp -> res.getString(R.string.pref_auto_updates_only_only_when_open_app)
|
||||
Never -> res.getString(R.string.pref_auto_updates_only_never)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.annotation.RestrictTo
|
||||
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
|
||||
import java.util.concurrent.TimeUnit.DAYS
|
||||
import java.util.concurrent.TimeUnit.HOURS
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
import org.fdroid.database.AppListSortOrder
|
||||
import org.fdroid.database.AppMetadata
|
||||
import org.fdroid.database.AppPrefs
|
||||
@@ -33,7 +32,7 @@ import org.fdroid.ui.apps.MyAppsModel
|
||||
import org.fdroid.ui.categories.CategoryItem
|
||||
import org.fdroid.ui.details.AntiFeature
|
||||
import org.fdroid.ui.details.AppDetailsActions
|
||||
import org.fdroid.ui.details.AppDetailsItem
|
||||
import org.fdroid.ui.details.LoadedAppDetailsItem
|
||||
import org.fdroid.ui.details.VersionItem
|
||||
import org.fdroid.ui.lists.AntiFeatureItem
|
||||
import org.fdroid.ui.lists.AppListActions
|
||||
@@ -143,14 +142,24 @@ val categoryItems =
|
||||
CategoryItem("doesn't exist", "Foo bar"),
|
||||
)
|
||||
|
||||
private val description =
|
||||
"NewPipe does not use any Google framework libraries, or the YouTube API. " +
|
||||
"It only parses the website in order to gain the information it needs. " +
|
||||
"Therefore this app can be used on devices without Google Services installed. " +
|
||||
"Also, you don't need a YouTube account to use NewPipe, and it's FLOSS.\n\n" +
|
||||
LoremIpsum(128).values.joinToString(" ")
|
||||
|
||||
val testApp =
|
||||
AppDetailsItem(
|
||||
LoadedAppDetailsItem(
|
||||
app =
|
||||
AppMetadata(
|
||||
repoId = 1,
|
||||
packageName = "org.schabi.newpipe",
|
||||
added = 1441756800000,
|
||||
lastUpdated = 1747214796000,
|
||||
name = mapOf("en-US" to "New Pipe"),
|
||||
summary = mapOf("en-US" to "Lightweight YouTube frontend"),
|
||||
description = mapOf("en-US" to description),
|
||||
webSite = "https://newpipe.net",
|
||||
changelog = "https://github.com/TeamNewPipe/NewPipe/releases",
|
||||
license = "GPL-3.0-or-later",
|
||||
@@ -179,12 +188,7 @@ val testApp =
|
||||
appPrefs = AppPrefs("org.schabi.newpipe"),
|
||||
name = "New Pipe",
|
||||
summary = "Lightweight YouTube frontend",
|
||||
description =
|
||||
"NewPipe does not use any Google framework libraries, or the YouTube API. " +
|
||||
"It only parses the website in order to gain the information it needs. " +
|
||||
"Therefore this app can be used on devices without Google Services installed. " +
|
||||
"Also, you don't need a YouTube account to use NewPipe, and it's FLOSS.\n\n" +
|
||||
LoremIpsum(128).values.joinToString(" "),
|
||||
description = description,
|
||||
categories = categoryItems.subList(0, 5),
|
||||
antiFeatures =
|
||||
listOf(
|
||||
@@ -411,6 +415,8 @@ fun getMyAppsInfo(model: MyAppsModel): MyAppsInfo =
|
||||
|
||||
override fun confirmAppInstall(packageName: String, state: InstallConfirmationState) {}
|
||||
|
||||
override fun clearInstallingApps() {}
|
||||
|
||||
override fun ignoreAppIssue(item: AppWithIssueItem) {}
|
||||
|
||||
override fun onUpdatesHintSeen() {}
|
||||
@@ -519,7 +525,7 @@ val repoItems =
|
||||
address = "http://example.org",
|
||||
name = "F-Droid",
|
||||
icon = null,
|
||||
timestamp = System.currentTimeMillis() - MINUTES.toMillis(18),
|
||||
timestamp = System.currentTimeMillis() - HOURS.toMillis(18),
|
||||
lastUpdated = System.currentTimeMillis() - 9_999_999,
|
||||
weight = 1,
|
||||
enabled = true,
|
||||
@@ -541,7 +547,7 @@ val repoItems =
|
||||
address = "http://example.net",
|
||||
name = "My first Repo",
|
||||
icon = null,
|
||||
timestamp = System.currentTimeMillis() - MINUTES.toMillis(14),
|
||||
timestamp = System.currentTimeMillis() - DAYS.toMillis(4),
|
||||
lastUpdated = System.currentTimeMillis(),
|
||||
weight = 3,
|
||||
enabled = true,
|
||||
@@ -620,13 +626,14 @@ fun getRepoDetailsInfo(
|
||||
}
|
||||
|
||||
fun getRepository(
|
||||
repoId: Long = 42L,
|
||||
address: String = "https://example.org/repo",
|
||||
username: String? = "foo",
|
||||
password: String? = "bar",
|
||||
lastError: String? = "NotFoundException FooBar technical blabla",
|
||||
) =
|
||||
Repository(
|
||||
repoId = 42L,
|
||||
repoId = repoId,
|
||||
address = address,
|
||||
timestamp = System.currentTimeMillis() - DAYS.toMillis(4),
|
||||
formatVersion = IndexFormatVersion.ONE,
|
||||
|
||||
@@ -49,7 +49,7 @@ constructor(
|
||||
@JvmStatic
|
||||
fun scheduleOrCancel(context: Context, autoUpdate: AutoUpdateValues) {
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
if (autoUpdate != AutoUpdateValues.Never) {
|
||||
if (autoUpdate.workerEnabled) {
|
||||
Log.i(TAG, "scheduleOrCancel: enqueueUniquePeriodicWork")
|
||||
val networkType =
|
||||
if (autoUpdate == AutoUpdateValues.Always) {
|
||||
|
||||
7
app/src/main/kotlin/org/fdroid/utils/Loadable.kt
Normal file
7
app/src/main/kotlin/org/fdroid/utils/Loadable.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
package org.fdroid.utils
|
||||
|
||||
sealed class Loadable<T>
|
||||
|
||||
class Loading<T> : Loadable<T>()
|
||||
|
||||
class Loaded<T>(val value: T) : Loadable<T>()
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.fdroid.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -30,6 +32,21 @@ fun getLogName(context: Context): String {
|
||||
return "${context.packageName}-$time"
|
||||
}
|
||||
|
||||
fun getCurrentLocation(context: Context): String {
|
||||
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
|
||||
return tm.simCountryIso
|
||||
?: tm.networkCountryIso
|
||||
?: run {
|
||||
val localeList = LocaleListCompat.getDefault()
|
||||
localeList.get(0)?.country ?: Locale.getDefault().country
|
||||
}
|
||||
}
|
||||
|
||||
fun isChina(context: Context): Boolean {
|
||||
val country = getCurrentLocation(context)
|
||||
return country.equals("cn", ignoreCase = true)
|
||||
}
|
||||
|
||||
val isFull: Boolean
|
||||
get() = FLAVOR.startsWith("full")
|
||||
val isBasic: Boolean
|
||||
|
||||
@@ -695,7 +695,6 @@
|
||||
<string name="category_group_storage">الملفات والتخزين</string>
|
||||
<string name="category_group_interests">اهتمامات</string>
|
||||
<string name="category_group_misc">متنوّع</string>
|
||||
<string name="author_by">بواسطة %1$s</string>
|
||||
<string name="last_updated">آخر تحديث: %1$s</string>
|
||||
<string name="last_updated_with_size">آخر تحديث: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">يجهز التثبيت…</string>
|
||||
|
||||
@@ -672,7 +672,6 @@
|
||||
<string name="category_group_storage">Файлы і Сховішча</string>
|
||||
<string name="category_group_interests">Інтарэсы</string>
|
||||
<string name="category_group_misc">Розныя</string>
|
||||
<string name="author_by">Ад %1$s</string>
|
||||
<string name="last_updated">Абноўлена: %1$s</string>
|
||||
<string name="last_updated_with_size">Абноўлена: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Падрыхтоўка ўсталёўкі…</string>
|
||||
|
||||
@@ -639,7 +639,6 @@
|
||||
<string name="category_group_storage">Файлове и хранилища</string>
|
||||
<string name="category_group_interests">Интереси</string>
|
||||
<string name="category_group_misc">Разни</string>
|
||||
<string name="author_by">От %1$s</string>
|
||||
<string name="last_updated">Последно обновяване: %1$s</string>
|
||||
<string name="last_updated_with_size">Последно обновяване: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Подготовка за инсталиране…</string>
|
||||
|
||||
@@ -650,7 +650,6 @@
|
||||
<string name="category_group_storage">Arxius i emmagatzematge</string>
|
||||
<string name="category_group_interests">Interessos</string>
|
||||
<string name="category_group_misc">Miscel·lània</string>
|
||||
<string name="author_by">Per %1$s</string>
|
||||
<string name="last_updated">Últimes actualitzacions: %1$s</string>
|
||||
<string name="last_updated_with_size">Últimes actualitzacions: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Preparant la instal·lació…</string>
|
||||
|
||||
@@ -653,7 +653,6 @@
|
||||
<string name="category_group_storage">Soubory a úložiště</string>
|
||||
<string name="category_group_interests">Zájmy</string>
|
||||
<string name="category_group_misc">Různé</string>
|
||||
<string name="author_by">Od %1$s</string>
|
||||
<string name="last_updated">Naposledy aktualizováno: %1$s</string>
|
||||
<string name="last_updated_with_size">Naposledy aktualizováno: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Příprava instalace…</string>
|
||||
|
||||
@@ -624,7 +624,6 @@
|
||||
<string name="app_list_new">Nye apps</string>
|
||||
<string name="my_apps_export_installed_apps">Eksportér appliste</string>
|
||||
<string name="category_group_tools">Værktøjer</string>
|
||||
<string name="author_by">Af %1$s</string>
|
||||
<string name="category_group_communication">Kommunikation</string>
|
||||
<string name="category_group_network">Netværk</string>
|
||||
<string name="donate_title">Donér</string>
|
||||
|
||||
@@ -649,7 +649,6 @@
|
||||
<string name="category_group_storage">Dateien und Speicher</string>
|
||||
<string name="category_group_interests">Interessen</string>
|
||||
<string name="category_group_misc">Verschiedenes</string>
|
||||
<string name="author_by">Von %1$s</string>
|
||||
<string name="last_updated">Zuletzt aktualisiert: %1$s</string>
|
||||
<string name="last_updated_with_size">Zuletzt aktualisiert: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Installation wird vorbereitet …</string>
|
||||
|
||||
@@ -638,7 +638,6 @@
|
||||
<string name="category_group_storage">Αρχεία & Αποθηκευτικός Χώρος</string>
|
||||
<string name="category_group_interests">Ενδιαφέροντα</string>
|
||||
<string name="category_group_misc">Διάφορα</string>
|
||||
<string name="author_by">Από %1$s</string>
|
||||
<string name="last_updated">Τελευταία ενημέρωση: %1$s</string>
|
||||
<string name="last_updated_with_size">Τελευταία ενημέρωση: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Προετοιμασία εγκατάστασης…</string>
|
||||
|
||||
@@ -658,7 +658,6 @@
|
||||
<string name="category_group_storage">Files & Storage</string>
|
||||
<string name="category_group_interests">Interests</string>
|
||||
<string name="category_group_misc">Miscellaneous</string>
|
||||
<string name="author_by">By %1$s</string>
|
||||
<string name="last_updated">Last updated: %1$s</string>
|
||||
<string name="last_updated_with_size">Last updated: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Preparing installation…</string>
|
||||
|
||||
@@ -638,7 +638,6 @@
|
||||
<string name="category_group_storage">Dosieroj kaj konservado</string>
|
||||
<string name="category_group_interests">Interesiĝoj</string>
|
||||
<string name="category_group_misc">Diversaĵoj</string>
|
||||
<string name="author_by">Fare de %1$s</string>
|
||||
<string name="last_updated">Lasta ĝisdatigo: %1$s</string>
|
||||
<string name="last_updated_with_size">Lasta ĝisdatigo: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Preparado de instalado…</string>
|
||||
|
||||
@@ -676,7 +676,6 @@
|
||||
<string name="category_group_storage">Archivos y Almacenamiento</string>
|
||||
<string name="category_group_interests">Intereses</string>
|
||||
<string name="category_group_misc">Misceláneo</string>
|
||||
<string name="author_by">Por %1$s</string>
|
||||
<string name="last_updated">Últimamente actualizado: %1$s</string>
|
||||
<string name="last_updated_with_size">Últimamente actualizado: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Preparando instalación…</string>
|
||||
|
||||
@@ -652,7 +652,6 @@
|
||||
<string name="category_group_storage">Failid ja andmeruum</string>
|
||||
<string name="category_group_interests">Huvid ja hobid</string>
|
||||
<string name="category_group_misc">Varia</string>
|
||||
<string name="author_by">Arendajalt %1$s</string>
|
||||
<string name="last_updated">Viimati uuendatud: %1$s</string>
|
||||
<string name="last_updated_with_size">Viimati uuendatud: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Valmistun paigaldama…</string>
|
||||
|
||||
@@ -664,7 +664,6 @@
|
||||
<string name="category_group_storage">Fitxategiak eta biltegiratzea</string>
|
||||
<string name="category_group_interests">Interesak</string>
|
||||
<string name="category_group_misc">Denetarik</string>
|
||||
<string name="author_by">%1$s-ek egina</string>
|
||||
<string name="last_updated">Azken eguneraketa: %1$s</string>
|
||||
<string name="last_updated_with_size">Azken eguneraketa: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Instalazioa prestatzen…</string>
|
||||
|
||||
@@ -640,7 +640,6 @@
|
||||
<string name="category_group_network">شبکه</string>
|
||||
<string name="category_group_storage">پروندهها و ذخیرهسازی</string>
|
||||
<string name="category_group_misc">متفرّقه</string>
|
||||
<string name="author_by">به دست %1$s</string>
|
||||
<string name="last_updated">آخرین بهروز رسانی: %1$s</string>
|
||||
<string name="last_updated_with_size">آخرین بهروز رسانی: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">آماده سازی نصب…</string>
|
||||
|
||||
@@ -625,7 +625,6 @@
|
||||
<string name="category_group_network">Verkko</string>
|
||||
<string name="category_group_storage">Tiedostot ja tallennus</string>
|
||||
<string name="category_group_misc">Sekalaiset</string>
|
||||
<string name="author_by">Tekijältä %1$s</string>
|
||||
<string name="last_updated">Viimeksi päivitetty: %1$s</string>
|
||||
<string name="last_updated_with_size">Viimeksi päivitetty: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Valmistellaan asennusta…</string>
|
||||
|
||||
@@ -666,7 +666,6 @@
|
||||
<string name="category_group_storage">Fichiers & Stockage</string>
|
||||
<string name="category_group_interests">Intérêts</string>
|
||||
<string name="category_group_misc">Divers</string>
|
||||
<string name="author_by">De %1$s</string>
|
||||
<string name="last_updated">Dernière mise à jour : %1$s</string>
|
||||
<string name="last_updated_with_size">Dernière mise à jour : %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Préparation de l\'installation…</string>
|
||||
|
||||
@@ -695,7 +695,6 @@
|
||||
<string name="category_group_storage">Comhaid & Stóráil</string>
|
||||
<string name="category_group_interests">Leasanna</string>
|
||||
<string name="category_group_misc">Ilghnéitheach</string>
|
||||
<string name="author_by">Faoi %1$s</string>
|
||||
<string name="last_updated">Nuashonraithe go deireanach: %1$s</string>
|
||||
<string name="last_updated_with_size">Nuashonraithe go deireanach: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Ag ullmhú suiteála…</string>
|
||||
|
||||
@@ -684,7 +684,6 @@
|
||||
<string name="category_group_storage">קבצים ואחסון</string>
|
||||
<string name="category_group_interests">תחומי עניין</string>
|
||||
<string name="category_group_misc">שונות</string>
|
||||
<string name="author_by">על ידי %1$s</string>
|
||||
<string name="last_updated">עדכון אחרון: %1$s</string>
|
||||
<string name="last_updated_with_size">עדכון אחרון: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">בהכנות להתקנה…</string>
|
||||
|
||||
@@ -658,7 +658,6 @@
|
||||
<string name="category_group_storage">Datoteke i spremišta</string>
|
||||
<string name="category_group_interests">Interesi</string>
|
||||
<string name="category_group_misc">Razno</string>
|
||||
<string name="author_by">Autor: %1$s</string>
|
||||
<string name="last_updated">Zadnje aktualiziranje: %1$s</string>
|
||||
<string name="last_updated_with_size">Zadnje aktualiziranje: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Pripremanje instalacije …</string>
|
||||
|
||||
@@ -641,7 +641,6 @@
|
||||
<string name="category_group_storage">Fájlok és tárolók</string>
|
||||
<string name="category_group_interests">Érdeklődési kör</string>
|
||||
<string name="category_group_misc">Egyéb</string>
|
||||
<string name="author_by">%1$s -től</string>
|
||||
<string name="last_updated">Utolsó frissítés: %1$s</string>
|
||||
<string name="last_updated_with_size">Utolsó frissítés: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Telepítés előkészítése…</string>
|
||||
|
||||
@@ -644,7 +644,6 @@
|
||||
<string name="category_group_storage">Fail & Penyimpanan</string>
|
||||
<string name="category_group_interests">Minat</string>
|
||||
<string name="category_group_misc">Lain-Lain</string>
|
||||
<string name="author_by">Oleh %1$s</string>
|
||||
<string name="last_updated">Terakhir diperbarui: %1$s</string>
|
||||
<string name="last_updated_with_size">Terakhir diperbarui: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Mempersiapkan instalasi…</string>
|
||||
|
||||
@@ -640,7 +640,6 @@
|
||||
<string name="category_group_storage">Skrár og geymslurými</string>
|
||||
<string name="category_group_interests">Áhugamál</string>
|
||||
<string name="category_group_misc">Ýmislegt</string>
|
||||
<string name="author_by">Frá %1$s</string>
|
||||
<string name="last_updated">Síðast uppfært: %1$s</string>
|
||||
<string name="last_updated_with_size">Síðast uppfært: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Undirbý uppsetningu…</string>
|
||||
|
||||
@@ -664,7 +664,6 @@
|
||||
<string name="category_group_storage">Archiviazione</string>
|
||||
<string name="category_group_interests">Interessi</string>
|
||||
<string name="category_group_misc">Miscellanea</string>
|
||||
<string name="author_by">Da %1$s</string>
|
||||
<string name="last_updated">Ultimo aggiornamento: %1$s</string>
|
||||
<string name="last_updated_with_size">Ultimo aggiornamento: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Preparando l\'installazione…</string>
|
||||
|
||||
@@ -645,7 +645,6 @@
|
||||
<string name="category_group_storage">ファイル&ストレージ</string>
|
||||
<string name="category_group_interests">趣味</string>
|
||||
<string name="category_group_misc">その他</string>
|
||||
<string name="author_by">%1$s による</string>
|
||||
<string name="last_updated">最終更新: %1$s</string>
|
||||
<string name="last_updated_with_size">最終更新: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">インストールの準備中…</string>
|
||||
|
||||
@@ -419,7 +419,6 @@
|
||||
<string name="tts_author_name">Ameskar %1$s</string>
|
||||
<string name="app_list_new">Isnasen imaynuten</string>
|
||||
<string name="app_list_all">Akk isnasen</string>
|
||||
<string name="author_by">Sɣur %1$s</string>
|
||||
<string name="status_install_preparing">Aheyyi n usbeddi…</string>
|
||||
<string name="whats_new_title">D acu i yellan d amaynut</string>
|
||||
<string name="copy_link">Nɣel aseɣwen</string>
|
||||
|
||||
@@ -630,7 +630,6 @@
|
||||
<string name="category_group_storage">파일 & 저장소</string>
|
||||
<string name="category_group_interests">관심분야</string>
|
||||
<string name="category_group_misc">기타</string>
|
||||
<string name="author_by">개발자: %1$s</string>
|
||||
<string name="last_updated">마지막 업데이트: %1$s</string>
|
||||
<string name="last_updated_with_size">마지막 업데이트: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">설치 준비중…</string>
|
||||
|
||||
@@ -611,7 +611,6 @@
|
||||
<string name="category_group_storage">Failai ir saugykla</string>
|
||||
<string name="category_group_interests">Pomėgiai</string>
|
||||
<string name="category_group_misc">Kiti</string>
|
||||
<string name="author_by">Sukurta %1$s</string>
|
||||
<string name="last_updated">Paskutinį kartą atnaujinta: %1$s</string>
|
||||
<string name="last_updated_with_size">Paskutinį kartą atnaujinta: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Įdiegimas ruošiamas…</string>
|
||||
|
||||
@@ -649,7 +649,6 @@
|
||||
<string name="category_group_network">Tīkls</string>
|
||||
<string name="category_group_storage">Datnes un krātuve</string>
|
||||
<string name="category_group_misc">Dažādi</string>
|
||||
<string name="author_by">No %1$s</string>
|
||||
<string name="last_updated">Pēdējoreiz atjaunināta: %1$s</string>
|
||||
<string name="last_updated_with_size">Pēdējoreiz atjaunināta: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Sagatavo uzstādīšanu…</string>
|
||||
|
||||
@@ -307,7 +307,6 @@
|
||||
<string name="category_group_storage">Датотеки и меморија</string>
|
||||
<string name="category_group_interests">Интереси</string>
|
||||
<string name="category_group_misc">Останато</string>
|
||||
<string name="author_by">Направено од %1$s</string>
|
||||
<string name="last_updated">Последно надоградено: %1$s</string>
|
||||
<string name="last_updated_with_size">Последно надоградено: %1$s%2$s</string>
|
||||
<string name="status_install_preparing">Подготовка на инсталација…</string>
|
||||
|
||||
@@ -655,7 +655,6 @@
|
||||
<string name="category_group_storage">Filer & Lagring</string>
|
||||
<string name="category_group_interests">Interesser</string>
|
||||
<string name="category_group_misc">Diverse</string>
|
||||
<string name="author_by">Fra %1$s</string>
|
||||
<string name="last_updated">Sist oppdatert: %1$s</string>
|
||||
<string name="last_updated_with_size">Sist oppdatert: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Forbereder installasjon…</string>
|
||||
|
||||
@@ -646,7 +646,6 @@
|
||||
<string name="category_group_storage">Bestanden & Opslag</string>
|
||||
<string name="category_group_interests">Interesses</string>
|
||||
<string name="category_group_misc">Diversen</string>
|
||||
<string name="author_by">Door %1$s</string>
|
||||
<string name="last_updated">Laatst geüpdatet: %1$s</string>
|
||||
<string name="last_updated_with_size">Laatst geüpdatet: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Installatie voorbereiden…</string>
|
||||
|
||||
@@ -684,7 +684,6 @@
|
||||
<string name="category_group_storage">Pliki i pamięć</string>
|
||||
<string name="category_group_interests">Zainteresowania</string>
|
||||
<string name="category_group_misc">Różne</string>
|
||||
<string name="author_by">Przez %1$s</string>
|
||||
<string name="last_updated">Ostatnia aktualizacja: %1$s</string>
|
||||
<string name="last_updated_with_size">Ostatnia aktualizacja: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Przygotowywanie instalacji…</string>
|
||||
|
||||
@@ -654,7 +654,6 @@
|
||||
<string name="category_group_device">Dispositivo</string>
|
||||
<string name="category_group_storage">Arquivos & Armazenamento</string>
|
||||
<string name="category_group_interests">Interesses</string>
|
||||
<string name="author_by">Por %1$s</string>
|
||||
<string name="last_updated_with_size">Última atualização: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Preparando instalação…</string>
|
||||
<string name="whats_new_title">O que há de novo</string>
|
||||
|
||||
@@ -651,7 +651,6 @@
|
||||
<string name="category_group_storage">Ficheiros & armazenamento</string>
|
||||
<string name="category_group_interests">Interesses</string>
|
||||
<string name="category_group_misc">Diversos</string>
|
||||
<string name="author_by">Por %1$s</string>
|
||||
<string name="last_updated">Última atualização: %1$s</string>
|
||||
<string name="last_updated_with_size">Última atualização: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">A preparar instalação…</string>
|
||||
|
||||
@@ -665,7 +665,6 @@
|
||||
<string name="category_group_storage">Ficheiros & armazenamento</string>
|
||||
<string name="category_group_interests">Interesses</string>
|
||||
<string name="category_group_misc">Diversos</string>
|
||||
<string name="author_by">Por %1$s</string>
|
||||
<string name="last_updated">Última atualização: %1$s</string>
|
||||
<string name="last_updated_with_size">Última atualização: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">A preparar instalação…</string>
|
||||
|
||||
@@ -661,7 +661,6 @@
|
||||
<string name="category_group_storage">Fișiere și stocare</string>
|
||||
<string name="category_group_interests">Pasiuni</string>
|
||||
<string name="category_group_misc">Diverse</string>
|
||||
<string name="author_by">De către %1$s</string>
|
||||
<string name="last_updated">Ultima actualizare: %1$s</string>
|
||||
<string name="last_updated_with_size">Ultima actualizare: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Pregătire instalare…</string>
|
||||
|
||||
@@ -665,7 +665,6 @@
|
||||
<string name="category_group_storage">Файлы и хранилище</string>
|
||||
<string name="category_group_interests">Интересы</string>
|
||||
<string name="category_group_misc">Разное</string>
|
||||
<string name="author_by">От %1$s</string>
|
||||
<string name="last_updated">Последнее обновление: %1$s</string>
|
||||
<string name="last_updated_with_size">Последнее обновление: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Подготовка к установке…</string>
|
||||
|
||||
@@ -589,7 +589,6 @@
|
||||
<string name="category_group_storage">Súbory a uložisko</string>
|
||||
<string name="category_group_interests">Záujmy</string>
|
||||
<string name="category_group_misc">Rôzne</string>
|
||||
<string name="author_by">Od %1$s</string>
|
||||
<string name="last_updated">Naposledy aktualizované: %1$s</string>
|
||||
<string name="last_updated_with_size">Naposledy aktualizované: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Príprava inštalácie…</string>
|
||||
|
||||
@@ -563,7 +563,6 @@
|
||||
<string name="category_group_storage">Datoteke in shranjevanje</string>
|
||||
<string name="category_group_interests">Interesi</string>
|
||||
<string name="category_group_misc">Razno</string>
|
||||
<string name="author_by">Od %1$s</string>
|
||||
<string name="last_updated">Zadnja posodobitev: %1$s</string>
|
||||
<string name="last_updated_with_size">Zadnja posodobitev: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Priprava namestitve …</string>
|
||||
|
||||
@@ -637,7 +637,6 @@
|
||||
<string name="category_group_storage">Kartela & Depozitim</string>
|
||||
<string name="category_group_interests">Interesa</string>
|
||||
<string name="category_group_misc">Të ndryshme</string>
|
||||
<string name="author_by">Nga %1$s</string>
|
||||
<string name="last_updated">Përditësuar së fundi më: %1$s</string>
|
||||
<string name="anti_features_title">Ky aplikacion përmban anti-veçori</string>
|
||||
<string name="developer_contact">Kontakt zhvilluesi</string>
|
||||
|
||||
@@ -637,7 +637,6 @@
|
||||
<string name="category_group_storage">Датотеке и складиште</string>
|
||||
<string name="category_group_interests">Интересовања</string>
|
||||
<string name="category_group_misc">Разно</string>
|
||||
<string name="author_by">Од %1$s</string>
|
||||
<string name="last_updated">Последње ажурирање: %1$s</string>
|
||||
<string name="last_updated_with_size">Последње ажурирано: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Припрема се инсталација…</string>
|
||||
|
||||
@@ -655,7 +655,6 @@
|
||||
<string name="category_group_storage">Filer & lagring</string>
|
||||
<string name="category_group_interests">Intressen</string>
|
||||
<string name="category_group_misc">Diverse</string>
|
||||
<string name="author_by">Av %1$s</string>
|
||||
<string name="last_updated">Senast uppdaterad: %1$s</string>
|
||||
<string name="last_updated_with_size">Senast uppdaterad: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Förbereder installationen…</string>
|
||||
|
||||
@@ -624,7 +624,6 @@
|
||||
<string name="app_list_most_downloaded">Iliyopakuliwa zaidi</string>
|
||||
<string name="app_list_author">Programu na %s</string>
|
||||
<string name="search_no_results">Hakuna programu zilizopatikana\n\nJaribu kutumia maneno machache ya utafutaji au uongeze hazina zaidi</string>
|
||||
<string name="author_by">Na %1$s</string>
|
||||
<string name="last_updated_with_size">Ilisasishwa mwisho: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Inatayarisha usakinishaji…</string>
|
||||
<string name="anti_features_title">Programu hii ina vipengele vya kupinga</string>
|
||||
|
||||
@@ -636,7 +636,6 @@
|
||||
<string name="category_group_media">Eğlence & Medya</string>
|
||||
<string name="category_group_communication">İletişim</string>
|
||||
<string name="category_group_interests">İlgi Alanları</string>
|
||||
<string name="author_by">%1$s tarafından</string>
|
||||
<string name="app_name_full_nightly">F-Nightly</string>
|
||||
<string name="app_name_basic_nightly">F-Nightly Temel</string>
|
||||
<string name="first_start_loading">Uygulama depoları getiriliyor…</string>
|
||||
|
||||
@@ -666,7 +666,6 @@
|
||||
<string name="category_group_storage">Файли та зберігання</string>
|
||||
<string name="category_group_interests">Інтереси</string>
|
||||
<string name="category_group_misc">Різне</string>
|
||||
<string name="author_by">Від %1$s</string>
|
||||
<string name="last_updated">Останні оновлено: %1$s</string>
|
||||
<string name="last_updated_with_size">Останні оновлено: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Підготовка до встановлення…</string>
|
||||
|
||||
@@ -613,7 +613,6 @@
|
||||
<string name="category_group_storage">Tệp & Lưu trữ</string>
|
||||
<string name="category_group_interests">Sở thích</string>
|
||||
<string name="category_group_misc">Linh tinh</string>
|
||||
<string name="author_by">Bởi %1$s</string>
|
||||
<string name="last_updated">Lần cuối cập nhật: %1$s</string>
|
||||
<string name="last_updated_with_size">Lần cuối cập nhật: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">Chuẩn bị cài đặt…</string>
|
||||
|
||||
@@ -638,7 +638,6 @@
|
||||
<string name="category_group_storage">文件和存储</string>
|
||||
<string name="category_group_interests">兴趣</string>
|
||||
<string name="category_group_misc">杂项</string>
|
||||
<string name="author_by">由 %1$s 开发</string>
|
||||
<string name="last_updated">上次更新:%1$s</string>
|
||||
<string name="last_updated_with_size">上次更新:%1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">正在准备安装…</string>
|
||||
|
||||
@@ -636,7 +636,6 @@
|
||||
<string name="last_updated_with_size">最後更新: %1$s (%2$s)</string>
|
||||
<string name="status_install_preparing">正在準備安裝…</string>
|
||||
<string name="whats_new_title">有什麼新事物</string>
|
||||
<string name="author_by">由 %1$s 提供</string>
|
||||
<string name="donate_title">課金</string>
|
||||
<string name="anti_features_title">此應用程式包含負面功能</string>
|
||||
<string name="developer_contact">聯絡開發者</string>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
<!-- The %1$s placeholder will be replaced by the app name, e.g. "F-Droid" -->
|
||||
<string name="my_apps_empty">Only apps available in %1$s\'s repositories are shown here</string>
|
||||
<string name="my_apps_header_recently_installed_apps">Recently installed</string>
|
||||
<string name="my_apps_header_apps_with_issue">Apps with issues</string>
|
||||
<string name="my_apps_ignore_dialog_title">Hide issue</string>
|
||||
<string name="my_apps_ignore_dialog_text">The issue with this version of \"%1$s\" will get ignored</string>
|
||||
@@ -58,32 +59,33 @@
|
||||
<string name="onboarding_app_list_filter_message">Use filters to only show apps from specific categories or repositories. You can also change the sort order.</string>
|
||||
<string name="got_it">Got it</string>
|
||||
<string name="clear">Clear</string>
|
||||
<string name="clear_all">Clear all</string>
|
||||
<string name="hide">Hide</string>
|
||||
|
||||
<string name="category_group_summary_productivity">Calendars, tasks, clocks & notes</string>
|
||||
<string name="category_group_summary_tools">Navigation, weather, calculators & converters</string>
|
||||
<string name="category_group_summary_wallets">Finance, money & pass managers</string>
|
||||
<string name="category_group_summary_media">Music, podcasts, games & ebook readers</string>
|
||||
<string name="category_group_summary_communication">Email, messaging & social networks</string>
|
||||
<string name="category_group_summary_device">Launcher, keyboard, security & theming</string>
|
||||
<string name="category_group_summary_network">Browser, VPNs, proxies & firewall</string>
|
||||
<string name="category_group_summary_storage">Files, gallery, cloud sync & encryption</string>
|
||||
<string name="category_group_summary_interests">Science, sports, drawing & recipes</string>
|
||||
<string name="category_group_summary_misc">Uncategorized</string>
|
||||
|
||||
<string name="category_group_productivity">Productivity</string>
|
||||
<string name="category_group_tools">Tools</string>
|
||||
<string name="category_group_wallets">Finances & Wallets</string>
|
||||
<string name="category_group_media">Entertainment & Media</string>
|
||||
<string name="category_group_communication">Communication</string>
|
||||
<string name="category_group_device">Device</string>
|
||||
<string name="category_group_games">Games</string>
|
||||
<string name="category_group_network">Network</string>
|
||||
<string name="category_group_storage">Files & Storage</string>
|
||||
<string name="category_group_interests">Interests</string>
|
||||
<string name="category_group_misc">Miscellaneous</string>
|
||||
|
||||
<!-- Used in app details page to show who is the author of the app -->
|
||||
<string name="author_by">By %1$s</string>
|
||||
<string name="category_group_summary_productivity">Calendars, tasks, clocks & notes</string>
|
||||
<string name="category_group_summary_tools">Navigation, weather, calculators & converters</string>
|
||||
<string name="category_group_summary_wallets">Finance, money & pass managers</string>
|
||||
<string name="category_group_summary_media">Music, podcasts & ebook readers</string>
|
||||
<string name="category_group_summary_communication">Email, messaging & social networks</string>
|
||||
<string name="category_group_summary_device">Launcher, keyboard, security & theming</string>
|
||||
<string name="category_group_summary_games">Board, card & party games</string>
|
||||
<string name="category_group_summary_network">Browser, VPNs, proxies & firewall</string>
|
||||
<string name="category_group_summary_storage">Files, gallery, cloud sync & encryption</string>
|
||||
<string name="category_group_summary_interests">Science, sports, drawing & recipes</string>
|
||||
<string name="category_group_summary_misc">Uncategorized</string>
|
||||
|
||||
<string name="last_updated">Last updated: %1$s</string>
|
||||
<!-- The placeholder in round brackets will be replaced with the size of the app -->
|
||||
<string name="last_updated_with_size">Last updated: %1$s (%2$s)</string>
|
||||
@@ -102,6 +104,7 @@
|
||||
</string>
|
||||
<string name="installIncompatible">App incompatible with your device, install anyway?</string>
|
||||
<string name="version">Version</string>
|
||||
<!-- Used in app details page to show who is the author of the app -->
|
||||
<string name="by_author_format">by %s</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="prompt_to_send_crash_reports">Prompt to send crash reports</string>
|
||||
@@ -183,6 +186,7 @@
|
||||
<string name="pref_language_summary">Open system language settings</string>
|
||||
<string name="pref_auto_updates_only_wifi">Only on Wi-Fi</string>
|
||||
<string name="pref_auto_updates_only_always">Always (even on mobile data)</string>
|
||||
<string name="pref_auto_updates_only_only_when_open_app">Only when opening the app</string>
|
||||
<string name="pref_auto_updates_only_never">Never</string>
|
||||
<string name="pref_auto_updates_summary_only_wifi">Download and update apps daily when on Wi-Fi and the device isn\'t being used</string>
|
||||
<string name="pref_auto_updates_summary_always">Download and update apps daily even on mobile data when the device isn\'t being used</string>
|
||||
@@ -190,6 +194,7 @@
|
||||
<string name="pref_repo_updates_title">Check for updates</string>
|
||||
<string name="pref_repo_updates_summary_only_wifi">Periodically fetch app updates from repositories only when on Wi-Fi</string>
|
||||
<string name="pref_repo_updates_summary_always">Periodically fetch app updates from repositories even when on mobile data</string>
|
||||
<string name="pref_repo_updates_summary_only_when_open_app">Check for updates only when opened • Apps will become outdated</string>
|
||||
<string name="pref_repo_updates_summary_never">Don\'t check for updates • Apps will become outdated</string>
|
||||
<string name="pref_category_network">Network</string>
|
||||
<string name="pref_mirror_chooser_title">Download mirror selection</string>
|
||||
@@ -956,6 +961,8 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="notification_channel_install_success_description">Displays a notification after apps were installed automatically</string>
|
||||
<string name="notification_channel_updates_available_title">Available app updates</string>
|
||||
<string name="notification_channel_updates_available_description">Displays a notification after repositories were updated and app updates were found</string>
|
||||
<string name="notification_channel_self_update_title">Self-update</string>
|
||||
<string name="notification_channel_self_update_description">Notifies when the app updated itself and was closed, so you can launch it again easily</string>
|
||||
|
||||
<string name="notification_repo_update_default">Connecting to %1$s…</string>
|
||||
<string name="notification_repo_update_downloading">Downloaded %1$s from %2$s</string>
|
||||
@@ -979,6 +986,8 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="notification_installing_section_confirmation">Needs user confirmation:</string>
|
||||
<string name="notification_installing_section_installed">Installed:</string>
|
||||
<string name="notification_installing_confirmation">Tap to confirm</string>
|
||||
<!-- The placeholder %s will be replaced with the name of this app -->
|
||||
<string name="notification_self_update_title">%s was updated. Tap to open</string>
|
||||
|
||||
<!-- Used by the TTS engine when showing a filter "chip" in the search box -->
|
||||
<string name="tts_category_name">Category %1$s</string>
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.fdroid.install.InstallState
|
||||
import org.fdroid.ui.ScreenshotTest
|
||||
import org.fdroid.ui.utils.getMyAppsInfo
|
||||
import org.fdroid.ui.utils.getPreviewVersion
|
||||
import org.fdroid.ui.utils.myAppsModel
|
||||
|
||||
@Composable
|
||||
@PreviewTest
|
||||
@@ -69,7 +70,12 @@ fun MyAppsInstalledAndIssues() {
|
||||
val model =
|
||||
MyAppsModel(
|
||||
appUpdates = emptyList(),
|
||||
installingApps = emptyList(),
|
||||
installingApps =
|
||||
listOf(
|
||||
myAppsModel.installingApps[0].copy(
|
||||
installState = InstallState.Installed("Installed App", "0.5.2", null, 1337L, null)
|
||||
)
|
||||
),
|
||||
appsWithIssue = getAppIssues(),
|
||||
installedApps = getInstalledApps(),
|
||||
showUpdatesHint = false,
|
||||
|
||||
@@ -10,13 +10,15 @@ import com.android.tools.screenshot.PreviewTest
|
||||
import org.fdroid.download.NetworkState
|
||||
import org.fdroid.install.InstallState
|
||||
import org.fdroid.ui.FDroidContent
|
||||
import org.fdroid.ui.utils.getRepository
|
||||
import org.fdroid.ui.utils.testApp
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@PreviewTest
|
||||
fun DetailsHeaderOpenTest() {
|
||||
FDroidContent { Column { AppDetailsHeader(testApp, PaddingValues(top = 8.dp)) } }
|
||||
FDroidContent { Column { AppDetailsHeader(testApp.copy(
|
||||
categories = testApp.categories?.subList(0, 2),), {}, PaddingValues(top = 8.dp)) } }
|
||||
}
|
||||
|
||||
@Preview
|
||||
@@ -30,9 +32,13 @@ private fun DetailsHeaderInstallTest() {
|
||||
installedVersion = null,
|
||||
installedVersionCode = null,
|
||||
installedVersionName = null,
|
||||
repositories = listOf(getRepository(testApp.app.repoId), getRepository()),
|
||||
preferredRepoId = 42L,
|
||||
categories = testApp.categories?.subList(0, 2),
|
||||
suggestedVersion = testApp.versions?.first()?.version,
|
||||
networkState = NetworkState(true, isMetered = true),
|
||||
),
|
||||
{},
|
||||
PaddingValues(top = 8.dp),
|
||||
)
|
||||
}
|
||||
@@ -47,9 +53,11 @@ private fun DetailsHeaderUpdateTest() {
|
||||
Column {
|
||||
AppDetailsHeader(
|
||||
testApp.copy(
|
||||
categories = testApp.categories?.subList(0, 2),
|
||||
suggestedVersion = testApp.versions?.first()?.version,
|
||||
networkState = NetworkState(true, isMetered = true),
|
||||
),
|
||||
{},
|
||||
PaddingValues(top = 8.dp),
|
||||
)
|
||||
}
|
||||
@@ -62,8 +70,8 @@ private fun DetailsHeaderUpdateTest() {
|
||||
private fun DetailsHeaderLoadingTest() {
|
||||
FDroidContent {
|
||||
Column {
|
||||
val app = testApp.copy(versions = null)
|
||||
AppDetailsHeader(app, PaddingValues(top = 8.dp))
|
||||
val app = testApp.copy(categories = testApp.categories?.subList(0, 2), versions = null)
|
||||
AppDetailsHeader(app, {}, PaddingValues(top = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,9 +96,10 @@ private fun DetailsHeaderDownloadingTest() {
|
||||
totalBytes = 1024 * 1024 * 8,
|
||||
startMillis = now - 2000,
|
||||
),
|
||||
categories = testApp.categories?.subList(0, 2),
|
||||
networkState = NetworkState(true, isMetered = true),
|
||||
)
|
||||
AppDetailsHeader(app, PaddingValues(top = 8.dp), now)
|
||||
AppDetailsHeader(app, {}, PaddingValues(top = 8.dp), now)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +113,7 @@ private fun DetailsHeaderDownloadingNightTest() {
|
||||
val now = System.currentTimeMillis()
|
||||
val app =
|
||||
testApp.copy(
|
||||
categories = testApp.categories?.subList(0, 2),
|
||||
installState =
|
||||
InstallState.Downloading(
|
||||
name = "",
|
||||
@@ -117,7 +127,7 @@ private fun DetailsHeaderDownloadingNightTest() {
|
||||
),
|
||||
networkState = NetworkState(true, isMetered = true),
|
||||
)
|
||||
AppDetailsHeader(app, PaddingValues(top = 8.dp), now)
|
||||
AppDetailsHeader(app, {}, PaddingValues(top = 8.dp), now)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,10 +140,11 @@ private fun DetailsHeaderStartingTest() {
|
||||
Column {
|
||||
val app =
|
||||
testApp.copy(
|
||||
categories = testApp.categories?.subList(0, 2),
|
||||
installState = InstallState.Starting("", "", "", 23),
|
||||
networkState = NetworkState(true, isMetered = true),
|
||||
)
|
||||
AppDetailsHeader(app, PaddingValues(top = 16.dp))
|
||||
AppDetailsHeader(app, {}, PaddingValues(top = 16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,31 @@ import org.fdroid.ui.utils.testApp
|
||||
fun DetailsTest() =
|
||||
ScreenshotTest(showBottomBar = false) {
|
||||
AppDetails(
|
||||
testApp.copy(
|
||||
versions = emptyList(),
|
||||
networkState = NetworkState(isOnline = true, isMetered = false),
|
||||
),
|
||||
{},
|
||||
{},
|
||||
item =
|
||||
testApp.copy(
|
||||
versions = emptyList(),
|
||||
categories = testApp.categories?.subList(0, 2),
|
||||
phoneScreenshots =
|
||||
listOf(
|
||||
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
|
||||
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
|
||||
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
|
||||
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
|
||||
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
|
||||
),
|
||||
networkState = NetworkState(isOnline = true, isMetered = false),
|
||||
),
|
||||
onNav = {},
|
||||
onBackNav = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@PreviewTest
|
||||
fun AppDetailsNotFoundTest() =
|
||||
ScreenshotTest(showBottomBar = false) { AppDetails(NotFoundAppDetailsItem, {}, {}) }
|
||||
|
||||
@Composable
|
||||
@PreviewTest
|
||||
@Preview(showBackground = true, showSystemUi = true, heightDp = 3000)
|
||||
@@ -31,19 +47,21 @@ fun DetailsInstallTest() =
|
||||
// reduces information, so we can see the bottom of the expanded page
|
||||
// shows install button instead of open button
|
||||
AppDetails(
|
||||
testApp.copy(
|
||||
versions = listOf(testApp.versions!!.first()),
|
||||
whatsNew = null,
|
||||
antiFeatures = null,
|
||||
installedVersion = null,
|
||||
installedVersionCode = null,
|
||||
installedVersionName = null,
|
||||
suggestedVersion = testApp.versions.first().version,
|
||||
actions = testApp.actions.copy(launchIntent = null),
|
||||
networkState = NetworkState(isOnline = true, isMetered = false),
|
||||
),
|
||||
{},
|
||||
{},
|
||||
item =
|
||||
testApp.copy(
|
||||
versions = listOf(testApp.versions!!.first()),
|
||||
whatsNew = null,
|
||||
antiFeatures = null,
|
||||
installedVersion = null,
|
||||
installedVersionCode = null,
|
||||
installedVersionName = null,
|
||||
suggestedVersion = testApp.versions.first().version,
|
||||
categories = testApp.categories?.subList(0, 2),
|
||||
actions = testApp.actions.copy(launchIntent = null),
|
||||
networkState = NetworkState(isOnline = true, isMetered = false),
|
||||
),
|
||||
onNav = {},
|
||||
onBackNav = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,23 +72,25 @@ fun DetailsUpdateTest() =
|
||||
ScreenshotTest(showBottomBar = false) {
|
||||
// show update and open button, hide donation options
|
||||
AppDetails(
|
||||
testApp.copy(
|
||||
whatsNew = null,
|
||||
antiFeatures = null,
|
||||
app =
|
||||
testApp.app.copy(
|
||||
donate = null,
|
||||
bitcoin = null,
|
||||
litecoin = null,
|
||||
liberapay = null,
|
||||
liberapayID = null,
|
||||
openCollective = null,
|
||||
),
|
||||
suggestedVersion = testApp.versions?.first()?.version,
|
||||
networkState = NetworkState(isOnline = true, isMetered = false),
|
||||
),
|
||||
{},
|
||||
{},
|
||||
item =
|
||||
testApp.copy(
|
||||
whatsNew = null,
|
||||
antiFeatures = null,
|
||||
app =
|
||||
testApp.app.copy(
|
||||
donate = null,
|
||||
bitcoin = null,
|
||||
litecoin = null,
|
||||
liberapay = null,
|
||||
liberapayID = null,
|
||||
openCollective = null,
|
||||
),
|
||||
suggestedVersion = testApp.versions?.first()?.version,
|
||||
categories = testApp.categories?.subList(0, 2),
|
||||
networkState = NetworkState(isOnline = true, isMetered = false),
|
||||
),
|
||||
onNav = {},
|
||||
onBackNav = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,4 +103,10 @@ fun DetailsUpdateTest() =
|
||||
heightDp = 3000,
|
||||
)
|
||||
fun DetailsNightTest() =
|
||||
ScreenshotTest(showBottomBar = false) { AppDetails(testApp.copy(whatsNew = "foo bar"), {}, {}) }
|
||||
ScreenshotTest(showBottomBar = false) {
|
||||
AppDetails(
|
||||
item = testApp.copy(categories = testApp.categories?.subList(0, 2), whatsNew = "foo bar"),
|
||||
onNav = {},
|
||||
onBackNav = {},
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user