25 Commits

Author SHA1 Message Date
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
github-actions[bot]
24baeda80f chore(master): release 0.9.2 2026-04-19 11:07:27 +03:00
9d4c8327b0 build: reproducible build
Release-As: 0.9.2
2026-04-19 11:05:35 +03:00
github-actions[bot]
42125f89eb chore(master): release 0.9.1 2026-04-18 23:34:25 +03:00
7d0c7932ea build: add abi split
Release-As: 0.9.1
2026-04-18 23:31:14 +03:00
6f9f40a061 build: auto send to review 2026-04-18 22:42:26 +03:00
41bec3b656 build: add ios encryption compliance 2026-04-18 22:40:48 +03:00
8ba765e3be build: update ios release changes meta 2026-04-18 11:44:41 +03:00
github-actions[bot]
6cdb74a391 chore(master): release 0.9.0 2026-04-18 11:40:09 +03:00
eb797dd0e8 feat: add search in lists 2026-04-18 11:03:51 +03:00
7243e43bbb feat: allow uploading list item image 2026-04-18 10:37:47 +03:00
82966695b4 build: add debug build variance 2026-04-17 17:21:05 +03:00
322b3e29fa chore: update gitignore 2026-04-16 18:18:22 +03:00
634ac0be6b chore: fix ios release task 2026-04-16 18:16:10 +03:00
github-actions[bot]
2852e3ecf5 chore(master): release 0.8.0 2026-04-16 18:07:46 +03:00
179c6d781c feat: add more icons to categories 2026-04-16 12:03:02 +03:00
daac6f56fd chore: cleanups 2026-04-16 11:44:56 +03:00
a447fe1c8a feat: allow adding one-off list items 2026-04-16 11:42:47 +03:00
d73fa03a25 chore: update store metadata 2026-04-15 23:49:18 +03:00
github-actions[bot]
cb7133fcd7 chore(master): release 0.7.1 2026-04-14 14:29:30 +03:00
64af382f10 fix: about urls not opening 2026-04-14 13:47:39 +03:00
a535c6b49a chore: update icons 2026-04-14 13:39:32 +03:00
c15ad85d67 chore: fix android store icon size 2026-04-14 13:15:55 +03:00
d474663d44 build: ios metadata 2026-04-14 12:37:28 +03:00
91 changed files with 1177 additions and 99 deletions

View File

@@ -1 +1 @@
3.41.4
3.41.7

View File

@@ -21,7 +21,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: "3.41.7"
cache: true
- name: Cache pub dependencies
@@ -54,7 +54,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: "3.41.7"
cache: true
- name: Cache pub dependencies
@@ -109,7 +109,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: "3.41.7"
cache: true
- name: Cache pub dependencies
@@ -126,6 +126,9 @@ jobs:
- 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 +141,36 @@ 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 --obfuscate --split-debug-info=build/debug-info-apk --dart-define-from-file=.env
- name: Build App Bundle
run: flutter build appbundle --release --obfuscate --split-debug-info=build/debug-info-aab --dart-define-from-file=.env
- 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
@@ -175,7 +191,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: "3.41.7"
cache: true
- name: Cache pub dependencies

5
.gitignore vendored
View File

@@ -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

View File

@@ -1,5 +1,49 @@
# Changelog
## [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)

View File

@@ -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)"
@@ -120,6 +121,10 @@ test-coverage:
android-build-apk:
flutter build apk --release --obfuscate --split-debug-info=build/debug-info-apk --dart-define-from-file=.env
.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
.PHONY: android-install
android-install: android-build-apk
flutter install
@@ -241,11 +246,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:

View File

@@ -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 = "../.."
}

View File

@@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View 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

View File

@@ -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")

View File

@@ -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

View File

@@ -0,0 +1,2 @@
Bug Fixes
- about urls not opening

View File

@@ -0,0 +1,3 @@
Features
- add more icons to categories
- allow adding one-off list items

View File

@@ -0,0 +1,3 @@
Features
- add search in lists
- allow uploading list item image

View File

@@ -0,0 +1,2 @@
Build System
- add abi split

View File

@@ -0,0 +1,2 @@
Build System
- reproducible build

View File

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

View File

@@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1 +1 @@
Manage your household with your Nextcloud — lists, photos & notes.
Manage your household — shared lists, photos & notes on your own server.

View File

@@ -1 +1 @@
Nextcloud Pantry
Pantry for Nextcloud

View File

@@ -0,0 +1 @@
2026 Chen Asraf

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,2 @@
Bug Fixes
- about urls not opening

View File

@@ -0,0 +1,3 @@
Features
- add more icons to categories
- allow adding one-off list items

View File

@@ -0,0 +1,3 @@
Features
- add search in lists
- allow uploading list item image

View File

@@ -0,0 +1,2 @@
Build System
- add abi split

View File

@@ -0,0 +1,2 @@
Build System
- reproducible build

View File

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

View File

@@ -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.

View File

@@ -1 +1 @@
nextcloud,household,checklist,shopping list,photos,notes,self-hosted
nextcloud, checklist, todo, shopping list, notes, self-hosted, household

View File

@@ -1 +1 @@
https://casraf.dev
https://github.com/chenasraf/pantry-flutter

View File

@@ -1 +1 @@
Nextcloud Pantry
Pantry for Nextcloud

View File

@@ -0,0 +1 @@
Manage your household on your Nextcloud — shared lists, photos & notes.

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

View File

@@ -1 +1 @@
Household hub for Nextcloud
Home lists, photos & notes

View File

@@ -0,0 +1 @@
PRODUCTIVITY

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
EQq38!t9uA!@RdAkn6umJHo@nDh3ZZwM

View File

@@ -0,0 +1 @@
store-test

View File

@@ -0,0 +1 @@
casraf@pm.me

View File

@@ -0,0 +1 @@
Chen

View File

@@ -0,0 +1 @@
Asraf

View 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

View File

@@ -0,0 +1 @@
+972549107970

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -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>

View File

@@ -82,5 +82,7 @@
<string>fetch</string>
<string>processing</string>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>

View File

@@ -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,

View File

@@ -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?""",

View File

@@ -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?

View File

@@ -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""":

View File

@@ -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?"

View File

@@ -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.""",

View File

@@ -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?"

View File

@@ -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""":

View File

@@ -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 ?

View File

@@ -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""": """למחוק את הפריט?""",

View File

@@ -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: למחוק את הפריט?

View File

@@ -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,

View File

@@ -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),
);
}

View File

@@ -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,

View File

@@ -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),
);
}

View File

@@ -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: (_) {},
);
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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);

View File

@@ -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,
),
),
],
);
}

View File

@@ -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,
),
);
}
}

View File

@@ -735,7 +735,7 @@ packages:
source: hosted
version: "1.17.0"
mime:
dependency: transitive
dependency: "direct main"
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"

View File

@@ -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.3+15
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:

View File

@@ -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,

View File

@@ -1,3 +1,5 @@
// ignore_for_file: avoid_print
import 'dart:io';
/// Replaces YAML unicode escape sequences (\uXXXX, \xXX) with actual UTF-8

Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 B

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB