Image library: - "Clean up duplicates" finds byte-identical images by md5, keeps one file per group (preferring the most-used, then the oldest), merges the tags/descriptions and captions of the copies, repoints slides in open decks and in .md presentations on disk, and deletes the copies after a confirmation that lists every group. - A header toggle filters to images without tags/description, so it is easy to see which ones still need attention. - The delete warning now also lists presentations on disk that still reference the image (marked "not open"), next to the open decks. Editor and accessibility (already in tree): - Interface text scaling up to 200%, keyboard-operable panel divider, keyboard-first add-slide dialog, and screen-reader improvements. - Paste a spreadsheet/CSV/markdown selection into a table cell to fill the whole grid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
140 lines
4.7 KiB
Dart
140 lines
4.7 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import 'package:ocideck/services/image_reference_service.dart';
|
|
|
|
void main() {
|
|
late Directory tmp;
|
|
final service = ImageReferenceService();
|
|
|
|
setUp(() => tmp = Directory.systemTemp.createTempSync('ocideck_refs'));
|
|
tearDown(() => tmp.deleteSync(recursive: true));
|
|
|
|
String write(String relativePath, String content) {
|
|
final file = File(p.join(tmp.path, relativePath));
|
|
file.parent.createSync(recursive: true);
|
|
file.writeAsStringSync(content);
|
|
return file.path;
|
|
}
|
|
|
|
group('findDeckFiles', () {
|
|
test('finds .md files recursively but skips asset directories', () async {
|
|
final deck = write('presentaties/deck.md', '# Deck');
|
|
write('presentaties/images/notitie.md', 'hoort niet mee');
|
|
write('presentaties/.verborgen/geheim.md', 'hoort niet mee');
|
|
|
|
final found = await service.findDeckFiles([tmp.path]);
|
|
|
|
expect(found, [p.normalize(deck)]);
|
|
});
|
|
|
|
test('deduplicates hits from overlapping search paths', () async {
|
|
final deck = write('project/deck.md', '# Deck');
|
|
|
|
final found = await service.findDeckFiles([
|
|
tmp.path,
|
|
p.join(tmp.path, 'project'),
|
|
]);
|
|
|
|
expect(found, [p.normalize(deck)]);
|
|
});
|
|
});
|
|
|
|
group('countReferences', () {
|
|
test('resolves relative paths against the deck file directory', () async {
|
|
final img = p.join(tmp.path, 'project', 'images', 'foto.png');
|
|
final deck = write(
|
|
'project/deck.md',
|
|
'\n\n---\n\n\n',
|
|
);
|
|
|
|
final counts = await service.countReferences([deck], [img]);
|
|
|
|
expect(counts[p.normalize(img)], 2);
|
|
});
|
|
|
|
test('ignores other images and web URLs', () async {
|
|
final img = p.join(tmp.path, 'project', 'images', 'foto.png');
|
|
final deck = write(
|
|
'project/deck.md',
|
|
'\n\n',
|
|
);
|
|
|
|
expect(await service.countReferences([deck], [img]), isEmpty);
|
|
});
|
|
});
|
|
|
|
group('referencingFiles', () {
|
|
test('reports per deck file how often the image is referenced', () async {
|
|
final img = p.join(tmp.path, 'project', 'images', 'foto.png');
|
|
final twice = write(
|
|
'project/deck.md',
|
|
'\n---\n\n',
|
|
);
|
|
final never = write('project/anders.md', '\n');
|
|
|
|
final result = await service.referencingFiles([twice, never], img);
|
|
|
|
expect(result, {twice: 2});
|
|
});
|
|
});
|
|
|
|
group('replaceReferences', () {
|
|
test('rewrites relative references and keeps them relative', () async {
|
|
final from = p.join(tmp.path, 'project', 'images', 'kopie.png');
|
|
final to = p.join(tmp.path, 'project', 'images', 'origineel.png');
|
|
final deck = write(
|
|
'project/deck.md',
|
|
'# Titel\n\n\n\nTekst blijft staan.\n',
|
|
);
|
|
|
|
final changed = await service.replaceReferences(deck, from, to);
|
|
|
|
expect(changed, isTrue);
|
|
expect(
|
|
File(deck).readAsStringSync(),
|
|
'# Titel\n\n\n\nTekst blijft staan.\n',
|
|
);
|
|
});
|
|
|
|
test('rewrites absolute references to the absolute kept path', () async {
|
|
final from = p.join(tmp.path, 'elders', 'kopie.png');
|
|
final to = p.join(tmp.path, 'elders', 'origineel.png');
|
|
final deck = write('project/deck.md', '\n');
|
|
|
|
final changed = await service.replaceReferences(deck, from, to);
|
|
|
|
expect(changed, isTrue);
|
|
expect(File(deck).readAsStringSync(), '\n');
|
|
});
|
|
|
|
test('leaves the file untouched when nothing matches', () async {
|
|
final from = p.join(tmp.path, 'project', 'images', 'kopie.png');
|
|
final to = p.join(tmp.path, 'project', 'images', 'origineel.png');
|
|
final deck = write('project/deck.md', '\n');
|
|
final before = File(deck).lastModifiedSync();
|
|
|
|
final changed = await service.replaceReferences(deck, from, to);
|
|
|
|
expect(changed, isFalse);
|
|
expect(File(deck).readAsStringSync(), '\n');
|
|
expect(File(deck).lastModifiedSync(), before);
|
|
});
|
|
|
|
test(
|
|
'uses an absolute path when the kept file lies outside the project',
|
|
() async {
|
|
final from = p.join(tmp.path, 'project', 'images', 'kopie.png');
|
|
final to = p.join(tmp.path, 'elders', 'origineel.png');
|
|
final deck = write('project/deck.md', '\n');
|
|
|
|
final changed = await service.replaceReferences(deck, from, to);
|
|
|
|
expect(changed, isTrue);
|
|
expect(File(deck).readAsStringSync(), '\n');
|
|
},
|
|
);
|
|
});
|
|
}
|