diff --git a/.github/labeler.yml b/.github/labeler.yml index 4fca91ccd05..0000fd33d44 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -284,5 +284,6 @@ Prices: - any-glob-to-any-file: 'packages/smooth_app/lib/pages/onboarding/currency_selector.dart' - any-glob-to-any-file: 'packages/smooth_app/lib/pages/onboarding/currency_selector_helper.dart' - - +router: +- changed-files: + - any-glob-to-any-file: 'packages/smooth_app/lib/pages/navigator/app_navigator.dart' diff --git a/.github/workflows/android-release-to-org-openfoodfacts-scanner.yml b/.github/workflows/android-release-to-org-openfoodfacts-scanner.yml index 6da9452f366..5bef5c55971 100644 --- a/.github/workflows/android-release-to-org-openfoodfacts-scanner.yml +++ b/.github/workflows/android-release-to-org-openfoodfacts-scanner.yml @@ -63,7 +63,7 @@ jobs: - run: echo Release type ${{ inputs.RELEASE_TYPE }} + Build type ${{ inputs.BUILD_TYPE }} + Tag name null if not github release ${{ inputs.TAG_NAME }} - name: Setup Java JDK - uses: actions/setup-java@v4.4.0 + uses: actions/setup-java@v4.5.0 with: distribution: 'zulu' java-version: ${{ env.JAVA_VERSION }} diff --git a/.github/workflows/dartdoc.yml b/.github/workflows/dartdoc.yml new file mode 100644 index 00000000000..25b8000db46 --- /dev/null +++ b/.github/workflows/dartdoc.yml @@ -0,0 +1,36 @@ + name: GitHub Pages Deploy Action + on: + push: + branches: + - "develop" + #- "dartdoc-smoothie" + jobs: + deploy-pages: + name: Deploy to GitHub Pages + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./packages/smooth_app + steps: + + - name: Chekout code + uses: actions/checkout@v4 + + - name: Setup Flutter + uses: actions/cache@v4 + with: + path: ${{ runner.tool_cache }}/flutter + key: flutter-2.5.0-stable + - uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: 2.5.0 + + - name: Run Dartdoc + run: pub global activate dartdoc && dartdoc + + - name: Deploy API documentation to Github Pages + uses: JamesIves/github-pages-deploy-action@v4.6.8 + with: + BRANCH: gh-pages + FOLDER: packages/smooth_app/doc/api/ diff --git a/.github/workflows/ios-release-to-org-openfoodfacts-scanner.yml b/.github/workflows/ios-release-to-org-openfoodfacts-scanner.yml index e77d4592f88..566dd1869a4 100644 --- a/.github/workflows/ios-release-to-org-openfoodfacts-scanner.yml +++ b/.github/workflows/ios-release-to-org-openfoodfacts-scanner.yml @@ -61,7 +61,7 @@ jobs: run: bundle install working-directory: ./packages/smooth_app/android/ - name: Setup Java JDK - uses: actions/setup-java@v4.4.0 + uses: actions/setup-java@v4.5.0 with: distribution: 'zulu' java-version: ${{ env.JAVA_VERSION }} diff --git a/.github/workflows/postsubmit.yml b/.github/workflows/postsubmit.yml index 243cc88c9f2..fed9ccddf9b 100644 --- a/.github/workflows/postsubmit.yml +++ b/.github/workflows/postsubmit.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Java JDK - uses: actions/setup-java@v4.4.0 + uses: actions/setup-java@v4.5.0 with: distribution: 'zulu' java-version: 17 diff --git a/.github/workflows/waldo_sessions.yml b/.github/workflows/waldo_sessions.yml index b7f0ea59a15..81eb0d98e1d 100644 --- a/.github/workflows/waldo_sessions.yml +++ b/.github/workflows/waldo_sessions.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Setup Java JDK - uses: actions/setup-java@v4.4.0 + uses: actions/setup-java@v4.5.0 with: distribution: 'zulu' java-version: 17 diff --git a/flutter-version.txt b/flutter-version.txt index ffba2c8db92..057aa0d01e9 100644 --- a/flutter-version.txt +++ b/flutter-version.txt @@ -1 +1 @@ -3.24.3 \ No newline at end of file +3.24.4 \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000000..a95314760b6 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,26 @@ +# Where to find documentation +docs_dir: doc + +# Link to Github on every page +repo_url: https://github.com/openfoodfacts/smooth-app +edit_uri: blob/develop/docs/ + +site_name: Open Food Facts mobile app +site_dir: gh_pages + +theme: + name: material + palette: + primary: beige + text: black + logo: assets/app/logo_text_black.svg + +markdown_extensions: + - footnotes + - mdx_truly_sane_lists + - pymdownx.highlight + - pymdownx.superfences + +plugins: + - awesome-pages + - search diff --git a/packages/smooth_app/android/Gemfile.lock b/packages/smooth_app/android/Gemfile.lock index b087ba1e0f8..444c18b1c6e 100644 --- a/packages/smooth_app/android/Gemfile.lock +++ b/packages/smooth_app/android/Gemfile.lock @@ -16,17 +16,17 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.984.0) - aws-sdk-core (3.209.1) + aws-partitions (1.992.0) + aws-sdk-core (3.210.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) + aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.94.0) - aws-sdk-core (~> 3, >= 3.207.0) + aws-sdk-kms (1.95.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.167.0) - aws-sdk-core (~> 3, >= 3.207.0) + aws-sdk-s3 (1.169.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.10.0) @@ -74,7 +74,7 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.224.0) + fastlane (2.225.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -90,6 +90,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -117,6 +118,8 @@ GEM xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-plugin-flutter_dart_version_manager (0.1.5) fastlane-plugin-versioning (0.6.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) @@ -192,6 +195,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) diff --git a/packages/smooth_app/android/app/src/main/AndroidManifest.xml b/packages/smooth_app/android/app/src/main/AndroidManifest.xml index 8cd6614a446..212f5f042f2 100644 --- a/packages/smooth_app/android/app/src/main/AndroidManifest.xml +++ b/packages/smooth_app/android/app/src/main/AndroidManifest.xml @@ -52,7 +52,11 @@ - + + + + + diff --git a/packages/smooth_app/ios/Gemfile.lock b/packages/smooth_app/ios/Gemfile.lock index f11bf6f002a..b47a9626f5f 100644 --- a/packages/smooth_app/ios/Gemfile.lock +++ b/packages/smooth_app/ios/Gemfile.lock @@ -16,17 +16,17 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.984.0) - aws-sdk-core (3.209.1) + aws-partitions (1.992.0) + aws-sdk-core (3.210.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) + aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.94.0) - aws-sdk-core (~> 3, >= 3.207.0) + aws-sdk-kms (1.95.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.167.0) - aws-sdk-core (~> 3, >= 3.207.0) + aws-sdk-s3 (1.169.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.10.0) @@ -75,7 +75,7 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.224.0) + fastlane (2.225.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -91,6 +91,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -118,6 +119,8 @@ GEM xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-plugin-flutter_dart_version_manager (0.1.5) fastlane-plugin-versioning (0.6.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) @@ -193,6 +196,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) diff --git a/packages/smooth_app/ios/Podfile.lock b/packages/smooth_app/ios/Podfile.lock index c2373332d6c..657bc696e02 100644 --- a/packages/smooth_app/ios/Podfile.lock +++ b/packages/smooth_app/ios/Podfile.lock @@ -140,13 +140,14 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite (0.0.3): + - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - url_launcher_ios (0.0.1): - Flutter - webview_flutter_wkwebview (0.0.1): - Flutter + - FlutterMacOS DEPENDENCIES: - app_settings (from `.symlinks/plugins/app_settings/ios`) @@ -174,9 +175,9 @@ DEPENDENCIES: - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `.symlinks/plugins/sqflite/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) SPEC REPOS: trunk: @@ -251,12 +252,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sqflite: - :path: ".symlinks/plugins/sqflite/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" webview_flutter_wkwebview: - :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc @@ -303,9 +304,9 @@ SPEC CHECKSUMS: sentry_flutter: 0eb93e5279eb41e2392212afe1ccd2fecb4f8cbe share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 PODFILE CHECKSUM: e840dd57ba2b03bcb6fdba293a27c5f82151a80a diff --git a/packages/smooth_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/black_icon-1024.png b/packages/smooth_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/black_icon-1024.png index 930ffbc2101..371d4f94af3 100644 Binary files a/packages/smooth_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/black_icon-1024.png and b/packages/smooth_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/black_icon-1024.png differ diff --git a/packages/smooth_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/tinted_icon-1024.png b/packages/smooth_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/tinted_icon-1024.png index 11efe483231..68b75ce62cc 100644 Binary files a/packages/smooth_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/tinted_icon-1024.png and b/packages/smooth_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/tinted_icon-1024.png differ diff --git a/packages/smooth_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/white_icon-1024.png b/packages/smooth_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/white_icon-1024.png index 9e931a597f2..cbb8af90586 100644 Binary files a/packages/smooth_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/white_icon-1024.png and b/packages/smooth_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/white_icon-1024.png differ diff --git a/packages/smooth_app/ios/Runner/Runner.entitlements b/packages/smooth_app/ios/Runner/Runner.entitlements index a6d5cc08c4a..550e1759ddd 100644 --- a/packages/smooth_app/ios/Runner/Runner.entitlements +++ b/packages/smooth_app/ios/Runner/Runner.entitlements @@ -4,6 +4,9 @@ com.apple.developer.associated-domains + applinks:prices.openfoodfacts.org + applinks:prices.openfoodfacts.net + applinks:ae.openfoodfacts.org applinks:ar.openfoodfacts.org applinks:at-en.openfoodfacts.org @@ -201,6 +204,303 @@ applinks:www.openfoodfacts.net applinks:za.openfoodfacts.net applinks:zh.openfoodfacts.net + + applinks:ae.openbeautyfacts.org + applinks:ar.openbeautyfacts.org + applinks:at-en.openbeautyfacts.org + applinks:at.openbeautyfacts.org + applinks:au.openbeautyfacts.org + applinks:be-de.openbeautyfacts.org + applinks:be-en.openbeautyfacts.org + applinks:be-fr.openbeautyfacts.org + applinks:be.openbeautyfacts.org + applinks:bg.openbeautyfacts.org + applinks:br.openbeautyfacts.org + applinks:ca-fr.openbeautyfacts.org + applinks:ca.openbeautyfacts.org + applinks:ch-en.openbeautyfacts.org + applinks:ch-fr.openbeautyfacts.org + applinks:ch-it.openbeautyfacts.org + applinks:ch.openbeautyfacts.org + applinks:cl.openbeautyfacts.org + applinks:cn-en.openbeautyfacts.org + applinks:cn.openbeautyfacts.org + applinks:co.openbeautyfacts.org + applinks:cz.openbeautyfacts.org + applinks:de-en.openbeautyfacts.org + applinks:de.openbeautyfacts.org + applinks:dk.openbeautyfacts.org + applinks:dz-fr.openbeautyfacts.org + applinks:dz.openbeautyfacts.org + applinks:en.openbeautyfacts.org + applinks:es-ca.openbeautyfacts.org + applinks:es-en.openbeautyfacts.org + applinks:es-eu.openbeautyfacts.org + applinks:es-gl.openbeautyfacts.org + applinks:es.openbeautyfacts.org + applinks:fi.openbeautyfacts.org + applinks:fr-en.openbeautyfacts.org + applinks:fr-es.openbeautyfacts.org + applinks:fr.openbeautyfacts.org + applinks:gf.openbeautyfacts.org + applinks:gp.openbeautyfacts.org + applinks:gr.openbeautyfacts.org + applinks:hk.openbeautyfacts.org + applinks:hk-zh.openbeautyfacts.org + applinks:hu.openbeautyfacts.org + applinks:id.openbeautyfacts.org + applinks:ie.openbeautyfacts.org + applinks:il.openbeautyfacts.org + applinks:in.openbeautyfacts.org + applinks:ir.openbeautyfacts.org + applinks:it.openbeautyfacts.org + applinks:jp.openbeautyfacts.org + applinks:ke.openbeautyfacts.org + applinks:kh.openbeautyfacts.org + applinks:lu.openbeautyfacts.org + applinks:ma-fr.openbeautyfacts.org + applinks:ma.openbeautyfacts.org + applinks:mq.openbeautyfacts.org + applinks:mx.openbeautyfacts.org + applinks:nl-en.openbeautyfacts.org + applinks:nl.openbeautyfacts.org + applinks:no.openbeautyfacts.org + applinks:nz.openbeautyfacts.org + applinks:pe.openbeautyfacts.org + applinks:pf.openbeautyfacts.org + applinks:ph.openbeautyfacts.org + applinks:pl-en.openbeautyfacts.org + applinks:pl.openbeautyfacts.org + applinks:openbeautyfacts.org + applinks:pt-en.openbeautyfacts.org + applinks:pt.openbeautyfacts.org + applinks:re.openbeautyfacts.org + applinks:ro.openbeautyfacts.org + applinks:ru-en.openbeautyfacts.org + applinks:ru.openbeautyfacts.org + applinks:sa.openbeautyfacts.org + applinks:se.openbeautyfacts.org + applinks:sg.openbeautyfacts.org + applinks:sg-zh.openbeautyfacts.org + applinks:sk.openbeautyfacts.org + applinks:th.openbeautyfacts.org + applinks:tn-en.openbeautyfacts.org + applinks:tn.openbeautyfacts.org + applinks:tr.openbeautyfacts.org + applinks:tw.openbeautyfacts.org + applinks:uk.openbeautyfacts.org + applinks:us-es.openbeautyfacts.org + applinks:us.openbeautyfacts.org + applinks:world-de.openbeautyfacts.org + applinks:world-en.openbeautyfacts.org + applinks:world-es.openbeautyfacts.org + applinks:world-fr.openbeautyfacts.org + applinks:world-it.openbeautyfacts.org + applinks:world.openbeautyfacts.org + applinks:world-pt.openbeautyfacts.org + applinks:world-ru.openbeautyfacts.org + applinks:world-zh.openbeautyfacts.org + applinks:www.openbeautyfacts.org + applinks:za.openbeautyfacts.org + applinks:zh.openbeautyfacts.org + + applinks:ae.openproductsfacts.org + applinks:ar.openproductsfacts.org + applinks:at-en.openproductsfacts.org + applinks:at.openproductsfacts.org + applinks:au.openproductsfacts.org + applinks:be-de.openproductsfacts.org + applinks:be-en.openproductsfacts.org + applinks:be-fr.openproductsfacts.org + applinks:be.openproductsfacts.org + applinks:bg.openproductsfacts.org + applinks:br.openproductsfacts.org + applinks:ca-fr.openproductsfacts.org + applinks:ca.openproductsfacts.org + applinks:ch-en.openproductsfacts.org + applinks:ch-fr.openproductsfacts.org + applinks:ch-it.openproductsfacts.org + applinks:ch.openproductsfacts.org + applinks:cl.openproductsfacts.org + applinks:cn-en.openproductsfacts.org + applinks:cn.openproductsfacts.org + applinks:co.openproductsfacts.org + applinks:cz.openproductsfacts.org + applinks:de-en.openproductsfacts.org + applinks:de.openproductsfacts.org + applinks:dk.openproductsfacts.org + applinks:dz-fr.openproductsfacts.org + applinks:dz.openproductsfacts.org + applinks:en.openproductsfacts.org + applinks:es-ca.openproductsfacts.org + applinks:es-en.openproductsfacts.org + applinks:es-eu.openproductsfacts.org + applinks:es-gl.openproductsfacts.org + applinks:es.openproductsfacts.org + applinks:fi.openproductsfacts.org + applinks:fr-en.openproductsfacts.org + applinks:fr-es.openproductsfacts.org + applinks:fr.openproductsfacts.org + applinks:gf.openproductsfacts.org + applinks:gp.openproductsfacts.org + applinks:gr.openproductsfacts.org + applinks:hk.openproductsfacts.org + applinks:hk-zh.openproductsfacts.org + applinks:hu.openproductsfacts.org + applinks:id.openproductsfacts.org + applinks:ie.openproductsfacts.org + applinks:il.openproductsfacts.org + applinks:in.openproductsfacts.org + applinks:ir.openproductsfacts.org + applinks:it.openproductsfacts.org + applinks:jp.openproductsfacts.org + applinks:ke.openproductsfacts.org + applinks:kh.openproductsfacts.org + applinks:lu.openproductsfacts.org + applinks:ma-fr.openproductsfacts.org + applinks:ma.openproductsfacts.org + applinks:mq.openproductsfacts.org + applinks:mx.openproductsfacts.org + applinks:nl-en.openproductsfacts.org + applinks:nl.openproductsfacts.org + applinks:no.openproductsfacts.org + applinks:nz.openproductsfacts.org + applinks:pe.openproductsfacts.org + applinks:pf.openproductsfacts.org + applinks:ph.openproductsfacts.org + applinks:pl-en.openproductsfacts.org + applinks:pl.openproductsfacts.org + applinks:openproductsfacts.org + applinks:pt-en.openproductsfacts.org + applinks:pt.openproductsfacts.org + applinks:re.openproductsfacts.org + applinks:ro.openproductsfacts.org + applinks:ru-en.openproductsfacts.org + applinks:ru.openproductsfacts.org + applinks:sa.openproductsfacts.org + applinks:se.openproductsfacts.org + applinks:sg.openproductsfacts.org + applinks:sg-zh.openproductsfacts.org + applinks:sk.openproductsfacts.org + applinks:th.openproductsfacts.org + applinks:tn-en.openproductsfacts.org + applinks:tn.openproductsfacts.org + applinks:tr.openproductsfacts.org + applinks:tw.openproductsfacts.org + applinks:uk.openproductsfacts.org + applinks:us-es.openproductsfacts.org + applinks:us.openproductsfacts.org + applinks:world-de.openproductsfacts.org + applinks:world-en.openproductsfacts.org + applinks:world-es.openproductsfacts.org + applinks:world-fr.openproductsfacts.org + applinks:world-it.openproductsfacts.org + applinks:world.openproductsfacts.org + applinks:world-pt.openproductsfacts.org + applinks:world-ru.openproductsfacts.org + applinks:world-zh.openproductsfacts.org + applinks:www.openproductsfacts.org + applinks:za.openproductsfacts.org + applinks:zh.openproductsfacts.org + + applinks:ae.openpetfoodfacts.org + applinks:ar.openpetfoodfacts.org + applinks:at-en.openpetfoodfacts.org + applinks:at.openpetfoodfacts.org + applinks:au.openpetfoodfacts.org + applinks:be-de.openpetfoodfacts.org + applinks:be-en.openpetfoodfacts.org + applinks:be-fr.openpetfoodfacts.org + applinks:be.openpetfoodfacts.org + applinks:bg.openpetfoodfacts.org + applinks:br.openpetfoodfacts.org + applinks:ca-fr.openpetfoodfacts.org + applinks:ca.openpetfoodfacts.org + applinks:ch-en.openpetfoodfacts.org + applinks:ch-fr.openpetfoodfacts.org + applinks:ch-it.openpetfoodfacts.org + applinks:ch.openpetfoodfacts.org + applinks:cl.openpetfoodfacts.org + applinks:cn-en.openpetfoodfacts.org + applinks:cn.openpetfoodfacts.org + applinks:co.openpetfoodfacts.org + applinks:cz.openpetfoodfacts.org + applinks:de-en.openpetfoodfacts.org + applinks:de.openpetfoodfacts.org + applinks:dk.openpetfoodfacts.org + applinks:dz-fr.openpetfoodfacts.org + applinks:dz.openpetfoodfacts.org + applinks:en.openpetfoodfacts.org + applinks:es-ca.openpetfoodfacts.org + applinks:es-en.openpetfoodfacts.org + applinks:es-eu.openpetfoodfacts.org + applinks:es-gl.openpetfoodfacts.org + applinks:es.openpetfoodfacts.org + applinks:fi.openpetfoodfacts.org + applinks:fr-en.openpetfoodfacts.org + applinks:fr-es.openpetfoodfacts.org + applinks:fr.openpetfoodfacts.org + applinks:gf.openpetfoodfacts.org + applinks:gp.openpetfoodfacts.org + applinks:gr.openpetfoodfacts.org + applinks:hk.openpetfoodfacts.org + applinks:hk-zh.openpetfoodfacts.org + applinks:hu.openpetfoodfacts.org + applinks:id.openpetfoodfacts.org + applinks:ie.openpetfoodfacts.org + applinks:il.openpetfoodfacts.org + applinks:in.openpetfoodfacts.org + applinks:ir.openpetfoodfacts.org + applinks:it.openpetfoodfacts.org + applinks:jp.openpetfoodfacts.org + applinks:ke.openpetfoodfacts.org + applinks:kh.openpetfoodfacts.org + applinks:lu.openpetfoodfacts.org + applinks:ma-fr.openpetfoodfacts.org + applinks:ma.openpetfoodfacts.org + applinks:mq.openpetfoodfacts.org + applinks:mx.openpetfoodfacts.org + applinks:nl-en.openpetfoodfacts.org + applinks:nl.openpetfoodfacts.org + applinks:no.openpetfoodfacts.org + applinks:nz.openpetfoodfacts.org + applinks:pe.openpetfoodfacts.org + applinks:pf.openpetfoodfacts.org + applinks:ph.openpetfoodfacts.org + applinks:pl-en.openpetfoodfacts.org + applinks:pl.openpetfoodfacts.org + applinks:openpetfoodfacts.org + applinks:pt-en.openpetfoodfacts.org + applinks:pt.openpetfoodfacts.org + applinks:re.openpetfoodfacts.org + applinks:ro.openpetfoodfacts.org + applinks:ru-en.openpetfoodfacts.org + applinks:ru.openpetfoodfacts.org + applinks:sa.openpetfoodfacts.org + applinks:se.openpetfoodfacts.org + applinks:sg.openpetfoodfacts.org + applinks:sg-zh.openpetfoodfacts.org + applinks:sk.openpetfoodfacts.org + applinks:th.openpetfoodfacts.org + applinks:tn-en.openpetfoodfacts.org + applinks:tn.openpetfoodfacts.org + applinks:tr.openpetfoodfacts.org + applinks:tw.openpetfoodfacts.org + applinks:uk.openpetfoodfacts.org + applinks:us-es.openpetfoodfacts.org + applinks:us.openpetfoodfacts.org + applinks:world-de.openpetfoodfacts.org + applinks:world-en.openpetfoodfacts.org + applinks:world-es.openpetfoodfacts.org + applinks:world-fr.openpetfoodfacts.org + applinks:world-it.openpetfoodfacts.org + applinks:world.openpetfoodfacts.org + applinks:world-pt.openpetfoodfacts.org + applinks:world-ru.openpetfoodfacts.org + applinks:world-zh.openpetfoodfacts.org + applinks:www.openpetfoodfacts.org + applinks:za.openpetfoodfacts.org + applinks:zh.openpetfoodfacts.org diff --git a/packages/smooth_app/lib/background/background_task.dart b/packages/smooth_app/lib/background/background_task.dart index 5344986e3b6..09a166808a8 100644 --- a/packages/smooth_app/lib/background/background_task.dart +++ b/packages/smooth_app/lib/background/background_task.dart @@ -18,6 +18,7 @@ abstract class BackgroundTask { required this.stamp, final OpenFoodFactsLanguage? language, }) // TODO(monsieurtanuki): don't store the password in a clear format... +// TODO(monsieurtanuki): store the uriProductHelper as well : user = jsonEncode(ProductQuery.getWriteUser().toJson()), country = ProductQuery.getCountry().offTag, languageCode = (language ?? ProductQuery.getLanguage()).offTag; @@ -181,10 +182,6 @@ abstract class BackgroundTask { /// subtasks that call the next one at the end. bool get hasImmediateNextTask => false; -// TODO(monsieurtanuki): store the uriProductHelper as well - @protected - UriProductHelper get uriProductHelper => ProductQuery.getUriProductHelper(); - /// Returns true if tasks with the same stamp would overwrite each-other. bool isDeduplicable() => true; } diff --git a/packages/smooth_app/lib/background/background_task_barcode.dart b/packages/smooth_app/lib/background/background_task_barcode.dart index dcc36e0d1d4..203e5f4393c 100644 --- a/packages/smooth_app/lib/background/background_task_barcode.dart +++ b/packages/smooth_app/lib/background/background_task_barcode.dart @@ -56,7 +56,7 @@ abstract class BackgroundTaskBarcode extends BackgroundTask { localDatabase: localDatabase, ); - @override + @protected UriProductHelper get uriProductHelper => ProductQuery.getUriProductHelper( productType: productType, ); diff --git a/packages/smooth_app/lib/background/background_task_download_products.dart b/packages/smooth_app/lib/background/background_task_download_products.dart index 99244ed3cd8..f3ad234c055 100644 --- a/packages/smooth_app/lib/background/background_task_download_products.dart +++ b/packages/smooth_app/lib/background/background_task_download_products.dart @@ -18,6 +18,7 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { required super.work, required super.pageSize, required super.totalSize, + required super.productType, required this.downloadFlag, }); @@ -49,12 +50,14 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { required final int totalSize, required final int soFarSize, required final int downloadFlag, + required final ProductType productType, }) async { final String uniqueId = await _operationType.getNewKey( localDatabase, soFarSize: soFarSize, totalSize: totalSize, work: work, + productType: productType, ); final BackgroundTask task = _getNewTask( uniqueId, @@ -62,6 +65,7 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { pageSize, totalSize, downloadFlag, + productType, ); await task.addToManager(localDatabase); } @@ -77,6 +81,7 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { final int pageSize, final int totalSize, final int downloadFlag, + final ProductType productType, ) => BackgroundTaskDownloadProducts._( processName: _operationType.processName, @@ -86,6 +91,7 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { pageSize: pageSize, totalSize: totalSize, downloadFlag: downloadFlag, + productType: productType, ); @override @@ -133,8 +139,6 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { throw Exception('Something bad happened downloading products'); } final DaoProduct daoProduct = DaoProduct(localDatabase); - final ProductType? productType = - ProductQuery.extractProductType(uriProductHelper); for (final Product product in downloadedProducts) { if (await _shouldBeUpdated(daoProduct, product.barcode!)) { await daoProduct.put( @@ -159,6 +163,7 @@ class BackgroundTaskDownloadProducts extends BackgroundTaskProgressing { totalSize: totalSize, soFarSize: totalSize - remaining, downloadFlag: downloadFlag, + productType: productType, ); } } diff --git a/packages/smooth_app/lib/background/background_task_full_refresh.dart b/packages/smooth_app/lib/background/background_task_full_refresh.dart index b11ddd1c030..a03f27dd362 100644 --- a/packages/smooth_app/lib/background/background_task_full_refresh.dart +++ b/packages/smooth_app/lib/background/background_task_full_refresh.dart @@ -5,8 +5,8 @@ import 'package:provider/provider.dart'; import 'package:smooth_app/background/background_task.dart'; import 'package:smooth_app/background/background_task_download_products.dart'; import 'package:smooth_app/background/background_task_paged.dart'; -import 'package:smooth_app/background/background_task_progressing.dart'; import 'package:smooth_app/background/operation_type.dart'; +import 'package:smooth_app/background/work_type.dart'; import 'package:smooth_app/database/dao_product.dart'; import 'package:smooth_app/database/dao_work_barcode.dart'; import 'package:smooth_app/database/local_database.dart'; @@ -66,34 +66,44 @@ class BackgroundTaskFullRefresh extends BackgroundTaskPaged { final DaoProduct daoProduct = DaoProduct(localDatabase); final DaoWorkBarcode daoWorkBarcode = DaoWorkBarcode(localDatabase); - await daoWorkBarcode - .deleteWork(BackgroundTaskProgressing.workFreshWithoutKP); - await daoWorkBarcode.deleteWork(BackgroundTaskProgressing.workFreshWithKP); + for (final ProductType productType in ProductType.values) { + await daoWorkBarcode.deleteWork( + WorkType.freshKP.getWorkTag(productType), + ); + await daoWorkBarcode.deleteWork( + WorkType.freshNoKP.getWorkTag(productType), + ); + } - // We separate the products into two lists, products with or without - // knowledge panels - final List barcodes = await daoProduct.getAllKeys(); - final List productsWithoutKP = []; - final List productsWithKP = []; - for (final String barcode in barcodes) { - if (await _shouldBeDownloadedWithoutKP(daoProduct, barcode)) { - productsWithoutKP.add(barcode); - } else { - productsWithKP.add(barcode); + // We separate the products into lists, products with or without + // knowledge panels, and split by product types. + final Map> split = await daoProduct.splitAllProducts( + (Product product) { + final bool noKP = product.knowledgePanels == null; + final WorkType workType = noKP ? WorkType.freshNoKP : WorkType.freshKP; + final ProductType productType = product.productType ?? ProductType.food; + return workType.getWorkTag(productType); + }, + ); + for (int i = 0; i <= 1; i++) { + final bool noKP = i == 0; + final WorkType workType = noKP ? WorkType.freshNoKP : WorkType.freshKP; + for (final ProductType productType in ProductType.values) { + final String tag = workType.getWorkTag(productType); + final List? barcodes = split[tag]; + if (barcodes == null) { + continue; + } + await _startDownloadTask( + barcodes: barcodes, + work: tag, + localDatabase: localDatabase, + downloadFlag: + noKP ? BackgroundTaskDownloadProducts.flagMaskExcludeKP : 0, + productType: productType, + ); } } - await _startDownloadTask( - barcodes: productsWithoutKP, - work: BackgroundTaskProgressing.workFreshWithoutKP, - localDatabase: localDatabase, - downloadFlag: BackgroundTaskDownloadProducts.flagMaskExcludeKP, - ); - await _startDownloadTask( - barcodes: productsWithKP, - work: BackgroundTaskProgressing.workFreshWithKP, - localDatabase: localDatabase, - downloadFlag: 0, - ); } @override @@ -102,24 +112,12 @@ class BackgroundTaskFullRefresh extends BackgroundTaskPaged { @override bool hasImmediateNextTask = false; - /// Returns true if we should download data without KP. - /// - /// That happens in one case: - /// * we already have a corresponding local product that does not have - /// populated knowledge panel fields. - static Future _shouldBeDownloadedWithoutKP( - final DaoProduct daoProduct, - final String barcode, - ) async { - final Product? product = await daoProduct.get(barcode); - return product != null && product.knowledgePanels == null; - } - Future _startDownloadTask({ required final List barcodes, required final String work, required final LocalDatabase localDatabase, required final int downloadFlag, + required final ProductType productType, }) async { if (barcodes.isEmpty) { return; @@ -134,6 +132,7 @@ class BackgroundTaskFullRefresh extends BackgroundTaskPaged { totalSize: barcodes.length, soFarSize: 0, downloadFlag: downloadFlag, + productType: productType, ); } } diff --git a/packages/smooth_app/lib/background/background_task_language_refresh.dart b/packages/smooth_app/lib/background/background_task_language_refresh.dart index cec03a1f8aa..cd0c88056c3 100644 --- a/packages/smooth_app/lib/background/background_task_language_refresh.dart +++ b/packages/smooth_app/lib/background/background_task_language_refresh.dart @@ -14,10 +14,15 @@ class BackgroundTaskLanguageRefresh extends BackgroundTask { required super.uniqueId, required super.stamp, required this.excludeBarcodes, + required this.productType, }); BackgroundTaskLanguageRefresh.fromJson(super.json) : excludeBarcodes = _getStringList(json, _jsonTagExcludeBarcodes), + productType = + ProductType.fromOffTag(json[_jsonTagProductType] as String?) ?? +// for legacy reason (not refreshed products = no product type) + ProductType.food, super.fromJson(); static List _getStringList( @@ -32,28 +37,48 @@ class BackgroundTaskLanguageRefresh extends BackgroundTask { } final List excludeBarcodes; + final ProductType productType; static const String _jsonTagExcludeBarcodes = 'excludeBarcodes'; + static const String _jsonTagProductType = 'productType'; @override Map toJson() { final Map result = super.toJson(); result[_jsonTagExcludeBarcodes] = excludeBarcodes; + result[_jsonTagProductType] = productType.offTag; return result; } static const OperationType _operationType = OperationType.languageRefresh; + UriProductHelper get _uriProductHelper => ProductQuery.getUriProductHelper( + productType: productType, + ); + static Future addTask( final LocalDatabase localDatabase, { final List excludeBarcodes = const [], + final ProductType? productType, }) async { + if (productType == null) { + for (final ProductType item in ProductType.values) { + await addTask( + localDatabase, + excludeBarcodes: excludeBarcodes, + productType: item, + ); + } + return; + } final String uniqueId = await _operationType.getNewKey( localDatabase, + productType: productType, ); final BackgroundTask task = _getNewTask( uniqueId, excludeBarcodes, + productType, ); await task.addToManager(localDatabase); } @@ -66,12 +91,14 @@ class BackgroundTaskLanguageRefresh extends BackgroundTask { static BackgroundTask _getNewTask( final String uniqueId, final List excludeBarcodes, + final ProductType productType, ) => BackgroundTaskLanguageRefresh._( processName: _operationType.processName, uniqueId: uniqueId, - stamp: ';languageRefresh', + stamp: ';languageRefresh;${productType.offTag}', excludeBarcodes: excludeBarcodes, + productType: productType, ); @override @@ -91,6 +118,7 @@ class BackgroundTaskLanguageRefresh extends BackgroundTask { language, limit: _pageSize, excludeBarcodes: excludeBarcodes, + productType: productType, ); if (barcodes.isEmpty) { return; @@ -108,7 +136,7 @@ class BackgroundTaskLanguageRefresh extends BackgroundTask { country: ProductQuery.getCountry(), version: ProductQuery.productQueryVersion, ), - uriHelper: uriProductHelper, + uriHelper: _uriProductHelper, ); if (searchResult.products == null || searchResult.count == null) { throw Exception('Cannot refresh language'); diff --git a/packages/smooth_app/lib/background/background_task_manager.dart b/packages/smooth_app/lib/background/background_task_manager.dart index 66704385daf..2ca1815d627 100644 --- a/packages/smooth_app/lib/background/background_task_manager.dart +++ b/packages/smooth_app/lib/background/background_task_manager.dart @@ -40,7 +40,7 @@ class BackgroundTaskManager { ); await DaoStringList(localDatabase).add(DaoStringList.keyTasks, taskId); await task.preExecute(localDatabase); - run(); + run(forceNowIfPossible: true); } /// Finishes a task cleanly. @@ -103,11 +103,13 @@ class BackgroundTaskManager { static const int _minimumDurationBetweenRuns = 5 * 1000; /// Returns the "now" timestamp if we can run now, or `null`. - int? _canStartNow() { - final DaoInt daoInt = DaoInt(localDatabase); + /// + /// With [forceNowIfPossible] we can be more aggressive and force the decision + /// of running now or at least just after the current running block. + int? _canStartNow(final bool forceNowIfPossible) { final int now = LocalDatabase.nowInMillis(); - final int? latestRunStart = daoInt.get(_lastStartTimestampKey); - final int? latestRunStop = daoInt.get(_lastStopTimestampKey); + final int? latestRunStart = localDatabase.daoIntGet(_lastStartTimestampKey); + final int? latestRunStop = localDatabase.daoIntGet(_lastStopTimestampKey); if (_running) { // if pretending to be running but started a very very long time ago if (latestRunStart != null && @@ -115,6 +117,10 @@ class BackgroundTaskManager { // we assume we can run now. return now; } + // let's try again at the end of the current run. + if (forceNowIfPossible) { + _forceRunAgain = true; + } return null; } // if the last run stopped correctly or was started a long time ago. @@ -123,7 +129,10 @@ class BackgroundTaskManager { // if the last run stopped not enough time ago. if (latestRunStop != null && latestRunStop + _minimumDurationBetweenRuns >= now) { - return null; + // let's apply that minimum duration if there's no rush + if (!forceNowIfPossible) { + return null; + } } return now; } @@ -132,8 +141,8 @@ class BackgroundTaskManager { /// Signals we've just finished working and that we're ready for a new run. Future _justFinished() async { - await DaoInt(localDatabase).put(_lastStartTimestampKey, null); - await DaoInt(localDatabase).put( + await localDatabase.daoIntPut(_lastStartTimestampKey, null); + await localDatabase.daoIntPut( _lastStopTimestampKey, LocalDatabase.nowInMillis(), ); @@ -141,11 +150,18 @@ class BackgroundTaskManager { bool _running = false; + /// Flag to say: I know you're running, please try again, it's worth it. + bool _forceRunAgain = false; + /// Runs all the pending tasks, and then smoothly ends, without awaiting. - void run() { - // no await - _runAsync(); - } + /// + /// Can be called in 2 cases: + /// 1. we've just created a task and we really want it to be executed ASAP + /// `forceNowIfPossible = true` + /// 2. we're just checking casually if there are pending tasks + /// `forceNowIfPossible = false` + void run({final bool forceNowIfPossible = false}) => + unawaited(_runAsync(forceNowIfPossible)); /// Runs all the pending tasks, and then smoothly ends. /// @@ -154,19 +170,17 @@ class BackgroundTaskManager { /// If a task fails and another task with the same stamp comes after, /// we can remove the failed task from the list: it would have been /// overwritten anyway. - Future _runAsync() async { - final int? now = _canStartNow(); + Future _runAsync(final bool forceNowIfPossible) async { + final int? now = _canStartNow(forceNowIfPossible); if (now == null) { return; } _running = true; - /// /// Will also set the "latest start timestamp". /// With this, we can detect a run that went wrong. /// Like, still running 1 hour later. - final DaoInt daoInt = DaoInt(localDatabase); - await daoInt.put(_lastStartTimestampKey, now); + await localDatabase.daoIntPut(_lastStartTimestampKey, now); bool runAgain = true; while (runAgain) { runAgain = false; @@ -196,6 +210,12 @@ class BackgroundTaskManager { } } await _justFinished(); + if (!runAgain) { + if (_forceRunAgain) { + runAgain = true; + _forceRunAgain = false; + } + } } _running = false; } diff --git a/packages/smooth_app/lib/background/background_task_offline.dart b/packages/smooth_app/lib/background/background_task_offline.dart index a8e2b03929f..fba57b31924 100644 --- a/packages/smooth_app/lib/background/background_task_offline.dart +++ b/packages/smooth_app/lib/background/background_task_offline.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/background/background_task.dart'; import 'package:smooth_app/background/background_task_progressing.dart'; import 'package:smooth_app/background/background_task_top_barcodes.dart'; import 'package:smooth_app/background/operation_type.dart'; +import 'package:smooth_app/background/work_type.dart'; import 'package:smooth_app/database/dao_work_barcode.dart'; import 'package:smooth_app/database/local_database.dart'; @@ -17,6 +19,7 @@ class BackgroundTaskOffline extends BackgroundTaskProgressing { required super.work, required super.pageSize, required super.totalSize, + required super.productType, }); BackgroundTaskOffline.fromJson(super.json) : super.fromJson(); @@ -27,6 +30,7 @@ class BackgroundTaskOffline extends BackgroundTaskProgressing { required final BuildContext context, required final int pageSize, required final int totalSize, + required final ProductType productType, }) async { final LocalDatabase localDatabase = context.read(); final String uniqueId = await _operationType.getNewKey( @@ -36,9 +40,10 @@ class BackgroundTaskOffline extends BackgroundTaskProgressing { ); final BackgroundTask task = _getNewTask( uniqueId, - BackgroundTaskProgressing.workOffline, + WorkType.offline.getWorkTag(productType), pageSize, totalSize, + productType, ); if (!context.mounted) { return; @@ -59,6 +64,7 @@ class BackgroundTaskOffline extends BackgroundTaskProgressing { final String work, final int pageSize, final int totalSize, + final ProductType productType, ) => BackgroundTaskOffline._( processName: _operationType.processName, @@ -67,6 +73,7 @@ class BackgroundTaskOffline extends BackgroundTaskProgressing { work: work, pageSize: pageSize, totalSize: totalSize, + productType: productType, ); @override @@ -85,6 +92,7 @@ class BackgroundTaskOffline extends BackgroundTaskProgressing { pageSize: pageSize, totalSize: totalSize, soFarSize: 0, + productType: productType, ); } } diff --git a/packages/smooth_app/lib/background/background_task_progressing.dart b/packages/smooth_app/lib/background/background_task_progressing.dart index dca7c12b3ac..c7e92dabf47 100644 --- a/packages/smooth_app/lib/background/background_task_progressing.dart +++ b/packages/smooth_app/lib/background/background_task_progressing.dart @@ -1,4 +1,7 @@ +import 'package:flutter/foundation.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/background/background_task_paged.dart'; +import 'package:smooth_app/query/product_query.dart'; /// Abstract background task with work in progress actions. abstract class BackgroundTaskProgressing extends BackgroundTaskPaged { @@ -9,35 +12,39 @@ abstract class BackgroundTaskProgressing extends BackgroundTaskPaged { required super.pageSize, required this.work, required this.totalSize, + required this.productType, }); BackgroundTaskProgressing.fromJson(super.json) : work = json[_jsonTagWork] as String, totalSize = json[_jsonTagTotalSize] as int, + productType = + ProductType.fromOffTag(json[_jsonTagProductType] as String?) ?? +// for legacy reason (not refreshed products = no product type) + ProductType.food, super.fromJson(); final String work; final int totalSize; + final ProductType productType; static const String _jsonTagWork = 'work'; static const String _jsonTagTotalSize = 'totalSize'; + static const String _jsonTagProductType = 'productType'; @override Map toJson() { final Map result = super.toJson(); result[_jsonTagWork] = work; result[_jsonTagTotalSize] = totalSize; + result[_jsonTagProductType] = productType.offTag; return result; } - static const String noBarcode = 'NO_BARCODE'; - - /// Work about downloading top products. - static const String workOffline = 'O'; + @protected + UriProductHelper get uriProductHelper => ProductQuery.getUriProductHelper( + productType: productType, + ); - /// Work about downloading fresh products with Knowledge Panels. - static const String workFreshWithKP = 'K'; - - /// Work about downloading fresh products without Knowledge Panels. - static const String workFreshWithoutKP = 'w'; + static const String noBarcode = 'NO_BARCODE'; } diff --git a/packages/smooth_app/lib/background/background_task_top_barcodes.dart b/packages/smooth_app/lib/background/background_task_top_barcodes.dart index bf00f099e56..846422bf81d 100644 --- a/packages/smooth_app/lib/background/background_task_top_barcodes.dart +++ b/packages/smooth_app/lib/background/background_task_top_barcodes.dart @@ -18,6 +18,7 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { required super.work, required super.pageSize, required super.totalSize, + required super.productType, required this.pageNumber, }); @@ -44,12 +45,14 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { required final int pageSize, required final int totalSize, required final int soFarSize, + required final ProductType productType, final int pageNumber = 1, }) async { final String uniqueId = await _operationType.getNewKey( localDatabase, totalSize: totalSize, soFarSize: soFarSize, + productType: productType, ); final BackgroundTask task = _getNewTask( uniqueId, @@ -57,6 +60,7 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { pageSize, totalSize, pageNumber, + productType, ); await task.addToManager(localDatabase); } @@ -72,6 +76,7 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { final int pageSize, final int totalSize, final int pageNumber, + final ProductType productType, ) => BackgroundTaskTopBarcodes._( processName: _operationType.processName, @@ -81,6 +86,7 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { pageSize: pageSize, totalSize: totalSize, pageNumber: pageNumber, + productType: productType, ); @override @@ -131,6 +137,7 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { totalSize: newTotalSize, soFarSize: soFarAfter, pageNumber: pageNumber + 1, + productType: productType, ); } else { // we have all the barcodes; now we need to download the products. @@ -141,6 +148,7 @@ class BackgroundTaskTopBarcodes extends BackgroundTaskProgressing { totalSize: soFarAfter, soFarSize: 0, downloadFlag: BackgroundTaskDownloadProducts.flagMaskExcludeKP, + productType: productType, ); } } diff --git a/packages/smooth_app/lib/background/operation_type.dart b/packages/smooth_app/lib/background/operation_type.dart index f1d83e874ce..c86ca37b6d2 100644 --- a/packages/smooth_app/lib/background/operation_type.dart +++ b/packages/smooth_app/lib/background/operation_type.dart @@ -1,4 +1,5 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/background/background_task.dart'; import 'package:smooth_app/background/background_task_add_other_price.dart'; import 'package:smooth_app/background/background_task_add_price.dart'; @@ -58,6 +59,7 @@ enum OperationType { final int? totalSize, final int? soFarSize, final String? work, + final ProductType? productType, }) async { final int sequentialId = await getNextSequenceNumber(DaoInt(localDatabase), _uniqueSequenceKey); @@ -66,7 +68,8 @@ enum OperationType { '$_transientHeaderSeparator$barcode' '$_transientHeaderSeparator${totalSize == null ? '' : totalSize.toString()}' '$_transientHeaderSeparator${soFarSize == null ? '' : soFarSize.toString()}' - '$_transientHeaderSeparator${work ?? ''}'; + '$_transientHeaderSeparator${work ?? ''}' + '$_transientHeaderSeparator${productType == null ? '' : productType.offTag}'; } BackgroundTask fromJson(Map map) => switch (this) { diff --git a/packages/smooth_app/lib/background/work_type.dart b/packages/smooth_app/lib/background/work_type.dart new file mode 100644 index 00000000000..257a6e8af1e --- /dev/null +++ b/packages/smooth_app/lib/background/work_type.dart @@ -0,0 +1,62 @@ +import 'package:openfoodfacts/openfoodfacts.dart'; + +/// Type of long download work for some background tasks. +enum WorkType { + /// Top products. + offline( + tag: 'O', + englishLabel: 'Top products', + ), + + /// Fresh products with Knowledge Panels. + freshKP( + tag: 'K', + englishLabel: 'Refresh products with KP', + ), + + /// Fresh products without Knowledge Panels. + freshNoKP( + tag: 'w', + englishLabel: 'Refresh products without KP', + ); + + const WorkType({ + required this.tag, + required this.englishLabel, + }); + + final String tag; + final String englishLabel; + + String getWorkTag(final ProductType productType) => + '$tag:${productType.offTag}'; + + static (WorkType, ProductType)? extract(final String string) { + if (string.isEmpty) { + return null; + } + final List strings = string.split(':'); + if (strings.length > 2) { + return null; + } + final ProductType productType; + if (strings.length == 1) { + productType = ProductType.food; + } else { + productType = ProductType.fromOffTag(strings[1])!; + } + final WorkType workType = fromTag(strings[0])!; + return (workType, productType); + } + + static WorkType? fromTag( + final String tag, + ) { + for (final WorkType workType in values) { + if (workType.tag == tag) { + return workType; + } + } + return null; + } +} diff --git a/packages/smooth_app/lib/data_models/news_feed/newsfeed_provider.dart b/packages/smooth_app/lib/data_models/news_feed/newsfeed_provider.dart index 2fe18d8d600..3ee10f2fe91 100644 --- a/packages/smooth_app/lib/data_models/news_feed/newsfeed_provider.dart +++ b/packages/smooth_app/lib/data_models/news_feed/newsfeed_provider.dart @@ -5,7 +5,6 @@ import 'dart:isolate'; import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'package:http/http.dart' as http; -import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:smooth_app/data_models/news_feed/newsfeed_model.dart'; @@ -28,8 +27,6 @@ class AppNewsProvider extends ChangeNotifier { _preferences = preferences, _uriOverride = preferences.getDevModeString( UserPreferencesDevMode.userPreferencesCustomNewsJSONURI), - _domain = preferences.getDevModeString( - UserPreferencesDevMode.userPreferencesTestEnvDomain), _prodEnv = preferences .getFlag(UserPreferencesDevMode.userPreferencesFlagProd) { _preferences.addListener(_onPreferencesChanged); @@ -102,27 +99,20 @@ class AppNewsProvider extends ChangeNotifier { } } - /// API URL: [https://world.openfoodfacts.[org/net]/resources/files/tagline-off-ios-v3.json] - /// or [https://world.openfoodfacts.[org/net]/resources/files/tagline-off-android-v3.json] + /// The content is stored on GitHub with two static files: one for Android, + /// and the other for iOS. + /// [https://github.com/openfoodfacts/smooth-app_assets/tree/main/prod/tagline] Future _fetchJSON() async { try { - final UriProductHelper uriProductHelper = - ProductQuery.getUriProductHelper(); - final Map headers = {}; final Uri uri; if (_uriOverride?.isNotEmpty == true) { uri = Uri.parse(_uriOverride!); } else { - uri = uriProductHelper.getUri(path: _newsUrl); + uri = Uri.parse(_newsUrl); } - if (uriProductHelper.userInfoForPatch != null) { - headers['Authorization'] = - 'Basic ${base64Encode(utf8.encode(uriProductHelper.userInfoForPatch!))}'; - } - - final http.Response response = await http.get(uri, headers: headers); + final http.Response response = await http.get(uri); if (response.statusCode == 404) { Logs.e("Remote file $uri doesn't exist!"); @@ -143,10 +133,12 @@ class AppNewsProvider extends ChangeNotifier { /// Based on the platform, the URL may differ String get _newsUrl { + final String env = _prodEnv != false ? 'prod' : 'dev'; + if (Platform.isIOS || Platform.isMacOS) { - return '/resources/files/tagline-off-ios-v3.json'; + return 'https://raw.githubusercontent.com/openfoodfacts/smooth-app_assets/refs/heads/main/$env/tagline/ios/main.json'; } else { - return '/resources/files/tagline-off-android-v3.json'; + return 'https://raw.githubusercontent.com/openfoodfacts/smooth-app_assets/refs/heads/main/$env/tagline/android/main.json'; } } @@ -166,24 +158,19 @@ class AppNewsProvider extends ChangeNotifier { .isAfter(DateTime.now().add(const Duration(days: -1))); bool? _prodEnv; - String? _domain; String? _uriOverride; - /// [ProductQuery.uriProductHelper] is not synced yet, + /// [ProductQuery._uriProductHelper] is not synced yet, /// so we have to check it manually Future _onPreferencesChanged() async { final String jsonURI = _preferences.getDevModeString( UserPreferencesDevMode.userPreferencesCustomNewsJSONURI) ?? ''; - final String domain = _preferences.getDevModeString( - UserPreferencesDevMode.userPreferencesTestEnvDomain) ?? - ''; final bool prodEnv = _preferences.getFlag(UserPreferencesDevMode.userPreferencesFlagProd) ?? true; - if (domain != _domain || prodEnv != _prodEnv || jsonURI != _uriOverride) { - _domain = domain; + if (prodEnv != _prodEnv || jsonURI != _uriOverride) { _prodEnv = prodEnv; _uriOverride = jsonURI; loadLatestNews(forceUpdate: true); diff --git a/packages/smooth_app/lib/database/dao_product.dart b/packages/smooth_app/lib/database/dao_product.dart index de35e277776..1b677ae7ae9 100644 --- a/packages/smooth_app/lib/database/dao_product.dart +++ b/packages/smooth_app/lib/database/dao_product.dart @@ -92,6 +92,65 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { return result; } + /// Returns the local products split by product type. + Future>> getProductTypes( + final List barcodes, + ) async { + final Map> result = >{}; + if (barcodes.isEmpty) { + return result; + } + for (int start = 0; + start < barcodes.length; + start += BulkManager.SQLITE_MAX_VARIABLE_NUMBER) { + final int size = min( + barcodes.length - start, + BulkManager.SQLITE_MAX_VARIABLE_NUMBER, + ); + final List> queryResults = + await localDatabase.database.query( + _TABLE_PRODUCT, + columns: _columns, + where: '$_TABLE_PRODUCT_COLUMN_BARCODE in(? ${',?' * (size - 1)})', + whereArgs: barcodes.sublist(start, start + size), + ); + for (final Map row in queryResults) { + final Product product = _getProductFromQueryResult(row); + final ProductType productType = product.productType ?? ProductType.food; + List? barcodes = result[productType]; + if (barcodes == null) { + barcodes = []; + result[productType] = barcodes; + } + barcodes.add(product.barcode!); + } + } + return result; + } + + /// Returns all the local products split by a function. + Future>> splitAllProducts( + final String Function(Product) splitFunction, + ) async { + final Map> result = >{}; + final List> queryResults = + await localDatabase.database.query( + _TABLE_PRODUCT, + columns: _columns, + ); + for (final Map row in queryResults) { + final Product product = _getProductFromQueryResult(row); + final String splitValue = splitFunction(product); + List? barcodes = result[splitValue]; + if (barcodes == null) { + barcodes = []; + result[splitValue] = barcodes; + } + barcodes.add(product.barcode!); + } + return result; + } + Future put( final Product product, final OpenFoodFactsLanguage language, { @@ -232,13 +291,20 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { } /// Get the total number of products in the database - Future getTotalNoOfProducts() async { - return Sqflite.firstIntValue( - await localDatabase.database.rawQuery( - 'select count(*) from $_TABLE_PRODUCT', - ), - ) ?? - 0; + Future> getTotalNoOfProducts() async { + final Map result = {}; + final List> queryResults = + await localDatabase.database.query( + _TABLE_PRODUCT, + columns: _columns, + ); + for (final Map row in queryResults) { + final Product product = _getProductFromQueryResult(row); + final ProductType productType = product.productType ?? ProductType.food; + final int? count = result[productType]; + result[productType] = 1 + (count ?? 0); + } + return result; } /// Get the estimated total size of the database in MegaBytes @@ -277,10 +343,11 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { final OpenFoodFactsLanguage language, { required final int limit, required final List excludeBarcodes, + required final ProductType productType, }) async { /// Unfortunately, some SQFlite implementations don't support "nulls last" String getRawQuery(final bool withNullsLast) => - 'select p.$_TABLE_PRODUCT_COLUMN_BARCODE ' + 'select p.$_TABLE_PRODUCT_COLUMN_GZIPPED_JSON ' 'from' ' $_TABLE_PRODUCT p' ' left outer join ${DaoProductLastAccess.TABLE} a' @@ -288,8 +355,7 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { 'where' ' p.$_TABLE_PRODUCT_COLUMN_LANGUAGE is null' ' or p.$_TABLE_PRODUCT_COLUMN_LANGUAGE != ? ' - 'order by a.${DaoProductLastAccess.COLUMN_LAST_ACCESS} desc ${withNullsLast ? 'nulls last' : ''} ' - 'limit ?'; + 'order by a.${DaoProductLastAccess.COLUMN_LAST_ACCESS} desc ${withNullsLast ? 'nulls last' : ''} '; List> queryResults = >[]; try { @@ -297,7 +363,6 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { getRawQuery(true), [ language.offTag, - limit + excludeBarcodes.length, ], ); } catch (e) { @@ -310,7 +375,6 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { getRawQuery(false), [ language.offTag, - limit + excludeBarcodes.length, ], ); } @@ -318,10 +382,14 @@ class DaoProduct extends AbstractSqlDao implements BulkDeletable { final List result = []; for (final Map row in queryResults) { - final String barcode = row[_TABLE_PRODUCT_COLUMN_BARCODE] as String; + final Product product = _getProductFromQueryResult(row); + final String barcode = product.barcode!; if (excludeBarcodes.contains(barcode)) { continue; } + if ((product.productType ?? ProductType.food) != productType) { + continue; + } result.add(barcode); if (result.length == limit) { break; diff --git a/packages/smooth_app/lib/database/local_database.dart b/packages/smooth_app/lib/database/local_database.dart index 5d5d7abddda..7969c8145c1 100644 --- a/packages/smooth_app/lib/database/local_database.dart +++ b/packages/smooth_app/lib/database/local_database.dart @@ -51,6 +51,13 @@ class LocalDatabase extends ChangeNotifier { List getAllTaskIds() => DaoStringList(this).getAll(DaoStringList.keyTasks); + /// Ugly solution to be able to mock hive data. + int? daoIntGet(final String key) => DaoInt(this).get(key); + + /// Ugly solution to be able to mock hive data. + Future daoIntPut(final String key, final int? value) => + DaoInt(this).put(key, value); + static Future getLocalDatabase() async { // sql from there String? databasesRootPath; diff --git a/packages/smooth_app/lib/helpers/launch_url_helper.dart b/packages/smooth_app/lib/helpers/launch_url_helper.dart index 3e1264d2f16..42edfe2d2e3 100644 --- a/packages/smooth_app/lib/helpers/launch_url_helper.dart +++ b/packages/smooth_app/lib/helpers/launch_url_helper.dart @@ -14,7 +14,9 @@ class LaunchUrlHelper { ) async { assert(url.isNotEmpty); - if (url.startsWith(RegExp('http(s)?://[a-z]*.openfoodfacts.(net|org)'))) { + if (url.startsWith(RegExp( + 'http(s)?://[a-z]*.open(food|beauty|products|petfood)facts.(net|org)', + ))) { AnalyticsHelper.trackOutlink(url: url); GoRouter.of(context).go(url); } else { diff --git a/packages/smooth_app/lib/helpers/temp_product_list_share_helper.dart b/packages/smooth_app/lib/helpers/temp_product_list_share_helper.dart index d2adeb878e9..09f99228783 100644 --- a/packages/smooth_app/lib/helpers/temp_product_list_share_helper.dart +++ b/packages/smooth_app/lib/helpers/temp_product_list_share_helper.dart @@ -2,11 +2,14 @@ import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:smooth_app/query/product_query.dart'; // TODO(m123): Move this to off-dart -Uri shareProductList(List barcodes) { +Uri shareProductList( + final List barcodes, + final ProductType productType, +) { final String barcodesString = barcodes.join(','); return UriHelper.replaceSubdomain( - ProductQuery.getUriProductHelper().getUri( + ProductQuery.getUriProductHelper(productType: productType).getUri( path: 'products/$barcodesString', addUserAgentParameters: false, ), diff --git a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_action_card.dart b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_action_card.dart index 61c09bbaaad..c212a2d48bf 100644 --- a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_action_card.dart +++ b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_action_card.dart @@ -65,7 +65,9 @@ class KnowledgePanelActionCard extends StatelessWidget { ); } if (kpAction == KnowledgePanelAction.addNutritionFacts) { - return AddNutritionButton(product); + if (AddNutritionButton.acceptsNutritionFacts(product)) { + return AddNutritionButton(product); + } } Logs.e('unhandled knowledge panel action: $action'); return null; diff --git a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_card.dart b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_card.dart index b40ff5a410a..82f53fef727 100644 --- a/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_card.dart +++ b/packages/smooth_app/lib/knowledge_panel/knowledge_panels/knowledge_panel_card.dart @@ -43,11 +43,18 @@ class KnowledgePanelCard extends StatelessWidget { ); } + // in some cases there's nothing to click about. + // cf. https://github.com/openfoodfacts/smooth-app/issues/5700 + final bool improvedIsClickable = isClickable && + KnowledgePanelsBuilder.hasSomethingToDisplay( + product, + panelId, + ); return Padding( padding: const EdgeInsets.symmetric(vertical: SMALL_SPACE), child: InkWell( borderRadius: ANGULAR_BORDER_RADIUS, - onTap: !isClickable + onTap: !improvedIsClickable ? null : () async => Navigator.push( context, @@ -64,7 +71,7 @@ class KnowledgePanelCard extends StatelessWidget { ), child: KnowledgePanelsBuilder.getPanelSummaryWidget( panel, - isClickable: isClickable, + isClickable: improvedIsClickable, margin: EdgeInsets.zero, ) ?? const SizedBox(), diff --git a/packages/smooth_app/lib/knowledge_panel/knowledge_panels_builder.dart b/packages/smooth_app/lib/knowledge_panel/knowledge_panels_builder.dart index feaff5bacb7..e60edf05757 100644 --- a/packages/smooth_app/lib/knowledge_panel/knowledge_panels_builder.dart +++ b/packages/smooth_app/lib/knowledge_panel/knowledge_panels_builder.dart @@ -65,7 +65,9 @@ class KnowledgePanelsBuilder { ProductState.NUTRITION_FACTS_COMPLETED.toBeCompletedTag) ?? false; if (nutritionAddOrUpdate) { - children.add(AddNutritionButton(product)); + if (AddNutritionButton.acceptsNutritionFacts(product)) { + children.add(AddNutritionButton(product)); + } } final bool needEditIngredients = context @@ -149,6 +151,24 @@ class KnowledgePanelsBuilder { return elements.first; } + /// Returns true if there are elements to display for that panel. + static bool hasSomethingToDisplay( + final Product product, + final String panelId, + ) { + final KnowledgePanel panel = + KnowledgePanelsBuilder.getKnowledgePanel(product, panelId)!; + if (panel.elements == null) { + return false; + } + for (final KnowledgePanelElement element in panel.elements!) { + if (_hasSomethingToDisplay(element: element, product: product)) { + return true; + } + } + return false; + } + /// Returns a padded widget that displays the KP element, or rarely null. static Widget? getElementWidget({ required final KnowledgePanelElement knowledgePanelElement, @@ -178,6 +198,8 @@ class KnowledgePanelsBuilder { } /// Returns the widget that displays the KP element, or rarely null. + /// + /// cf. [_hasSomethingToDisplay]. static Widget? _getElementWidget({ required final KnowledgePanelElement element, required final Product product, @@ -242,10 +264,33 @@ class KnowledgePanelsBuilder { element.actionElement!, product, ); + } + } - default: - Logs.e('unexpected element type: ${element.elementType}'); - return null; + /// Returns true if the element has something to display. + /// + /// cf. [_getElementWidget]. + static bool _hasSomethingToDisplay({ + required final KnowledgePanelElement element, + required final Product product, + }) { + switch (element.elementType) { + case KnowledgePanelElementType.TEXT: + case KnowledgePanelElementType.IMAGE: + case KnowledgePanelElementType.PANEL_GROUP: + case KnowledgePanelElementType.TABLE: + case KnowledgePanelElementType.MAP: + case KnowledgePanelElementType.ACTION: + return true; + case KnowledgePanelElementType.UNKNOWN: + return false; + case KnowledgePanelElementType.PANEL: + final String panelId = element.panelElement!.panelId; + final KnowledgePanel? panel = getKnowledgePanel(product, panelId); + if (panel == null) { + return false; + } + return true; } } diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 2887e976840..5381887348d 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -355,6 +355,8 @@ "@contribute_develop_text_2": {}, "contribute_develop_dev_mode_title": "DEV Mode?", "contribute_develop_dev_mode_subtitle": "Activate the DEV Mode", + "contribute_donate_title": "Donate", + "@contribute_donate_title": {}, "contribute_donate_header": "Donate to Open Food Facts", "@contribute_donate_header": {}, "contribute_enroll_alpha": "Enroll in internal alpha version", diff --git a/packages/smooth_app/lib/main.dart b/packages/smooth_app/lib/main.dart index e9005326b92..5ea9df3799c 100644 --- a/packages/smooth_app/lib/main.dart +++ b/packages/smooth_app/lib/main.dart @@ -304,7 +304,7 @@ class _SmoothAppState extends State { Widget _buildError(AsyncSnapshot snapshot) { return MaterialApp( - theme: ThemeData(useMaterial3: false), + theme: ThemeData(), home: SmoothScaffold( body: Center( child: Text( diff --git a/packages/smooth_app/lib/pages/navigator/app_navigator.dart b/packages/smooth_app/lib/pages/navigator/app_navigator.dart index e28f03f64e2..41696039971 100644 --- a/packages/smooth_app/lib/pages/navigator/app_navigator.dart +++ b/packages/smooth_app/lib/pages/navigator/app_navigator.dart @@ -72,6 +72,13 @@ class AppNavigator extends InheritedWidget { _router.router.pushReplacement(routeName, extra: extra); } + /// Remove all the screens from the stack + void clearStack() { + while (_router.router.canPop() == true) { + _router.router.pop(); + } + } + void pop([dynamic result]) { _router.router.pop(result); } @@ -228,18 +235,18 @@ class _SmoothGoRouter { } }, ), - GoRoute( - path: _InternalAppRoutes.EXTERNAL_PAGE, - builder: (BuildContext context, GoRouterState state) { - return ExternalPage(path: state.uri.queryParameters['path']!); - }, - ), GoRoute( path: _InternalAppRoutes.SIGNUP_PAGE, builder: (_, __) => const SignUpPage(), ) ], ), + GoRoute( + path: '/${_InternalAppRoutes.EXTERNAL_PAGE}/:page', + builder: (BuildContext context, GoRouterState state) { + return ExternalPage(path: state.pathParameters['page']!); + }, + ), ], redirect: (BuildContext context, GoRouterState state) { final String path = state.matchedLocation; @@ -286,7 +293,7 @@ class _SmoothGoRouter { externalLink = true; } } else if (path == _ExternalRoutes.MOBILE_APP_DOWNLOAD) { - return AppRoutes.HOME; + return AppRoutes.HOME(); } else if (path == _ExternalRoutes.GUIDE_NUTRISCORE_V2) { return AppRoutes.GUIDE_NUTRISCORE_V2; } else if (path == _ExternalRoutes.SIGNUP) { @@ -421,7 +428,8 @@ class AppRoutes { AppRoutes._(); // Home page (or walkthrough during the onboarding) - static String get HOME => _InternalAppRoutes.HOME_PAGE; + static String HOME({bool redraw = false}) => + '${_InternalAppRoutes.HOME_PAGE}?redraw:$redraw'; // Product details (a [Product] is mandatory in the extra) static String PRODUCT( @@ -460,5 +468,5 @@ class AppRoutes { // Open an external link (where path is relative to the OFF website) static String EXTERNAL(String path) => - '/${_InternalAppRoutes.EXTERNAL_PAGE}/?path=$path'; + '/${_InternalAppRoutes.EXTERNAL_PAGE}/$path'; } diff --git a/packages/smooth_app/lib/pages/offline_data_page.dart b/packages/smooth_app/lib/pages/offline_data_page.dart index ee3a26d37e8..382e502fbd5 100644 --- a/packages/smooth_app/lib/pages/offline_data_page.dart +++ b/packages/smooth_app/lib/pages/offline_data_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/background/background_task_full_refresh.dart'; import 'package:smooth_app/background/background_task_offline.dart'; @@ -10,6 +11,7 @@ import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/duration_constants.dart'; import 'package:smooth_app/helpers/app_helper.dart'; +import 'package:smooth_app/query/product_query.dart'; import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; @@ -60,16 +62,19 @@ class _OfflineDataPageState extends State { _StatsWidget( daoProduct: daoProduct, ), - _OfflinePageListTile( - title: appLocalizations.download_data, - subtitle: appLocalizations.download_top_n_products(_topNSize), - onTap: () async => BackgroundTaskOffline.addTask( - context: context, - pageSize: _pageSize, - totalSize: _topNSize, + for (final ProductType productType in ProductType.values) + _OfflinePageListTile( + title: + '${appLocalizations.download_data} (${productType.getLabel(appLocalizations)})', + subtitle: appLocalizations.download_top_n_products(_topNSize), + onTap: () async => BackgroundTaskOffline.addTask( + context: context, + pageSize: _pageSize, + totalSize: _topNSize, + productType: productType, + ), + trailing: const Icon(Icons.download), ), - trailing: const Icon(Icons.download), - ), _OfflinePageListTile( title: appLocalizations.update_offline_data, subtitle: appLocalizations.update_local_database_sub, @@ -129,16 +134,26 @@ class _StatsWidget extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: SMALL_SPACE), child: ListTile( title: Text(applocalizations.offline_product_data_title), - subtitle: FutureBuilder( + subtitle: FutureBuilder>( future: daoProduct.getTotalNoOfProducts(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Text( - applocalizations.available_for_download(snapshot.data!), - ); - } else { + builder: ( + BuildContext context, + AsyncSnapshot> snapshot, + ) { + if (!snapshot.hasData) { return Text(applocalizations.loading); } + int count = 0; + final List list = []; + for (final MapEntry item + in snapshot.data!.entries) { + count += item.value; + list.add( + '${item.value} (${item.key.getLabel(applocalizations)})'); + } + return Text( + '${applocalizations.available_for_download(count)} ${list.join(', ')}', + ); }, ), trailing: FutureBuilder( diff --git a/packages/smooth_app/lib/pages/offline_tasks_page.dart b/packages/smooth_app/lib/pages/offline_tasks_page.dart index 3758922aae7..21fec7668f9 100644 --- a/packages/smooth_app/lib/pages/offline_tasks_page.dart +++ b/packages/smooth_app/lib/pages/offline_tasks_page.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/background/background_task_manager.dart'; import 'package:smooth_app/background/background_task_progressing.dart'; import 'package:smooth_app/background/operation_type.dart'; +import 'package:smooth_app/background/work_type.dart'; import 'package:smooth_app/database/dao_instant_string.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; @@ -32,7 +34,9 @@ class _OfflineTaskState extends State { actions: [ IconButton( onPressed: () => - BackgroundTaskManager.getInstance(localDatabase).run(), + BackgroundTaskManager.getInstance(localDatabase).run( + forceNowIfPossible: true, + ), icon: const Icon(Icons.refresh), ), ], @@ -124,16 +128,13 @@ class _OfflineTaskState extends State { String? _getWorkText(final String taskId) { final String? work = OperationType.getWork(taskId); - switch (work) { - case null: - case '': - return null; - case BackgroundTaskProgressing.workOffline: - return 'Top products'; - case BackgroundTaskProgressing.workFreshWithoutKP: - return 'Refresh products without KP'; - case BackgroundTaskProgressing.workFreshWithKP: - return 'Refresh products with KP'; + if (work == null || work.isEmpty) { + return null; + } + final (WorkType workType, ProductType productType)? item = + WorkType.extract(work); + if (item != null) { + return '${item.$1.englishLabel} (${item.$2.offTag})'; } return 'Unknown work ($work)!'; } diff --git a/packages/smooth_app/lib/pages/onboarding/onboarding_flow_navigator.dart b/packages/smooth_app/lib/pages/onboarding/onboarding_flow_navigator.dart index 2391b6abb1d..0fa723075c5 100644 --- a/packages/smooth_app/lib/pages/onboarding/onboarding_flow_navigator.dart +++ b/packages/smooth_app/lib/pages/onboarding/onboarding_flow_navigator.dart @@ -139,16 +139,23 @@ class OnboardingFlowNavigator { static final List _historyOnboardingNav = []; Future navigateToPage(BuildContext context, OnboardingPage page) async { - _userPreferences.setLastVisitedOnboardingPage(page); + await _userPreferences.setLastVisitedOnboardingPage(page); _historyOnboardingNav.add(page); - final MaterialPageRoute route = MaterialPageRoute( - builder: (BuildContext context) => page.getPageWidget(context), - ); + if (!context.mounted) { + return; + } if (page.isOnboardingComplete()) { - AppNavigator.of(context).pushReplacement(AppRoutes.HOME); + AppNavigator.of(context) + ..clearStack() + ..pushReplacement( + AppRoutes.HOME(redraw: true), + ); } else { + final MaterialPageRoute route = MaterialPageRoute( + builder: (BuildContext context) => page.getPageWidget(context), + ); await Navigator.of(context).push(route); } } diff --git a/packages/smooth_app/lib/pages/onboarding/v2/onboarding_bottom_hills.dart b/packages/smooth_app/lib/pages/onboarding/v2/onboarding_bottom_hills.dart index e8ecaa7c70b..1e4b1eed85d 100644 --- a/packages/smooth_app/lib/pages/onboarding/v2/onboarding_bottom_hills.dart +++ b/packages/smooth_app/lib/pages/onboarding/v2/onboarding_bottom_hills.dart @@ -24,7 +24,13 @@ class OnboardingBottomHills extends StatelessWidget { @override Widget build(BuildContext context) { final TextDirection textDirection = Directionality.of(context); - final double bottomPadding = MediaQuery.viewPaddingOf(context).bottom; + double bottomPadding = MediaQuery.viewPaddingOf(context).bottom; + if (bottomPadding == 0) { + // Add a slight padding for devices without a transparent nav bar + // (eg: iPhone SE) + bottomPadding = 4.0; + } + final double maxHeight = OnboardingBottomHills.height(context); final SmoothColorsThemeExtension colors = Theme.of(context).extension()!; diff --git a/packages/smooth_app/lib/pages/preferences/lazy_counter.dart b/packages/smooth_app/lib/pages/preferences/lazy_counter.dart index a14432500ea..3d530e9cfa6 100644 --- a/packages/smooth_app/lib/pages/preferences/lazy_counter.dart +++ b/packages/smooth_app/lib/pages/preferences/lazy_counter.dart @@ -84,7 +84,9 @@ class LazyCounterUserSearch extends LazyCounter { final SearchResult result = await OpenFoodAPIClient.searchProducts( user, configuration, - uriHelper: ProductQuery.getUriProductHelper(), + uriHelper: ProductQuery.getUriProductHelper( + productType: ProductType.food, + ), ); return result.count; } catch (e) { diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart index 8a951e8e08d..0548121d344 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:provider/provider.dart'; @@ -17,12 +16,6 @@ import 'package:smooth_app/pages/preferences/lazy_counter_widget.dart'; import 'package:smooth_app/pages/preferences/user_preferences_item.dart'; import 'package:smooth_app/pages/preferences/user_preferences_list_tile.dart'; import 'package:smooth_app/pages/preferences/user_preferences_page.dart'; -import 'package:smooth_app/pages/prices/get_prices_model.dart'; -import 'package:smooth_app/pages/prices/price_user_button.dart'; -import 'package:smooth_app/pages/prices/prices_page.dart'; -import 'package:smooth_app/pages/prices/prices_proofs_page.dart'; -import 'package:smooth_app/pages/prices/prices_users_page.dart'; -import 'package:smooth_app/pages/prices/product_price_add_page.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/pages/user_management/login_page.dart'; import 'package:smooth_app/query/paged_product_query.dart'; @@ -217,92 +210,6 @@ class UserPreferencesAccount extends AbstractUserPreferences { lazyCounter: const LazyCounterUserSearch(UserSearchType.TO_BE_COMPLETED), ), - _getListTile( - PriceUserButton.showUserTitle( - user: ProductQuery.getWriteUser().userId, - context: context, - ), - () async => PriceUserButton.showUserPrices( - user: ProductQuery.getWriteUser().userId, - context: context, - ), - CupertinoIcons.money_dollar_circle, - lazyCounter: LazyCounterPrices(ProductQuery.getWriteUser().userId), - ), - _getListTile( - appLocalizations.user_search_proofs_title, - () async => Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => const PricesProofsPage( - selectProof: false, - ), - ), - ), - Icons.receipt, - ), - _getListTile( - appLocalizations.prices_add_a_receipt, - () async => ProductPriceAddPage.showProductPage( - context: context, - proofType: ProofType.receipt, - ), - Icons.add_shopping_cart, - ), - _getListTile( - appLocalizations.prices_add_price_tags, - () async => ProductPriceAddPage.showProductPage( - context: context, - proofType: ProofType.priceTag, - ), - Icons.add_shopping_cart, - ), - _getListTile( - appLocalizations.all_search_prices_latest_title, - () async => Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => PricesPage( - GetPricesModel( - parameters: GetPricesParameters() - ..orderBy = >[ - const OrderBy( - field: GetPricesOrderField.created, - ascending: false, - ), - ] - ..pageSize = GetPricesModel.pageSize - ..pageNumber = 1, - displayOwner: true, - displayProduct: true, - uri: OpenPricesAPIClient.getUri( - path: 'prices', - uriHelper: ProductQuery.uriPricesHelper, - ), - title: appLocalizations.all_search_prices_latest_title, - lazyCounterPrices: const LazyCounterPrices(null), - ), - ), - ), - ), - CupertinoIcons.money_dollar_circle, - lazyCounter: const LazyCounterPrices(null), - ), - _getListTile( - appLocalizations.all_search_prices_top_user_title, - () async => Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => const PricesUsersPage(), - ), - ), - Icons.account_box, - ), - _getPriceListTile( - appLocalizations.all_search_prices_top_location_title, - 'locations', - ), - _getPriceListTile( - appLocalizations.all_search_prices_top_product_title, - 'products', - ), _buildProductQueryTile( productQuery: PagedToBeCompletedProductQuery( productType: ProductType.food, @@ -356,21 +263,6 @@ class UserPreferencesAccount extends AbstractUserPreferences { ]; } - UserPreferencesItem _getPriceListTile( - final String title, - final String path, - ) => - _getListTile( - title, - () async => LaunchUrlHelper.launchURL( - OpenPricesAPIClient.getUri( - path: path, - uriHelper: ProductQuery.uriPricesHelper, - ).toString(), - ), - Icons.open_in_new, - ); - Future _confirmLogout() async => showDialog( context: context, builder: (BuildContext context) { diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_contribute.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_contribute.dart index d0e438e2c1c..f5478fe4ca7 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_contribute.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_contribute.dart @@ -94,16 +94,6 @@ class UserPreferencesContribute extends AbstractUserPreferences { () async => _share(appLocalizations.contribute_share_content), Icons.adaptive.share, ), - _getListTile( - appLocalizations.contribute_donate_header, - () async => LaunchUrlHelper.launchURL( - AppLocalizations.of(context).donate_url, - ), - Icons.volunteer_activism, - icon: - UserPreferencesListTile.getTintedIcon(Icons.open_in_new, context), - externalLink: true, - ), if (GlobalVars.appStore.getEnrollInBetaURL() != null) _getListTile( appLocalizations.contribute_enroll_alpha, diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_dev_debug_info.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_dev_debug_info.dart index 38507150fa9..eb536edf0c3 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_dev_debug_info.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_dev_debug_info.dart @@ -29,10 +29,12 @@ class _UserPreferencesDebugInfoState extends State { 'IsLoggedIn': ProductQuery.isLoggedIn().toString(), 'UUID': OpenFoodAPIConfiguration.uuid.toString(), 'Matomo Visitor ID': AnalyticsHelper.matomoVisitorId, - 'QueryType': ProductQuery.getUriProductHelper().isTestMode + 'QueryType': ProductQuery.getUriProductHelper(productType: ProductType.food) + .isTestMode ? 'QueryType.TEST' : 'QueryType.PROD', - 'Domain': ProductQuery.getUriProductHelper().domain, + 'Domain': + ProductQuery.getUriProductHelper(productType: ProductType.food).domain, 'UserAgent-name': '${OpenFoodAPIConfiguration.userAgent?.name}', 'UserAgent-system': '${OpenFoodAPIConfiguration.userAgent?.system}', }; diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_donation.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_donation.dart new file mode 100644 index 00000000000..92d580480f2 --- /dev/null +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_donation.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:smooth_app/helpers/launch_url_helper.dart'; +import 'package:smooth_app/pages/preferences/abstract_user_preferences.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_item.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_list_tile.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_page.dart'; + +/// Display of "Donation" for the preferences page. +class UserPreferencesDonation extends AbstractUserPreferences { + UserPreferencesDonation({ + required super.context, + required super.userPreferences, + required super.appLocalizations, + required super.themeData, + }); + + @override + PreferencePageType getPreferencePageType() => PreferencePageType.DONATION; + + @override + String getTitleString() => appLocalizations.contribute_donate_title; + + @override + String getSubtitleString() => appLocalizations.contribute_donate_header; + + @override + IconData getLeadingIconData() => Icons.volunteer_activism; + + @override + Icon? getForwardIcon() => UserPreferencesListTile.getTintedIcon( + Icons.open_in_new, + context, + ); + + @override + Future runHeaderAction() async => LaunchUrlHelper.launchURL( + appLocalizations.donate_url, + ); + + @override + List getChildren() => []; +} diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_page.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_page.dart index 1aae09487a3..984a1de4481 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_page.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_page.dart @@ -5,9 +5,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:matomo_tracker/matomo_tracker.dart'; import 'package:provider/provider.dart'; +import 'package:smooth_app/background/background_task_manager.dart'; import 'package:smooth_app/data_models/preferences/user_preferences.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; import 'package:smooth_app/data_models/user_management_provider.dart'; +import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart'; import 'package:smooth_app/helpers/app_helper.dart'; @@ -16,9 +18,11 @@ import 'package:smooth_app/pages/preferences/user_preferences_account.dart'; import 'package:smooth_app/pages/preferences/user_preferences_connect.dart'; import 'package:smooth_app/pages/preferences/user_preferences_contribute.dart'; import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_donation.dart'; import 'package:smooth_app/pages/preferences/user_preferences_faq.dart'; import 'package:smooth_app/pages/preferences/user_preferences_food.dart'; import 'package:smooth_app/pages/preferences/user_preferences_item.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_prices.dart'; import 'package:smooth_app/pages/preferences/user_preferences_settings.dart'; import 'package:smooth_app/pages/preferences/user_preferences_widgets.dart'; import 'package:smooth_app/themes/theme_provider.dart'; @@ -32,6 +36,8 @@ enum PreferencePageType { SETTINGS('settings'), CONTRIBUTE('contribute'), FAQ('faq'), + DONATION('donation'), + PRICES('prices'), CONNECT('connect'); const PreferencePageType(this.tag); @@ -44,6 +50,8 @@ enum PreferencePageType { required final UserPreferences userPreferences, required final BuildContext context, }) { + final LocalDatabase localDatabase = context.read(); + BackgroundTaskManager.getInstance(localDatabase).run(); final AppLocalizations appLocalizations = AppLocalizations.of(context); final ThemeProvider themeProvider = context.read(); final ThemeData themeData = Theme.of(context); @@ -97,6 +105,20 @@ enum PreferencePageType { appLocalizations: appLocalizations, themeData: themeData, ); + case PreferencePageType.DONATION: + return UserPreferencesDonation( + context: context, + userPreferences: userPreferences, + appLocalizations: appLocalizations, + themeData: themeData, + ); + case PreferencePageType.PRICES: + return UserPreferencesPrices( + context: context, + userPreferences: userPreferences, + appLocalizations: appLocalizations, + themeData: themeData, + ); case PreferencePageType.CONNECT: return UserPreferencesConnect( context: context, @@ -113,6 +135,8 @@ enum PreferencePageType { [ PreferencePageType.ACCOUNT, PreferencePageType.FOOD, + PreferencePageType.PRICES, + PreferencePageType.DONATION, PreferencePageType.SETTINGS, PreferencePageType.CONTRIBUTE, PreferencePageType.FAQ, diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_prices.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_prices.dart new file mode 100644 index 00000000000..8cb5528a303 --- /dev/null +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_prices.dart @@ -0,0 +1,181 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:smooth_app/helpers/launch_url_helper.dart'; +import 'package:smooth_app/pages/navigator/app_navigator.dart'; +import 'package:smooth_app/pages/preferences/abstract_user_preferences.dart'; +import 'package:smooth_app/pages/preferences/lazy_counter.dart'; +import 'package:smooth_app/pages/preferences/lazy_counter_widget.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_item.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_list_tile.dart'; +import 'package:smooth_app/pages/preferences/user_preferences_page.dart'; +import 'package:smooth_app/pages/prices/get_prices_model.dart'; +import 'package:smooth_app/pages/prices/price_user_button.dart'; +import 'package:smooth_app/pages/prices/prices_page.dart'; +import 'package:smooth_app/pages/prices/prices_proofs_page.dart'; +import 'package:smooth_app/pages/prices/prices_users_page.dart'; +import 'package:smooth_app/pages/prices/product_price_add_page.dart'; +import 'package:smooth_app/query/product_query.dart'; + +/// Display of "Prices" for the preferences page. +class UserPreferencesPrices extends AbstractUserPreferences { + UserPreferencesPrices({ + required super.context, + required super.userPreferences, + required super.appLocalizations, + required super.themeData, + }); + + @override + PreferencePageType getPreferencePageType() => PreferencePageType.PRICES; + + @override + String getTitleString() => appLocalizations.prices_generic_title; + + @override + IconData getLeadingIconData() => CupertinoIcons.money_dollar_circle; + + @override + List getChildren() { + final String userId = ProductQuery.getWriteUser().userId; + final bool isConnected = OpenFoodAPIConfiguration.globalUser != null; + return [ + if (isConnected) + _getListTile( + PriceUserButton.showUserTitle( + user: userId, + context: context, + ), + () async => PriceUserButton.showUserPrices( + user: userId, + context: context, + ), + CupertinoIcons.money_dollar_circle, + lazyCounter: LazyCounterPrices(userId), + ), + if (isConnected) + _getListTile( + appLocalizations.user_search_proofs_title, + () async => Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => const PricesProofsPage( + selectProof: false, + ), + ), + ), + Icons.receipt, + ), + _getListTile( + appLocalizations.prices_add_a_receipt, + () async => ProductPriceAddPage.showProductPage( + context: context, + proofType: ProofType.receipt, + ), + Icons.add_shopping_cart, + ), + _getListTile( + appLocalizations.prices_add_price_tags, + () async => ProductPriceAddPage.showProductPage( + context: context, + proofType: ProofType.priceTag, + ), + Icons.add_shopping_cart, + ), + _getListTile( + appLocalizations.all_search_prices_latest_title, + () async => Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => PricesPage( + GetPricesModel( + parameters: GetPricesParameters() + ..orderBy = >[ + const OrderBy( + field: GetPricesOrderField.created, + ascending: false, + ), + ] + ..pageSize = GetPricesModel.pageSize + ..pageNumber = 1, + displayOwner: true, + displayProduct: true, + uri: OpenPricesAPIClient.getUri( + path: 'prices', + uriHelper: ProductQuery.uriPricesHelper, + ), + title: appLocalizations.all_search_prices_latest_title, + lazyCounterPrices: const LazyCounterPrices(null), + ), + ), + ), + ), + CupertinoIcons.money_dollar_circle, + lazyCounter: const LazyCounterPrices(null), + ), + _getListTile( + appLocalizations.all_search_prices_top_user_title, + () async => Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => const PricesUsersPage(), + ), + ), + Icons.account_box, + ), + _getPriceListTile( + appLocalizations.all_search_prices_top_location_title, + 'locations', + ), + _getPriceListTile( + appLocalizations.all_search_prices_top_product_title, + 'products', + ), + ]; + } + + // we need the [AppNavigator] for a better back-gesture management. + @override + Future runHeaderAction() async => AppNavigator.of(context).push( + AppRoutes.PREFERENCES(PreferencePageType.PRICES), + ); + + UserPreferencesItem _getPriceListTile( + final String title, + final String path, + ) => + _getListTile( + title, + () async => LaunchUrlHelper.launchURL( + OpenPricesAPIClient.getUri( + path: path, + uriHelper: ProductQuery.uriPricesHelper, + ).toString(), + ), + Icons.open_in_new, + ); + + UserPreferencesItem _getListTile( + final String title, + final VoidCallback onTap, + final IconData leading, { + final LazyCounter? lazyCounter, + }) => + UserPreferencesItemSimple( + labels: [title], + builder: (_) => Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + elevation: 5, + color: Theme.of(context).cardColor, + child: UserPreferencesListTile( + title: Text(title), + onTap: onTap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + leading: UserPreferencesListTile.getTintedIcon(leading, context), + trailing: + lazyCounter == null ? null : LazyCounterWidget(lazyCounter), + ), + ), + ); +} diff --git a/packages/smooth_app/lib/pages/prices/price_add_product_card.dart b/packages/smooth_app/lib/pages/prices/price_add_product_card.dart index fd1930d32d4..1a8c709293c 100644 --- a/packages/smooth_app/lib/pages/prices/price_add_product_card.dart +++ b/packages/smooth_app/lib/pages/prices/price_add_product_card.dart @@ -24,6 +24,17 @@ class _PriceAddProductCardState extends State { String? _latestScannedBarcode; + // we create dummy focus nodes to focus on, when we need to unfocus. + final List _dummyFocusNodes = []; + + @override + void dispose() { + for (final FocusNode focusNode in _dummyFocusNodes) { + focusNode.dispose(); + } + super.dispose(); + } + @override Widget build(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); @@ -85,7 +96,8 @@ class _PriceAddProductCardState extends State { context, listen: false, ); - for (final PriceAmountModel model in priceModel.priceAmountModels) { + for (int i = 0; i < priceModel.length; i++) { + final PriceAmountModel model = priceModel.elementAt(i); if (model.product.barcode == barcode) { await showDialog( context: context, @@ -100,7 +112,7 @@ class _PriceAddProductCardState extends State { return; } } - priceModel.priceAmountModels.add( + priceModel.add( PriceAmountModel( product: PriceMetaProduct.unknown( barcode, @@ -109,6 +121,13 @@ class _PriceAddProductCardState extends State { ), ), ); + + // unfocus from the previous price amount text field. + // looks like the most efficient way to unfocus: focus somewhere in space... + final FocusNode focusNode = FocusNode(); + _dummyFocusNodes.add(focusNode); + FocusScope.of(context).requestFocus(focusNode); + priceModel.notifyListeners(); } diff --git a/packages/smooth_app/lib/pages/prices/price_amount_card.dart b/packages/smooth_app/lib/pages/prices/price_amount_card.dart index 893ff6d8144..65553aa5244 100644 --- a/packages/smooth_app/lib/pages/prices/price_amount_card.dart +++ b/packages/smooth_app/lib/pages/prices/price_amount_card.dart @@ -32,7 +32,7 @@ class _PriceAmountCardState extends State { final PriceAmountModel model = Provider.of( context, listen: false, - ).priceAmountModels[widget.index]; + ).elementAt(widget.index); _controllerPaid = TextEditingController(text: model.paidPrice); _controllerWithoutDiscount = TextEditingController(text: model.priceWithoutDiscount); @@ -49,8 +49,8 @@ class _PriceAmountCardState extends State { Widget build(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); final PriceModel priceModel = Provider.of(context); - final PriceAmountModel model = priceModel.priceAmountModels[widget.index]; - final int total = priceModel.priceAmountModels.length; + final PriceAmountModel model = priceModel.elementAt(widget.index); + final int total = priceModel.length; return SmoothCard( child: Column( @@ -62,12 +62,8 @@ class _PriceAmountCardState extends State { PriceProductListTile( product: model.product, trailingIconData: total == 1 ? null : Icons.clear, - onPressed: total == 1 - ? null - : () { - priceModel.priceAmountModels.removeAt(widget.index); - priceModel.notifyListeners(); - }, + onPressed: + total == 1 ? null : () => priceModel.removeAt(widget.index), ), SmoothLargeButtonWithIcon( icon: model.promo ? Icons.check_box : Icons.check_box_outline_blank, diff --git a/packages/smooth_app/lib/pages/prices/price_amount_model.dart b/packages/smooth_app/lib/pages/prices/price_amount_model.dart index 089d4cfd4e3..8d3f61129ac 100644 --- a/packages/smooth_app/lib/pages/prices/price_amount_model.dart +++ b/packages/smooth_app/lib/pages/prices/price_amount_model.dart @@ -8,10 +8,29 @@ class PriceAmountModel { required this.product, }); - PriceMetaProduct product; + final PriceMetaProduct product; - String paidPrice = ''; - String priceWithoutDiscount = ''; + bool _hasChanged = false; + + bool get hasChanged => _hasChanged; + + String _paidPrice = ''; + + String get paidPrice => _paidPrice; + + set paidPrice(final String value) { + _hasChanged = true; + _paidPrice = value; + } + + String _priceWithoutDiscount = ''; + + String get priceWithoutDiscount => _priceWithoutDiscount; + + set priceWithoutDiscount(final String value) { + _hasChanged = true; + _priceWithoutDiscount = value; + } late double _checkedPaidPrice; double? _checkedPriceWithoutDiscount; @@ -20,7 +39,14 @@ class PriceAmountModel { double? get checkedPriceWithoutDiscount => _checkedPriceWithoutDiscount; - bool promo = false; + bool _promo = false; + + bool get promo => _promo; + + set promo(final bool value) { + _hasChanged = true; + _promo = value; + } static double? validateDouble(final String value) => double.tryParse(value) ?? diff --git a/packages/smooth_app/lib/pages/prices/price_model.dart b/packages/smooth_app/lib/pages/prices/price_model.dart index ee2d9d09764..c4193e077de 100644 --- a/packages/smooth_app/lib/pages/prices/price_model.dart +++ b/packages/smooth_app/lib/pages/prices/price_model.dart @@ -24,23 +24,43 @@ class PriceModel with ChangeNotifier { _date = DateTime.now(), _currency = currency, _locations = locations, - priceAmountModels = [ + _priceAmountModels = [ if (initialProduct != null) PriceAmountModel(product: initialProduct), ]; PriceModel.proof({ required Proof proof, - }) : priceAmountModels = [] { - setProof(proof); + }) : _priceAmountModels = [] { + setProof(proof, init: true); } - void setProof(final Proof proof) { + bool _hasChanged = false; + + bool get hasChanged { + if (_hasChanged) { + return true; + } + for (final PriceAmountModel priceAmountModel in _priceAmountModels) { + if (priceAmountModel.hasChanged) { + return true; + } + } + return false; + } + + void setProof(final Proof proof, {final bool init = false}) { + if (!init) { + _hasChanged = true; + } _proof = proof; _cropParameters = null; _proofType = proof.type!; _date = proof.date!; _locations = null; _currency = proof.currency!; + if (!init) { + notifyListeners(); + } } /// Checks if a proof cannot be reused for prices adding. @@ -58,13 +78,30 @@ class PriceModel with ChangeNotifier { bool get hasImage => _proof != null || _cropParameters != null; - final List priceAmountModels; + final List _priceAmountModels; + + void add(final PriceAmountModel priceAmountModel) { + _hasChanged = true; + _priceAmountModels.add(priceAmountModel); + notifyListeners(); + } + + void removeAt(final int index) { + _hasChanged = true; + _priceAmountModels.removeAt(index); + notifyListeners(); + } + + PriceAmountModel elementAt(final int index) => _priceAmountModels[index]; + + int get length => _priceAmountModels.length; CropParameters? _cropParameters; CropParameters? get cropParameters => _cropParameters; set cropParameters(final CropParameters? value) { + _hasChanged = true; _cropParameters = value; _proof = null; notifyListeners(); @@ -79,6 +116,7 @@ class PriceModel with ChangeNotifier { ProofType get proofType => _proof != null ? _proof!.type! : _proofType; set proofType(final ProofType proofType) { + _hasChanged = true; _proofType = proofType; notifyListeners(); } @@ -88,6 +126,7 @@ class PriceModel with ChangeNotifier { DateTime get date => _date; set date(final DateTime date) { + _hasChanged = true; _date = date; notifyListeners(); } @@ -100,6 +139,7 @@ class PriceModel with ChangeNotifier { List? get locations => _locations; set locations(final List? locations) { + _hasChanged = true; _locations = locations; notifyListeners(); } @@ -113,6 +153,7 @@ class PriceModel with ChangeNotifier { Currency get currency => _currency; set currency(final Currency currency) { + _hasChanged = true; _currency = currency; notifyListeners(); } @@ -133,7 +174,7 @@ class PriceModel with ChangeNotifier { } } - for (final PriceAmountModel priceAmountModel in priceAmountModels) { + for (final PriceAmountModel priceAmountModel in _priceAmountModels) { final String? checkParameters = priceAmountModel.checkParameters(context); if (checkParameters != null) { return checkParameters; @@ -152,7 +193,7 @@ class PriceModel with ChangeNotifier { final List pricesAreDiscounted = []; final List prices = []; final List pricesWithoutDiscount = []; - for (final PriceAmountModel priceAmountModel in priceAmountModels) { + for (final PriceAmountModel priceAmountModel in _priceAmountModels) { barcodes.add(priceAmountModel.product.barcode); pricesAreDiscounted.add(priceAmountModel.promo); prices.add(priceAmountModel.checkedPaidPrice); diff --git a/packages/smooth_app/lib/pages/prices/price_proof_card.dart b/packages/smooth_app/lib/pages/prices/price_proof_card.dart index 6ad223dfc7b..287f4e8a14e 100644 --- a/packages/smooth_app/lib/pages/prices/price_proof_card.dart +++ b/packages/smooth_app/lib/pages/prices/price_proof_card.dart @@ -128,7 +128,6 @@ enum _ProofSource { ); if (proof != null) { model.setProof(proof); - model.notifyListeners(); } return; case _ProofSource.camera: diff --git a/packages/smooth_app/lib/pages/prices/product_price_add_page.dart b/packages/smooth_app/lib/pages/prices/product_price_add_page.dart index 13d71fac9fc..b9efbf90fbe 100644 --- a/packages/smooth_app/lib/pages/prices/product_price_add_page.dart +++ b/packages/smooth_app/lib/pages/prices/product_price_add_page.dart @@ -20,8 +20,10 @@ import 'package:smooth_app/pages/prices/price_meta_product.dart'; import 'package:smooth_app/pages/prices/price_model.dart'; import 'package:smooth_app/pages/prices/price_proof_card.dart'; import 'package:smooth_app/pages/product/common/product_refresher.dart'; +import 'package:smooth_app/pages/product/may_exit_page_helper.dart'; import 'package:smooth_app/widgets/smooth_app_bar.dart'; import 'package:smooth_app/widgets/smooth_scaffold.dart'; +import 'package:smooth_app/widgets/will_pop_scope.dart'; /// Single page that displays all the elements of price adding. class ProductPriceAddPage extends StatefulWidget { @@ -81,7 +83,6 @@ class _ProductPriceAddPageState extends State @override Widget build(BuildContext context) { - // TODO(monsieurtanuki): add WillPopScope2 return ChangeNotifierProvider.value( value: widget.model, builder: ( @@ -90,85 +91,67 @@ class _ProductPriceAddPageState extends State ) { final AppLocalizations appLocalizations = AppLocalizations.of(context); final PriceModel model = Provider.of(context); - return Form( - key: _formKey, - child: SmoothScaffold( - appBar: SmoothAppBar( - centerTitle: false, - leading: const SmoothBackButton(), - title: Text( - appLocalizations.prices_add_n_prices( - model.priceAmountModels.length, + return WillPopScope2( + onWillPop: () async => ( + await _mayExitPage( + saving: false, + model: model, + ), + null + ), + child: Form( + key: _formKey, + child: SmoothScaffold( + appBar: SmoothAppBar( + centerTitle: false, + leading: const SmoothBackButton(), + title: Text( + appLocalizations.prices_add_n_prices( + model.length, + ), ), + actions: [ + IconButton( + icon: const Icon(Icons.info), + onPressed: () async => _doesAcceptWarning(justInfo: true), + ), + ], ), - actions: [ - IconButton( - icon: const Icon(Icons.info), - onPressed: () async => _doesAcceptWarning(justInfo: true), + body: SingleChildScrollView( + padding: const EdgeInsets.all(LARGE_SPACE), + child: Column( + children: [ + const PriceProofCard(), + const SizedBox(height: LARGE_SPACE), + const PriceDateCard(), + const SizedBox(height: LARGE_SPACE), + const PriceLocationCard(), + const SizedBox(height: LARGE_SPACE), + const PriceCurrencyCard(), + const SizedBox(height: LARGE_SPACE), + for (int i = 0; i < model.length; i++) + PriceAmountCard( + key: Key(model.elementAt(i).product.barcode), + index: i, + ), + const PriceAddProductCard(), + // so that the last items don't get hidden by the FAB + const SizedBox(height: MINIMUM_TOUCH_SIZE * 2), + ], ), - ], - ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(LARGE_SPACE), - child: Column( - children: [ - const PriceProofCard(), - const SizedBox(height: LARGE_SPACE), - const PriceDateCard(), - const SizedBox(height: LARGE_SPACE), - const PriceLocationCard(), - const SizedBox(height: LARGE_SPACE), - const PriceCurrencyCard(), - const SizedBox(height: LARGE_SPACE), - for (int i = 0; i < model.priceAmountModels.length; i++) - PriceAmountCard( - key: Key(model.priceAmountModels[i].product.barcode), - index: i, - ), - const PriceAddProductCard(), - // so that the last items don't get hidden by the FAB - const SizedBox(height: MINIMUM_TOUCH_SIZE * 2), - ], ), - ), - floatingActionButton: FloatingActionButton.extended( - onPressed: model.priceAmountModels.isEmpty - ? null - : () async { - if (!await _check(context)) { - return; - } - if (!context.mounted) { - return; - } - - final UserPreferences userPreferences = - context.read(); - const String flagTag = - UserPreferences.TAG_PRICE_PRIVACY_WARNING; - final bool? already = userPreferences.getFlag(flagTag); - if (already != true) { - final bool? accepts = - await _doesAcceptWarning(justInfo: false); - if (accepts != true) { - return; - } - await userPreferences.setFlag(flagTag, true); - } - if (!context.mounted) { - return; - } - - await model.addTask(context); - if (!context.mounted) { - return; - } - Navigator.of(context).pop(); - }, - icon: const Icon(Icons.send), - label: Text( - appLocalizations.prices_send_n_prices( - model.priceAmountModels.length, + floatingActionButton: FloatingActionButton.extended( + onPressed: () async => _exitPage( + await _mayExitPage( + saving: true, + model: model, + ), + ), + icon: const Icon(Icons.send), + label: Text( + appLocalizations.prices_send_n_prices( + model.length, + ), ), ), ), @@ -201,15 +184,17 @@ class _ProductPriceAddPageState extends State } /// Returns true if the basic checks passed. - Future _check(final BuildContext context) async { + Future _check( + final BuildContext context, + final PriceModel model, + ) async { if (!_formKey.currentState!.validate()) { return false; } String? error; try { - error = Provider.of(context, listen: false) - .checkParameters(context); + error = model.checkParameters(context); } catch (e) { error = e.toString(); } @@ -232,4 +217,68 @@ class _ProductPriceAddPageState extends State @override String get actionName => 'Opened price_page with ${widget.model.proofType.offTag}'; + + /// Exits the page if the [flag] is `true`. + void _exitPage(final bool flag) { + if (flag) { + Navigator.of(context).pop(); + } + } + + /// Returns `true` if we should really exit the page. + /// + /// Parameter [saving] tells about the context: are we leaving the page, + /// or have we clicked on the "save" button? + Future _mayExitPage({ + required final bool saving, + required PriceModel model, + }) async { + if (!model.hasChanged) { + return true; + } + + if (!saving) { + final bool? pleaseSave = + await MayExitPageHelper().openSaveBeforeLeavingDialog( + context, + title: AppLocalizations.of(context).prices_add_n_prices( + model.length, + ), + ); + if (pleaseSave == null) { + return false; + } + if (pleaseSave == false) { + return true; + } + if (!mounted) { + return false; + } + } + + if (!await _check(context, model)) { + return false; + } + if (!mounted) { + return false; + } + + final UserPreferences userPreferences = context.read(); + const String flagTag = UserPreferences.TAG_PRICE_PRIVACY_WARNING; + final bool? already = userPreferences.getFlag(flagTag); + if (already != true) { + final bool? accepts = await _doesAcceptWarning(justInfo: false); + if (accepts != true) { + return false; + } + await userPreferences.setFlag(flagTag, true); + } + if (!mounted) { + return true; + } + + await model.addTask(context); + + return true; + } } diff --git a/packages/smooth_app/lib/pages/product/add_nutrition_button.dart b/packages/smooth_app/lib/pages/product/add_nutrition_button.dart index 445fa12d418..27e7b002405 100644 --- a/packages/smooth_app/lib/pages/product/add_nutrition_button.dart +++ b/packages/smooth_app/lib/pages/product/add_nutrition_button.dart @@ -10,6 +10,10 @@ class AddNutritionButton extends StatelessWidget { final Product product; + static bool acceptsNutritionFacts(final Product product) => + product.productType != ProductType.product && + product.productType != ProductType.beauty; + @override Widget build(BuildContext context) => addPanelButton( AppLocalizations.of(context).score_add_missing_nutrition_facts, diff --git a/packages/smooth_app/lib/pages/product/common/product_list_page.dart b/packages/smooth_app/lib/pages/product/common/product_list_page.dart index d9f89d8bd28..edf7293dec0 100644 --- a/packages/smooth_app/lib/pages/product/common/product_list_page.dart +++ b/packages/smooth_app/lib/pages/product/common/product_list_page.dart @@ -447,26 +447,34 @@ class _ProductListPageState extends State final List barcodes, final LocalDatabase localDatabase, ) async { + bool fresh = true; try { final OpenFoodFactsLanguage language = ProductQuery.getLanguage(); - final SearchResult searchResult = await OpenFoodAPIClient.searchProducts( - ProductQuery.getReadUser(), - ProductRefresher().getBarcodeListQueryConfiguration( - barcodes, - language, - ), - uriHelper: ProductQuery.getUriProductHelper(), - ); - final List? freshProducts = searchResult.products; - if (freshProducts == null) { - return false; + final Map> productTypes = + await DaoProduct(localDatabase).getProductTypes(barcodes); + for (final MapEntry> entry + in productTypes.entries) { + final SearchResult searchResult = + await OpenFoodAPIClient.searchProducts( + ProductQuery.getReadUser(), + ProductRefresher().getBarcodeListQueryConfiguration( + entry.value, + language, + ), + uriHelper: ProductQuery.getUriProductHelper(productType: entry.key), + ); + final List? freshProducts = searchResult.products; + if (freshProducts == null) { + fresh = false; + } else { + await DaoProduct(localDatabase).putAll(freshProducts, language); + localDatabase.upToDate.setLatestDownloadedProducts(freshProducts); + } } - await DaoProduct(localDatabase).putAll(freshProducts, language); - localDatabase.upToDate.setLatestDownloadedProducts(freshProducts); final RobotoffInsightHelper robotoffInsightHelper = RobotoffInsightHelper(localDatabase); await robotoffInsightHelper.clearInsightAnnotationsSaved(); - return true; + return fresh; } catch (e) { // } diff --git a/packages/smooth_app/lib/pages/product/common/product_list_popup_items.dart b/packages/smooth_app/lib/pages/product/common/product_list_popup_items.dart index fbbd20e1796..d52e1f82729 100644 --- a/packages/smooth_app/lib/pages/product/common/product_list_popup_items.dart +++ b/packages/smooth_app/lib/pages/product/common/product_list_popup_items.dart @@ -1,7 +1,11 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:openfoodfacts/openfoodfacts.dart'; import 'package:share_plus/share_plus.dart'; import 'package:smooth_app/data_models/product_list.dart'; +import 'package:smooth_app/database/dao_product.dart'; import 'package:smooth_app/database/dao_product_list.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; @@ -53,6 +57,22 @@ abstract class ProductListPopupItem { label: getTitle(appLocalizations), type: isDestructive() ? SmoothPopupMenuItemType.destructive : null, ); + + /// Returns the first possible URL/server that contains at least one product. + @protected + Future _getFirstUrl({ + required final ProductList productList, + required final LocalDatabase localDatabase, + }) async { + final List products = productList.getList(); + final Map> productTypes = + await DaoProduct(localDatabase).getProductTypes(products); + for (final MapEntry> entry + in productTypes.entries) { + return shareProductList(entry.value, entry.key); + } + return null; + } } /// Popup menu item for the product list page: clear list. @@ -147,15 +167,21 @@ class ProductListPopupShare extends ProductListPopupItem { required final BuildContext context, }) async { final AppLocalizations appLocalizations = AppLocalizations.of(context); - final List products = productList.getList(); - final String url = shareProductList(products).toString(); - final RenderBox? box = context.findRenderObject() as RenderBox?; - AnalyticsHelper.trackEvent(AnalyticsEvent.shareList); - Share.share( - appLocalizations.share_product_list_text(url), - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - ); + final String? url = (await _getFirstUrl( + productList: productList, + localDatabase: localDatabase, + )) + ?.toString(); + if (url != null) { + AnalyticsHelper.trackEvent(AnalyticsEvent.shareList); + unawaited( + Share.share( + appLocalizations.share_product_list_text(url), + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ), + ); + } return null; } } @@ -179,9 +205,14 @@ class ProductListPopupOpenInWeb extends ProductListPopupItem { required final LocalDatabase localDatabase, required final BuildContext context, }) async { - final List products = productList.getList(); - AnalyticsHelper.trackEvent(AnalyticsEvent.openListWeb); - await launchUrl(shareProductList(products)); + final Uri? firstUrl = await _getFirstUrl( + productList: productList, + localDatabase: localDatabase, + ); + if (firstUrl != null) { + AnalyticsHelper.trackEvent(AnalyticsEvent.openListWeb); + unawaited(launchUrl(firstUrl)); + } return null; } } diff --git a/packages/smooth_app/lib/pages/product/common/product_refresher.dart b/packages/smooth_app/lib/pages/product/common/product_refresher.dart index be1553bbf4e..12d9b338d07 100644 --- a/packages/smooth_app/lib/pages/product/common/product_refresher.dart +++ b/packages/smooth_app/lib/pages/product/common/product_refresher.dart @@ -107,8 +107,9 @@ class ProductRefresher { Future silentFetchAndRefreshList({ required final List barcodes, required final LocalDatabase localDatabase, + required final ProductType productType, }) async => - _fetchAndRefreshList(localDatabase, barcodes); + _fetchAndRefreshList(localDatabase, barcodes, productType); /// Fetches the product from the server and refreshes the local database. /// @@ -246,13 +247,14 @@ class ProductRefresher { Future _fetchAndRefreshList( final LocalDatabase localDatabase, final List barcodes, + final ProductType productType, ) async { try { final OpenFoodFactsLanguage language = ProductQuery.getLanguage(); final SearchResult searchResult = await OpenFoodAPIClient.searchProducts( ProductQuery.getReadUser(), getBarcodeListQueryConfiguration(barcodes, language), - uriHelper: ProductQuery.getUriProductHelper(), + uriHelper: ProductQuery.getUriProductHelper(productType: productType), ); if (searchResult.products == null) { return null; diff --git a/packages/smooth_app/lib/pages/product/ordered_nutrients_cache.dart b/packages/smooth_app/lib/pages/product/ordered_nutrients_cache.dart index 1c08aa7d3a3..ec0dd4e688e 100644 --- a/packages/smooth_app/lib/pages/product/ordered_nutrients_cache.dart +++ b/packages/smooth_app/lib/pages/product/ordered_nutrients_cache.dart @@ -54,12 +54,16 @@ class OrderedNutrientsCache { return null; } + UriProductHelper get _uriProductHelper => ProductQuery.getUriProductHelper( + productType: ProductType.food, + ); + /// Downloads the ordered nutrients and caches them in the database. Future _download() async { final String string = await OpenFoodAPIClient.getOrderedNutrientsJsonString( country: ProductQuery.getCountry(), language: ProductQuery.getLanguage(), - uriHelper: ProductQuery.getUriProductHelper(), + uriHelper: _uriProductHelper, ); final OrderedNutrients result = OrderedNutrients.fromJson( jsonDecode(string) as Map, @@ -75,6 +79,6 @@ class OrderedNutrientsCache { return 'nutrients.pl' '/${country.offTag}' '/${language.code}' - '/${ProductQuery.getUriProductHelper().domain}'; + '/${_uriProductHelper.domain}'; } } diff --git a/packages/smooth_app/lib/query/product_query.dart b/packages/smooth_app/lib/query/product_query.dart index 5a892f89e1f..315942d1ecf 100644 --- a/packages/smooth_app/lib/query/product_query.dart +++ b/packages/smooth_app/lib/query/product_query.dart @@ -216,7 +216,7 @@ abstract class ProductQuery { // TODO(monsieurtanuki): make the parameter "required" static UriProductHelper getUriProductHelper({ - final ProductType? productType, + required final ProductType? productType, }) { final UriProductHelper currentUriProductHelper = _uriProductHelper; if (productType == null) { diff --git a/packages/smooth_app/lib/query/random_questions_query.dart b/packages/smooth_app/lib/query/random_questions_query.dart index 15bdd7c0bad..b368b4a79ba 100644 --- a/packages/smooth_app/lib/query/random_questions_query.dart +++ b/packages/smooth_app/lib/query/random_questions_query.dart @@ -31,6 +31,7 @@ class RandomQuestionsQuery extends QuestionsQuery { await ProductRefresher().silentFetchAndRefreshList( barcodes: barcodes, localDatabase: localDatabase, + productType: ProductType.food, ); return result.questions ?? []; } diff --git a/packages/smooth_app/lib/themes/smooth_theme.dart b/packages/smooth_app/lib/themes/smooth_theme.dart index 1d41a28ecbe..dabc6bb5d32 100644 --- a/packages/smooth_app/lib/themes/smooth_theme.dart +++ b/packages/smooth_app/lib/themes/smooth_theme.dart @@ -39,7 +39,6 @@ class SmoothTheme { } return ThemeData( - useMaterial3: false, fontFamily: 'OpenSans', primaryColor: DARK_BROWN_COLOR, extensions: [ @@ -49,7 +48,11 @@ class SmoothTheme { canvasColor: themeProvider.currentTheme == THEME_AMOLED ? myColorScheme.surface : null, + scaffoldBackgroundColor: + brightness == Brightness.light ? null : const Color(0xFF303030), bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: + brightness == Brightness.light ? null : const Color(0xFF303030), selectedIconTheme: const IconThemeData(size: 24.0), showSelectedLabels: true, selectedItemColor: brightness == Brightness.dark diff --git a/packages/smooth_app/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/smooth_app/macos/Flutter/GeneratedPluginRegistrant.swift index 89d3e87fb16..7e96d3476de 100644 --- a/packages/smooth_app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/packages/smooth_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -19,8 +19,9 @@ import rive_common import sentry_flutter import share_plus import shared_preferences_foundation -import sqflite +import sqflite_darwin import url_launcher_macos +import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) @@ -39,4 +40,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + FLTWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "FLTWebViewFlutterPlugin")) } diff --git a/packages/smooth_app/pubspec.lock b/packages/smooth_app/pubspec.lock index f2c7f72af07..90fa2c08bda 100644 --- a/packages/smooth_app/pubspec.lock +++ b/packages/smooth_app/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: ansicolor - sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" app_settings: dependency: "direct main" description: @@ -73,10 +73,10 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" async: dependency: "direct main" description: @@ -632,10 +632,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840 + sha256: ee5c9bd2b74ea8676442fd4ab876b5d41681df49276488854d6c81a5377c0ef1 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -1317,18 +1317,18 @@ packages: dependency: "direct main" description: name: rive - sha256: cd45b071b288e4bef05f25423e1001a9b3218b81745deae18c9b4d2a2952cc56 + sha256: "468f0880d49c513e09fdfba26e4abd9d50433c2cf398210b62948d8de3837dd5" url: "https://pub.dev" source: hosted - version: "0.13.14" + version: "0.13.15" rive_common: dependency: transitive description: name: rive_common - sha256: c7bf0781b1621629361579c300ac2f8aa1a238227a242202a596e82becc244d7 + sha256: a3e5786f8d85c89977062b9ceeb3b72a7c28f81e32fb68497744042ce20bee2f url: "https://pub.dev" source: hosted - version: "0.4.11" + version: "0.4.12" scanner_ml_kit: dependency: "direct main" description: @@ -1511,10 +1511,10 @@ packages: dependency: "direct main" description: name: sqflite_common_ffi - sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5" + sha256: d316908f1537725427ff2827a5c5f3b2c1bc311caed985fe3c9b10939c9e11ca url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.4" sqflite_darwin: dependency: transitive description: @@ -1853,4 +1853,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.5.3 <4.0.0" - flutter: ">=3.22.0" + flutter: ">=3.24.0" diff --git a/packages/smooth_app/pubspec.yaml b/packages/smooth_app/pubspec.yaml index db77bc79af6..2ae7057e37c 100644 --- a/packages/smooth_app/pubspec.yaml +++ b/packages/smooth_app/pubspec.yaml @@ -39,7 +39,7 @@ dependencies: provider: 6.1.2 sentry_flutter: 8.9.0 sqflite: 2.4.0 - sqflite_common_ffi: 2.3.3 + sqflite_common_ffi: 2.3.4 url_launcher: 6.3.0 visibility_detector: 0.4.0+2 app_settings: 5.1.1 @@ -48,7 +48,7 @@ dependencies: path: ../app_store/shared audioplayers: 6.1.0 flutter_email_sender: 6.0.3 - flutter_native_splash: 2.4.1 + flutter_native_splash: 2.4.2 image: 4.3.0 auto_size_text: 3.0.0 crop_image: 1.0.13 @@ -60,7 +60,7 @@ dependencies: share_plus: 10.0.0 fimber: 0.7.0 shimmer: ^3.0.0 - rive: 0.13.14 + rive: 0.13.15 webview_flutter: 4.10.0 webview_flutter_android: 4.0.0 webview_flutter_wkwebview: 3.16.0 diff --git a/packages/smooth_app/test/dialogs/generic_lib/dialogs_test.dart b/packages/smooth_app/test/dialogs/generic_lib/dialogs_test.dart index 7d56d66a46f..526c55a091a 100644 --- a/packages/smooth_app/test/dialogs/generic_lib/dialogs_test.dart +++ b/packages/smooth_app/test/dialogs/generic_lib/dialogs_test.dart @@ -14,6 +14,7 @@ import 'package:smooth_app/themes/contrast_provider.dart'; import 'package:smooth_app/themes/theme_provider.dart'; import '../../tests_utils/goldens.dart'; +import '../../tests_utils/local_database_mock.dart'; import '../../tests_utils/mocks.dart'; void main() { @@ -72,6 +73,7 @@ void main() { const UserPreferencesPage( type: PreferencePageType.CONTRIBUTE, ), + localDatabase: MockLocalDatabase(), ), ); await tester.pumpAndSettle(); diff --git a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Improving-amoled.png b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Improving-amoled.png index b79b8afec1c..f25fedf1e9c 100644 Binary files a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Improving-amoled.png and b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Improving-amoled.png differ diff --git a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Improving-dark.png b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Improving-dark.png index caa158d5192..1a9d9ff2460 100644 Binary files a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Improving-dark.png and b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Improving-dark.png differ diff --git a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Improving-light.png b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Improving-light.png index 6d5b48f3e20..344412aa8b8 100644 Binary files a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Improving-light.png and b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Improving-light.png differ diff --git a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Software development-amoled.png b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Software development-amoled.png index e5f07246fbd..a77edc1daa8 100644 Binary files a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Software development-amoled.png and b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Software development-amoled.png differ diff --git a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Software development-dark.png b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Software development-dark.png index 6cabe8e4e76..9e929155379 100644 Binary files a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Software development-dark.png and b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Software development-dark.png differ diff --git a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Software development-light.png b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Software development-light.png index c921cf18e43..b3684890501 100644 Binary files a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Software development-light.png and b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Software development-light.png differ diff --git a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Translate-amoled.png b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Translate-amoled.png index 293e3fe825a..9c529dfc451 100644 Binary files a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Translate-amoled.png and b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Translate-amoled.png differ diff --git a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Translate-dark.png b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Translate-dark.png index c33f743a550..34a51af35f5 100644 Binary files a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Translate-dark.png and b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Translate-dark.png differ diff --git a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Translate-light.png b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Translate-light.png index 27f7f335f12..fed17855358 100644 Binary files a/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Translate-light.png and b/packages/smooth_app/test/dialogs/generic_lib/goldens/user_preferences_page_dialogs_Translate-light.png differ diff --git a/packages/smooth_app/test/pages/goldens/user_preferences_page-amoled.png b/packages/smooth_app/test/pages/goldens/user_preferences_page-amoled.png index 64382a53292..3c9875c5ae2 100644 Binary files a/packages/smooth_app/test/pages/goldens/user_preferences_page-amoled.png and b/packages/smooth_app/test/pages/goldens/user_preferences_page-amoled.png differ diff --git a/packages/smooth_app/test/pages/goldens/user_preferences_page-dark.png b/packages/smooth_app/test/pages/goldens/user_preferences_page-dark.png index e76e022f32c..69a553f39bf 100644 Binary files a/packages/smooth_app/test/pages/goldens/user_preferences_page-dark.png and b/packages/smooth_app/test/pages/goldens/user_preferences_page-dark.png differ diff --git a/packages/smooth_app/test/pages/goldens/user_preferences_page-light.png b/packages/smooth_app/test/pages/goldens/user_preferences_page-light.png index ca39caa42e0..5f4a6a1facb 100644 Binary files a/packages/smooth_app/test/pages/goldens/user_preferences_page-light.png and b/packages/smooth_app/test/pages/goldens/user_preferences_page-light.png differ diff --git a/packages/smooth_app/test/tests_utils/local_database_mock.dart b/packages/smooth_app/test/tests_utils/local_database_mock.dart index ef0de9bb4ac..3ae0aa6edcd 100644 --- a/packages/smooth_app/test/tests_utils/local_database_mock.dart +++ b/packages/smooth_app/test/tests_utils/local_database_mock.dart @@ -2,6 +2,15 @@ import 'package:mockito/mockito.dart'; import 'package:smooth_app/database/local_database.dart'; class MockLocalDatabase extends Mock implements LocalDatabase { + final Map _daoInt = {}; + @override List getAllTaskIds() => []; + + @override + int? daoIntGet(final String key) => _daoInt[key]; + + @override + Future daoIntPut(final String key, final int? value) async => + _daoInt[key] = value; } diff --git a/packages/smooth_app/test/users/forgot_password_page_layout_test.dart b/packages/smooth_app/test/users/forgot_password_page_layout_test.dart index a626e8c30d1..d28891bf296 100644 --- a/packages/smooth_app/test/users/forgot_password_page_layout_test.dart +++ b/packages/smooth_app/test/users/forgot_password_page_layout_test.dart @@ -11,6 +11,7 @@ import 'package:smooth_app/themes/contrast_provider.dart'; import 'package:smooth_app/themes/theme_provider.dart'; import '../tests_utils/goldens.dart'; +import '../tests_utils/local_database_mock.dart'; import '../tests_utils/mocks.dart'; void main() { @@ -51,6 +52,7 @@ void main() { textContrastProvider, colorProvider, const ForgotPasswordPage(), + localDatabase: MockLocalDatabase(), ), ); await tester.pump(); diff --git a/packages/smooth_app/test/users/goldens/forgot_password_page-amoled.png b/packages/smooth_app/test/users/goldens/forgot_password_page-amoled.png index 4fc677b97b9..0e6ba4a8b27 100644 Binary files a/packages/smooth_app/test/users/goldens/forgot_password_page-amoled.png and b/packages/smooth_app/test/users/goldens/forgot_password_page-amoled.png differ diff --git a/packages/smooth_app/test/users/goldens/forgot_password_page-dark.png b/packages/smooth_app/test/users/goldens/forgot_password_page-dark.png index 1b3b69af5d1..61f6eaacb71 100644 Binary files a/packages/smooth_app/test/users/goldens/forgot_password_page-dark.png and b/packages/smooth_app/test/users/goldens/forgot_password_page-dark.png differ diff --git a/packages/smooth_app/test/users/goldens/forgot_password_page-light.png b/packages/smooth_app/test/users/goldens/forgot_password_page-light.png index 40e8d359963..587f6843fa9 100644 Binary files a/packages/smooth_app/test/users/goldens/forgot_password_page-light.png and b/packages/smooth_app/test/users/goldens/forgot_password_page-light.png differ diff --git a/packages/smooth_app/test/users/goldens/login_page-amoled.png b/packages/smooth_app/test/users/goldens/login_page-amoled.png index 9b17255e087..1995d859309 100644 Binary files a/packages/smooth_app/test/users/goldens/login_page-amoled.png and b/packages/smooth_app/test/users/goldens/login_page-amoled.png differ diff --git a/packages/smooth_app/test/users/goldens/login_page-dark.png b/packages/smooth_app/test/users/goldens/login_page-dark.png index ddc5665b0b6..73055b8e859 100644 Binary files a/packages/smooth_app/test/users/goldens/login_page-dark.png and b/packages/smooth_app/test/users/goldens/login_page-dark.png differ diff --git a/packages/smooth_app/test/users/goldens/login_page-light.png b/packages/smooth_app/test/users/goldens/login_page-light.png index 3fef36993c0..9c3882d254b 100644 Binary files a/packages/smooth_app/test/users/goldens/login_page-light.png and b/packages/smooth_app/test/users/goldens/login_page-light.png differ diff --git a/packages/smooth_app/test/users/goldens/signup_page-amoled.png b/packages/smooth_app/test/users/goldens/signup_page-amoled.png index 6b42e517f8e..418ac47a6b5 100644 Binary files a/packages/smooth_app/test/users/goldens/signup_page-amoled.png and b/packages/smooth_app/test/users/goldens/signup_page-amoled.png differ diff --git a/packages/smooth_app/test/users/goldens/signup_page-dark.png b/packages/smooth_app/test/users/goldens/signup_page-dark.png index c2b579ff965..b5e852ea974 100644 Binary files a/packages/smooth_app/test/users/goldens/signup_page-dark.png and b/packages/smooth_app/test/users/goldens/signup_page-dark.png differ diff --git a/packages/smooth_app/test/users/goldens/signup_page-light.png b/packages/smooth_app/test/users/goldens/signup_page-light.png index 13694afb6b6..4bef8080769 100644 Binary files a/packages/smooth_app/test/users/goldens/signup_page-light.png and b/packages/smooth_app/test/users/goldens/signup_page-light.png differ diff --git a/packages/smooth_app/test/users/login_page_layout_test.dart b/packages/smooth_app/test/users/login_page_layout_test.dart index fc372dae573..7719b164caa 100644 --- a/packages/smooth_app/test/users/login_page_layout_test.dart +++ b/packages/smooth_app/test/users/login_page_layout_test.dart @@ -11,6 +11,7 @@ import 'package:smooth_app/themes/contrast_provider.dart'; import 'package:smooth_app/themes/theme_provider.dart'; import '../tests_utils/goldens.dart'; +import '../tests_utils/local_database_mock.dart'; import '../tests_utils/mocks.dart'; void main() { @@ -51,6 +52,7 @@ void main() { textContrastProvider, colorProvider, const LoginPage(), + localDatabase: MockLocalDatabase(), ), ); await tester.pump(); diff --git a/packages/smooth_app/test/users/signup_page_layout_test.dart b/packages/smooth_app/test/users/signup_page_layout_test.dart index 24246e34560..fa91ca43728 100644 --- a/packages/smooth_app/test/users/signup_page_layout_test.dart +++ b/packages/smooth_app/test/users/signup_page_layout_test.dart @@ -11,6 +11,7 @@ import 'package:smooth_app/themes/contrast_provider.dart'; import 'package:smooth_app/themes/theme_provider.dart'; import '../tests_utils/goldens.dart'; +import '../tests_utils/local_database_mock.dart'; import '../tests_utils/mocks.dart'; void main() { @@ -51,6 +52,7 @@ void main() { textContrastProvider, colorProvider, const SignUpPage(), + localDatabase: MockLocalDatabase(), ), ); await tester.pump();