From 27a262b54a736f129b0a19e44663ab2f6eb5029c Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Sat, 9 May 2026 16:00:06 +0200 Subject: [PATCH] changed image upload screen from mobile upload screen --- lib/core/routes/app_router.dart | 7 +- .../blocs/image_upload_cubit.dart | 60 ++++ .../blocs/image_upload_state.dart | 29 ++ .../image_upload/ui/image_upload_screen.dart | 306 ++++++++++++++++++ .../ui/old_image_upload_screen.dart} | 10 +- .../ui}/upload_success_screen.dart | 0 .../shared_forms/shared_files_section.dart | 4 +- 7 files changed, 405 insertions(+), 11 deletions(-) create mode 100644 lib/core/widgets/image_upload/blocs/image_upload_cubit.dart create mode 100644 lib/core/widgets/image_upload/blocs/image_upload_state.dart create mode 100644 lib/core/widgets/image_upload/ui/image_upload_screen.dart rename lib/core/widgets/{shared_forms/mobile_upload_screen.dart => image_upload/ui/old_image_upload_screen.dart} (97%) rename lib/core/widgets/{shared_forms => image_upload/ui}/upload_success_screen.dart (100%) diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index af24795..339c53d 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -4,9 +4,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/data/core_repository.dart'; import 'package:flux/core/layout/app_shell.dart'; +import 'package:flux/core/widgets/image_upload/ui/image_upload_screen.dart'; import 'package:flux/core/widgets/set_password_screen.dart'; -import 'package:flux/core/widgets/shared_forms/mobile_upload_screen.dart'; -import 'package:flux/core/widgets/shared_forms/upload_success_screen.dart'; +import 'package:flux/core/widgets/image_upload/ui/old_image_upload_screen.dart'; +import 'package:flux/core/widgets/image_upload/ui/upload_success_screen.dart'; import 'package:flux/features/auth/ui/auth_screen.dart'; import 'package:flux/features/company/bloc/company_settings_cubit.dart'; import 'package:flux/features/company/ui/company_settings_screen.dart'; @@ -297,7 +298,7 @@ class AppRouter { return BlocProvider( create: (context) => AttachmentsBloc(parentId: id, parentType: parentType), - child: SharedMobileUploadScreen( + child: ImageUploadScreen( title: 'Caricamento Rapido', companyId: companyId, ), diff --git a/lib/core/widgets/image_upload/blocs/image_upload_cubit.dart b/lib/core/widgets/image_upload/blocs/image_upload_cubit.dart new file mode 100644 index 0000000..9383f6f --- /dev/null +++ b/lib/core/widgets/image_upload/blocs/image_upload_cubit.dart @@ -0,0 +1,60 @@ +import 'package:equatable/equatable.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:image_picker/image_picker.dart'; + +part 'image_upload_state.dart'; + +class ImageUploadCubit extends Cubit { + ImageUploadCubit() : super(const ImageUploadState()); + + void setStatus(ImageUploadStatus status) { + emit(state.copyWith(status: status)); + } + + void setError(String? message) { + emit( + state.copyWith(status: ImageUploadStatus.failure, errorMessage: message), + ); + } + + void addFiles(List files) { + List newFiles = List.from(state.stagedFiles); + newFiles.addAll(files); + emit( + state.copyWith(status: ImageUploadStatus.success, stagedFiles: newFiles), + ); + } + + void removeFile(PlatformFile file) { + List newFiles = List.from(state.stagedFiles); + newFiles.remove(file); + emit( + state.copyWith(status: ImageUploadStatus.success, stagedFiles: newFiles), + ); + } + + Future addPhoto(XFile photo) async { + final List files = List.from(state.stagedFiles); + files.add(PlatformFile(name: photo.name, size: 0)); + emit( + state.copyWith( + status: ImageUploadStatus.addingPicture, + stagedFiles: files, + ), + ); + final List newFiles = List.from(files); + newFiles.removeLast(); + final PlatformFile loadedFile = PlatformFile( + name: photo.name, + size: await photo.length(), + bytes: await photo.readAsBytes(), + path: photo.path, + ); + newFiles.add(loadedFile); + + emit( + state.copyWith(status: ImageUploadStatus.success, stagedFiles: newFiles), + ); + } +} diff --git a/lib/core/widgets/image_upload/blocs/image_upload_state.dart b/lib/core/widgets/image_upload/blocs/image_upload_state.dart new file mode 100644 index 0000000..8078ec9 --- /dev/null +++ b/lib/core/widgets/image_upload/blocs/image_upload_state.dart @@ -0,0 +1,29 @@ +part of 'image_upload_cubit.dart'; + +enum ImageUploadStatus { initial, addingPicture, uploading, success, failure } + +class ImageUploadState extends Equatable { + final ImageUploadStatus status; + final String? errorMessage; + final List stagedFiles; + + const ImageUploadState({ + this.status = ImageUploadStatus.initial, + this.errorMessage, + this.stagedFiles = const [], + }); + ImageUploadState copyWith({ + ImageUploadStatus? status, + String? errorMessage, + List? stagedFiles, + }) { + return ImageUploadState( + status: status ?? this.status, + errorMessage: errorMessage, + stagedFiles: stagedFiles ?? this.stagedFiles, + ); + } + + @override + List get props => [status, errorMessage, stagedFiles]; +} diff --git a/lib/core/widgets/image_upload/ui/image_upload_screen.dart b/lib/core/widgets/image_upload/ui/image_upload_screen.dart new file mode 100644 index 0000000..d1f2191 --- /dev/null +++ b/lib/core/widgets/image_upload/ui/image_upload_screen.dart @@ -0,0 +1,306 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/utils/extensions.dart'; +import 'package:flux/core/widgets/image_upload/blocs/image_upload_cubit.dart'; +import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; + +class ImageUploadScreen extends StatelessWidget { + final String title; + final String companyId; + + const ImageUploadScreen({ + super.key, + required this.title, + required this.companyId, + }); + + bool _isImage(String path) { + return ['jpg', 'jpeg', 'png', 'webp'].contains(path.fileExtension()); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return BlocListener( + listener: (context, attachmentState) { + if (attachmentState.status == AttachmentsStatus.success && + state.status == ImageUploadStatus.uploading) { + if (Navigator.of(context).canPop()) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("File caricati con successo! ✅"), + ), + ); + Navigator.of(context).pop(); + } else { + context.go('/upload-success'); + } + } + if (attachmentState.status == AttachmentsStatus.failure) { + context.read().setError(attachmentState.error); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Errore: ${state.errorMessage}")), + ); + } + }, + child: Scaffold( + appBar: AppBar( + title: Text('Upload: $title'), + automaticallyImplyLeading: + state.status != ImageUploadStatus.uploading, + ), + body: Stack( + children: [ + Column( + children: [ + // --- SEZIONE PULSANTI (Fotocamera / Galleria) --- + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: + state.status == ImageUploadStatus.uploading + ? null + : () => _handleCamera(context), + icon: const Icon(Icons.camera_alt), + label: const Text('SCATTA'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + ), + ), + ), + Expanded( + child: OutlinedButton.icon( + onPressed: + state.status == ImageUploadStatus.uploading + ? null + : () => _handleFilePicker(context), + icon: const Icon(Icons.folder), + label: const Text("GALLERIA"), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + ), + ), + ), + ], + ), + ), + + const Divider(), + + // --- SEZIONE ANTEPRIME (La GridView Magica) --- + Expanded( + child: state.stagedFiles.isEmpty + ? const Center( + child: Text( + "Nessun file selezionato.\nScatta una foto o scegli dalla galleria.", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey), + ), + ) + : GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: + 3, // 3 colonne stile galleria + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: state.stagedFiles.length, + itemBuilder: (context, index) { + final file = state.stagedFiles[index]; + final isImg = _isImage(file.name); + if (file.bytes == null) { + return Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.grey.shade300, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: const Center( + child: CircularProgressIndicator( + color: Colors.blue, + ), + ), + ), + ); + } + return Stack( + clipBehavior: Clip.none, + children: [ + // L'ANTEPRIMA + Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.grey.shade300, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: isImg + ? (file.bytes != null + // Se abbiamo i bytes (es. scatto da fotocamera) usiamo quelli (a prova di Web!) + ? Image.memory( + file.bytes!, + fit: BoxFit.cover, + ) + // Altrimenti andiamo di file fisico + : const Center( + child: + CircularProgressIndicator( + color: Colors.blue, + ), + )) + : const Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.picture_as_pdf, + color: Colors.red, + size: 36, + ), + SizedBox(height: 4), + Text( + "PDF", + style: TextStyle( + fontSize: 10, + fontWeight: + FontWeight.bold, + ), + ), + ], + ), + ), + ), + + // IL PULSANTE CESTINO (In alto a destra) + Positioned( + top: -8, + right: -8, + child: GestureDetector( + onTap: () => context + .read() + .removeFile(file), + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + color: Colors.white, + size: 16, + ), + ), + ), + ), + ], + ); + }, + ), + ), + // --- SEZIONE INVIA E CHIUDI --- + SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + // Il pulsante si accende SOLO se ci sono file nel carrello + onPressed: + state.stagedFiles.isEmpty || + state.status == ImageUploadStatus.uploading + ? null + : () => _submitAllFiles(context), + icon: const Icon(Icons.cloud_upload), + label: Text( + "INVIA ${state.stagedFiles.length} FILE E CHIUDI", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of( + context, + ).colorScheme.primary, + foregroundColor: Theme.of( + context, + ).colorScheme.onPrimary, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + } + + // --- LOGICA FOTOCAMERA E LIBRERIA --- + Future _handleCamera(BuildContext context) async { + final ImageUploadCubit imageUploadCubit = context.read(); + + final picker = ImagePicker(); + final photo = await picker.pickImage(source: ImageSource.camera); + + if (photo != null) { + imageUploadCubit.addPhoto(photo); + } + } + + Future _handleFilePicker(BuildContext context) async { + final ImageUploadCubit imageUploadCubit = context.read(); + final result = await FilePicker.pickFiles( + allowMultiple: true, + withData: true, + ); + if (result != null) { + imageUploadCubit.addFiles(result.files); + } + } + + // --- LOGICA DI INVIO AL BLoC --- + void _submitAllFiles(BuildContext context) { + final ImageUploadCubit imageUploadCubit = context.read(); + + imageUploadCubit.setStatus(ImageUploadStatus.uploading); + + // Lanciamo l'evento del nostro nuovo AttachmentsBloc Agnostico! + context.read().add( + UploadAttachmentsEvent( + pickedFiles: imageUploadCubit.state.stagedFiles, + companyId: companyId, + ), + ); + } +} diff --git a/lib/core/widgets/shared_forms/mobile_upload_screen.dart b/lib/core/widgets/image_upload/ui/old_image_upload_screen.dart similarity index 97% rename from lib/core/widgets/shared_forms/mobile_upload_screen.dart rename to lib/core/widgets/image_upload/ui/old_image_upload_screen.dart index 7408d6c..b7e3949 100644 --- a/lib/core/widgets/shared_forms/mobile_upload_screen.dart +++ b/lib/core/widgets/image_upload/ui/old_image_upload_screen.dart @@ -6,22 +6,21 @@ import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; -class SharedMobileUploadScreen extends StatefulWidget { +class OldSharedUploadScreen extends StatefulWidget { final String title; final String companyId; - const SharedMobileUploadScreen({ + const OldSharedUploadScreen({ super.key, required this.title, required this.companyId, }); @override - State createState() => - _SharedMobileUploadScreenState(); + State createState() => _OldSharedUploadScreenState(); } -class _SharedMobileUploadScreenState extends State { +class _OldSharedUploadScreenState extends State { // 1. LA NOSTRA STAGING AREA (Il "Carrello") final List _stagedFiles = []; @@ -280,7 +279,6 @@ class _SharedMobileUploadScreenState extends State { try { final picker = ImagePicker(); - // NIENTE PIÙ IMAGE QUALITY! final photo = await picker.pickImage(source: ImageSource.camera); if (photo != null) { diff --git a/lib/core/widgets/shared_forms/upload_success_screen.dart b/lib/core/widgets/image_upload/ui/upload_success_screen.dart similarity index 100% rename from lib/core/widgets/shared_forms/upload_success_screen.dart rename to lib/core/widgets/image_upload/ui/upload_success_screen.dart diff --git a/lib/core/widgets/shared_forms/shared_files_section.dart b/lib/core/widgets/shared_forms/shared_files_section.dart index a993661..79802c9 100644 --- a/lib/core/widgets/shared_forms/shared_files_section.dart +++ b/lib/core/widgets/shared_forms/shared_files_section.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/widgets/image_upload/ui/image_upload_screen.dart'; import 'package:flux/core/widgets/qr_upload_dialog.dart'; -import 'package:flux/core/widgets/shared_forms/mobile_upload_screen.dart'; import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; import 'package:get_it/get_it.dart'; @@ -98,7 +98,7 @@ class SharedFilesSection extends StatelessWidget { MaterialPageRoute( builder: (_) => BlocProvider.value( value: bloc, - child: SharedMobileUploadScreen( + child: ImageUploadScreen( title: titleNameForUpload, companyId: GetIt.I .get()