Ocideck/lib/widgets/editors/two_images_editor.dart
Brenno de Winter 68725341a7 Add image-library dedupe and untagged filter, UI text scaling, table paste
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>
2026-06-11 13:36:44 +02:00

144 lines
4.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/slide.dart';
import '../../state/deck_provider.dart';
import '_editor_field.dart';
class TwoImagesEditor extends ConsumerStatefulWidget {
final Slide slide;
final ValueChanged<Slide> onUpdate;
final List<String> searchPaths;
final String? captionBasePath;
const TwoImagesEditor({
super.key,
required this.slide,
required this.onUpdate,
this.searchPaths = const [],
this.captionBasePath,
});
@override
ConsumerState<TwoImagesEditor> createState() => _TwoImagesEditorState();
}
class _TwoImagesEditorState extends ConsumerState<TwoImagesEditor> {
late final TextEditingController _title;
@override
void initState() {
super.initState();
_title = TextEditingController(text: widget.slide.title);
_title.addListener(_emitTitle);
}
void _emitTitle() {
widget.onUpdate(widget.slide.copyWith(title: _title.text));
}
@override
void dispose() {
_title.dispose();
super.dispose();
}
Future<void> _pasteImage(bool isSecond) async {
final imgService = ref.read(imageServiceProvider);
final path = await imgService.pasteImage();
if (path != null) {
widget.onUpdate(
isSecond
? widget.slide.copyWith(imagePath2: path, imageCaption2: '')
: widget.slide.copyWith(imagePath: path, imageCaption: ''),
);
}
}
Future<void> _pickImage(bool isSecond) async {
final imgService = ref.read(imageServiceProvider);
final path = await imgService.pickImage();
if (path != null) {
widget.onUpdate(
isSecond
? widget.slide.copyWith(imagePath2: path, imageCaption2: '')
: widget.slide.copyWith(imagePath: path, imageCaption: ''),
);
}
}
void _clearImage(bool isSecond) {
widget.onUpdate(
isSecond
? widget.slide.copyWith(imagePath2: '', imageCaption2: '')
: widget.slide.copyWith(imagePath: '', imageCaption: ''),
);
}
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
EditorField(
label: 'Ondertitel (optioneel)',
controller: _title,
hint: 'Tekst onder de afbeeldingen',
),
const SizedBox(height: 20),
const SectionLabel('Linker afbeelding'),
ImagePickerBar(
imagePath: widget.slide.imagePath,
imageCaption: widget.slide.imageCaption,
searchPaths: widget.searchPaths,
captionBasePath: widget.captionBasePath,
onPicked: (path, caption) => widget.onUpdate(
widget.slide.copyWith(imagePath: path, imageCaption: caption),
),
onBrowse: () => _pickImage(false),
onPaste: () => _pasteImage(false),
onClear: widget.slide.imagePath.isNotEmpty
? () => _clearImage(false)
: null,
onCaptionChanged: (caption) =>
widget.onUpdate(widget.slide.copyWith(imageCaption: caption)),
),
const SizedBox(height: 20),
const SectionLabel('Rechter afbeelding'),
ImagePickerBar(
imagePath: widget.slide.imagePath2,
imageCaption: widget.slide.imageCaption2,
searchPaths: widget.searchPaths,
captionBasePath: widget.captionBasePath,
onPicked: (path, caption) => widget.onUpdate(
widget.slide.copyWith(imagePath2: path, imageCaption2: caption),
),
onBrowse: () => _pickImage(true),
onPaste: () => _pasteImage(true),
onClear: widget.slide.imagePath2.isNotEmpty
? () => _clearImage(true)
: null,
onCaptionChanged: (caption) =>
widget.onUpdate(widget.slide.copyWith(imageCaption2: caption)),
),
const SizedBox(height: 20),
const SectionLabel('Verdeling (links / rechts)'),
ImageZoomControl(
value: widget.slide.imageSize > 0 ? widget.slide.imageSize : 50,
onChanged: (v) =>
widget.onUpdate(widget.slide.copyWith(imageSize: v)),
step: 5,
minValue: 20,
maxValue: 80,
),
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
'Links ${widget.slide.imageSize > 0 ? widget.slide.imageSize : 50}% — '
'Rechts ${100 - (widget.slide.imageSize > 0 ? widget.slide.imageSize : 50)}%',
style: const TextStyle(fontSize: 11, color: Color(0xFF64748B)),
),
),
],
);
}
}