2026-06-02 23:28:39 +02:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import '../../models/slide.dart';
|
|
|
|
|
import '../../services/image_service.dart';
|
2026-06-04 02:30:03 +02:00
|
|
|
import '../../l10n/app_localizations.dart';
|
2026-06-02 23:28:39 +02:00
|
|
|
import '_editor_field.dart';
|
|
|
|
|
import 'audio_attachment_editor.dart';
|
|
|
|
|
|
|
|
|
|
class VideoSlideEditor extends StatefulWidget {
|
|
|
|
|
final Slide slide;
|
|
|
|
|
final ValueChanged<Slide> onUpdate;
|
|
|
|
|
final ImageService imageService;
|
|
|
|
|
|
|
|
|
|
const VideoSlideEditor({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.slide,
|
|
|
|
|
required this.onUpdate,
|
|
|
|
|
required this.imageService,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<VideoSlideEditor> createState() => _VideoSlideEditorState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _VideoSlideEditorState extends State<VideoSlideEditor> {
|
|
|
|
|
late final TextEditingController _title;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_title = TextEditingController(text: widget.slide.title);
|
|
|
|
|
_title.addListener(_emit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _emit() {
|
|
|
|
|
widget.onUpdate(widget.slide.copyWith(title: _title.text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _pickVideo() async {
|
|
|
|
|
final path = await widget.imageService.pickVideo();
|
|
|
|
|
if (path != null) widget.onUpdate(widget.slide.copyWith(videoPath: path));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_title.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-06-04 02:30:03 +02:00
|
|
|
final l10n = context.l10n;
|
2026-06-02 23:28:39 +02:00
|
|
|
return ListView(
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
children: [
|
|
|
|
|
EditorField(
|
|
|
|
|
label: 'Titel (optioneel)',
|
|
|
|
|
controller: _title,
|
|
|
|
|
hint: 'Titel boven de video',
|
|
|
|
|
maxLines: 2,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
const SectionLabel('Video'),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(child: _PathBox(path: widget.slide.videoPath)),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
ElevatedButton.icon(
|
|
|
|
|
onPressed: _pickVideo,
|
|
|
|
|
icon: const Icon(Icons.movie_outlined, size: 16),
|
2026-06-04 02:30:03 +02:00
|
|
|
label: Text(l10n.d('Kiezen')),
|
2026-06-02 23:28:39 +02:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
Material(
|
|
|
|
|
color: Colors.transparent,
|
|
|
|
|
child: CheckboxListTile(
|
|
|
|
|
value: widget.slide.videoAutoplay,
|
|
|
|
|
onChanged: (value) => widget.onUpdate(
|
|
|
|
|
widget.slide.copyWith(videoAutoplay: value ?? false),
|
|
|
|
|
),
|
2026-06-04 02:30:03 +02:00
|
|
|
title: Text(l10n.d('Video automatisch afspelen')),
|
2026-06-02 23:28:39 +02:00
|
|
|
dense: true,
|
|
|
|
|
contentPadding: EdgeInsets.zero,
|
|
|
|
|
controlAffinity: ListTileControlAffinity.leading,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
AudioAttachmentEditor(
|
|
|
|
|
slide: widget.slide,
|
|
|
|
|
imageService: widget.imageService,
|
|
|
|
|
onUpdate: widget.onUpdate,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _PathBox extends StatelessWidget {
|
|
|
|
|
final String path;
|
|
|
|
|
|
|
|
|
|
const _PathBox({required this.path});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-06-04 02:30:03 +02:00
|
|
|
final l10n = context.l10n;
|
2026-06-02 23:28:39 +02:00
|
|
|
return Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
border: Border.all(color: const Color(0xFFCBD5E1)),
|
|
|
|
|
borderRadius: BorderRadius.circular(6),
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
),
|
|
|
|
|
child: Text(
|
2026-06-04 02:30:03 +02:00
|
|
|
path.isEmpty ? l10n.d('Geen video gekozen') : path,
|
2026-06-02 23:28:39 +02:00
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
color: path.isEmpty
|
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
|
|
|
? const Color(0xFF64748B)
|
2026-06-02 23:28:39 +02:00
|
|
|
: const Color(0xFF334155),
|
|
|
|
|
),
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|