Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
768e78ace9 | ||
| d41d2b81be | |||
| 1f09e9d5aa | |||
|
|
1bb5b85b3e | ||
| 3e4051a846 | |||
|
|
24baeda80f | ||
| 9d4c8327b0 | |||
|
|
42125f89eb | ||
| 7d0c7932ea | |||
| 6f9f40a061 | |||
| 41bec3b656 | |||
| 8ba765e3be | |||
|
|
6cdb74a391 | ||
| eb797dd0e8 | |||
| 7243e43bbb | |||
| 82966695b4 | |||
| 322b3e29fa | |||
| 634ac0be6b | |||
|
|
2852e3ecf5 | ||
| 179c6d781c | |||
| daac6f56fd | |||
| a447fe1c8a | |||
| d73fa03a25 | |||
|
|
cb7133fcd7 | ||
| 64af382f10 | |||
| a535c6b49a | |||
| c15ad85d67 | |||
| d474663d44 |
@@ -1 +1 @@
|
||||
3.41.4
|
||||
3.41.7
|
||||
|
||||
72
.github/workflows/release.yml
vendored
@@ -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:
|
||||
channel: 'stable'
|
||||
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:
|
||||
channel: 'stable'
|
||||
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,7 +98,7 @@ 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
|
||||
|
||||
@@ -109,7 +116,7 @@ jobs:
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: ${{ needs.setup.outputs.flutter-version }}
|
||||
cache: true
|
||||
|
||||
- name: Cache pub dependencies
|
||||
@@ -123,8 +130,8 @@ 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
|
||||
|
||||
- name: Decode keystore
|
||||
run: echo ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | base64 -d > android/app/upload-keystore.jks
|
||||
@@ -138,23 +145,34 @@ jobs:
|
||||
storeFile=upload-keystore.jks
|
||||
EOF
|
||||
|
||||
- name: Build APK
|
||||
run: flutter build apk --release --obfuscate --split-debug-info=build/debug-info-apk --dart-define-from-file=.env
|
||||
- name: Print signing key SHA-256
|
||||
run: |
|
||||
keytool -list -v -keystore android/app/upload-keystore.jks \
|
||||
-alias ${{ secrets.ANDROID_KEY_ALIAS }} \
|
||||
-storepass ${{ secrets.ANDROID_STORE_PASSWORD }} \
|
||||
2>/dev/null | grep "SHA256:" | awk '{print $2}'
|
||||
|
||||
- name: Build split APKs
|
||||
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: |
|
||||
mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/pantry-${{ needs.release-please.outputs.version }}.apk
|
||||
mv build/app/outputs/bundle/release/app-release.aab build/app/outputs/bundle/release/pantry-${{ needs.release-please.outputs.version }}.aab
|
||||
VERSION=${{ needs.release-please.outputs.version }}
|
||||
APK_DIR=build/app/outputs/flutter-apk
|
||||
mv ${APK_DIR}/app-armeabi-v7a-release.apk ${APK_DIR}/pantry-${VERSION}-armeabi-v7a.apk
|
||||
mv ${APK_DIR}/app-arm64-v8a-release.apk ${APK_DIR}/pantry-${VERSION}-arm64-v8a.apk
|
||||
mv ${APK_DIR}/app-x86_64-release.apk ${APK_DIR}/pantry-${VERSION}-x86_64.apk
|
||||
mv build/app/outputs/bundle/release/app-release.aab build/app/outputs/bundle/release/pantry-${VERSION}.aab
|
||||
|
||||
- name: Upload APK to release
|
||||
- name: Upload APKs to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.release-please.outputs.tag_name }}
|
||||
files: |
|
||||
build/app/outputs/flutter-apk/pantry-${{ needs.release-please.outputs.version }}.apk
|
||||
build/app/outputs/flutter-apk/pantry-${{ needs.release-please.outputs.version }}-armeabi-v7a.apk
|
||||
build/app/outputs/flutter-apk/pantry-${{ needs.release-please.outputs.version }}-arm64-v8a.apk
|
||||
build/app/outputs/flutter-apk/pantry-${{ needs.release-please.outputs.version }}-x86_64.apk
|
||||
|
||||
- name: Upload App Bundle to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
@@ -164,7 +182,7 @@ 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
|
||||
|
||||
@@ -175,7 +193,7 @@ jobs:
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: ${{ needs.setup.outputs.flutter-version }}
|
||||
cache: true
|
||||
|
||||
- name: Cache pub dependencies
|
||||
@@ -198,12 +216,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
|
||||
|
||||
5
.gitignore
vendored
@@ -47,6 +47,8 @@ app.*.map.json
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.envrc
|
||||
/.flutter-flags.yml
|
||||
|
||||
# Android signing
|
||||
android/key.properties
|
||||
@@ -57,4 +59,5 @@ android/app/*.keystore
|
||||
fastlane/play-store-key.json
|
||||
fastlane/.image_hashes.json
|
||||
fastlane/report.xml
|
||||
/.envrc
|
||||
fastlane/Preview.html
|
||||
fastlane/metadata/ios/en-US/release_notes.txt
|
||||
|
||||
51
CHANGELOG.md
@@ -1,5 +1,56 @@
|
||||
# Changelog
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Build System
|
||||
|
||||
* reproducible build ([9d4c832](https://github.com/chenasraf/pantry-flutter/commit/9d4c8327b035eb0433d6d0a571af406ffca84f83))
|
||||
|
||||
## [0.9.1](https://github.com/chenasraf/pantry-flutter/compare/v0.9.0...v0.9.1) (2026-04-18)
|
||||
|
||||
|
||||
### Build System
|
||||
|
||||
* add abi split ([7d0c793](https://github.com/chenasraf/pantry-flutter/commit/7d0c7932ea9bf18d5ff391351f0058889abba5d8))
|
||||
|
||||
## [0.9.0](https://github.com/chenasraf/pantry-flutter/compare/v0.8.0...v0.9.0) (2026-04-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add search in lists ([eb797dd](https://github.com/chenasraf/pantry-flutter/commit/eb797dd0e87f7eb14576a3bdf7e77f9fe1c0cb09))
|
||||
* allow uploading list item image ([7243e43](https://github.com/chenasraf/pantry-flutter/commit/7243e43bbbfe8072327fc921ed2a4ffba228bd3f))
|
||||
|
||||
## [0.8.0](https://github.com/chenasraf/pantry-flutter/compare/v0.7.1...v0.8.0) (2026-04-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add more icons to categories ([179c6d7](https://github.com/chenasraf/pantry-flutter/commit/179c6d781c1342434608b9af88daa807795c9a46))
|
||||
* allow adding one-off list items ([a447fe1](https://github.com/chenasraf/pantry-flutter/commit/a447fe1c8a1d9de655c081015f287afeba75bee1))
|
||||
|
||||
## [0.7.1](https://github.com/chenasraf/pantry-flutter/compare/v0.7.0...v0.7.1) (2026-04-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* about urls not opening ([64af382](https://github.com/chenasraf/pantry-flutter/commit/64af382f10bd696f05a23d31ee8e04d746fc4b46))
|
||||
|
||||
## [0.7.0](https://github.com/chenasraf/pantry-flutter/compare/v0.6.0...v0.7.0) (2026-04-14)
|
||||
|
||||
|
||||
|
||||
35
Makefile
@@ -37,6 +37,7 @@ help:
|
||||
@echo " Building:"
|
||||
@echo " android-install Build APK and install on connected device"
|
||||
@echo " android-build-apk Build Android APK"
|
||||
@echo " android-build-apk-split Build Android split-per-ABI APKs"
|
||||
@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)"
|
||||
@@ -82,12 +83,10 @@ i18n-watch:
|
||||
# Development
|
||||
.PHONY: run
|
||||
run:
|
||||
flutter run --dart-define-from-file=.env
|
||||
|
||||
flutter run
|
||||
.PHONY: webapp-run
|
||||
webapp-run:
|
||||
open http://localhost:5111 & flutter run -d web-server --web-port=5111 --dart-define-from-file=.env
|
||||
|
||||
open http://localhost:5111 & flutter run -d web-server --web-port=5111
|
||||
.PHONY: format
|
||||
format:
|
||||
dart format .
|
||||
@@ -105,21 +104,20 @@ check:
|
||||
.PHONY: test
|
||||
test:
|
||||
ifdef FILES
|
||||
flutter test $(FILES) --dart-define-from-file=.env
|
||||
else
|
||||
flutter test --dart-define-from-file=.env
|
||||
endif
|
||||
flutter test $(FILES)else
|
||||
flutter testendif
|
||||
|
||||
.PHONY: test-coverage
|
||||
test-coverage:
|
||||
flutter test --coverage --dart-define-from-file=.env
|
||||
@echo "Coverage report generated at coverage/lcov.info"
|
||||
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
|
||||
.PHONY: android-install
|
||||
android-install: android-build-apk
|
||||
flutter install
|
||||
@@ -131,20 +129,17 @@ 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
|
||||
|
||||
flutter build web --release
|
||||
.PHONY: build-all
|
||||
build-all: android-build-apk android-build-aab web-build
|
||||
|
||||
@@ -241,11 +236,11 @@ endif
|
||||
.PHONY: icons
|
||||
icons:
|
||||
mkdir -p assets/icon
|
||||
rsvg-convert -h 1024 assets/logo_icon.svg > assets/icon/icon.png
|
||||
rsvg-convert -w 1024 -h 1024 assets/logo_icon_squircle.svg > assets/icon/icon.png
|
||||
rsvg-convert -w 1024 -h 1024 assets/logo_icon_square.svg > assets/icon/icon_ios.png
|
||||
rsvg-convert -w 1024 -h 1024 assets/logo_icon_foreground.svg > assets/icon/icon_foreground.png
|
||||
dart run flutter_launcher_icons
|
||||
cp assets/icon/icon_ios.png fastlane/metadata/android/en-US/images/icon.png
|
||||
rsvg-convert -w 512 -h 512 assets/logo_icon_squircle.svg > fastlane/metadata/android/en-US/images/icon.png
|
||||
|
||||
.PHONY: splash
|
||||
splash:
|
||||
|
||||
@@ -5,6 +5,7 @@ plugins {
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
import com.android.build.gradle.internal.api.ApkVariantOutputImpl
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
@@ -49,6 +50,10 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-dev"
|
||||
}
|
||||
release {
|
||||
signingConfig = if (signingConfigs.names.contains("release"))
|
||||
signingConfigs.getByName("release")
|
||||
@@ -58,6 +63,17 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
val abiCodes = mapOf("armeabi-v7a" to 1, "arm64-v8a" to 2, "x86_64" to 3)
|
||||
android.applicationVariants.configureEach {
|
||||
val variant = this
|
||||
variant.outputs.forEach { output ->
|
||||
val abiVersionCode = abiCodes[output.filters.find { it.filterType == "ABI" }?.identifier]
|
||||
if (abiVersionCode != null) {
|
||||
(output as ApkVariantOutputImpl).versionCodeOverride = variant.versionCode * 10 + abiVersionCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application android:label="Pantry Dev" tools:replace="android:label" />
|
||||
</manifest>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 29 KiB |
25
assets/logo_icon_squircle.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<clipPath id="squircle">
|
||||
<path d="
|
||||
M256,0
|
||||
C353,0 406,0 443,18
|
||||
C468,30 482,44 494,69
|
||||
C512,106 512,159 512,256
|
||||
C512,353 512,406 494,443
|
||||
C482,468 468,482 443,494
|
||||
C406,512 353,512 256,512
|
||||
C159,512 106,512 69,494
|
||||
C44,482 30,468 18,443
|
||||
C0,406 0,353 0,256
|
||||
C0,159 0,106 18,69
|
||||
C30,44 44,30 69,18
|
||||
C106,0 159,0 256,0
|
||||
Z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect width="512" height="512" fill="#0082C9" clip-path="url(#squircle)"/>
|
||||
<g transform="translate(106, 106) scale(12.5)">
|
||||
<path fill="#FFFFFF" d="M12,3L2,12H5V20H19V12H22L12,3M12,8.75A2.25,2.25 0 0,1 14.25,11A2.25,2.25 0 0,1 12,13.25A2.25,2.25 0 0,1 9.75,11A2.25,2.25 0 0,1 12,8.75M12,15C13.5,15 16.5,15.75 16.5,17.25V18H7.5V17.25C7.5,15.75 10.5,15 12,15Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 913 B |
@@ -1,3 +1,2 @@
|
||||
# The Deliverfile allows you to store various App Store Connect metadata
|
||||
# For more information, check out the docs
|
||||
# https://docs.fastlane.tools/actions/deliver/
|
||||
metadata_path("./fastlane/metadata/ios")
|
||||
screenshots_path("./fastlane/metadata/ios/screenshots")
|
||||
|
||||
@@ -122,6 +122,7 @@ platform :android do
|
||||
upload_to_play_store(
|
||||
skip_upload_aab: true,
|
||||
skip_upload_apk: true,
|
||||
skip_upload_changelogs: true,
|
||||
metadata_path: File.expand_path("metadata/android", __dir__),
|
||||
skip_upload_images: !changed,
|
||||
skip_upload_screenshots: !changed,
|
||||
@@ -169,24 +170,38 @@ platform :ios do
|
||||
ipa_path
|
||||
end
|
||||
|
||||
def sync_release_notes
|
||||
version_code = version_info[:build]
|
||||
changelog_file = File.expand_path("metadata/ios/en-US/changelogs/#{version_code}.txt", __dir__)
|
||||
notes = File.exist?(changelog_file) ? File.read(changelog_file).strip : changelog_notes
|
||||
release_notes_path = File.expand_path("metadata/ios/en-US/release_notes.txt", __dir__)
|
||||
File.write(release_notes_path, notes)
|
||||
UI.message("Synced release notes from build #{version_code} (#{notes.length} chars)")
|
||||
notes
|
||||
end
|
||||
|
||||
desc "Upload to TestFlight"
|
||||
lane :beta do
|
||||
notes = sync_release_notes
|
||||
upload_to_testflight(
|
||||
api_key: api_key,
|
||||
ipa: find_ipa,
|
||||
changelog: changelog_notes,
|
||||
changelog: notes,
|
||||
skip_waiting_for_build_processing: true,
|
||||
)
|
||||
end
|
||||
|
||||
desc "Upload to App Store"
|
||||
lane :release do
|
||||
sync_release_notes
|
||||
deliver(
|
||||
api_key: api_key,
|
||||
ipa: find_ipa,
|
||||
metadata_path: File.expand_path("metadata/ios", __dir__),
|
||||
screenshots_path: File.expand_path("metadata/ios/en-US/screenshots", __dir__),
|
||||
skip_screenshots: true,
|
||||
submit_for_review: false,
|
||||
submit_for_review: true,
|
||||
precheck_include_in_app_purchases: false,
|
||||
)
|
||||
end
|
||||
|
||||
@@ -195,9 +210,11 @@ platform :ios 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: false,
|
||||
precheck_include_in_app_purchases: false,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
2
fastlane/metadata/android/en-US/changelogs/10.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bug Fixes
|
||||
- about urls not opening
|
||||
3
fastlane/metadata/android/en-US/changelogs/11.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Features
|
||||
- add more icons to categories
|
||||
- allow adding one-off list items
|
||||
3
fastlane/metadata/android/en-US/changelogs/12.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Features
|
||||
- add search in lists
|
||||
- allow uploading list item image
|
||||
2
fastlane/metadata/android/en-US/changelogs/13.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Build System
|
||||
- add abi split
|
||||
2
fastlane/metadata/android/en-US/changelogs/14.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Build System
|
||||
- reproducible build
|
||||
2
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Build System
|
||||
- upgrade flutter version
|
||||
2
fastlane/metadata/android/en-US/changelogs/16.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Build System
|
||||
- remove apk obfuscation
|
||||
@@ -10,7 +10,7 @@ Upload and organize photos into folders. Add captions, drag to reorder, and brow
|
||||
Keep shared notes with your household. Color-code them, write in markdown, and pin the important stuff where everyone can see it.
|
||||
|
||||
* Your data, your server
|
||||
Pantry connects directly to your self-hosted Nextcloud instance. No accounts to create, no cloud services in between. Your data never leaves your server.
|
||||
Pantry connects directly to your Nextcloud instance. No accounts to create, no cloud services in between. Your data never leaves your server.
|
||||
|
||||
* Features
|
||||
- Shared checklists with categories, quantities, and recurrence
|
||||
@@ -20,6 +20,6 @@ Pantry connects directly to your self-hosted Nextcloud instance. No accounts to
|
||||
- Multi-select for bulk actions
|
||||
- Offline caching for fast loading
|
||||
- Material Design 3 with dark mode support
|
||||
- Nextcloud Login Flow v2 authentication
|
||||
- Secure login flow authentication
|
||||
|
||||
* Pantry requires a Nextcloud server with the Pantry app installed. Visit the project page for setup instructions.
|
||||
* Requires a Nextcloud server with the Pantry app installed. Visit the project page for setup instructions.
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
@@ -1 +1 @@
|
||||
Manage your household with your Nextcloud — lists, photos & notes.
|
||||
Manage your household — shared lists, photos & notes on your own server.
|
||||
@@ -1 +1 @@
|
||||
Nextcloud Pantry
|
||||
Pantry for Nextcloud
|
||||
1
fastlane/metadata/ios/copyright.txt
Normal file
@@ -0,0 +1 @@
|
||||
2026 Chen Asraf
|
||||
1
fastlane/metadata/ios/en-US/apple_tv_privacy_policy.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
2
fastlane/metadata/ios/en-US/changelogs/10.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bug Fixes
|
||||
- about urls not opening
|
||||
3
fastlane/metadata/ios/en-US/changelogs/11.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Features
|
||||
- add more icons to categories
|
||||
- allow adding one-off list items
|
||||
3
fastlane/metadata/ios/en-US/changelogs/12.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Features
|
||||
- add search in lists
|
||||
- allow uploading list item image
|
||||
2
fastlane/metadata/ios/en-US/changelogs/13.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Build System
|
||||
- add abi split
|
||||
2
fastlane/metadata/ios/en-US/changelogs/14.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Build System
|
||||
- reproducible build
|
||||
2
fastlane/metadata/ios/en-US/changelogs/15.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Build System
|
||||
- upgrade flutter version
|
||||
2
fastlane/metadata/ios/en-US/changelogs/16.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Build System
|
||||
- remove apk obfuscation
|
||||
@@ -10,7 +10,7 @@ Upload and organize photos into folders. Add captions, drag to reorder, and brow
|
||||
Keep shared notes with your household. Color-code them, write in markdown, and pin the important stuff where everyone can see it.
|
||||
|
||||
* Your data, your server
|
||||
Pantry connects directly to your self-hosted Nextcloud instance. No accounts to create, no cloud services in between. Your data never leaves your server.
|
||||
Pantry connects directly to your Nextcloud instance. No accounts to create, no cloud services in between. Your data never leaves your server.
|
||||
|
||||
* Features
|
||||
- Shared checklists with categories, quantities, and recurrence
|
||||
@@ -20,6 +20,6 @@ Pantry connects directly to your self-hosted Nextcloud instance. No accounts to
|
||||
- Multi-select for bulk actions
|
||||
- Offline caching for fast loading
|
||||
- Material Design 3 with dark mode support
|
||||
- Nextcloud Login Flow v2 authentication
|
||||
- Secure login flow authentication
|
||||
|
||||
* Pantry requires a Nextcloud server with the Pantry app installed. Visit the project page for setup instructions.
|
||||
* Requires a Nextcloud server with the Pantry app installed. Visit the project page for setup instructions.
|
||||
|
||||
@@ -1 +1 @@
|
||||
nextcloud,household,checklist,shopping list,photos,notes,self-hosted
|
||||
nextcloud, checklist, todo, shopping list, notes, self-hosted, household
|
||||
|
||||
@@ -1 +1 @@
|
||||
https://casraf.dev
|
||||
https://github.com/chenasraf/pantry-flutter
|
||||
|
||||
@@ -1 +1 @@
|
||||
Nextcloud Pantry
|
||||
Pantry for Nextcloud
|
||||
|
||||
1
fastlane/metadata/ios/en-US/promotional_text.txt
Normal file
@@ -0,0 +1 @@
|
||||
Manage your household on your Nextcloud — shared lists, photos & notes.
|
||||
|
After Width: | Height: | Size: 171 KiB |
BIN
fastlane/metadata/ios/en-US/screenshots/0_APP_IPHONE_67_0.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 685 KiB |
BIN
fastlane/metadata/ios/en-US/screenshots/1_APP_IPHONE_67_1.png
Normal file
|
After Width: | Height: | Size: 882 KiB |
|
After Width: | Height: | Size: 210 KiB |
BIN
fastlane/metadata/ios/en-US/screenshots/2_APP_IPHONE_67_2.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
@@ -1 +1 @@
|
||||
Household hub for Nextcloud
|
||||
Home lists, photos & notes
|
||||
1
fastlane/metadata/ios/primary_category.txt
Normal file
@@ -0,0 +1 @@
|
||||
PRODUCTIVITY
|
||||
1
fastlane/metadata/ios/primary_first_sub_category.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
fastlane/metadata/ios/primary_second_sub_category.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
EQq38!t9uA!@RdAkn6umJHo@nDh3ZZwM
|
||||
1
fastlane/metadata/ios/review_information/demo_user.txt
Normal file
@@ -0,0 +1 @@
|
||||
store-test
|
||||
@@ -0,0 +1 @@
|
||||
casraf@pm.me
|
||||
1
fastlane/metadata/ios/review_information/first_name.txt
Normal file
@@ -0,0 +1 @@
|
||||
Chen
|
||||
1
fastlane/metadata/ios/review_information/last_name.txt
Normal file
@@ -0,0 +1 @@
|
||||
Asraf
|
||||
3
fastlane/metadata/ios/review_information/notes.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
1. In the login screen, for the server, use: spider.casraf.dev and click "Connect"
|
||||
2. Use the given username/password to sign in to the Nextcloud website loaded in the in-app browser
|
||||
3. Click "Grant Access" to grant access to the app inside the in-app browser
|
||||
@@ -0,0 +1 @@
|
||||
+972549107970
|
||||
1
fastlane/metadata/ios/secondary_category.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
fastlane/metadata/ios/secondary_first_sub_category.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
fastlane/metadata/ios/secondary_second_sub_category.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<key>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>destination</key>
|
||||
<string>upload</string>
|
||||
<string>export</string>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>stripSwiftSymbols</key>
|
||||
|
||||
@@ -82,5 +82,7 @@
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
</array>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -117,7 +117,7 @@ class PantryAppState extends State<PantryApp> {
|
||||
textDirection: LocaleService.instance.textDirection,
|
||||
child: MaterialApp(
|
||||
key: ValueKey(locale),
|
||||
debugShowCheckedModeBanner: false,
|
||||
// debugShowCheckedModeBanner: false,
|
||||
navigatorKey: rootNavigatorKey,
|
||||
locale: locale,
|
||||
supportedLocales: supportedLocales,
|
||||
|
||||
@@ -607,6 +607,21 @@ class ChecklistsMessages {
|
||||
/// ```
|
||||
String get noItems => """No items in this list.""";
|
||||
|
||||
/// ```dart
|
||||
/// "No items match your search."
|
||||
/// ```
|
||||
String get noSearchResults => """No items match your search.""";
|
||||
|
||||
/// ```dart
|
||||
/// "Type to filter..."
|
||||
/// ```
|
||||
String get searchHint => """Type to filter...""";
|
||||
|
||||
/// ```dart
|
||||
/// "All"
|
||||
/// ```
|
||||
String get allCategories => """All""";
|
||||
|
||||
/// ```dart
|
||||
/// "Failed to load checklists."
|
||||
/// ```
|
||||
@@ -785,6 +800,37 @@ class ItemFormChecklistsMessages {
|
||||
/// ```
|
||||
String get repeat => """Repeat""";
|
||||
|
||||
/// ```dart
|
||||
/// "Once"
|
||||
/// ```
|
||||
String get once => """Once""";
|
||||
|
||||
/// ```dart
|
||||
/// "Delete this item once it is marked as done."
|
||||
/// ```
|
||||
String get onceDescription =>
|
||||
"""Delete this item once it is marked as done.""";
|
||||
|
||||
/// ```dart
|
||||
/// "Image"
|
||||
/// ```
|
||||
String get image => """Image""";
|
||||
|
||||
/// ```dart
|
||||
/// "Add image"
|
||||
/// ```
|
||||
String get addImage => """Add image""";
|
||||
|
||||
/// ```dart
|
||||
/// "Replace"
|
||||
/// ```
|
||||
String get replaceImage => """Replace""";
|
||||
|
||||
/// ```dart
|
||||
/// "Remove"
|
||||
/// ```
|
||||
String get removeImage => """Remove""";
|
||||
|
||||
/// ```dart
|
||||
/// "Failed to save item."
|
||||
/// ```
|
||||
@@ -1387,6 +1433,9 @@ Please complete login in your browser.""",
|
||||
"""checklists.categories""": """Categories""",
|
||||
"""checklists.noChecklists""": """No checklists yet.""",
|
||||
"""checklists.noItems""": """No items in this list.""",
|
||||
"""checklists.noSearchResults""": """No items match your search.""",
|
||||
"""checklists.searchHint""": """Type to filter...""",
|
||||
"""checklists.allCategories""": """All""",
|
||||
"""checklists.failedToLoad""": """Failed to load checklists.""",
|
||||
"""checklists.failedToLoadItems""": """Failed to load items.""",
|
||||
"""checklists.editItem""": """Edit item""",
|
||||
@@ -1421,6 +1470,13 @@ Please complete login in your browser.""",
|
||||
"""checklists.itemForm.categoryCreateFailed""":
|
||||
"""Failed to create category.""",
|
||||
"""checklists.itemForm.repeat""": """Repeat""",
|
||||
"""checklists.itemForm.once""": """Once""",
|
||||
"""checklists.itemForm.onceDescription""":
|
||||
"""Delete this item once it is marked as done.""",
|
||||
"""checklists.itemForm.image""": """Image""",
|
||||
"""checklists.itemForm.addImage""": """Add image""",
|
||||
"""checklists.itemForm.replaceImage""": """Replace""",
|
||||
"""checklists.itemForm.removeImage""": """Remove""",
|
||||
"""checklists.itemForm.saveFailed""": """Failed to save item.""",
|
||||
"""checklists.itemForm.deleteFailed""": """Failed to delete item.""",
|
||||
"""checklists.itemForm.deleteConfirm""": """Delete this item?""",
|
||||
|
||||
@@ -110,6 +110,9 @@ checklists:
|
||||
categories: Categories
|
||||
noChecklists: No checklists yet.
|
||||
noItems: No items in this list.
|
||||
noSearchResults: No items match your search.
|
||||
searchHint: Type to filter...
|
||||
allCategories: All
|
||||
failedToLoad: Failed to load checklists.
|
||||
failedToLoadItems: Failed to load items.
|
||||
completedCount(int count): "Completed ($count)"
|
||||
@@ -145,6 +148,12 @@ checklists:
|
||||
categoryCreated: Category created.
|
||||
categoryCreateFailed: Failed to create category.
|
||||
repeat: Repeat
|
||||
once: Once
|
||||
onceDescription: Delete this item once it is marked as done.
|
||||
image: Image
|
||||
addImage: Add image
|
||||
replaceImage: Replace
|
||||
removeImage: Remove
|
||||
saveFailed: Failed to save item.
|
||||
deleteFailed: Failed to delete item.
|
||||
deleteConfirm: Delete this item?
|
||||
|
||||
@@ -611,6 +611,21 @@ class ChecklistsMessagesDe extends ChecklistsMessages {
|
||||
/// ```
|
||||
String get noItems => """Keine Einträge in dieser Liste.""";
|
||||
|
||||
/// ```dart
|
||||
/// "Keine Einträge entsprechen Ihrer Suche."
|
||||
/// ```
|
||||
String get noSearchResults => """Keine Einträge entsprechen Ihrer Suche.""";
|
||||
|
||||
/// ```dart
|
||||
/// "Zum Filtern tippen..."
|
||||
/// ```
|
||||
String get searchHint => """Zum Filtern tippen...""";
|
||||
|
||||
/// ```dart
|
||||
/// "Alle"
|
||||
/// ```
|
||||
String get allCategories => """Alle""";
|
||||
|
||||
/// ```dart
|
||||
/// "Checklisten konnten nicht geladen werden."
|
||||
/// ```
|
||||
@@ -793,6 +808,37 @@ class ItemFormChecklistsMessagesDe extends ItemFormChecklistsMessages {
|
||||
/// ```
|
||||
String get repeat => """Wiederholen""";
|
||||
|
||||
/// ```dart
|
||||
/// "Einmalig"
|
||||
/// ```
|
||||
String get once => """Einmalig""";
|
||||
|
||||
/// ```dart
|
||||
/// "Diesen Eintrag löschen, sobald er als erledigt markiert ist."
|
||||
/// ```
|
||||
String get onceDescription =>
|
||||
"""Diesen Eintrag löschen, sobald er als erledigt markiert ist.""";
|
||||
|
||||
/// ```dart
|
||||
/// "Bild"
|
||||
/// ```
|
||||
String get image => """Bild""";
|
||||
|
||||
/// ```dart
|
||||
/// "Bild hinzufügen"
|
||||
/// ```
|
||||
String get addImage => """Bild hinzufügen""";
|
||||
|
||||
/// ```dart
|
||||
/// "Ersetzen"
|
||||
/// ```
|
||||
String get replaceImage => """Ersetzen""";
|
||||
|
||||
/// ```dart
|
||||
/// "Entfernen"
|
||||
/// ```
|
||||
String get removeImage => """Entfernen""";
|
||||
|
||||
/// ```dart
|
||||
/// "Eintrag konnte nicht gespeichert werden."
|
||||
/// ```
|
||||
@@ -1401,6 +1447,10 @@ Bitte melde dich in deinem Browser an.""",
|
||||
"""checklists.categories""": """Kategorien""",
|
||||
"""checklists.noChecklists""": """Noch keine Checklisten.""",
|
||||
"""checklists.noItems""": """Keine Einträge in dieser Liste.""",
|
||||
"""checklists.noSearchResults""":
|
||||
"""Keine Einträge entsprechen Ihrer Suche.""",
|
||||
"""checklists.searchHint""": """Zum Filtern tippen...""",
|
||||
"""checklists.allCategories""": """Alle""",
|
||||
"""checklists.failedToLoad""":
|
||||
"""Checklisten konnten nicht geladen werden.""",
|
||||
"""checklists.failedToLoadItems""":
|
||||
@@ -1437,6 +1487,13 @@ Bitte melde dich in deinem Browser an.""",
|
||||
"""checklists.itemForm.categoryCreateFailed""":
|
||||
"""Kategorie konnte nicht erstellt werden.""",
|
||||
"""checklists.itemForm.repeat""": """Wiederholen""",
|
||||
"""checklists.itemForm.once""": """Einmalig""",
|
||||
"""checklists.itemForm.onceDescription""":
|
||||
"""Diesen Eintrag löschen, sobald er als erledigt markiert ist.""",
|
||||
"""checklists.itemForm.image""": """Bild""",
|
||||
"""checklists.itemForm.addImage""": """Bild hinzufügen""",
|
||||
"""checklists.itemForm.replaceImage""": """Ersetzen""",
|
||||
"""checklists.itemForm.removeImage""": """Entfernen""",
|
||||
"""checklists.itemForm.saveFailed""":
|
||||
"""Eintrag konnte nicht gespeichert werden.""",
|
||||
"""checklists.itemForm.deleteFailed""":
|
||||
|
||||
@@ -110,6 +110,9 @@ checklists:
|
||||
categories: Kategorien
|
||||
noChecklists: Noch keine Checklisten.
|
||||
noItems: Keine Einträge in dieser Liste.
|
||||
noSearchResults: Keine Einträge entsprechen Ihrer Suche.
|
||||
searchHint: Zum Filtern tippen...
|
||||
allCategories: Alle
|
||||
failedToLoad: Checklisten konnten nicht geladen werden.
|
||||
failedToLoadItems: "Einträge konnten nicht geladen werden."
|
||||
completedCount(int count): "Erledigt ($count)"
|
||||
@@ -145,6 +148,12 @@ checklists:
|
||||
categoryCreated: Kategorie erstellt.
|
||||
categoryCreateFailed: Kategorie konnte nicht erstellt werden.
|
||||
repeat: Wiederholen
|
||||
once: Einmalig
|
||||
onceDescription: Diesen Eintrag löschen, sobald er als erledigt markiert ist.
|
||||
image: Bild
|
||||
addImage: Bild hinzufügen
|
||||
replaceImage: Ersetzen
|
||||
removeImage: Entfernen
|
||||
saveFailed: Eintrag konnte nicht gespeichert werden.
|
||||
deleteFailed: "Eintrag konnte nicht gelöscht werden."
|
||||
deleteConfirm: "Diesen Eintrag löschen?"
|
||||
|
||||
@@ -610,6 +610,21 @@ class ChecklistsMessagesEs extends ChecklistsMessages {
|
||||
/// ```
|
||||
String get noItems => """No hay artículos en esta lista.""";
|
||||
|
||||
/// ```dart
|
||||
/// "Ningún artículo coincide con tu búsqueda."
|
||||
/// ```
|
||||
String get noSearchResults => """Ningún artículo coincide con tu búsqueda.""";
|
||||
|
||||
/// ```dart
|
||||
/// "Escribe para filtrar..."
|
||||
/// ```
|
||||
String get searchHint => """Escribe para filtrar...""";
|
||||
|
||||
/// ```dart
|
||||
/// "Todos"
|
||||
/// ```
|
||||
String get allCategories => """Todos""";
|
||||
|
||||
/// ```dart
|
||||
/// "No se pudieron cargar las listas."
|
||||
/// ```
|
||||
@@ -791,6 +806,37 @@ class ItemFormChecklistsMessagesEs extends ItemFormChecklistsMessages {
|
||||
/// ```
|
||||
String get repeat => """Repetir""";
|
||||
|
||||
/// ```dart
|
||||
/// "Una vez"
|
||||
/// ```
|
||||
String get once => """Una vez""";
|
||||
|
||||
/// ```dart
|
||||
/// "Eliminar este artículo cuando se marque como hecho."
|
||||
/// ```
|
||||
String get onceDescription =>
|
||||
"""Eliminar este artículo cuando se marque como hecho.""";
|
||||
|
||||
/// ```dart
|
||||
/// "Imagen"
|
||||
/// ```
|
||||
String get image => """Imagen""";
|
||||
|
||||
/// ```dart
|
||||
/// "Agregar imagen"
|
||||
/// ```
|
||||
String get addImage => """Agregar imagen""";
|
||||
|
||||
/// ```dart
|
||||
/// "Reemplazar"
|
||||
/// ```
|
||||
String get replaceImage => """Reemplazar""";
|
||||
|
||||
/// ```dart
|
||||
/// "Eliminar"
|
||||
/// ```
|
||||
String get removeImage => """Eliminar""";
|
||||
|
||||
/// ```dart
|
||||
/// "No se pudo guardar el artículo."
|
||||
/// ```
|
||||
@@ -1396,6 +1442,10 @@ Por favor, completa el inicio de sesión en tu navegador.""",
|
||||
"""checklists.categories""": """Categorías""",
|
||||
"""checklists.noChecklists""": """Aún no hay listas.""",
|
||||
"""checklists.noItems""": """No hay artículos en esta lista.""",
|
||||
"""checklists.noSearchResults""":
|
||||
"""Ningún artículo coincide con tu búsqueda.""",
|
||||
"""checklists.searchHint""": """Escribe para filtrar...""",
|
||||
"""checklists.allCategories""": """Todos""",
|
||||
"""checklists.failedToLoad""": """No se pudieron cargar las listas.""",
|
||||
"""checklists.failedToLoadItems""":
|
||||
"""No se pudieron cargar los artículos.""",
|
||||
@@ -1431,6 +1481,13 @@ Por favor, completa el inicio de sesión en tu navegador.""",
|
||||
"""checklists.itemForm.categoryCreateFailed""":
|
||||
"""No se pudo crear la categoría.""",
|
||||
"""checklists.itemForm.repeat""": """Repetir""",
|
||||
"""checklists.itemForm.once""": """Una vez""",
|
||||
"""checklists.itemForm.onceDescription""":
|
||||
"""Eliminar este artículo cuando se marque como hecho.""",
|
||||
"""checklists.itemForm.image""": """Imagen""",
|
||||
"""checklists.itemForm.addImage""": """Agregar imagen""",
|
||||
"""checklists.itemForm.replaceImage""": """Reemplazar""",
|
||||
"""checklists.itemForm.removeImage""": """Eliminar""",
|
||||
"""checklists.itemForm.saveFailed""": """No se pudo guardar el artículo.""",
|
||||
"""checklists.itemForm.deleteFailed""":
|
||||
"""No se pudo eliminar el artículo.""",
|
||||
|
||||
@@ -110,6 +110,9 @@ checklists:
|
||||
categories: "Categorías"
|
||||
noChecklists: "Aún no hay listas."
|
||||
noItems: No hay artículos en esta lista.
|
||||
noSearchResults: Ningún artículo coincide con tu búsqueda.
|
||||
searchHint: Escribe para filtrar...
|
||||
allCategories: Todos
|
||||
failedToLoad: No se pudieron cargar las listas.
|
||||
failedToLoadItems: "No se pudieron cargar los artículos."
|
||||
completedCount(int count): "Completados ($count)"
|
||||
@@ -145,6 +148,12 @@ checklists:
|
||||
categoryCreated: "Categoría creada."
|
||||
categoryCreateFailed: "No se pudo crear la categoría."
|
||||
repeat: Repetir
|
||||
once: "Una vez"
|
||||
onceDescription: "Eliminar este artículo cuando se marque como hecho."
|
||||
image: Imagen
|
||||
addImage: Agregar imagen
|
||||
replaceImage: Reemplazar
|
||||
removeImage: Eliminar
|
||||
saveFailed: "No se pudo guardar el artículo."
|
||||
deleteFailed: "No se pudo eliminar el artículo."
|
||||
deleteConfirm: "¿Eliminar este artículo?"
|
||||
|
||||
@@ -610,6 +610,22 @@ class ChecklistsMessagesFr extends ChecklistsMessages {
|
||||
/// ```
|
||||
String get noItems => """Aucun article dans cette liste.""";
|
||||
|
||||
/// ```dart
|
||||
/// "Aucun article ne correspond à votre recherche."
|
||||
/// ```
|
||||
String get noSearchResults =>
|
||||
"""Aucun article ne correspond à votre recherche.""";
|
||||
|
||||
/// ```dart
|
||||
/// "Filtrer..."
|
||||
/// ```
|
||||
String get searchHint => """Filtrer...""";
|
||||
|
||||
/// ```dart
|
||||
/// "Tout"
|
||||
/// ```
|
||||
String get allCategories => """Tout""";
|
||||
|
||||
/// ```dart
|
||||
/// "Impossible de charger les listes."
|
||||
/// ```
|
||||
@@ -791,6 +807,37 @@ class ItemFormChecklistsMessagesFr extends ItemFormChecklistsMessages {
|
||||
/// ```
|
||||
String get repeat => """Répéter""";
|
||||
|
||||
/// ```dart
|
||||
/// "Une fois"
|
||||
/// ```
|
||||
String get once => """Une fois""";
|
||||
|
||||
/// ```dart
|
||||
/// "Supprimer cet article une fois qu'il est marqué comme fait."
|
||||
/// ```
|
||||
String get onceDescription =>
|
||||
"""Supprimer cet article une fois qu'il est marqué comme fait.""";
|
||||
|
||||
/// ```dart
|
||||
/// "Image"
|
||||
/// ```
|
||||
String get image => """Image""";
|
||||
|
||||
/// ```dart
|
||||
/// "Ajouter une image"
|
||||
/// ```
|
||||
String get addImage => """Ajouter une image""";
|
||||
|
||||
/// ```dart
|
||||
/// "Remplacer"
|
||||
/// ```
|
||||
String get replaceImage => """Remplacer""";
|
||||
|
||||
/// ```dart
|
||||
/// "Supprimer"
|
||||
/// ```
|
||||
String get removeImage => """Supprimer""";
|
||||
|
||||
/// ```dart
|
||||
/// "Impossible d'enregistrer l'article."
|
||||
/// ```
|
||||
@@ -1398,6 +1445,10 @@ Veuillez terminer la connexion dans votre navigateur.""",
|
||||
"""checklists.categories""": """Catégories""",
|
||||
"""checklists.noChecklists""": """Aucune liste pour le moment.""",
|
||||
"""checklists.noItems""": """Aucun article dans cette liste.""",
|
||||
"""checklists.noSearchResults""":
|
||||
"""Aucun article ne correspond à votre recherche.""",
|
||||
"""checklists.searchHint""": """Filtrer...""",
|
||||
"""checklists.allCategories""": """Tout""",
|
||||
"""checklists.failedToLoad""": """Impossible de charger les listes.""",
|
||||
"""checklists.failedToLoadItems""": """Impossible de charger les articles.""",
|
||||
"""checklists.editItem""": """Modifier l'article""",
|
||||
@@ -1432,6 +1483,13 @@ Veuillez terminer la connexion dans votre navigateur.""",
|
||||
"""checklists.itemForm.categoryCreateFailed""":
|
||||
"""Impossible de créer la catégorie.""",
|
||||
"""checklists.itemForm.repeat""": """Répéter""",
|
||||
"""checklists.itemForm.once""": """Une fois""",
|
||||
"""checklists.itemForm.onceDescription""":
|
||||
"""Supprimer cet article une fois qu'il est marqué comme fait.""",
|
||||
"""checklists.itemForm.image""": """Image""",
|
||||
"""checklists.itemForm.addImage""": """Ajouter une image""",
|
||||
"""checklists.itemForm.replaceImage""": """Remplacer""",
|
||||
"""checklists.itemForm.removeImage""": """Supprimer""",
|
||||
"""checklists.itemForm.saveFailed""":
|
||||
"""Impossible d'enregistrer l'article.""",
|
||||
"""checklists.itemForm.deleteFailed""":
|
||||
|
||||
@@ -110,6 +110,9 @@ checklists:
|
||||
categories: "Catégories"
|
||||
noChecklists: Aucune liste pour le moment.
|
||||
noItems: Aucun article dans cette liste.
|
||||
noSearchResults: Aucun article ne correspond à votre recherche.
|
||||
searchHint: Filtrer...
|
||||
allCategories: Tout
|
||||
failedToLoad: Impossible de charger les listes.
|
||||
failedToLoadItems: Impossible de charger les articles.
|
||||
completedCount(int count): "Terminés ($count)"
|
||||
@@ -145,6 +148,12 @@ checklists:
|
||||
categoryCreated: "Catégorie créée."
|
||||
categoryCreateFailed: "Impossible de créer la catégorie."
|
||||
repeat: "Répéter"
|
||||
once: "Une fois"
|
||||
onceDescription: "Supprimer cet article une fois qu'il est marqué comme fait."
|
||||
image: Image
|
||||
addImage: Ajouter une image
|
||||
replaceImage: Remplacer
|
||||
removeImage: Supprimer
|
||||
saveFailed: "Impossible d'enregistrer l'article."
|
||||
deleteFailed: Impossible de supprimer l'article.
|
||||
deleteConfirm: Supprimer cet article ?
|
||||
|
||||
@@ -608,6 +608,21 @@ class ChecklistsMessagesHe extends ChecklistsMessages {
|
||||
/// ```
|
||||
String get noItems => """אין פריטים ברשימה.""";
|
||||
|
||||
/// ```dart
|
||||
/// "אין פריטים תואמים לחיפוש."
|
||||
/// ```
|
||||
String get noSearchResults => """אין פריטים תואמים לחיפוש.""";
|
||||
|
||||
/// ```dart
|
||||
/// "הקלד לסינון..."
|
||||
/// ```
|
||||
String get searchHint => """הקלד לסינון...""";
|
||||
|
||||
/// ```dart
|
||||
/// "הכל"
|
||||
/// ```
|
||||
String get allCategories => """הכל""";
|
||||
|
||||
/// ```dart
|
||||
/// "טעינת הרשימות נכשלה."
|
||||
/// ```
|
||||
@@ -788,6 +803,36 @@ class ItemFormChecklistsMessagesHe extends ItemFormChecklistsMessages {
|
||||
/// ```
|
||||
String get repeat => """חזרה""";
|
||||
|
||||
/// ```dart
|
||||
/// "פעם אחת"
|
||||
/// ```
|
||||
String get once => """פעם אחת""";
|
||||
|
||||
/// ```dart
|
||||
/// "מחק את הפריט ברגע שהוא מסומן כבוצע."
|
||||
/// ```
|
||||
String get onceDescription => """מחק את הפריט ברגע שהוא מסומן כבוצע.""";
|
||||
|
||||
/// ```dart
|
||||
/// "תמונה"
|
||||
/// ```
|
||||
String get image => """תמונה""";
|
||||
|
||||
/// ```dart
|
||||
/// "הוסף תמונה"
|
||||
/// ```
|
||||
String get addImage => """הוסף תמונה""";
|
||||
|
||||
/// ```dart
|
||||
/// "החלף"
|
||||
/// ```
|
||||
String get replaceImage => """החלף""";
|
||||
|
||||
/// ```dart
|
||||
/// "הסר"
|
||||
/// ```
|
||||
String get removeImage => """הסר""";
|
||||
|
||||
/// ```dart
|
||||
/// "שמירת הפריט נכשלה."
|
||||
/// ```
|
||||
@@ -1389,6 +1434,9 @@ Map<String, String> get messagesHeMap => {
|
||||
"""checklists.categories""": """קטגוריות""",
|
||||
"""checklists.noChecklists""": """אין רשימות עדיין.""",
|
||||
"""checklists.noItems""": """אין פריטים ברשימה.""",
|
||||
"""checklists.noSearchResults""": """אין פריטים תואמים לחיפוש.""",
|
||||
"""checklists.searchHint""": """הקלד לסינון...""",
|
||||
"""checklists.allCategories""": """הכל""",
|
||||
"""checklists.failedToLoad""": """טעינת הרשימות נכשלה.""",
|
||||
"""checklists.failedToLoadItems""": """טעינת הפריטים נכשלה.""",
|
||||
"""checklists.editItem""": """ערוך פריט""",
|
||||
@@ -1421,6 +1469,13 @@ Map<String, String> get messagesHeMap => {
|
||||
"""checklists.itemForm.categoryCreated""": """הקטגוריה נוצרה.""",
|
||||
"""checklists.itemForm.categoryCreateFailed""": """יצירת הקטגוריה נכשלה.""",
|
||||
"""checklists.itemForm.repeat""": """חזרה""",
|
||||
"""checklists.itemForm.once""": """פעם אחת""",
|
||||
"""checklists.itemForm.onceDescription""":
|
||||
"""מחק את הפריט ברגע שהוא מסומן כבוצע.""",
|
||||
"""checklists.itemForm.image""": """תמונה""",
|
||||
"""checklists.itemForm.addImage""": """הוסף תמונה""",
|
||||
"""checklists.itemForm.replaceImage""": """החלף""",
|
||||
"""checklists.itemForm.removeImage""": """הסר""",
|
||||
"""checklists.itemForm.saveFailed""": """שמירת הפריט נכשלה.""",
|
||||
"""checklists.itemForm.deleteFailed""": """מחיקת הפריט נכשלה.""",
|
||||
"""checklists.itemForm.deleteConfirm""": """למחוק את הפריט?""",
|
||||
|
||||
@@ -110,6 +110,9 @@ checklists:
|
||||
categories: קטגוריות
|
||||
noChecklists: אין רשימות עדיין.
|
||||
noItems: אין פריטים ברשימה.
|
||||
noSearchResults: אין פריטים תואמים לחיפוש.
|
||||
searchHint: הקלד לסינון...
|
||||
allCategories: הכל
|
||||
failedToLoad: טעינת הרשימות נכשלה.
|
||||
failedToLoadItems: טעינת הפריטים נכשלה.
|
||||
completedCount(int count): "הושלמו ($count)"
|
||||
@@ -145,6 +148,12 @@ checklists:
|
||||
categoryCreated: הקטגוריה נוצרה.
|
||||
categoryCreateFailed: יצירת הקטגוריה נכשלה.
|
||||
repeat: חזרה
|
||||
once: פעם אחת
|
||||
onceDescription: מחק את הפריט ברגע שהוא מסומן כבוצע.
|
||||
image: תמונה
|
||||
addImage: הוסף תמונה
|
||||
replaceImage: החלף
|
||||
removeImage: הסר
|
||||
saveFailed: שמירת הפריט נכשלה.
|
||||
deleteFailed: מחיקת הפריט נכשלה.
|
||||
deleteConfirm: למחוק את הפריט?
|
||||
|
||||
@@ -54,6 +54,7 @@ class ListItem {
|
||||
final String? doneBy;
|
||||
final String? rrule;
|
||||
final bool repeatFromCompletion;
|
||||
final bool deleteOnDone;
|
||||
final int? nextDueAt;
|
||||
final int? imageFileId;
|
||||
final String? imageUploadedBy;
|
||||
@@ -73,6 +74,7 @@ class ListItem {
|
||||
this.doneBy,
|
||||
this.rrule,
|
||||
required this.repeatFromCompletion,
|
||||
required this.deleteOnDone,
|
||||
this.nextDueAt,
|
||||
this.imageFileId,
|
||||
this.imageUploadedBy,
|
||||
@@ -93,6 +95,7 @@ class ListItem {
|
||||
doneBy: json['doneBy'] as String?,
|
||||
rrule: json['rrule'] as String?,
|
||||
repeatFromCompletion: json['repeatFromCompletion'] as bool,
|
||||
deleteOnDone: json['deleteOnDone'] as bool? ?? false,
|
||||
nextDueAt: json['nextDueAt'] as int?,
|
||||
imageFileId: json['imageFileId'] as int?,
|
||||
imageUploadedBy: json['imageUploadedBy'] as String?,
|
||||
@@ -113,6 +116,7 @@ class ListItem {
|
||||
'doneBy': doneBy,
|
||||
'rrule': rrule,
|
||||
'repeatFromCompletion': repeatFromCompletion,
|
||||
'deleteOnDone': deleteOnDone,
|
||||
'nextDueAt': nextDueAt,
|
||||
'imageFileId': imageFileId,
|
||||
'imageUploadedBy': imageUploadedBy,
|
||||
@@ -133,6 +137,7 @@ class ListItem {
|
||||
doneBy: doneBy ?? this.doneBy,
|
||||
rrule: rrule,
|
||||
repeatFromCompletion: repeatFromCompletion,
|
||||
deleteOnDone: deleteOnDone,
|
||||
nextDueAt: nextDueAt,
|
||||
imageFileId: imageFileId,
|
||||
imageUploadedBy: imageUploadedBy,
|
||||
|
||||
@@ -46,11 +46,7 @@ class CategoryService {
|
||||
}) async {
|
||||
return ApiClient.instance.patch<Map<String, dynamic>, Category>(
|
||||
'/houses/$houseId/categories/$categoryId',
|
||||
body: {
|
||||
if (name != null) 'name': name,
|
||||
if (icon != null) 'icon': icon,
|
||||
if (color != null) 'color': color,
|
||||
},
|
||||
body: {'name': ?name, 'icon': ?icon, 'color': ?color},
|
||||
fromJson: (data) => Category.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ class ChecklistService {
|
||||
String? quantity,
|
||||
int? categoryId,
|
||||
String? rrule,
|
||||
bool? deleteOnDone,
|
||||
}) async {
|
||||
return ApiClient.instance.post<Map<String, dynamic>, ListItem>(
|
||||
'/houses/$houseId/lists/$listId/items',
|
||||
@@ -142,8 +143,9 @@ class ChecklistService {
|
||||
if (description != null && description.isNotEmpty)
|
||||
'description': description,
|
||||
if (quantity != null && quantity.isNotEmpty) 'quantity': quantity,
|
||||
if (categoryId != null) 'categoryId': categoryId,
|
||||
'categoryId': ?categoryId,
|
||||
if (rrule != null && rrule.isNotEmpty) 'rrule': rrule,
|
||||
'deleteOnDone': ?deleteOnDone,
|
||||
},
|
||||
fromJson: (data) => ListItem.fromJson(data),
|
||||
);
|
||||
@@ -160,18 +162,19 @@ class ChecklistService {
|
||||
bool clearCategory = false,
|
||||
String? rrule,
|
||||
bool? repeatFromCompletion,
|
||||
bool? deleteOnDone,
|
||||
}) async {
|
||||
return ApiClient.instance.patch<Map<String, dynamic>, ListItem>(
|
||||
'/houses/$houseId/lists/$listId/items/$itemId',
|
||||
body: {
|
||||
if (name != null) 'name': name,
|
||||
if (description != null) 'description': description,
|
||||
if (quantity != null) 'quantity': quantity,
|
||||
'name': ?name,
|
||||
'description': ?description,
|
||||
'quantity': ?quantity,
|
||||
if (clearCategory) 'categoryId': 0,
|
||||
if (!clearCategory && categoryId != null) 'categoryId': categoryId,
|
||||
if (rrule != null) 'rrule': rrule,
|
||||
if (repeatFromCompletion != null)
|
||||
'repeatFromCompletion': repeatFromCompletion,
|
||||
'rrule': ?rrule,
|
||||
'repeatFromCompletion': ?repeatFromCompletion,
|
||||
'deleteOnDone': ?deleteOnDone,
|
||||
},
|
||||
fromJson: (data) => ListItem.fromJson(data),
|
||||
);
|
||||
@@ -190,6 +193,30 @@ class ChecklistService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<ListItem> uploadItemImage(
|
||||
int houseId,
|
||||
int listId,
|
||||
int itemId, {
|
||||
required List<int> bytes,
|
||||
required String fileName,
|
||||
required String mimeType,
|
||||
}) async {
|
||||
return ApiClient.instance.uploadMultipart<Map<String, dynamic>, ListItem>(
|
||||
'/houses/$houseId/lists/$listId/items/$itemId/image',
|
||||
bytes: bytes,
|
||||
fileName: fileName,
|
||||
mimeType: mimeType,
|
||||
fieldName: 'image',
|
||||
fromJson: (data) => ListItem.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteItemImage(int houseId, int listId, int itemId) async {
|
||||
await ApiClient.instance.delete(
|
||||
'/houses/$houseId/lists/$listId/items/$itemId/image',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> reorderItems(
|
||||
int houseId,
|
||||
int listId,
|
||||
|
||||
@@ -51,11 +51,7 @@ class NoteService {
|
||||
}) async {
|
||||
return ApiClient.instance.post<Map<String, dynamic>, Note>(
|
||||
'/houses/$houseId/notes',
|
||||
body: {
|
||||
'title': title,
|
||||
if (content != null) 'content': content,
|
||||
if (color != null) 'color': color,
|
||||
},
|
||||
body: {'title': title, 'content': ?content, 'color': ?color},
|
||||
fromJson: (data) => Note.fromJson(data),
|
||||
);
|
||||
}
|
||||
@@ -69,11 +65,7 @@ class NoteService {
|
||||
}) async {
|
||||
return ApiClient.instance.patch<Map<String, dynamic>, Note>(
|
||||
'/houses/$houseId/notes/$noteId',
|
||||
body: {
|
||||
if (title != null) 'title': title,
|
||||
if (content != null) 'content': content,
|
||||
if (color != null) 'color': color,
|
||||
},
|
||||
body: {'title': ?title, 'content': ?content, 'color': ?color},
|
||||
fromJson: (data) => Note.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ class PhotoService {
|
||||
return ApiClient.instance.patch<Map<String, dynamic>, Photo>(
|
||||
'/houses/$houseId/photos/$photoId',
|
||||
body: {
|
||||
if (caption != null) 'caption': caption,
|
||||
'caption': ?caption,
|
||||
if (moveToRoot) 'folderId': 0,
|
||||
if (!moveToRoot && folderId != null) 'folderId': folderId,
|
||||
},
|
||||
@@ -155,7 +155,7 @@ class PhotoService {
|
||||
}) async {
|
||||
return ApiClient.instance.patch<Map<String, dynamic>, PhotoFolder>(
|
||||
'/houses/$houseId/photos/folders/$folderId',
|
||||
body: {if (name != null) 'name': name},
|
||||
body: {'name': ?name},
|
||||
fromJson: (data) => PhotoFolder.fromJson(data),
|
||||
);
|
||||
}
|
||||
@@ -203,10 +203,7 @@ class PhotoService {
|
||||
}) async {
|
||||
await ApiClient.instance.put<Map<String, dynamic>, void>(
|
||||
'/houses/$houseId/prefs',
|
||||
body: {
|
||||
if (photoSort != null) 'photoSort': photoSort,
|
||||
if (photoFoldersFirst != null) 'photoFoldersFirst': photoFoldersFirst,
|
||||
},
|
||||
body: {'photoSort': ?photoSort, 'photoFoldersFirst': ?photoFoldersFirst},
|
||||
fromJson: (_) {},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,41 @@ const categoryIconMap = <String, IconData>{
|
||||
'home': Icons.home,
|
||||
'leaf': Icons.eco,
|
||||
'pizza': Icons.local_pizza,
|
||||
'clipboard-check': Icons.assignment_turned_in,
|
||||
'clipboard-list': Icons.assignment,
|
||||
'format-list-checks': Icons.checklist,
|
||||
'cart': Icons.shopping_cart,
|
||||
'basket': Icons.shopping_basket,
|
||||
'star': Icons.star,
|
||||
'heart': Icons.favorite,
|
||||
'calendar': Icons.calendar_today,
|
||||
'bell': Icons.notifications,
|
||||
'flag': Icons.flag,
|
||||
'bookmark': Icons.bookmark,
|
||||
'pin': Icons.push_pin,
|
||||
'map-marker': Icons.place,
|
||||
'briefcase': Icons.work,
|
||||
'wrench': Icons.build,
|
||||
'silverware': Icons.restaurant,
|
||||
'gift': Icons.card_giftcard,
|
||||
'book': Icons.menu_book,
|
||||
'school': Icons.school,
|
||||
'palette': Icons.palette,
|
||||
'camera': Icons.camera_alt,
|
||||
'music': Icons.music_note,
|
||||
'gamepad': Icons.sports_esports,
|
||||
'run': Icons.directions_run,
|
||||
'dumbbell': Icons.fitness_center,
|
||||
'pill': Icons.medication,
|
||||
'paw': Icons.pets,
|
||||
'flower': Icons.local_florist,
|
||||
'tree': Icons.park,
|
||||
'broom': Icons.cleaning_services,
|
||||
'lightbulb': Icons.lightbulb,
|
||||
'package': Icons.inventory_2,
|
||||
'car': Icons.directions_car,
|
||||
'bike': Icons.directions_bike,
|
||||
'beach': Icons.beach_access,
|
||||
};
|
||||
|
||||
const defaultCategoryIcon = Icons.label;
|
||||
|
||||
@@ -26,10 +26,7 @@ class _AboutViewState extends State<AboutView> {
|
||||
}
|
||||
|
||||
Future<void> _launch(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -278,6 +278,7 @@ class ChecklistsController extends ChangeNotifier {
|
||||
String? quantity,
|
||||
int? categoryId,
|
||||
String? rrule,
|
||||
bool? deleteOnDone,
|
||||
}) async {
|
||||
final item = await _checklistService.createItem(
|
||||
houseId,
|
||||
@@ -287,6 +288,7 @@ class ChecklistsController extends ChangeNotifier {
|
||||
quantity: quantity,
|
||||
categoryId: categoryId,
|
||||
rrule: rrule,
|
||||
deleteOnDone: deleteOnDone,
|
||||
);
|
||||
_items.insert(0, item);
|
||||
_checklistService.cacheItems(_currentList!.id, List.of(_items));
|
||||
@@ -303,6 +305,7 @@ class ChecklistsController extends ChangeNotifier {
|
||||
bool clearCategory = false,
|
||||
String? rrule,
|
||||
bool? repeatFromCompletion,
|
||||
bool? deleteOnDone,
|
||||
}) async {
|
||||
final updated = await _checklistService.updateItem(
|
||||
houseId,
|
||||
@@ -315,6 +318,7 @@ class ChecklistsController extends ChangeNotifier {
|
||||
clearCategory: clearCategory,
|
||||
rrule: rrule,
|
||||
repeatFromCompletion: repeatFromCompletion,
|
||||
deleteOnDone: deleteOnDone,
|
||||
);
|
||||
final index = _items.indexWhere((i) => i.id == item.id);
|
||||
if (index != -1) {
|
||||
@@ -325,6 +329,59 @@ class ChecklistsController extends ChangeNotifier {
|
||||
return updated;
|
||||
}
|
||||
|
||||
Future<ListItem> uploadItemImage(
|
||||
ListItem item, {
|
||||
required List<int> bytes,
|
||||
required String fileName,
|
||||
required String mimeType,
|
||||
}) async {
|
||||
final updated = await _checklistService.uploadItemImage(
|
||||
houseId,
|
||||
item.listId,
|
||||
item.id,
|
||||
bytes: bytes,
|
||||
fileName: fileName,
|
||||
mimeType: mimeType,
|
||||
);
|
||||
final index = _items.indexWhere((i) => i.id == item.id);
|
||||
if (index != -1) {
|
||||
_items[index] = updated;
|
||||
_checklistService.cacheItems(_currentList!.id, List.of(_items));
|
||||
notifyListeners();
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
Future<void> deleteItemImage(ListItem item) async {
|
||||
await _checklistService.deleteItemImage(houseId, item.listId, item.id);
|
||||
final index = _items.indexWhere((i) => i.id == item.id);
|
||||
if (index != -1) {
|
||||
// Clear image fields locally
|
||||
_items[index] = ListItem(
|
||||
id: item.id,
|
||||
listId: item.listId,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
categoryId: item.categoryId,
|
||||
quantity: item.quantity,
|
||||
done: item.done,
|
||||
doneAt: item.doneAt,
|
||||
doneBy: item.doneBy,
|
||||
rrule: item.rrule,
|
||||
repeatFromCompletion: item.repeatFromCompletion,
|
||||
deleteOnDone: item.deleteOnDone,
|
||||
nextDueAt: item.nextDueAt,
|
||||
imageFileId: null,
|
||||
imageUploadedBy: null,
|
||||
sortOrder: item.sortOrder,
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt,
|
||||
);
|
||||
_checklistService.cacheItems(_currentList!.id, List.of(_items));
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteItem(ListItem item) async {
|
||||
await _checklistService.deleteItem(houseId, item.listId, item.id);
|
||||
_items.removeWhere((i) => i.id == item.id);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pantry/i18n.dart';
|
||||
import 'package:pantry/models/category.dart' as models;
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pantry/models/checklist.dart';
|
||||
import 'package:pantry/utils/category_icons.dart';
|
||||
import 'package:pantry/utils/checklist_icons.dart';
|
||||
import 'package:pantry/widgets/checklist_selector.dart';
|
||||
import 'package:pantry/widgets/checklist_sort_button.dart';
|
||||
@@ -45,9 +47,60 @@ class _ChecklistsViewState extends State<ChecklistsView> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ChecklistsBody extends StatelessWidget {
|
||||
class _ChecklistsBody extends StatefulWidget {
|
||||
const _ChecklistsBody();
|
||||
|
||||
@override
|
||||
State<_ChecklistsBody> createState() => _ChecklistsBodyState();
|
||||
}
|
||||
|
||||
class _ChecklistsBodyState extends State<_ChecklistsBody> {
|
||||
bool _searchOpen = false;
|
||||
final _searchController = TextEditingController();
|
||||
final Set<int> _selectedCategoryIds = {};
|
||||
|
||||
String get _query => _searchController.text.trim().toLowerCase();
|
||||
|
||||
bool get _isFiltering =>
|
||||
_searchOpen && (_query.isNotEmpty || _selectedCategoryIds.isNotEmpty);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _toggleSearch() {
|
||||
setState(() {
|
||||
_searchOpen = !_searchOpen;
|
||||
if (!_searchOpen) {
|
||||
_searchController.clear();
|
||||
_selectedCategoryIds.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
List<ListItem> _filterItems(List<ListItem> items) {
|
||||
if (!_isFiltering) return items;
|
||||
|
||||
return items.where((item) {
|
||||
// Category filter
|
||||
if (_selectedCategoryIds.isNotEmpty) {
|
||||
if (!_selectedCategoryIds.contains(item.categoryId)) return false;
|
||||
}
|
||||
|
||||
// Text filter
|
||||
if (_query.isNotEmpty) {
|
||||
final nameMatch = item.name.toLowerCase().contains(_query);
|
||||
final descMatch =
|
||||
item.description?.toLowerCase().contains(_query) ?? false;
|
||||
if (!nameMatch && !descMatch) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = context.watch<ChecklistsController>();
|
||||
@@ -79,6 +132,8 @@ class _ChecklistsBody extends StatelessWidget {
|
||||
return Center(child: Text(m.checklists.noChecklists));
|
||||
}
|
||||
|
||||
final filteredItems = _filterItems(controller.items);
|
||||
|
||||
Widget itemsArea;
|
||||
if (controller.isLoading) {
|
||||
itemsArea = const Center(child: CircularProgressIndicator());
|
||||
@@ -102,7 +157,11 @@ class _ChecklistsBody extends StatelessWidget {
|
||||
} else {
|
||||
itemsArea = RefreshIndicator(
|
||||
onRefresh: controller.refresh,
|
||||
child: _ItemList(controller: controller),
|
||||
child: _ItemList(
|
||||
controller: controller,
|
||||
items: filteredItems,
|
||||
isFiltering: _isFiltering,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,12 +178,30 @@ class _ChecklistsBody extends StatelessWidget {
|
||||
onSelected: controller.selectList,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(_searchOpen ? Icons.search_off : Icons.search),
|
||||
onPressed: _toggleSearch,
|
||||
),
|
||||
ChecklistSortButton(
|
||||
currentSort: controller.sortBy,
|
||||
onSelected: controller.setSortBy,
|
||||
),
|
||||
],
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
alignment: Alignment.topCenter,
|
||||
child: _searchOpen
|
||||
? _SearchPanel(
|
||||
searchController: _searchController,
|
||||
selectedCategoryIds: _selectedCategoryIds,
|
||||
items: controller.items,
|
||||
categories: controller.categories,
|
||||
onChanged: () => setState(() {}),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
Expanded(child: itemsArea),
|
||||
],
|
||||
),
|
||||
@@ -151,21 +228,231 @@ class _ChecklistsBody extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ItemList extends StatelessWidget {
|
||||
final ChecklistsController controller;
|
||||
class _SearchPanel extends StatelessWidget {
|
||||
final TextEditingController searchController;
|
||||
final Set<int> selectedCategoryIds;
|
||||
final List<ListItem> items;
|
||||
final Map<int, models.Category> categories;
|
||||
final VoidCallback onChanged;
|
||||
|
||||
const _ItemList({required this.controller});
|
||||
const _SearchPanel({
|
||||
required this.searchController,
|
||||
required this.selectedCategoryIds,
|
||||
required this.items,
|
||||
required this.categories,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final unchecked = controller.items.where((i) => !i.done).toList();
|
||||
final checked = controller.items.where((i) => i.done).toList();
|
||||
final theme = Theme.of(context);
|
||||
|
||||
if (controller.items.isEmpty) {
|
||||
// Collect categories actually used in the current list, with counts
|
||||
final categoryCounts = <int, int>{};
|
||||
for (final item in items) {
|
||||
if (item.categoryId != null) {
|
||||
categoryCounts[item.categoryId!] =
|
||||
(categoryCounts[item.categoryId!] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by category sortOrder
|
||||
final usedCategories =
|
||||
categoryCounts.keys.where((id) => categories.containsKey(id)).toList()
|
||||
..sort(
|
||||
(a, b) =>
|
||||
categories[a]!.sortOrder.compareTo(categories[b]!.sortOrder),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: m.checklists.searchHint,
|
||||
prefixIcon: const Icon(Icons.search, size: 20),
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
suffixIcon: ListenableBuilder(
|
||||
listenable: searchController,
|
||||
builder: (_, _) => searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear, size: 18),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
onChanged();
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
onChanged: (_) => onChanged(),
|
||||
),
|
||||
if (usedCategories.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 36,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
_CategoryChip(
|
||||
label: m.checklists.allCategories,
|
||||
count: items.length,
|
||||
selected: selectedCategoryIds.isEmpty,
|
||||
color: theme.colorScheme.primary,
|
||||
onTap: () {
|
||||
selectedCategoryIds.clear();
|
||||
onChanged();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
...usedCategories.map((catId) {
|
||||
final cat = categories[catId]!;
|
||||
final count = categoryCounts[catId]!;
|
||||
final color = _parseColor(cat.color, theme);
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 6),
|
||||
child: _CategoryChip(
|
||||
icon: categoryIcon(cat.icon),
|
||||
label: cat.name,
|
||||
count: count,
|
||||
selected: selectedCategoryIds.contains(catId),
|
||||
color: color,
|
||||
onTap: () {
|
||||
if (selectedCategoryIds.contains(catId)) {
|
||||
selectedCategoryIds.remove(catId);
|
||||
} else {
|
||||
selectedCategoryIds.add(catId);
|
||||
}
|
||||
onChanged();
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Color _parseColor(String hex, ThemeData theme) {
|
||||
try {
|
||||
final value = int.parse(hex.replaceFirst('#', ''), radix: 16);
|
||||
return Color(value | 0xFF000000);
|
||||
} catch (_) {
|
||||
return theme.colorScheme.primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CategoryChip extends StatelessWidget {
|
||||
final IconData? icon;
|
||||
final String label;
|
||||
final int count;
|
||||
final bool selected;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _CategoryChip({
|
||||
this.icon,
|
||||
required this.label,
|
||||
required this.count,
|
||||
required this.selected,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final fgColor = selected ? color : theme.colorScheme.onSurfaceVariant;
|
||||
|
||||
return FilterChip(
|
||||
avatar: icon != null ? Icon(icon, size: 16, color: fgColor) : null,
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(child: Text(label)),
|
||||
const SizedBox(width: 6),
|
||||
_CountBadge(count: count, color: fgColor),
|
||||
],
|
||||
),
|
||||
selected: selected,
|
||||
onSelected: (_) => onTap(),
|
||||
selectedColor: color.withValues(alpha: 0.2),
|
||||
showCheckmark: false,
|
||||
labelStyle: TextStyle(fontSize: 12, color: selected ? color : null),
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CountBadge extends StatelessWidget {
|
||||
final int count;
|
||||
final Color color;
|
||||
|
||||
const _CountBadge({required this.count, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minWidth: 20),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.15),
|
||||
shape: count < 100 ? BoxShape.circle : BoxShape.rectangle,
|
||||
borderRadius: count >= 100 ? BorderRadius.circular(10) : null,
|
||||
),
|
||||
child: Text(
|
||||
'$count',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ItemList extends StatelessWidget {
|
||||
final ChecklistsController controller;
|
||||
final List<ListItem> items;
|
||||
final bool isFiltering;
|
||||
|
||||
const _ItemList({
|
||||
required this.controller,
|
||||
required this.items,
|
||||
required this.isFiltering,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final unchecked = items.where((i) => !i.done).toList();
|
||||
final checked = items.where((i) => i.done).toList();
|
||||
|
||||
if (items.isEmpty) {
|
||||
return ListView(
|
||||
children: [
|
||||
const SizedBox(height: 100),
|
||||
Center(child: Text(m.checklists.noItems)),
|
||||
Center(
|
||||
child: Text(
|
||||
isFiltering ? m.checklists.noSearchResults : m.checklists.noItems,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:pantry/i18n.dart';
|
||||
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/utils/text_direction.dart';
|
||||
import 'package:pantry/widgets/category_picker.dart';
|
||||
import 'package:pantry/widgets/recurrence_dialog.dart';
|
||||
@@ -27,11 +34,16 @@ class _ItemFormViewState extends State<ItemFormView> {
|
||||
int? _selectedCategoryId;
|
||||
String? _rrule;
|
||||
bool _repeatFromCompletion = false;
|
||||
bool _deleteOnDone = false;
|
||||
bool _saving = false;
|
||||
TextDirection _nameDir = TextDirection.ltr;
|
||||
TextDirection _descriptionDir = TextDirection.ltr;
|
||||
XFile? _pickedImage;
|
||||
bool _removeExistingImage = false;
|
||||
|
||||
bool get _isEditing => widget.item != null;
|
||||
bool get _hasExistingImage =>
|
||||
widget.item?.imageFileId != null && !_removeExistingImage;
|
||||
|
||||
List<models.Category> get _categories =>
|
||||
widget.controller.categories.values.toList()
|
||||
@@ -49,6 +61,7 @@ class _ItemFormViewState extends State<ItemFormView> {
|
||||
_selectedCategoryId = item?.categoryId;
|
||||
_rrule = item?.rrule;
|
||||
_repeatFromCompletion = item?.repeatFromCompletion ?? false;
|
||||
_deleteOnDone = item?.deleteOnDone ?? false;
|
||||
_nameDir = detectTextDirection(item?.name);
|
||||
_nameController.addListener(() {
|
||||
final dir = detectTextDirection(_nameController.text);
|
||||
@@ -75,27 +88,51 @@ class _ItemFormViewState extends State<ItemFormView> {
|
||||
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
final effectiveRrule = _deleteOnDone ? '' : (_rrule ?? '');
|
||||
final effectiveRepeatFromCompletion = _deleteOnDone
|
||||
? false
|
||||
: _repeatFromCompletion;
|
||||
ListItem savedItem;
|
||||
if (_isEditing) {
|
||||
final item = widget.item!;
|
||||
await widget.controller.updateItem(
|
||||
savedItem = await widget.controller.updateItem(
|
||||
item,
|
||||
name: name,
|
||||
description: _descriptionController.text.trim(),
|
||||
quantity: _quantityController.text.trim(),
|
||||
categoryId: _selectedCategoryId,
|
||||
clearCategory: _selectedCategoryId == null && item.categoryId != null,
|
||||
rrule: _rrule ?? '',
|
||||
repeatFromCompletion: _repeatFromCompletion,
|
||||
rrule: effectiveRrule,
|
||||
repeatFromCompletion: effectiveRepeatFromCompletion,
|
||||
deleteOnDone: _deleteOnDone,
|
||||
);
|
||||
} else {
|
||||
await widget.controller.addItem(
|
||||
savedItem = await widget.controller.addItem(
|
||||
name: name,
|
||||
description: _descriptionController.text.trim(),
|
||||
quantity: _quantityController.text.trim(),
|
||||
categoryId: _selectedCategoryId,
|
||||
rrule: _rrule,
|
||||
rrule: _deleteOnDone ? null : _rrule,
|
||||
deleteOnDone: _deleteOnDone,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle image changes after the item is saved
|
||||
if (_removeExistingImage && _pickedImage == null) {
|
||||
await widget.controller.deleteItemImage(savedItem);
|
||||
}
|
||||
if (_pickedImage != null) {
|
||||
final bytes = await _pickedImage!.readAsBytes();
|
||||
final mime =
|
||||
lookupMimeType(_pickedImage!.name) ?? 'application/octet-stream';
|
||||
await widget.controller.uploadItemImage(
|
||||
savedItem,
|
||||
bytes: bytes,
|
||||
fileName: _pickedImage!.name,
|
||||
mimeType: mime,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) Navigator.of(context).pop(true);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
@@ -173,25 +210,164 @@ class _ItemFormViewState extends State<ItemFormView> {
|
||||
setState(() => _selectedCategoryId = cat.id);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
RepeatButton(
|
||||
rrule: _rrule,
|
||||
onTap: () async {
|
||||
final result = await showRecurrenceDialog(
|
||||
context,
|
||||
initialRrule: _rrule,
|
||||
initialRepeatFromCompletion: _repeatFromCompletion,
|
||||
);
|
||||
if (result != null) {
|
||||
setState(() {
|
||||
_rrule = result.rrule;
|
||||
_repeatFromCompletion = result.repeatFromCompletion;
|
||||
});
|
||||
}
|
||||
},
|
||||
const SizedBox(height: 8),
|
||||
CheckboxListTile(
|
||||
value: _deleteOnDone,
|
||||
onChanged: (v) => setState(() => _deleteOnDone = v ?? false),
|
||||
title: Text(f.once),
|
||||
subtitle: Text(f.onceDescription),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsetsDirectional.zero,
|
||||
),
|
||||
if (!_deleteOnDone) ...[
|
||||
const SizedBox(height: 8),
|
||||
RepeatButton(
|
||||
rrule: _rrule,
|
||||
onTap: () async {
|
||||
final result = await showRecurrenceDialog(
|
||||
context,
|
||||
initialRrule: _rrule,
|
||||
initialRepeatFromCompletion: _repeatFromCompletion,
|
||||
);
|
||||
if (result != null) {
|
||||
setState(() {
|
||||
_rrule = result.rrule;
|
||||
_repeatFromCompletion = result.repeatFromCompletion;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Text(f.image, style: theme.textTheme.bodyMedium),
|
||||
const SizedBox(height: 8),
|
||||
_buildImageSection(theme),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageSection(ThemeData theme) {
|
||||
if (_pickedImage != null) {
|
||||
return _ImagePreviewTile(
|
||||
image: FileImage(File(_pickedImage!.path)),
|
||||
onRemove: () => setState(() {
|
||||
_pickedImage = null;
|
||||
if (!_isEditing) _removeExistingImage = false;
|
||||
}),
|
||||
onReplace: _pickImage,
|
||||
);
|
||||
}
|
||||
|
||||
if (_hasExistingImage) {
|
||||
final uri = ChecklistService.instance.itemImagePreviewUri(
|
||||
widget.controller.houseId,
|
||||
widget.item!.imageFileId!,
|
||||
widget.item!.imageUploadedBy ?? '',
|
||||
size: 256,
|
||||
);
|
||||
final headers = AuthService.instance.credentials?.basicAuthHeaders ?? {};
|
||||
return _ImagePreviewTile(
|
||||
image: CachedNetworkImageProvider(uri.toString(), headers: headers),
|
||||
onRemove: () => setState(() {
|
||||
_removeExistingImage = true;
|
||||
}),
|
||||
onReplace: _pickImage,
|
||||
);
|
||||
}
|
||||
|
||||
return OutlinedButton.icon(
|
||||
onPressed: _pickImage,
|
||||
icon: const Icon(Icons.add_photo_alternate_outlined),
|
||||
label: Text(m.checklists.itemForm.addImage),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickImage() async {
|
||||
final picker = ImagePicker();
|
||||
final file = await picker.pickImage(source: ImageSource.gallery);
|
||||
if (file != null) {
|
||||
setState(() {
|
||||
_pickedImage = file;
|
||||
_removeExistingImage = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ImagePreviewTile extends StatelessWidget {
|
||||
final ImageProvider image;
|
||||
final VoidCallback onRemove;
|
||||
final VoidCallback onReplace;
|
||||
|
||||
const _ImagePreviewTile({
|
||||
required this.image,
|
||||
required this.onRemove,
|
||||
required this.onReplace,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final f = m.checklists.itemForm;
|
||||
return Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image(
|
||||
image: image,
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_ImageActionButton(
|
||||
icon: Icons.swap_horiz,
|
||||
tooltip: f.replaceImage,
|
||||
onPressed: onReplace,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_ImageActionButton(
|
||||
icon: Icons.close,
|
||||
tooltip: f.removeImage,
|
||||
onPressed: onRemove,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ImageActionButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String tooltip;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _ImageActionButton({
|
||||
required this.icon,
|
||||
required this.tooltip,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.black54,
|
||||
shape: const CircleBorder(),
|
||||
child: IconButton(
|
||||
icon: Icon(icon, color: Colors.white, size: 20),
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
constraints: const BoxConstraints.tightFor(width: 36, height: 36),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,7 +735,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: pantry
|
||||
description: "Manage your household with your Nextcloud — lists, photos & notes."
|
||||
description: "Manage your household — shared lists, photos & notes on your own server."
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
@@ -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.7.0+9
|
||||
version: 0.9.4+16
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.1
|
||||
@@ -52,6 +52,7 @@ dependencies:
|
||||
i18n:
|
||||
git: https://github.com/chenasraf/i18n
|
||||
package_info_plus: ^9.0.1
|
||||
mime: ^2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -161,6 +161,7 @@ ListItem makeListItem({
|
||||
String? doneBy,
|
||||
String? rrule,
|
||||
bool repeatFromCompletion = false,
|
||||
bool deleteOnDone = false,
|
||||
int? nextDueAt,
|
||||
int? imageFileId,
|
||||
String? imageUploadedBy,
|
||||
@@ -179,6 +180,7 @@ ListItem makeListItem({
|
||||
doneBy: doneBy,
|
||||
rrule: rrule,
|
||||
repeatFromCompletion: repeatFromCompletion,
|
||||
deleteOnDone: deleteOnDone,
|
||||
nextDueAt: nextDueAt,
|
||||
imageFileId: imageFileId,
|
||||
imageUploadedBy: imageUploadedBy,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
/// Replaces YAML unicode escape sequences (\uXXXX, \xXX) with actual UTF-8
|
||||
|
||||
BIN
web/favicon.png
|
Before Width: | Height: | Size: 489 B After Width: | Height: | Size: 451 B |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |