40 Commits

Author SHA1 Message Date
github-actions[bot]
903fd823d8 chore(master): release 0.12.0 2026-05-15 17:11:17 +03:00
bca375d701 ci: pin release builds to release tag 2026-05-15 17:06:44 +03:00
28f8a269f8 chore(ios): fix build settings 2026-05-15 17:06:44 +03:00
c944ec5140 build: update makefile targets 2026-05-15 01:58:05 +03:00
550027e1bc test: cover list creation, share queues, and photo FAB menu 2026-05-14 17:37:26 +03:00
41e8ac13a0 feat: create new lists from the list selector 2026-05-14 15:22:29 +03:00
d8802690c0 feat: take photos directly from the photo board 2026-05-14 15:15:36 +03:00
60b16aad30 feat: share photos, links, and text to Pantry from other apps 2026-05-14 14:48:57 +03:00
689e4d6cad feat: add setting to show spacing between categories in checklist items 2026-05-14 12:02:57 +03:00
db3bbc0f17 build: add ios submit-only fastlane lane 2026-05-12 12:18:06 +03:00
github-actions[bot]
504da80c09 chore(master): release 0.11.0 2026-05-12 12:00:27 +03:00
0c575eaa26 fix: preserve subpath for Nextcloud instances hosted on sub-paths 2026-05-12 11:56:59 +03:00
38b5d8b464 build: update makefile targets 2026-05-12 11:37:15 +03:00
ef2bc851de feat: add setting to require checkbox tap to complete checklist items 2026-05-12 11:37:15 +03:00
github-actions[bot]
448d85834b chore(master): release 0.10.1 2026-04-26 23:42:06 +03:00
7b5f9c1518 fix: make markdown links clickable 2026-04-26 22:43:52 +03:00
67581d04f0 build: update fastlane 2026-04-21 11:29:39 +03:00
be83067fb7 build: disable preview html for fastlane deliver 2026-04-21 11:18:18 +03:00
ea4590f0ed build: fix makefile syntax 2026-04-21 11:11:41 +03:00
github-actions[bot]
721f32e1ea chore(master): release 0.10.0 2026-04-21 10:08:29 +03:00
36a74b39e1 feat: update notes view & edit ui 2026-04-21 09:52:38 +03:00
08159faec2 fix: bug where note grid would not clip correctly 2026-04-21 09:38:41 +03:00
346bfb9d92 docs: add apple app store link 2026-04-20 09:57:20 +03:00
github-actions[bot]
0b9cda92ca chore(master): release 0.9.10 2026-04-19 17:25:15 +03:00
9f45b2344e build: fix signing
Release-As: 0.9.10
2026-04-19 17:14:18 +03:00
github-actions[bot]
4ae96c37d1 chore(master): release 0.9.9 2026-04-19 15:31:34 +03:00
852e9c47f3 build: re-sign with stripping
Release-As: 0.9.9
2026-04-19 15:28:18 +03:00
github-actions[bot]
769bf74400 chore(master): release 0.9.8 2026-04-19 14:12:40 +03:00
132d9e33a6 build: strip deps metadata from build
Release-As: 0.9.8
2026-04-19 14:10:06 +03:00
github-actions[bot]
7ff161f1c5 chore(master): release 0.9.7 2026-04-19 13:26:45 +03:00
7ea2901867 build: disable deps metadata in apk
Release-As: 0.9.7
2026-04-19 13:22:59 +03:00
github-actions[bot]
7a849d5d36 chore(master): release 0.9.6 2026-04-19 12:47:04 +03:00
b1d7eccd82 build: remove zipalign
Release-As: 0.9.6
2026-04-19 12:44:20 +03:00
github-actions[bot]
0f999750f1 chore(master): release 0.9.5 2026-04-19 12:25:07 +03:00
6d2173f08d build: zipalign
Release-As: 0.9.5
2026-04-19 12:15:46 +03:00
github-actions[bot]
768e78ace9 chore(master): release 0.9.4 2026-04-19 11:56:13 +03:00
d41d2b81be build: remove apk obfuscation
Release-As: 0.9.4
2026-04-19 11:39:31 +03:00
1f09e9d5aa build: use .flutter-version file for github actions 2026-04-19 11:30:24 +03:00
github-actions[bot]
1bb5b85b3e chore(master): release 0.9.3 2026-04-19 11:21:13 +03:00
3e4051a846 build: upgrade flutter version
Release-As: 0.9.3
2026-04-19 11:19:17 +03:00
86 changed files with 3906 additions and 489 deletions

View File

@@ -1 +1 @@
3.41.4
3.41.7

View File

@@ -11,7 +11,20 @@ permissions:
pull-requests: write
jobs:
setup:
runs-on: ubuntu-latest
outputs:
flutter-version: ${{ steps.read.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: .flutter-version
sparse-checkout-cone-mode: false
- id: read
run: echo "version=$(cat .flutter-version | tr -d '[:space:]')" >> "$GITHUB_OUTPUT"
lint:
needs: setup
runs-on: ubuntu-latest
steps:
@@ -21,7 +34,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version-file: .flutter-version
flutter-version: ${{ needs.setup.outputs.flutter-version }}
cache: true
- name: Cache pub dependencies
@@ -35,9 +48,6 @@ jobs:
- name: Install dependencies
run: flutter pub get
- name: Stub .env
run: cp .env.example .env
- name: Verify formatting
run: dart format --output=none --set-exit-if-changed .
@@ -45,6 +55,7 @@ jobs:
run: flutter analyze --no-fatal-infos
test:
needs: setup
runs-on: ubuntu-latest
steps:
@@ -54,7 +65,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version-file: .flutter-version
flutter-version: ${{ needs.setup.outputs.flutter-version }}
cache: true
- name: Cache pub dependencies
@@ -68,12 +79,8 @@ jobs:
- name: Install dependencies
run: flutter pub get
- name: Stub .env
run: cp .env.example .env
- name: Run tests
run: flutter test --coverage --dart-define-from-file=.env
run: flutter test --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
@@ -91,13 +98,15 @@ jobs:
fastlane/metadata/ios/en-US/changelogs
build-android:
needs: release-please
needs: [setup, release-please]
if: ${{ needs.release-please.outputs.release_created }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ needs.release-please.outputs.tag_name }}
- name: Setup Java
uses: actions/setup-java@v4
@@ -109,7 +118,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version-file: .flutter-version
flutter-version: ${{ needs.setup.outputs.flutter-version }}
cache: true
- name: Cache pub dependencies
@@ -123,9 +132,6 @@ jobs:
- name: Install dependencies
run: flutter pub get
- name: Create .env
run: cp .env.example .env
- name: Remove JNI build-id for reproducible builds
run: sed -i -e 's/-Wl,/-Wl,--build-id=none,/' $PUB_CACHE/hosted/pub.dev/jni-*/src/CMakeLists.txt
@@ -149,11 +155,10 @@ jobs:
2>/dev/null | grep "SHA256:" | awk '{print $2}'
- name: Build split APKs
run: flutter build apk --release --split-per-abi --obfuscate --split-debug-info=build/debug-info-apk --dart-define-from-file=.env
run: flutter build apk --release --split-per-abi
- name: Build App Bundle
run: flutter build appbundle --release --obfuscate --split-debug-info=build/debug-info-aab --dart-define-from-file=.env
run: flutter build appbundle --release --obfuscate --split-debug-info=build/debug-info-aab
- name: Rename artifacts
run: |
VERSION=${{ needs.release-please.outputs.version }}
@@ -180,18 +185,20 @@ jobs:
build/app/outputs/bundle/release/pantry-${{ needs.release-please.outputs.version }}.aab
build-ios:
needs: release-please
needs: [setup, release-please]
if: false # TEMPORARILY DISABLED — was: ${{ needs.release-please.outputs.release_created }}
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ needs.release-please.outputs.tag_name }}
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version-file: .flutter-version
flutter-version: ${{ needs.setup.outputs.flutter-version }}
cache: true
- name: Cache pub dependencies
@@ -214,12 +221,8 @@ jobs:
- name: Install dependencies
run: flutter pub get
- name: Create .env
run: cp .env.example .env
- name: Build iOS (no codesign)
run: flutter build ios --release --no-codesign --obfuscate --split-debug-info=build/debug-info-ios --dart-define-from-file=.env
run: flutter build ios --release --no-codesign --obfuscate --split-debug-info=build/debug-info-ios
- name: Create unsigned IPA
run: |
mkdir -p build/ios/ipa

View File

