import 'package:flutter/material.dart'; import '../services/memory_backup_service.dart'; class MemoryTransferSettings extends StatefulWidget { const MemoryTransferSettings({super.key}); @override State createState() => _MemoryTransferSettingsState(); } class _MemoryTransferSettingsState extends State { final MemoryBackupService _memory = MemoryBackupService.instance; bool _busy = false; int? _entryCount; @override void initState() { super.initState(); _refreshCount(); } Future _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 _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 _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( 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 _chooseImportMode(MemoryImportCandidate candidate) { final exported = candidate.exportedAtUtc == null ? 'Date inconnue' : _formatDate(candidate.exportedAtUtc!.toLocal()); final source = _platformLabel(candidate.sourcePlatform); return showDialog( 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 _confirmReplace() async { return await showDialog( 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( 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, ), ), ], ), ); } }