feat(profile): add local backup export/import UI (#84 F1/F2)
- F1: "Sauvegarde locale" panel in Profil (both signed-out/signed-in states, positioned per UX cadrage), "Exporter mes données" calling DataExportUseCase.exportAll() then handing the file to the system share sheet, with the exact UX success/error snackbars. - F2: "Importer des données" picks a file, previews it, then confirms with the exact UX dialogs: simplified single-choice when there is no local data yet, Fusionner/Remplacer tout (with a destructive second confirmation) otherwise. All typed LocalBackupValidationError cases are mapped to their dedicated dialog copy, including the active workout session block.
This commit is contained in:
@ -227,6 +227,8 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
authUseCases: widget.bootstrap.authUseCases,
|
authUseCases: widget.bootstrap.authUseCases,
|
||||||
syncUseCases: widget.bootstrap.syncUseCases,
|
syncUseCases: widget.bootstrap.syncUseCases,
|
||||||
shareUseCases: widget.bootstrap.shareUseCases,
|
shareUseCases: widget.bootstrap.shareUseCases,
|
||||||
|
dataExportUseCase: widget.bootstrap.dataExportUseCase,
|
||||||
|
dataImportUseCase: widget.bootstrap.dataImportUseCase,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
import '../application/application.dart';
|
import '../application/application.dart';
|
||||||
import '../domain/domain.dart';
|
import '../domain/domain.dart';
|
||||||
@ -12,12 +15,20 @@ final class ProfileScreen extends StatefulWidget {
|
|||||||
required this.authUseCases,
|
required this.authUseCases,
|
||||||
required this.syncUseCases,
|
required this.syncUseCases,
|
||||||
required this.shareUseCases,
|
required this.shareUseCases,
|
||||||
|
required this.dataExportUseCase,
|
||||||
|
required this.dataImportUseCase,
|
||||||
|
this.backupFileExporter = const SharePlusLocalBackupFileExporter(),
|
||||||
|
this.backupFilePicker = const SystemLocalBackupFilePicker(),
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final AuthUseCases authUseCases;
|
final AuthUseCases authUseCases;
|
||||||
final SyncUseCases syncUseCases;
|
final SyncUseCases syncUseCases;
|
||||||
final ShareUseCases shareUseCases;
|
final ShareUseCases shareUseCases;
|
||||||
|
final DataExportUseCase dataExportUseCase;
|
||||||
|
final DataImportUseCase dataImportUseCase;
|
||||||
|
final LocalBackupFileExporter backupFileExporter;
|
||||||
|
final LocalBackupFilePicker backupFilePicker;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ProfileScreen> createState() => _ProfileScreenState();
|
State<ProfileScreen> createState() => _ProfileScreenState();
|
||||||
@ -47,6 +58,10 @@ final class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
return _SignedOutProfile(
|
return _SignedOutProfile(
|
||||||
onRegister: () => _openRegister(context),
|
onRegister: () => _openRegister(context),
|
||||||
onLogin: () => _openLogin(context),
|
onLogin: () => _openLogin(context),
|
||||||
|
dataExportUseCase: widget.dataExportUseCase,
|
||||||
|
dataImportUseCase: widget.dataImportUseCase,
|
||||||
|
backupFileExporter: widget.backupFileExporter,
|
||||||
|
backupFilePicker: widget.backupFilePicker,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return _SignedInProfile(
|
return _SignedInProfile(
|
||||||
@ -54,6 +69,10 @@ final class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
syncUseCases: widget.syncUseCases,
|
syncUseCases: widget.syncUseCases,
|
||||||
onLogout: () => _confirmLogout(context),
|
onLogout: () => _confirmLogout(context),
|
||||||
onShares: () => _openReceivedShares(context),
|
onShares: () => _openReceivedShares(context),
|
||||||
|
dataExportUseCase: widget.dataExportUseCase,
|
||||||
|
dataImportUseCase: widget.dataImportUseCase,
|
||||||
|
backupFileExporter: widget.backupFileExporter,
|
||||||
|
backupFilePicker: widget.backupFilePicker,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -144,10 +163,21 @@ final class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class _SignedOutProfile extends StatelessWidget {
|
final class _SignedOutProfile extends StatelessWidget {
|
||||||
const _SignedOutProfile({required this.onRegister, required this.onLogin});
|
const _SignedOutProfile({
|
||||||
|
required this.onRegister,
|
||||||
|
required this.onLogin,
|
||||||
|
required this.dataExportUseCase,
|
||||||
|
required this.dataImportUseCase,
|
||||||
|
required this.backupFileExporter,
|
||||||
|
required this.backupFilePicker,
|
||||||
|
});
|
||||||
|
|
||||||
final VoidCallback onRegister;
|
final VoidCallback onRegister;
|
||||||
final VoidCallback onLogin;
|
final VoidCallback onLogin;
|
||||||
|
final DataExportUseCase dataExportUseCase;
|
||||||
|
final DataImportUseCase dataImportUseCase;
|
||||||
|
final LocalBackupFileExporter backupFileExporter;
|
||||||
|
final LocalBackupFilePicker backupFilePicker;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -199,6 +229,14 @@ final class _SignedOutProfile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
LocalBackupPanel(
|
||||||
|
dataExportUseCase: dataExportUseCase,
|
||||||
|
dataImportUseCase: dataImportUseCase,
|
||||||
|
isAccountConnected: false,
|
||||||
|
fileExporter: backupFileExporter,
|
||||||
|
filePicker: backupFilePicker,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
CourtBlazerAccentPanel(
|
CourtBlazerAccentPanel(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -223,12 +261,20 @@ final class _SignedInProfile extends StatelessWidget {
|
|||||||
required this.syncUseCases,
|
required this.syncUseCases,
|
||||||
required this.onLogout,
|
required this.onLogout,
|
||||||
required this.onShares,
|
required this.onShares,
|
||||||
|
required this.dataExportUseCase,
|
||||||
|
required this.dataImportUseCase,
|
||||||
|
required this.backupFileExporter,
|
||||||
|
required this.backupFilePicker,
|
||||||
});
|
});
|
||||||
|
|
||||||
final UserAccountSession session;
|
final UserAccountSession session;
|
||||||
final SyncUseCases syncUseCases;
|
final SyncUseCases syncUseCases;
|
||||||
final VoidCallback onLogout;
|
final VoidCallback onLogout;
|
||||||
final VoidCallback onShares;
|
final VoidCallback onShares;
|
||||||
|
final DataExportUseCase dataExportUseCase;
|
||||||
|
final DataImportUseCase dataImportUseCase;
|
||||||
|
final LocalBackupFileExporter backupFileExporter;
|
||||||
|
final LocalBackupFilePicker backupFilePicker;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -268,6 +314,14 @@ final class _SignedInProfile extends StatelessWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
SyncStatusPanel(syncUseCases: syncUseCases),
|
SyncStatusPanel(syncUseCases: syncUseCases),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
LocalBackupPanel(
|
||||||
|
dataExportUseCase: dataExportUseCase,
|
||||||
|
dataImportUseCase: dataImportUseCase,
|
||||||
|
isAccountConnected: true,
|
||||||
|
fileExporter: backupFileExporter,
|
||||||
|
filePicker: backupFilePicker,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
CourtBlazerAccentPanel(
|
CourtBlazerAccentPanel(
|
||||||
child: Material(
|
child: Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
@ -420,6 +474,403 @@ _SyncStatusView _syncStatusView(SyncMetadataSnapshot? metadata, bool syncing) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract interface class LocalBackupFileExporter {
|
||||||
|
Future<ShareResultStatus> exportFile({
|
||||||
|
required String fileName,
|
||||||
|
required Uint8List bytes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SharePlusLocalBackupFileExporter implements LocalBackupFileExporter {
|
||||||
|
const SharePlusLocalBackupFileExporter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ShareResultStatus> exportFile({
|
||||||
|
required String fileName,
|
||||||
|
required Uint8List bytes,
|
||||||
|
}) async {
|
||||||
|
final result = await SharePlus.instance.share(
|
||||||
|
ShareParams(
|
||||||
|
files: [
|
||||||
|
XFile.fromData(
|
||||||
|
bytes,
|
||||||
|
name: fileName,
|
||||||
|
mimeType: 'application/octet-stream',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return result.status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class LocalBackupPickedFile {
|
||||||
|
const LocalBackupPickedFile({required this.fileName, required this.bytes});
|
||||||
|
|
||||||
|
final String fileName;
|
||||||
|
final Uint8List bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface class LocalBackupFilePicker {
|
||||||
|
Future<LocalBackupPickedFile?> pickFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SystemLocalBackupFilePicker implements LocalBackupFilePicker {
|
||||||
|
const SystemLocalBackupFilePicker();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LocalBackupPickedFile?> pickFile() async {
|
||||||
|
final result = await FilePicker.pickFiles(withData: true);
|
||||||
|
if (result == null || result.files.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final file = result.files.single;
|
||||||
|
final bytes = file.bytes;
|
||||||
|
if (bytes == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return LocalBackupPickedFile(fileName: file.name, bytes: bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _ImportChoice { cancel, merge, replaceAll }
|
||||||
|
|
||||||
|
final class LocalBackupPanel extends StatefulWidget {
|
||||||
|
const LocalBackupPanel({
|
||||||
|
required this.dataExportUseCase,
|
||||||
|
required this.dataImportUseCase,
|
||||||
|
required this.isAccountConnected,
|
||||||
|
this.fileExporter = const SharePlusLocalBackupFileExporter(),
|
||||||
|
this.filePicker = const SystemLocalBackupFilePicker(),
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DataExportUseCase dataExportUseCase;
|
||||||
|
final DataImportUseCase dataImportUseCase;
|
||||||
|
final bool isAccountConnected;
|
||||||
|
final LocalBackupFileExporter fileExporter;
|
||||||
|
final LocalBackupFilePicker filePicker;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LocalBackupPanel> createState() => _LocalBackupPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _LocalBackupPanelState extends State<LocalBackupPanel> {
|
||||||
|
var _exporting = false;
|
||||||
|
var _importBusy = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CourtBlazerAccentPanel(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Sauvegarde locale',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'Exporte un fichier de sauvegarde ou importe une sauvegarde '
|
||||||
|
'GameTime depuis cet appareil.',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: _exporting ? null : _export,
|
||||||
|
icon: const Icon(Icons.ios_share),
|
||||||
|
label: Text(
|
||||||
|
_exporting
|
||||||
|
? 'Préparation de l’export...'
|
||||||
|
: 'Exporter mes données',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _importBusy ? null : _startImport,
|
||||||
|
icon: const Icon(Icons.file_download_outlined),
|
||||||
|
label: Text(
|
||||||
|
_importBusy
|
||||||
|
? 'Lecture de la sauvegarde...'
|
||||||
|
: 'Importer des données',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _export() async {
|
||||||
|
setState(() => _exporting = true);
|
||||||
|
try {
|
||||||
|
final document = await widget.dataExportUseCase.exportAll();
|
||||||
|
final status = await widget.fileExporter.exportFile(
|
||||||
|
fileName: document.fileName,
|
||||||
|
bytes: document.bytes,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
final message = switch (status) {
|
||||||
|
ShareResultStatus.success => 'Sauvegarde exportée.',
|
||||||
|
ShareResultStatus.dismissed => null,
|
||||||
|
ShareResultStatus.unavailable =>
|
||||||
|
'Export prêt. Choisis où enregistrer le fichier.',
|
||||||
|
};
|
||||||
|
if (message != null) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(message)));
|
||||||
|
}
|
||||||
|
} on Object {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Export impossible pour le moment.')),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _exporting = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startImport() async {
|
||||||
|
final picked = await widget.filePicker.pickFile();
|
||||||
|
if (picked == null || !mounted) return;
|
||||||
|
|
||||||
|
setState(() => _importBusy = true);
|
||||||
|
final LocalBackupPreview preview;
|
||||||
|
try {
|
||||||
|
preview = await widget.dataImportUseCase.preview(picked.bytes);
|
||||||
|
} on LocalBackupException catch (error) {
|
||||||
|
if (mounted) setState(() => _importBusy = false);
|
||||||
|
if (!mounted) return;
|
||||||
|
await _showBackupErrorDialog(error.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final hasLocalData = await widget.dataImportUseCase.hasAnyLocalData();
|
||||||
|
if (mounted) setState(() => _importBusy = false);
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
final mode = hasLocalData
|
||||||
|
? await _resolveModeWithChoice(preview)
|
||||||
|
: await _confirmSimpleImport(preview);
|
||||||
|
if (mode == null || !mounted) return;
|
||||||
|
|
||||||
|
setState(() => _importBusy = true);
|
||||||
|
try {
|
||||||
|
await widget.dataImportUseCase.importFrom(picked.bytes, mode: mode);
|
||||||
|
if (!mounted) return;
|
||||||
|
final message = mode == LocalBackupImportMode.replaceAll
|
||||||
|
? 'Sauvegarde restaurée.'
|
||||||
|
: 'Données importées.';
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(message)));
|
||||||
|
} on LocalBackupException catch (error) {
|
||||||
|
if (!mounted) return;
|
||||||
|
await _showBackupErrorDialog(error.error);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _importBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<LocalBackupImportMode?> _resolveModeWithChoice(
|
||||||
|
LocalBackupPreview preview,
|
||||||
|
) async {
|
||||||
|
final choice = await _confirmFullImport(preview);
|
||||||
|
if (choice == _ImportChoice.merge) {
|
||||||
|
return LocalBackupImportMode.merge;
|
||||||
|
}
|
||||||
|
if (choice == _ImportChoice.replaceAll) {
|
||||||
|
if (!mounted) return null;
|
||||||
|
final confirmedReplace = await _confirmReplaceAll();
|
||||||
|
return confirmedReplace ? LocalBackupImportMode.replaceAll : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<LocalBackupImportMode?> _confirmSimpleImport(
|
||||||
|
LocalBackupPreview preview,
|
||||||
|
) {
|
||||||
|
return showDialog<LocalBackupImportMode>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Importer cette sauvegarde ?'),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: _previewSummaryLines(preview, showChoiceHint: false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () =>
|
||||||
|
Navigator.of(context).pop(LocalBackupImportMode.merge),
|
||||||
|
child: const Text('Importer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<_ImportChoice?> _confirmFullImport(LocalBackupPreview preview) {
|
||||||
|
return showDialog<_ImportChoice>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Importer cette sauvegarde ?'),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
..._previewSummaryLines(preview, showChoiceHint: true),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
'Fusionner ajoute ce qui manque et conserve tes données '
|
||||||
|
'actuelles.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(_ImportChoice.cancel),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(_ImportChoice.merge),
|
||||||
|
child: const Text('Fusionner'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () =>
|
||||||
|
Navigator.of(context).pop(_ImportChoice.replaceAll),
|
||||||
|
child: const Text('Remplacer tout'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _confirmReplaceAll() async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Remplacer toutes les données locales ?'),
|
||||||
|
content: const Text(
|
||||||
|
'Tes exercices, programmes, séances et historiques actuels seront '
|
||||||
|
'remplacés par ceux de cette sauvegarde. Cette action ne peut pas '
|
||||||
|
'être annulée.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Remplacer tout'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return confirmed == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _previewSummaryLines(
|
||||||
|
LocalBackupPreview preview, {
|
||||||
|
required bool showChoiceHint,
|
||||||
|
}) {
|
||||||
|
final counts = preview.counts;
|
||||||
|
final lines = <Widget>[
|
||||||
|
const Text('Cette sauvegarde contient :'),
|
||||||
|
Text('${counts.exercises} exercices'),
|
||||||
|
Text('${counts.programs} programmes'),
|
||||||
|
Text('${counts.workoutTemplates} séances'),
|
||||||
|
Text('${counts.workoutHistories} historiques'),
|
||||||
|
];
|
||||||
|
if (preview.hasEmbeddedMedia) {
|
||||||
|
lines.add(const SizedBox(height: 8));
|
||||||
|
lines.add(const Text('Médias inclus'));
|
||||||
|
}
|
||||||
|
if (preview.missingMediaCount > 0) {
|
||||||
|
lines.add(const SizedBox(height: 8));
|
||||||
|
lines.add(const Text('Les médias absents seront ignorés.'));
|
||||||
|
}
|
||||||
|
if (preview.hasNameDuplicates) {
|
||||||
|
lines.add(const SizedBox(height: 8));
|
||||||
|
lines.add(
|
||||||
|
const Text(
|
||||||
|
'Des éléments portent déjà le même nom. En fusion, ils seront '
|
||||||
|
'conservés séparément si GameTime ne peut pas reconnaître qu’il '
|
||||||
|
's’agit du même élément.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (showChoiceHint) {
|
||||||
|
lines.add(const SizedBox(height: 12));
|
||||||
|
lines.add(const Text('Choisis comment l’ajouter à tes données locales.'));
|
||||||
|
}
|
||||||
|
if (widget.isAccountConnected) {
|
||||||
|
lines.add(const SizedBox(height: 8));
|
||||||
|
lines.add(
|
||||||
|
const Text(
|
||||||
|
'Les données importées resteront locales et seront '
|
||||||
|
'synchronisées selon tes réglages habituels.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showBackupErrorDialog(LocalBackupValidationError error) {
|
||||||
|
final (title, message) = _backupErrorText(error);
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(String, String) _backupErrorText(LocalBackupValidationError error) {
|
||||||
|
return switch (error) {
|
||||||
|
LocalBackupValidationError.invalidFile => (
|
||||||
|
'Fichier invalide',
|
||||||
|
'Ce fichier n’est pas une sauvegarde GameTime valide.',
|
||||||
|
),
|
||||||
|
LocalBackupValidationError.incompatibleFormat => (
|
||||||
|
'Format incompatible',
|
||||||
|
'Cette sauvegarde ne peut pas être importée par cette version de '
|
||||||
|
'GameTime.',
|
||||||
|
),
|
||||||
|
LocalBackupValidationError.newerVersion => (
|
||||||
|
'Mets GameTime à jour',
|
||||||
|
'Cette sauvegarde vient d’une version plus récente de GameTime.',
|
||||||
|
),
|
||||||
|
LocalBackupValidationError.corrupted => (
|
||||||
|
'Sauvegarde illisible',
|
||||||
|
'Le fichier semble incomplet ou endommagé.',
|
||||||
|
),
|
||||||
|
LocalBackupValidationError.activeWorkoutInProgress => (
|
||||||
|
'Séance en cours',
|
||||||
|
'Termine ou sauvegarde ta séance avant d’importer des données.',
|
||||||
|
),
|
||||||
|
LocalBackupValidationError.importFailedNoMutation => (
|
||||||
|
'Import impossible',
|
||||||
|
'Aucune donnée n’a été modifiée.',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
final class LoginScreen extends StatefulWidget {
|
final class LoginScreen extends StatefulWidget {
|
||||||
const LoginScreen({
|
const LoginScreen({
|
||||||
required this.authUseCases,
|
required this.authUseCases,
|
||||||
|
|||||||
Reference in New Issue
Block a user