Files
youtube_personal/lib/widgets/memory_transfer_settings.dart

355 lines
11 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import '../services/memory_backup_service.dart';
class MemoryTransferSettings extends StatefulWidget {
const MemoryTransferSettings({super.key});
@override
State<MemoryTransferSettings> createState() => _MemoryTransferSettingsState();
}
class _MemoryTransferSettingsState extends State<MemoryTransferSettings> {
final MemoryBackupService _memory = MemoryBackupService.instance;
bool _busy = false;
int? _entryCount;
@override
void initState() {
super.initState();
_refreshCount();
}
Future<void> _refreshCount() async {
try {
final count = await _memory.memoryEntryCount();
if (!mounted) return;
setState(() => _entryCount = count);
} catch (_) {
// Le compteur est informatif. Une erreur ici ne bloque pas les actions.
}
}
Future<void> _export() async {
if (_busy) return;
setState(() => _busy = true);
try {
final result = await _memory.exportMemory();
if (!mounted || result == null) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Mémoire exportée : ${result.entryCount} bloc(s).'),
),
);
} catch (error) {
if (!mounted) return;
_showError('Export impossible', error);
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<void> _import() async {
if (_busy) return;
setState(() => _busy = true);
try {
final candidate = await _memory.pickMemoryBackup();
if (!mounted || candidate == null) return;
final mode = await _chooseImportMode(candidate);
if (!mounted || mode == null) return;
if (mode == MemoryImportMode.replace) {
final confirmed = await _confirmReplace();
if (!mounted || !confirmed) return;
}
final result = await _memory.importMemory(candidate, mode: mode);
if (!mounted) return;
await _refreshCount();
if (!mounted) return;
await showDialog<void>(
context: context,
builder: (context) => AlertDialog(
icon: const Icon(Icons.check_circle_outline),
title: const Text('Mémoire importée'),
content: Text(
'${result.importedCount} bloc(s) de mémoire ont été importés.\n\n'
'Ferme complètement puis rouvre YouTube Personal pour que '
'l’historique, les favoris et le Radar rechargent la nouvelle mémoire.',
),
actions: [
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
} catch (error) {
if (!mounted) return;
_showError('Import impossible', error);
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<MemoryImportMode?> _chooseImportMode(MemoryImportCandidate candidate) {
final exported = candidate.exportedAtUtc == null
? 'Date inconnue'
: _formatDate(candidate.exportedAtUtc!.toLocal());
final source = _platformLabel(candidate.sourcePlatform);
return showDialog<MemoryImportMode>(
context: context,
builder: (context) => AlertDialog(
icon: const Icon(Icons.memory_rounded),
title: const Text('Importer cette mémoire ?'),
content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Source : $source'),
const SizedBox(height: 4),
Text('Export : $exported'),
const SizedBox(height: 4),
Text('${candidate.entryCount} bloc(s) de mémoire'),
const SizedBox(height: 18),
const Text(
'Fusionner garde les blocs locaux absents de la sauvegarde. '
'Pour les blocs présents des deux côtés, la sauvegarde importée '
'devient la référence.',
),
const SizedBox(height: 10),
const Text(
'Remplacer efface d’abord la mémoire comportementale locale, '
'puis restaure exactement cette sauvegarde.',
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
OutlinedButton.icon(
onPressed: () => Navigator.of(context).pop(MemoryImportMode.merge),
icon: const Icon(Icons.merge_type_rounded),
label: const Text('Fusionner'),
),
FilledButton.icon(
onPressed: () =>
Navigator.of(context).pop(MemoryImportMode.replace),
icon: const Icon(Icons.restore_rounded),
label: const Text('Remplacer'),
),
],
),
);
}
Future<bool> _confirmReplace() async {
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
icon: const Icon(Icons.warning_amber_rounded),
title: const Text('Remplacer la mémoire locale ?'),
content: const Text(
'L’historique, les favoris, les reprises, les compteurs, les '
'chaînes masquées et l’apprentissage du Radar présents sur cet '
'appareil seront remplacés par la sauvegarde sélectionnée.\n\n'
'La connexion Google et les données OAuth ne sont pas touchées.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Oui, remplacer'),
),
],
),
) ??
false;
}
void _showError(String title, Object error) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
icon: const Icon(Icons.error_outline),
title: Text(title),
content: Text('$error'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
],
),
);
}
String _formatDate(DateTime value) {
String two(int number) => number.toString().padLeft(2, '0');
return '${two(value.day)}/${two(value.month)}/${value.year} '
'${two(value.hour)}:${two(value.minute)}';
}
String _platformLabel(String platform) {
switch (platform.toLowerCase()) {
case 'windows':
return 'Windows';
case 'android':
return 'Android';
case 'linux':
return 'Linux';
case 'macos':
return 'macOS';
default:
return platform;
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
return Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: colors.surfaceContainerLow,
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: colors.outlineVariant.withValues(alpha: 0.55),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: colors.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.sync_alt_rounded,
color: colors.onPrimaryContainer,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Mémoire & transfert',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 3),
Text(
_entryCount == null
? 'Mémoire locale de ton profil utilisateur'
: '$_entryCount bloc(s) de mémoire transférable(s)',
style: TextStyle(
color: colors.onSurfaceVariant,
fontSize: 12.5,
),
),
],
),
),
if (_busy)
const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(strokeWidth: 2.4),
),
],
),
const SizedBox(height: 14),
Text(
'Transfère l’apprentissage de YouTube Personal entre Windows, '
'Android et plus tard Linux : historique, favoris, reprises, '
'habitudes du Radar, catégories et chaînes masquées.',
style: TextStyle(
color: colors.onSurfaceVariant,
height: 1.45,
fontSize: 12.5,
),
),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.lock_outline_rounded,
size: 17,
color: colors.onSurfaceVariant,
),
const SizedBox(width: 7),
Expanded(
child: Text(
'La connexion Google, les tokens OAuth, les secrets et les '
'chemins de cache ne sont jamais inclus.',
style: TextStyle(
color: colors.onSurfaceVariant,
height: 1.4,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 16),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
FilledButton.icon(
onPressed: _busy ? null : _export,
icon: const Icon(Icons.upload_file_rounded),
label: const Text('Exporter la mémoire'),
),
OutlinedButton.icon(
onPressed: _busy ? null : _import,
icon: const Icon(Icons.download_rounded),
label: const Text('Importer une mémoire'),
),
],
),
const SizedBox(height: 12),
Text(
'Format versionné V1, prévu pour servir de base à une future '
'synchronisation automatique multi-appareils.',
style: TextStyle(
color: colors.onSurfaceVariant.withValues(alpha: 0.76),
fontSize: 11.5,
fontStyle: FontStyle.italic,
),
),
],
),
);
}
}