changed image upload screen from mobile upload screen
This commit is contained in:
@@ -4,9 +4,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
import 'package:flux/core/data/core_repository.dart';
|
import 'package:flux/core/data/core_repository.dart';
|
||||||
import 'package:flux/core/layout/app_shell.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/set_password_screen.dart';
|
||||||
import 'package:flux/core/widgets/shared_forms/mobile_upload_screen.dart';
|
import 'package:flux/core/widgets/image_upload/ui/old_image_upload_screen.dart';
|
||||||
import 'package:flux/core/widgets/shared_forms/upload_success_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/auth/ui/auth_screen.dart';
|
||||||
import 'package:flux/features/company/bloc/company_settings_cubit.dart';
|
import 'package:flux/features/company/bloc/company_settings_cubit.dart';
|
||||||
import 'package:flux/features/company/ui/company_settings_screen.dart';
|
import 'package:flux/features/company/ui/company_settings_screen.dart';
|
||||||
@@ -297,7 +298,7 @@ class AppRouter {
|
|||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) =>
|
create: (context) =>
|
||||||
AttachmentsBloc(parentId: id, parentType: parentType),
|
AttachmentsBloc(parentId: id, parentType: parentType),
|
||||||
child: SharedMobileUploadScreen(
|
child: ImageUploadScreen(
|
||||||
title: 'Caricamento Rapido',
|
title: 'Caricamento Rapido',
|
||||||
companyId: companyId,
|
companyId: companyId,
|
||||||
),
|
),
|
||||||
|
|||||||
60
lib/core/widgets/image_upload/blocs/image_upload_cubit.dart
Normal file
60
lib/core/widgets/image_upload/blocs/image_upload_cubit.dart
Normal file
@@ -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<ImageUploadState> {
|
||||||
|
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<PlatformFile> files) {
|
||||||
|
List<PlatformFile> newFiles = List.from(state.stagedFiles);
|
||||||
|
newFiles.addAll(files);
|
||||||
|
emit(
|
||||||
|
state.copyWith(status: ImageUploadStatus.success, stagedFiles: newFiles),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeFile(PlatformFile file) {
|
||||||
|
List<PlatformFile> newFiles = List.from(state.stagedFiles);
|
||||||
|
newFiles.remove(file);
|
||||||
|
emit(
|
||||||
|
state.copyWith(status: ImageUploadStatus.success, stagedFiles: newFiles),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addPhoto(XFile photo) async {
|
||||||
|
final List<PlatformFile> files = List.from(state.stagedFiles);
|
||||||
|
files.add(PlatformFile(name: photo.name, size: 0));
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: ImageUploadStatus.addingPicture,
|
||||||
|
stagedFiles: files,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final List<PlatformFile> 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lib/core/widgets/image_upload/blocs/image_upload_state.dart
Normal file
29
lib/core/widgets/image_upload/blocs/image_upload_state.dart
Normal file
@@ -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<PlatformFile> stagedFiles;
|
||||||
|
|
||||||
|
const ImageUploadState({
|
||||||
|
this.status = ImageUploadStatus.initial,
|
||||||
|
this.errorMessage,
|
||||||
|
this.stagedFiles = const [],
|
||||||
|
});
|
||||||
|
ImageUploadState copyWith({
|
||||||
|
ImageUploadStatus? status,
|
||||||
|
String? errorMessage,
|
||||||
|
List<PlatformFile>? stagedFiles,
|
||||||
|
}) {
|
||||||
|
return ImageUploadState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
stagedFiles: stagedFiles ?? this.stagedFiles,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [status, errorMessage, stagedFiles];
|
||||||
|
}
|
||||||
306
lib/core/widgets/image_upload/ui/image_upload_screen.dart
Normal file
306
lib/core/widgets/image_upload/ui/image_upload_screen.dart
Normal file
@@ -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<ImageUploadCubit, ImageUploadState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return BlocListener<AttachmentsBloc, AttachmentsState>(
|
||||||
|
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<ImageUploadCubit>().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<ImageUploadCubit>()
|
||||||
|
.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<void> _handleCamera(BuildContext context) async {
|
||||||
|
final ImageUploadCubit imageUploadCubit = context.read<ImageUploadCubit>();
|
||||||
|
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final photo = await picker.pickImage(source: ImageSource.camera);
|
||||||
|
|
||||||
|
if (photo != null) {
|
||||||
|
imageUploadCubit.addPhoto(photo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleFilePicker(BuildContext context) async {
|
||||||
|
final ImageUploadCubit imageUploadCubit = context.read<ImageUploadCubit>();
|
||||||
|
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>();
|
||||||
|
|
||||||
|
imageUploadCubit.setStatus(ImageUploadStatus.uploading);
|
||||||
|
|
||||||
|
// Lanciamo l'evento del nostro nuovo AttachmentsBloc Agnostico!
|
||||||
|
context.read<AttachmentsBloc>().add(
|
||||||
|
UploadAttachmentsEvent(
|
||||||
|
pickedFiles: imageUploadCubit.state.stagedFiles,
|
||||||
|
companyId: companyId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,22 +6,21 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
||||||
|
|
||||||
class SharedMobileUploadScreen extends StatefulWidget {
|
class OldSharedUploadScreen extends StatefulWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String companyId;
|
final String companyId;
|
||||||
|
|
||||||
const SharedMobileUploadScreen({
|
const OldSharedUploadScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.companyId,
|
required this.companyId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SharedMobileUploadScreen> createState() =>
|
State<OldSharedUploadScreen> createState() => _OldSharedUploadScreenState();
|
||||||
_SharedMobileUploadScreenState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SharedMobileUploadScreenState extends State<SharedMobileUploadScreen> {
|
class _OldSharedUploadScreenState extends State<OldSharedUploadScreen> {
|
||||||
// 1. LA NOSTRA STAGING AREA (Il "Carrello")
|
// 1. LA NOSTRA STAGING AREA (Il "Carrello")
|
||||||
final List<PlatformFile> _stagedFiles = [];
|
final List<PlatformFile> _stagedFiles = [];
|
||||||
|
|
||||||
@@ -280,7 +279,6 @@ class _SharedMobileUploadScreenState extends State<SharedMobileUploadScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
// NIENTE PIÙ IMAGE QUALITY!
|
|
||||||
final photo = await picker.pickImage(source: ImageSource.camera);
|
final photo = await picker.pickImage(source: ImageSource.camera);
|
||||||
|
|
||||||
if (photo != null) {
|
if (photo != null) {
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/core/blocs/session/session_cubit.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/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:flux/features/attachments/blocs/attachments_bloc.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ class SharedFilesSection extends StatelessWidget {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => BlocProvider.value(
|
builder: (_) => BlocProvider.value(
|
||||||
value: bloc,
|
value: bloc,
|
||||||
child: SharedMobileUploadScreen(
|
child: ImageUploadScreen(
|
||||||
title: titleNameForUpload,
|
title: titleNameForUpload,
|
||||||
companyId: GetIt.I
|
companyId: GetIt.I
|
||||||
.get<SessionCubit>()
|
.get<SessionCubit>()
|
||||||
|
|||||||
Reference in New Issue
Block a user