2026-06-02 23:28:39 +02:00
|
|
|
import 'dart:convert';
|
|
|
|
|
import 'dart:io';
|
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
|
import 'package:path/path.dart' as p;
|
|
|
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
|
|
2026-06-11 22:16:39 +02:00
|
|
|
import '../utils/log.dart';
|
|
|
|
|
|
2026-06-02 23:28:39 +02:00
|
|
|
/// Eén automatisch bewaard herstelbestand voor een (nog) niet-opgeslagen deck.
|
|
|
|
|
class RecoverySnapshot {
|
|
|
|
|
final String id;
|
|
|
|
|
final DateTime savedAt;
|
|
|
|
|
final String? filePath;
|
|
|
|
|
final String label;
|
|
|
|
|
final String markdown;
|
|
|
|
|
|
|
|
|
|
const RecoverySnapshot({
|
|
|
|
|
required this.id,
|
|
|
|
|
required this.savedAt,
|
|
|
|
|
required this.filePath,
|
|
|
|
|
required this.label,
|
|
|
|
|
required this.markdown,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Map<String, Object?> toJson() => {
|
|
|
|
|
'id': id,
|
|
|
|
|
'savedAt': savedAt.toIso8601String(),
|
|
|
|
|
'filePath': filePath,
|
|
|
|
|
'label': label,
|
|
|
|
|
'markdown': markdown,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static RecoverySnapshot fromJson(Map<String, Object?> json) {
|
|
|
|
|
return RecoverySnapshot(
|
|
|
|
|
id: json['id'] as String,
|
|
|
|
|
savedAt:
|
|
|
|
|
DateTime.tryParse(json['savedAt'] as String? ?? '') ?? DateTime.now(),
|
|
|
|
|
filePath: json['filePath'] as String?,
|
|
|
|
|
label: (json['label'] as String?) ?? 'Presentatie',
|
|
|
|
|
markdown: (json['markdown'] as String?) ?? '',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Schrijft en leest autosave-herstelbestanden. Elk dirty tabblad krijgt een
|
|
|
|
|
/// eigen `<id>.json` in de herstelmap; bij opslaan/sluiten wordt het gewist,
|
|
|
|
|
/// zodat resterende bestanden bij de volgende start op niet-opgeslagen werk
|
|
|
|
|
/// (een crash) wijzen.
|
|
|
|
|
class RecoveryService {
|
|
|
|
|
/// Tests injecteren een tijdelijke map; in productie wordt de app-support-map
|
|
|
|
|
/// gebruikt (path_provider).
|
|
|
|
|
final Future<Directory> Function() _resolveDir;
|
|
|
|
|
|
|
|
|
|
RecoveryService({Directory? baseDir})
|
|
|
|
|
: _resolveDir = baseDir != null ? (() async => baseDir) : _defaultDir;
|
|
|
|
|
|
|
|
|
|
static Future<Directory> _defaultDir() async {
|
|
|
|
|
final support = await getApplicationSupportDirectory();
|
|
|
|
|
return Directory(p.join(support.path, 'recovery'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<Directory> _dir() async {
|
|
|
|
|
final dir = await _resolveDir();
|
|
|
|
|
if (!dir.existsSync()) await dir.create(recursive: true);
|
|
|
|
|
return dir;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
File _file(Directory dir, String id) => File(p.join(dir.path, '$id.json'));
|
|
|
|
|
|
|
|
|
|
Future<void> save(RecoverySnapshot snapshot) async {
|
|
|
|
|
try {
|
|
|
|
|
final dir = await _dir();
|
|
|
|
|
await _file(
|
|
|
|
|
dir,
|
|
|
|
|
snapshot.id,
|
|
|
|
|
).writeAsString(jsonEncode(snapshot.toJson()), flush: true);
|
2026-06-11 22:16:39 +02:00
|
|
|
} catch (e) {
|
|
|
|
|
logWarning('RecoveryService.save: write recovery snapshot', e);
|
2026-06-02 23:28:39 +02:00
|
|
|
// Autosave mag nooit de app verstoren.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> discard(String id) async {
|
|
|
|
|
try {
|
|
|
|
|
final file = _file(await _dir(), id);
|
|
|
|
|
if (file.existsSync()) await file.delete();
|
2026-06-11 22:16:39 +02:00
|
|
|
} catch (e) {
|
|
|
|
|
logWarning('RecoveryService.discard: delete recovery file', e);
|
|
|
|
|
}
|
2026-06-02 23:28:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<List<RecoverySnapshot>> loadAll() async {
|
|
|
|
|
try {
|
|
|
|
|
final dir = await _dir();
|
|
|
|
|
final out = <RecoverySnapshot>[];
|
|
|
|
|
for (final entry in dir.listSync()) {
|
|
|
|
|
if (entry is File && entry.path.endsWith('.json')) {
|
|
|
|
|
try {
|
|
|
|
|
final data = jsonDecode(await entry.readAsString());
|
|
|
|
|
out.add(RecoverySnapshot.fromJson(Map<String, Object?>.from(data)));
|
2026-06-11 22:16:39 +02:00
|
|
|
} catch (e, s) {
|
|
|
|
|
logError('RecoveryService.loadAll: decode recovery snapshot', e, s);
|
|
|
|
|
}
|
2026-06-02 23:28:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out.sort((a, b) => b.savedAt.compareTo(a.savedAt));
|
|
|
|
|
return out;
|
2026-06-11 22:16:39 +02:00
|
|
|
} catch (e) {
|
|
|
|
|
logWarning('RecoveryService.loadAll: list recovery dir', e);
|
2026-06-02 23:28:39 +02:00
|
|
|
return const [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> clearAll() async {
|
|
|
|
|
try {
|
|
|
|
|
final dir = await _dir();
|
|
|
|
|
for (final entry in dir.listSync()) {
|
|
|
|
|
if (entry is File && entry.path.endsWith('.json')) {
|
|
|
|
|
try {
|
|
|
|
|
await entry.delete();
|
2026-06-11 22:16:39 +02:00
|
|
|
} catch (e) {
|
|
|
|
|
logWarning('RecoveryService.clearAll: delete recovery file', e);
|
|
|
|
|
}
|
2026-06-02 23:28:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-11 22:16:39 +02:00
|
|
|
} catch (e) {
|
|
|
|
|
logWarning('RecoveryService.clearAll: list recovery dir', e);
|
|
|
|
|
}
|
2026-06-02 23:28:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final recoveryServiceProvider = Provider<RecoveryService>(
|
|
|
|
|
(_) => RecoveryService(),
|
|
|
|
|
);
|