Ocideck/lib/widgets/slides/slide_thumbnail.dart

279 lines
11 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/deck.dart';
import '../../models/settings.dart';
import '../../models/slide.dart';
import '../../state/slide_clipboard_provider.dart';
import '../../theme/app_theme.dart';
2026-06-04 02:30:03 +02:00
import '../../l10n/app_localizations.dart';
import 'slide_preview.dart';
class SlideThumbnail extends ConsumerWidget {
final Slide slide;
final int index;
final bool isSelected;
/// De actieve slide binnen een meervoudige selectie (de slide die in de
/// editor wordt getoond). Krijgt een iets sterkere markering.
final bool isPrimary;
final String? projectPath;
final ThemeProfile themeProfile;
final int slideCount;
final TlpLevel tlp;
final VoidCallback onTap;
final VoidCallback onDuplicate;
final VoidCallback onDelete;
final VoidCallback onToggleSkip;
final VoidCallback onCopyImage;
const SlideThumbnail({
super.key,
required this.slide,
required this.index,
required this.isSelected,
required this.onTap,
required this.onDuplicate,
required this.onDelete,
required this.onToggleSkip,
required this.onCopyImage,
this.isPrimary = true,
this.projectPath,
this.themeProfile = const ThemeProfile(),
this.slideCount = 1,
this.tlp = TlpLevel.none,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
2026-06-04 02:30:03 +02:00
final l10n = context.l10n;
final skipped = slide.skipped;
final borderColor = isSelected
? AppTheme.accent
: skipped
? const Color(0xFF8A6D3B)
: const Color(0xFF3A3F4B);
// Actieve slide krijgt een dikkere rand dan de overige geselecteerde.
final borderWidth = isSelected ? (isPrimary ? 2.5 : 1.6) : 1.0;
// Eén beknopt label per kaart voor schermlezers: nummer, titel (of type)
// en status. De mini-preview eronder is puur visueel en zou anders de
// volledige slide-inhoud per thumbnail laten voorlezen.
final title = slide.title.trim();
final semanticLabel =
'${l10n.d('Slide')} ${index + 1}/$slideCount: '
'${title.isNotEmpty ? title : l10n.d(slide.type.label)}'
'${skipped ? ' (${l10n.d('Overgeslagen')})' : ''}';
return Semantics(
button: true,
selected: isSelected,
label: semanticLabel,
child: GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(color: borderColor, width: borderWidth),
color: isSelected
? const Color(0xFF2A2F3B)
: const Color(0xFF252830),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Mini slide preview
ExcludeSemantics(
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(5),
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
fit: StackFit.expand,
children: [
// Overgeslagen slides worden gedimd weergegeven.
Opacity(
opacity: skipped ? 0.32 : 1,
child: SlidePreviewWidget(
slide: slide,
projectPath: projectPath,
themeProfile: themeProfile,
slideNumber: index + 1,
slideCount: slideCount,
tlp: tlp,
),
),
if (skipped)
Positioned(
top: 4,
left: 4,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(0xCC8A6D3B),
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.visibility_off_outlined,
size: 10,
color: Colors.white,
),
const SizedBox(width: 3),
Text(
l10n.d('Overgeslagen'),
style: const TextStyle(
color: Colors.white,
fontSize: 8,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
],
),
),
),
),
// Footer: slide number, type label, action buttons
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: Row(
children: [
Container(
width: 18,
height: 18,
decoration: BoxDecoration(
color: isSelected
? AppTheme.accent
: const Color(0xFF4A4F5B),
borderRadius: BorderRadius.circular(9),
),
child: Center(
child: Text(
'${index + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 4),
Expanded(
child: Text(
l10n.d(slide.type.label),
style: const TextStyle(
color: Color(0xFF94A3B8),
fontSize: 9,
),
overflow: TextOverflow.ellipsis,
),
),
// Drag handle
ReorderableDragStartListener(
index: index,
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
child: Icon(
Icons.drag_handle,
size: 14,
color: Color(0xFF64748B),
),
),
),
// Snelle overslaan-toggle
SizedBox(
width: 20,
height: 20,
child: IconButton(
padding: EdgeInsets.zero,
iconSize: 14,
splashRadius: 12,
tooltip: skipped
? l10n.d('Weer tonen bij presenteren/exporteren')
: l10n.d('Overslaan bij presenteren/exporteren'),
icon: Icon(
skipped
? Icons.visibility_off
: Icons.visibility_outlined,
color: skipped
? const Color(0xFFD4A24E)
: const Color(0xFF64748B),
),
onPressed: onToggleSkip,
),
),
// Context menu
SizedBox(
width: 20,
height: 20,
child: PopupMenuButton<String>(
icon: const Icon(
Icons.more_vert,
color: Color(0xFF64748B),
size: 14,
),
padding: EdgeInsets.zero,
itemBuilder: (_) => [
PopupMenuItem(
value: 'copy',
child: Text(l10n.d('Kopiëren')),
),
PopupMenuItem(
value: 'copy_image',
child: Text(l10n.d('Kopieer als afbeelding')),
),
PopupMenuItem(
value: 'duplicate',
child: Text(l10n.d('Dupliceren')),
),
PopupMenuItem(
value: 'skip',
child: Text(
skipped
? l10n.d('Niet meer overslaan')
: l10n.d('Overslaan'),
),
),
PopupMenuItem(
value: 'delete',
child: Text(
l10n.d('Verwijderen'),
style: const TextStyle(color: Colors.red),
),
),
],
onSelected: (v) {
if (v == 'copy') {
ref.read(slideClipboardProvider.notifier).state =
slide;
}
if (v == 'copy_image') onCopyImage();
if (v == 'duplicate') onDuplicate();
if (v == 'skip') onToggleSkip();
if (v == 'delete') onDelete();
},
),
),
],
),
),
],
),
),
),
);
}
}