Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
63
lib/features/backup/data/models/backup_config_model.dart
Normal file
63
lib/features/backup/data/models/backup_config_model.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
/// Modèle de configuration des sauvegardes
|
||||
/// Correspond à BackupConfigResponse du backend
|
||||
library backup_config_model;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'backup_config_model.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class BackupConfigModel extends Equatable {
|
||||
final bool? autoBackupEnabled;
|
||||
final String? frequency; // HOURLY, DAILY, WEEKLY
|
||||
final String? retention;
|
||||
final int? retentionDays;
|
||||
final String? backupTime;
|
||||
final bool? includeDatabase;
|
||||
final bool? includeFiles;
|
||||
final bool? includeConfiguration;
|
||||
final DateTime? lastBackup;
|
||||
final DateTime? nextScheduledBackup;
|
||||
final int? totalBackups;
|
||||
final int? totalSizeBytes;
|
||||
final String? totalSizeFormatted;
|
||||
|
||||
const BackupConfigModel({
|
||||
this.autoBackupEnabled,
|
||||
this.frequency,
|
||||
this.retention,
|
||||
this.retentionDays,
|
||||
this.backupTime,
|
||||
this.includeDatabase,
|
||||
this.includeFiles,
|
||||
this.includeConfiguration,
|
||||
this.lastBackup,
|
||||
this.nextScheduledBackup,
|
||||
this.totalBackups,
|
||||
this.totalSizeBytes,
|
||||
this.totalSizeFormatted,
|
||||
});
|
||||
|
||||
factory BackupConfigModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$BackupConfigModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$BackupConfigModelToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
autoBackupEnabled,
|
||||
frequency,
|
||||
retention,
|
||||
retentionDays,
|
||||
backupTime,
|
||||
includeDatabase,
|
||||
includeFiles,
|
||||
includeConfiguration,
|
||||
lastBackup,
|
||||
nextScheduledBackup,
|
||||
totalBackups,
|
||||
totalSizeBytes,
|
||||
totalSizeFormatted,
|
||||
];
|
||||
}
|
||||
45
lib/features/backup/data/models/backup_config_model.g.dart
Normal file
45
lib/features/backup/data/models/backup_config_model.g.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'backup_config_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
BackupConfigModel _$BackupConfigModelFromJson(Map<String, dynamic> json) =>
|
||||
BackupConfigModel(
|
||||
autoBackupEnabled: json['autoBackupEnabled'] as bool?,
|
||||
frequency: json['frequency'] as String?,
|
||||
retention: json['retention'] as String?,
|
||||
retentionDays: (json['retentionDays'] as num?)?.toInt(),
|
||||
backupTime: json['backupTime'] as String?,
|
||||
includeDatabase: json['includeDatabase'] as bool?,
|
||||
includeFiles: json['includeFiles'] as bool?,
|
||||
includeConfiguration: json['includeConfiguration'] as bool?,
|
||||
lastBackup: json['lastBackup'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastBackup'] as String),
|
||||
nextScheduledBackup: json['nextScheduledBackup'] == null
|
||||
? null
|
||||
: DateTime.parse(json['nextScheduledBackup'] as String),
|
||||
totalBackups: (json['totalBackups'] as num?)?.toInt(),
|
||||
totalSizeBytes: (json['totalSizeBytes'] as num?)?.toInt(),
|
||||
totalSizeFormatted: json['totalSizeFormatted'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BackupConfigModelToJson(BackupConfigModel instance) =>
|
||||
<String, dynamic>{
|
||||
'autoBackupEnabled': instance.autoBackupEnabled,
|
||||
'frequency': instance.frequency,
|
||||
'retention': instance.retention,
|
||||
'retentionDays': instance.retentionDays,
|
||||
'backupTime': instance.backupTime,
|
||||
'includeDatabase': instance.includeDatabase,
|
||||
'includeFiles': instance.includeFiles,
|
||||
'includeConfiguration': instance.includeConfiguration,
|
||||
'lastBackup': instance.lastBackup?.toIso8601String(),
|
||||
'nextScheduledBackup': instance.nextScheduledBackup?.toIso8601String(),
|
||||
'totalBackups': instance.totalBackups,
|
||||
'totalSizeBytes': instance.totalSizeBytes,
|
||||
'totalSizeFormatted': instance.totalSizeFormatted,
|
||||
};
|
||||
69
lib/features/backup/data/models/backup_model.dart
Normal file
69
lib/features/backup/data/models/backup_model.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
/// Modèle de sauvegarde
|
||||
/// Correspond à BackupResponse du backend
|
||||
library backup_model;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'backup_model.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class BackupModel extends Equatable {
|
||||
final String? id;
|
||||
final String? name;
|
||||
final String? description;
|
||||
final String? type; // AUTO, MANUAL, RESTORE_POINT
|
||||
final int? sizeBytes;
|
||||
final String? sizeFormatted;
|
||||
final String? status; // PENDING, IN_PROGRESS, COMPLETED, FAILED
|
||||
final DateTime? createdAt;
|
||||
final DateTime? completedAt;
|
||||
final String? createdBy;
|
||||
final bool? includesDatabase;
|
||||
final bool? includesFiles;
|
||||
final bool? includesConfiguration;
|
||||
final String? filePath;
|
||||
final String? errorMessage;
|
||||
|
||||
const BackupModel({
|
||||
this.id,
|
||||
this.name,
|
||||
this.description,
|
||||
this.type,
|
||||
this.sizeBytes,
|
||||
this.sizeFormatted,
|
||||
this.status,
|
||||
this.createdAt,
|
||||
this.completedAt,
|
||||
this.createdBy,
|
||||
this.includesDatabase,
|
||||
this.includesFiles,
|
||||
this.includesConfiguration,
|
||||
this.filePath,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
factory BackupModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$BackupModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$BackupModelToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
type,
|
||||
sizeBytes,
|
||||
sizeFormatted,
|
||||
status,
|
||||
createdAt,
|
||||
completedAt,
|
||||
createdBy,
|
||||
includesDatabase,
|
||||
includesFiles,
|
||||
includesConfiguration,
|
||||
filePath,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
48
lib/features/backup/data/models/backup_model.g.dart
Normal file
48
lib/features/backup/data/models/backup_model.g.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'backup_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
BackupModel _$BackupModelFromJson(Map<String, dynamic> json) => BackupModel(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
description: json['description'] as String?,
|
||||
type: json['type'] as String?,
|
||||
sizeBytes: (json['sizeBytes'] as num?)?.toInt(),
|
||||
sizeFormatted: json['sizeFormatted'] as String?,
|
||||
status: json['status'] as String?,
|
||||
createdAt: json['createdAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['createdAt'] as String),
|
||||
completedAt: json['completedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['completedAt'] as String),
|
||||
createdBy: json['createdBy'] as String?,
|
||||
includesDatabase: json['includesDatabase'] as bool?,
|
||||
includesFiles: json['includesFiles'] as bool?,
|
||||
includesConfiguration: json['includesConfiguration'] as bool?,
|
||||
filePath: json['filePath'] as String?,
|
||||
errorMessage: json['errorMessage'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BackupModelToJson(BackupModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'type': instance.type,
|
||||
'sizeBytes': instance.sizeBytes,
|
||||
'sizeFormatted': instance.sizeFormatted,
|
||||
'status': instance.status,
|
||||
'createdAt': instance.createdAt?.toIso8601String(),
|
||||
'completedAt': instance.completedAt?.toIso8601String(),
|
||||
'createdBy': instance.createdBy,
|
||||
'includesDatabase': instance.includesDatabase,
|
||||
'includesFiles': instance.includesFiles,
|
||||
'includesConfiguration': instance.includesConfiguration,
|
||||
'filePath': instance.filePath,
|
||||
'errorMessage': instance.errorMessage,
|
||||
};
|
||||
131
lib/features/backup/data/repositories/backup_repository.dart
Normal file
131
lib/features/backup/data/repositories/backup_repository.dart
Normal file
@@ -0,0 +1,131 @@
|
||||
/// Repository pour la gestion des sauvegardes
|
||||
/// Interface avec l'API backend BackupResource
|
||||
library backup_repository;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
|
||||
import '../models/backup_model.dart';
|
||||
import '../models/backup_config_model.dart';
|
||||
|
||||
abstract class BackupRepository {
|
||||
Future<List<BackupModel>> getAll();
|
||||
Future<BackupModel> getById(String id);
|
||||
Future<BackupModel> create(String name, {String? description});
|
||||
Future<void> restore(String backupId, {bool createRestorePoint = true});
|
||||
Future<void> delete(String id);
|
||||
Future<BackupConfigModel> getConfig();
|
||||
Future<BackupConfigModel> updateConfig(Map<String, dynamic> config);
|
||||
Future<BackupModel> createRestorePoint();
|
||||
}
|
||||
|
||||
@LazySingleton(as: BackupRepository)
|
||||
class BackupRepositoryImpl implements BackupRepository {
|
||||
final ApiClient _apiClient;
|
||||
static const String _base = '/api/backups';
|
||||
|
||||
BackupRepositoryImpl(this._apiClient);
|
||||
|
||||
List<BackupModel> _parseListResponse(dynamic data) {
|
||||
if (data is List) {
|
||||
return data
|
||||
.map((e) => BackupModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
if (data is Map && data.containsKey('content')) {
|
||||
final content = data['content'] as List<dynamic>? ?? [];
|
||||
return content
|
||||
.map((e) => BackupModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<BackupModel>> getAll() async {
|
||||
final response = await _apiClient.get(_base);
|
||||
if (response.statusCode == 200) {
|
||||
return _parseListResponse(response.data);
|
||||
}
|
||||
throw Exception('Erreur ${response.statusCode}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BackupModel> getById(String id) async {
|
||||
final response = await _apiClient.get('$_base/$id');
|
||||
if (response.statusCode == 200) {
|
||||
return BackupModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
throw Exception('Erreur ${response.statusCode}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BackupModel> create(String name, {String? description}) async {
|
||||
final response = await _apiClient.post(
|
||||
_base,
|
||||
data: {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'type': 'MANUAL',
|
||||
'includeDatabase': true,
|
||||
'includeFiles': true,
|
||||
'includeConfiguration': true,
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return BackupModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
throw Exception('Erreur ${response.statusCode}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> restore(String backupId, {bool createRestorePoint = true}) async {
|
||||
final response = await _apiClient.post(
|
||||
'$_base/restore',
|
||||
data: {
|
||||
'backupId': backupId,
|
||||
'restoreDatabase': true,
|
||||
'restoreFiles': true,
|
||||
'restoreConfiguration': true,
|
||||
'createRestorePoint': createRestorePoint,
|
||||
},
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Erreur ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String id) async {
|
||||
final response = await _apiClient.delete('$_base/$id');
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Erreur ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BackupConfigModel> getConfig() async {
|
||||
final response = await _apiClient.get('$_base/config');
|
||||
if (response.statusCode == 200) {
|
||||
return BackupConfigModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
throw Exception('Erreur ${response.statusCode}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BackupConfigModel> updateConfig(Map<String, dynamic> config) async {
|
||||
final response = await _apiClient.put('$_base/config', data: config);
|
||||
if (response.statusCode == 200) {
|
||||
return BackupConfigModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
throw Exception('Erreur ${response.statusCode}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BackupModel> createRestorePoint() async {
|
||||
final response = await _apiClient.post('$_base/restore-point');
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return BackupModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
throw Exception('Erreur ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
166
lib/features/backup/presentation/bloc/backup_bloc.dart
Normal file
166
lib/features/backup/presentation/bloc/backup_bloc.dart
Normal file
@@ -0,0 +1,166 @@
|
||||
/// BLoC pour la gestion des sauvegardes
|
||||
library backup_bloc;
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../data/repositories/backup_repository.dart';
|
||||
import '../../data/models/backup_model.dart';
|
||||
import '../../data/models/backup_config_model.dart';
|
||||
|
||||
// Events
|
||||
abstract class BackupEvent extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class LoadBackups extends BackupEvent {}
|
||||
|
||||
class CreateBackup extends BackupEvent {
|
||||
final String name;
|
||||
final String? description;
|
||||
CreateBackup(this.name, {this.description});
|
||||
@override
|
||||
List<Object?> get props => [name, description];
|
||||
}
|
||||
|
||||
class RestoreBackup extends BackupEvent {
|
||||
final String backupId;
|
||||
RestoreBackup(this.backupId);
|
||||
@override
|
||||
List<Object?> get props => [backupId];
|
||||
}
|
||||
|
||||
class DeleteBackup extends BackupEvent {
|
||||
final String backupId;
|
||||
DeleteBackup(this.backupId);
|
||||
@override
|
||||
List<Object?> get props => [backupId];
|
||||
}
|
||||
|
||||
class LoadBackupConfig extends BackupEvent {}
|
||||
|
||||
class UpdateBackupConfig extends BackupEvent {
|
||||
final Map<String, dynamic> config;
|
||||
UpdateBackupConfig(this.config);
|
||||
@override
|
||||
List<Object?> get props => [config];
|
||||
}
|
||||
|
||||
// States
|
||||
abstract class BackupState extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class BackupInitial extends BackupState {}
|
||||
|
||||
class BackupLoading extends BackupState {}
|
||||
|
||||
class BackupsLoaded extends BackupState {
|
||||
final List<BackupModel> backups;
|
||||
BackupsLoaded(this.backups);
|
||||
@override
|
||||
List<Object?> get props => [backups];
|
||||
}
|
||||
|
||||
class BackupConfigLoaded extends BackupState {
|
||||
final BackupConfigModel config;
|
||||
BackupConfigLoaded(this.config);
|
||||
@override
|
||||
List<Object?> get props => [config];
|
||||
}
|
||||
|
||||
class BackupSuccess extends BackupState {
|
||||
final String message;
|
||||
BackupSuccess(this.message);
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
class BackupError extends BackupState {
|
||||
final String error;
|
||||
BackupError(this.error);
|
||||
@override
|
||||
List<Object?> get props => [error];
|
||||
}
|
||||
|
||||
// Bloc
|
||||
@injectable
|
||||
class BackupBloc extends Bloc<BackupEvent, BackupState> {
|
||||
final BackupRepository _repository;
|
||||
|
||||
BackupBloc(this._repository) : super(BackupInitial()) {
|
||||
on<LoadBackups>(_onLoadBackups);
|
||||
on<CreateBackup>(_onCreateBackup);
|
||||
on<RestoreBackup>(_onRestoreBackup);
|
||||
on<DeleteBackup>(_onDeleteBackup);
|
||||
on<LoadBackupConfig>(_onLoadBackupConfig);
|
||||
on<UpdateBackupConfig>(_onUpdateBackupConfig);
|
||||
}
|
||||
|
||||
Future<void> _onLoadBackups(LoadBackups event, Emitter<BackupState> emit) async {
|
||||
emit(BackupLoading());
|
||||
try {
|
||||
final backups = await _repository.getAll();
|
||||
emit(BackupsLoaded(backups));
|
||||
} catch (e) {
|
||||
emit(BackupError('Erreur: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCreateBackup(CreateBackup event, Emitter<BackupState> emit) async {
|
||||
emit(BackupLoading());
|
||||
try {
|
||||
await _repository.create(event.name, description: event.description);
|
||||
final backups = await _repository.getAll();
|
||||
emit(BackupsLoaded(backups));
|
||||
emit(BackupSuccess('Sauvegarde créée'));
|
||||
} catch (e) {
|
||||
emit(BackupError('Erreur: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRestoreBackup(RestoreBackup event, Emitter<BackupState> emit) async {
|
||||
emit(BackupLoading());
|
||||
try {
|
||||
await _repository.restore(event.backupId);
|
||||
emit(BackupSuccess('Restauration en cours'));
|
||||
} catch (e) {
|
||||
emit(BackupError('Erreur: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDeleteBackup(DeleteBackup event, Emitter<BackupState> emit) async {
|
||||
emit(BackupLoading());
|
||||
try {
|
||||
await _repository.delete(event.backupId);
|
||||
final backups = await _repository.getAll();
|
||||
emit(BackupsLoaded(backups));
|
||||
emit(BackupSuccess('Sauvegarde supprimée'));
|
||||
} catch (e) {
|
||||
emit(BackupError('Erreur: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadBackupConfig(LoadBackupConfig event, Emitter<BackupState> emit) async {
|
||||
emit(BackupLoading());
|
||||
try {
|
||||
final config = await _repository.getConfig();
|
||||
emit(BackupConfigLoaded(config));
|
||||
} catch (e) {
|
||||
emit(BackupError('Erreur: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onUpdateBackupConfig(UpdateBackupConfig event, Emitter<BackupState> emit) async {
|
||||
emit(BackupLoading());
|
||||
try {
|
||||
final config = await _repository.updateConfig(event.config);
|
||||
emit(BackupConfigLoaded(config));
|
||||
emit(BackupSuccess('Configuration mise à jour'));
|
||||
} catch (e) {
|
||||
emit(BackupError('Erreur: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
}
|
||||
773
lib/features/backup/presentation/pages/backup_page.dart
Normal file
773
lib/features/backup/presentation/pages/backup_page.dart
Normal file
@@ -0,0 +1,773 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import '../../../../shared/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../shared/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../../core/utils/logger.dart';
|
||||
import '../../data/models/backup_model.dart';
|
||||
import '../../data/models/backup_config_model.dart';
|
||||
import '../../data/repositories/backup_repository.dart';
|
||||
import '../bloc/backup_bloc.dart';
|
||||
|
||||
/// Page Sauvegarde & Restauration - UnionFlow Mobile
|
||||
///
|
||||
/// Page complète de gestion des sauvegardes avec création, restauration,
|
||||
/// planification et monitoring des sauvegardes système.
|
||||
class BackupPage extends StatefulWidget {
|
||||
const BackupPage({super.key});
|
||||
|
||||
@override
|
||||
State<BackupPage> createState() => _BackupPageState();
|
||||
}
|
||||
|
||||
class _BackupPageState extends State<BackupPage>
|
||||
with TickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
bool _autoBackupEnabled = true;
|
||||
String _selectedFrequency = 'Quotidien';
|
||||
String _selectedRetention = '30 jours';
|
||||
|
||||
List<BackupModel>? _cachedBackups;
|
||||
BackupConfigModel? _cachedConfig;
|
||||
|
||||
final List<String> _frequencies = ['Horaire', 'Quotidien', 'Hebdomadaire'];
|
||||
final List<String> _retentions = ['7 jours', '30 jours', '90 jours', '1 an'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => sl<BackupBloc>()
|
||||
..add(LoadBackups())
|
||||
..add(LoadBackupConfig()),
|
||||
child: BlocConsumer<BackupBloc, BackupState>(
|
||||
listener: (context, state) {
|
||||
if (state is BackupsLoaded) {
|
||||
_cachedBackups = state.backups;
|
||||
} else if (state is BackupConfigLoaded) {
|
||||
_cachedConfig = state.config;
|
||||
}
|
||||
if (state is BackupSuccess) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: const Color(0xFF00B894),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
} else if (state is BackupError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.error),
|
||||
backgroundColor: const Color(0xFFD63031),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
backgroundColor: ColorTokens.background,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
_buildTabBar(),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildBackupsTab(state),
|
||||
_buildScheduleTab(),
|
||||
_buildRestoreTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Header harmonisé
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(SpacingTokens.lg),
|
||||
padding: const EdgeInsets.all(SpacingTokens.xl),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: ColorTokens.primaryGradient,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(SpacingTokens.radiusXl),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorTokens.primary.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.backup,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Sauvegarde & Restauration',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Gestion des sauvegardes système',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => _createBackupNow(),
|
||||
icon: const Icon(
|
||||
Icons.save,
|
||||
color: Colors.white,
|
||||
),
|
||||
tooltip: 'Sauvegarde immédiate',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Dernière sauvegarde',
|
||||
_lastBackupDisplay(),
|
||||
Icons.schedule,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Taille totale',
|
||||
_totalSizeDisplay(),
|
||||
Icons.storage,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Statut',
|
||||
_statusDisplay(),
|
||||
Icons.check_circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _lastBackupDisplay() {
|
||||
if (_cachedConfig?.lastBackup != null) {
|
||||
final d = _cachedConfig!.lastBackup!;
|
||||
final diff = DateTime.now().difference(d);
|
||||
if (diff.inMinutes < 60) return '${diff.inMinutes} min';
|
||||
if (diff.inHours < 24) return '${diff.inHours}h';
|
||||
if (diff.inDays < 7) return '${diff.inDays} j';
|
||||
return '${d.day}/${d.month}/${d.year}';
|
||||
}
|
||||
if (_cachedBackups != null && _cachedBackups!.isNotEmpty) {
|
||||
final sorted = List<BackupModel>.from(_cachedBackups!)
|
||||
..sort((a, b) => (b.createdAt ?? DateTime(0)).compareTo(a.createdAt ?? DateTime(0)));
|
||||
final d = sorted.first.createdAt;
|
||||
if (d != null) {
|
||||
final diff = DateTime.now().difference(d);
|
||||
if (diff.inMinutes < 60) return '${diff.inMinutes} min';
|
||||
if (diff.inHours < 24) return '${diff.inHours}h';
|
||||
return '${diff.inDays} j';
|
||||
}
|
||||
}
|
||||
return '—';
|
||||
}
|
||||
|
||||
String _totalSizeDisplay() {
|
||||
if (_cachedConfig?.totalSizeFormatted != null && _cachedConfig!.totalSizeFormatted!.isNotEmpty) {
|
||||
return _cachedConfig!.totalSizeFormatted!;
|
||||
}
|
||||
if (_cachedBackups != null && _cachedBackups!.isNotEmpty) {
|
||||
int total = 0;
|
||||
for (final b in _cachedBackups!) {
|
||||
total += b.sizeBytes ?? 0;
|
||||
}
|
||||
if (total >= 1024 * 1024 * 1024) return '${(total / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||
if (total >= 1024 * 1024) return '${(total / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
if (total >= 1024) return '${(total / 1024).toStringAsFixed(0)} KB';
|
||||
return '$total B';
|
||||
}
|
||||
return '0 B';
|
||||
}
|
||||
|
||||
String _statusDisplay() {
|
||||
if (_cachedBackups != null && _cachedBackups!.isNotEmpty) {
|
||||
final sorted = List<BackupModel>.from(_cachedBackups!)
|
||||
..sort((a, b) => (b.createdAt ?? DateTime(0)).compareTo(a.createdAt ?? DateTime(0)));
|
||||
final s = sorted.first.status;
|
||||
if (s == 'COMPLETED') return 'OK';
|
||||
if (s == 'FAILED') return 'Erreur';
|
||||
if (s == 'IN_PROGRESS') return 'En cours';
|
||||
}
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
/// Carte de statistique
|
||||
Widget _buildStatCard(String label, String value, IconData icon) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: Colors.white, size: 20),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Barre d'onglets
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: const Color(0xFF6C5CE7),
|
||||
unselectedLabelColor: Colors.grey[600],
|
||||
indicatorColor: const Color(0xFF6C5CE7),
|
||||
indicatorWeight: 3,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12),
|
||||
tabs: const [
|
||||
Tab(icon: Icon(Icons.folder, size: 18), text: 'Sauvegardes'),
|
||||
Tab(icon: Icon(Icons.schedule, size: 18), text: 'Planification'),
|
||||
Tab(icon: Icon(Icons.restore, size: 18), text: 'Restauration'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Onglet sauvegardes
|
||||
Widget _buildBackupsTab(BackupState state) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
state is BackupLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _buildBackupsList(state is BackupsLoaded ? state.backups : (_cachedBackups ?? [])),
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Liste des sauvegardes
|
||||
Widget _buildBackupsList(List<dynamic> backupsData) {
|
||||
final backups = backupsData.map((backup) => {
|
||||
'id': backup.id?.toString() ?? '',
|
||||
'name': backup.name ?? 'Sans nom',
|
||||
'date': backup.createdAt?.toString() ?? '',
|
||||
'size': backup.sizeFormatted ?? '0 B',
|
||||
'type': backup.type ?? 'Manual',
|
||||
}).toList();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.folder, color: Color(0xFF6C5CE7), size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Sauvegardes disponibles',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...backups.map((backup) => _buildBackupItem(backup)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Élément de sauvegarde
|
||||
Widget _buildBackupItem(Map<String, dynamic> backup) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
backup['type'] == 'Auto' ? Icons.schedule : Icons.touch_app,
|
||||
color: backup['type'] == 'Auto' ? Colors.blue : Colors.green,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
backup['name']!,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${backup['date']} • ${backup['size']}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (action) => _handleBackupAction(backup, action),
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(value: 'restore', child: Text('Restaurer')),
|
||||
const PopupMenuItem(value: 'download', child: Text('Télécharger')),
|
||||
const PopupMenuItem(value: 'delete', child: Text('Supprimer')),
|
||||
],
|
||||
child: const Icon(Icons.more_vert, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Onglet planification
|
||||
Widget _buildScheduleTab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
_buildScheduleSettings(),
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Paramètres de planification
|
||||
Widget _buildScheduleSettings() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.schedule, color: Color(0xFF6C5CE7), size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Configuration automatique',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildSwitchSetting(
|
||||
'Sauvegarde automatique',
|
||||
'Activer les sauvegardes programmées',
|
||||
_autoBackupEnabled,
|
||||
(value) => setState(() => _autoBackupEnabled = value),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDropdownSetting(
|
||||
'Fréquence',
|
||||
_selectedFrequency,
|
||||
_frequencies,
|
||||
(value) => setState(() => _selectedFrequency = value!),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDropdownSetting(
|
||||
'Rétention',
|
||||
_selectedRetention,
|
||||
_retentions,
|
||||
(value) => setState(() => _selectedRetention = value!),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Onglet restauration
|
||||
Widget _buildRestoreTab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
_buildRestoreOptions(),
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Options de restauration
|
||||
Widget _buildRestoreOptions() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.restore, color: Color(0xFF6C5CE7), size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Options de restauration',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildActionButton(
|
||||
'Restaurer depuis un fichier',
|
||||
'Importer une sauvegarde externe',
|
||||
Icons.file_upload,
|
||||
const Color(0xFF0984E3),
|
||||
() => _restoreFromFile(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildActionButton(
|
||||
'Restauration sélective',
|
||||
'Restaurer uniquement certaines données',
|
||||
Icons.checklist,
|
||||
const Color(0xFF00B894),
|
||||
() => _selectiveRestore(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildActionButton(
|
||||
'Point de restauration',
|
||||
'Créer un point de restauration avant modification',
|
||||
Icons.bookmark,
|
||||
const Color(0xFFE17055),
|
||||
() => _createRestorePoint(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Méthodes de construction des composants
|
||||
Widget _buildSwitchSetting(String title, String subtitle, bool value, Function(bool) onChanged) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
Text(subtitle, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(value: value, onChanged: onChanged, activeColor: const Color(0xFF6C5CE7)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDropdownSetting(String title, String value, List<String> options, Function(String?) onChanged) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: value,
|
||||
isExpanded: true,
|
||||
onChanged: onChanged,
|
||||
items: options.map((option) => DropdownMenuItem(value: option, child: Text(option))).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton(String title, String subtitle, IconData icon, Color color, VoidCallback onTap) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: color)),
|
||||
Text(subtitle, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios, color: Colors.grey[400], size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Méthodes d'action
|
||||
void _createBackupNow() {
|
||||
context.read<BackupBloc>().add(CreateBackup('Sauvegarde manuelle', description: 'Créée depuis l\'application mobile'));
|
||||
}
|
||||
|
||||
void _handleBackupAction(Map<String, dynamic> backup, String action) {
|
||||
final backupId = backup['id'];
|
||||
if (backupId == null) return;
|
||||
|
||||
if (action == 'restore') {
|
||||
context.read<BackupBloc>().add(RestoreBackup(backupId));
|
||||
} else if (action == 'delete') {
|
||||
context.read<BackupBloc>().add(DeleteBackup(backupId));
|
||||
} else if (action == 'download') {
|
||||
_downloadBackup(backupId);
|
||||
} else {
|
||||
_showSuccessSnackBar('Action "$action" exécutée');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _downloadBackup(String backupId) async {
|
||||
try {
|
||||
final repo = sl<BackupRepository>();
|
||||
final b = await repo.getById(backupId);
|
||||
if (b.filePath != null && b.filePath!.isNotEmpty) {
|
||||
try {
|
||||
await Share.share(
|
||||
b.filePath!,
|
||||
subject: 'Sauvegarde ${b.name ?? backupId}',
|
||||
);
|
||||
_showSuccessSnackBar('Partage du lien de téléchargement');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('BackupPage: partage échoué', error: e, stackTrace: st);
|
||||
_showSuccessSnackBar('Téléchargement: configurez l\'URL de téléchargement côté backend');
|
||||
}
|
||||
} else {
|
||||
_showSuccessSnackBar('Téléchargement: l\'API ne fournit pas encore de lien (filePath).');
|
||||
}
|
||||
} catch (e, st) {
|
||||
AppLogger.error('BackupPage: téléchargement échoué', error: e, stackTrace: st);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Impossible de récupérer la sauvegarde.'), backgroundColor: Color(0xFFD63031)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _restoreFromFile() async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.any,
|
||||
allowMultiple: false,
|
||||
);
|
||||
if (result == null || result.files.isEmpty) return;
|
||||
final path = result.files.single.path;
|
||||
if (path != null && path.isNotEmpty) {
|
||||
_showSuccessSnackBar('Fichier sélectionné. Restauration depuis fichier à brancher côté API.');
|
||||
} else {
|
||||
_showSuccessSnackBar('Restauration depuis fichier à brancher côté API.');
|
||||
}
|
||||
} catch (e, st) {
|
||||
AppLogger.error('BackupPage: restauration depuis fichier', error: e, stackTrace: st);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Sélection de fichier impossible.'), backgroundColor: Color(0xFFD63031)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectiveRestore() async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.any,
|
||||
allowMultiple: true,
|
||||
);
|
||||
if (result == null || result.files.isEmpty) {
|
||||
_showSuccessSnackBar('Restauration sélective: sélectionnez un ou plusieurs fichiers.');
|
||||
return;
|
||||
}
|
||||
final paths = result.files.map((f) => f.path).whereType<String>().toList();
|
||||
if (paths.isNotEmpty) {
|
||||
_showSuccessSnackBar('Restauration sélective: ${paths.length} fichier(s) (API à brancher).');
|
||||
}
|
||||
} catch (e, st) {
|
||||
AppLogger.error('BackupPage: restauration sélective', error: e, stackTrace: st);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Sélection impossible.'), backgroundColor: Color(0xFFD63031)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _createRestorePoint() {
|
||||
context.read<BackupBloc>().add(CreateBackup('Point de restauration', description: 'Point de restauration'));
|
||||
}
|
||||
|
||||
void _showSuccessSnackBar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message), backgroundColor: const Color(0xFF00B894), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user