2026-06-02 23:28:39 +02:00
|
|
|
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)}%',
|
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
|
|
|
style: const TextStyle(fontSize: 11, color: Color(0xFF64748B)),
|
2026-06-02 23:28:39 +02:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|