@@ -1,5 +1,102 @@
# Changelog
## [0.12.0](https://github.com/chenasraf/pantry-flutter/compare/v0.11.0...v0.12.0) (2026-05-15)
### Features
* add setting to show spacing between categories in checklist items ([689e4d6](https://github.com/chenasraf/pantry-flutter/commit/689e4d6cad89a84621b77e300198d12ac43131ff))
* create new lists from the list selector ([41e8ac1](https://github.com/chenasraf/pantry-flutter/commit/41e8ac13a0ab78436bc2674220ed282f8410862d))
* share photos, links, and text to Pantry from other apps ([60b16aa](https://github.com/chenasraf/pantry-flutter/commit/60b16aad309ec0f02b542c53a7bfafb9f9652da3))
* take photos directly from the photo board ([d880269](https://github.com/chenasraf/pantry-flutter/commit/d8802690c0a3ab80346f84ea1df4b3774ba6e4ee))
## [0.11.0](https://github.com/chenasraf/pantry-flutter/compare/v0.10.1...v0.11.0) (2026-05-12)
### Features
* add setting to require checkbox tap to complete checklist items ([ef2bc85](https://github.com/chenasraf/pantry-flutter/commit/ef2bc851deedc180dda51fbfb7378aef7145cf5f))
### Bug Fixes
* preserve subpath for Nextcloud instances hosted on sub-paths ([0c575ea](https://github.com/chenasraf/pantry-flutter/commit/0c575eaa2601dc8c83c6b874f2d81b54a0f6bf01))
## [0.10.1](https://github.com/chenasraf/pantry-flutter/compare/v0.10.0...v0.10.1) (2026-04-26)
### Bug Fixes
* make markdown links clickable ([7b5f9c1](https://github.com/chenasraf/pantry-flutter/commit/7b5f9c151845dde90275a8289b3114483d2b214d))
## [0.10.0](https://github.com/chenasraf/pantry-flutter/compare/v0.9.10...v0.10.0) (2026-04-21)
### Features
* update notes view & edit ui ([36a74b3](https://github.com/chenasraf/pantry-flutter/commit/36a74b39e1beb37fc2f1446f8d45945a22289c6b))
### Bug Fixes
* bug where note grid would not clip correctly ([08159fa](https://github.com/chenasraf/pantry-flutter/commit/08159faec22422da983f23685c53b45088d74b2a))
## [0.9.10](https://github.com/chenasraf/pantry-flutter/compare/v0.9.9...v0.9.10) (2026-04-19)
### Build System
* fix signing ([9f45b23](https://github.com/chenasraf/pantry-flutter/commit/9f45b2344ef87708d55889e8fb80f465808fb0c7))
## [0.9.9](https://github.com/chenasraf/pantry-flutter/compare/v0.9.8...v0.9.9) (2026-04-19)
### Build System
* re-sign with stripping ([852e9c4](https://github.com/chenasraf/pantry-flutter/commit/852e9c47f3cf11145a72ed78b6415ecd0da2b111))
## [0.9.8](https://github.com/chenasraf/pantry-flutter/compare/v0.9.7...v0.9.8) (2026-04-19)
### Build System
* strip deps metadata from build ([132d9e3](https://github.com/chenasraf/pantry-flutter/commit/132d9e33a6662554149a7941053594c6c7ab9043))
## [0.9.7](https://github.com/chenasraf/pantry-flutter/compare/v0.9.6...v0.9.7) (2026-04-19)
### Build System
* disable deps metadata in apk ([7ea2901](https://github.com/chenasraf/pantry-flutter/commit/7ea2901867b25e2c4bcd46cd744af7b12656b6dd))
## [0.9.6](https://github.com/chenasraf/pantry-flutter/compare/v0.9.5...v0.9.6) (2026-04-19)
### Build System
* remove zipalign ([b1d7ecc](https://github.com/chenasraf/pantry-flutter/commit/b1d7eccd822fcbac56683a4085a329784c71ca7f))
## [0.9.5](https://github.com/chenasraf/pantry-flutter/compare/v0.9.4...v0.9.5) (2026-04-19)
### Build System
* zipalign ([6d2173f](https://github.com/chenasraf/pantry-flutter/commit/6d2173f08d72d8e188c9a4aabe044ff922d4b32f))
## [0.9.4](https://github.com/chenasraf/pantry-flutter/compare/v0.9.3...v0.9.4) (2026-04-19)
### Build System
* remove apk obfuscation ([d41d2b8](https://github.com/chenasraf/pantry-flutter/commit/d41d2b81beb512384cb3c2978bebd201c3bf53a0))
## [0.9.3](https://github.com/chenasraf/pantry-flutter/compare/v0.9.2...v0.9.3) (2026-04-19)
### Build System
* upgrade flutter version ([3e4051a](https://github.com/chenasraf/pantry-flutter/commit/3e4051a8462271543381496a9d60a22239b8d8da))
## [0.9.2](https://github.com/chenasraf/pantry-flutter/compare/v0.9.1...v0.9.2) (2026-04-19)

View File

@@ -8,8 +8,8 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1237.0)
aws-sdk-core (3.244.0)
aws-partitions (1.1246.0)
aws-sdk-core (3.246.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -17,19 +17,19 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.123.0)
aws-sdk-kms (1.124.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.219.0)
aws-sdk-s3 (1.221.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
base64 (0.3.0)
benchmark (0.5.0)
bigdecimal (4.1.1)
bigdecimal (4.1.2)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
@@ -72,14 +72,14 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.1)
fastlane (2.232.2)
CFPropertyList (>= 2.3, < 4.0.0)
abbrev (~> 0.1.2)
fastlane (2.234.0)
CFPropertyList (>= 2.3, < 5.0.0)
abbrev (~> 0.1)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.197)
babosa (>= 1.0.3, < 2.0.0)
base64 (~> 0.2.0)
base64 (~> 0.2)
benchmark (>= 0.1.0)
bundler (>= 1.17.3, < 5.0.0)
colored (~> 1.2)
@@ -92,7 +92,7 @@ GEM
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
fastlane-sirp (>= 1.1.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
@@ -105,9 +105,9 @@ GEM
logger (>= 1.6, < 2.0)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
mutex_m (~> 0.3.0)
mutex_m (~> 0.3)
naturally (~> 2.2)
nkf (~> 0.2.0)
nkf (~> 0.2)
optparse (>= 0.1.1, < 1.0.0)
ostruct (>= 0.1.0)
plist (>= 3.1.0, < 4.0.0)
@@ -122,10 +122,9 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
fastlane-sirp (1.1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.98.0)
google-apis-androidpublisher_v3 (0.100.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (0.18.0)
addressable (~> 2.5, >= 2.5.1)
@@ -135,11 +134,11 @@ GEM
mutex_m
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
google-apis-iamcredentials_v1 (0.26.0)
google-apis-iamcredentials_v1 (0.27.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-playcustomapp_v1 (0.17.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.61.0)
google-apis-storage_v1 (0.62.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
@@ -147,7 +146,7 @@ GEM
google-cloud-env (2.1.1)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.6.0)
google-cloud-storage (1.59.0)
google-cloud-storage (1.60.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-core (>= 0.18, < 2)
@@ -169,13 +168,13 @@ GEM
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.19.3)
json (2.19.5)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.19.1)
multi_json (1.21.1)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
@@ -186,7 +185,7 @@ GEM
ostruct (0.6.3)
plist (3.7.2)
public_suffix (7.0.5)
rake (13.3.1)
rake (13.4.2)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
@@ -205,7 +204,6 @@ 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)
@@ -243,15 +241,15 @@ CHECKSUMS
artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263
atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f
aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
aws-partitions (1.1237.0) sha256=9b82f529b69ad83a8e4c5e123038924ed5e8f59bd6064a293ef20efc63364841
aws-sdk-core (3.244.0) sha256=3e458c078b0c5bdee95bc370c3a483374b3224cf730c1f9f0faf849a5d9a18ea
aws-sdk-kms (1.123.0) sha256=d405f37e82f8fa32045ca8980be266c0b45b37aaf2012afe0254321a1e811f20
aws-sdk-s3 (1.219.0) sha256=6a755d7377978525758b3c29185ca6a10128ce2b07555ca37c4549de10c2f1c7
aws-partitions (1.1246.0) sha256=809cd7d38b7ba4ea651c9879248ecf9fd0f8289412e76f26478d2b37064faa1d
aws-sdk-core (3.246.0) sha256=393864ec8948560e69fcccc2e4d256b40c7028eb98930608dd295279e3c4ddcc
aws-sdk-kms (1.124.0) sha256=40d00ab706d7e49fd620270bd0dcb546f266295abdd49b54fec2611e2a41f37c
aws-sdk-s3 (1.221.0) sha256=a05488eab2083a1e90b02e18479d8f65e401081d671b2d138992a2c5fef85491
aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00
babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99
base64 (0.2.0) sha256=0f25e9b21a02a0cc0cea8ef92b2041035d39350946e8789c562b2d1a3da01507
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
bigdecimal (4.1.1) sha256=1c09efab961da45203c8316b0cdaec0ff391dfadb952dd459584b63ebf8054ca
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e
colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c
colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a
@@ -277,29 +275,29 @@ CHECKSUMS
faraday-retry (1.0.4) sha256=dc659233777fabf96c69c2ffe56c0a5d2c102af90321a42cc6c90157bcd716aa
faraday_middleware (1.2.1) sha256=d45b78c8ee864c4783fbc276f845243d4a7918a67301c052647bacabec0529e9
fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20
fastlane (2.232.2) sha256=978689f60f0fc3d54699de86ef12be4eda9f5b52217c1798965257c390d2b112
fastlane-sirp (1.0.0) sha256=66478f25bcd039ec02ccf65625373fca29646fa73d655eb533c915f106c5e641
fastlane (2.234.0) sha256=b74835681ad9a8e9c0931a5727dad1bab433895ac534c864a1ed5749625d26e9
fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847
gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939
google-apis-androidpublisher_v3 (0.98.0) sha256=094fb952419c1131c16c4dfa66e0c96e6a2fa33adbe266f614b84b22cbc8c5cb
google-apis-androidpublisher_v3 (0.100.0) sha256=7a82935bee985190e8fe23bf5e53df3a27d65dd084114bb71b846b617de16489
google-apis-core (0.18.0) sha256=96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee
google-apis-iamcredentials_v1 (0.26.0) sha256=3ff70a10a1d6cddf2554e95b7c5df2c26afdeaeb64100048a355194da19e48a3
google-apis-iamcredentials_v1 (0.27.0) sha256=9289f29968610754ef11d98b9ec627f0153f3e2616fef839aef096de529f6d1e
google-apis-playcustomapp_v1 (0.17.0) sha256=d5bc90b705f3f862bab4998086449b0abe704ee1685a84821daa90ca7fa95a78
google-apis-storage_v1 (0.61.0) sha256=b330e599b58e6a01533c189525398d6dbdbaf101ffb0c60145940b57e1c982e8
google-apis-storage_v1 (0.62.0) sha256=f62467c36df53287fb0252ebb4da85f9e25d7b4c5809d045c2aab1fc307760c1
google-cloud-core (1.8.0) sha256=e572edcbf189cfcab16590628a516cec3f4f63454b730e59f0b36575120281cf
google-cloud-env (2.1.1) sha256=cf4bb8c7d517ee1ea692baedf06e0b56ce68007549d8d5a66481aa9f97f46999
google-cloud-errors (1.6.0) sha256=1da8476dd706ad04b9d32e3c4b90d07d3463b37d6407cb56d41342ea7647d0a1
google-cloud-storage (1.59.0) sha256=b8c9a5661d775d65ccb279bb1d6be07fd8152576eb0146c2026bd023c4b186b9
google-cloud-storage (1.60.0) sha256=b21b752d37945d678a4533be5ef4303f15d33a964d8bc709c7c41c3600f650db
googleauth (1.11.2) sha256=7e6bacaeed7aea3dd66dcea985266839816af6633e9f5983c3c2e0e40a44731e
highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479
http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6
httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8
jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
json (2.19.3) sha256=289b0bb53052a1fa8c34ab33cc750b659ba14a5c45f3fcf4b18762dc67c78646
json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59
jwt (2.10.2) sha256=31e1ee46f7359883d5e622446969fe9c118c3da87a0b1dca765ce269c3a0c4f4
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
multi_json (1.19.1) sha256=7aefeff8f2c854bf739931a238e4aea64592845e0c0395c8a7d2eea7fdd631b7
multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080
multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8
mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723
@@ -310,7 +308,7 @@ CHECKSUMS
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace
retriable (3.4.1) sha256=fb3f114b7d492121c158c01f3d5152b5a615c5b70d5877d0bc08c7ec3725c3bc
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
@@ -320,7 +318,6 @@ CHECKSUMS
security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7
signet (0.21.0) sha256=d617e9fbf24928280d39dcfefba9a0372d1c38187ffffd0a9283957a10a8cd5b
simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b
sysrandom (1.0.5) sha256=5ac1ac3c2ec64ef76ac91018059f541b7e8f437fbda1ccddb4f2c56a9ccf1e75
terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea
terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91
trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3

View File

@@ -18,7 +18,6 @@ help:
@echo ""
@echo " Development:"
@echo " run Run the app in debug mode"
@echo " webapp-run Run the web app in default browser"
@echo " format Format all Dart files"
@echo " analyze Analyze all Dart files"
@echo " check Check all files (format + analyze, no changes)"
@@ -41,20 +40,21 @@ help:
@echo " android-build-aab Build Android App Bundle"
@echo " android-push Build APK and push to device via adb"
@echo " ios-build Build iOS (no codesign)"
@echo " web-build Build web app"
@echo " build-all Build all platforms"
@echo ""
@echo " Release:"
@echo " android-release-apk Build APK and copy to build/release/"
@echo " android-release-aab Build AAB and copy to build/release/"
@echo " ios-release Build iOS and create unsigned IPA in build/release/"
@echo " web-release Build web and create zip in build/release/"
@echo " ios-release Build IPA and copy to build/release/"
@echo " release-all Build and release all platforms"
@echo ""
@echo " Deploying:"
@echo " android-deploy Build AAB and upload to Google Play (TRACK=internal|beta|production, STATUS=draft|completed)"
@echo " android-promote Promote release between tracks (FROM=internal, TO=production, STATUS=draft|completed)"
@echo " ios-deploy Build IPA and upload (DEST=testflight|appstore, default: testflight)"
@echo " ios-submit Submit the existing App Store build for review (no upload)"
@echo " deploy-production Build and deploy to production (Google Play + App Store)"
@echo " deploy-beta Build and deploy to beta (Google Play beta + TestFlight)"
# Setup
.PHONY: get
@@ -83,12 +83,7 @@ i18n-watch:
# Development
.PHONY: run
run:
flutter run --dart-define-from-file=.env
.PHONY: webapp-run
webapp-run:
open http://localhost:5111 & flutter run -d web-server --web-port=5111 --dart-define-from-file=.env
flutter run
.PHONY: format
format:
dart format .
@@ -106,25 +101,23 @@ check:
.PHONY: test
test:
ifdef FILES
flutter test $(FILES) --dart-define-from-file=.env
flutter test $(FILES)
else
flutter test --dart-define-from-file=.env
flutter test
endif
.PHONY: test-coverage
test-coverage:
flutter test --coverage --dart-define-from-file=.env
flutter test --coverage
@echo "Coverage report generated at coverage/lcov.info"
# Building
.PHONY: android-build-apk
android-build-apk:
flutter build apk --release --obfuscate --split-debug-info=build/debug-info-apk --dart-define-from-file=.env
flutter build apk --release --obfuscate --split-debug-info=build/debug-info-apk
.PHONY: android-build-apk-split
android-build-apk-split:
flutter build apk --release --split-per-abi --obfuscate --split-debug-info=build/debug-info-apk --dart-define-from-file=.env
flutter build apk --release --split-per-abi --obfuscate --split-debug-info=build/debug-info-apk
.PHONY: android-install
android-install: android-build-apk
flutter install
@@ -136,22 +129,16 @@ android-push: android-build-apk
.PHONY: android-build-aab
android-build-aab:
flutter build appbundle --release --obfuscate --split-debug-info=build/debug-info-aab --dart-define-from-file=.env
flutter build appbundle --release --obfuscate --split-debug-info=build/debug-info-aab
.PHONY: ios-build
ios-build:
flutter build ios --release --no-codesign --obfuscate --split-debug-info=build/debug-info-ios --dart-define-from-file=.env
flutter build ios --release --no-codesign --obfuscate --split-debug-info=build/debug-info-ios
.PHONY: ios-build-ipa
ios-build-ipa:
flutter build ipa --release --obfuscate --split-debug-info=build/debug-info-ios --dart-define-from-file=.env --export-options-plist=ios/ExportOptions.plist
.PHONY: web-build
web-build:
flutter build web --release --dart-define-from-file=.env
.PHONY: build-all
build-all: android-build-apk android-build-aab web-build
build-all: android-build-apk android-build-aab
# Release (build + copy renamed artifacts to build/release/)
.PHONY: android-release-apk
@@ -167,7 +154,7 @@ android-release-aab: android-build-aab
@echo "-> build/release/pantry-$(VERSION).aab"
.PHONY: ios-release
ios-release: ios-build
ios-release: ios-build-ipa
mkdir -p build/release
cp build/ios/ipa/*.ipa build/release/pantry-$(VERSION).ipa
@echo "-> build/release/pantry-$(VERSION).ipa"
@@ -203,14 +190,22 @@ ios-upload:
.PHONY: ios-deploy
ios-deploy: ios-build-ipa ios-upload
.PHONY: web-release
web-release: web-build
mkdir -p build/release
cd build/web && zip -r ../release/pantry-$(VERSION)-web.zip .
@echo "-> build/release/pantry-$(VERSION)-web.zip"
.PHONY: ios-submit
ios-submit:
bundle exec fastlane ios submit
.PHONY: release-all
release-all: build-clean android-release-apk android-release-aab web-release
release-all: android-release-apk android-release-aab
.PHONY: deploy-production
deploy-production:
$(MAKE) android-deploy TRACK=production STATUS=completed
$(MAKE) ios-deploy DEST=appstore
.PHONY: deploy-beta
deploy-beta:
$(MAKE) android-deploy TRACK=beta STATUS=completed
$(MAKE) ios-deploy DEST=testflight
# CocoaPods
.PHONY: pods

View File

@@ -43,9 +43,9 @@ It may take a few minutes for your tester status to propagate.
Download the latest APK from the
[releases page](https://github.com/chenasraf/pantry-flutter/releases) and sideload onto your device.
### iOS
### App Store (iOS)
[Coming soon — TestFlight]
[Install from the App Store](https://apps.apple.com/us/app/pantry-for-nextcloud/id6762161619)
## Development

View File

@@ -49,6 +49,11 @@ android {
}
}
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
buildTypes {
debug {
applicationIdSuffix = ".debug"

View File

@@ -27,6 +27,21 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View File

@@ -17,6 +17,24 @@ subprojects {
}
subprojects {
project.evaluationDependsOn(":app")
// Force any Flutter plugin subproject (e.g. receive_sharing_intent)
// that defaults to a different Kotlin jvmTarget to match this app's
// Java/Kotlin compatibility, avoiding "Inconsistent JVM Target
// Compatibility Between Java and Kotlin Tasks" build failures.
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile>().configureEach {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
plugins.withId("com.android.library") {
extensions.configure<com.android.build.gradle.LibraryExtension>("android") {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
}
}
tasks.register<Delete>("clean") {

View File

@@ -1,2 +1,2 @@
metadata_path("./fastlane/metadata/ios")
screenshots_path("./fastlane/metadata/ios/screenshots")
screenshots_path("./fastlane/metadata/ios/en-US/screenshots")

View File

@@ -202,6 +202,7 @@ platform :ios do
skip_screenshots: true,
submit_for_review: true,
precheck_include_in_app_purchases: false,
force: true,
)
end
@@ -215,6 +216,21 @@ platform :ios do
skip_screenshots: true,
submit_for_review: false,
precheck_include_in_app_purchases: false,
force: true,
)
end
desc "Submit existing App Store build for review (no IPA upload)"
lane :submit do
deliver(
api_key: api_key,
metadata_path: File.expand_path("metadata/ios", __dir__),
screenshots_path: File.expand_path("metadata/ios/en-US/screenshots", __dir__),
skip_binary_upload: true,
skip_screenshots: true,
submit_for_review: true,
precheck_include_in_app_purchases: false,
force: true,
)
end
end

View File

@@ -68,6 +68,14 @@ Upload to App Store
Sync iOS metadata only (no IPA upload)
### ios submit
```sh
[bundle exec] fastlane ios submit
```
Submit existing App Store build for review (no IPA upload)
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.

View File

@@ -0,0 +1,2 @@
Build System
- upgrade flutter version

View File

@@ -0,0 +1,2 @@
Build System
- remove apk obfuscation

View File

@@ -0,0 +1,2 @@
Build System
- zipalign

View File

@@ -0,0 +1,2 @@
Build System
- remove zipalign

View File

@@ -0,0 +1,2 @@
Build System
- disable deps metadata in apk

View File

@@ -0,0 +1,2 @@
Build System
- strip deps metadata from build

View File

@@ -0,0 +1,2 @@
Build System
- re-sign with stripping

View File

@@ -0,0 +1,2 @@
Build System
- fix signing

View File

@@ -0,0 +1,4 @@
Features
- update notes view & edit ui
Bug Fixes
- bug where note grid would not clip correctly

View File

@@ -0,0 +1,2 @@
Bug Fixes
- make markdown links clickable

View File

@@ -0,0 +1,4 @@
Features
- add setting to require checkbox tap to complete checklist items
Bug Fixes
- preserve subpath for Nextcloud instances hosted on sub-paths

View File

@@ -0,0 +1,5 @@
Features
- add setting to show spacing between categories in checklist items
- create new lists from the list selector
- share photos, links, and text to Pantry from other apps
- take photos directly from the photo board

View File

@@ -0,0 +1,2 @@
Build System
- upgrade flutter version

View File

@@ -0,0 +1,2 @@
Build System
- remove apk obfuscation

View File

@@ -0,0 +1,2 @@
Build System
- zipalign

View File

@@ -0,0 +1,2 @@
Build System
- remove zipalign

View File

@@ -0,0 +1,2 @@
Build System
- disable deps metadata in apk

View File

@@ -0,0 +1,2 @@
Build System
- strip deps metadata from build

View File

@@ -0,0 +1,2 @@
Build System
- re-sign with stripping

View File

@@ -0,0 +1,2 @@
Build System
- fix signing

View File

@@ -0,0 +1,4 @@
Features
- update notes view & edit ui
Bug Fixes
- bug where note grid would not clip correctly

View File

@@ -0,0 +1,2 @@
Bug Fixes
- make markdown links clickable

View File

@@ -0,0 +1,4 @@
Features
- add setting to require checkbox tap to complete checklist items
Bug Fixes
- preserve subpath for Nextcloud instances hosted on sub-paths

View File

@@ -0,0 +1,5 @@
Features
- add setting to show spacing between categories in checklist items
- create new lists from the list selector
- share photos, links, and text to Pantry from other apps
- take photos directly from the photo board

10
ios/Gemfile Normal file
View File

@@ -0,0 +1,10 @@
source 'https://rubygems.org'
gem 'cocoapods'
# Pin xcodeproj to a commit on master that supports objectVersion 70
# (generated by Xcode 16+). Drop this override once a release > 1.27.0
# is published on rubygems.
gem 'xcodeproj',
git: 'https://github.com/CocoaPods/Xcodeproj.git',
ref: 'c12d2ae619ae42f947a6b07d865f69948c752df5'

194
ios/Gemfile.lock Normal file
View File

@@ -0,0 +1,194 @@
GIT
remote: https://github.com/CocoaPods/Xcodeproj.git
revision: c12d2ae619ae42f947a6b07d865f69948c752df5
ref: c12d2ae619ae42f947a6b07d865f69948c752df5
specs:
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.8)
activesupport (7.2.3.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1, < 6)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
base64 (0.3.0)
benchmark (0.5.0)
bigdecimal (4.1.2)
claide (1.1.0)
cocoapods (1.16.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.16.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.6.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.27.0, < 2.0)
cocoapods-core (1.16.2)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (2.1)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored2 (3.1.2)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
drb (2.2.3)
escape (0.0.4)
ethon (0.18.0)
ffi (>= 1.15.0)
logger
ffi (1.17.4)
ffi (1.17.4-aarch64-linux-gnu)
ffi (1.17.4-aarch64-linux-musl)
ffi (1.17.4-arm-linux-gnu)
ffi (1.17.4-arm-linux-musl)
ffi (1.17.4-arm64-darwin)
ffi (1.17.4-x86-linux-gnu)
ffi (1.17.4-x86-linux-musl)
ffi (1.17.4-x86_64-darwin)
ffi (1.17.4-x86_64-linux-gnu)
ffi (1.17.4-x86_64-linux-musl)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
httpclient (2.9.0)
mutex_m
i18n (1.14.8)
concurrent-ruby (~> 1.0)
json (2.19.5)
logger (1.7.0)
minitest (5.27.0)
molinillo (0.8.0)
mutex_m (0.3.0)
nanaimo (0.4.0)
nap (1.1.0)
netrc (0.11.0)
public_suffix (4.0.7)
rexml (3.4.4)
ruby-macho (2.5.1)
securerandom (0.4.1)
typhoeus (1.6.0)
ethon (>= 0.18.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
PLATFORMS
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
ruby
x86-linux-gnu
x86-linux-musl
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
cocoapods
xcodeproj!
CHECKSUMS
CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261
activesupport (7.2.3.1) sha256=11ebed516a43a0bb47346227a35ebae4d9427465a7c9eb197a03d5c8d283cb34
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
algoliasearch (1.27.5) sha256=26c1cddf3c2ec4bd60c148389e42702c98fdac862881dc6b07a4c0b89ffec853
atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e
cocoapods (1.16.2) sha256=0ff1c860f32df3db8b16df09b58da1a6bb2a12fe55f6d5e8be994a74fadd1e5e
cocoapods-core (1.16.2) sha256=4bb1b5c420691e60cf36fa227dec6bc48c096c34c97bb7aa512ea7f3246fc12b
cocoapods-deintegrate (1.0.5) sha256=517c2a448ef563afe99b6e7668704c27f5de9e02715a88ee9de6974dc1b3f6a2
cocoapods-downloader (2.1) sha256=bb6ebe1b3966dc4055de54f7a28b773485ac724fdf575d9bee2212d235e7b6d1
cocoapods-plugins (1.0.0) sha256=725d17ce90b52f862e73476623fd91441b4430b742d8a071000831efb440ca9a
cocoapods-search (1.0.1) sha256=1b133b0e6719ed439bd840e84a1828cca46425ab73a11eff5e096c3b2df05589
cocoapods-trunk (1.6.0) sha256=5f5bda8c172afead48fa2d43a718cf534b1313c367ba1194cebdeb9bfee9ed31
cocoapods-try (1.2.0) sha256=145b946c6e7747ed0301d975165157951153d27469e6b2763c83e25c84b9defe
colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
escape (0.0.4) sha256=e49f44ae2b4f47c6a3abd544ae77fe4157802794e32f19b8e773cbc4dcec4169
ethon (0.18.0) sha256=b598afc9f30448cb068b850714b7d6948e941476095d04f90a4ac65b8d6efcb2
ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf
ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564
ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95
ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b
ffi (1.17.4-x86-linux-gnu) sha256=38e150df5f4ca555e25beca4090823ae09657bceded154e3c52f8631c1ed72cf
ffi (1.17.4-x86-linux-musl) sha256=fbeec0fc7c795bcf86f623bb18d31ea1820f7bd580e1703a3d3740d527437809
ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496
ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
fourflusher (2.3.1) sha256=1b3de61c7c791b6a4e64f31e3719eb25203d151746bb519a0292bff1065ccaa9
fuzzy_match (2.0.4) sha256=b5de4f95816589c5b5c3ad13770c0af539b75131c158135b3f3bbba75d0cfca5
gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939
httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
molinillo (0.8.0) sha256=efbff2716324e2a30bccd3eba1ff3a735f4d5d53ffddbc6a2f32c0ca9433045d
mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723
nap (1.1.0) sha256=949691660f9d041d75be611bb2a8d2fd559c467537deac241f4097d9b5eea576
netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
public_suffix (4.0.7) sha256=8be161e2421f8d45b0098c042c06486789731ea93dc3a896d30554ee38b573b8
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
ruby-macho (2.5.1) sha256=9075e52e0f9270b552a90b24fcc6219ad149b0d15eae1bc364ecd0ac8984f5c9
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
typhoeus (1.6.0) sha256=bacc41c23e379547e29801dc235cd1699b70b955a1ba3d32b2b877aa844c331d
tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
xcodeproj (1.27.0)
BUNDLED WITH
4.0.7

View File

@@ -1,5 +1,4 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
platform :ios, '14.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -36,6 +35,17 @@ target 'Runner' do
end
end
# Share Extension target has no pod dependencies — the
# `RSIShareViewController` base class is vendored directly into the
# extension's sources (ios/Share/RSIShareViewController.swift). This
# avoids dragging in Flutter.framework and `addApplicationDelegate`
# from the receive_sharing_intent plugin, which are banned in app
# extensions. `use_frameworks!` is declared so CocoaPods doesn't flag
# the host/embedded mismatch with Runner.
target 'Share' do
use_frameworks!
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)

View File

@@ -11,6 +11,8 @@ PODS:
- Flutter
- package_info_plus (0.4.5):
- Flutter
- receive_sharing_intent (1.8.1):
- Flutter
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
@@ -28,6 +30,7 @@ DEPENDENCIES:
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
@@ -46,6 +49,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_picker_ios/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
@@ -62,11 +67,12 @@ SPEC CHECKSUMS:
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
PODFILE CHECKSUM: e8d97e41f073e724afe14e2390c9692116d292ff
COCOAPODS: 1.16.2

2
ios/Profile.xcconfig Normal file
View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
#include "Generated.xcconfig"

View File

@@ -11,8 +11,11 @@
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
4B2FECE151D6658169DC6E88 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E836629E52B02A5B6D0F862 /* Pods_RunnerTests.framework */; };
745F12F32FB5CF07007C0ADF /* Share.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 745F12E92FB5CF07007C0ADF /* Share.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
745F13182FB5D473007C0ADF /* Profile.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 745F13172FB5D473007C0ADF /* Profile.xcconfig */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
971DDFFA6B83AAC1FD474A95 /* Pods_Share.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B495CAE63B8B48AA6114881 /* Pods_Share.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -27,9 +30,27 @@
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
745F12F12FB5CF07007C0ADF /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 745F12E82FB5CF07007C0ADF;
remoteInfo = Share;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
745F12F42FB5CF07007C0ADF /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
745F12F32FB5CF07007C0ADF /* Share.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -46,12 +67,17 @@
0D02B01150F8705A7BE3C6D1 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1B495CAE63B8B48AA6114881 /* Pods_Share.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Share.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1E836629E52B02A5B6D0F862 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
31D13B974C1302A5F0CE86D2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
33DF0655613AFDEC743B1898 /* Pods-Share.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share.profile.xcconfig"; path = "Target Support Files/Pods-Share/Pods-Share.profile.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3D05507B181A60A566EB9904 /* Pods-Share.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share.debug.xcconfig"; path = "Target Support Files/Pods-Share/Pods-Share.debug.xcconfig"; sourceTree = "<group>"; };
736C2329657A6FBB9CE1D305 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
745F12E92FB5CF07007C0ADF /* Share.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Share.appex; sourceTree = BUILT_PRODUCTS_DIR; };
745F13172FB5D473007C0ADF /* Profile.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Profile.xcconfig; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
@@ -65,10 +91,36 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9885F82953FBB27851D4D171 /* Pods-Share.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share.release.xcconfig"; path = "Target Support Files/Pods-Share/Pods-Share.release.xcconfig"; sourceTree = "<group>"; };
C28EA37C4C36CE14FE19E720 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
ECEC55E4DC05821BB0BEA50F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
745F13312FB5D55F007C0ADF /* Exceptions for "Share" folder in "Share" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 745F12E82FB5CF07007C0ADF /* Share */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
745F12EA2FB5CF07007C0ADF /* Share */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
745F13312FB5D55F007C0ADF /* Exceptions for "Share" folder in "Share" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = Share;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
55917F8B3405971C4327A3FF /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -78,6 +130,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
745F12E62FB5CF07007C0ADF /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
971DDFFA6B83AAC1FD474A95 /* Pods_Share.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -102,6 +162,7 @@
children = (
31D13B974C1302A5F0CE86D2 /* Pods_Runner.framework */,
1E836629E52B02A5B6D0F862 /* Pods_RunnerTests.framework */,
1B495CAE63B8B48AA6114881 /* Pods_Share.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -122,10 +183,12 @@
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
745F12EA2FB5CF07007C0ADF /* Share */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
ECEC5B714C9B60C200CF4964 /* Pods */,
43389EC5ADFFCB84CB05BC4D /* Frameworks */,
745F13172FB5D473007C0ADF /* Profile.xcconfig */,
);
sourceTree = "<group>";
};
@@ -134,6 +197,7 @@
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
745F12E92FB5CF07007C0ADF /* Share.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -163,8 +227,10 @@
0D02B01150F8705A7BE3C6D1 /* Pods-RunnerTests.debug.xcconfig */,
91DAEB15490ECD076B82EE80 /* Pods-RunnerTests.release.xcconfig */,
736C2329657A6FBB9CE1D305 /* Pods-RunnerTests.profile.xcconfig */,
3D05507B181A60A566EB9904 /* Pods-Share.debug.xcconfig */,
9885F82953FBB27851D4D171 /* Pods-Share.release.xcconfig */,
33DF0655613AFDEC743B1898 /* Pods-Share.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
@@ -190,6 +256,27 @@
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
745F12E82FB5CF07007C0ADF /* Share */ = {
isa = PBXNativeTarget;
buildConfigurationList = 745F12F92FB5CF07007C0ADF /* Build configuration list for PBXNativeTarget "Share" */;
buildPhases = (
1DFA5925966D69304295FDAF /* [CP] Check Pods Manifest.lock */,
745F12E52FB5CF07007C0ADF /* Sources */,
745F12E62FB5CF07007C0ADF /* Frameworks */,
745F12E72FB5CF07007C0ADF /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
745F12EA2FB5CF07007C0ADF /* Share */,
);
name = Share;
productName = Share;
productReference = 745F12E92FB5CF07007C0ADF /* Share.appex */;
productType = "com.apple.product-type.app-extension";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
@@ -200,12 +287,14 @@
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
745F12F42FB5CF07007C0ADF /* Embed Foundation Extensions */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
60CC7BE4F08CFECA259D64E9 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
745F12F22FB5CF07007C0ADF /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
@@ -219,6 +308,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 2630;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@@ -226,6 +316,9 @@
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
745F12E82FB5CF07007C0ADF = {
CreatedOnToolsVersion = 26.3;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
@@ -247,6 +340,7 @@
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
745F12E82FB5CF07007C0ADF /* Share */,
);
};
/* End PBXProject section */
@@ -259,6 +353,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
745F12E72FB5CF07007C0ADF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
745F13182FB5D473007C0ADF /* Profile.xcconfig in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -273,6 +375,28 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
1DFA5925966D69304295FDAF /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Share-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -376,6 +500,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
745F12E52FB5CF07007C0ADF /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -394,6 +525,11 @@
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
745F12F22FB5CF07007C0ADF /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 745F12E82FB5CF07007C0ADF /* Share */;
targetProxy = 745F12F12FB5CF07007C0ADF /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -470,10 +606,11 @@
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
baseConfigurationReference = 745F13172FB5D473007C0ADF /* Profile.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = Y893L6NQP2;
ENABLE_BITCODE = NO;
@@ -543,6 +680,134 @@
};
name = Profile;
};
745F12F52FB5CF07007C0ADF /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3D05507B181A60A566EB9904 /* Pods-Share.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = Share/ShareRelease.entitlements;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Y893L6NQP2;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Share/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Share;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = dev.casraf.pantry.Share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
745F12F62FB5CF07007C0ADF /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9885F82953FBB27851D4D171 /* Pods-Share.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = Share/ShareRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Y893L6NQP2;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Share/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Share;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = dev.casraf.pantry.Share;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Pantry Share Distribution";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
745F12F72FB5CF07007C0ADF /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33DF0655613AFDEC743B1898 /* Pods-Share.profile.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = Share/ShareRelease.entitlements;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Y893L6NQP2;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Share/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Share;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = dev.casraf.pantry.Share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -660,6 +925,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = Y893L6NQP2;
ENABLE_BITCODE = NO;
@@ -686,6 +952,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -721,6 +988,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
745F12F92FB5CF07007C0ADF /* Build configuration list for PBXNativeTarget "Share" */ = {
isa = XCConfigurationList;
buildConfigurations = (
745F12F52FB5CF07007C0ADF /* Debug */,
745F12F62FB5CF07007C0ADF /* Release */,
745F12F72FB5CF07007C0ADF /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@@ -84,5 +84,24 @@
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSCameraUsageDescription</key>
<string>Pantry needs access to the camera so you can take photos and upload them to your photo board.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Pantry needs access to your photo library so you can pick existing photos and upload them to your photo board.</string>
<key>AppGroupId</key>
<string>group.dev.casraf.pantry</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>dev.casraf.pantry.share</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-dev.casraf.pantry</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.dev.casraf.pantry</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Share View Controller-->
<scene sceneID="ceB-am-kn3">
<objects>
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

29
ios/Share/Info.plist Normal file
View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>20</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>5</integer>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>5</integer>
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
<integer>10</integer>
</dict>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,346 @@
// Vendored from receive_sharing_intent 1.8.1 so the Share Extension does
// not need to import the pod (which transitively pulls in Flutter +
// UIApplication APIs that are banned in app extensions).
//
// Keep the constants and `SharedMediaFile` / `SharedMediaType` shapes in
// sync with `SwiftReceiveSharingIntentPlugin.swift` in the upstream pod,
// otherwise the host app side will not decode payloads correctly.
import UIKit
import Social
import MobileCoreServices
import Photos
import UniformTypeIdentifiers
let kSchemePrefix = "ShareMedia"
let kUserDefaultsKey = "ShareKey"
let kUserDefaultsMessageKey = "ShareMessageKey"
let kAppGroupIdKey = "AppGroupId"
class SharedMediaFile: Codable {
var path: String
var mimeType: String?
var thumbnail: String?
var duration: Double?
var message: String?
var type: SharedMediaType
init(
path: String,
mimeType: String? = nil,
thumbnail: String? = nil,
duration: Double? = nil,
message: String? = nil,
type: SharedMediaType
) {
self.path = path
self.mimeType = mimeType
self.thumbnail = thumbnail
self.duration = duration
self.message = message
self.type = type
}
}
enum SharedMediaType: String, Codable, CaseIterable {
case image
case video
case text
case file
case url
var toUTTypeIdentifier: String {
if #available(iOS 14.0, *) {
switch self {
case .image: return UTType.image.identifier
case .video: return UTType.movie.identifier
case .text: return UTType.text.identifier
case .file: return UTType.fileURL.identifier
case .url: return UTType.url.identifier
}
}
switch self {
case .image: return "public.image"
case .video: return "public.movie"
case .text: return "public.text"
case .file: return "public.file-url"
case .url: return "public.url"
}
}
}
@available(swift, introduced: 5.0)
open class RSIShareViewController: SLComposeServiceViewController {
var hostAppBundleIdentifier = ""
var appGroupId = ""
var sharedMedia: [SharedMediaFile] = []
open func shouldAutoRedirect() -> Bool { true }
open override func isContentValid() -> Bool { true }
open override func viewDidLoad() {
super.viewDidLoad()
loadIds()
}
open override func didSelectPost() {
saveAndRedirect(message: contentText)
}
open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let content = extensionContext!.inputItems[0] as? NSExtensionItem,
let contents = content.attachments {
for (index, attachment) in contents.enumerated() {
for type in SharedMediaType.allCases {
if attachment.hasItemConformingToTypeIdentifier(type.toUTTypeIdentifier) {
attachment.loadItem(forTypeIdentifier: type.toUTTypeIdentifier) { [weak self] data, error in
guard let this = self, error == nil else {
self?.dismissWithError()
return
}
switch type {
case .text:
if let text = data as? String {
this.handleMedia(forLiteral: text, type: type, index: index, content: content)
}
case .url:
if let url = data as? URL {
this.handleMedia(forLiteral: url.absoluteString, type: type, index: index, content: content)
}
default:
if let url = data as? URL {
this.handleMedia(forFile: url, type: type, index: index, content: content)
} else if let image = data as? UIImage {
this.handleMedia(forUIImage: image, type: type, index: index, content: content)
}
}
}
break
}
}
}
}
}
open override func configurationItems() -> [Any]! { [] }
private func loadIds() {
let shareExtensionAppBundleIdentifier = Bundle.main.bundleIdentifier!
let lastIndexOfPoint = shareExtensionAppBundleIdentifier.lastIndex(of: ".")
hostAppBundleIdentifier = String(shareExtensionAppBundleIdentifier[..<lastIndexOfPoint!])
let defaultAppGroupId = "group.\(hostAppBundleIdentifier)"
let customAppGroupId = Bundle.main.object(forInfoDictionaryKey: kAppGroupIdKey) as? String
appGroupId = customAppGroupId ?? defaultAppGroupId
}
private func handleMedia(forLiteral item: String, type: SharedMediaType, index: Int, content: NSExtensionItem) {
sharedMedia.append(SharedMediaFile(
path: item,
mimeType: type == .text ? "text/plain" : nil,
type: type
))
if index == (content.attachments?.count ?? 0) - 1, shouldAutoRedirect() {
saveAndRedirect()
}
}
private func handleMedia(forUIImage image: UIImage, type: SharedMediaType, index: Int, content: NSExtensionItem) {
guard let containerURL = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else {
if index == (content.attachments?.count ?? 0) - 1, shouldAutoRedirect() {
saveAndRedirect()
}
return
}
let tempPath = containerURL.appendingPathComponent("TempImage.png")
if writeTempFile(image, to: tempPath) {
let newPathDecoded = tempPath.absoluteString.removingPercentEncoding!
sharedMedia.append(SharedMediaFile(
path: newPathDecoded,
mimeType: type == .image ? "image/png" : nil,
type: type
))
}
if index == (content.attachments?.count ?? 0) - 1, shouldAutoRedirect() {
saveAndRedirect()
}
}
private func handleMedia(forFile url: URL, type: SharedMediaType, index: Int, content: NSExtensionItem) {
let fileName = getFileName(from: url, type: type)
guard let containerURL = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else {
if index == (content.attachments?.count ?? 0) - 1, shouldAutoRedirect() {
saveAndRedirect()
}
return
}
let newPath = containerURL.appendingPathComponent(fileName)
if copyFile(at: url, to: newPath) {
let newPathDecoded = newPath.absoluteString.removingPercentEncoding!
if type == .video, let videoInfo = getVideoInfo(from: url) {
let thumbnailPathDecoded = videoInfo.thumbnail?.removingPercentEncoding
sharedMedia.append(SharedMediaFile(
path: newPathDecoded,
mimeType: url.mimeType(),
thumbnail: thumbnailPathDecoded,
duration: videoInfo.duration,
type: type
))
} else {
sharedMedia.append(SharedMediaFile(
path: newPathDecoded,
mimeType: url.mimeType(),
type: type
))
}
}
if index == (content.attachments?.count ?? 0) - 1, shouldAutoRedirect() {
saveAndRedirect()
}
}
private func saveAndRedirect(message: String? = nil) {
let userDefaults = UserDefaults(suiteName: appGroupId)
userDefaults?.set(toData(data: sharedMedia), forKey: kUserDefaultsKey)
userDefaults?.set(message, forKey: kUserDefaultsMessageKey)
userDefaults?.synchronize()
redirectToHostApp()
}
private func redirectToHostApp() {
loadIds()
let url = URL(string: "\(kSchemePrefix)-\(hostAppBundleIdentifier):share")!
var responder = self as UIResponder?
// iOS 18 removed the legacy `openURL:` selector, so we have to
// walk the responder chain and call `UIApplication.open(_:)`
// through the dynamic cast this path is what the upstream
// receive_sharing_intent package uses.
if #available(iOS 18.0, *) {
while responder != nil {
if let application = responder as? UIApplication {
application.open(url, options: [:], completionHandler: nil)
}
responder = responder?.next
}
} else {
let selectorOpenURL = sel_registerName("openURL:")
while responder != nil {
if responder?.responds(to: selectorOpenURL) == true {
_ = responder?.perform(selectorOpenURL, with: url)
}
responder = responder?.next
}
}
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
private func dismissWithError() {
let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel) { _ in
self.dismiss(animated: true, completion: nil)
})
present(alert, animated: true, completion: nil)
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
private func getFileName(from url: URL, type: SharedMediaType) -> String {
var name = url.lastPathComponent
if name.isEmpty {
switch type {
case .image: name = UUID().uuidString + ".png"
case .video: name = UUID().uuidString + ".mp4"
case .text: name = UUID().uuidString + ".txt"
default: name = UUID().uuidString
}
}
return name
}
private func writeTempFile(_ image: UIImage, to dstURL: URL) -> Bool {
do {
if FileManager.default.fileExists(atPath: dstURL.path) {
try FileManager.default.removeItem(at: dstURL)
}
try image.pngData()?.write(to: dstURL)
return true
} catch {
return false
}
}
private func copyFile(at srcURL: URL, to dstURL: URL) -> Bool {
do {
if FileManager.default.fileExists(atPath: dstURL.path) {
try FileManager.default.removeItem(at: dstURL)
}
try FileManager.default.copyItem(at: srcURL, to: dstURL)
} catch {
return false
}
return true
}
private func getVideoInfo(from url: URL) -> (thumbnail: String?, duration: Double)? {
let asset = AVAsset(url: url)
let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded()
let thumbnailPath = getThumbnailPath(for: url)
if FileManager.default.fileExists(atPath: thumbnailPath.path) {
return (thumbnail: thumbnailPath.absoluteString, duration: duration)
}
var saved = false
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
assetImgGenerate.appliesPreferredTrackTransform = true
assetImgGenerate.maximumSize = CGSize(width: 360, height: 360)
do {
let img = try assetImgGenerate.copyCGImage(
at: CMTimeMakeWithSeconds(600, preferredTimescale: 1),
actualTime: nil
)
try UIImage(cgImage: img).pngData()?.write(to: thumbnailPath)
saved = true
} catch {
saved = false
}
return saved ? (thumbnail: thumbnailPath.absoluteString, duration: duration) : nil
}
private func getThumbnailPath(for url: URL) -> URL {
let fileName = Data(url.lastPathComponent.utf8)
.base64EncodedString()
.replacingOccurrences(of: "==", with: "")
return FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)!
.appendingPathComponent("\(fileName).jpg")
}
private func toData(data: [SharedMediaFile]) -> Data {
try! JSONEncoder().encode(data)
}
}
extension URL {
func mimeType() -> String {
if #available(iOS 14.0, *) {
if let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType {
return mimeType
}
} else {
if let uti = UTTypeCreatePreferredIdentifierForTag(
kUTTagClassFilenameExtension, pathExtension as NSString, nil
)?.takeRetainedValue(),
let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
return mimetype as String
}
}
return "application/octet-stream"
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.dev.casraf.pantry</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
import UIKit
class ShareViewController: RSIShareViewController {
// Inherits default behavior from `RSIShareViewController` (vendored in
// RSIShareViewController.swift): collect the shared payload (images,
// text, URLs), persist it to the App Group's UserDefaults, then open
// the host app via the registered URL scheme. Override
// `shouldAutoRedirect()` to return `false` if a custom share sheet UI
// is needed before posting.
}

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'i18n.dart';
@@ -16,6 +17,7 @@ import 'services/local_notifications_service.dart';
import 'services/note_service.dart';
import 'services/photo_service.dart';
import 'services/prefs_service.dart';
import 'services/share_intent_service.dart';
import 'services/theming_service.dart';
import 'views/home/home_view.dart';
import 'views/login/login_view.dart';
@@ -46,6 +48,7 @@ void main() async {
}
}
LocaleService.instance.apply();
unawaited(ShareIntentService.instance.init());
runApp(const PantryApp());
}
@@ -113,74 +116,77 @@ class PantryAppState extends State<PantryApp> {
Widget build(BuildContext context) {
final color = ThemingService.instance.effectiveColor;
final locale = LocaleService.instance.effectiveLocale;
return Directionality(
textDirection: LocaleService.instance.textDirection,
child: MaterialApp(
key: ValueKey(locale),
// debugShowCheckedModeBanner: false,
navigatorKey: rootNavigatorKey,
locale: locale,
supportedLocales: supportedLocales,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
title: m.common.appTitle,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: color,
).copyWith(primary: color),
useMaterial3: true,
popupMenuTheme: PopupMenuThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
return ChangeNotifierProvider<PrefsService>.value(
value: PrefsService.instance,
child: Directionality(
textDirection: LocaleService.instance.textDirection,
child: MaterialApp(
key: ValueKey(locale),
// debugShowCheckedModeBanner: false,
navigatorKey: rootNavigatorKey,
locale: locale,
supportedLocales: supportedLocales,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
title: m.common.appTitle,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: color,
).copyWith(primary: color),
useMaterial3: true,
popupMenuTheme: PopupMenuThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
elevation: 8,
position: PopupMenuPosition.under,
),
elevation: 8,
position: PopupMenuPosition.under,
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: color,
brightness: Brightness.dark,
).copyWith(primary: color),
useMaterial3: true,
popupMenuTheme: PopupMenuThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: color,
brightness: Brightness.dark,
).copyWith(primary: color),
useMaterial3: true,
popupMenuTheme: PopupMenuThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
elevation: 8,
position: PopupMenuPosition.under,
),
elevation: 8,
position: PopupMenuPosition.under,
),
themeMode: ThemingService.instance.themeMode,
onGenerateInitialRoutes: (initialRoute) => [
MaterialPageRoute(
builder: (_) => _isLoggedIn
? (PrefsService.instance.notificationsIntroSeen
? HomeView(onLogout: _onLogout)
: NotificationsIntroView(onDone: _onIntroDone))
: LoginView(onLoginSuccess: _onLoginSuccess),
),
],
onGenerateRoute: (settings) {
switch (settings.name) {
case '/home':
return MaterialPageRoute(
builder: (_) => HomeView(onLogout: _onLogout),
);
case '/notifications-intro':
return MaterialPageRoute(
builder: (_) => NotificationsIntroView(onDone: _onIntroDone),
);
case '/login':
default:
return MaterialPageRoute(
builder: (_) => LoginView(onLoginSuccess: _onLoginSuccess),
);
}
},
),
themeMode: ThemingService.instance.themeMode,
onGenerateInitialRoutes: (initialRoute) => [
MaterialPageRoute(
builder: (_) => _isLoggedIn
? (PrefsService.instance.notificationsIntroSeen
? HomeView(onLogout: _onLogout)
: NotificationsIntroView(onDone: _onIntroDone))
: LoginView(onLoginSuccess: _onLoginSuccess),
),
],
onGenerateRoute: (settings) {
switch (settings.name) {
case '/home':
return MaterialPageRoute(
builder: (_) => HomeView(onLogout: _onLogout),
);
case '/notifications-intro':
return MaterialPageRoute(
builder: (_) => NotificationsIntroView(onDone: _onIntroDone),
);
case '/login':
default:
return MaterialPageRoute(
builder: (_) => LoginView(onLoginSuccess: _onLoginSuccess),
);
}
},
),
);
}

View File

@@ -75,6 +75,7 @@ class Messages {
ChecklistsMessages get checklists => ChecklistsMessages(this);
NotesWallMessages get notesWall => NotesWallMessages(this);
PhotoBoardMessages get photoBoard => PhotoBoardMessages(this);
ShareMessages get share => ShareMessages(this);
RecurrenceMessages get recurrence => RecurrenceMessages(this);
}
@@ -361,6 +362,35 @@ class SettingsMessages {
/// ```
String get generalSection => """General""";
/// ```dart
/// "Interface"
/// ```
String get interfaceSection => """Interface""";
/// ```dart
/// "Tap row to complete items"
/// ```
String get tapRowToComplete => """Tap row to complete items""";
/// ```dart
/// "When off, items are only marked complete by tapping the checkbox."
/// ```
String get tapRowToCompleteBody =>
"""When off, items are only marked complete by tapping the checkbox.""";
/// ```dart
/// "Show spacing between categories in list items"
/// ```
String get categorySpacing =>
"""Show spacing between categories in list items""";
/// ```dart
/// "Only visible when sorting by category"
/// ```
String get categorySpacingBody => """Only visible when sorting by category""";
CategorySpacingNamesSettingsMessages get categorySpacingNames =>
CategorySpacingNamesSettingsMessages(this);
/// ```dart
/// "Language"
/// ```
@@ -427,6 +457,26 @@ class SettingsMessages {
"""Notification permission was denied. Enable it in system settings.""";
}
class CategorySpacingNamesSettingsMessages {
final SettingsMessages _parent;
const CategorySpacingNamesSettingsMessages(this._parent);
/// ```dart
/// "Disabled"
/// ```
String get disabled => """Disabled""";
/// ```dart
/// "Space"
/// ```
String get space => """Space""";
/// ```dart
/// "Divider"
/// ```
String get divider => """Divider""";
}
class LanguageNamesSettingsMessages {
final SettingsMessages _parent;
const LanguageNamesSettingsMessages(this._parent);
@@ -1053,9 +1103,30 @@ class PhotoBoardMessages {
/// "$count"
/// ```
String photoCount(int count) => """$count""";
AddMenuPhotoBoardMessages get addMenu => AddMenuPhotoBoardMessages(this);
SortPhotoBoardMessages get sort => SortPhotoBoardMessages(this);
}
class AddMenuPhotoBoardMessages {
final PhotoBoardMessages _parent;
const AddMenuPhotoBoardMessages(this._parent);
/// ```dart
/// "Upload photos"
/// ```
String get upload => """Upload photos""";
/// ```dart
/// "Take photo"
/// ```
String get camera => """Take photo""";
/// ```dart
/// "New folder"
/// ```
String get newFolder => """New folder""";
}
class SortPhotoBoardMessages {
final PhotoBoardMessages _parent;
const SortPhotoBoardMessages(this._parent);
@@ -1091,6 +1162,56 @@ class SortPhotoBoardMessages {
String get custom => """Custom""";
}
class ShareMessages {
final Messages _parent;
const ShareMessages(this._parent);
/// ```dart
/// "Share to Pantry"
/// ```
String get title => """Share to Pantry""";
/// ```dart
/// "Choose house"
/// ```
String get chooseHouse => """Choose house""";
/// ```dart
/// "Upload to"
/// ```
String get choosePhotoDestination => """Upload to""";
/// ```dart
/// "Photo Board"
/// ```
String get photoBoardRoot => """Photo Board""";
/// ```dart
/// "New folder"
/// ```
String get newFolder => """New folder""";
/// ```dart
/// "Folder name"
/// ```
String get newFolderName => """Folder name""";
/// ```dart
/// "Failed to create folder."
/// ```
String get failedToCreateFolder => """Failed to create folder.""";
/// ```dart
/// "Could not open the shared content."
/// ```
String get failedToOpenShare => """Could not open the shared content.""";
/// ```dart
/// "No houses available. Create a house first."
/// ```
String get noHouses => """No houses available. Create a house first.""";
}
class RecurrenceMessages {
final Messages _parent;
const RecurrenceMessages(this._parent);
@@ -1390,6 +1511,17 @@ Please complete login in your browser.""",
"""about.feedback""": """Feedback & issues""",
"""settings.title""": """App Settings""",
"""settings.generalSection""": """General""",
"""settings.interfaceSection""": """Interface""",
"""settings.tapRowToComplete""": """Tap row to complete items""",
"""settings.tapRowToCompleteBody""":
"""When off, items are only marked complete by tapping the checkbox.""",
"""settings.categorySpacing""":
"""Show spacing between categories in list items""",
"""settings.categorySpacingBody""":
"""Only visible when sorting by category""",
"""settings.categorySpacingNames.disabled""": """Disabled""",
"""settings.categorySpacingNames.space""": """Space""",
"""settings.categorySpacingNames.divider""": """Divider""",
"""settings.language""": """Language""",
"""settings.languageNames.system""": """System default""",
"""settings.languageNames.english""": """English""",
@@ -1514,12 +1646,24 @@ Please complete login in your browser.""",
"""photoBoard.folderName""": """Folder name""",
"""photoBoard.renameFolder""": """Rename folder""",
"""photoBoard.caption""": """Caption""",
"""photoBoard.addMenu.upload""": """Upload photos""",
"""photoBoard.addMenu.camera""": """Take photo""",
"""photoBoard.addMenu.newFolder""": """New folder""",
"""photoBoard.sort.foldersFirst""": """Folders first""",
"""photoBoard.sort.newestFirst""": """Newest first""",
"""photoBoard.sort.oldestFirst""": """Oldest first""",
"""photoBoard.sort.captionAZ""": """Caption AZ""",
"""photoBoard.sort.captionZA""": """Caption ZA""",
"""photoBoard.sort.custom""": """Custom""",
"""share.title""": """Share to Pantry""",
"""share.chooseHouse""": """Choose house""",
"""share.choosePhotoDestination""": """Upload to""",
"""share.photoBoardRoot""": """Photo Board""",
"""share.newFolder""": """New folder""",
"""share.newFolderName""": """Folder name""",
"""share.failedToCreateFolder""": """Failed to create folder.""",
"""share.failedToOpenShare""": """Could not open the shared content.""",
"""share.noHouses""": """No houses available. Create a house first.""",
"""recurrence.title""": """Recurrence""",
"""recurrence.presets""": """Presets""",
"""recurrence.daily""": """Daily""",

View File

@@ -59,6 +59,15 @@ about:
settings:
title: App Settings
generalSection: General
interfaceSection: Interface
tapRowToComplete: Tap row to complete items
tapRowToCompleteBody: "When off, items are only marked complete by tapping the checkbox."
categorySpacing: Show spacing between categories in list items
categorySpacingBody: Only visible when sorting by category
categorySpacingNames:
disabled: Disabled
space: Space
divider: Divider
language: Language
languageNames:
system: "System default"
@@ -200,6 +209,10 @@ photoBoard:
renameFolder: Rename folder
caption: Caption
photoCount(int count): "$count"
addMenu:
upload: Upload photos
camera: Take photo
newFolder: New folder
sort:
foldersFirst: Folders first
newestFirst: Newest first
@@ -208,6 +221,17 @@ photoBoard:
captionZA: "Caption ZA"
custom: Custom
share:
title: Share to Pantry
chooseHouse: Choose house
choosePhotoDestination: Upload to
photoBoardRoot: Photo Board
newFolder: New folder
newFolderName: Folder name
failedToCreateFolder: Failed to create folder.
failedToOpenShare: Could not open the shared content.
noHouses: No houses available. Create a house first.
recurrence:
title: Recurrence
presets: Presets

View File

@@ -76,6 +76,7 @@ class MessagesDe extends Messages {
ChecklistsMessagesDe get checklists => ChecklistsMessagesDe(this);
NotesWallMessagesDe get notesWall => NotesWallMessagesDe(this);
PhotoBoardMessagesDe get photoBoard => PhotoBoardMessagesDe(this);
ShareMessagesDe get share => ShareMessagesDe(this);
RecurrenceMessagesDe get recurrence => RecurrenceMessagesDe(this);
}
@@ -363,6 +364,36 @@ class SettingsMessagesDe extends SettingsMessages {
/// ```
String get generalSection => """Allgemein""";
/// ```dart
/// "Oberfläche"
/// ```
String get interfaceSection => """Oberfläche""";
/// ```dart
/// "Eintrag durch Tippen der Zeile abhaken"
/// ```
String get tapRowToComplete => """Eintrag durch Tippen der Zeile abhaken""";
/// ```dart
/// "Wenn aus, werden Einträge nur durch Tippen auf das Kontrollkästchen abgehakt."
/// ```
String get tapRowToCompleteBody =>
"""Wenn aus, werden Einträge nur durch Tippen auf das Kontrollkästchen abgehakt.""";
/// ```dart
/// "Abstand zwischen Kategorien in Listeneinträgen anzeigen"
/// ```
String get categorySpacing =>
"""Abstand zwischen Kategorien in Listeneinträgen anzeigen""";
/// ```dart
/// "Nur sichtbar bei Sortierung nach Kategorie"
/// ```
String get categorySpacingBody =>
"""Nur sichtbar bei Sortierung nach Kategorie""";
CategorySpacingNamesSettingsMessagesDe get categorySpacingNames =>
CategorySpacingNamesSettingsMessagesDe(this);
/// ```dart
/// "Sprache"
/// ```
@@ -430,6 +461,27 @@ class SettingsMessagesDe extends SettingsMessages {
"""Benachrichtigungsberechtigung wurde verweigert. Aktiviere sie in den Systemeinstellungen.""";
}
class CategorySpacingNamesSettingsMessagesDe
extends CategorySpacingNamesSettingsMessages {
final SettingsMessagesDe _parent;
const CategorySpacingNamesSettingsMessagesDe(this._parent) : super(_parent);
/// ```dart
/// "Deaktiviert"
/// ```
String get disabled => """Deaktiviert""";
/// ```dart
/// "Abstand"
/// ```
String get space => """Abstand""";
/// ```dart
/// "Trennlinie"
/// ```
String get divider => """Trennlinie""";
}
class LanguageNamesSettingsMessagesDe extends LanguageNamesSettingsMessages {
final SettingsMessagesDe _parent;
const LanguageNamesSettingsMessagesDe(this._parent) : super(_parent);
@@ -1062,9 +1114,30 @@ class PhotoBoardMessagesDe extends PhotoBoardMessages {
/// "$count"
/// ```
String photoCount(int count) => """$count""";
AddMenuPhotoBoardMessagesDe get addMenu => AddMenuPhotoBoardMessagesDe(this);
SortPhotoBoardMessagesDe get sort => SortPhotoBoardMessagesDe(this);
}
class AddMenuPhotoBoardMessagesDe extends AddMenuPhotoBoardMessages {
final PhotoBoardMessagesDe _parent;
const AddMenuPhotoBoardMessagesDe(this._parent) : super(_parent);
/// ```dart
/// "Fotos hochladen"
/// ```
String get upload => """Fotos hochladen""";
/// ```dart
/// "Foto aufnehmen"
/// ```
String get camera => """Foto aufnehmen""";
/// ```dart
/// "Neuer Ordner"
/// ```
String get newFolder => """Neuer Ordner""";
}
class SortPhotoBoardMessagesDe extends SortPhotoBoardMessages {
final PhotoBoardMessagesDe _parent;
const SortPhotoBoardMessagesDe(this._parent) : super(_parent);
@@ -1100,6 +1173,58 @@ class SortPhotoBoardMessagesDe extends SortPhotoBoardMessages {
String get custom => """Benutzerdefiniert""";
}
class ShareMessagesDe extends ShareMessages {
final MessagesDe _parent;
const ShareMessagesDe(this._parent) : super(_parent);
/// ```dart
/// "An Pantry senden"
/// ```
String get title => """An Pantry senden""";
/// ```dart
/// "Haus auswählen"
/// ```
String get chooseHouse => """Haus auswählen""";
/// ```dart
/// "Hochladen nach"
/// ```
String get choosePhotoDestination => """Hochladen nach""";
/// ```dart
/// "Fotowand"
/// ```
String get photoBoardRoot => """Fotowand""";
/// ```dart
/// "Neuer Ordner"
/// ```
String get newFolder => """Neuer Ordner""";
/// ```dart
/// "Ordnername"
/// ```
String get newFolderName => """Ordnername""";
/// ```dart
/// "Ordner konnte nicht erstellt werden."
/// ```
String get failedToCreateFolder => """Ordner konnte nicht erstellt werden.""";
/// ```dart
/// "Der geteilte Inhalt konnte nicht geöffnet werden."
/// ```
String get failedToOpenShare =>
"""Der geteilte Inhalt konnte nicht geöffnet werden.""";
/// ```dart
/// "Keine Häuser verfügbar. Erstelle zuerst ein Haus."
/// ```
String get noHouses =>
"""Keine Häuser verfügbar. Erstelle zuerst ein Haus.""";
}
class RecurrenceMessagesDe extends RecurrenceMessages {
final MessagesDe _parent;
const RecurrenceMessagesDe(this._parent) : super(_parent);
@@ -1403,6 +1528,17 @@ Bitte melde dich in deinem Browser an.""",
"""about.feedback""": """Feedback & Probleme""",
"""settings.title""": """App-Einstellungen""",
"""settings.generalSection""": """Allgemein""",
"""settings.interfaceSection""": """Oberfläche""",
"""settings.tapRowToComplete""": """Eintrag durch Tippen der Zeile abhaken""",
"""settings.tapRowToCompleteBody""":
"""Wenn aus, werden Einträge nur durch Tippen auf das Kontrollkästchen abgehakt.""",
"""settings.categorySpacing""":
"""Abstand zwischen Kategorien in Listeneinträgen anzeigen""",
"""settings.categorySpacingBody""":
"""Nur sichtbar bei Sortierung nach Kategorie""",
"""settings.categorySpacingNames.disabled""": """Deaktiviert""",
"""settings.categorySpacingNames.space""": """Abstand""",
"""settings.categorySpacingNames.divider""": """Trennlinie""",
"""settings.language""": """Sprache""",
"""settings.languageNames.system""": """Systemstandard""",
"""settings.languageNames.english""": """English""",
@@ -1534,12 +1670,25 @@ Bitte melde dich in deinem Browser an.""",
"""photoBoard.folderName""": """Ordnername""",
"""photoBoard.renameFolder""": """Ordner umbenennen""",
"""photoBoard.caption""": """Beschriftung""",
"""photoBoard.addMenu.upload""": """Fotos hochladen""",
"""photoBoard.addMenu.camera""": """Foto aufnehmen""",
"""photoBoard.addMenu.newFolder""": """Neuer Ordner""",
"""photoBoard.sort.foldersFirst""": """Ordner zuerst""",
"""photoBoard.sort.newestFirst""": """Neueste zuerst""",
"""photoBoard.sort.oldestFirst""": """Älteste zuerst""",
"""photoBoard.sort.captionAZ""": """Beschriftung AZ""",
"""photoBoard.sort.captionZA""": """Beschriftung ZA""",
"""photoBoard.sort.custom""": """Benutzerdefiniert""",
"""share.title""": """An Pantry senden""",
"""share.chooseHouse""": """Haus auswählen""",
"""share.choosePhotoDestination""": """Hochladen nach""",
"""share.photoBoardRoot""": """Fotowand""",
"""share.newFolder""": """Neuer Ordner""",
"""share.newFolderName""": """Ordnername""",
"""share.failedToCreateFolder""": """Ordner konnte nicht erstellt werden.""",
"""share.failedToOpenShare""":
"""Der geteilte Inhalt konnte nicht geöffnet werden.""",
"""share.noHouses""": """Keine Häuser verfügbar. Erstelle zuerst ein Haus.""",
"""recurrence.title""": """Wiederholung""",
"""recurrence.presets""": """Voreinstellungen""",
"""recurrence.daily""": """Täglich""",

View File

@@ -59,6 +59,15 @@ about:
settings:
title: App-Einstellungen
generalSection: Allgemein
interfaceSection: Oberfläche
tapRowToComplete: Eintrag durch Tippen der Zeile abhaken
tapRowToCompleteBody: "Wenn aus, werden Einträge nur durch Tippen auf das Kontrollkästchen abgehakt."
categorySpacing: Abstand zwischen Kategorien in Listeneinträgen anzeigen
categorySpacingBody: Nur sichtbar bei Sortierung nach Kategorie
categorySpacingNames:
disabled: Deaktiviert
space: Abstand
divider: Trennlinie
language: Sprache
languageNames:
system: Systemstandard
@@ -200,6 +209,10 @@ photoBoard:
renameFolder: Ordner umbenennen
caption: Beschriftung
photoCount(int count): "$count"
addMenu:
upload: Fotos hochladen
camera: Foto aufnehmen
newFolder: Neuer Ordner
sort:
foldersFirst: Ordner zuerst
newestFirst: Neueste zuerst
@@ -208,6 +221,17 @@ photoBoard:
captionZA: "Beschriftung ZA"
custom: Benutzerdefiniert
share:
title: An Pantry senden
chooseHouse: Haus auswählen
choosePhotoDestination: Hochladen nach
photoBoardRoot: Fotowand
newFolder: Neuer Ordner
newFolderName: Ordnername
failedToCreateFolder: Ordner konnte nicht erstellt werden.
failedToOpenShare: Der geteilte Inhalt konnte nicht geöffnet werden.
noHouses: Keine Häuser verfügbar. Erstelle zuerst ein Haus.
recurrence:
title: Wiederholung
presets: Voreinstellungen

View File

@@ -76,6 +76,7 @@ class MessagesEs extends Messages {
ChecklistsMessagesEs get checklists => ChecklistsMessagesEs(this);
NotesWallMessagesEs get notesWall => NotesWallMessagesEs(this);
PhotoBoardMessagesEs get photoBoard => PhotoBoardMessagesEs(this);
ShareMessagesEs get share => ShareMessagesEs(this);
RecurrenceMessagesEs get recurrence => RecurrenceMessagesEs(this);
}
@@ -363,6 +364,35 @@ class SettingsMessagesEs extends SettingsMessages {
/// ```
String get generalSection => """General""";
/// ```dart
/// "Interfaz"
/// ```
String get interfaceSection => """Interfaz""";
/// ```dart
/// "Tocar la fila para completar elementos"
/// ```
String get tapRowToComplete => """Tocar la fila para completar elementos""";
/// ```dart
/// "Cuando está desactivado, los elementos solo se marcan como completados al tocar la casilla."
/// ```
String get tapRowToCompleteBody =>
"""Cuando está desactivado, los elementos solo se marcan como completados al tocar la casilla.""";
/// ```dart
/// "Mostrar espacio entre categorías en los elementos de la lista"
/// ```
String get categorySpacing =>
"""Mostrar espacio entre categorías en los elementos de la lista""";
/// ```dart
/// "Solo visible al ordenar por categoría"
/// ```
String get categorySpacingBody => """Solo visible al ordenar por categoría""";
CategorySpacingNamesSettingsMessagesEs get categorySpacingNames =>
CategorySpacingNamesSettingsMessagesEs(this);
/// ```dart
/// "Idioma"
/// ```
@@ -430,6 +460,27 @@ class SettingsMessagesEs extends SettingsMessages {
"""El permiso de notificaciones fue denegado. Actívalo en los ajustes del sistema.""";
}
class CategorySpacingNamesSettingsMessagesEs
extends CategorySpacingNamesSettingsMessages {
final SettingsMessagesEs _parent;
const CategorySpacingNamesSettingsMessagesEs(this._parent) : super(_parent);
/// ```dart
/// "Desactivado"
/// ```
String get disabled => """Desactivado""";
/// ```dart
/// "Espacio"
/// ```
String get space => """Espacio""";
/// ```dart
/// "Separador"
/// ```
String get divider => """Separador""";
}
class LanguageNamesSettingsMessagesEs extends LanguageNamesSettingsMessages {
final SettingsMessagesEs _parent;
const LanguageNamesSettingsMessagesEs(this._parent) : super(_parent);
@@ -1059,9 +1110,30 @@ class PhotoBoardMessagesEs extends PhotoBoardMessages {
/// "$count"
/// ```
String photoCount(int count) => """$count""";
AddMenuPhotoBoardMessagesEs get addMenu => AddMenuPhotoBoardMessagesEs(this);
SortPhotoBoardMessagesEs get sort => SortPhotoBoardMessagesEs(this);
}
class AddMenuPhotoBoardMessagesEs extends AddMenuPhotoBoardMessages {
final PhotoBoardMessagesEs _parent;
const AddMenuPhotoBoardMessagesEs(this._parent) : super(_parent);
/// ```dart
/// "Subir fotos"
/// ```
String get upload => """Subir fotos""";
/// ```dart
/// "Tomar foto"
/// ```
String get camera => """Tomar foto""";
/// ```dart
/// "Nueva carpeta"
/// ```
String get newFolder => """Nueva carpeta""";
}
class SortPhotoBoardMessagesEs extends SortPhotoBoardMessages {
final PhotoBoardMessagesEs _parent;
const SortPhotoBoardMessagesEs(this._parent) : super(_parent);
@@ -1097,6 +1169,57 @@ class SortPhotoBoardMessagesEs extends SortPhotoBoardMessages {
String get custom => """Personalizado""";
}
class ShareMessagesEs extends ShareMessages {
final MessagesEs _parent;
const ShareMessagesEs(this._parent) : super(_parent);
/// ```dart
/// "Compartir con Pantry"
/// ```
String get title => """Compartir con Pantry""";
/// ```dart
/// "Elegir casa"
/// ```
String get chooseHouse => """Elegir casa""";
/// ```dart
/// "Subir a"
/// ```
String get choosePhotoDestination => """Subir a""";
/// ```dart
/// "Tablón de fotos"
/// ```
String get photoBoardRoot => """Tablón de fotos""";
/// ```dart
/// "Nueva carpeta"
/// ```
String get newFolder => """Nueva carpeta""";
/// ```dart
/// "Nombre de la carpeta"
/// ```
String get newFolderName => """Nombre de la carpeta""";
/// ```dart
/// "No se pudo crear la carpeta."
/// ```
String get failedToCreateFolder => """No se pudo crear la carpeta.""";
/// ```dart
/// "No se pudo abrir el contenido compartido."
/// ```
String get failedToOpenShare =>
"""No se pudo abrir el contenido compartido.""";
/// ```dart
/// "No hay casas disponibles. Crea una casa primero."
/// ```
String get noHouses => """No hay casas disponibles. Crea una casa primero.""";
}
class RecurrenceMessagesEs extends RecurrenceMessages {
final MessagesEs _parent;
const RecurrenceMessagesEs(this._parent) : super(_parent);
@@ -1398,6 +1521,17 @@ Por favor, completa el inicio de sesión en tu navegador.""",
"""about.feedback""": """Comentarios y problemas""",
"""settings.title""": """Ajustes de la app""",
"""settings.generalSection""": """General""",
"""settings.interfaceSection""": """Interfaz""",
"""settings.tapRowToComplete""": """Tocar la fila para completar elementos""",
"""settings.tapRowToCompleteBody""":
"""Cuando está desactivado, los elementos solo se marcan como completados al tocar la casilla.""",
"""settings.categorySpacing""":
"""Mostrar espacio entre categorías en los elementos de la lista""",
"""settings.categorySpacingBody""":
"""Solo visible al ordenar por categoría""",
"""settings.categorySpacingNames.disabled""": """Desactivado""",
"""settings.categorySpacingNames.space""": """Espacio""",
"""settings.categorySpacingNames.divider""": """Separador""",
"""settings.language""": """Idioma""",
"""settings.languageNames.system""": """Predeterminado del sistema""",
"""settings.languageNames.english""": """English""",
@@ -1526,12 +1660,25 @@ Por favor, completa el inicio de sesión en tu navegador.""",
"""photoBoard.folderName""": """Nombre de la carpeta""",
"""photoBoard.renameFolder""": """Renombrar carpeta""",
"""photoBoard.caption""": """Descripción""",
"""photoBoard.addMenu.upload""": """Subir fotos""",
"""photoBoard.addMenu.camera""": """Tomar foto""",
"""photoBoard.addMenu.newFolder""": """Nueva carpeta""",
"""photoBoard.sort.foldersFirst""": """Carpetas primero""",
"""photoBoard.sort.newestFirst""": """Más recientes""",
"""photoBoard.sort.oldestFirst""": """Más antiguos""",
"""photoBoard.sort.captionAZ""": """Descripción AZ""",
"""photoBoard.sort.captionZA""": """Descripción ZA""",
"""photoBoard.sort.custom""": """Personalizado""",
"""share.title""": """Compartir con Pantry""",
"""share.chooseHouse""": """Elegir casa""",
"""share.choosePhotoDestination""": """Subir a""",
"""share.photoBoardRoot""": """Tablón de fotos""",
"""share.newFolder""": """Nueva carpeta""",
"""share.newFolderName""": """Nombre de la carpeta""",
"""share.failedToCreateFolder""": """No se pudo crear la carpeta.""",
"""share.failedToOpenShare""":
"""No se pudo abrir el contenido compartido.""",
"""share.noHouses""": """No hay casas disponibles. Crea una casa primero.""",
"""recurrence.title""": """Recurrencia""",
"""recurrence.presets""": """Preajustes""",
"""recurrence.daily""": """Diario""",

View File

@@ -59,6 +59,15 @@ about:
settings:
title: Ajustes de la app
generalSection: General
interfaceSection: Interfaz
tapRowToComplete: Tocar la fila para completar elementos
tapRowToCompleteBody: "Cuando está desactivado, los elementos solo se marcan como completados al tocar la casilla."
categorySpacing: Mostrar espacio entre categorías en los elementos de la lista
categorySpacingBody: Solo visible al ordenar por categoría
categorySpacingNames:
disabled: Desactivado
space: Espacio
divider: Separador
language: Idioma
languageNames:
system: Predeterminado del sistema
@@ -200,6 +209,10 @@ photoBoard:
renameFolder: Renombrar carpeta
caption: "Descripción"
photoCount(int count): "$count"
addMenu:
upload: Subir fotos
camera: Tomar foto
newFolder: Nueva carpeta
sort:
foldersFirst: Carpetas primero
newestFirst: "Más recientes"
@@ -208,6 +221,17 @@ photoBoard:
captionZA: "Descripción ZA"
custom: Personalizado
share:
title: Compartir con Pantry
chooseHouse: Elegir casa
choosePhotoDestination: Subir a
photoBoardRoot: Tablón de fotos
newFolder: Nueva carpeta
newFolderName: Nombre de la carpeta
failedToCreateFolder: No se pudo crear la carpeta.
failedToOpenShare: No se pudo abrir el contenido compartido.
noHouses: No hay casas disponibles. Crea una casa primero.
recurrence:
title: Recurrencia
presets: Preajustes

View File

@@ -76,6 +76,7 @@ class MessagesFr extends Messages {
ChecklistsMessagesFr get checklists => ChecklistsMessagesFr(this);
NotesWallMessagesFr get notesWall => NotesWallMessagesFr(this);
PhotoBoardMessagesFr get photoBoard => PhotoBoardMessagesFr(this);
ShareMessagesFr get share => ShareMessagesFr(this);
RecurrenceMessagesFr get recurrence => RecurrenceMessagesFr(this);
}
@@ -363,6 +364,37 @@ class SettingsMessagesFr extends SettingsMessages {
/// ```
String get generalSection => """Général""";
/// ```dart
/// "Interface"
/// ```
String get interfaceSection => """Interface""";
/// ```dart
/// "Toucher la ligne pour cocher les éléments"
/// ```
String get tapRowToComplete =>
"""Toucher la ligne pour cocher les éléments""";
/// ```dart
/// "Quand désactivé, les éléments ne sont cochés qu'en touchant la case."
/// ```
String get tapRowToCompleteBody =>
"""Quand désactivé, les éléments ne sont cochés qu'en touchant la case.""";
/// ```dart
/// "Afficher un espacement entre les catégories dans les éléments de la liste"
/// ```
String get categorySpacing =>
"""Afficher un espacement entre les catégories dans les éléments de la liste""";
/// ```dart
/// "Visible uniquement lors du tri par catégorie"
/// ```
String get categorySpacingBody =>
"""Visible uniquement lors du tri par catégorie""";
CategorySpacingNamesSettingsMessagesFr get categorySpacingNames =>
CategorySpacingNamesSettingsMessagesFr(this);
/// ```dart
/// "Langue"
/// ```
@@ -430,6 +462,27 @@ class SettingsMessagesFr extends SettingsMessages {
"""La permission de notification a été refusée. Activez-la dans les réglages système.""";
}
class CategorySpacingNamesSettingsMessagesFr
extends CategorySpacingNamesSettingsMessages {
final SettingsMessagesFr _parent;
const CategorySpacingNamesSettingsMessagesFr(this._parent) : super(_parent);
/// ```dart
/// "Désactivé"
/// ```
String get disabled => """Désactivé""";
/// ```dart
/// "Espace"
/// ```
String get space => """Espace""";
/// ```dart
/// "Séparateur"
/// ```
String get divider => """Séparateur""";
}
class LanguageNamesSettingsMessagesFr extends LanguageNamesSettingsMessages {
final SettingsMessagesFr _parent;
const LanguageNamesSettingsMessagesFr(this._parent) : super(_parent);
@@ -1060,9 +1113,30 @@ class PhotoBoardMessagesFr extends PhotoBoardMessages {
/// "$count"
/// ```
String photoCount(int count) => """$count""";
AddMenuPhotoBoardMessagesFr get addMenu => AddMenuPhotoBoardMessagesFr(this);
SortPhotoBoardMessagesFr get sort => SortPhotoBoardMessagesFr(this);
}
class AddMenuPhotoBoardMessagesFr extends AddMenuPhotoBoardMessages {
final PhotoBoardMessagesFr _parent;
const AddMenuPhotoBoardMessagesFr(this._parent) : super(_parent);
/// ```dart
/// "Téléverser des photos"
/// ```
String get upload => """Téléverser des photos""";
/// ```dart
/// "Prendre une photo"
/// ```
String get camera => """Prendre une photo""";
/// ```dart
/// "Nouveau dossier"
/// ```
String get newFolder => """Nouveau dossier""";
}
class SortPhotoBoardMessagesFr extends SortPhotoBoardMessages {
final PhotoBoardMessagesFr _parent;
const SortPhotoBoardMessagesFr(this._parent) : super(_parent);
@@ -1098,6 +1172,57 @@ class SortPhotoBoardMessagesFr extends SortPhotoBoardMessages {
String get custom => """Personnalisé""";
}
class ShareMessagesFr extends ShareMessages {
final MessagesFr _parent;
const ShareMessagesFr(this._parent) : super(_parent);
/// ```dart
/// "Partager vers Pantry"
/// ```
String get title => """Partager vers Pantry""";
/// ```dart
/// "Choisir une maison"
/// ```
String get chooseHouse => """Choisir une maison""";
/// ```dart
/// "Téléverser vers"
/// ```
String get choosePhotoDestination => """Téléverser vers""";
/// ```dart
/// "Tableau photos"
/// ```
String get photoBoardRoot => """Tableau photos""";
/// ```dart
/// "Nouveau dossier"
/// ```
String get newFolder => """Nouveau dossier""";
/// ```dart
/// "Nom du dossier"
/// ```
String get newFolderName => """Nom du dossier""";
/// ```dart
/// "Impossible de créer le dossier."
/// ```
String get failedToCreateFolder => """Impossible de créer le dossier.""";
/// ```dart
/// "Impossible d'ouvrir le contenu partagé."
/// ```
String get failedToOpenShare => """Impossible d'ouvrir le contenu partagé.""";
/// ```dart
/// "Aucune maison disponible. Créez d'abord une maison."
/// ```
String get noHouses =>
"""Aucune maison disponible. Créez d'abord une maison.""";
}
class RecurrenceMessagesFr extends RecurrenceMessages {
final MessagesFr _parent;
const RecurrenceMessagesFr(this._parent) : super(_parent);
@@ -1401,6 +1526,18 @@ Veuillez terminer la connexion dans votre navigateur.""",
"""about.feedback""": """Commentaires & problèmes""",
"""settings.title""": """Réglages de l'app""",
"""settings.generalSection""": """Général""",
"""settings.interfaceSection""": """Interface""",
"""settings.tapRowToComplete""":
"""Toucher la ligne pour cocher les éléments""",
"""settings.tapRowToCompleteBody""":
"""Quand désactivé, les éléments ne sont cochés qu'en touchant la case.""",
"""settings.categorySpacing""":
"""Afficher un espacement entre les catégories dans les éléments de la liste""",
"""settings.categorySpacingBody""":
"""Visible uniquement lors du tri par catégorie""",
"""settings.categorySpacingNames.disabled""": """Désactivé""",
"""settings.categorySpacingNames.space""": """Espace""",
"""settings.categorySpacingNames.divider""": """Séparateur""",
"""settings.language""": """Langue""",
"""settings.languageNames.system""": """Par défaut du système""",
"""settings.languageNames.english""": """English""",
@@ -1531,12 +1668,25 @@ Veuillez terminer la connexion dans votre navigateur.""",
"""photoBoard.folderName""": """Nom du dossier""",
"""photoBoard.renameFolder""": """Renommer le dossier""",
"""photoBoard.caption""": """Légende""",
"""photoBoard.addMenu.upload""": """Téléverser des photos""",
"""photoBoard.addMenu.camera""": """Prendre une photo""",
"""photoBoard.addMenu.newFolder""": """Nouveau dossier""",
"""photoBoard.sort.foldersFirst""": """Dossiers en premier""",
"""photoBoard.sort.newestFirst""": """Plus récents""",
"""photoBoard.sort.oldestFirst""": """Plus anciens""",
"""photoBoard.sort.captionAZ""": """Légende AZ""",
"""photoBoard.sort.captionZA""": """Légende ZA""",
"""photoBoard.sort.custom""": """Personnalisé""",
"""share.title""": """Partager vers Pantry""",
"""share.chooseHouse""": """Choisir une maison""",
"""share.choosePhotoDestination""": """Téléverser vers""",
"""share.photoBoardRoot""": """Tableau photos""",
"""share.newFolder""": """Nouveau dossier""",
"""share.newFolderName""": """Nom du dossier""",
"""share.failedToCreateFolder""": """Impossible de créer le dossier.""",
"""share.failedToOpenShare""": """Impossible d'ouvrir le contenu partagé.""",
"""share.noHouses""":
"""Aucune maison disponible. Créez d'abord une maison.""",
"""recurrence.title""": """Récurrence""",
"""recurrence.presets""": """Préréglages""",
"""recurrence.daily""": """Quotidien""",

View File

@@ -59,6 +59,15 @@ about:
settings:
title: "Réglages de l'app"
generalSection: "Général"
interfaceSection: Interface
tapRowToComplete: Toucher la ligne pour cocher les éléments
tapRowToCompleteBody: "Quand désactivé, les éléments ne sont cochés qu'en touchant la case."
categorySpacing: Afficher un espacement entre les catégories dans les éléments de la liste
categorySpacingBody: Visible uniquement lors du tri par catégorie
categorySpacingNames:
disabled: "Désactivé"
space: Espace
divider: "Séparateur"
language: Langue
languageNames:
system: "Par défaut du système"
@@ -200,6 +209,10 @@ photoBoard:
renameFolder: Renommer le dossier
caption: "Légende"
photoCount(int count): "$count"
addMenu:
upload: Téléverser des photos
camera: Prendre une photo
newFolder: Nouveau dossier
sort:
foldersFirst: Dossiers en premier
newestFirst: "Plus récents"
@@ -208,6 +221,17 @@ photoBoard:
captionZA: "Légende ZA"
custom: "Personnalisé"
share:
title: Partager vers Pantry
chooseHouse: Choisir une maison
choosePhotoDestination: Téléverser vers
photoBoardRoot: Tableau photos
newFolder: Nouveau dossier
newFolderName: Nom du dossier
failedToCreateFolder: "Impossible de créer le dossier."
failedToOpenShare: "Impossible d'ouvrir le contenu partagé."
noHouses: "Aucune maison disponible. Créez d'abord une maison."
recurrence:
title: "Récurrence"
presets: "Préréglages"

View File

@@ -76,6 +76,7 @@ class MessagesHe extends Messages {
ChecklistsMessagesHe get checklists => ChecklistsMessagesHe(this);
NotesWallMessagesHe get notesWall => NotesWallMessagesHe(this);
PhotoBoardMessagesHe get photoBoard => PhotoBoardMessagesHe(this);
ShareMessagesHe get share => ShareMessagesHe(this);
RecurrenceMessagesHe get recurrence => RecurrenceMessagesHe(this);
}
@@ -361,6 +362,34 @@ class SettingsMessagesHe extends SettingsMessages {
/// ```
String get generalSection => """כללי""";
/// ```dart
/// "ממשק"
/// ```
String get interfaceSection => """ממשק""";
/// ```dart
/// "השלם פריטים בלחיצה על השורה"
/// ```
String get tapRowToComplete => """השלם פריטים בלחיצה על השורה""";
/// ```dart
/// "כאשר כבוי, פריטים מסומנים כהושלמו רק בלחיצה על תיבת הסימון."
/// ```
String get tapRowToCompleteBody =>
"""כאשר כבוי, פריטים מסומנים כהושלמו רק בלחיצה על תיבת הסימון.""";
/// ```dart
/// "הצג רווח בין קטגוריות בפריטי הרשימה"
/// ```
String get categorySpacing => """הצג רווח בין קטגוריות בפריטי הרשימה""";
/// ```dart
/// "מוצג רק בעת מיון לפי קטגוריה"
/// ```
String get categorySpacingBody => """מוצג רק בעת מיון לפי קטגוריה""";
CategorySpacingNamesSettingsMessagesHe get categorySpacingNames =>
CategorySpacingNamesSettingsMessagesHe(this);
/// ```dart
/// "שפה"
/// ```
@@ -428,6 +457,27 @@ class SettingsMessagesHe extends SettingsMessages {
"""הרשאת ההתראות נדחתה. הפעל אותה בהגדרות המערכת.""";
}
class CategorySpacingNamesSettingsMessagesHe
extends CategorySpacingNamesSettingsMessages {
final SettingsMessagesHe _parent;
const CategorySpacingNamesSettingsMessagesHe(this._parent) : super(_parent);
/// ```dart
/// "מושבת"
/// ```
String get disabled => """מושבת""";
/// ```dart
/// "רווח"
/// ```
String get space => """רווח""";
/// ```dart
/// "קו מפריד"
/// ```
String get divider => """קו מפריד""";
}
class LanguageNamesSettingsMessagesHe extends LanguageNamesSettingsMessages {
final SettingsMessagesHe _parent;
const LanguageNamesSettingsMessagesHe(this._parent) : super(_parent);
@@ -1055,9 +1105,30 @@ class PhotoBoardMessagesHe extends PhotoBoardMessages {
/// "$count"
/// ```
String photoCount(int count) => """$count""";
AddMenuPhotoBoardMessagesHe get addMenu => AddMenuPhotoBoardMessagesHe(this);
SortPhotoBoardMessagesHe get sort => SortPhotoBoardMessagesHe(this);
}
class AddMenuPhotoBoardMessagesHe extends AddMenuPhotoBoardMessages {
final PhotoBoardMessagesHe _parent;
const AddMenuPhotoBoardMessagesHe(this._parent) : super(_parent);
/// ```dart
/// "העלאת תמונות"
/// ```
String get upload => """העלאת תמונות""";
/// ```dart
/// "צילום תמונה"
/// ```
String get camera => """צילום תמונה""";
/// ```dart
/// "תיקייה חדשה"
/// ```
String get newFolder => """תיקייה חדשה""";
}
class SortPhotoBoardMessagesHe extends SortPhotoBoardMessages {
final PhotoBoardMessagesHe _parent;
const SortPhotoBoardMessagesHe(this._parent) : super(_parent);
@@ -1093,6 +1164,56 @@ class SortPhotoBoardMessagesHe extends SortPhotoBoardMessages {
String get custom => """מותאם אישית""";
}
class ShareMessagesHe extends ShareMessages {
final MessagesHe _parent;
const ShareMessagesHe(this._parent) : super(_parent);
/// ```dart
/// "שיתוף ל-Pantry"
/// ```
String get title => """שיתוף ל-Pantry""";
/// ```dart
/// "בחר בית"
/// ```
String get chooseHouse => """בחר בית""";
/// ```dart
/// "העלה אל"
/// ```
String get choosePhotoDestination => """העלה אל""";
/// ```dart
/// "לוח התמונות"
/// ```
String get photoBoardRoot => """לוח התמונות""";
/// ```dart
/// "תיקייה חדשה"
/// ```
String get newFolder => """תיקייה חדשה""";
/// ```dart
/// "שם התיקייה"
/// ```
String get newFolderName => """שם התיקייה""";
/// ```dart
/// "יצירת התיקייה נכשלה."
/// ```
String get failedToCreateFolder => """יצירת התיקייה נכשלה.""";
/// ```dart
/// "לא ניתן לפתוח את התוכן ששותף."
/// ```
String get failedToOpenShare => """לא ניתן לפתוח את התוכן ששותף.""";
/// ```dart
/// "אין בתים זמינים. צור תחילה בית."
/// ```
String get noHouses => """אין בתים זמינים. צור תחילה בית.""";
}
class RecurrenceMessagesHe extends RecurrenceMessages {
final MessagesHe _parent;
const RecurrenceMessagesHe(this._parent) : super(_parent);
@@ -1391,6 +1512,15 @@ Map<String, String> get messagesHeMap => {
"""about.feedback""": """משוב ובעיות""",
"""settings.title""": """הגדרות האפליקציה""",
"""settings.generalSection""": """כללי""",
"""settings.interfaceSection""": """ממשק""",
"""settings.tapRowToComplete""": """השלם פריטים בלחיצה על השורה""",
"""settings.tapRowToCompleteBody""":
"""כאשר כבוי, פריטים מסומנים כהושלמו רק בלחיצה על תיבת הסימון.""",
"""settings.categorySpacing""": """הצג רווח בין קטגוריות בפריטי הרשימה""",
"""settings.categorySpacingBody""": """מוצג רק בעת מיון לפי קטגוריה""",
"""settings.categorySpacingNames.disabled""": """מושבת""",
"""settings.categorySpacingNames.space""": """רווח""",
"""settings.categorySpacingNames.divider""": """קו מפריד""",
"""settings.language""": """שפה""",
"""settings.languageNames.system""": """ברירת מחדל (שפת המערכת)""",
"""settings.languageNames.english""": """English""",
@@ -1513,12 +1643,24 @@ Map<String, String> get messagesHeMap => {
"""photoBoard.folderName""": """שם התיקייה""",
"""photoBoard.renameFolder""": """שנה שם תיקייה""",
"""photoBoard.caption""": """כיתוב""",
"""photoBoard.addMenu.upload""": """העלאת תמונות""",
"""photoBoard.addMenu.camera""": """צילום תמונה""",
"""photoBoard.addMenu.newFolder""": """תיקייה חדשה""",
"""photoBoard.sort.foldersFirst""": """תיקיות קודם""",
"""photoBoard.sort.newestFirst""": """החדש ביותר""",
"""photoBoard.sort.oldestFirst""": """הישן ביותר""",
"""photoBoard.sort.captionAZ""": """כיתוב א–ת""",
"""photoBoard.sort.captionZA""": """כיתוב ת–א""",
"""photoBoard.sort.custom""": """מותאם אישית""",
"""share.title""": """שיתוף ל-Pantry""",
"""share.chooseHouse""": """בחר בית""",
"""share.choosePhotoDestination""": """העלה אל""",
"""share.photoBoardRoot""": """לוח התמונות""",
"""share.newFolder""": """תיקייה חדשה""",
"""share.newFolderName""": """שם התיקייה""",
"""share.failedToCreateFolder""": """יצירת התיקייה נכשלה.""",
"""share.failedToOpenShare""": """לא ניתן לפתוח את התוכן ששותף.""",
"""share.noHouses""": """אין בתים זמינים. צור תחילה בית.""",
"""recurrence.title""": """חזרה""",
"""recurrence.presets""": """הגדרות מוכנות""",
"""recurrence.daily""": """יומי""",

View File

@@ -59,6 +59,15 @@ about:
settings:
title: הגדרות האפליקציה
generalSection: כללי
interfaceSection: ממשק
tapRowToComplete: השלם פריטים בלחיצה על השורה
tapRowToCompleteBody: כאשר כבוי, פריטים מסומנים כהושלמו רק בלחיצה על תיבת הסימון.
categorySpacing: הצג רווח בין קטגוריות בפריטי הרשימה
categorySpacingBody: מוצג רק בעת מיון לפי קטגוריה
categorySpacingNames:
disabled: מושבת
space: רווח
divider: קו מפריד
language: שפה
languageNames:
system: ברירת מחדל (שפת המערכת)
@@ -200,6 +209,10 @@ photoBoard:
renameFolder: שנה שם תיקייה
caption: כיתוב
photoCount(int count): "$count"
addMenu:
upload: העלאת תמונות
camera: צילום תמונה
newFolder: תיקייה חדשה
sort:
foldersFirst: תיקיות קודם
newestFirst: החדש ביותר
@@ -208,6 +221,17 @@ photoBoard:
captionZA: "כיתוב ת–א"
custom: מותאם אישית
share:
title: שיתוף ל-Pantry
chooseHouse: בחר בית
choosePhotoDestination: העלה אל
photoBoardRoot: לוח התמונות
newFolder: תיקייה חדשה
newFolderName: שם התיקייה
failedToCreateFolder: יצירת התיקייה נכשלה.
failedToOpenShare: לא ניתן לפתוח את התוכן ששותף.
noHouses: אין בתים זמינים. צור תחילה בית.
recurrence:
title: חזרה
presets: הגדרות מוכנות

View File

@@ -34,8 +34,11 @@ class ApiClient {
Uri _uri(String path, [Map<String, String>? queryParameters]) {
final base = Uri.parse(_credentials.serverUrl);
final prefix = base.path.endsWith('/')
? base.path.substring(0, base.path.length - 1)
: base.path;
return base.replace(
path: '$basePath$path',
path: '$prefix$basePath$path',
queryParameters: queryParameters,
);
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/foundation.dart';
/// A pending note to be created from an OS share intent. Held until the
/// notes wall picks it up and opens the new-note form prefilled.
class PendingNoteShare {
final int houseId;
final String content;
const PendingNoteShare({required this.houseId, required this.content});
}
/// Cross-screen signal carrying a pending note share. The share router
/// pushes here, then asks the home view to switch to the notes tab; the
/// [NotesWallView] for the same house listens and opens the form.
class PendingNoteShareService extends ChangeNotifier {
PendingNoteShareService._();
static final PendingNoteShareService instance = PendingNoteShareService._();
PendingNoteShare? _pending;
PendingNoteShare? get pending => _pending;
void push(PendingNoteShare share) {
_pending = share;
notifyListeners();
}
/// Returns the pending share if it matches [houseId] and clears it.
PendingNoteShare? takeForHouse(int houseId) {
if (_pending?.houseId != houseId) return null;
final taken = _pending;
_pending = null;
return taken;
}
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/foundation.dart';
/// A pending photo upload originating from an OS share intent. Held in a
/// queue until the [PhotoBoardController] for the same house picks it up.
class PendingPhotoShare {
final int houseId;
final int? folderId;
final List<String> paths;
const PendingPhotoShare({
required this.houseId,
required this.folderId,
required this.paths,
});
}
/// Cross-screen queue of pending photo uploads from share intents. The
/// share-router enqueues entries here, then asks the home view to switch
/// to the correct house + photo tab. The [PhotoBoardController] for that
/// house listens and consumes its matching entries.
class PendingPhotoShareService extends ChangeNotifier {
PendingPhotoShareService._();
static final PendingPhotoShareService instance = PendingPhotoShareService._();
final List<PendingPhotoShare> _queue = [];
void enqueue(PendingPhotoShare share) {
_queue.add(share);
notifyListeners();
}
/// Remove and return all pending uploads belonging to [houseId].
List<PendingPhotoShare> takeForHouse(int houseId) {
final taken = _queue.where((s) => s.houseId == houseId).toList();
if (taken.isNotEmpty) {
_queue.removeWhere((s) => s.houseId == houseId);
}
return taken;
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class PrefsService {
class PrefsService extends ChangeNotifier {
PrefsService._();
static final PrefsService instance = PrefsService._();
@@ -10,6 +11,8 @@ class PrefsService {
static const _notificationsIntroSeenKey = 'notifications_intro_seen';
static const _localeKey = 'locale';
static const _themeModeKey = 'theme_mode';
static const _checklistTapRowToToggleKey = 'checklist_tap_row_to_toggle';
static const _checklistCategorySpacingKey = 'checklist_category_spacing';
final _storage = const FlutterSecureStorage();
int? _lastHouseId;
@@ -32,6 +35,13 @@ class PrefsService {
String? _themeMode;
String? get themeMode => _themeMode;
bool _checklistTapRowToToggle = false;
bool get checklistTapRowToToggle => _checklistTapRowToToggle;
/// "disabled", "space", "divider"
String _checklistCategorySpacing = 'disabled';
String get checklistCategorySpacing => _checklistCategorySpacing;
Future<void> load() async {
final lastHouse = await _storage.read(key: _lastHouseKey);
if (lastHouse != null) _lastHouseId = int.tryParse(lastHouse);
@@ -50,11 +60,21 @@ class PrefsService {
_locale = await _storage.read(key: _localeKey);
_themeMode = await _storage.read(key: _themeModeKey);
final tapRow = await _storage.read(key: _checklistTapRowToToggleKey);
if (tapRow != null) _checklistTapRowToToggle = tapRow == 'true';
final spacing = await _storage.read(key: _checklistCategorySpacingKey);
if (spacing != null &&
(spacing == 'disabled' || spacing == 'space' || spacing == 'divider')) {
_checklistCategorySpacing = spacing;
}
}
Future<void> setLastHouseId(int id) async {
_lastHouseId = id;
await _storage.write(key: _lastHouseKey, value: id.toString());
notifyListeners();
}
Future<void> setNotificationsEnabled(bool value) async {
@@ -63,6 +83,7 @@ class PrefsService {
key: _notificationsEnabledKey,
value: value.toString(),
);
notifyListeners();
}
Future<void> setPollIntervalMinutes(int minutes) async {
@@ -71,6 +92,7 @@ class PrefsService {
key: _pollIntervalMinutesKey,
value: minutes.toString(),
);
notifyListeners();
}
Future<void> setLocale(String? locale) async {
@@ -80,6 +102,7 @@ class PrefsService {
} else {
await _storage.write(key: _localeKey, value: locale);
}
notifyListeners();
}
Future<void> setThemeMode(String? mode) async {
@@ -89,6 +112,22 @@ class PrefsService {
} else {
await _storage.write(key: _themeModeKey, value: mode);
}
notifyListeners();
}
Future<void> setChecklistTapRowToToggle(bool value) async {
_checklistTapRowToToggle = value;
await _storage.write(
key: _checklistTapRowToToggleKey,
value: value.toString(),
);
notifyListeners();
}
Future<void> setChecklistCategorySpacing(String value) async {
_checklistCategorySpacing = value;
await _storage.write(key: _checklistCategorySpacingKey, value: value);
notifyListeners();
}
Future<void> setNotificationsIntroSeen(bool value) async {
@@ -97,6 +136,7 @@ class PrefsService {
key: _notificationsIntroSeenKey,
value: value.toString(),
);
notifyListeners();
}
Future<void> clear() async {
@@ -106,11 +146,16 @@ class PrefsService {
_notificationsIntroSeen = false;
_locale = null;
_themeMode = null;
_checklistTapRowToToggle = false;
_checklistCategorySpacing = 'disabled';
await _storage.delete(key: _lastHouseKey);
await _storage.delete(key: _notificationsEnabledKey);
await _storage.delete(key: _pollIntervalMinutesKey);
await _storage.delete(key: _notificationsIntroSeenKey);
await _storage.delete(key: _localeKey);
await _storage.delete(key: _themeModeKey);
await _storage.delete(key: _checklistTapRowToToggleKey);
await _storage.delete(key: _checklistCategorySpacingKey);
notifyListeners();
}
}

View File

@@ -0,0 +1,50 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
/// Listens for incoming OS-level share intents (photos shared from Photos
/// app, plain text/URL shared from any app, etc.) and exposes the most
/// recent batch via a [ValueListenable]. Consumers should call [consume]
/// after navigating to the share handler so the same payload isn't
/// processed twice.
class ShareIntentService {
ShareIntentService._();
static final ShareIntentService instance = ShareIntentService._();
final ValueNotifier<List<SharedMediaFile>?> pending = ValueNotifier(null);
StreamSubscription<List<SharedMediaFile>>? _sub;
/// Begin listening for share intents. Idempotent.
Future<void> init() async {
_sub ??= ReceiveSharingIntent.instance.getMediaStream().listen(
(files) {
if (files.isNotEmpty) pending.value = files;
},
onError: (Object e) {
debugPrint('[ShareIntentService] stream error: $e');
},
);
final initial = await ReceiveSharingIntent.instance.getInitialMedia();
if (initial.isNotEmpty) {
pending.value = initial;
}
// Clear the native cache so the same intent isn't re-delivered on
// subsequent cold starts.
await ReceiveSharingIntent.instance.reset();
}
/// Take the most recent share payload and clear it.
List<SharedMediaFile>? consume() {
final v = pending.value;
pending.value = null;
return v;
}
Future<void> dispose() async {
await _sub?.cancel();
_sub = null;
}
}

View File

@@ -5,9 +5,11 @@ import 'package:pantry/models/category.dart' as models;
import 'package:pantry/models/checklist.dart';
import 'package:pantry/services/auth_service.dart';
import 'package:pantry/services/checklist_service.dart';
import 'package:pantry/services/prefs_service.dart';
import 'package:pantry/utils/category_icons.dart';
import 'package:pantry/utils/rrule.dart';
import 'package:pantry/widgets/image_preview.dart';
import 'package:provider/provider.dart';
class ChecklistItemTile extends StatelessWidget {
final ListItem item;
@@ -35,13 +37,16 @@ class ChecklistItemTile extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final dimmed = item.done;
final tapRowToToggle = context
.watch<PrefsService>()
.checklistTapRowToToggle;
return Material(
type: MaterialType.transparency,
child: Opacity(
opacity: dimmed ? 0.5 : 1.0,
child: InkWell(
onTap: () => onToggle(item),
onTap: tapRowToToggle ? () => onToggle(item) : null,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Row(

View File

@@ -4,6 +4,7 @@ import 'package:pantry/models/category.dart' as models;
import 'package:provider/provider.dart';
import 'package:pantry/models/checklist.dart';
import 'package:pantry/services/prefs_service.dart';
import 'package:pantry/utils/category_icons.dart';
import 'package:pantry/utils/checklist_icons.dart';
import 'package:pantry/widgets/checklist_selector.dart';
@@ -176,6 +177,7 @@ class _ChecklistsBodyState extends State<_ChecklistsBody> {
lists: controller.lists,
currentList: controller.currentList,
onSelected: controller.selectList,
onCreateNew: () => _createList(context, controller),
),
),
IconButton(
@@ -226,6 +228,16 @@ class _ChecklistsBodyState extends State<_ChecklistsBody> {
],
);
}
Future<void> _createList(
BuildContext context,
ChecklistsController controller,
) async {
final created = await showCreateListDialog(context, controller);
if (created != null) {
await controller.selectList(created);
}
}
}
class _SearchPanel extends StatelessWidget {
@@ -457,9 +469,18 @@ class _ItemList extends StatelessWidget {
);
}
final spacingPref = context.watch<PrefsService>().checklistCategorySpacing;
final categorySpacing = controller.sortBy == 'category'
? spacingPref
: 'disabled';
return CustomScrollView(
slivers: [
_ReorderablePartition(items: unchecked, controller: controller),
_ReorderablePartition(
items: unchecked,
controller: controller,
categorySpacing: categorySpacing,
),
if (checked.isNotEmpty) ...[
SliverToBoxAdapter(
child: Column(
@@ -481,7 +502,11 @@ class _ItemList extends StatelessWidget {
],
),
),
_ReorderablePartition(items: checked, controller: controller),
_ReorderablePartition(
items: checked,
controller: controller,
categorySpacing: categorySpacing,
),
],
const SliverToBoxAdapter(child: SizedBox(height: 88)),
],
@@ -492,8 +517,13 @@ class _ItemList extends StatelessWidget {
class _ReorderablePartition extends StatelessWidget {
final List<ListItem> items;
final ChecklistsController controller;
final String categorySpacing;
const _ReorderablePartition({required this.items, required this.controller});
const _ReorderablePartition({
required this.items,
required this.controller,
this.categorySpacing = 'disabled',
});
void _viewItem(
BuildContext context,
@@ -644,21 +674,36 @@ class _ReorderablePartition extends StatelessWidget {
},
itemBuilder: (context, index) {
final item = items[index];
final showSeparator =
categorySpacing != 'disabled' &&
index > 0 &&
items[index - 1].categoryId != item.categoryId;
final tile = ChecklistItemTile(
item: item,
category: item.categoryId != null
? controller.categories[item.categoryId]
: null,
houseId: controller.houseId,
onToggle: controller.toggleItem,
onView: (item) => _viewItem(context, controller, item),
onEdit: (item) => _editItem(context, controller, item),
onMove: (item) => _moveItem(context, controller, item),
onDelete: (item) => _deleteItem(context, controller, item),
);
return ReorderableDelayedDragStartListener(
key: ValueKey(item.id),
index: index,
child: ChecklistItemTile(
item: item,
category: item.categoryId != null
? controller.categories[item.categoryId]
: null,
houseId: controller.houseId,
onToggle: controller.toggleItem,
onView: (item) => _viewItem(context, controller, item),
onEdit: (item) => _editItem(context, controller, item),
onMove: (item) => _moveItem(context, controller, item),
onDelete: (item) => _deleteItem(context, controller, item),
),
child: showSeparator
? Column(
children: [
if (categorySpacing == 'divider')
const Divider(height: 25)
else
const SizedBox(height: 20),
tile,
],
)
: tile,
);
},
);

View File

@@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:pantry/i18n.dart';
import 'package:pantry/models/category.dart' as models;
import 'package:pantry/models/checklist.dart';
@@ -77,6 +78,11 @@ class ItemDetailView extends StatelessWidget {
child: MarkdownBody(
data: item.description!,
shrinkWrap: true,
onTapLink: (text, href, title) {
if (href != null) {
launchUrl(Uri.parse(href));
}
},
styleSheet: MarkdownStyleSheet.fromTheme(theme).copyWith(
p: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,

View File

@@ -9,8 +9,10 @@ import 'package:pantry/models/house.dart';
import 'package:pantry/services/deep_link_service.dart';
import 'package:pantry/views/notifications/notifications_controller.dart';
import 'package:pantry/views/notifications/notifications_view.dart';
import 'package:pantry/services/share_intent_service.dart';
import 'package:pantry/views/photos/photo_board_view.dart';
import 'package:pantry/views/settings/settings_view.dart';
import 'package:pantry/views/share/share_router_view.dart';
import 'package:pantry/widgets/create_house_dialog.dart';
import 'package:pantry/widgets/no_houses_view.dart';
import 'package:pantry/widgets/notifications_bell.dart';
@@ -72,20 +74,23 @@ class _HomeViewBodyState extends State<_HomeViewBody>
WidgetsBinding.instance.addObserver(this);
_notificationsController.load();
// Consume any deep link that arrived before we mounted (e.g. from a
// cold-start notification tap).
// Consume any deep link or share intent that arrived before we
// mounted (e.g. from a cold-start notification tap or share sheet).
WidgetsBinding.instance.addPostFrameCallback((_) {
_consumePendingDeepLink();
_consumePendingShare();
});
// Listen for deep links that arrive while the home view is mounted
// (notification tapped while app is in foreground or background).
// Listen for deep links and share intents that arrive while the home
// view is mounted.
DeepLinkService.instance.pending.addListener(_consumePendingDeepLink);
ShareIntentService.instance.pending.addListener(_consumePendingShare);
}
@override
void dispose() {
DeepLinkService.instance.pending.removeListener(_consumePendingDeepLink);
ShareIntentService.instance.pending.removeListener(_consumePendingShare);
WidgetsBinding.instance.removeObserver(this);
_pageController.dispose();
_notificationsController.dispose();
@@ -97,9 +102,21 @@ class _HomeViewBodyState extends State<_HomeViewBody>
if (state == AppLifecycleState.resumed) {
_notificationsController.refresh();
_consumePendingDeepLink();
_consumePendingShare();
}
}
void _consumePendingShare() {
final files = ShareIntentService.instance.consume();
if (files == null || files.isEmpty || !mounted) return;
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ShareRouterView(files: files),
fullscreenDialog: true,
),
);
}
void _goToTab(int index) {
if (index == _tabIndex) return;
_pageController.animateToPage(

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:pantry/models/note.dart';
import 'package:pantry/utils/text_direction.dart';
@@ -42,6 +43,7 @@ class NoteDetailView extends StatelessWidget {
),
),
floatingActionButton: FloatingActionButton(
heroTag: null,
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
@@ -51,70 +53,84 @@ class NoteDetailView extends StatelessWidget {
},
child: const Icon(Icons.edit),
),
body: note.content != null && note.content!.isNotEmpty
? Directionality(
textDirection: contentDir,
child: Markdown(
data: note.content!,
padding: const EdgeInsets.all(16),
selectable: true,
styleSheet: MarkdownStyleSheet.fromTheme(theme).copyWith(
p: theme.textTheme.bodyLarge?.copyWith(
color: textColor.withAlpha(230),
),
h1: theme.textTheme.headlineMedium?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
h2: theme.textTheme.headlineSmall?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
h3: theme.textTheme.titleLarge?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
h4: theme.textTheme.titleMedium?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
listBullet: theme.textTheme.bodyLarge?.copyWith(
color: textColor.withAlpha(230),
),
code: TextStyle(
color: textColor,
backgroundColor: textColor.withAlpha(30),
fontFamily: 'monospace',
),
codeblockDecoration: BoxDecoration(
color: textColor.withAlpha(30),
borderRadius: BorderRadius.circular(6),
),
blockquote: theme.textTheme.bodyLarge?.copyWith(
color: textColor.withAlpha(180),
fontStyle: FontStyle.italic,
),
blockquoteDecoration: BoxDecoration(
border: Border(
left: BorderSide(
color: textColor.withAlpha(100),
width: 4,
body: Hero(
tag: 'note-${note.id}',
child: Material(
color: bgColor,
child: note.content != null && note.content!.isNotEmpty
? Directionality(
textDirection: contentDir,
child: Markdown(
data: note.content!,
padding: const EdgeInsets.all(16),
selectable: true,
onTapLink: (text, href, title) {
if (href != null) {
launchUrl(Uri.parse(href));
}
},
styleSheet: MarkdownStyleSheet.fromTheme(theme).copyWith(
p: theme.textTheme.bodyLarge?.copyWith(
color: textColor.withAlpha(230),
),
h1: theme.textTheme.headlineMedium?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
h2: theme.textTheme.headlineSmall?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
h3: theme.textTheme.titleLarge?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
h4: theme.textTheme.titleMedium?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
listBullet: theme.textTheme.bodyLarge?.copyWith(
color: textColor.withAlpha(230),
),
code: TextStyle(
color: textColor,
backgroundColor: textColor.withAlpha(30),
fontFamily: 'monospace',
),
codeblockDecoration: BoxDecoration(
color: textColor.withAlpha(30),
borderRadius: BorderRadius.circular(6),
),
blockquote: theme.textTheme.bodyLarge?.copyWith(
color: textColor.withAlpha(180),
fontStyle: FontStyle.italic,
),
blockquoteDecoration: BoxDecoration(
border: Border(
left: BorderSide(
color: textColor.withAlpha(100),
width: 4,
),
),
),
a: TextStyle(
color: textColor,
decoration: TextDecoration.underline,
),
strong: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
em: TextStyle(
color: textColor,
fontStyle: FontStyle.italic,
),
),
),
a: TextStyle(
color: textColor,
decoration: TextDecoration.underline,
),
strong: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
em: TextStyle(color: textColor, fontStyle: FontStyle.italic),
),
),
)
: null,
)
: null,
),
),
);
}
}

View File

@@ -29,7 +29,16 @@ class NoteFormView extends StatefulWidget {
final NotesController controller;
final Note? note;
const NoteFormView({super.key, required this.controller, this.note});
/// When opening a new note seeded from an OS share intent, this holds
/// the shared text/URL to prefill into the content field.
final String? prefillContent;
const NoteFormView({
super.key,
required this.controller,
this.note,
this.prefillContent,
});
@override
State<NoteFormView> createState() => _NoteFormViewState();
@@ -50,11 +59,13 @@ class _NoteFormViewState extends State<NoteFormView> {
super.initState();
_titleController = TextEditingController(text: widget.note?.title ?? '');
_contentController = TextEditingController(
text: widget.note?.content ?? '',
text: widget.note?.content ?? widget.prefillContent ?? '',
);
_selectedColor = widget.note?.color;
_titleDir = detectTextDirection(widget.note?.title);
_contentDir = detectTextDirection(widget.note?.content);
_contentDir = detectTextDirection(
widget.note?.content ?? widget.prefillContent,
);
_titleController.addListener(() {
final dir = detectTextDirection(_titleController.text);
if (dir != _titleDir) setState(() => _titleDir = dir);
@@ -104,10 +115,25 @@ class _NoteFormViewState extends State<NoteFormView> {
}
}
Color get _bgColor {
if (_selectedColor != null && _selectedColor!.isNotEmpty) {
final hex = _selectedColor!.replaceFirst('#', '');
final value = int.tryParse('FF$hex', radix: 16);
if (value != null) return Color(value);
}
return Theme.of(context).colorScheme.surfaceContainerHighest;
}
@override
Widget build(BuildContext context) {
final bgColor = _bgColor;
final textColor = _contrastColor(bgColor);
return Scaffold(
backgroundColor: bgColor,
appBar: AppBar(
backgroundColor: bgColor,
foregroundColor: textColor,
title: Text(_isEditing ? m.notesWall.editNote : m.notesWall.newNote),
),
floatingActionButton: FloatingActionButton(
@@ -120,76 +146,92 @@ class _NoteFormViewState extends State<NoteFormView> {
)
: const Icon(Icons.check),
),
body: ListView(
padding: const EdgeInsets.all(16),
body: Column(
children: [
TextField(
controller: _titleController,
decoration: InputDecoration(
labelText: m.notesWall.title,
border: const OutlineInputBorder(),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(16, 16, 16, 0),
child: TextField(
controller: _titleController,
decoration: InputDecoration(
hintText: m.notesWall.title,
hintStyle: TextStyle(color: textColor.withAlpha(100)),
border: InputBorder.none,
),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
autofocus: !_isEditing,
textCapitalization: TextCapitalization.sentences,
textInputAction: TextInputAction.next,
textDirection: _titleDir,
),
autofocus: !_isEditing,
textCapitalization: TextCapitalization.sentences,
textInputAction: TextInputAction.next,
textDirection: _titleDir,
),
const SizedBox(height: 16),
TextField(
controller: _contentController,
decoration: InputDecoration(
labelText: m.notesWall.content,
border: const OutlineInputBorder(),
alignLabelWithHint: true,
),
textCapitalization: TextCapitalization.sentences,
maxLines: 10,
minLines: 4,
textDirection: _contentDir,
),
const SizedBox(height: 16),
Text(
m.notesWall.color,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _colorOptions.map((hex) {
final color = hex != null
? Color(
int.parse('FF${hex.replaceFirst('#', '')}', radix: 16),
)
: Theme.of(context).colorScheme.surfaceContainerHighest;
final isSelected = _selectedColor == hex;
return GestureDetector(
onTap: () => setState(() => _selectedColor = hex),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: isSelected
? Border.all(
color: Theme.of(context).colorScheme.primary,
width: 3,
)
: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
child: isSelected
? Icon(
Icons.check,
size: 18,
color: _contrastColor(color),
)
: null,
Expanded(
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 0),
child: TextField(
controller: _contentController,
decoration: InputDecoration(
hintText: m.notesWall.content,
hintStyle: TextStyle(color: textColor.withAlpha(100)),
border: InputBorder.none,
),
);
}).toList(),
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: textColor.withAlpha(230),
),
textCapitalization: TextCapitalization.sentences,
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
textDirection: _contentDir,
),
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsetsDirectional.fromSTEB(16, 8, 80, 48),
child: Row(
children: _colorOptions.map((hex) {
final color = hex != null
? Color(
int.parse('FF${hex.replaceFirst('#', '')}', radix: 16),
)
: Theme.of(context).colorScheme.surfaceContainerHighest;
final isSelected = _selectedColor == hex;
return Padding(
padding: const EdgeInsetsDirectional.only(end: 8),
child: GestureDetector(
onTap: () => setState(() => _selectedColor = hex),
child: CustomPaint(
foregroundPainter: hex == null
? _DiagonalLinePainter(
color: textColor.withAlpha(120),
)
: null,
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: isSelected
? Border.all(color: textColor, width: 3)
: Border.all(color: textColor.withAlpha(60)),
),
child: isSelected
? Icon(
Icons.check,
size: 18,
color: _contrastColor(color),
)
: null,
),
),
),
);
}).toList(),
),
),
],
),
@@ -200,3 +242,34 @@ class _NoteFormViewState extends State<NoteFormView> {
return bg.computeLuminance() > 0.5 ? Colors.black87 : Colors.white;
}
}
class _DiagonalLinePainter extends CustomPainter {
final Color color;
_DiagonalLinePainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = 2
..style = PaintingStyle.stroke;
final center = size.center(Offset.zero);
final radius = size.width / 2;
canvas.clipRRect(
RRect.fromRectAndRadius(
Rect.fromCircle(center: center, radius: radius),
Radius.circular(radius),
),
);
canvas.drawLine(
Offset(size.width * 0.15, size.height * 0.85),
Offset(size.width * 0.85, size.height * 0.15),
paint,
);
}
@override
bool shouldRepaint(_DiagonalLinePainter oldDelegate) =>
color != oldDelegate.color;
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:pantry/i18n.dart';
import 'package:pantry/services/pending_note_share_service.dart';
import 'package:pantry/widgets/note_selection_actions.dart';
import 'package:pantry/widgets/note_sort_button.dart';
import 'package:pantry/widgets/note_tile.dart';
@@ -23,14 +24,30 @@ class _NotesWallViewState extends State<NotesWallView> {
void initState() {
super.initState();
_controller.load();
PendingNoteShareService.instance.addListener(_handlePendingShare);
WidgetsBinding.instance.addPostFrameCallback((_) => _handlePendingShare());
}
@override
void dispose() {
PendingNoteShareService.instance.removeListener(_handlePendingShare);
_controller.dispose();
super.dispose();
}
void _handlePendingShare() {
final share = PendingNoteShareService.instance.takeForHouse(widget.houseId);
if (share == null || !mounted) return;
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => NoteFormView(
controller: _controller,
prefillContent: share.content,
),
),
);
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(

View File

@@ -2,12 +2,14 @@ import 'package:flutter/foundation.dart';
import 'package:image_picker/image_picker.dart';
import 'package:pantry/i18n.dart';
import 'package:pantry/models/photo.dart';
import 'package:pantry/services/pending_photo_share_service.dart';
import 'package:pantry/services/photo_service.dart';
class UploadTask {
final String fileName;
final Uint8List? thumbnailBytes;
final String mimeType;
final int? folderId;
double progress;
bool done;
String? error;
@@ -17,6 +19,7 @@ class UploadTask {
required this.fileName,
this.thumbnailBytes,
this.mimeType = 'image/jpeg',
this.folderId,
}) : progress = 0.0,
done = false;
@@ -31,13 +34,18 @@ class UploadTask {
class PhotoBoardController extends ChangeNotifier {
final int houseId;
PhotoBoardController({required this.houseId});
PhotoBoardController({required this.houseId}) {
PendingPhotoShareService.instance.addListener(_consumePendingShares);
// Consume any shares that arrived while this controller didn't exist.
_consumePendingShares();
}
bool _disposed = false;
@override
void dispose() {
_disposed = true;
PendingPhotoShareService.instance.removeListener(_consumePendingShares);
super.dispose();
}
@@ -281,7 +289,8 @@ class PhotoBoardController extends ChangeNotifier {
// -- Upload --
Future<void> uploadPhotos(List<XFile> files) async {
Future<void> uploadPhotos(List<XFile> files, {int? folderId}) async {
final target = folderId ?? _currentFolderId;
// Create all tasks up front with thumbnail bytes
final tasks = <(UploadTask, XFile)>[];
for (final file in files) {
@@ -290,6 +299,7 @@ class PhotoBoardController extends ChangeNotifier {
fileName: file.name,
thumbnailBytes: bytes,
mimeType: file.mimeType ?? 'image/jpeg',
folderId: target,
);
_uploads.add(task);
tasks.add((task, file));
@@ -313,7 +323,7 @@ class PhotoBoardController extends ChangeNotifier {
bytes: task.thumbnailBytes!,
fileName: task.fileName,
mimeType: task.mimeType,
folderId: _currentFolderId,
folderId: task.folderId,
);
_photos.insert(0, photo);
_service.cachePhotos(houseId, _photos);
@@ -336,6 +346,15 @@ class PhotoBoardController extends ChangeNotifier {
_cleanUpDoneUploads();
}
void _consumePendingShares() {
final shares = PendingPhotoShareService.instance.takeForHouse(houseId);
if (shares.isEmpty) return;
for (final share in shares) {
final files = share.paths.map((p) => XFile(p)).toList();
uploadPhotos(files, folderId: share.folderId);
}
}
void dismissUpload(UploadTask task) {
_uploads.remove(task);
notifyListeners();

View File

@@ -96,11 +96,7 @@ class _PhotoBoardBody extends StatelessWidget {
),
],
),
PositionedDirectional(
end: 16,
bottom: 16,
child: PhotoAddButton(controller: controller),
),
Positioned.fill(child: PhotoAddButton(controller: controller)),
],
),
);

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pantry/i18n.dart';
import 'package:pantry/services/background_notification_task.dart';
@@ -15,22 +16,36 @@ class SettingsView extends StatefulWidget {
}
class _SettingsViewState extends State<SettingsView> {
late bool _notificationsEnabled;
late int _pollIntervalMinutes;
late String? _selectedLocale;
late String? _selectedTheme;
static const _pollOptions = [15, 30, 60, 120, 360];
static const _categorySpacingOptions = ['disabled', 'space', 'divider'];
@override
void initState() {
super.initState();
_notificationsEnabled = PrefsService.instance.notificationsEnabled;
_pollIntervalMinutes = PrefsService.instance.pollIntervalMinutes;
_selectedLocale = PrefsService.instance.locale;
_selectedTheme = PrefsService.instance.themeMode;
}
Future<void> _setTapRowToComplete(bool value) async {
await context.read<PrefsService>().setChecklistTapRowToToggle(value);
}
Future<void> _setCategorySpacing(String? value) async {
if (value == null) return;
final prefs = context.read<PrefsService>();
if (value == prefs.checklistCategorySpacing) return;
await prefs.setChecklistCategorySpacing(value);
}
String _categorySpacingLabel(String value) => switch (value) {
'space' => m.settings.categorySpacingNames.space,
'divider' => m.settings.categorySpacingNames.divider,
_ => m.settings.categorySpacingNames.disabled,
};
// -- Language --
Future<void> _setLocale(String? value) async {
@@ -78,8 +93,8 @@ class _SettingsViewState extends State<SettingsView> {
}
}
await PrefsService.instance.setNotificationsEnabled(value);
setState(() => _notificationsEnabled = value);
if (!mounted) return;
await context.read<PrefsService>().setNotificationsEnabled(value);
if (value) {
await registerBackgroundNotificationPoll();
@@ -90,10 +105,11 @@ class _SettingsViewState extends State<SettingsView> {
}
Future<void> _setPollInterval(int? minutes) async {
if (minutes == null || minutes == _pollIntervalMinutes) return;
await PrefsService.instance.setPollIntervalMinutes(minutes);
setState(() => _pollIntervalMinutes = minutes);
if (_notificationsEnabled) {
if (minutes == null) return;
final prefs = context.read<PrefsService>();
if (minutes == prefs.pollIntervalMinutes) return;
await prefs.setPollIntervalMinutes(minutes);
if (prefs.notificationsEnabled) {
await rescheduleBackgroundNotificationPoll();
}
}
@@ -109,6 +125,12 @@ class _SettingsViewState extends State<SettingsView> {
@override
Widget build(BuildContext context) {
final prefs = context.watch<PrefsService>();
final notificationsEnabled = prefs.notificationsEnabled;
final pollIntervalMinutes = prefs.pollIntervalMinutes;
final tapRowToComplete = prefs.checklistTapRowToToggle;
final categorySpacing = prefs.checklistCategorySpacing;
return Scaffold(
appBar: AppBar(title: Text(m.settings.title)),
body: ListView(
@@ -173,21 +195,45 @@ class _SettingsViewState extends State<SettingsView> {
),
),
// -- Interface --
_SectionHeader(m.settings.interfaceSection),
SwitchListTile(
title: Text(m.settings.tapRowToComplete),
subtitle: Text(m.settings.tapRowToCompleteBody),
value: tapRowToComplete,
onChanged: _setTapRowToComplete,
),
ListTile(
title: Text(m.settings.categorySpacing),
subtitle: Text(m.settings.categorySpacingBody),
trailing: DropdownButton<String>(
value: categorySpacing,
onChanged: _setCategorySpacing,
items: [
for (final option in _categorySpacingOptions)
DropdownMenuItem(
value: option,
child: Text(_categorySpacingLabel(option)),
),
],
),
),
// -- Notifications --
_SectionHeader(m.settings.notificationsSection),
SwitchListTile(
title: Text(m.settings.enableNotifications),
subtitle: Text(m.settings.enableNotificationsBody),
value: _notificationsEnabled,
value: notificationsEnabled,
onChanged: _toggleNotifications,
),
ListTile(
enabled: _notificationsEnabled,
enabled: notificationsEnabled,
title: Text(m.settings.pollInterval),
subtitle: Text(_pollIntervalLabel(_pollIntervalMinutes)),
subtitle: Text(_pollIntervalLabel(pollIntervalMinutes)),
trailing: DropdownButton<int>(
value: _pollIntervalMinutes,
onChanged: _notificationsEnabled ? _setPollInterval : null,
value: pollIntervalMinutes,
onChanged: notificationsEnabled ? _setPollInterval : null,
items: [
for (final minutes in _pollOptions)
DropdownMenuItem(

View File

@@ -0,0 +1,303 @@
import 'package:flutter/material.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:pantry/i18n.dart';
import 'package:pantry/models/house.dart';
import 'package:pantry/models/photo.dart';
import 'package:pantry/services/deep_link_service.dart';
import 'package:pantry/services/house_service.dart';
import 'package:pantry/services/pending_note_share_service.dart';
import 'package:pantry/services/pending_photo_share_service.dart';
import 'package:pantry/services/photo_service.dart';
import 'package:pantry/services/prefs_service.dart';
/// Entry screen for an incoming OS share intent. Classifies the payload,
/// optionally asks the user to pick a house, and then routes:
/// * photo flow → folder picker → enqueue uploads → switch to photo tab
/// * text/URL flow → push the notes-form view prefilled with the content
///
/// On completion this view pops itself. On cancel/error it also pops.
class ShareRouterView extends StatefulWidget {
final List<SharedMediaFile> files;
const ShareRouterView({super.key, required this.files});
@override
State<ShareRouterView> createState() => _ShareRouterViewState();
}
class _ShareRouterViewState extends State<ShareRouterView> {
bool _busy = true;
String? _error;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _run());
}
Future<void> _run() async {
try {
final houses = await HouseService.instance.getHouses();
if (!mounted) return;
if (houses.isEmpty) {
setState(() {
_busy = false;
_error = m.share.noHouses;
});
return;
}
final house = houses.length == 1
? houses.first
: await _pickHouse(houses);
if (!mounted) return;
if (house == null) {
Navigator.of(context).pop();
return;
}
final hasImages = widget.files.any(
(f) => f.type == SharedMediaType.image,
);
if (hasImages) {
await _runPhotoFlow(house);
} else {
await _runNoteFlow(house);
}
} catch (_) {
if (!mounted) return;
setState(() {
_busy = false;
_error = m.share.failedToOpenShare;
});
}
}
Future<House?> _pickHouse(List<House> houses) async {
return showDialog<House>(
context: context,
builder: (ctx) => SimpleDialog(
title: Text(m.share.chooseHouse),
contentPadding: const EdgeInsets.symmetric(vertical: 8),
children: houses
.map(
(h) => ListTile(
leading: const Icon(Icons.home_outlined),
title: Text(h.name),
onTap: () => Navigator.pop(ctx, h),
),
)
.toList(),
),
);
}
Future<void> _runPhotoFlow(House house) async {
// Fetch folders for the selected house (used in the picker).
List<PhotoFolder> folders;
try {
folders = await PhotoService.instance.getFolders(house.id);
} catch (_) {
folders = [];
}
if (!mounted) return;
final dest = await showModalBottomSheet<_PhotoDestination>(
context: context,
isScrollControlled: true,
builder: (ctx) =>
_PhotoDestinationPicker(folders: folders, houseId: house.id),
);
if (!mounted) return;
if (dest == null) {
Navigator.of(context).pop();
return;
}
int? folderId = dest.folderId;
if (dest.newFolderName != null) {
try {
final folder = await PhotoService.instance.createFolder(
house.id,
name: dest.newFolderName!,
);
folderId = folder.id;
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(m.share.failedToCreateFolder)));
}
return;
}
}
final paths = widget.files
.where((f) => f.type == SharedMediaType.image)
.map((f) => f.path)
.toList();
await PrefsService.instance.setLastHouseId(house.id);
if (!mounted) return;
Navigator.of(context).pop();
DeepLinkService.instance.push(DeepLink(tabIndex: 1, houseId: house.id));
PendingPhotoShareService.instance.enqueue(
PendingPhotoShare(houseId: house.id, folderId: folderId, paths: paths),
);
}
Future<void> _runNoteFlow(House house) async {
final text = widget.files
.where(
(f) =>
f.type == SharedMediaType.text || f.type == SharedMediaType.url,
)
.map((f) => f.path)
.where((s) => s.isNotEmpty)
.join('\n\n');
await PrefsService.instance.setLastHouseId(house.id);
if (!mounted) return;
// Pop the share router BEFORE notifying listeners. If the notes wall
// is already mounted, its listener pushes the form synchronously, and
// we don't want our own pop to remove that form by accident.
Navigator.of(context).pop();
DeepLinkService.instance.push(DeepLink(tabIndex: 2, houseId: house.id));
PendingNoteShareService.instance.push(
PendingNoteShare(houseId: house.id, content: text),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(m.share.title)),
body: Center(
child: _busy
? const CircularProgressIndicator()
: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(_error ?? '', textAlign: TextAlign.center),
const SizedBox(height: 16),
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(m.common.cancel),
),
],
),
),
),
);
}
}
class _PhotoDestination {
/// null = root, or a real folder id.
final int? folderId;
/// non-null when the user picked "new folder" with this name.
final String? newFolderName;
const _PhotoDestination.root() : folderId = null, newFolderName = null;
const _PhotoDestination.folder(this.folderId) : newFolderName = null;
const _PhotoDestination.newFolder(this.newFolderName) : folderId = null;
}
class _PhotoDestinationPicker extends StatelessWidget {
final List<PhotoFolder> folders;
final int houseId;
const _PhotoDestinationPicker({required this.folders, required this.houseId});
@override
Widget build(BuildContext context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(16, 8, 16, 8),
child: Text(
m.share.choosePhotoDestination,
style: Theme.of(context).textTheme.titleMedium,
),
),
ListTile(
leading: const Icon(Icons.photo_library_outlined),
title: Text(m.share.photoBoardRoot),
onTap: () =>
Navigator.pop(context, const _PhotoDestination.root()),
),
const Divider(height: 1),
Flexible(
child: ListView(
shrinkWrap: true,
children: [
for (final folder in folders)
ListTile(
leading: const Icon(Icons.folder_outlined),
title: Text(folder.name),
onTap: () => Navigator.pop(
context,
_PhotoDestination.folder(folder.id),
),
),
],
),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.create_new_folder_outlined),
title: Text(m.share.newFolder),
onTap: () => _promptNewFolder(context),
),
],
),
),
);
}
Future<void> _promptNewFolder(BuildContext context) async {
final textController = TextEditingController();
final result = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(m.share.newFolder),
content: TextField(
controller: textController,
autofocus: true,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
labelText: m.share.newFolderName,
border: const OutlineInputBorder(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(m.common.cancel),
),
FilledButton(
onPressed: () {
final name = textController.text.trim();
if (name.isNotEmpty) Navigator.pop(ctx, name);
},
child: Text(m.common.save),
),
],
),
);
if (result != null && context.mounted) {
Navigator.pop(context, _PhotoDestination.newFolder(result));
}
}
}

View File

@@ -1,64 +1,100 @@
import 'package:flutter/material.dart';
import 'package:pantry/i18n.dart';
import 'package:pantry/models/checklist.dart';
import 'package:pantry/utils/checklist_icons.dart';
class ChecklistSelector extends StatelessWidget {
const int _kCreateNewListSentinel = -1;
class ChecklistSelector extends StatefulWidget {
final List<ChecklistList> lists;
final ChecklistList? currentList;
final ValueChanged<ChecklistList> onSelected;
final VoidCallback onCreateNew;
const ChecklistSelector({
super.key,
required this.lists,
required this.currentList,
required this.onSelected,
required this.onCreateNew,
});
@override
State<ChecklistSelector> createState() => _ChecklistSelectorState();
}
class _ChecklistSelectorState extends State<ChecklistSelector> {
int _resetCount = 0;
void _handleChanged(int? id) {
if (id == null) return;
if (id == _kCreateNewListSentinel) {
// Force the dropdown to drop its internal -1 state and snap back to
// the current list before we open the create flow.
setState(() => _resetCount++);
widget.onCreateNew();
return;
}
final list = widget.lists.firstWhere((l) => l.id == id);
widget.onSelected(list);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final primary = theme.colorScheme.primary;
return Padding(
padding: const EdgeInsetsDirectional.only(start: 16, top: 8, bottom: 8),
child: DropdownButtonFormField<int>(
initialValue: currentList?.id,
key: ValueKey('${widget.currentList?.id}-$_resetCount'),
initialValue: widget.currentList?.id,
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
),
items: lists
.map(
(list) => DropdownMenuItem(
value: list.id,
child: Row(
children: [
Icon(checklistIcon(list.icon), size: 20),
const SizedBox(width: 8),
Flexible(
child: Text(list.name, overflow: TextOverflow.ellipsis),
),
],
),
),
)
.toList(),
selectedItemBuilder: (context) => lists
.map(
(list) => Row(
mainAxisSize: MainAxisSize.min,
items: [
...widget.lists.map(
(list) => DropdownMenuItem(
value: list.id,
child: Row(
children: [
Icon(checklistIcon(list.icon), size: 20),
const SizedBox(width: 8),
Text(list.name, overflow: TextOverflow.ellipsis),
Flexible(
child: Text(list.name, overflow: TextOverflow.ellipsis),
),
],
),
)
.toList(),
onChanged: (id) {
if (id == null) return;
final list = lists.firstWhere((l) => l.id == id);
onSelected(list);
},
),
),
DropdownMenuItem(
value: _kCreateNewListSentinel,
child: Row(
children: [
Icon(Icons.add, size: 20, color: primary),
const SizedBox(width: 8),
Text(m.checklists.createList, style: TextStyle(color: primary)),
],
),
),
],
selectedItemBuilder: (context) => [
...widget.lists.map(
(list) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(checklistIcon(list.icon), size: 20),
const SizedBox(width: 8),
Text(list.name, overflow: TextOverflow.ellipsis),
],
),
),
const SizedBox.shrink(),
],
onChanged: _handleChanged,
),
);
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:pantry/i18n.dart';
import 'package:pantry/models/note.dart';
@@ -74,94 +75,112 @@ class NoteTile extends StatelessWidget {
final titleDir = detectTextDirection(note.title);
final contentDir = detectTextDirection(note.content);
return Container(
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Directionality(
textDirection: titleDir,
child: Text(
note.title,
style: theme.textTheme.titleSmall?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
return Hero(
tag: 'note-${note.id}',
child: Container(
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Directionality(
textDirection: titleDir,
child: Text(
note.title,
style: theme.textTheme.titleSmall?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
_NoteMenuButton(
note: note,
controller: controller,
color: textColor,
),
],
),
if (note.content != null && note.content!.isNotEmpty) ...[
const SizedBox(height: 6),
Expanded(
child: ShaderMask(
shaderCallback: (bounds) => LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white, Colors.white, Colors.transparent],
stops: const [0.0, 0.7, 1.0],
).createShader(bounds),
blendMode: BlendMode.dstIn,
child: Directionality(
textDirection: contentDir,
child: Markdown(
data: note.content!,
shrinkWrap: false,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
onTapLink: (text, href, title) {
if (href != null) {
launchUrl(Uri.parse(href));
}
},
styleSheet: MarkdownStyleSheet.fromTheme(theme).copyWith(
p: theme.textTheme.bodySmall?.copyWith(
color: textColor.withAlpha(200),
),
h1: theme.textTheme.titleMedium?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
h2: theme.textTheme.titleSmall?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
h3: theme.textTheme.bodyMedium?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
listBullet: theme.textTheme.bodySmall?.copyWith(
color: textColor.withAlpha(200),
),
strong: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
em: TextStyle(
color: textColor.withAlpha(200),
fontStyle: FontStyle.italic,
),
code: TextStyle(
color: textColor,
backgroundColor: textColor.withAlpha(30),
fontFamily: 'monospace',
fontSize: 12,
),
blockquote: theme.textTheme.bodySmall?.copyWith(
color: textColor.withAlpha(180),
fontStyle: FontStyle.italic,
),
a: TextStyle(
color: textColor,
decoration: TextDecoration.underline,
),
),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
_NoteMenuButton(
note: note,
controller: controller,
color: textColor,
),
],
),
if (note.content != null && note.content!.isNotEmpty) ...[
const SizedBox(height: 6),
Expanded(
child: Directionality(
textDirection: contentDir,
child: MarkdownBody(
data: note.content!,
shrinkWrap: true,
fitContent: false,
styleSheet: MarkdownStyleSheet.fromTheme(theme).copyWith(
p: theme.textTheme.bodySmall?.copyWith(
color: textColor.withAlpha(200),
),
h1: theme.textTheme.titleMedium?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
h2: theme.textTheme.titleSmall?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
h3: theme.textTheme.bodyMedium?.copyWith(
color: textColor,
fontWeight: FontWeight.bold,
),
listBullet: theme.textTheme.bodySmall?.copyWith(
color: textColor.withAlpha(200),
),
strong: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
em: TextStyle(
color: textColor.withAlpha(200),
fontStyle: FontStyle.italic,
),
code: TextStyle(
color: textColor,
backgroundColor: textColor.withAlpha(30),
fontFamily: 'monospace',
fontSize: 12,
),
blockquote: theme.textTheme.bodySmall?.copyWith(
color: textColor.withAlpha(180),
fontStyle: FontStyle.italic,
),
a: TextStyle(
color: textColor,
decoration: TextDecoration.underline,
),
),
),
),
),
],
],
),
),
);
}

View File

@@ -1,47 +1,80 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:pantry/i18n.dart';
import 'package:pantry/views/photos/photo_board_controller.dart';
class PhotoAddButton extends StatelessWidget {
class PhotoAddButton extends StatefulWidget {
final PhotoBoardController controller;
const PhotoAddButton({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton.small(
heroTag: 'photo_folder',
onPressed: () => _createFolder(context),
child: const Icon(Icons.create_new_folder),
),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: 'photo_upload',
onPressed: () => _pickPhotos(context),
child: const Icon(Icons.add_photo_alternate),
),
],
State<PhotoAddButton> createState() => _PhotoAddButtonState();
}
class _PhotoAddButtonState extends State<PhotoAddButton>
with SingleTickerProviderStateMixin {
late final AnimationController _animController;
bool _open = false;
@override
void initState() {
super.initState();
_animController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 380),
);
}
Future<void> _pickPhotos(BuildContext context) async {
final picker = ImagePicker();
final files = await picker.pickMultiImage();
if (files.isNotEmpty) {
controller.uploadPhotos(files);
@override
void dispose() {
_animController.dispose();
super.dispose();
}
void _toggle() {
setState(() => _open = !_open);
if (_open) {
_animController.forward();
} else {
_animController.reverse();
}
}
void _createFolder(BuildContext context) {
void _close() {
if (!_open) return;
setState(() => _open = false);
_animController.reverse();
}
Future<void> _pickPhotos() async {
_close();
final picker = ImagePicker();
final files = await picker.pickMultiImage();
if (files.isNotEmpty) {
widget.controller.uploadPhotos(files);
}
}
Future<void> _takePhoto() async {
_close();
final picker = ImagePicker();
final file = await picker.pickImage(source: ImageSource.camera);
if (file != null) {
widget.controller.uploadPhotos([file]);
}
}
Future<void> _createFolderDialog() async {
_close();
if (!mounted) return;
final textController = TextEditingController();
showDialog(
final name = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
builder: (dialogCtx) => AlertDialog(
title: Text(m.photoBoard.newFolder),
content: TextField(
controller: textController,
@@ -51,24 +84,211 @@ class PhotoAddButton extends StatelessWidget {
labelText: m.photoBoard.folderName,
border: const OutlineInputBorder(),
),
onSubmitted: (value) {
final v = value.trim();
if (v.isNotEmpty) Navigator.pop(dialogCtx, v);
},
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
onPressed: () => Navigator.pop(dialogCtx),
child: Text(m.common.cancel),
),
FilledButton(
onPressed: () {
final name = textController.text.trim();
if (name.isNotEmpty) {
controller.createFolder(name);
Navigator.pop(ctx);
}
final v = textController.text.trim();
if (v.isNotEmpty) Navigator.pop(dialogCtx, v);
},
child: Text(m.common.save),
),
],
),
);
if (name != null && name.isNotEmpty) {
widget.controller.createFolder(name);
}
}
@override
Widget build(BuildContext context) {
final actions = <_FabAction>[
_FabAction(
icon: Icons.add_photo_alternate,
label: m.photoBoard.addMenu.upload,
onTap: _pickPhotos,
),
_FabAction(
icon: Icons.camera_alt,
label: m.photoBoard.addMenu.camera,
onTap: _takePhoto,
),
_FabAction(
icon: Icons.create_new_folder,
label: m.photoBoard.addMenu.newFolder,
onTap: _createFolderDialog,
),
];
return PopScope(
canPop: !_open,
onPopInvokedWithResult: (didPop, _) {
if (!didPop && _open) _close();
},
child: Stack(
children: [
Positioned.fill(
child: IgnorePointer(
ignoring: !_open,
child: AnimatedOpacity(
opacity: _open ? 1 : 0,
duration: const Duration(milliseconds: 180),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _close,
child: const ColoredBox(color: Color(0x66000000)),
),
),
),
),
PositionedDirectional(
end: 16,
bottom: 16,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
for (var i = 0; i < actions.length; i++)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _AnimatedAction(
action: actions[i],
controller: _animController,
index: actions.length - 1 - i,
total: actions.length,
),
),
_buildMainFab(),
],
),
),
],
),
);
}
Widget _buildMainFab() {
return FloatingActionButton(
heroTag: 'photo_add_menu_main',
onPressed: _toggle,
child: AnimatedBuilder(
animation: _animController,
builder: (ctx, _) {
return Transform.rotate(
angle: _animController.value * (math.pi * 3 / 4),
child: const Icon(Icons.add),
);
},
),
);
}
}
class _FabAction {
final IconData icon;
final String label;
final VoidCallback onTap;
const _FabAction({
required this.icon,
required this.label,
required this.onTap,
});
}
class _AnimatedAction extends StatelessWidget {
final _FabAction action;
final AnimationController controller;
final int index;
final int total;
const _AnimatedAction({
required this.action,
required this.controller,
required this.index,
required this.total,
});
@override
Widget build(BuildContext context) {
final start = (index / total) * 0.4;
final end = math.min(1.0, start + 0.7);
final anim = CurvedAnimation(
parent: controller,
curve: Interval(start, end, curve: Curves.easeOutCubic),
reverseCurve: Interval(start, end, curve: Curves.easeInCubic),
);
return AnimatedBuilder(
animation: anim,
builder: (ctx, child) {
final v = anim.value.clamp(0.0, 1.0);
if (v == 0) return const SizedBox.shrink();
return Opacity(
opacity: v,
child: Transform.translate(
offset: Offset(0, (1 - v) * 16),
child: child,
),
);
},
child: _ActionRow(action: action),
);
}
}
class _ActionRow extends StatelessWidget {
final _FabAction action;
const _ActionRow({required this.action});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Material(
color: scheme.surfaceContainerHighest,
elevation: 2,
shape: const StadiumBorder(),
child: InkWell(
customBorder: const StadiumBorder(),
onTap: action.onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
child: Text(
action.label,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
),
const SizedBox(width: 12),
Material(
color: scheme.secondaryContainer,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: action.onTap,
child: SizedBox(
width: 48,
height: 48,
child: Icon(action.icon, color: scheme.onSecondaryContainer),
),
),
),
],
);
}
}

View File

@@ -942,6 +942,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.5.3"
receive_sharing_intent:
dependency: "direct main"
description:
name: receive_sharing_intent
sha256: ec76056e4d258ad708e76d85591d933678625318e411564dcb9059048ca3a593
url: "https://pub.dev"
source: hosted
version: "1.8.1"
rxdart:
dependency: transitive
description:

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.9.2+14
version: 0.12.0+26
environment:
sdk: ^3.11.1
@@ -53,6 +53,7 @@ dependencies:
git: https://github.com/chenasraf/i18n
package_info_plus: ^9.0.1
mime: ^2.0.0
receive_sharing_intent: ^1.8.1
dev_dependencies:
flutter_test:

View File

@@ -80,7 +80,28 @@ class FakePhotoBoardController extends PhotoBoardController {
Future<void> deletePhoto(Photo photo) async {}
@override
Future<void> uploadPhotos(List<XFile> files) async {}
Future<void> uploadPhotos(List<XFile> files, {int? folderId}) async {
lastUploaded = files;
lastUploadFolderId = folderId;
}
String? lastCreatedFolderName;
List<XFile>? lastUploaded;
int? lastUploadFolderId;
@override
Future<PhotoFolder> createFolder(String name) async {
lastCreatedFolderName = name;
final folder = PhotoFolder(
id: 999,
houseId: houseId,
name: name,
sortOrder: 0,
createdAt: 0,
updatedAt: 0,
);
return folder;
}
}
/// A fake [NotesController] that does not touch any services.

View File

@@ -0,0 +1,47 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pantry/services/pending_note_share_service.dart';
void main() {
final service = PendingNoteShareService.instance;
setUp(() {
// Drain anything left over from a previous test.
service.takeForHouse(1);
service.takeForHouse(2);
});
test('takeForHouse returns null when there is no pending share', () {
expect(service.takeForHouse(1), isNull);
});
test('push then takeForHouse returns the share and clears it', () {
service.push(const PendingNoteShare(houseId: 1, content: 'hello'));
final taken = service.takeForHouse(1);
expect(taken, isNotNull);
expect(taken!.content, 'hello');
// Once taken, the pending slot is empty.
expect(service.pending, isNull);
expect(service.takeForHouse(1), isNull);
});
test('takeForHouse for a different house returns null and keeps share', () {
service.push(const PendingNoteShare(houseId: 1, content: 'hello'));
expect(service.takeForHouse(2), isNull);
expect(service.pending, isNotNull);
expect(service.pending!.houseId, 1);
});
test('push notifies listeners', () {
var notifications = 0;
void listener() => notifications++;
service.addListener(listener);
service.push(const PendingNoteShare(houseId: 1, content: 'hi'));
service.removeListener(listener);
expect(notifications, 1);
});
}

View File

@@ -0,0 +1,71 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pantry/services/pending_photo_share_service.dart';
void main() {
final service = PendingPhotoShareService.instance;
// Singleton — drain anything left from a previous test before each case so
// tests are order-independent.
setUp(() {
service.takeForHouse(1);
service.takeForHouse(2);
service.takeForHouse(3);
});
test('takeForHouse returns empty list when nothing is queued', () {
expect(service.takeForHouse(1), isEmpty);
});
test('enqueue makes the share available to the matching house', () {
service.enqueue(
const PendingPhotoShare(houseId: 1, folderId: null, paths: ['/a.jpg']),
);
final taken = service.takeForHouse(1);
expect(taken, hasLength(1));
expect(taken.first.houseId, 1);
expect(taken.first.paths, ['/a.jpg']);
});
test('takeForHouse drains only entries for the requested house', () {
service.enqueue(
const PendingPhotoShare(houseId: 1, folderId: null, paths: ['/a.jpg']),
);
service.enqueue(
const PendingPhotoShare(houseId: 2, folderId: 7, paths: ['/b.jpg']),
);
final fromHouse1 = service.takeForHouse(1);
expect(fromHouse1, hasLength(1));
expect(fromHouse1.first.houseId, 1);
// House 2's entry is still there until claimed.
final fromHouse2 = service.takeForHouse(2);
expect(fromHouse2, hasLength(1));
expect(fromHouse2.first.folderId, 7);
expect(fromHouse2.first.paths, ['/b.jpg']);
});
test('takeForHouse removes consumed entries (idempotent on second call)', () {
service.enqueue(
const PendingPhotoShare(houseId: 1, folderId: null, paths: ['/a.jpg']),
);
expect(service.takeForHouse(1), hasLength(1));
expect(service.takeForHouse(1), isEmpty);
});
test('enqueue notifies listeners', () {
var notifications = 0;
void listener() => notifications++;
service.addListener(listener);
service.enqueue(
const PendingPhotoShare(houseId: 1, folderId: null, paths: ['/a.jpg']),
);
service.removeListener(listener);
expect(notifications, 1);
});
}

View File

@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pantry/models/checklist.dart';
import 'package:pantry/widgets/checklist_selector.dart';
import '../helpers/test_app.dart';
import '../helpers/test_models.dart';
void main() {
ChecklistList listA() =>
makeChecklistList(id: 1, name: 'Groceries', icon: 'cart');
ChecklistList listB() =>
makeChecklistList(id: 2, name: 'Hardware', icon: 'wrench');
testWidgets('renders current list name in the closed state', (tester) async {
final a = listA();
final b = listB();
await tester.pumpWidget(
wrapForTest(
ChecklistSelector(
lists: [a, b],
currentList: a,
onSelected: (_) {},
onCreateNew: () {},
),
),
);
expect(find.text('Groceries'), findsOneWidget);
expect(find.text('Hardware'), findsNothing);
});
testWidgets('opening the dropdown shows all lists plus "+ New list"', (
tester,
) async {
final a = listA();
final b = listB();
await tester.pumpWidget(
wrapForTest(
ChecklistSelector(
lists: [a, b],
currentList: a,
onSelected: (_) {},
onCreateNew: () {},
),
),
);
await tester.tap(find.byType(DropdownButtonFormField<int>));
await tester.pumpAndSettle();
// Each list shows in the open menu in addition to the closed-state copy.
expect(find.text('Groceries'), findsNWidgets(2));
expect(find.text('Hardware'), findsOneWidget);
expect(find.text('New list'), findsOneWidget);
expect(find.byIcon(Icons.add), findsOneWidget);
});
testWidgets('selecting a different list invokes onSelected', (tester) async {
final a = listA();
final b = listB();
ChecklistList? selected;
var createNewCalls = 0;
await tester.pumpWidget(
wrapForTest(
ChecklistSelector(
lists: [a, b],
currentList: a,
onSelected: (l) => selected = l,
onCreateNew: () => createNewCalls++,
),
),
);
await tester.tap(find.byType(DropdownButtonFormField<int>));
await tester.pumpAndSettle();
await tester.tap(find.text('Hardware'));
await tester.pumpAndSettle();
expect(selected?.id, 2);
expect(createNewCalls, 0);
});
testWidgets('selecting "+ New list" invokes onCreateNew and not onSelected', (
tester,
) async {
final a = listA();
final b = listB();
ChecklistList? selected;
var createNewCalls = 0;
await tester.pumpWidget(
wrapForTest(
ChecklistSelector(
lists: [a, b],
currentList: a,
onSelected: (l) => selected = l,
onCreateNew: () => createNewCalls++,
),
),
);
await tester.tap(find.byType(DropdownButtonFormField<int>));
await tester.pumpAndSettle();
await tester.tap(find.text('New list'));
await tester.pumpAndSettle();
expect(createNewCalls, 1);
expect(selected, isNull);
});
testWidgets(
'after tapping "+ New list" the closed state still shows the current list',
(tester) async {
final a = listA();
final b = listB();
await tester.pumpWidget(
wrapForTest(
ChecklistSelector(
lists: [a, b],
currentList: a,
onSelected: (_) {},
onCreateNew: () {},
),
),
);
await tester.tap(find.byType(DropdownButtonFormField<int>));
await tester.pumpAndSettle();
await tester.tap(find.text('New list'));
await tester.pumpAndSettle();
// Sentinel must not leak into the closed-state label.
expect(find.text('New list'), findsNothing);
expect(find.text('Groceries'), findsOneWidget);
},
);
}

View File

@@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pantry/widgets/photo_add_button.dart';
import '../helpers/fakes.dart';
import '../helpers/test_app.dart';
void main() {
testWidgets('renders a single main FAB closed by default', (tester) async {
final controller = FakePhotoBoardController();
await tester.pumpWidget(
wrapForTest(PhotoAddButton(controller: controller)),
);
expect(find.byType(FloatingActionButton), findsOneWidget);
// None of the action labels should be visible while closed.
expect(find.text('Upload photos'), findsNothing);
expect(find.text('Take photo'), findsNothing);
expect(find.text('New folder'), findsNothing);
});
testWidgets('tapping the FAB opens the menu with all three actions', (
tester,
) async {
final controller = FakePhotoBoardController();
await tester.pumpWidget(
wrapForTest(PhotoAddButton(controller: controller)),
);
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
expect(find.text('Upload photos'), findsOneWidget);
expect(find.text('Take photo'), findsOneWidget);
expect(find.text('New folder'), findsOneWidget);
expect(find.byIcon(Icons.add_photo_alternate), findsOneWidget);
expect(find.byIcon(Icons.camera_alt), findsOneWidget);
expect(find.byIcon(Icons.create_new_folder), findsOneWidget);
});
testWidgets('tapping the FAB a second time closes the menu', (tester) async {
final controller = FakePhotoBoardController();
await tester.pumpWidget(
wrapForTest(PhotoAddButton(controller: controller)),
);
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
expect(find.text('Take photo'), findsOneWidget);
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
expect(find.text('Take photo'), findsNothing);
});
testWidgets('tapping "New folder" opens the create-folder dialog', (
tester,
) async {
final controller = FakePhotoBoardController();
await tester.pumpWidget(
wrapForTest(PhotoAddButton(controller: controller)),
);
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
await tester.tap(find.text('New folder'));
await tester.pumpAndSettle();
// Dialog field label + the action buttons confirm the dialog opened.
expect(find.text('Folder name'), findsOneWidget);
expect(find.widgetWithText(FilledButton, 'Save'), findsOneWidget);
expect(find.widgetWithText(TextButton, 'Cancel'), findsOneWidget);
});
testWidgets(
'submitting the create-folder dialog calls controller.createFolder',
(tester) async {
final controller = FakePhotoBoardController();
await tester.pumpWidget(
wrapForTest(PhotoAddButton(controller: controller)),
);
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
await tester.tap(find.text('New folder'));
await tester.pumpAndSettle();
await tester.enterText(
find.widgetWithText(TextField, 'Folder name'),
'Vacation',
);
await tester.tap(find.widgetWithText(FilledButton, 'Save'));
await tester.pumpAndSettle();
expect(controller.lastCreatedFolderName, 'Vacation');
},
);
testWidgets('empty folder name does not invoke createFolder', (tester) async {
final controller = FakePhotoBoardController();
await tester.pumpWidget(
wrapForTest(PhotoAddButton(controller: controller)),
);
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
await tester.tap(find.text('New folder'));
await tester.pumpAndSettle();
// Don't type anything, just submit.
await tester.tap(find.widgetWithText(FilledButton, 'Save'));
await tester.pumpAndSettle();
expect(controller.lastCreatedFolderName, isNull);
});
}