mirror of
https://github.com/chenasraf/pantry-flutter.git
synced 2026-05-17 17:28:03 +00:00
feat: photos/notes multiselect
This commit is contained in:
138
Makefile
138
Makefile
@@ -4,10 +4,6 @@ VERSION := $(shell grep '^version:' pubspec.yaml | sed 's/version: *//;s/+.*//')
|
||||
# Get staged Dart files
|
||||
STAGED_DART_FILES := $(shell git diff --cached --name-only --diff-filter=ACM | grep '\.dart$$' 2>/dev/null)
|
||||
|
||||
# Check for staged files in subprojects
|
||||
STAGED_FUNCTIONS_FILES := $(shell git diff --cached --name-only --diff-filter=ACM | grep '^functions/' 2>/dev/null)
|
||||
STAGED_WEBSITE_FILES := $(shell git diff --cached --name-only --diff-filter=ACM | grep '^website/' 2>/dev/null)
|
||||
|
||||
# Default target
|
||||
.PHONY: help
|
||||
help:
|
||||
@@ -17,21 +13,7 @@ help:
|
||||
@echo " get Install dependencies"
|
||||
@echo " clean Clean build artifacts"
|
||||
@echo " install-precommit Install git pre-commit hook"
|
||||
@echo " sync-env Sync .env to iOS Secrets.xcconfig"
|
||||
@echo " pods Update CocoaPods repo and install pods"
|
||||
@echo " rules-deploy Deploy Firestore and Storage security rules"
|
||||
@echo ""
|
||||
@echo " Website:"
|
||||
@echo " website-install Install website dependencies"
|
||||
@echo " website-run Run website dev server"
|
||||
@echo " website-build Build website"
|
||||
@echo ""
|
||||
@echo " Functions:"
|
||||
@echo " functions-install Install functions dependencies"
|
||||
@echo " functions-build Build functions"
|
||||
@echo " functions-serve Run functions with emulators"
|
||||
@echo " functions-deploy Deploy functions to Firebase"
|
||||
@echo " functions-logs View functions logs"
|
||||
@echo ""
|
||||
@echo " i18n:"
|
||||
@echo " i18n Build i18n generated Dart code"
|
||||
@@ -63,6 +45,7 @@ help:
|
||||
@echo " android-install Build APK and install on connected device"
|
||||
@echo " android-build-apk Build Android APK"
|
||||
@echo " android-build-aab Build Android App Bundle"
|
||||
@echo " android-push Build APK and push to device via adb"
|
||||
@echo " ios-build Build iOS (no codesign)"
|
||||
@echo " web-build Build web app"
|
||||
@echo " build-all Build all platforms"
|
||||
@@ -78,10 +61,6 @@ help:
|
||||
@echo " android-deploy Build AAB and upload to Google Play (TRACK=internal|beta|production, STATUS=draft|completed)"
|
||||
@echo " android-promote Promote release between tracks (FROM=internal, TO=production, STATUS=draft|completed)"
|
||||
@echo " ios-deploy Build IPA and upload (DEST=testflight|appstore, default: testflight)"
|
||||
@echo " webapp-deploy Build and deploy Flutter web app"
|
||||
@echo " hosting-deploy Build and deploy marketing website"
|
||||
@echo " website-deploy Deploy website + functions"
|
||||
@echo " deploy-all Deploy all platforms (SKIP_WEBAPP=1 SKIP_IOS=1 SKIP_ANDROID=1 SKIP_WEBSITE=1 to skip)"
|
||||
|
||||
# Setup
|
||||
.PHONY: get
|
||||
@@ -149,21 +128,11 @@ precommit: format analyze
|
||||
echo "Re-staging formatted files..."; \
|
||||
git add $(STAGED_DART_FILES); \
|
||||
fi
|
||||
@if [ -n "$(STAGED_FUNCTIONS_FILES)" ]; then \
|
||||
echo "Running lint-staged for functions..."; \
|
||||
cd functions && pnpm lint-staged; \
|
||||
fi
|
||||
@if [ -n "$(STAGED_WEBSITE_FILES)" ]; then \
|
||||
echo "Running lint-staged for website..."; \
|
||||
cd website && pnpm lint-staged; \
|
||||
fi
|
||||
|
||||
# Full project commands
|
||||
.PHONY: format-all
|
||||
format-all:
|
||||
dart format .
|
||||
cd functions && pnpm prettier --write .
|
||||
cd website && pnpm prettier --write .
|
||||
|
||||
.PHONY: analyze-all
|
||||
analyze-all:
|
||||
@@ -197,6 +166,11 @@ android-build-apk:
|
||||
android-install: android-build-apk
|
||||
flutter install
|
||||
|
||||
.PHONY: android-push
|
||||
android-push: android-build-apk
|
||||
adb push build/app/outputs/flutter-apk/app-release.apk /sdcard/Download/pantry-$(VERSION).apk
|
||||
@echo "-> /sdcard/Download/pantry-$(VERSION).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
|
||||
@@ -216,20 +190,20 @@ build-all: android-build-apk android-build-aab web-build
|
||||
.PHONY: android-release-apk
|
||||
android-release-apk: android-build-apk
|
||||
mkdir -p build/release
|
||||
cp build/app/outputs/flutter-apk/app-release.apk build/release/retroachieved-$(VERSION).apk
|
||||
@echo "-> build/release/retroachieved-$(VERSION).apk"
|
||||
cp build/app/outputs/flutter-apk/app-release.apk build/release/pantry-$(VERSION).apk
|
||||
@echo "-> build/release/pantry-$(VERSION).apk"
|
||||
|
||||
.PHONY: android-release-aab
|
||||
android-release-aab: android-build-aab
|
||||
mkdir -p build/release
|
||||
cp build/app/outputs/bundle/release/app-release.aab build/release/retroachieved-$(VERSION).aab
|
||||
@echo "-> build/release/retroachieved-$(VERSION).aab"
|
||||
cp build/app/outputs/bundle/release/app-release.aab build/release/pantry-$(VERSION).aab
|
||||
@echo "-> build/release/pantry-$(VERSION).aab"
|
||||
|
||||
.PHONY: ios-release
|
||||
ios-release: ios-build
|
||||
mkdir -p build/release
|
||||
cp build/ios/ipa/*.ipa build/release/retroachieved-$(VERSION).ipa
|
||||
@echo "-> build/release/retroachieved-$(VERSION).ipa"
|
||||
cp build/ios/ipa/*.ipa build/release/pantry-$(VERSION).ipa
|
||||
@echo "-> build/release/pantry-$(VERSION).ipa"
|
||||
|
||||
.PHONY: android-upload
|
||||
android-upload:
|
||||
@@ -265,25 +239,12 @@ ios-deploy: ios-build ios-upload
|
||||
.PHONY: web-release
|
||||
web-release: web-build
|
||||
mkdir -p build/release
|
||||
cd build/web && zip -r ../release/retroachieved-$(VERSION)-web.zip .
|
||||
@echo "-> build/release/retroachieved-$(VERSION)-web.zip"
|
||||
cd build/web && zip -r ../release/pantry-$(VERSION)-web.zip .
|
||||
@echo "-> build/release/pantry-$(VERSION)-web.zip"
|
||||
|
||||
.PHONY: release-all
|
||||
release-all: build-clean android-release-apk android-release-aab web-release
|
||||
|
||||
# Environment sync
|
||||
.PHONY: sync-env
|
||||
sync-env:
|
||||
@echo "Syncing .env to iOS Secrets.xcconfig..."
|
||||
@if [ -f .env ]; then \
|
||||
echo "// Auto-generated from .env - DO NOT EDIT" > ios/Flutter/Secrets.xcconfig; \
|
||||
grep -E "^[A-Za-z_][A-Za-z0-9_]*=" .env >> ios/Flutter/Secrets.xcconfig; \
|
||||
echo "Secrets.xcconfig updated."; \
|
||||
else \
|
||||
echo "Error: .env file not found"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# CocoaPods
|
||||
.PHONY: pods
|
||||
pods:
|
||||
@@ -303,36 +264,6 @@ install-precommit:
|
||||
echo "Pre-commit hook already exists, skipping."; \
|
||||
fi
|
||||
|
||||
# Firebase
|
||||
.PHONY: rules-deploy
|
||||
rules-deploy:
|
||||
firebase deploy --only firestore:rules,storage
|
||||
|
||||
# Functions
|
||||
.PHONY: functions-install
|
||||
functions-install:
|
||||
cd functions && pnpm install
|
||||
|
||||
.PHONY: functions-build
|
||||
functions-build:
|
||||
cd functions && pnpm build
|
||||
|
||||
.PHONY: functions-serve
|
||||
functions-serve:
|
||||
cd functions && pnpm serve
|
||||
|
||||
.PHONY: functions-deploy
|
||||
functions-deploy:
|
||||
ifdef FN
|
||||
firebase deploy --only functions:$(FN)
|
||||
else
|
||||
firebase deploy --only functions
|
||||
endif
|
||||
|
||||
.PHONY: functions-logs
|
||||
functions-logs:
|
||||
firebase functions:log
|
||||
|
||||
# API
|
||||
.PHONY: fetch-openapi
|
||||
fetch-openapi:
|
||||
@@ -364,44 +295,3 @@ splash:
|
||||
mkdir -p assets/icon
|
||||
rsvg-convert -h 512 --page-width 1024 --page-height 1024 --top 256 --left 256 assets/logo_icon.svg > assets/icon/splash.png
|
||||
dart run flutter_native_splash:create
|
||||
|
||||
# Website
|
||||
.PHONY: website-install
|
||||
website-install:
|
||||
cd website && pnpm install
|
||||
|
||||
.PHONY: website-run
|
||||
website-run:
|
||||
cd website && pnpm dev
|
||||
|
||||
.PHONY: website-build
|
||||
website-build:
|
||||
cd website && pnpm build
|
||||
|
||||
# Hosting
|
||||
.PHONY: hosting-deploy
|
||||
hosting-deploy: website-build
|
||||
firebase deploy --only hosting:website
|
||||
|
||||
.PHONY: webapp-deploy
|
||||
webapp-deploy: web-build
|
||||
firebase deploy --only hosting:webapp
|
||||
|
||||
.PHONY: website-deploy
|
||||
website-deploy:
|
||||
firebase deploy --only hosting:website,functions
|
||||
|
||||
.PHONY: deploy-all
|
||||
deploy-all:
|
||||
ifndef SKIP_WEBAPP
|
||||
$(MAKE) webapp-deploy
|
||||
endif
|
||||
ifndef SKIP_IOS
|
||||
$(MAKE) ios-deploy
|
||||
endif
|
||||
ifndef SKIP_ANDROID
|
||||
$(MAKE) android-deploy STATUS=completed
|
||||
endif
|
||||
ifndef SKIP_WEBSITE
|
||||
$(MAKE) website-deploy
|
||||
endif
|
||||
|
||||
@@ -384,6 +384,12 @@ class NotesWallMessages {
|
||||
/// ```
|
||||
String get deleteConfirm => """Delete this note?""";
|
||||
|
||||
/// ```dart
|
||||
/// "Delete ${_plural(count, one: 'this note', many: '$count notes')}?"
|
||||
/// ```
|
||||
String deleteSelectedConfirm(int count) =>
|
||||
"""Delete ${_plural(count, one: 'this note', many: '$count notes')}?""";
|
||||
|
||||
/// ```dart
|
||||
/// "New note"
|
||||
/// ```
|
||||
@@ -470,6 +476,12 @@ class PhotoBoardMessages {
|
||||
/// ```
|
||||
String get deleteConfirm => """Delete this photo?""";
|
||||
|
||||
/// ```dart
|
||||
/// "Delete ${_plural(count, one: 'this photo', many: '$count photos')}?"
|
||||
/// ```
|
||||
String deleteSelectedConfirm(int count) =>
|
||||
"""Delete ${_plural(count, one: 'this photo', many: '$count photos')}?""";
|
||||
|
||||
/// ```dart
|
||||
/// "Delete folder"
|
||||
/// ```
|
||||
|
||||
@@ -64,6 +64,7 @@ notesWall:
|
||||
saveFailed: Failed to save note.
|
||||
deleteFailed: Failed to delete note.
|
||||
deleteConfirm: Delete this note?
|
||||
deleteSelectedConfirm(int count): "Delete ${_plural(count, one: 'this note', many: '$count notes')}?"
|
||||
newNote: New note
|
||||
editNote: Edit note
|
||||
title: Title
|
||||
@@ -82,6 +83,7 @@ photoBoard:
|
||||
uploadFailed: Failed to upload photo.
|
||||
deleteFailed: Failed to delete photo.
|
||||
deleteConfirm: Delete this photo?
|
||||
deleteSelectedConfirm(int count): "Delete ${_plural(count, one: 'this photo', many: '$count photos')}?"
|
||||
deleteFolder: Delete folder
|
||||
deleteFolderConfirm: Delete this folder?
|
||||
deleteFolderKeepPhotos: Move photos to root
|
||||
|
||||
@@ -23,6 +23,51 @@ class NotesController extends ChangeNotifier {
|
||||
String? _error;
|
||||
String? get error => _error;
|
||||
|
||||
// -- Selection --
|
||||
|
||||
bool _selectMode = false;
|
||||
bool get selectMode => _selectMode;
|
||||
|
||||
final Set<int> _selected = {};
|
||||
Set<int> get selected => _selected;
|
||||
|
||||
void toggleSelectMode() {
|
||||
_selectMode = !_selectMode;
|
||||
if (!_selectMode) _selected.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleSelection(int noteId) {
|
||||
if (_selected.contains(noteId)) {
|
||||
_selected.remove(noteId);
|
||||
} else {
|
||||
_selected.add(noteId);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearSelection() {
|
||||
_selected.clear();
|
||||
_selectMode = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> deleteSelected() async {
|
||||
final ids = Set<int>.from(_selected);
|
||||
for (final id in ids) {
|
||||
try {
|
||||
await _service.deleteNote(houseId, id);
|
||||
_notes.removeWhere((n) => n.id == id);
|
||||
} catch (e) {
|
||||
debugPrint('[NotesController] Failed to delete note $id: $e');
|
||||
}
|
||||
}
|
||||
_selected.clear();
|
||||
_selectMode = false;
|
||||
_service.cacheNotes(houseId, _notes);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// -- Drag reorder state --
|
||||
|
||||
int? _draggingId;
|
||||
|
||||
@@ -76,7 +76,16 @@ class _NotesWallBody extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
_SortButton(controller: controller),
|
||||
if (controller.selectMode)
|
||||
_NoteSelectionActions(controller: controller)
|
||||
else ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.checklist),
|
||||
tooltip: '',
|
||||
onPressed: controller.toggleSelectMode,
|
||||
),
|
||||
_SortButton(controller: controller),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -107,6 +116,57 @@ class _NotesWallBody extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _NoteSelectionActions extends StatelessWidget {
|
||||
final NotesController controller;
|
||||
|
||||
const _NoteSelectionActions({required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final count = controller.selected.length;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('$count', style: Theme.of(context).textTheme.titleSmall),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
tooltip: '',
|
||||
onPressed: count > 0 ? () => _confirmDelete(context) : null,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: '',
|
||||
onPressed: controller.clearSelection,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDelete(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(
|
||||
m.notesWall.deleteSelectedConfirm(controller.selected.length),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: Text(m.common.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
controller.deleteSelected();
|
||||
},
|
||||
child: Text(m.common.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SortButton extends StatelessWidget {
|
||||
final NotesController controller;
|
||||
|
||||
@@ -221,6 +281,27 @@ class _NoteTile extends StatelessWidget {
|
||||
_parseColor(note.color) ?? theme.colorScheme.surfaceContainerHighest;
|
||||
final textColor = _contrastColor(bgColor);
|
||||
|
||||
if (controller.selectMode) {
|
||||
final isSelected = controller.selected.contains(note.id);
|
||||
return GestureDetector(
|
||||
onTap: () => controller.toggleSelection(note.id),
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildCard(theme, bgColor, textColor),
|
||||
Positioned(
|
||||
top: 4,
|
||||
left: 4,
|
||||
child: Icon(
|
||||
isSelected ? Icons.check_circle : Icons.circle_outlined,
|
||||
color: isSelected ? theme.colorScheme.primary : textColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return DragTarget<int>(
|
||||
onWillAcceptWithDetails: (details) => details.data != note.id,
|
||||
onAcceptWithDetails: (_) {},
|
||||
|
||||
@@ -67,6 +67,70 @@ class PhotoBoardController extends ChangeNotifier {
|
||||
final List<UploadTask> _uploads = [];
|
||||
List<UploadTask> get uploads => _uploads;
|
||||
|
||||
// -- Selection --
|
||||
|
||||
bool _selectMode = false;
|
||||
bool get selectMode => _selectMode;
|
||||
|
||||
final Set<int> _selected = {};
|
||||
Set<int> get selected => _selected;
|
||||
|
||||
void toggleSelectMode() {
|
||||
_selectMode = !_selectMode;
|
||||
if (!_selectMode) _selected.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleSelection(int photoId) {
|
||||
if (_selected.contains(photoId)) {
|
||||
_selected.remove(photoId);
|
||||
} else {
|
||||
_selected.add(photoId);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearSelection() {
|
||||
_selected.clear();
|
||||
_selectMode = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> deleteSelected() async {
|
||||
final ids = Set<int>.from(_selected);
|
||||
for (final id in ids) {
|
||||
try {
|
||||
await _service.deletePhoto(houseId, id);
|
||||
_photos.removeWhere((p) => p.id == id);
|
||||
} catch (e) {
|
||||
debugPrint('[PhotoBoardController] Failed to delete photo $id: $e');
|
||||
}
|
||||
}
|
||||
_selected.clear();
|
||||
_selectMode = false;
|
||||
_service.cachePhotos(houseId, _photos);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> moveSelectedToFolder(int? folderId) async {
|
||||
final ids = Set<int>.from(_selected);
|
||||
for (final id in ids) {
|
||||
try {
|
||||
await _service.updatePhoto(
|
||||
houseId,
|
||||
id,
|
||||
folderId: folderId,
|
||||
moveToRoot: folderId == null,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[PhotoBoardController] Failed to move photo $id: $e');
|
||||
}
|
||||
}
|
||||
_selected.clear();
|
||||
_selectMode = false;
|
||||
await _reloadPhotos();
|
||||
}
|
||||
|
||||
/// Items visible in the current view (folders at root + photos in current folder).
|
||||
List<Photo> get visiblePhotos {
|
||||
if (_currentFolderId != null) {
|
||||
|
||||
@@ -120,7 +120,16 @@ class _TopBar extends StatelessWidget {
|
||||
),
|
||||
] else
|
||||
const Spacer(),
|
||||
_SortButton(controller: controller),
|
||||
if (controller.selectMode)
|
||||
_SelectionActions(controller: controller)
|
||||
else ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.checklist),
|
||||
tooltip: '',
|
||||
onPressed: controller.toggleSelectMode,
|
||||
),
|
||||
_SortButton(controller: controller),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -192,6 +201,104 @@ class _SortButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectionActions extends StatelessWidget {
|
||||
final PhotoBoardController controller;
|
||||
|
||||
const _SelectionActions({required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final count = controller.selected.length;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('$count', style: Theme.of(context).textTheme.titleSmall),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.drive_file_move_outlined),
|
||||
tooltip: '',
|
||||
onPressed: count > 0 ? () => _showMoveDialog(context) : null,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
tooltip: '',
|
||||
onPressed: count > 0 ? () => _confirmDelete(context) : null,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: '',
|
||||
onPressed: controller.clearSelection,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDelete(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(
|
||||
m.photoBoard.deleteSelectedConfirm(controller.selected.length),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: Text(m.common.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
controller.deleteSelected();
|
||||
},
|
||||
child: Text(m.common.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMoveDialog(BuildContext context) {
|
||||
final folders = controller.folders;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => SimpleDialog(
|
||||
title: Text(m.photoBoard.folderName),
|
||||
children: [
|
||||
// Move to root option
|
||||
if (controller.currentFolderId != null)
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
controller.moveSelectedToFolder(null);
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.home_outlined, size: 20),
|
||||
SizedBox(width: 12),
|
||||
Text('Root'),
|
||||
],
|
||||
),
|
||||
),
|
||||
...folders.map(
|
||||
(f) => SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
controller.moveSelectedToFolder(f.id);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.folder, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(f.name),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PhotoGrid extends StatelessWidget {
|
||||
final PhotoBoardController controller;
|
||||
|
||||
@@ -601,11 +708,73 @@ class _PhotoTile extends StatelessWidget {
|
||||
final uri = PhotoService.instance.photoPreviewUri(houseId, photo.id);
|
||||
final headers = AuthService.instance.credentials?.basicAuthHeaders ?? {};
|
||||
|
||||
if (controller.selectMode) {
|
||||
final isSelected = controller.selected.contains(photo.id);
|
||||
return GestureDetector(
|
||||
onTap: () => controller.toggleSelection(photo.id),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: uri.toString(),
|
||||
httpHeaders: headers,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, _, _) => Container(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
child: const Icon(Icons.broken_image_outlined, size: 32),
|
||||
),
|
||||
),
|
||||
if (photo.caption != null && photo.caption!.isNotEmpty)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Colors.black.withAlpha(180),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
photo.caption!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
left: 4,
|
||||
child: Icon(
|
||||
isSelected ? Icons.check_circle : Icons.circle_outlined,
|
||||
color: isSelected ? theme.colorScheme.primary : Colors.white,
|
||||
size: 24,
|
||||
shadows: const [Shadow(blurRadius: 4)],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return DragTarget<int>(
|
||||
onWillAcceptWithDetails: (details) => details.data != photo.id,
|
||||
onAcceptWithDetails: (_) {
|
||||
// Drop finalized — endDrag is called via onDragEnd
|
||||
},
|
||||
onAcceptWithDetails: (_) {},
|
||||
onMove: (details) {
|
||||
controller.hoverReorder(photo.id);
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ name: pantry
|
||||
description: "A new Flutter project."
|
||||
# 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
|
||||
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
@@ -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: 1.0.0+1
|
||||
version: 0.1.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.1
|
||||
@@ -66,7 +66,6 @@ dev_dependencies:
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
|
||||
Reference in New Issue
Block a user