feat-add-files-from-qr #8

Merged
brontomark merged 13 commits from feat-add-files-from-qr into main 2026-04-26 10:15:35 +02:00
25 changed files with 518 additions and 157 deletions
Showing only changes of commit 41f6d0dd43 - Show all commits

View File

@@ -6,6 +6,7 @@ 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/features/auth/ui/auth_screen.dart';
import 'package:flux/features/customers/blocs/customer_files_bloc.dart';
import 'package:flux/features/customers/models/customer_model.dart';
import 'package:flux/features/customers/ui/customer_detail_screen.dart';
import 'package:flux/features/home/ui/home_screen.dart';
@@ -90,9 +91,13 @@ class AppRouter {
builder: (context, state) {
// Recuperiamo l'oggetto customer passato tramite extra
final customer = state.extra as CustomerModel;
return CustomerDetailScreen(customer: customer);
return BlocProvider(
create: (context) => CustomerFilesBloc(customer.id!),
child: CustomerDetailScreen(customer: customer),
);
},
),
GoRoute(
path: '/products',
name: 'products',

View File

@@ -0,0 +1,9 @@
// Funzione che chiede le chiavi a Supabase
import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
Future<String> getSignedUrl(String storagePath) async {
return await GetIt.I<SupabaseClient>().storage
.from('documents')
.createSignedUrl(storagePath, 60); // Link che si autodistrugge in 60s
}

View File

@@ -1,6 +1,6 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; // <--- AGGIUNGI QUESTO
import 'package:flux/core/utils/functions.dart';
class ImageViewerWidget extends StatelessWidget {
final String? storagePath; // ATTENZIONE: Ora contiene lo storagePath!
@@ -12,13 +12,6 @@ class ImageViewerWidget extends StatelessWidget {
'Errore: Devi fornire un Path valido o i bytes del file!',
);
// Funzione che chiede le chiavi a Supabase
Future<String> _getSignedUrl() async {
return await Supabase.instance.client.storage
.from('documents')
.createSignedUrl(storagePath!, 60); // Link che si autodistrugge in 60s
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -37,7 +30,7 @@ class ImageViewerWidget extends StatelessWidget {
child: bytes != null
? Image.memory(bytes!)
: FutureBuilder<String>(
future: _getSignedUrl(),
future: getSignedUrl(storagePath!),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();

View File

@@ -1,5 +1,6 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flux/core/utils/functions.dart';
import 'package:get_it/get_it.dart';
import 'package:pdfx/pdfx.dart';
import 'package:internet_file/internet_file.dart';
@@ -39,11 +40,7 @@ class _PdfViewerWidgetState extends State<PdfViewerWidget> {
pdfData = widget.bytes!;
} else if (widget.storagePath != null && widget.storagePath!.isNotEmpty) {
// SCENARIO 2: Pratica salvata, scarichiamo da Supabase (Remoto)
final signedUrl = await GetIt.I
.get<SupabaseClient>()
.storage
.from('documents')
.createSignedUrl(widget.storagePath!, 60);
final signedUrl = await getSignedUrl(widget.storagePath!);
pdfData = await InternetFile.get(signedUrl);
} else {
throw Exception("Nessun documento trovato");

View File

@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/customers/data/customer_repository.dart';
import 'package:flux/features/customers/models/customer_file_model.dart';
import 'package:flux/features/customers/models/customer_model.dart';
import 'package:get_it/get_it.dart';

View File

@@ -0,0 +1,93 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flux/features/customers/data/customer_repository.dart';
import 'package:flux/features/customers/models/customer_file_model.dart';
import 'package:get_it/get_it.dart';
part 'customer_files_events.dart';
part 'customer_files_state.dart';
class CustomerFilesBloc extends Bloc<CustomerFilesEvent, CustomerFilesState> {
final CustomerRepository _repository = GetIt.I<CustomerRepository>();
final String customerId;
CustomerFilesBloc(this.customerId)
: super(const CustomerFilesState(status: CustomerFilesStatus.initial)) {
on<LoadCustomerFilesEvent>(_loadCustomerFiles);
on<UploadCustomerFileEvent>(_uploadCustomerFile);
on<DeleteCustomerFileEvent>(_deleteCustomerFile);
on<ToggleCustomerFileSelectionEvent>(_toggleCustomerFileSelection);
}
void _loadCustomerFiles(
LoadCustomerFilesEvent event,
Emitter<CustomerFilesState> emit,
) async {
await emit.forEach<List<CustomerFileModel>>(
_repository.getCustomerFilesStream(customerId),
onData: (customerFiles) => CustomerFilesState(
status: CustomerFilesStatus.success,
customerFiles: customerFiles,
),
onError: (error, stackTrace) => CustomerFilesState(
status: CustomerFilesStatus.failure,
error: error.toString(),
),
);
}
Future<void> _uploadCustomerFile(
UploadCustomerFileEvent event,
Emitter<CustomerFilesState> emit,
) async {
emit(state.copyWith(status: CustomerFilesStatus.uploading));
if (event.pickedFile != null) {
try {
await _repository.uploadAndRegisterFile(
customerId: customerId,
pickedFile: event.pickedFile!,
);
emit(state.copyWith(status: CustomerFilesStatus.success));
} catch (e) {
emit(
state.copyWith(
status: CustomerFilesStatus.failure,
error: e.toString(),
),
);
}
}
}
Future<void> _deleteCustomerFile(
DeleteCustomerFileEvent event,
Emitter<CustomerFilesState> emit,
) async {
emit(state.copyWith(status: CustomerFilesStatus.loading));
try {
await _repository.deleteDocument(event.file);
emit(state.copyWith(status: CustomerFilesStatus.success));
} catch (e) {
emit(
state.copyWith(
status: CustomerFilesStatus.failure,
error: e.toString(),
),
);
}
}
void _toggleCustomerFileSelection(
ToggleCustomerFileSelectionEvent event,
Emitter<CustomerFilesState> emit,
) {
List<CustomerFileModel> selectedFiles = List.from(state.selectedFiles);
if (selectedFiles.contains(event.file)) {
selectedFiles.remove(event.file);
} else {
selectedFiles.add(event.file);
}
emit(state.copyWith(selectedFiles: selectedFiles));
}
}

View File

@@ -0,0 +1,26 @@
part of 'customer_files_bloc.dart';
abstract class CustomerFilesEvent extends Equatable {
const CustomerFilesEvent();
@override
List<Object> get props => [];
}
class LoadCustomerFilesEvent extends CustomerFilesEvent {}
class UploadCustomerFileEvent extends CustomerFilesEvent {
final PlatformFile? pickedFile;
final File? photo;
const UploadCustomerFileEvent({this.pickedFile, this.photo});
}
class DeleteCustomerFileEvent extends CustomerFilesEvent {
final CustomerFileModel file;
const DeleteCustomerFileEvent(this.file);
}
class ToggleCustomerFileSelectionEvent extends CustomerFilesEvent {
final CustomerFileModel file;
const ToggleCustomerFileSelectionEvent(this.file);
}

View File

@@ -0,0 +1,34 @@
part of 'customer_files_bloc.dart';
enum CustomerFilesStatus { initial, loading, uploading, success, failure }
class CustomerFilesState extends Equatable {
const CustomerFilesState({
required this.status,
this.error,
this.customerFiles = const [],
this.selectedFiles = const [],
});
final CustomerFilesStatus status;
final String? error;
final List<CustomerFileModel> customerFiles;
final List<CustomerFileModel> selectedFiles;
@override
List<Object?> get props => [status, error, customerFiles, selectedFiles];
CustomerFilesState copyWith({
CustomerFilesStatus? status,
String? error,
List<CustomerFileModel>? customerFiles,
List<CustomerFileModel>? selectedFiles,
}) {
return CustomerFilesState(
status: status ?? this.status,
error: error,
customerFiles: customerFiles ?? this.customerFiles,
selectedFiles: selectedFiles ?? this.selectedFiles,
);
}
}

View File

@@ -1,18 +1,27 @@
part of 'customer_cubit.dart';
enum CustomerStatus { initial, loading, success, failure }
enum CustomerStatus {
initial,
loading,
filesLoading,
filesUploading,
success,
failure,
}
class CustomerState extends Equatable {
final CustomerStatus status;
final List<CustomerModel> customers;
final CustomerModel? lastCreatedCustomer;
final String? errorMessage;
final List<CustomerFileModel> customerFiles;
const CustomerState({
this.status = CustomerStatus.initial,
this.customers = const [],
this.lastCreatedCustomer,
this.errorMessage,
this.customerFiles = const [],
});
CustomerState copyWith({
@@ -20,12 +29,14 @@ class CustomerState extends Equatable {
List<CustomerModel>? customers,
CustomerModel? lastCreatedCustomer,
String? errorMessage,
List<CustomerFileModel>? customerFiles,
}) {
return CustomerState(
status: status ?? this.status,
customers: customers ?? this.customers,
lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer,
errorMessage: errorMessage ?? this.errorMessage,
customerFiles: customerFiles ?? this.customerFiles,
);
}
@@ -35,5 +46,6 @@ class CustomerState extends Equatable {
customers,
lastCreatedCustomer,
errorMessage,
customerFiles,
];
}

View File

@@ -1,5 +1,8 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/utils/functions.dart';
import 'package:flux/core/utils/string_extensions.dart';
import 'package:flux/features/customers/models/customer_file_model.dart';
import 'package:get_it/get_it.dart';
@@ -76,6 +79,19 @@ class CustomerRepository {
}
}
/// Ascolta in tempo reale i file caricati per un cliente
Stream<List<CustomerFileModel>> getCustomerFilesStream(String customerId) {
return _supabase
.from('customer_file')
.stream(primaryKey: ['id'])
.eq('customer_id', customerId)
.order('created_at', ascending: false)
.map(
(listOfMaps) =>
listOfMaps.map((map) => CustomerFileModel.fromMap(map)).toList(),
);
}
/// Recupera i file di un cliente specifico
Future<List<CustomerFileModel>> getCustomerFiles(String customerId) async {
try {
@@ -113,7 +129,7 @@ class CustomerRepository {
customerId: customerId,
name: cleanFileName.fileNameWithoutExtension(),
extension: cleanFileName.fileExtension(),
url: storagePath,
storagePath: storagePath,
fileSize: fileSize,
);
final String mimeType = fileToSave.extension.toLowerCase() == 'pdf'
@@ -161,9 +177,13 @@ class CustomerRepository {
}
/// Elimina un file dallo storage
Future<void> deleteDocument(String fullPath) async {
// Il path dovrebbe essere ricavato dall'URL
final path = fullPath.split('documents/').last;
Future<void> deleteDocument(CustomerFileModel file) async {
try {
final path = await getSignedUrl(file.storagePath);
await _supabase.from('customer_file').delete().eq('id', file.id!);
await _supabase.storage.from('documents').remove([path]);
} on Exception catch (e) {
throw 'Errore durante l\'eliminazione del file: $e';
}
}
}

View File

@@ -4,7 +4,7 @@ class CustomerFileModel extends Equatable {
final String? id;
final String customerId; // Riferimento UUID
final String name;
final String url;
final String storagePath;
final String extension;
final DateTime? createdAt;
final int fileSize;
@@ -13,7 +13,7 @@ class CustomerFileModel extends Equatable {
this.id,
required this.customerId,
required this.name,
required this.url,
required this.storagePath,
required this.extension,
this.createdAt,
required this.fileSize,
@@ -35,7 +35,7 @@ class CustomerFileModel extends Equatable {
String? id,
String? customerId,
String? name,
String? url,
String? storagePath,
String? extension,
DateTime? createdAt,
int? fileSize,
@@ -44,7 +44,7 @@ class CustomerFileModel extends Equatable {
id: id ?? this.id,
customerId: customerId ?? this.customerId,
name: name ?? this.name,
url: url ?? this.url,
storagePath: storagePath ?? this.storagePath,
extension: extension ?? this.extension,
createdAt: createdAt ?? this.createdAt,
fileSize: fileSize ?? this.fileSize,
@@ -56,7 +56,7 @@ class CustomerFileModel extends Equatable {
id: map['id'] as String,
customerId: map['customer_id'],
name: map['name'],
url: map['url'],
storagePath: map['storage_path'],
extension: map['extension'] ?? '',
createdAt: map['created_at'] != null
? DateTime.parse(map['created_at'])
@@ -72,7 +72,7 @@ class CustomerFileModel extends Equatable {
if (id != null) 'id': id,
'customer_id': customerId,
'name': name,
'url': url,
'storage_path': storagePath,
'extension': extension,
'file_size': fileSize,
};
@@ -83,7 +83,7 @@ class CustomerFileModel extends Equatable {
id,
customerId,
name,
url,
storagePath,
extension,
createdAt,
fileSize,

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/customers/data/customer_repository.dart';
import 'package:flux/core/widgets/image_viewer_widget.dart';
import 'package:flux/core/widgets/pdf_viewer_widget.dart';
import 'package:flux/features/customers/blocs/customer_files_bloc.dart';
import 'package:flux/features/customers/models/customer_model.dart';
import 'package:flux/features/customers/models/customer_file_model.dart';
import 'package:get_it/get_it.dart';
class CustomerDetailScreen extends StatefulWidget {
final CustomerModel customer;
@@ -15,36 +17,19 @@ class CustomerDetailScreen extends StatefulWidget {
}
class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
final _repository = GetIt.I<CustomerRepository>();
List<CustomerFileModel> _files = [];
bool _isLoadingFiles = true;
@override
void initState() {
super.initState();
_loadFiles();
}
Future<void> _loadFiles() async {
try {
final files = await _repository.getCustomerFiles(
widget.customer.id.toString(),
);
setState(() {
_files = files;
_isLoadingFiles = false;
});
} catch (e) {
setState(() => _isLoadingFiles = false);
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toString())));
}
}
void _loadFiles() {
context.read<CustomerFilesBloc>().add(LoadCustomerFilesEvent());
}
Future<void> _pickAndUpload() async {
CustomerFilesBloc customerFilesBloc = context.read<CustomerFilesBloc>();
// Chiamata statica pulita
FilePickerResult? result = await FilePicker.pickFiles(
allowMultiple: true,
@@ -55,11 +40,9 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
if (result != null) {
for (var pickedFile in result.files) {
try {
final newFile = await _repository.uploadAndRegisterFile(
customerId: widget.customer.id.toString(),
pickedFile: pickedFile,
customerFilesBloc.add(
UploadCustomerFileEvent(pickedFile: pickedFile),
);
setState(() => _files.add(newFile));
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -158,6 +141,8 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
}
Widget _buildDocumentSection() {
return BlocBuilder<CustomerFilesBloc, CustomerFilesState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -180,9 +165,9 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
],
),
const SizedBox(height: 20),
if (_isLoadingFiles)
if (state.status == CustomerFilesStatus.loading)
const Center(child: CircularProgressIndicator())
else if (_files.isEmpty)
else if (state.customerFiles.isEmpty)
const Center(child: Text("Nessun documento presente"))
else
Expanded(
@@ -193,12 +178,15 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
crossAxisSpacing: 16,
childAspectRatio: 1.2,
),
itemCount: _files.length,
itemBuilder: (context, index) => _FileCard(file: _files[index]),
itemCount: state.customerFiles.length,
itemBuilder: (context, index) =>
_FileCard(file: state.customerFiles[index], state: state),
),
),
],
);
},
);
}
Widget _infoTile(IconData icon, String label, String value) {
@@ -227,11 +215,19 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
class _FileCard extends StatelessWidget {
final CustomerFileModel file;
const _FileCard({required this.file});
final CustomerFilesState state;
const _FileCard({required this.file, required this.state});
@override
Widget build(BuildContext context) {
return Container(
return GestureDetector(
onTap: () => context.read<CustomerFilesBloc>().add(
ToggleCustomerFileSelectionEvent(file),
),
onDoubleTap: () => _handleDoubleClickOnFile(context, file),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
color: context.background,
borderRadius: BorderRadius.circular(12),
@@ -240,7 +236,11 @@ class _FileCard extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(_getFileIcon(file.extension), size: 48, color: context.accent),
Icon(
_getFileIcon(file.extension),
size: 48,
color: context.accent,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
@@ -248,9 +248,21 @@ class _FileCard extends StatelessWidget {
file.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
if (state.selectedFiles.contains(file))
Positioned(
top: 10,
left: 10,
child: Icon(Icons.check_circle, color: context.accent, size: 24),
),
],
),
);
@@ -268,4 +280,25 @@ class _FileCard extends StatelessWidget {
return Icons.insert_drive_file;
}
}
void _handleDoubleClickOnFile(BuildContext context, CustomerFileModel file) {
showDialog(
context: context,
barrierDismissible: true,
builder: (ctx) => Dialog(
insetPadding: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
width: double.infinity,
height: MediaQuery.of(context).size.height * 0.8,
child: file.isPdf
? PdfViewerWidget(storagePath: file.storagePath)
: ImageViewerWidget(storagePath: file.storagePath),
),
),
),
);
}
}

View File

@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/customers/blocs/customer_cubit.dart';
import 'package:flux/features/customers/blocs/customer_files_bloc.dart';
import 'package:flux/features/customers/models/customer_model.dart';
import 'package:flux/features/customers/ui/customer_form.dart';
import 'package:go_router/go_router.dart';
@@ -37,39 +38,6 @@ class _CustomersContentState extends State<CustomersContent> {
}
}
/// Funzione unica per gestire Creazione e Modifica
void _openCustomerForm({CustomerModel? customer}) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
backgroundColor: context.background,
content: SizedBox(
width: 500, // Larghezza ottimale per desktop
child: CustomerForm(
customer: customer,
onSave: (customerFromForm) {
final session = context.read<SessionCubit>().state;
final companyId = session.company?.id;
if (companyId == null) return;
if (customer == null) {
// CASO NUOVO: Iniettiamo il companyId e inviamo l'evento create
context.read<CustomerCubit>().createCustomer(
customerFromForm.copyWith(companyId: companyId),
);
} else {
// CASO MODIFICA: L'ID e il companyId sono già nel modello
context.read<CustomerCubit>().updateCustomer(customerFromForm);
}
Navigator.pop(dialogContext);
},
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -85,7 +53,7 @@ class _CustomersContentState extends State<CustomersContent> {
Padding(
padding: const EdgeInsets.only(right: 16),
child: ElevatedButton.icon(
onPressed: () => _openCustomerForm(),
onPressed: () => openCustomerForm(context: context),
icon: const Icon(Icons.person_add_alt_1_rounded, size: 20),
label: const Text('NUOVO'),
style: ElevatedButton.styleFrom(
@@ -244,8 +212,48 @@ class _CustomerTile extends StatelessWidget {
],
),
),
trailing: Icon(Icons.edit_note_rounded, color: context.accent),
trailing: IconButton(
onPressed: () =>
openCustomerForm(context: context, customer: customer),
icon: Icon(Icons.edit_note_rounded, color: context.accent),
),
),
);
}
}
/// Funzione unica per gestire Creazione e Modifica
void openCustomerForm({
CustomerModel? customer,
required BuildContext context,
}) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
backgroundColor: context.background,
content: SizedBox(
width: 500, // Larghezza ottimale per desktop
child: CustomerForm(
customer: customer,
onSave: (customerFromForm) {
final session = context.read<SessionCubit>().state;
final companyId = session.company?.id;
if (companyId == null) return;
if (customer == null) {
// CASO NUOVO: Iniettiamo il companyId e inviamo l'evento create
context.read<CustomerCubit>().createCustomer(
customerFromForm.copyWith(companyId: companyId),
);
} else {
// CASO MODIFICA: L'ID e il companyId sono già nel modello
context.read<CustomerCubit>().updateCustomer(customerFromForm);
}
Navigator.pop(dialogContext);
},
),
),
),
);
}

View File

@@ -235,7 +235,7 @@ class ServicesCubit extends Cubit<ServicesState> {
serviceId: state.currentService?.id ?? '',
name: file.name.fileNameWithoutExtension(),
extension: file.name.fileExtension(),
url: '',
storagePath: '',
fileSize: file.size,
localBytes: file.bytes,
createdAt: DateTime.now(),
@@ -295,7 +295,7 @@ class ServicesCubit extends Cubit<ServicesState> {
orElse: () => file,
);
if (savedFile.url.isEmpty) {
if (savedFile.storagePath.isEmpty) {
throw Exception(
"Errore: URL del file non trovato dopo il salvataggio.",
);

View File

@@ -159,7 +159,10 @@ class ServicesRepository {
final String mimeType = file.extension.toLowerCase() == 'pdf'
? 'application/pdf'
: 'image/${file.extension}';
final fileToSave = file.copyWith(serviceId: newId, url: storagePath);
final fileToSave = file.copyWith(
serviceId: newId,
storagePath: storagePath,
);
// Creiamo una funzione asincrona per caricare file e scrivere nel DB
Future<void> uploadAndLink() async {
@@ -235,6 +238,15 @@ class ServicesRepository {
}
}
/// Ascolta in tempo reale i file caricati per una pratica
Stream<List<Map<String, dynamic>>> getServiceFilesStream(String serviceId) {
return _supabase
.from('service_file')
.stream(primaryKey: ['id'])
.eq('service_id', serviceId)
.order('created_at', ascending: false);
}
Future<void> copyFileToCustomer({
required ServiceFileModel file,
required String customerId,
@@ -242,7 +254,7 @@ class ServicesRepository {
CustomerFileModel fileToCopy = CustomerFileModel(
customerId: customerId,
name: file.name,
url: file.url,
storagePath: file.storagePath,
extension: file.extension,
fileSize: file.fileSize,
);

View File

@@ -7,7 +7,7 @@ class ServiceFileModel extends Equatable {
final DateTime? createdAt;
final String name;
final String extension;
final String url;
final String storagePath;
final String serviceId;
final int fileSize;
final Uint8List? localBytes;
@@ -17,7 +17,7 @@ class ServiceFileModel extends Equatable {
this.createdAt,
required this.name,
required this.extension,
required this.url,
required this.storagePath,
required this.serviceId,
required this.fileSize,
this.localBytes,
@@ -40,7 +40,7 @@ class ServiceFileModel extends Equatable {
DateTime? createdAt,
String? name,
String? extension,
String? url,
String? storagePath,
String? serviceId,
int? fileSize,
Uint8List? localBytes,
@@ -50,7 +50,7 @@ class ServiceFileModel extends Equatable {
createdAt: createdAt ?? this.createdAt,
name: name ?? this.name,
extension: extension ?? this.extension,
url: url ?? this.url,
storagePath: storagePath ?? this.storagePath,
serviceId: serviceId ?? this.serviceId,
fileSize: fileSize ?? this.fileSize,
localBytes: localBytes ?? this.localBytes,
@@ -65,7 +65,7 @@ class ServiceFileModel extends Equatable {
: null,
name: map['name'] ?? '',
extension: map['extension'] ?? '',
url: map['url'] ?? '',
storagePath: map['storage_path'] ?? '',
serviceId: map['service_id']?.toString() ?? '',
fileSize: map['file_size'] is int
? map['file_size']
@@ -78,7 +78,7 @@ class ServiceFileModel extends Equatable {
if (id != null) 'id': id,
'name': name,
'extension': extension,
'url': url,
'storage_path': storagePath,
'service_id': serviceId,
'file_size': fileSize,
};
@@ -90,7 +90,7 @@ class ServiceFileModel extends Equatable {
createdAt,
name,
extension,
url,
storagePath,
serviceId,
fileSize,
localBytes,

View File

@@ -169,11 +169,15 @@ class AttachmentsSection extends StatelessWidget {
height: MediaQuery.of(context).size.height * 0.8,
child: file.isPdf
? PdfViewerWidget(
storagePath: file.url.isNotEmpty ? file.url : null,
storagePath: file.storagePath.isNotEmpty
? file.storagePath
: null,
bytes: file.localBytes,
)
: ImageViewerWidget(
storagePath: file.url.isNotEmpty ? file.url : null,
storagePath: file.storagePath.isNotEmpty
? file.storagePath
: null,
bytes: file.localBytes,
),
),

View File

@@ -6,10 +6,14 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <gtk/gtk_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar);

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
gtk
url_launcher_linux
)

View File

@@ -7,6 +7,7 @@ import Foundation
import app_links
import file_picker
import file_selector_macos
import pdfx
import shared_preferences_foundation
import url_launcher_macos
@@ -14,6 +15,7 @@ import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View File

@@ -3,6 +3,8 @@ PODS:
- FlutterMacOS
- file_picker (0.0.1):
- FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- pdfx (1.0.0):
- FlutterMacOS
@@ -15,6 +17,7 @@ PODS:
DEPENDENCIES:
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- pdfx (from `Flutter/ephemeral/.symlinks/plugins/pdfx/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
@@ -25,6 +28,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
file_picker:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
file_selector_macos:
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
FlutterMacOS:
:path: Flutter/ephemeral
pdfx:
@@ -37,6 +42,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
pdfx: 1e79f57f7a6ce2f4a4c30f21fa54d3dc82441b51
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb

View File

@@ -185,6 +185,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "11.0.2"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
source: hosted
version: "0.9.5"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
fixnum:
dependency: transitive
description:
@@ -328,6 +360,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622"
url: "https://pub.dev"
source: hosted
version: "0.8.13+16"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
url: "https://pub.dev"
source: hosted
version: "0.8.13+6"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
source: hosted
version: "0.2.2"
internet_file:
dependency: "direct main"
description:

View File

@@ -18,6 +18,7 @@ dependencies:
get_it: ^9.2.1
go_router: ^17.2.0
google_fonts: ^8.0.2
image_picker: ^1.2.1
internet_file: ^1.3.0
intl: ^0.20.2
pdfx: ^2.9.2

View File

@@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h"
#include <app_links/app_links_plugin_c_api.h>
#include <file_selector_windows/file_selector_windows.h>
#include <pdfx/pdfx_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
@@ -14,6 +15,8 @@
void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
PdfxPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PdfxPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
app_links
file_selector_windows
pdfx
permission_handler_windows
url_launcher_windows