feat-add-files-from-qr #8
@@ -6,6 +6,7 @@ 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/features/auth/ui/auth_screen.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/models/customer_model.dart';
|
||||||
import 'package:flux/features/customers/ui/customer_detail_screen.dart';
|
import 'package:flux/features/customers/ui/customer_detail_screen.dart';
|
||||||
import 'package:flux/features/home/ui/home_screen.dart';
|
import 'package:flux/features/home/ui/home_screen.dart';
|
||||||
@@ -90,9 +91,13 @@ class AppRouter {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
// Recuperiamo l'oggetto customer passato tramite extra
|
// Recuperiamo l'oggetto customer passato tramite extra
|
||||||
final customer = state.extra as CustomerModel;
|
final customer = state.extra as CustomerModel;
|
||||||
return CustomerDetailScreen(customer: customer);
|
return BlocProvider(
|
||||||
|
create: (context) => CustomerFilesBloc(customer.id!),
|
||||||
|
child: CustomerDetailScreen(customer: customer),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/products',
|
path: '/products',
|
||||||
name: 'products',
|
name: 'products',
|
||||||
|
|||||||
9
lib/core/utils/functions.dart
Normal file
9
lib/core/utils/functions.dart
Normal 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
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
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 {
|
class ImageViewerWidget extends StatelessWidget {
|
||||||
final String? storagePath; // ATTENZIONE: Ora contiene lo storagePath!
|
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!',
|
'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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -37,7 +30,7 @@ class ImageViewerWidget extends StatelessWidget {
|
|||||||
child: bytes != null
|
child: bytes != null
|
||||||
? Image.memory(bytes!)
|
? Image.memory(bytes!)
|
||||||
: FutureBuilder<String>(
|
: FutureBuilder<String>(
|
||||||
future: _getSignedUrl(),
|
future: getSignedUrl(storagePath!),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const CircularProgressIndicator();
|
return const CircularProgressIndicator();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flux/core/utils/functions.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:pdfx/pdfx.dart';
|
import 'package:pdfx/pdfx.dart';
|
||||||
import 'package:internet_file/internet_file.dart';
|
import 'package:internet_file/internet_file.dart';
|
||||||
@@ -39,11 +40,7 @@ class _PdfViewerWidgetState extends State<PdfViewerWidget> {
|
|||||||
pdfData = widget.bytes!;
|
pdfData = widget.bytes!;
|
||||||
} else if (widget.storagePath != null && widget.storagePath!.isNotEmpty) {
|
} else if (widget.storagePath != null && widget.storagePath!.isNotEmpty) {
|
||||||
// SCENARIO 2: Pratica salvata, scarichiamo da Supabase (Remoto)
|
// SCENARIO 2: Pratica salvata, scarichiamo da Supabase (Remoto)
|
||||||
final signedUrl = await GetIt.I
|
final signedUrl = await getSignedUrl(widget.storagePath!);
|
||||||
.get<SupabaseClient>()
|
|
||||||
.storage
|
|
||||||
.from('documents')
|
|
||||||
.createSignedUrl(widget.storagePath!, 60);
|
|
||||||
pdfData = await InternetFile.get(signedUrl);
|
pdfData = await InternetFile.get(signedUrl);
|
||||||
} else {
|
} else {
|
||||||
throw Exception("Nessun documento trovato");
|
throw Exception("Nessun documento trovato");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
import 'package:flux/features/customers/data/customer_repository.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:flux/features/customers/models/customer_model.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
|
|||||||
93
lib/features/customers/blocs/customer_files_bloc.dart
Normal file
93
lib/features/customers/blocs/customer_files_bloc.dart
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib/features/customers/blocs/customer_files_events.dart
Normal file
26
lib/features/customers/blocs/customer_files_events.dart
Normal 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);
|
||||||
|
}
|
||||||
34
lib/features/customers/blocs/customer_files_state.dart
Normal file
34
lib/features/customers/blocs/customer_files_state.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,27 @@
|
|||||||
part of 'customer_cubit.dart';
|
part of 'customer_cubit.dart';
|
||||||
|
|
||||||
enum CustomerStatus { initial, loading, success, failure }
|
enum CustomerStatus {
|
||||||
|
initial,
|
||||||
|
loading,
|
||||||
|
filesLoading,
|
||||||
|
filesUploading,
|
||||||
|
success,
|
||||||
|
failure,
|
||||||
|
}
|
||||||
|
|
||||||
class CustomerState extends Equatable {
|
class CustomerState extends Equatable {
|
||||||
final CustomerStatus status;
|
final CustomerStatus status;
|
||||||
final List<CustomerModel> customers;
|
final List<CustomerModel> customers;
|
||||||
final CustomerModel? lastCreatedCustomer;
|
final CustomerModel? lastCreatedCustomer;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
final List<CustomerFileModel> customerFiles;
|
||||||
|
|
||||||
const CustomerState({
|
const CustomerState({
|
||||||
this.status = CustomerStatus.initial,
|
this.status = CustomerStatus.initial,
|
||||||
this.customers = const [],
|
this.customers = const [],
|
||||||
this.lastCreatedCustomer,
|
this.lastCreatedCustomer,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.customerFiles = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
CustomerState copyWith({
|
CustomerState copyWith({
|
||||||
@@ -20,12 +29,14 @@ class CustomerState extends Equatable {
|
|||||||
List<CustomerModel>? customers,
|
List<CustomerModel>? customers,
|
||||||
CustomerModel? lastCreatedCustomer,
|
CustomerModel? lastCreatedCustomer,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
|
List<CustomerFileModel>? customerFiles,
|
||||||
}) {
|
}) {
|
||||||
return CustomerState(
|
return CustomerState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
customers: customers ?? this.customers,
|
customers: customers ?? this.customers,
|
||||||
lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer,
|
lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer,
|
||||||
errorMessage: errorMessage ?? this.errorMessage,
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
customerFiles: customerFiles ?? this.customerFiles,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,5 +46,6 @@ class CustomerState extends Equatable {
|
|||||||
customers,
|
customers,
|
||||||
lastCreatedCustomer,
|
lastCreatedCustomer,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
customerFiles,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flux/core/blocs/session/session_cubit.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/core/utils/string_extensions.dart';
|
||||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||||
import 'package:get_it/get_it.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
|
/// Recupera i file di un cliente specifico
|
||||||
Future<List<CustomerFileModel>> getCustomerFiles(String customerId) async {
|
Future<List<CustomerFileModel>> getCustomerFiles(String customerId) async {
|
||||||
try {
|
try {
|
||||||
@@ -113,7 +129,7 @@ class CustomerRepository {
|
|||||||
customerId: customerId,
|
customerId: customerId,
|
||||||
name: cleanFileName.fileNameWithoutExtension(),
|
name: cleanFileName.fileNameWithoutExtension(),
|
||||||
extension: cleanFileName.fileExtension(),
|
extension: cleanFileName.fileExtension(),
|
||||||
url: storagePath,
|
storagePath: storagePath,
|
||||||
fileSize: fileSize,
|
fileSize: fileSize,
|
||||||
);
|
);
|
||||||
final String mimeType = fileToSave.extension.toLowerCase() == 'pdf'
|
final String mimeType = fileToSave.extension.toLowerCase() == 'pdf'
|
||||||
@@ -161,9 +177,13 @@ class CustomerRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Elimina un file dallo storage
|
/// Elimina un file dallo storage
|
||||||
Future<void> deleteDocument(String fullPath) async {
|
Future<void> deleteDocument(CustomerFileModel file) async {
|
||||||
// Il path dovrebbe essere ricavato dall'URL
|
try {
|
||||||
final path = fullPath.split('documents/').last;
|
final path = await getSignedUrl(file.storagePath);
|
||||||
await _supabase.storage.from('documents').remove([path]);
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class CustomerFileModel extends Equatable {
|
|||||||
final String? id;
|
final String? id;
|
||||||
final String customerId; // Riferimento UUID
|
final String customerId; // Riferimento UUID
|
||||||
final String name;
|
final String name;
|
||||||
final String url;
|
final String storagePath;
|
||||||
final String extension;
|
final String extension;
|
||||||
final DateTime? createdAt;
|
final DateTime? createdAt;
|
||||||
final int fileSize;
|
final int fileSize;
|
||||||
@@ -13,7 +13,7 @@ class CustomerFileModel extends Equatable {
|
|||||||
this.id,
|
this.id,
|
||||||
required this.customerId,
|
required this.customerId,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.url,
|
required this.storagePath,
|
||||||
required this.extension,
|
required this.extension,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
required this.fileSize,
|
required this.fileSize,
|
||||||
@@ -35,7 +35,7 @@ class CustomerFileModel extends Equatable {
|
|||||||
String? id,
|
String? id,
|
||||||
String? customerId,
|
String? customerId,
|
||||||
String? name,
|
String? name,
|
||||||
String? url,
|
String? storagePath,
|
||||||
String? extension,
|
String? extension,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
int? fileSize,
|
int? fileSize,
|
||||||
@@ -44,7 +44,7 @@ class CustomerFileModel extends Equatable {
|
|||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
customerId: customerId ?? this.customerId,
|
customerId: customerId ?? this.customerId,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
url: url ?? this.url,
|
storagePath: storagePath ?? this.storagePath,
|
||||||
extension: extension ?? this.extension,
|
extension: extension ?? this.extension,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
fileSize: fileSize ?? this.fileSize,
|
fileSize: fileSize ?? this.fileSize,
|
||||||
@@ -56,7 +56,7 @@ class CustomerFileModel extends Equatable {
|
|||||||
id: map['id'] as String,
|
id: map['id'] as String,
|
||||||
customerId: map['customer_id'],
|
customerId: map['customer_id'],
|
||||||
name: map['name'],
|
name: map['name'],
|
||||||
url: map['url'],
|
storagePath: map['storage_path'],
|
||||||
extension: map['extension'] ?? '',
|
extension: map['extension'] ?? '',
|
||||||
createdAt: map['created_at'] != null
|
createdAt: map['created_at'] != null
|
||||||
? DateTime.parse(map['created_at'])
|
? DateTime.parse(map['created_at'])
|
||||||
@@ -72,7 +72,7 @@ class CustomerFileModel extends Equatable {
|
|||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
'customer_id': customerId,
|
'customer_id': customerId,
|
||||||
'name': name,
|
'name': name,
|
||||||
'url': url,
|
'storage_path': storagePath,
|
||||||
'extension': extension,
|
'extension': extension,
|
||||||
'file_size': fileSize,
|
'file_size': fileSize,
|
||||||
};
|
};
|
||||||
@@ -83,7 +83,7 @@ class CustomerFileModel extends Equatable {
|
|||||||
id,
|
id,
|
||||||
customerId,
|
customerId,
|
||||||
name,
|
name,
|
||||||
url,
|
storagePath,
|
||||||
extension,
|
extension,
|
||||||
createdAt,
|
createdAt,
|
||||||
fileSize,
|
fileSize,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:file_picker/file_picker.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/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_model.dart';
|
||||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
|
||||||
|
|
||||||
class CustomerDetailScreen extends StatefulWidget {
|
class CustomerDetailScreen extends StatefulWidget {
|
||||||
final CustomerModel customer;
|
final CustomerModel customer;
|
||||||
@@ -15,36 +17,19 @@ class CustomerDetailScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
|
class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
|
||||||
final _repository = GetIt.I<CustomerRepository>();
|
|
||||||
List<CustomerFileModel> _files = [];
|
|
||||||
bool _isLoadingFiles = true;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadFiles();
|
_loadFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadFiles() async {
|
void _loadFiles() {
|
||||||
try {
|
context.read<CustomerFilesBloc>().add(LoadCustomerFilesEvent());
|
||||||
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())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickAndUpload() async {
|
Future<void> _pickAndUpload() async {
|
||||||
|
CustomerFilesBloc customerFilesBloc = context.read<CustomerFilesBloc>();
|
||||||
|
|
||||||
// Chiamata statica pulita
|
// Chiamata statica pulita
|
||||||
FilePickerResult? result = await FilePicker.pickFiles(
|
FilePickerResult? result = await FilePicker.pickFiles(
|
||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
@@ -55,11 +40,9 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
|
|||||||
if (result != null) {
|
if (result != null) {
|
||||||
for (var pickedFile in result.files) {
|
for (var pickedFile in result.files) {
|
||||||
try {
|
try {
|
||||||
final newFile = await _repository.uploadAndRegisterFile(
|
customerFilesBloc.add(
|
||||||
customerId: widget.customer.id.toString(),
|
UploadCustomerFileEvent(pickedFile: pickedFile),
|
||||||
pickedFile: pickedFile,
|
|
||||||
);
|
);
|
||||||
setState(() => _files.add(newFile));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -158,46 +141,51 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDocumentSection() {
|
Widget _buildDocumentSection() {
|
||||||
return Column(
|
return BlocBuilder<CustomerFilesBloc, CustomerFilesState>(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
builder: (context, state) {
|
||||||
children: [
|
return Column(
|
||||||
Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
"DOCUMENTI",
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
style: TextStyle(
|
children: [
|
||||||
fontSize: 18,
|
Text(
|
||||||
fontWeight: FontWeight.bold,
|
"DOCUMENTI",
|
||||||
color: context.accent,
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: context.accent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _pickAndUpload,
|
||||||
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
|
label: const Text("CARICA FILE"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
if (state.status == CustomerFilesStatus.loading)
|
||||||
|
const Center(child: CircularProgressIndicator())
|
||||||
|
else if (state.customerFiles.isEmpty)
|
||||||
|
const Center(child: Text("Nessun documento presente"))
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: GridView.builder(
|
||||||
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 200,
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
crossAxisSpacing: 16,
|
||||||
|
childAspectRatio: 1.2,
|
||||||
|
),
|
||||||
|
itemCount: state.customerFiles.length,
|
||||||
|
itemBuilder: (context, index) =>
|
||||||
|
_FileCard(file: state.customerFiles[index], state: state),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: _pickAndUpload,
|
|
||||||
icon: const Icon(Icons.add_circle_outline),
|
|
||||||
label: const Text("CARICA FILE"),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
const SizedBox(height: 20),
|
},
|
||||||
if (_isLoadingFiles)
|
|
||||||
const Center(child: CircularProgressIndicator())
|
|
||||||
else if (_files.isEmpty)
|
|
||||||
const Center(child: Text("Nessun documento presente"))
|
|
||||||
else
|
|
||||||
Expanded(
|
|
||||||
child: GridView.builder(
|
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
|
||||||
maxCrossAxisExtent: 200,
|
|
||||||
mainAxisSpacing: 16,
|
|
||||||
crossAxisSpacing: 16,
|
|
||||||
childAspectRatio: 1.2,
|
|
||||||
),
|
|
||||||
itemCount: _files.length,
|
|
||||||
itemBuilder: (context, index) => _FileCard(file: _files[index]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,30 +215,54 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
|
|||||||
|
|
||||||
class _FileCard extends StatelessWidget {
|
class _FileCard extends StatelessWidget {
|
||||||
final CustomerFileModel file;
|
final CustomerFileModel file;
|
||||||
const _FileCard({required this.file});
|
final CustomerFilesState state;
|
||||||
|
const _FileCard({required this.file, required this.state});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return GestureDetector(
|
||||||
decoration: BoxDecoration(
|
onTap: () => context.read<CustomerFilesBloc>().add(
|
||||||
color: context.background,
|
ToggleCustomerFileSelectionEvent(file),
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: context.accent.withValues(alpha: 0.1)),
|
|
||||||
),
|
),
|
||||||
child: Column(
|
onDoubleTap: () => _handleDoubleClickOnFile(context, file),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Icon(_getFileIcon(file.extension), size: 48, color: context.accent),
|
Container(
|
||||||
const SizedBox(height: 8),
|
decoration: BoxDecoration(
|
||||||
Padding(
|
color: context.background,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Text(
|
border: Border.all(color: context.accent.withValues(alpha: 0.1)),
|
||||||
file.name,
|
),
|
||||||
maxLines: 1,
|
child: Column(
|
||||||
overflow: TextOverflow.ellipsis,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
children: [
|
||||||
|
Icon(
|
||||||
|
_getFileIcon(file.extension),
|
||||||
|
size: 48,
|
||||||
|
color: context.accent,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Text(
|
||||||
|
file.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
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;
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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/theme/theme.dart';
|
import 'package:flux/core/theme/theme.dart';
|
||||||
import 'package:flux/features/customers/blocs/customer_cubit.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/models/customer_model.dart';
|
||||||
import 'package:flux/features/customers/ui/customer_form.dart';
|
import 'package:flux/features/customers/ui/customer_form.dart';
|
||||||
import 'package:go_router/go_router.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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -85,7 +53,7 @@ class _CustomersContentState extends State<CustomersContent> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 16),
|
padding: const EdgeInsets.only(right: 16),
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () => _openCustomerForm(),
|
onPressed: () => openCustomerForm(context: context),
|
||||||
icon: const Icon(Icons.person_add_alt_1_rounded, size: 20),
|
icon: const Icon(Icons.person_add_alt_1_rounded, size: 20),
|
||||||
label: const Text('NUOVO'),
|
label: const Text('NUOVO'),
|
||||||
style: ElevatedButton.styleFrom(
|
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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
serviceId: state.currentService?.id ?? '',
|
serviceId: state.currentService?.id ?? '',
|
||||||
name: file.name.fileNameWithoutExtension(),
|
name: file.name.fileNameWithoutExtension(),
|
||||||
extension: file.name.fileExtension(),
|
extension: file.name.fileExtension(),
|
||||||
url: '',
|
storagePath: '',
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
localBytes: file.bytes,
|
localBytes: file.bytes,
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
@@ -295,7 +295,7 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
orElse: () => file,
|
orElse: () => file,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (savedFile.url.isEmpty) {
|
if (savedFile.storagePath.isEmpty) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
"Errore: URL del file non trovato dopo il salvataggio.",
|
"Errore: URL del file non trovato dopo il salvataggio.",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -159,7 +159,10 @@ class ServicesRepository {
|
|||||||
final String mimeType = file.extension.toLowerCase() == 'pdf'
|
final String mimeType = file.extension.toLowerCase() == 'pdf'
|
||||||
? 'application/pdf'
|
? 'application/pdf'
|
||||||
: 'image/${file.extension}';
|
: '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
|
// Creiamo una funzione asincrona per caricare file e scrivere nel DB
|
||||||
Future<void> uploadAndLink() async {
|
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({
|
Future<void> copyFileToCustomer({
|
||||||
required ServiceFileModel file,
|
required ServiceFileModel file,
|
||||||
required String customerId,
|
required String customerId,
|
||||||
@@ -242,7 +254,7 @@ class ServicesRepository {
|
|||||||
CustomerFileModel fileToCopy = CustomerFileModel(
|
CustomerFileModel fileToCopy = CustomerFileModel(
|
||||||
customerId: customerId,
|
customerId: customerId,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
url: file.url,
|
storagePath: file.storagePath,
|
||||||
extension: file.extension,
|
extension: file.extension,
|
||||||
fileSize: file.fileSize,
|
fileSize: file.fileSize,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class ServiceFileModel extends Equatable {
|
|||||||
final DateTime? createdAt;
|
final DateTime? createdAt;
|
||||||
final String name;
|
final String name;
|
||||||
final String extension;
|
final String extension;
|
||||||
final String url;
|
final String storagePath;
|
||||||
final String serviceId;
|
final String serviceId;
|
||||||
final int fileSize;
|
final int fileSize;
|
||||||
final Uint8List? localBytes;
|
final Uint8List? localBytes;
|
||||||
@@ -17,7 +17,7 @@ class ServiceFileModel extends Equatable {
|
|||||||
this.createdAt,
|
this.createdAt,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.extension,
|
required this.extension,
|
||||||
required this.url,
|
required this.storagePath,
|
||||||
required this.serviceId,
|
required this.serviceId,
|
||||||
required this.fileSize,
|
required this.fileSize,
|
||||||
this.localBytes,
|
this.localBytes,
|
||||||
@@ -40,7 +40,7 @@ class ServiceFileModel extends Equatable {
|
|||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
String? name,
|
String? name,
|
||||||
String? extension,
|
String? extension,
|
||||||
String? url,
|
String? storagePath,
|
||||||
String? serviceId,
|
String? serviceId,
|
||||||
int? fileSize,
|
int? fileSize,
|
||||||
Uint8List? localBytes,
|
Uint8List? localBytes,
|
||||||
@@ -50,7 +50,7 @@ class ServiceFileModel extends Equatable {
|
|||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
extension: extension ?? this.extension,
|
extension: extension ?? this.extension,
|
||||||
url: url ?? this.url,
|
storagePath: storagePath ?? this.storagePath,
|
||||||
serviceId: serviceId ?? this.serviceId,
|
serviceId: serviceId ?? this.serviceId,
|
||||||
fileSize: fileSize ?? this.fileSize,
|
fileSize: fileSize ?? this.fileSize,
|
||||||
localBytes: localBytes ?? this.localBytes,
|
localBytes: localBytes ?? this.localBytes,
|
||||||
@@ -65,7 +65,7 @@ class ServiceFileModel extends Equatable {
|
|||||||
: null,
|
: null,
|
||||||
name: map['name'] ?? '',
|
name: map['name'] ?? '',
|
||||||
extension: map['extension'] ?? '',
|
extension: map['extension'] ?? '',
|
||||||
url: map['url'] ?? '',
|
storagePath: map['storage_path'] ?? '',
|
||||||
serviceId: map['service_id']?.toString() ?? '',
|
serviceId: map['service_id']?.toString() ?? '',
|
||||||
fileSize: map['file_size'] is int
|
fileSize: map['file_size'] is int
|
||||||
? map['file_size']
|
? map['file_size']
|
||||||
@@ -78,7 +78,7 @@ class ServiceFileModel extends Equatable {
|
|||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
'name': name,
|
'name': name,
|
||||||
'extension': extension,
|
'extension': extension,
|
||||||
'url': url,
|
'storage_path': storagePath,
|
||||||
'service_id': serviceId,
|
'service_id': serviceId,
|
||||||
'file_size': fileSize,
|
'file_size': fileSize,
|
||||||
};
|
};
|
||||||
@@ -90,7 +90,7 @@ class ServiceFileModel extends Equatable {
|
|||||||
createdAt,
|
createdAt,
|
||||||
name,
|
name,
|
||||||
extension,
|
extension,
|
||||||
url,
|
storagePath,
|
||||||
serviceId,
|
serviceId,
|
||||||
fileSize,
|
fileSize,
|
||||||
localBytes,
|
localBytes,
|
||||||
|
|||||||
@@ -169,11 +169,15 @@ class AttachmentsSection extends StatelessWidget {
|
|||||||
height: MediaQuery.of(context).size.height * 0.8,
|
height: MediaQuery.of(context).size.height * 0.8,
|
||||||
child: file.isPdf
|
child: file.isPdf
|
||||||
? PdfViewerWidget(
|
? PdfViewerWidget(
|
||||||
storagePath: file.url.isNotEmpty ? file.url : null,
|
storagePath: file.storagePath.isNotEmpty
|
||||||
|
? file.storagePath
|
||||||
|
: null,
|
||||||
bytes: file.localBytes,
|
bytes: file.localBytes,
|
||||||
)
|
)
|
||||||
: ImageViewerWidget(
|
: ImageViewerWidget(
|
||||||
storagePath: file.url.isNotEmpty ? file.url : null,
|
storagePath: file.storagePath.isNotEmpty
|
||||||
|
? file.storagePath
|
||||||
|
: null,
|
||||||
bytes: file.localBytes,
|
bytes: file.localBytes,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,10 +6,14 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <gtk/gtk_plugin.h>
|
#include <gtk/gtk_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
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 =
|
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_linux
|
||||||
gtk
|
gtk
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Foundation
|
|||||||
|
|
||||||
import app_links
|
import app_links
|
||||||
import file_picker
|
import file_picker
|
||||||
|
import file_selector_macos
|
||||||
import pdfx
|
import pdfx
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
@@ -14,6 +15,7 @@ import url_launcher_macos
|
|||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin"))
|
PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- file_picker (0.0.1):
|
- file_picker (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- file_selector_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
- pdfx (1.0.0):
|
- pdfx (1.0.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -15,6 +17,7 @@ PODS:
|
|||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
|
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
|
||||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/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`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- pdfx (from `Flutter/ephemeral/.symlinks/plugins/pdfx/macos`)
|
- pdfx (from `Flutter/ephemeral/.symlinks/plugins/pdfx/macos`)
|
||||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- 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
|
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
|
||||||
file_picker:
|
file_picker:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
||||||
|
file_selector_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
pdfx:
|
pdfx:
|
||||||
@@ -37,6 +42,7 @@ EXTERNAL SOURCES:
|
|||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
|
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
|
||||||
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
|
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
|
||||||
|
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
|
||||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
pdfx: 1e79f57f7a6ce2f4a4c30f21fa54d3dc82441b51
|
pdfx: 1e79f57f7a6ce2f4a4c30f21fa54d3dc82441b51
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
|
|||||||
96
pubspec.lock
96
pubspec.lock
@@ -185,6 +185,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "11.0.2"
|
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:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -328,6 +360,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
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:
|
internet_file:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ dependencies:
|
|||||||
get_it: ^9.2.1
|
get_it: ^9.2.1
|
||||||
go_router: ^17.2.0
|
go_router: ^17.2.0
|
||||||
google_fonts: ^8.0.2
|
google_fonts: ^8.0.2
|
||||||
|
image_picker: ^1.2.1
|
||||||
internet_file: ^1.3.0
|
internet_file: ^1.3.0
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
pdfx: ^2.9.2
|
pdfx: ^2.9.2
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <app_links/app_links_plugin_c_api.h>
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <pdfx/pdfx_plugin.h>
|
#include <pdfx/pdfx_plugin.h>
|
||||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
@@ -14,6 +15,8 @@
|
|||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
AppLinksPluginCApiRegisterWithRegistrar(
|
AppLinksPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
PdfxPluginRegisterWithRegistrar(
|
PdfxPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PdfxPlugin"));
|
registry->GetRegistrarForPlugin("PdfxPlugin"));
|
||||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
app_links
|
app_links
|
||||||
|
file_selector_windows
|
||||||
pdfx
|
pdfx
|
||||||
permission_handler_windows
|
permission_handler_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
|
|||||||
Reference in New Issue
Block a user