feat: add multilingual interface
This commit is contained in:
parent
3e664193ce
commit
d0bd1a85bf
38 changed files with 2759 additions and 669 deletions
19
lib/app.dart
19
lib/app.dart
|
|
@ -1,16 +1,31 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'state/settings_provider.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
import 'widgets/app_shell.dart';
|
||||
|
||||
class OciDeckApp extends StatelessWidget {
|
||||
class OciDeckApp extends ConsumerWidget {
|
||||
const OciDeckApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final languageCode = ref.watch(
|
||||
settingsProvider.select((s) => s.languageCode),
|
||||
);
|
||||
return MaterialApp(
|
||||
title: 'OciDeck',
|
||||
theme: AppTheme.light,
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: Locale(languageCode),
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
home: const AppShell(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
1418
lib/l10n/app_localizations.dart
Normal file
1418
lib/l10n/app_localizations.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -145,6 +145,7 @@ class ThemeProfile {
|
|||
}
|
||||
|
||||
class AppSettings {
|
||||
final String languageCode;
|
||||
final String? homeDirectory;
|
||||
|
||||
/// Folder where all exports (PDF/PPTX) are written. When null, exports land
|
||||
|
|
@ -155,6 +156,7 @@ class AppSettings {
|
|||
final List<String> recentFiles;
|
||||
|
||||
const AppSettings({
|
||||
this.languageCode = 'nl',
|
||||
this.homeDirectory,
|
||||
this.exportDirectory,
|
||||
this.themeProfiles = const [ThemeProfile()],
|
||||
|
|
@ -184,6 +186,7 @@ class AppSettings {
|
|||
];
|
||||
|
||||
AppSettings copyWith({
|
||||
String? languageCode,
|
||||
String? homeDirectory,
|
||||
String? exportDirectory,
|
||||
ThemeProfile? themeProfile,
|
||||
|
|
@ -195,6 +198,7 @@ class AppSettings {
|
|||
}) {
|
||||
final nextProfiles = themeProfiles ?? this.themeProfiles;
|
||||
return AppSettings(
|
||||
languageCode: languageCode ?? this.languageCode,
|
||||
homeDirectory: clearHomeDirectory
|
||||
? null
|
||||
: (homeDirectory ?? this.homeDirectory),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:path/path.dart' as p;
|
|||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
|
||||
import '../models/settings.dart';
|
||||
import 'marp_html_service.dart';
|
||||
|
||||
enum ExportFormat { pdf, pptx, html }
|
||||
|
|
@ -101,6 +102,7 @@ class ExportService {
|
|||
String? outputDirectory,
|
||||
List<String>? notes,
|
||||
String? markdown,
|
||||
ThemeProfile? themeProfile,
|
||||
}) async {
|
||||
if (format == ExportFormat.html) {
|
||||
if (markdown == null || markdown.trim().isEmpty) {
|
||||
|
|
@ -128,7 +130,9 @@ class ExportService {
|
|||
case ExportFormat.pptx:
|
||||
bytes = _buildPptx(images, notes: notes);
|
||||
case ExportFormat.html:
|
||||
bytes = Uint8List.fromList(utf8.encode(await _html.build(markdown!)));
|
||||
bytes = Uint8List.fromList(
|
||||
utf8.encode(await _html.build(markdown!, theme: themeProfile)),
|
||||
);
|
||||
}
|
||||
await File(outputPath).writeAsBytes(bytes, flush: true);
|
||||
return ExportResult.ok(outputPath);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:file_picker/file_picker.dart';
|
|||
import 'package:path/path.dart' as p;
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import '../models/deck.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../models/settings.dart';
|
||||
import '../models/slide.dart';
|
||||
import 'caption_service.dart';
|
||||
|
|
@ -40,12 +41,20 @@ class FileService {
|
|||
final MarkdownService _md;
|
||||
final ImageService _img;
|
||||
final ThemeProfile Function() _themeProfile;
|
||||
final String Function() _languageCode;
|
||||
final CaptionService _captions = CaptionService();
|
||||
|
||||
FileService(this._md, this._img, this._themeProfile);
|
||||
FileService(
|
||||
this._md,
|
||||
this._img,
|
||||
this._themeProfile, {
|
||||
String Function()? languageCode,
|
||||
}) : _languageCode = languageCode ?? (() => 'nl');
|
||||
|
||||
ThemeProfile get currentThemeProfile => _themeProfile();
|
||||
|
||||
String _d(String text) => AppLocalizations.sourceFor(_languageCode(), text);
|
||||
|
||||
static const _ignoredDirs = {
|
||||
'images',
|
||||
'logos',
|
||||
|
|
@ -117,7 +126,7 @@ class FileService {
|
|||
|
||||
Future<String?> pickMarkdownFile({String? initialDirectory}) async {
|
||||
final result = await FilePicker.pickFiles(
|
||||
dialogTitle: 'Presentatie openen',
|
||||
dialogTitle: _d('Presentatie openen'),
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['md'],
|
||||
initialDirectory: initialDirectory,
|
||||
|
|
@ -144,7 +153,7 @@ class FileService {
|
|||
.replaceAll(RegExp(r'[^\w\s-]'), '')
|
||||
.replaceAll(' ', '_');
|
||||
final result = await FilePicker.saveFile(
|
||||
dialogTitle: 'Opslaan als',
|
||||
dialogTitle: _d('Opslaan als'),
|
||||
fileName: '$safeName.md',
|
||||
initialDirectory: initialDirectory,
|
||||
);
|
||||
|
|
@ -360,7 +369,7 @@ class FileService {
|
|||
|
||||
Future<String?> pickPackageFile({String? initialDirectory}) async {
|
||||
final result = await FilePicker.pickFiles(
|
||||
dialogTitle: 'Pakket importeren',
|
||||
dialogTitle: _d('Pakket importeren'),
|
||||
type: FileType.custom,
|
||||
allowedExtensions: [packageExtension, 'zip'],
|
||||
initialDirectory: initialDirectory,
|
||||
|
|
@ -370,7 +379,7 @@ class FileService {
|
|||
|
||||
Future<String?> pickPackageDestination(Deck deck) async {
|
||||
return FilePicker.saveFile(
|
||||
dialogTitle: 'Pakket exporteren',
|
||||
dialogTitle: _d('Pakket exporteren'),
|
||||
fileName: '${_safeName(deck.title)}.$packageExtension',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,21 @@ import 'package:file_picker/file_picker.dart';
|
|||
import 'package:pasteboard/pasteboard.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../models/slide.dart';
|
||||
|
||||
class ImageService {
|
||||
final String Function() _languageCode;
|
||||
|
||||
ImageService({String Function()? languageCode})
|
||||
: _languageCode = languageCode ?? (() => 'nl');
|
||||
|
||||
String _d(String text) => AppLocalizations.sourceFor(_languageCode(), text);
|
||||
|
||||
Future<String?> pickImage() async {
|
||||
final result = await FilePicker.pickFiles(
|
||||
type: FileType.image,
|
||||
dialogTitle: 'Kies een afbeelding',
|
||||
dialogTitle: _d('Kies een afbeelding'),
|
||||
);
|
||||
return result?.files.single.path;
|
||||
}
|
||||
|
|
@ -18,7 +26,7 @@ class ImageService {
|
|||
Future<String?> pickVideo() async {
|
||||
final result = await FilePicker.pickFiles(
|
||||
type: FileType.video,
|
||||
dialogTitle: 'Kies een video',
|
||||
dialogTitle: _d('Kies een video'),
|
||||
);
|
||||
return result?.files.single.path;
|
||||
}
|
||||
|
|
@ -26,7 +34,7 @@ class ImageService {
|
|||
Future<String?> pickAudio() async {
|
||||
final result = await FilePicker.pickFiles(
|
||||
type: FileType.audio,
|
||||
dialogTitle: 'Kies een audiobestand',
|
||||
dialogTitle: _d('Kies een audiobestand'),
|
||||
);
|
||||
return result?.files.single.path;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
|
||||
import '../models/settings.dart';
|
||||
|
||||
/// Builds a single, self-contained HTML file from a deck's Marp Markdown.
|
||||
///
|
||||
/// The output embeds (inlines) `marked` for Markdown, `highlight.js` for code,
|
||||
|
|
@ -10,21 +15,32 @@ import 'package:flutter/services.dart' show rootBundle;
|
|||
/// so theme fidelity differs from the in-app preview / PDF / PPTX. The strength
|
||||
/// here is a portable, dependency-free presentation that opens in any browser.
|
||||
class MarpHtmlService {
|
||||
/// Reads a bundled asset (defaults to the Flutter asset bundle). Injectable so
|
||||
/// the builder can be unit-tested against the on-disk asset files.
|
||||
/// Reads a bundled text asset (defaults to the Flutter asset bundle).
|
||||
/// Injectable so the builder can be unit-tested against the on-disk files.
|
||||
final Future<String> Function(String asset) loadAsset;
|
||||
|
||||
MarpHtmlService({Future<String> Function(String asset)? loadAsset})
|
||||
: loadAsset = loadAsset ?? rootBundle.loadString;
|
||||
/// Reads a bundled binary asset (used to embed the EB Garamond font).
|
||||
final Future<Uint8List> Function(String asset) loadBytes;
|
||||
|
||||
MarpHtmlService({
|
||||
Future<String> Function(String asset)? loadAsset,
|
||||
Future<Uint8List> Function(String asset)? loadBytes,
|
||||
}) : loadAsset = loadAsset ?? rootBundle.loadString,
|
||||
loadBytes =
|
||||
loadBytes ??
|
||||
((a) async => (await rootBundle.load(a)).buffer.asUint8List());
|
||||
|
||||
static const _assetDir = 'assets/web_export';
|
||||
|
||||
Future<String> build(String deckMarkdown) async {
|
||||
/// Builds the HTML. When [theme] is given, the slides take that profile's
|
||||
/// colours and font so the export matches the in-app / PDF look.
|
||||
Future<String> build(String deckMarkdown, {ThemeProfile? theme}) async {
|
||||
final marked = await loadAsset('$_assetDir/marked.min.js');
|
||||
final hljs = await loadAsset('$_assetDir/highlight.min.js');
|
||||
final hljsCss = await loadAsset('$_assetDir/highlight.css');
|
||||
final mathjax = await loadAsset('$_assetDir/tex-svg.js');
|
||||
final mermaid = await loadAsset('$_assetDir/mermaid.min.js');
|
||||
final css = theme == null ? _baseCss : await _themedCss(theme);
|
||||
|
||||
final sections = StringBuffer();
|
||||
for (final slide in marpSlides(deckMarkdown)) {
|
||||
|
|
@ -40,7 +56,7 @@ class MarpHtmlService {
|
|||
'<html lang="nl"><head><meta charset="utf-8">'
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
||||
'<title>OciDeck export</title>'
|
||||
'<style>$_baseCss\n$hljsCss</style>'
|
||||
'<style>$css\n$hljsCss</style>'
|
||||
'<script>$_mathjaxConfig</script>'
|
||||
'${inline(marked)}'
|
||||
'${inline(hljs)}'
|
||||
|
|
@ -85,6 +101,61 @@ class MarpHtmlService {
|
|||
.replaceAll('</script', r'<\/script')
|
||||
.replaceAll('</SCRIPT', r'<\/SCRIPT');
|
||||
|
||||
/// CSS that mirrors the deck's [ThemeProfile]: slide background, text and
|
||||
/// accent colours, table colours and font. The EB Garamond font is embedded
|
||||
/// (base64) so it renders offline; other fonts resolve to system families.
|
||||
Future<String> _themedCss(ThemeProfile t) async {
|
||||
final fontFace = await _ebGaramondFontFace(t.fontFamily);
|
||||
final family = _cssFontStack(t.fontFamily);
|
||||
return '$fontFace\n'
|
||||
'*{box-sizing:border-box}'
|
||||
'html,body{margin:0;padding:0}'
|
||||
'body{background:#1e1e1e;font-family:$family;color:${t.textColor}}'
|
||||
'.slide{position:relative;width:1280px;min-height:720px;margin:24px auto;'
|
||||
'background:${t.slideBackgroundColor};color:${t.textColor};padding:60px;'
|
||||
'overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,.4);border-radius:4px;'
|
||||
'font-family:$family}'
|
||||
'.slide h1{font-size:48px;margin:.15em 0;color:${t.textColor}}'
|
||||
'.slide h2{font-size:34px;margin:.15em 0;color:${t.accentColor}}'
|
||||
'.slide a{color:${t.accentColor}}'
|
||||
'.slide p,.slide li{font-size:24px;line-height:1.45}'
|
||||
'.slide pre{background:#f6f8fa;border:1px solid #e1e4e8;border-radius:6px;'
|
||||
'padding:16px;overflow:auto;font-size:18px}'
|
||||
'.slide code{font-family:SFMono-Regular,Consolas,"Liberation Mono",monospace}'
|
||||
'.slide pre.mermaid{background:transparent;border:0;text-align:center}'
|
||||
'.slide img{max-width:100%}'
|
||||
'.slide blockquote{border-left:4px solid ${t.accentColor};margin:.5em 0;'
|
||||
'padding-left:16px;opacity:.85}'
|
||||
'.slide table{border-collapse:collapse}'
|
||||
'.slide th{background:${t.sectionBackgroundColor};color:${t.tableHeaderTextColor};'
|
||||
'border:1px solid #ccc;padding:6px 12px;font-size:20px}'
|
||||
'.slide td{color:${t.tableTextColor};border:1px solid #ccc;padding:6px 12px;font-size:20px}'
|
||||
'@media print{body{background:#fff}.slide{margin:0;box-shadow:none;'
|
||||
'border-radius:0;page-break-after:always;width:100%;min-height:100vh}}';
|
||||
}
|
||||
|
||||
String _cssFontStack(String font) {
|
||||
if (font == 'EB Garamond') return "'EB Garamond', Georgia, serif";
|
||||
const serif = {'Georgia', 'Times New Roman'};
|
||||
final generic = serif.contains(font) ? 'serif' : 'sans-serif';
|
||||
return "'$font', $generic";
|
||||
}
|
||||
|
||||
/// Embed the bundled EB Garamond variable font as base64 so it works offline.
|
||||
/// Returns an empty string for any other (system) font.
|
||||
Future<String> _ebGaramondFontFace(String font) async {
|
||||
if (font != 'EB Garamond') return '';
|
||||
try {
|
||||
final bytes = await loadBytes('assets/fonts/EBGaramond-Variable.ttf');
|
||||
final b64 = base64Encode(bytes);
|
||||
return "@font-face{font-family:'EB Garamond';font-weight:400 800;"
|
||||
"font-style:normal;src:url(data:font/ttf;base64,$b64) "
|
||||
"format('truetype');}";
|
||||
} catch (_) {
|
||||
return ''; // Fall back to the CSS font stack if the asset is missing.
|
||||
}
|
||||
}
|
||||
|
||||
static const _mathjaxConfig =
|
||||
r'''window.MathJax={tex:{inlineMath:[['$','$']],displayMath:[['$$','$$']]},svg:{fontCache:'global'},startup:{typeset:false}};''';
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,17 @@ import 'settings_provider.dart';
|
|||
final markdownServiceProvider = Provider<MarkdownService>(
|
||||
(_) => MarkdownService(),
|
||||
);
|
||||
final imageServiceProvider = Provider<ImageService>((_) => ImageService());
|
||||
final imageServiceProvider = Provider<ImageService>((ref) {
|
||||
return ImageService(
|
||||
languageCode: () => ref.read(settingsProvider).languageCode,
|
||||
);
|
||||
});
|
||||
final fileServiceProvider = Provider<FileService>((ref) {
|
||||
return FileService(
|
||||
ref.read(markdownServiceProvider),
|
||||
ref.read(imageServiceProvider),
|
||||
() => ref.read(settingsProvider).themeProfile,
|
||||
languageCode: () => ref.read(settingsProvider).languageCode,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ class SettingsNotifier extends StateNotifier<AppSettings> {
|
|||
.toList();
|
||||
final profiles = _uniqueProfiles(loadedProfiles);
|
||||
state = AppSettings(
|
||||
languageCode: prefs.getString('languageCode') ?? 'nl',
|
||||
homeDirectory: prefs.getString('homeDirectory'),
|
||||
exportDirectory: prefs.getString('exportDirectory'),
|
||||
themeProfiles: profiles.isEmpty ? const [ThemeProfile()] : profiles,
|
||||
|
|
@ -48,6 +49,12 @@ class SettingsNotifier extends StateNotifier<AppSettings> {
|
|||
await prefs.setStringList('recentFiles', updated);
|
||||
}
|
||||
|
||||
Future<void> setLanguageCode(String code) async {
|
||||
state = state.copyWith(languageCode: code);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('languageCode', code);
|
||||
}
|
||||
|
||||
Future<void> setHomeDirectory(String? path) async {
|
||||
state = path == null
|
||||
? state.copyWith(clearHomeDirectory: true)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import '../state/editor_provider.dart';
|
|||
import '../state/settings_provider.dart';
|
||||
import '../state/tabs_provider.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'dialogs/export_dialog.dart';
|
||||
import 'dialogs/find_replace_dialog.dart';
|
||||
import 'dialogs/image_carousel_picker.dart';
|
||||
|
|
@ -50,20 +51,23 @@ Future<void> _openWithSearch(
|
|||
|
||||
/// Vraag een URL op om een presentatie (pakket of markdown) op te halen.
|
||||
Future<String?> _showUrlDialog(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final controller = TextEditingController();
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Importeren via URL'),
|
||||
title: Text(l10n.d('Importeren via URL')),
|
||||
content: SizedBox(
|
||||
width: 460,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Plak de link naar een .ocideck-pakket of een Marp-markdownbestand.',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF64748B)),
|
||||
Text(
|
||||
l10n.d(
|
||||
'Plak de link naar een .ocideck-pakket of een Marp-markdownbestand.',
|
||||
),
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF64748B)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
|
|
@ -84,12 +88,12 @@ Future<String?> _showUrlDialog(BuildContext context) {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Annuleren'),
|
||||
child: Text(l10n.t('cancel')),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => Navigator.pop(ctx, controller.text),
|
||||
icon: const Icon(Icons.download, size: 16),
|
||||
label: const Text('Ophalen'),
|
||||
label: Text(l10n.d('Ophalen')),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -160,46 +164,48 @@ class _AppShellState extends ConsumerState<AppShell> with WindowListener {
|
|||
final restore = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Niet-opgeslagen werk herstellen?'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
snapshots.length == 1
|
||||
? 'Er is een presentatie met niet-opgeslagen wijzigingen '
|
||||
'gevonden van een vorige sessie:'
|
||||
: 'Er zijn ${snapshots.length} presentaties met '
|
||||
'niet-opgeslagen wijzigingen gevonden van een vorige '
|
||||
'sessie:',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
for (final s in snapshots)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
child: Text(
|
||||
'• ${s.label} · ${_formatWhen(s.savedAt)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: Color(0xFF475569),
|
||||
builder: (ctx) {
|
||||
final l10n = ctx.l10n;
|
||||
return AlertDialog(
|
||||
title: Text(l10n.d('Niet-opgeslagen werk herstellen?')),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
snapshots.length == 1
|
||||
? l10n.d(
|
||||
'Er is een presentatie met niet-opgeslagen wijzigingen gevonden van een vorige sessie:',
|
||||
)
|
||||
: '${l10n.d('Er zijn')} ${snapshots.length} ${l10n.d('presentaties met niet-opgeslagen wijzigingen gevonden van een vorige sessie:')}',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
for (final s in snapshots)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
child: Text(
|
||||
'• ${s.label} · ${_formatWhen(s.savedAt)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: Color(0xFF475569),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(l10n.d('Verwijderen')),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(l10n.d('Herstellen')),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Verwijderen'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('Herstellen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (restore == true) {
|
||||
|
|
@ -224,8 +230,9 @@ class _AppShellState extends ConsumerState<AppShell> with WindowListener {
|
|||
void onWindowClose() async {
|
||||
if (ref.read(tabsProvider).anyDirty) {
|
||||
final shouldSave = await _confirmSaveBeforeClose(
|
||||
'Er zijn presentaties met niet-opgeslagen wijzigingen. '
|
||||
'Sla ze op voordat de app sluit.',
|
||||
context.l10n.d(
|
||||
'Er zijn presentaties met niet-opgeslagen wijzigingen. Sla ze op voordat de app sluit.',
|
||||
),
|
||||
);
|
||||
if (!shouldSave) return;
|
||||
final saved = await _saveAllDirtyTabs();
|
||||
|
|
@ -246,20 +253,23 @@ class _AppShellState extends ConsumerState<AppShell> with WindowListener {
|
|||
return await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Niet-opgeslagen wijzigingen'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Annuleren'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('Opslaan en sluiten'),
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (ctx) {
|
||||
final l10n = ctx.l10n;
|
||||
return AlertDialog(
|
||||
title: Text(l10n.d('Niet-opgeslagen wijzigingen')),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(l10n.t('cancel')),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text(l10n.d('Opslaan en sluiten')),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
|
@ -278,8 +288,9 @@ class _AppShellState extends ConsumerState<AppShell> with WindowListener {
|
|||
final tab = ref.read(tabsProvider).tabs[index];
|
||||
if (tab.isDirty) {
|
||||
final shouldSave = await _confirmSaveBeforeClose(
|
||||
'Deze presentatie heeft niet-opgeslagen wijzigingen. '
|
||||
'Sla de presentatie op voordat het tabblad sluit.',
|
||||
context.l10n.d(
|
||||
'Deze presentatie heeft niet-opgeslagen wijzigingen. Sla de presentatie op voordat het tabblad sluit.',
|
||||
),
|
||||
);
|
||||
if (!shouldSave) return;
|
||||
final saved = await tab.deckNotifier.save(
|
||||
|
|
@ -343,10 +354,11 @@ class _AppShellState extends ConsumerState<AppShell> with WindowListener {
|
|||
if (tab == null || !tab.isOpen) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Open eerst een presentatie om afbeeldingen toe te '
|
||||
'voegen.',
|
||||
context.l10n.d(
|
||||
'Open eerst een presentatie om afbeeldingen toe te voegen.',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -502,6 +514,7 @@ class _AppTabBar extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
height: 36,
|
||||
color: _bgColor,
|
||||
|
|
@ -525,7 +538,7 @@ class _AppTabBar extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: 'Nieuw tabblad',
|
||||
message: l10n.t('newTab'),
|
||||
child: InkWell(
|
||||
onTap: onAdd,
|
||||
child: const SizedBox(
|
||||
|
|
@ -635,6 +648,7 @@ class _WelcomeScreen extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = context.l10n;
|
||||
final homeDir = ref.watch(settingsProvider.select((s) => s.homeDirectory));
|
||||
final recentFiles = ref.watch(
|
||||
settingsProvider.select((s) => s.recentFiles),
|
||||
|
|
@ -667,7 +681,7 @@ class _WelcomeScreen extends ConsumerWidget {
|
|||
child: ElevatedButton.icon(
|
||||
onPressed: () => _newDeck(context, ref),
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
label: const Text('Nieuwe presentatie'),
|
||||
label: Text(l10n.t('newPresentation')),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
|
@ -676,7 +690,7 @@ class _WelcomeScreen extends ConsumerWidget {
|
|||
child: OutlinedButton.icon(
|
||||
onPressed: () => _openWithSearch(context, ref, homeDir),
|
||||
icon: const Icon(Icons.folder_open_outlined, size: 18),
|
||||
label: const Text('Openen...'),
|
||||
label: Text(l10n.t('open')),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -694,11 +708,11 @@ class _WelcomeScreen extends ConsumerWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 20, 16, 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 10),
|
||||
child: Text(
|
||||
'Recente presentaties',
|
||||
style: TextStyle(
|
||||
l10n.t('recentPresentations'),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF94A3B8),
|
||||
|
|
@ -802,6 +816,7 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
final deck = deckState.deck!;
|
||||
final editor = ref.watch(editorProvider);
|
||||
final settings = ref.watch(settingsProvider);
|
||||
final l10n = context.l10n;
|
||||
final deckNotifier = ref.read(deckProvider.notifier);
|
||||
final editorNotifier = ref.read(editorProvider.notifier);
|
||||
|
||||
|
|
@ -868,9 +883,11 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
if (updated == null) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Deze slide kan geen afbeelding ontvangen. Kies eerst een afbeeldingsslide.',
|
||||
l10n.d(
|
||||
'Deze slide kan geen afbeelding ontvangen. Kies eerst een afbeeldingsslide.',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -889,8 +906,10 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
];
|
||||
if (visible.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Alle slides zijn overgeslagen — niets om te tonen.'),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10n.d('Alle slides zijn overgeslagen — niets om te tonen.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
|
|
@ -911,9 +930,9 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
final slides = deck.slides.where((s) => !s.skipped).toList();
|
||||
if (slides.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Alle slides zijn overgeslagen — niets om te exporteren.',
|
||||
l10n.d('Alle slides zijn overgeslagen — niets om te exporteren.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -932,6 +951,13 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
);
|
||||
}
|
||||
|
||||
final canExport = deckState.filePath != null && !deckState.isDirty;
|
||||
final exportTooltip = deckState.filePath == null
|
||||
? l10n.t('exportNeedsSave')
|
||||
: deckState.isDirty
|
||||
? l10n.t('exportNeedsClean')
|
||||
: l10n.t('exportReady');
|
||||
|
||||
void toggleMarkdownMode() {
|
||||
if (isMarkdownMode) {
|
||||
editorNotifier.setMode(EditorMode.visual);
|
||||
|
|
@ -981,13 +1007,15 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
await fileService.exportPackage(deck, dest);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Pakket geëxporteerd naar:\n$dest')),
|
||||
SnackBar(
|
||||
content: Text('${l10n.d('Pakket geëxporteerd naar:')}\n$dest'),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Export mislukt: $e')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${l10n.d('Export mislukt:')} $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1002,7 +1030,7 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
.importPackageFile(path, homeDir: settings.homeDirectory);
|
||||
if (!ok && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Kon dit pakket niet importeren.')),
|
||||
SnackBar(content: Text(l10n.d('Kon dit pakket niet importeren.'))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1015,8 +1043,8 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
.importFromUrl(url, homeDir: settings.homeDirectory);
|
||||
if (!ok && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Kon van deze URL geen presentatie ophalen.'),
|
||||
SnackBar(
|
||||
content: Text(l10n.d('Kon van deze URL geen presentatie ophalen.')),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -1106,14 +1134,14 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
actions: [
|
||||
// ── Bewerken ────────────────────────────────────────────────
|
||||
Tooltip(
|
||||
message: 'Ongedaan maken (Ctrl/Cmd+Z)',
|
||||
message: l10n.t('undo'),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.undo, size: 18),
|
||||
onPressed: deckState.canUndo ? deckNotifier.undo : null,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: 'Opnieuw uitvoeren (Ctrl/Cmd+Shift+Z)',
|
||||
message: l10n.t('redo'),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.redo, size: 18),
|
||||
onPressed: deckState.canRedo ? deckNotifier.redo : null,
|
||||
|
|
@ -1122,7 +1150,7 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
const _ActionsDivider(),
|
||||
// ── Inhoud ──────────────────────────────────────────────────
|
||||
Tooltip(
|
||||
message: 'Afbeeldingenbibliotheek',
|
||||
message: l10n.t('imageLibrary'),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.photo_library_outlined, size: 18),
|
||||
onPressed: openImageCarousel,
|
||||
|
|
@ -1131,15 +1159,16 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
const _ActionsDivider(),
|
||||
// ── Presenteren & uitvoer ───────────────────────────────────
|
||||
Tooltip(
|
||||
message:
|
||||
'Presenteren (volledig scherm) · P voor presenter view',
|
||||
message: l10n.t('presentFullscreen'),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.play_circle_outline, size: 20),
|
||||
onPressed: presentDeck,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: isMarkdownMode ? 'Visuele modus' : 'Markdown modus',
|
||||
message: isMarkdownMode
|
||||
? l10n.t('visualMode')
|
||||
: l10n.t('markdownMode'),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
isMarkdownMode ? Icons.view_quilt : Icons.code,
|
||||
|
|
@ -1149,25 +1178,23 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: 'Opslaan (Ctrl/Cmd+S)',
|
||||
message: l10n.t('saveShortcut'),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.save_outlined, size: 18),
|
||||
onPressed: saveDeck,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: 'Exporteren (PDF/PPTX)',
|
||||
message: exportTooltip,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.upload_file_outlined, size: 18),
|
||||
onPressed: (deckState.filePath != null && !deckState.isDirty)
|
||||
? exportDeck
|
||||
: null,
|
||||
onPressed: canExport ? exportDeck : null,
|
||||
),
|
||||
),
|
||||
const _ActionsDivider(),
|
||||
// ── Overig (minder vaak gebruikt) ───────────────────────────
|
||||
PopupMenuButton<String>(
|
||||
tooltip: 'Meer',
|
||||
tooltip: l10n.t('more'),
|
||||
icon: const Icon(Icons.more_vert, size: 20),
|
||||
position: PopupMenuPosition.under,
|
||||
onSelected: (v) {
|
||||
|
|
@ -1205,27 +1232,31 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
menuItem(
|
||||
'new_tab',
|
||||
Icons.add_circle_outline,
|
||||
'Nieuwe presentatie (tab)',
|
||||
l10n.t('newPresentationTab'),
|
||||
),
|
||||
menuItem(
|
||||
'open',
|
||||
Icons.folder_open_outlined,
|
||||
l10n.t('openEllipsis'),
|
||||
),
|
||||
menuItem('open', Icons.folder_open_outlined, 'Openen…'),
|
||||
const PopupMenuDivider(),
|
||||
menuItem(
|
||||
'export_package',
|
||||
Icons.inventory_2_outlined,
|
||||
'Pakket exporteren…',
|
||||
l10n.t('exportPackage'),
|
||||
),
|
||||
menuItem(
|
||||
'import_package',
|
||||
Icons.unarchive_outlined,
|
||||
'Pakket importeren…',
|
||||
l10n.t('importPackage'),
|
||||
),
|
||||
menuItem('import_url', Icons.link, 'Importeren via URL…'),
|
||||
menuItem('import_url', Icons.link, l10n.t('importUrl')),
|
||||
const PopupMenuDivider(),
|
||||
menuItem('find', Icons.find_replace, 'Zoeken en vervangen'),
|
||||
menuItem('find', Icons.find_replace, l10n.t('findReplace')),
|
||||
menuItem(
|
||||
'full_preview',
|
||||
Icons.preview_outlined,
|
||||
'Volledig deck bekijken',
|
||||
l10n.t('fullDeckPreview'),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
for (final profile in settings.themeProfiles)
|
||||
|
|
@ -1243,7 +1274,7 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
const SizedBox(width: 10),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Stijl: ${profile.name}',
|
||||
'${l10n.t('styleProfile')}: ${profile.name}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
|
@ -1254,14 +1285,26 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
menuItem(
|
||||
'properties',
|
||||
Icons.info_outline,
|
||||
'Presentatie-eigenschappen',
|
||||
l10n.t('presentationProperties'),
|
||||
),
|
||||
menuItem(
|
||||
'settings',
|
||||
Icons.settings_outlined,
|
||||
l10n.t('settings'),
|
||||
),
|
||||
menuItem('settings', Icons.settings_outlined, 'Instellingen'),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: _DeckStatusBar(
|
||||
deck: deck,
|
||||
deckState: deckState,
|
||||
exportDirectory: settings.exportDirectory,
|
||||
onSave: saveDeck,
|
||||
onExport: canExport ? exportDeck : null,
|
||||
exportTooltip: exportTooltip,
|
||||
),
|
||||
body: Builder(
|
||||
builder: (ctx) {
|
||||
if (deckState.error != null) {
|
||||
|
|
@ -1323,6 +1366,212 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
|
||||
// ── AppBar helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
class _DeckStatusBar extends StatelessWidget {
|
||||
final Deck deck;
|
||||
final DeckState deckState;
|
||||
final String? exportDirectory;
|
||||
final Future<void> Function() onSave;
|
||||
final VoidCallback? onExport;
|
||||
final String exportTooltip;
|
||||
|
||||
const _DeckStatusBar({
|
||||
required this.deck,
|
||||
required this.deckState,
|
||||
required this.exportDirectory,
|
||||
required this.onSave,
|
||||
required this.onExport,
|
||||
required this.exportTooltip,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final skipped = deck.slides.where((s) => s.skipped).length;
|
||||
final fileLabel = deckState.filePath == null
|
||||
? l10n.t('notSavedYet')
|
||||
: p.basename(deckState.filePath!);
|
||||
final saveLabel = deckState.isDirty ? l10n.t('unsaved') : l10n.t('saved');
|
||||
final exportLabel = exportDirectory == null
|
||||
? l10n.t('exportNextToDeck')
|
||||
: '${l10n.t('exportFolder')}: ${p.basename(exportDirectory!)}';
|
||||
|
||||
return Material(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
child: Container(
|
||||
height: 30,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(top: BorderSide(color: Color(0xFFE2E8F0))),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_StatusAction(
|
||||
icon: deckState.isDirty
|
||||
? Icons.radio_button_checked
|
||||
: Icons.check_circle_outline,
|
||||
label: saveLabel,
|
||||
tooltip: deckState.isDirty
|
||||
? l10n.t('unsavedChanges')
|
||||
: l10n.t('noUnsavedChanges'),
|
||||
color: deckState.isDirty
|
||||
? const Color(0xFFD97706)
|
||||
: const Color(0xFF15803D),
|
||||
onTap: () => onSave(),
|
||||
),
|
||||
const _StatusDivider(),
|
||||
_StatusItem(
|
||||
icon: Icons.description_outlined,
|
||||
label: fileLabel,
|
||||
tooltip: deckState.filePath ?? l10n.t('noFileYet'),
|
||||
),
|
||||
const _StatusDivider(),
|
||||
_StatusItem(
|
||||
icon: Icons.slideshow_outlined,
|
||||
label: skipped == 0
|
||||
? '${deck.slides.length} ${l10n.t('slides')}'
|
||||
: '${deck.slides.length} ${l10n.t('slides')} · $skipped ${l10n.t('skipped')}',
|
||||
tooltip: skipped == 0
|
||||
? l10n.t('allSlidesIncluded')
|
||||
: '$skipped ${l10n.t('skippedSlidesExcluded')}',
|
||||
color: skipped == 0 ? null : const Color(0xFF8A6D3B),
|
||||
),
|
||||
const _StatusDivider(),
|
||||
_StatusItem(
|
||||
icon: Icons.palette_outlined,
|
||||
label: deck.themeProfile.name,
|
||||
tooltip: '${l10n.t('styleProfile')}: ${deck.themeProfile.name}',
|
||||
),
|
||||
if (deck.tlp != TlpLevel.none) ...[
|
||||
const _StatusDivider(),
|
||||
_StatusItem(
|
||||
icon: Icons.shield_outlined,
|
||||
label: deck.tlp.label,
|
||||
tooltip: '${l10n.t('classification')}: ${deck.tlp.label}',
|
||||
color: Color(deck.tlp.foreground),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
_StatusItem(
|
||||
icon: Icons.folder_outlined,
|
||||
label: exportLabel,
|
||||
tooltip: exportDirectory ?? l10n.t('exportsNextToDeck'),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
_StatusAction(
|
||||
icon: Icons.upload_file_outlined,
|
||||
label: l10n.t('export'),
|
||||
tooltip: exportTooltip,
|
||||
onTap: onExport,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String tooltip;
|
||||
final Color? color;
|
||||
|
||||
const _StatusItem({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.tooltip,
|
||||
this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fg = color ?? const Color(0xFF64748B);
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 13, color: fg),
|
||||
const SizedBox(width: 4),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 210),
|
||||
child: Text(
|
||||
label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: fg,
|
||||
fontWeight: color == null ? FontWeight.normal : FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusAction extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String tooltip;
|
||||
final Color? color;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _StatusAction({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.tooltip,
|
||||
this.color,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enabled = onTap != null;
|
||||
final fg = enabled ? (color ?? AppTheme.accent) : const Color(0xFF94A3B8);
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 13, color: fg),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: fg,
|
||||
fontWeight: enabled ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusDivider extends StatelessWidget {
|
||||
const _StatusDivider();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 1,
|
||||
height: 14,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
color: const Color(0xFFE2E8F0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dunne verticale scheiding tussen groepen AppBar-knoppen.
|
||||
class _ActionsDivider extends StatelessWidget {
|
||||
const _ActionsDivider();
|
||||
|
|
@ -1353,6 +1602,7 @@ class _ResizableDividerState extends State<_ResizableDivider> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final active = _hovered || _dragging;
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.resizeColumn,
|
||||
|
|
@ -1365,7 +1615,9 @@ class _ResizableDividerState extends State<_ResizableDivider> {
|
|||
onHorizontalDragCancel: () => setState(() => _dragging = false),
|
||||
onHorizontalDragUpdate: (details) => widget.onDrag(details.delta.dx),
|
||||
child: Tooltip(
|
||||
message: 'Sleep om de slide-preview breder of smaller te maken',
|
||||
message: l10n.d(
|
||||
'Sleep om de slide-preview breder of smaller te maken',
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 9,
|
||||
child: Center(
|
||||
|
|
@ -1393,6 +1645,7 @@ class _TlpChip extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final isSet = tlp != TlpLevel.none;
|
||||
final fg = Color(tlp.foreground);
|
||||
|
||||
|
|
@ -1432,7 +1685,7 @@ class _TlpChip extends StatelessWidget {
|
|||
);
|
||||
|
||||
return PopupMenuButton<TlpLevel>(
|
||||
tooltip: 'TLP-classificatie (Traffic Light Protocol)',
|
||||
tooltip: l10n.d('TLP-classificatie (Traffic Light Protocol)'),
|
||||
position: PopupMenuPosition.under,
|
||||
onSelected: onSelected,
|
||||
itemBuilder: (_) => [
|
||||
|
|
@ -1453,7 +1706,7 @@ class _TlpChip extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(level.menuLabel),
|
||||
Text(level == TlpLevel.none ? l10n.d('Geen') : level.label),
|
||||
if (level == tlp) ...[
|
||||
const SizedBox(width: 12),
|
||||
const Spacer(),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import '../../models/slide.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
class AddSlideDialog extends StatelessWidget {
|
||||
const AddSlideDialog({super.key});
|
||||
|
|
@ -33,6 +34,7 @@ class AddSlideDialog extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return CallbackShortcuts(
|
||||
bindings: {
|
||||
const SingleActivator(LogicalKeyboardKey.escape): () =>
|
||||
|
|
@ -41,7 +43,7 @@ class AddSlideDialog extends StatelessWidget {
|
|||
child: Focus(
|
||||
autofocus: true,
|
||||
child: AlertDialog(
|
||||
title: const Text('Slide type kiezen'),
|
||||
title: Text(l10n.d('Slide type kiezen')),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
child: Wrap(
|
||||
|
|
@ -51,7 +53,7 @@ class AddSlideDialog extends StatelessWidget {
|
|||
final (type, icon, label) = entry;
|
||||
return _TypeCard(
|
||||
icon: icon,
|
||||
label: label,
|
||||
label: l10n.d(label),
|
||||
onTap: () => Navigator.pop(context, type),
|
||||
);
|
||||
}).toList(),
|
||||
|
|
@ -60,7 +62,7 @@ class AddSlideDialog extends StatelessWidget {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuleren'),
|
||||
child: Text(l10n.t('cancel')),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import '../../models/settings.dart';
|
|||
import '../../models/slide.dart';
|
||||
import '../../services/export_service.dart';
|
||||
import '../../services/slide_rasterizer.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
/// Exports the deck by rendering the on-screen slide previews to images and
|
||||
/// packing them into a PDF or PPTX (WYSIWYG — the export matches the preview).
|
||||
|
|
@ -79,12 +80,13 @@ class _ExportDialogState extends State<ExportDialog> {
|
|||
bool _compress = false;
|
||||
|
||||
Future<void> _export(ExportFormat format, {bool compress = false}) async {
|
||||
final l10n = context.l10n;
|
||||
// HTML renders from Markdown in the browser, so it needs no slide raster.
|
||||
final needsRaster = format != ExportFormat.html;
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_result = null;
|
||||
_phase = needsRaster ? 'Slides renderen…' : 'HTML samenstellen…';
|
||||
_phase = needsRaster ? l10n.t('renderingSlides') : l10n.t('buildingHtml');
|
||||
_done = 0;
|
||||
_total = needsRaster ? widget.slides.length : 0;
|
||||
});
|
||||
|
|
@ -103,7 +105,7 @@ class _ExportDialogState extends State<ExportDialog> {
|
|||
: const <Uint8List>[];
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _phase = '${format.label} samenstellen…');
|
||||
setState(() => _phase = '${format.label} ${l10n.t('buildingExport')}');
|
||||
|
||||
final r = await widget.exportService.export(
|
||||
widget.deckPath,
|
||||
|
|
@ -114,37 +116,42 @@ class _ExportDialogState extends State<ExportDialog> {
|
|||
// Speaker notes travel 1:1 with the rendered slides (PPTX notes pane).
|
||||
notes: [for (final s in widget.slides) s.notes],
|
||||
markdown: widget.markdown,
|
||||
themeProfile: widget.themeProfile,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_success = r.success;
|
||||
_result = r.success ? 'Geëxporteerd naar:\n${r.outputPath}' : r.error;
|
||||
_result = r.success
|
||||
? '${l10n.t('exportedTo')}\n${r.outputPath}'
|
||||
: r.error;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: const Text('Exporteren'),
|
||||
title: Text(l10n.t('exportDialogTitle')),
|
||||
content: SizedBox(width: 380, child: _content()),
|
||||
actions: [
|
||||
if (_result != null && _success)
|
||||
TextButton(
|
||||
onPressed: () => setState(() => _result = null),
|
||||
child: const Text('Nogmaals exporteren'),
|
||||
child: Text(l10n.t('exportAgain')),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _loading ? null : () => Navigator.pop(context),
|
||||
child: const Text('Sluiten'),
|
||||
child: Text(l10n.t('close')),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _content() {
|
||||
final l10n = context.l10n;
|
||||
if (_loading) {
|
||||
final fraction = _total == 0 ? null : _done / _total;
|
||||
return Column(
|
||||
|
|
@ -162,7 +169,9 @@ class _ExportDialogState extends State<ExportDialog> {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_total == 0 ? '' : 'Slide $_done van $_total',
|
||||
_total == 0
|
||||
? ''
|
||||
: '${l10n.t('slideOf')} $_done ${l10n.t('of')} $_total',
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
],
|
||||
|
|
@ -195,19 +204,18 @@ class _ExportDialogState extends State<ExportDialog> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Text(
|
||||
'De export gebruikt exact de weergave uit de editor, inclusief je '
|
||||
'stijlprofiel.',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF64748B)),
|
||||
l10n.t('exportIntro'),
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF64748B)),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 6),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Text(
|
||||
'Afbeeldingskwaliteit (PDF)',
|
||||
style: TextStyle(
|
||||
l10n.t('imageQualityPdf'),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF475569),
|
||||
|
|
@ -215,16 +223,16 @@ class _ExportDialogState extends State<ExportDialog> {
|
|||
),
|
||||
),
|
||||
SegmentedButton<bool>(
|
||||
segments: const [
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: false,
|
||||
icon: Icon(Icons.image_outlined),
|
||||
label: Text('Normaal'),
|
||||
icon: const Icon(Icons.image_outlined),
|
||||
label: Text(l10n.t('normal')),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: true,
|
||||
icon: Icon(Icons.compress),
|
||||
label: Text('Gecomprimeerd'),
|
||||
icon: const Icon(Icons.compress),
|
||||
label: Text(l10n.t('compressed')),
|
||||
),
|
||||
],
|
||||
selected: {_compress},
|
||||
|
|
@ -235,26 +243,23 @@ class _ExportDialogState extends State<ExportDialog> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 8),
|
||||
child: Text(
|
||||
_compress
|
||||
? 'JPEG op lagere resolutie — bedoeld als handout, veel kleiner '
|
||||
'bestand (apart opgeslagen als “-compact”).'
|
||||
: 'Verliesvrije afbeeldingen op volledige resolutie.',
|
||||
_compress ? l10n.t('compressedHelp') : l10n.t('losslessHelp'),
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
),
|
||||
_exportButton(
|
||||
icon: _formatIcon(ExportFormat.pdf),
|
||||
label: 'Exporteer als PDF',
|
||||
label: l10n.t('exportAsPdf'),
|
||||
onPressed: () => _export(ExportFormat.pdf, compress: _compress),
|
||||
),
|
||||
_exportButton(
|
||||
icon: _formatIcon(ExportFormat.pptx),
|
||||
label: 'Exporteer als ${ExportFormat.pptx.label}',
|
||||
label: l10n.t('exportAsPptx'),
|
||||
onPressed: () => _export(ExportFormat.pptx),
|
||||
),
|
||||
_exportButton(
|
||||
icon: _formatIcon(ExportFormat.html),
|
||||
label: 'Exporteer als HTML (Marp, offline)',
|
||||
label: l10n.t('exportAsHtml'),
|
||||
onPressed: () => _export(ExportFormat.html),
|
||||
),
|
||||
const Padding(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
/// Telt hoe vaak [query] voorkomt in de hele presentatie.
|
||||
typedef MatchCounter = int Function(String query, bool caseSensitive);
|
||||
|
|
@ -78,9 +79,10 @@ class _FindReplaceDialogState extends State<FindReplaceDialog> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final hasQuery = _find.text.isNotEmpty;
|
||||
return AlertDialog(
|
||||
title: const Text('Zoeken en vervangen'),
|
||||
title: Text(l10n.d('Zoeken en vervangen')),
|
||||
content: SizedBox(
|
||||
width: 420,
|
||||
child: Column(
|
||||
|
|
@ -91,9 +93,9 @@ class _FindReplaceDialogState extends State<FindReplaceDialog> {
|
|||
controller: _find,
|
||||
focusNode: _findFocus,
|
||||
onChanged: (_) => _recount(),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Zoeken naar',
|
||||
prefixIcon: Icon(Icons.search, size: 18),
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.d('Zoeken naar'),
|
||||
prefixIcon: const Icon(Icons.search, size: 18),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
|
|
@ -101,9 +103,9 @@ class _FindReplaceDialogState extends State<FindReplaceDialog> {
|
|||
TextField(
|
||||
controller: _replace,
|
||||
onChanged: (_) => setState(() => _replaced = null),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Vervangen door',
|
||||
prefixIcon: Icon(Icons.edit_outlined, size: 18),
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.d('Vervangen door'),
|
||||
prefixIcon: const Icon(Icons.edit_outlined, size: 18),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
|
|
@ -119,9 +121,9 @@ class _FindReplaceDialogState extends State<FindReplaceDialog> {
|
|||
_recount();
|
||||
},
|
||||
),
|
||||
const Text(
|
||||
'Hoofdlettergevoelig',
|
||||
style: TextStyle(fontSize: 13),
|
||||
Text(
|
||||
l10n.d('Hoofdlettergevoelig'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
const Spacer(),
|
||||
_statusText(hasQuery),
|
||||
|
|
@ -133,25 +135,26 @@ class _FindReplaceDialogState extends State<FindReplaceDialog> {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Sluiten'),
|
||||
child: Text(l10n.t('close')),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: (hasQuery && _matches > 0) ? _runReplace : null,
|
||||
icon: const Icon(Icons.find_replace, size: 16),
|
||||
label: const Text('Vervang alles'),
|
||||
label: Text(l10n.d('Vervang alles')),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _statusText(bool hasQuery) {
|
||||
final l10n = context.l10n;
|
||||
if (_replaced != null) {
|
||||
return Text(
|
||||
_replaced == 0
|
||||
? 'Niets vervangen'
|
||||
? l10n.d('Niets vervangen')
|
||||
: _replaced == 1
|
||||
? '1 vervangen'
|
||||
: '$_replaced vervangen',
|
||||
? '1 ${l10n.d('vervangen')}'
|
||||
: '$_replaced ${l10n.d('vervangen')}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF15803D),
|
||||
|
|
@ -162,10 +165,10 @@ class _FindReplaceDialogState extends State<FindReplaceDialog> {
|
|||
if (!hasQuery) return const SizedBox.shrink();
|
||||
return Text(
|
||||
_matches == 0
|
||||
? 'Geen resultaten'
|
||||
? l10n.d('Geen resultaten')
|
||||
: _matches == 1
|
||||
? '1 resultaat'
|
||||
: '$_matches resultaten',
|
||||
? '1 ${l10n.d('resultaat')}'
|
||||
: '$_matches ${l10n.d('resultaten')}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _matches == 0
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:path/path.dart' as p;
|
|||
import '../../services/caption_service.dart';
|
||||
import '../../services/description_service.dart';
|
||||
import '../../services/image_service.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
/// Resultaat van de afbeeldingencarousel.
|
||||
class ImagePickResult {
|
||||
|
|
@ -290,7 +291,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
Future<void> _browse() async {
|
||||
final result = await FilePicker.pickFiles(
|
||||
type: FileType.image,
|
||||
dialogTitle: 'Kies een afbeelding',
|
||||
dialogTitle: context.l10n.d('Kies een afbeelding'),
|
||||
);
|
||||
if (result?.files.single.path != null && mounted) {
|
||||
final path = result!.files.single.path!;
|
||||
|
|
@ -378,7 +379,9 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
});
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Kopiëren naar klembord mislukt.')),
|
||||
SnackBar(
|
||||
content: Text(context.l10n.d('Kopiëren naar klembord mislukt.')),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -418,107 +421,117 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
Future<bool?> _showDeleteDialog(String path, List<String> usages) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: const Color(0xFF161B22),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
usages.isEmpty
|
||||
? Icons.delete_outline
|
||||
: Icons.warning_amber_rounded,
|
||||
color: usages.isEmpty
|
||||
? const Color(0xFFE5534B)
|
||||
: const Color(0xFFF0B429),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Afbeelding verwijderen?',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
builder: (ctx) {
|
||||
final l10n = ctx.l10n;
|
||||
return AlertDialog(
|
||||
backgroundColor: const Color(0xFF161B22),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
usages.isEmpty
|
||||
? Icons.delete_outline
|
||||
: Icons.warning_amber_rounded,
|
||||
color: usages.isEmpty
|
||||
? const Color(0xFFE5534B)
|
||||
: const Color(0xFFF0B429),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
p.basename(path),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFCDD9E5),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.d('Afbeelding verwijderen?'),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (usages.isEmpty)
|
||||
const Text(
|
||||
'Het bestand wordt permanent van schijf verwijderd. '
|
||||
'Deze actie kan niet ongedaan worden gemaakt.',
|
||||
style: TextStyle(color: Color(0xFF8B949E), fontSize: 13),
|
||||
)
|
||||
else ...[
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Let op: deze afbeelding wordt nog gebruikt in '
|
||||
'${usages.length} ${usages.length == 1 ? "slide" : "slides"}:',
|
||||
p.basename(path),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFF0B429),
|
||||
color: Color(0xFFCDD9E5),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 160),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final u in usages)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
child: Text(
|
||||
'• $u',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFCDD9E5),
|
||||
fontSize: 12.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
if (usages.isEmpty)
|
||||
Text(
|
||||
l10n.d(
|
||||
'Het bestand wordt permanent van schijf verwijderd. Deze actie kan niet ongedaan worden gemaakt.',
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF8B949E),
|
||||
fontSize: 13,
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
Text(
|
||||
'${l10n.d('Let op: deze afbeelding wordt nog gebruikt in')} ${usages.length} ${usages.length == 1 ? l10n.d("slide") : l10n.t("slides")}:',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFF0B429),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Verwijderen maakt die slides leeg. Dit kan niet ongedaan '
|
||||
'worden gemaakt.',
|
||||
style: TextStyle(color: Color(0xFF8B949E), fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 160),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final u in usages)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 3),
|
||||
child: Text(
|
||||
'• $u',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFCDD9E5),
|
||||
fontSize: 12.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
l10n.d(
|
||||
'Verwijderen maakt die slides leeg. Dit kan niet ongedaan worden gemaakt.',
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF8B949E),
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF8B949E),
|
||||
),
|
||||
child: Text(l10n.t('cancel')),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
icon: const Icon(Icons.delete_outline, size: 16),
|
||||
label: Text(l10n.d('Verwijderen')),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFB62324),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF8B949E),
|
||||
),
|
||||
child: const Text('Annuleren'),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
icon: const Icon(Icons.delete_outline, size: 16),
|
||||
label: const Text('Verwijderen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFB62324),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -595,15 +608,16 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
}
|
||||
|
||||
Widget _buildLoading() {
|
||||
return const Center(
|
||||
final l10n = context.l10n;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(color: Color(0xFF3B82F6)),
|
||||
SizedBox(height: 16),
|
||||
const CircularProgressIndicator(color: Color(0xFF3B82F6)),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Afbeeldingen laden…',
|
||||
style: TextStyle(color: Color(0xFF8B949E), fontSize: 14),
|
||||
l10n.d('Afbeeldingen laden…'),
|
||||
style: const TextStyle(color: Color(0xFF8B949E), fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -611,6 +625,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
|
|
@ -632,9 +647,9 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
const Text(
|
||||
'Afbeelding kiezen',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
l10n.d('Afbeelding kiezen'),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
|
@ -667,7 +682,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Color(0xFF6E7681), size: 20),
|
||||
onPressed: () => _close(),
|
||||
tooltip: 'Sluiten (Esc)',
|
||||
tooltip: l10n.d('Sluiten (Esc)'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -675,6 +690,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
}
|
||||
|
||||
Widget _buildSearchField() {
|
||||
final l10n = context.l10n;
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: TextField(
|
||||
|
|
@ -682,7 +698,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
onChanged: _onSearchChanged,
|
||||
style: const TextStyle(color: Color(0xFFCDD9E5), fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Zoek op naam of beschrijving…',
|
||||
hintText: l10n.d('Zoek op naam of beschrijving…'),
|
||||
hintStyle: const TextStyle(color: Color(0xFF6E7681), fontSize: 13),
|
||||
prefixIcon: const Icon(
|
||||
Icons.search,
|
||||
|
|
@ -725,6 +741,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
|
||||
/// Segmented control om tussen raster- en coverflow-weergave te wisselen.
|
||||
Widget _buildViewToggle() {
|
||||
final l10n = context.l10n;
|
||||
Widget seg(_ViewMode mode, IconData icon, String tip) {
|
||||
final active = _viewMode == mode;
|
||||
return Tooltip(
|
||||
|
|
@ -758,9 +775,13 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
seg(_ViewMode.grid, Icons.grid_view_rounded, 'Raster'),
|
||||
seg(_ViewMode.grid, Icons.grid_view_rounded, l10n.d('Raster')),
|
||||
const SizedBox(width: 3),
|
||||
seg(_ViewMode.cover, Icons.view_carousel_rounded, 'Coverflow'),
|
||||
seg(
|
||||
_ViewMode.cover,
|
||||
Icons.view_carousel_rounded,
|
||||
l10n.d('Coverflow'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -768,6 +789,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
|
||||
/// Lege staat — gedeeld door raster- en coverflow-weergave.
|
||||
Widget _buildEmptyState() {
|
||||
final l10n = context.l10n;
|
||||
final filtering = _query.trim().isNotEmpty;
|
||||
return Expanded(
|
||||
flex: 13,
|
||||
|
|
@ -790,8 +812,8 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
const SizedBox(height: 20),
|
||||
Text(
|
||||
filtering
|
||||
? 'Geen resultaten voor "${_query.trim()}"'
|
||||
: 'Geen afbeeldingen gevonden',
|
||||
? '${l10n.d('Geen resultaten voor')} "${_query.trim()}"'
|
||||
: l10n.d('Geen afbeeldingen gevonden'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFCDD9E5),
|
||||
fontSize: 16,
|
||||
|
|
@ -801,8 +823,10 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
const SizedBox(height: 8),
|
||||
Text(
|
||||
filtering
|
||||
? 'Pas je zoekterm aan of voeg een beschrijving toe.'
|
||||
: 'Gebruik "Bladeren" om afbeeldingen van elke locatie te kiezen.',
|
||||
? l10n.d('Pas je zoekterm aan of voeg een beschrijving toe.')
|
||||
: l10n.d(
|
||||
'Gebruik "Bladeren" om afbeeldingen van elke locatie te kiezen.',
|
||||
),
|
||||
style: const TextStyle(color: Color(0xFF6E7681), fontSize: 13),
|
||||
),
|
||||
],
|
||||
|
|
@ -1261,25 +1285,26 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
}
|
||||
|
||||
Widget _buildPreview() {
|
||||
final l10n = context.l10n;
|
||||
return SizedBox(
|
||||
width: 300,
|
||||
child: Container(
|
||||
color: const Color(0xFF080D14),
|
||||
child: _selected == null
|
||||
? const Center(
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
const Icon(
|
||||
Icons.touch_app_outlined,
|
||||
size: 40,
|
||||
color: Color(0xFF30363D),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Selecteer een\nafbeelding',
|
||||
l10n.d('Selecteer een\nafbeelding'),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6E7681),
|
||||
fontSize: 13,
|
||||
height: 1.5,
|
||||
|
|
@ -1363,7 +1388,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
fontSize: 12,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Caption / bronvermelding',
|
||||
hintText: l10n.d('Caption / bronvermelding'),
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xFF6E7681),
|
||||
fontSize: 12,
|
||||
|
|
@ -1408,7 +1433,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
fontSize: 12,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Beschrijving (doorzoekbaar)',
|
||||
hintText: l10n.d('Beschrijving (doorzoekbaar)'),
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xFF6E7681),
|
||||
fontSize: 12,
|
||||
|
|
@ -1455,7 +1480,9 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
size: 16,
|
||||
),
|
||||
label: Text(
|
||||
_justCopied ? 'Gekopieerd' : 'Kopiëren',
|
||||
_justCopied
|
||||
? l10n.d('Gekopieerd')
|
||||
: l10n.d('Kopiëren'),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: _justCopied
|
||||
|
|
@ -1477,7 +1504,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
Icons.delete_outline,
|
||||
size: 16,
|
||||
),
|
||||
label: const Text('Verwijderen'),
|
||||
label: Text(l10n.d('Verwijderen')),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFE5746E),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
|
|
@ -1499,6 +1526,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
}
|
||||
|
||||
Widget _buildFooter() {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
height: 64,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
|
|
@ -1511,7 +1539,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
OutlinedButton.icon(
|
||||
onPressed: _browse,
|
||||
icon: const Icon(Icons.folder_open_outlined, size: 16),
|
||||
label: const Text('Bladeren…'),
|
||||
label: Text(l10n.d('Bladeren…')),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF8B949E),
|
||||
side: const BorderSide(color: Color(0xFF30363D)),
|
||||
|
|
@ -1520,9 +1548,9 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
// Hint
|
||||
const Text(
|
||||
'↑↓←→ navigeren · Enter kiezen · Dubbelklik selecteert',
|
||||
style: TextStyle(color: Color(0xFF484F58), fontSize: 11),
|
||||
Text(
|
||||
l10n.d('↑↓←→ navigeren · Enter kiezen · Dubbelklik selecteert'),
|
||||
style: const TextStyle(color: Color(0xFF484F58), fontSize: 11),
|
||||
),
|
||||
const Spacer(),
|
||||
// Annuleren
|
||||
|
|
@ -1532,14 +1560,14 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
|||
foregroundColor: const Color(0xFF8B949E),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
),
|
||||
child: const Text('Annuleren'),
|
||||
child: Text(l10n.t('cancel')),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
// Kiezen
|
||||
ElevatedButton.icon(
|
||||
onPressed: _selected != null ? () => _confirm() : null,
|
||||
icon: const Icon(Icons.check_circle_outline, size: 17),
|
||||
label: const Text('Kiezen'),
|
||||
label: Text(l10n.d('Kiezen')),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF238636),
|
||||
foregroundColor: Colors.white,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import '../../models/settings.dart';
|
|||
import '../../models/slide.dart';
|
||||
import '../../services/file_service.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../slides/slide_preview.dart';
|
||||
|
||||
/// Dialog that scans a directory for other Marp presentations, lets the user
|
||||
|
|
@ -74,7 +75,7 @@ class _ImportSlidesDialogState extends State<ImportSlidesDialog> {
|
|||
|
||||
Future<void> _pickDirectory() async {
|
||||
final result = await FilePicker.getDirectoryPath(
|
||||
dialogTitle: 'Map met presentaties kiezen',
|
||||
dialogTitle: context.l10n.d('Map met presentaties kiezen'),
|
||||
initialDirectory: _directory,
|
||||
);
|
||||
if (result != null) {
|
||||
|
|
@ -148,6 +149,7 @@ class _ImportSlidesDialogState extends State<ImportSlidesDialog> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final visible = _visible();
|
||||
final selectedCount = _selectedIds.length;
|
||||
|
||||
|
|
@ -156,11 +158,11 @@ class _ImportSlidesDialogState extends State<ImportSlidesDialog> {
|
|||
children: [
|
||||
const Icon(Icons.library_add_outlined, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Slides importeren'),
|
||||
Text(l10n.d('Slides importeren')),
|
||||
const Spacer(),
|
||||
if (selectedCount > 0)
|
||||
Text(
|
||||
'$selectedCount geselecteerd',
|
||||
'$selectedCount ${l10n.d('geselecteerd')}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.accent,
|
||||
|
|
@ -185,7 +187,7 @@ class _ImportSlidesDialogState extends State<ImportSlidesDialog> {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuleren'),
|
||||
child: Text(l10n.t('cancel')),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: selectedCount == 0
|
||||
|
|
@ -193,7 +195,9 @@ class _ImportSlidesDialogState extends State<ImportSlidesDialog> {
|
|||
: () => Navigator.pop(context, _collectSelected()),
|
||||
icon: const Icon(Icons.download_done, size: 16),
|
||||
label: Text(
|
||||
selectedCount == 0 ? 'Importeren' : 'Importeren ($selectedCount)',
|
||||
selectedCount == 0
|
||||
? l10n.d('Importeren')
|
||||
: '${l10n.d('Importeren')} ($selectedCount)',
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -201,27 +205,30 @@ class _ImportSlidesDialogState extends State<ImportSlidesDialog> {
|
|||
}
|
||||
|
||||
Widget _toolbar() {
|
||||
final l10n = context.l10n;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
prefixIcon: Icon(Icons.search, size: 18),
|
||||
hintText: 'Zoek op presentatie, titel of tekst…',
|
||||
prefixIcon: const Icon(Icons.search, size: 18),
|
||||
hintText: l10n.d('Zoek op presentatie, titel of tekst…'),
|
||||
),
|
||||
onChanged: (v) => setState(() => _query = v),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Tooltip(
|
||||
message: _directory ?? 'Geen map gekozen',
|
||||
message: _directory ?? l10n.d('Geen map gekozen'),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _pickDirectory,
|
||||
icon: const Icon(Icons.folder_open_outlined, size: 16),
|
||||
label: Text(
|
||||
_directory == null ? 'Map kiezen' : p.basename(_directory!),
|
||||
_directory == null
|
||||
? l10n.d('Map kiezen')
|
||||
: p.basename(_directory!),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
|
@ -231,25 +238,26 @@ class _ImportSlidesDialogState extends State<ImportSlidesDialog> {
|
|||
}
|
||||
|
||||
Widget _body(List<(ScannedPresentation, List<Slide>)> visible) {
|
||||
final l10n = context.l10n;
|
||||
if (_loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (_directory == null) {
|
||||
return _empty(
|
||||
Icons.folder_off_outlined,
|
||||
'Kies een map met presentaties om te beginnen.',
|
||||
l10n.d('Kies een map met presentaties om te beginnen.'),
|
||||
);
|
||||
}
|
||||
if (_presentations.isEmpty) {
|
||||
return _empty(
|
||||
Icons.search_off_outlined,
|
||||
'Geen andere presentaties (.md) in deze map gevonden.',
|
||||
l10n.d('Geen andere presentaties (.md) in deze map gevonden.'),
|
||||
);
|
||||
}
|
||||
if (visible.isEmpty) {
|
||||
return _empty(
|
||||
Icons.search_off_outlined,
|
||||
'Geen slides gevonden voor "$_query".',
|
||||
'${l10n.d('Geen slides gevonden voor')} "$_query".',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -315,6 +323,7 @@ class _PresentationSection extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final allSelected =
|
||||
slides.isNotEmpty && slides.every((s) => selectedIds.contains(s.id));
|
||||
final deck = presentation.deck;
|
||||
|
|
@ -365,7 +374,9 @@ class _PresentationSection extends StatelessWidget {
|
|||
textStyle: const TextStyle(fontSize: 11),
|
||||
),
|
||||
child: Text(
|
||||
allSelected ? 'Deselecteer alles' : 'Selecteer alles',
|
||||
allSelected
|
||||
? l10n.d('Deselecteer alles')
|
||||
: l10n.d('Selecteer alles'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
class NewDeckDialog extends StatefulWidget {
|
||||
const NewDeckDialog({super.key});
|
||||
|
|
@ -28,13 +29,14 @@ class _NewDeckDialogState extends State<NewDeckDialog> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return CallbackShortcuts(
|
||||
bindings: {
|
||||
const SingleActivator(LogicalKeyboardKey.escape): () =>
|
||||
Navigator.pop(context),
|
||||
},
|
||||
child: AlertDialog(
|
||||
title: const Text('Nieuwe presentatie'),
|
||||
title: Text(l10n.d('Nieuwe presentatie')),
|
||||
content: Form(
|
||||
key: _formKey,
|
||||
child: SizedBox(
|
||||
|
|
@ -42,12 +44,13 @@ class _NewDeckDialogState extends State<NewDeckDialog> {
|
|||
child: TextFormField(
|
||||
controller: _ctrl,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Titel',
|
||||
hintText: 'Bijv. Kwartaalupdate Q4',
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.d('Titel'),
|
||||
hintText: l10n.d('Bijv. Kwartaalupdate Q4'),
|
||||
),
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? 'Vul een titel in' : null,
|
||||
validator: (v) => (v == null || v.trim().isEmpty)
|
||||
? l10n.d('Vul een titel in')
|
||||
: null,
|
||||
onFieldSubmitted: (_) => _submit(),
|
||||
),
|
||||
),
|
||||
|
|
@ -55,9 +58,9 @@ class _NewDeckDialogState extends State<NewDeckDialog> {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuleren'),
|
||||
child: Text(l10n.t('cancel')),
|
||||
),
|
||||
ElevatedButton(onPressed: _submit, child: const Text('Aanmaken')),
|
||||
ElevatedButton(onPressed: _submit, child: Text(l10n.d('Aanmaken'))),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:path/path.dart' as p;
|
|||
import '../../models/slide.dart';
|
||||
import '../../services/file_service.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
/// What the open dialog returns: a presentation path and, optionally, the
|
||||
/// index of a slide to jump to (when the user picked a search hit).
|
||||
|
|
@ -71,7 +72,7 @@ class _OpenPresentationDialogState extends State<OpenPresentationDialog> {
|
|||
|
||||
Future<void> _pickDirectory() async {
|
||||
final result = await FilePicker.getDirectoryPath(
|
||||
dialogTitle: 'Map met presentaties kiezen',
|
||||
dialogTitle: context.l10n.d('Map met presentaties kiezen'),
|
||||
initialDirectory: _directory,
|
||||
);
|
||||
if (result != null) {
|
||||
|
|
@ -158,14 +159,15 @@ class _OpenPresentationDialogState extends State<OpenPresentationDialog> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final visible = _visible();
|
||||
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: const [
|
||||
Icon(Icons.folder_open_outlined, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('Presentatie openen'),
|
||||
children: [
|
||||
const Icon(Icons.folder_open_outlined, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(l10n.d('Presentatie openen')),
|
||||
],
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 12, 24, 0),
|
||||
|
|
@ -185,11 +187,11 @@ class _OpenPresentationDialogState extends State<OpenPresentationDialog> {
|
|||
OutlinedButton.icon(
|
||||
onPressed: _browse,
|
||||
icon: const Icon(Icons.insert_drive_file_outlined, size: 16),
|
||||
label: const Text('Bladeren…'),
|
||||
label: Text(l10n.d('Bladeren…')),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuleren'),
|
||||
child: Text(l10n.t('cancel')),
|
||||
),
|
||||
],
|
||||
// Knoppen uit elkaar: Bladeren links, Annuleren rechts. (Geen Spacer in
|
||||
|
|
@ -199,27 +201,32 @@ class _OpenPresentationDialogState extends State<OpenPresentationDialog> {
|
|||
}
|
||||
|
||||
Widget _toolbar() {
|
||||
final l10n = context.l10n;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
prefixIcon: Icon(Icons.search, size: 18),
|
||||
hintText: 'Zoek op bestandsnaam, titel of tekst in de slides…',
|
||||
prefixIcon: const Icon(Icons.search, size: 18),
|
||||
hintText: l10n.d(
|
||||
'Zoek op bestandsnaam, titel of tekst in de slides…',
|
||||
),
|
||||
),
|
||||
onChanged: (v) => setState(() => _query = v),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Tooltip(
|
||||
message: _directory ?? 'Geen map gekozen',
|
||||
message: _directory ?? l10n.d('Geen map gekozen'),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _pickDirectory,
|
||||
icon: const Icon(Icons.folder_outlined, size: 16),
|
||||
label: Text(
|
||||
_directory == null ? 'Map kiezen' : p.basename(_directory!),
|
||||
_directory == null
|
||||
? l10n.d('Map kiezen')
|
||||
: p.basename(_directory!),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
|
@ -229,25 +236,26 @@ class _OpenPresentationDialogState extends State<OpenPresentationDialog> {
|
|||
}
|
||||
|
||||
Widget _body(List<(ScannedPresentation, List<_SlideHit>)> visible) {
|
||||
final l10n = context.l10n;
|
||||
if (_loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (_directory == null) {
|
||||
return _empty(
|
||||
Icons.folder_off_outlined,
|
||||
'Kies een map met presentaties om te beginnen.',
|
||||
l10n.d('Kies een map met presentaties om te beginnen.'),
|
||||
);
|
||||
}
|
||||
if (_presentations.isEmpty) {
|
||||
return _empty(
|
||||
Icons.search_off_outlined,
|
||||
'Geen presentaties (.md) in deze map gevonden.',
|
||||
l10n.d('Geen presentaties (.md) in deze map gevonden.'),
|
||||
);
|
||||
}
|
||||
if (visible.isEmpty) {
|
||||
return _empty(
|
||||
Icons.search_off_outlined,
|
||||
'Geen presentaties gevonden voor "$_query".',
|
||||
'${l10n.d('Geen presentaties gevonden voor')} "$_query".',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -308,6 +316,7 @@ class _PresentationRow extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final deck = presentation.deck;
|
||||
final title = deck.title.isEmpty ? presentation.fileName : deck.title;
|
||||
|
||||
|
|
@ -343,7 +352,7 @@ class _PresentationRow extends StatelessWidget {
|
|||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
'${presentation.fileName} · ${deck.slides.length} slides',
|
||||
'${presentation.fileName} · ${deck.slides.length} ${l10n.t('slides')}',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF94A3B8),
|
||||
|
|
@ -378,7 +387,7 @@ class _PresentationRow extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Slide ${hit.index + 1}',
|
||||
'${l10n.d('Slide')} ${hit.index + 1}',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
|
@ -405,7 +414,7 @@ class _PresentationRow extends StatelessWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, top: 2),
|
||||
child: Text(
|
||||
'+ ${hits.length - 4} meer treffer(s)',
|
||||
'+ ${hits.length - 4} ${l10n.d('meer treffer(s)')}',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF94A3B8),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../models/deck.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
/// The editable general metadata of a presentation.
|
||||
class PresentationInfo {
|
||||
|
|
@ -86,6 +87,7 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return CallbackShortcuts(
|
||||
bindings: {
|
||||
const SingleActivator(LogicalKeyboardKey.escape): () =>
|
||||
|
|
@ -93,10 +95,10 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
|||
},
|
||||
child: AlertDialog(
|
||||
title: Row(
|
||||
children: const [
|
||||
Icon(Icons.info_outline, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('Presentatie-eigenschappen'),
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(l10n.d('Presentatie-eigenschappen')),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
|
|
@ -160,10 +162,14 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
|||
'Komma-gescheiden, bijv. kwartaal, cijfers, 2026',
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Deze gegevens worden in de markdown opgeslagen en zijn '
|
||||
'doorzoekbaar bij het openen.',
|
||||
style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
Text(
|
||||
l10n.d(
|
||||
'Deze gegevens worden in de markdown opgeslagen en zijn doorzoekbaar bij het openen.',
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF94A3B8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -172,9 +178,9 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuleren'),
|
||||
child: Text(l10n.t('cancel')),
|
||||
),
|
||||
ElevatedButton(onPressed: _save, child: const Text('Opslaan')),
|
||||
ElevatedButton(onPressed: _save, child: Text(l10n.t('save'))),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -186,12 +192,13 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
|||
String hint, {
|
||||
int maxLines = 1,
|
||||
}) {
|
||||
final l10n = context.l10n;
|
||||
return TextField(
|
||||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
labelText: l10n.d(label),
|
||||
hintText: l10n.d(hint),
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import '../../models/settings.dart';
|
|||
import '../../state/settings_provider.dart';
|
||||
import '../../state/tabs_provider.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
TextStyle _fontStyle(String font, TextStyle base) {
|
||||
return base.copyWith(fontFamily: font);
|
||||
|
|
@ -93,7 +94,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
|
||||
Future<void> _pickHomeDirectory() async {
|
||||
final result = await FilePicker.getDirectoryPath(
|
||||
dialogTitle: 'Standaard map voor presentaties',
|
||||
dialogTitle: context.l10n.d('Standaard map voor presentaties'),
|
||||
initialDirectory: _homeDirectory,
|
||||
);
|
||||
if (result != null) setState(() => _homeDirectory = result);
|
||||
|
|
@ -101,7 +102,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
|
||||
Future<void> _pickExportDirectory() async {
|
||||
final result = await FilePicker.getDirectoryPath(
|
||||
dialogTitle: 'Map voor exports',
|
||||
dialogTitle: context.l10n.d('Map voor exports'),
|
||||
initialDirectory: _exportDirectory ?? _homeDirectory,
|
||||
);
|
||||
if (result != null) setState(() => _exportDirectory = result);
|
||||
|
|
@ -109,7 +110,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
|
||||
Future<void> _pickLogo() async {
|
||||
final result = await FilePicker.pickFiles(
|
||||
dialogTitle: 'Logo kiezen',
|
||||
dialogTitle: context.l10n.d('Logo kiezen'),
|
||||
type: FileType.image,
|
||||
);
|
||||
final path = result?.files.single.path;
|
||||
|
|
@ -158,6 +159,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final profiles = _profiles;
|
||||
final dropdownValue = profiles.any((p) => p.name == _originalName)
|
||||
? _originalName
|
||||
|
|
@ -166,7 +168,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: AlertDialog(
|
||||
title: const Text('Instellingen'),
|
||||
title: Text(l10n.t('settings')),
|
||||
content: SizedBox(
|
||||
width: 520,
|
||||
height: 560,
|
||||
|
|
@ -177,11 +179,20 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
const SizedBox(height: 12),
|
||||
_profileNameField(),
|
||||
const SizedBox(height: 12),
|
||||
const TabBar(
|
||||
TabBar(
|
||||
tabs: [
|
||||
Tab(icon: Icon(Icons.tune), text: 'Algemeen'),
|
||||
Tab(icon: Icon(Icons.palette_outlined), text: 'Kleuren'),
|
||||
Tab(icon: Icon(Icons.image_outlined), text: 'Logo'),
|
||||
Tab(
|
||||
icon: const Icon(Icons.tune),
|
||||
text: l10n.t('settingsGeneral'),
|
||||
),
|
||||
Tab(
|
||||
icon: const Icon(Icons.palette_outlined),
|
||||
text: l10n.t('settingsColors'),
|
||||
),
|
||||
Tab(
|
||||
icon: const Icon(Icons.image_outlined),
|
||||
text: l10n.t('settingsLogo'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
|
@ -200,23 +211,24 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuleren'),
|
||||
child: Text(l10n.t('cancel')),
|
||||
),
|
||||
ElevatedButton(onPressed: _save, child: const Text('Opslaan')),
|
||||
ElevatedButton(onPressed: _save, child: Text(l10n.t('saveSettings'))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _profileNameField() {
|
||||
final l10n = context.l10n;
|
||||
return TextField(
|
||||
controller: _profileName,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Profielnaam',
|
||||
hintText: 'Naam van het stijlprofiel',
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.d('Profielnaam'),
|
||||
hintText: l10n.d('Naam van het stijlprofiel'),
|
||||
isDense: true,
|
||||
prefixIcon: Icon(Icons.badge_outlined, size: 18),
|
||||
prefixIcon: const Icon(Icons.badge_outlined, size: 18),
|
||||
),
|
||||
onChanged: (value) {
|
||||
final name = value.trim();
|
||||
|
|
@ -229,12 +241,13 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
}
|
||||
|
||||
Widget _profileSelector(List<ThemeProfile> profiles, String dropdownValue) {
|
||||
final l10n = context.l10n;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Stijlprofiel',
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.d('Stijlprofiel'),
|
||||
isDense: true,
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
|
|
@ -258,17 +271,17 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
tooltip: 'Nieuw profiel',
|
||||
tooltip: l10n.d('Nieuw profiel'),
|
||||
onPressed: _createProfile,
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Standaardprofiel laden',
|
||||
tooltip: l10n.d('Standaardprofiel laden'),
|
||||
onPressed: _loadDefaultProfile,
|
||||
icon: const Icon(Icons.restart_alt, size: 18),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Profiel verwijderen',
|
||||
tooltip: l10n.d('Profiel verwijderen'),
|
||||
onPressed: profiles.length <= 1
|
||||
? null
|
||||
: () {
|
||||
|
|
@ -328,15 +341,50 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
}
|
||||
|
||||
Widget _generalTab() {
|
||||
final l10n = context.l10n;
|
||||
final languageCode = ref.watch(
|
||||
settingsProvider.select((s) => s.languageCode),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_sectionTitle('Presentatiemap'),
|
||||
_sectionTitle(l10n.t('language')),
|
||||
InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.t('applicationLanguage'),
|
||||
isDense: true,
|
||||
prefixIcon: const Icon(Icons.language, size: 18),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: languageCode,
|
||||
isExpanded: true,
|
||||
isDense: true,
|
||||
items: [
|
||||
for (final entry in AppLocalizations.languageNames.entries)
|
||||
DropdownMenuItem(value: entry.key, child: Text(entry.value)),
|
||||
],
|
||||
onChanged: (code) {
|
||||
if (code == null) return;
|
||||
ref.read(settingsProvider.notifier).setLanguageCode(code);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(
|
||||
l10n.t('languageHelp'),
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_sectionTitle(l10n.t('presentationFolder')),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _pathBox(
|
||||
_homeDirectory ?? 'Niet ingesteld',
|
||||
_homeDirectory ?? l10n.t('notSet'),
|
||||
muted: _homeDirectory == null,
|
||||
),
|
||||
),
|
||||
|
|
@ -344,23 +392,23 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
ElevatedButton.icon(
|
||||
onPressed: _pickHomeDirectory,
|
||||
icon: const Icon(Icons.folder_open, size: 16),
|
||||
label: const Text('Kiezen'),
|
||||
label: Text(l10n.t('choose')),
|
||||
),
|
||||
if (_homeDirectory != null)
|
||||
IconButton(
|
||||
onPressed: () => setState(() => _homeDirectory = null),
|
||||
icon: const Icon(Icons.clear, size: 18),
|
||||
tooltip: 'Verwijder standaard map',
|
||||
tooltip: l10n.t('removeDefaultFolder'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_sectionTitle('Exportmap'),
|
||||
_sectionTitle(l10n.t('exportFolderSetting')),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _pathBox(
|
||||
_exportDirectory ?? 'Naast het presentatiebestand',
|
||||
_exportDirectory ?? l10n.t('nextToPresentationFile'),
|
||||
muted: _exportDirectory == null,
|
||||
),
|
||||
),
|
||||
|
|
@ -368,22 +416,21 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
ElevatedButton.icon(
|
||||
onPressed: _pickExportDirectory,
|
||||
icon: const Icon(Icons.folder_open, size: 16),
|
||||
label: const Text('Kiezen'),
|
||||
label: Text(l10n.t('choose')),
|
||||
),
|
||||
if (_exportDirectory != null)
|
||||
IconButton(
|
||||
onPressed: () => setState(() => _exportDirectory = null),
|
||||
icon: const Icon(Icons.clear, size: 18),
|
||||
tooltip: 'Verwijder exportmap',
|
||||
tooltip: l10n.t('removeExportFolder'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 6),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(
|
||||
'Alle exports (PDF/PPTX) worden hier opgeslagen. Niet ingesteld? '
|
||||
'Dan komt de export naast het presentatiebestand te staan.',
|
||||
style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
l10n.t('exportFolderHelp'),
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -446,60 +493,61 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
}
|
||||
|
||||
Widget _colorsTab() {
|
||||
final l10n = context.l10n;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_sectionTitle('Lettertype'),
|
||||
_sectionTitle(l10n.d('Lettertype')),
|
||||
_fontSection(),
|
||||
const SizedBox(height: 20),
|
||||
_sectionTitle('Kleuren'),
|
||||
_sectionTitle(l10n.d('Kleuren')),
|
||||
_colorSetting(
|
||||
'Achtergrond slides',
|
||||
l10n.d('Achtergrond slides'),
|
||||
_themeProfile.slideBackgroundColor,
|
||||
(v) =>
|
||||
_themeProfile = _themeProfile.copyWith(slideBackgroundColor: v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_colorSetting(
|
||||
'Tekst',
|
||||
l10n.d('Tekst'),
|
||||
_themeProfile.textColor,
|
||||
(v) => _themeProfile = _themeProfile.copyWith(textColor: v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_colorSetting(
|
||||
'Accent / bullets',
|
||||
l10n.d('Accent / bullets'),
|
||||
_themeProfile.accentColor,
|
||||
(v) => _themeProfile = _themeProfile.copyWith(accentColor: v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_colorSetting(
|
||||
'Tabeltekst',
|
||||
l10n.d('Tabeltekst'),
|
||||
_themeProfile.tableTextColor,
|
||||
(v) => _themeProfile = _themeProfile.copyWith(tableTextColor: v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_colorSetting(
|
||||
'Tabel koptekst',
|
||||
l10n.d('Tabel koptekst'),
|
||||
_themeProfile.tableHeaderTextColor,
|
||||
(v) =>
|
||||
_themeProfile = _themeProfile.copyWith(tableHeaderTextColor: v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_colorSetting(
|
||||
'Titelachtergrond',
|
||||
l10n.d('Titelachtergrond'),
|
||||
_themeProfile.titleBackgroundColor,
|
||||
(v) =>
|
||||
_themeProfile = _themeProfile.copyWith(titleBackgroundColor: v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_colorSetting(
|
||||
'Titeltekst',
|
||||
l10n.d('Titeltekst'),
|
||||
_themeProfile.titleTextColor,
|
||||
(v) => _themeProfile = _themeProfile.copyWith(titleTextColor: v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_colorSetting(
|
||||
'Sectieachtergrond',
|
||||
l10n.d('Sectieachtergrond'),
|
||||
_themeProfile.sectionBackgroundColor,
|
||||
(v) =>
|
||||
_themeProfile = _themeProfile.copyWith(sectionBackgroundColor: v),
|
||||
|
|
@ -511,15 +559,16 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
}
|
||||
|
||||
Widget _logoTab() {
|
||||
final l10n = context.l10n;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_sectionTitle('Logo'),
|
||||
_sectionTitle(l10n.d('Logo')),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _pathBox(
|
||||
_themeProfile.logoPath ?? 'Geen logo ingesteld',
|
||||
_themeProfile.logoPath ?? l10n.d('Geen logo ingesteld'),
|
||||
muted: _themeProfile.logoPath == null,
|
||||
),
|
||||
),
|
||||
|
|
@ -527,7 +576,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
ElevatedButton.icon(
|
||||
onPressed: _pickLogo,
|
||||
icon: const Icon(Icons.image_outlined, size: 16),
|
||||
label: const Text('Kiezen'),
|
||||
label: Text(l10n.d('Kiezen')),
|
||||
),
|
||||
if (_themeProfile.logoPath != null)
|
||||
IconButton(
|
||||
|
|
@ -536,22 +585,34 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
_profileTouched = true;
|
||||
}),
|
||||
icon: const Icon(Icons.clear, size: 18),
|
||||
tooltip: 'Verwijder logo',
|
||||
tooltip: l10n.d('Verwijder logo'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: _themeProfile.logoPosition,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Logo positie',
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.d('Logo positie'),
|
||||
isDense: true,
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'top-left', child: Text('Linksboven')),
|
||||
DropdownMenuItem(value: 'top-right', child: Text('Rechtsboven')),
|
||||
DropdownMenuItem(value: 'bottom-left', child: Text('Linksonder')),
|
||||
DropdownMenuItem(value: 'bottom-right', child: Text('Rechtsonder')),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'top-left',
|
||||
child: Text(l10n.d('Linksboven')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'top-right',
|
||||
child: Text(l10n.d('Rechtsboven')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'bottom-left',
|
||||
child: Text(l10n.d('Linksonder')),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'bottom-right',
|
||||
child: Text(l10n.d('Rechtsonder')),
|
||||
),
|
||||
],
|
||||
onChanged: (v) {
|
||||
if (v != null) {
|
||||
|
|
@ -567,10 +628,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
width: 160,
|
||||
child: TextField(
|
||||
controller: _logoSize,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Logo px',
|
||||
isDense: true,
|
||||
),
|
||||
decoration: InputDecoration(labelText: 'Logo px', isDense: true),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
onChanged: (_) => _profileTouched = true,
|
||||
|
|
@ -580,30 +638,31 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
_sectionTitle('Footer'),
|
||||
TextField(
|
||||
controller: _footerText,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Footertekst',
|
||||
hintText: 'bijv. Vertrouwelijk · {title} · {date}',
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.d('Footertekst'),
|
||||
hintText: l10n.d('bijv. Vertrouwelijk · {title} · {date}'),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: (_) => _profileTouched = true,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'Tokens: {page}, {total}, {date}, {title}. Footer verschijnt op alle '
|
||||
'slides behalve titel- en sectieslides, tenzij je hem per slide uitzet.',
|
||||
style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
Text(
|
||||
l10n.d(
|
||||
'Tokens: {page}, {total}, {date}, {title}. Footer verschijnt op alle slides behalve titel- en sectieslides, tenzij je hem per slide uitzet.',
|
||||
),
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: _themeProfile.footerPosition,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Footerpositie',
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.d('Footerpositie'),
|
||||
isDense: true,
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'left', child: Text('Links')),
|
||||
DropdownMenuItem(value: 'center', child: Text('Midden')),
|
||||
DropdownMenuItem(value: 'right', child: Text('Rechts')),
|
||||
items: [
|
||||
DropdownMenuItem(value: 'left', child: Text(l10n.d('Links'))),
|
||||
DropdownMenuItem(value: 'center', child: Text(l10n.d('Midden'))),
|
||||
DropdownMenuItem(value: 'right', child: Text(l10n.d('Rechts'))),
|
||||
],
|
||||
onChanged: (v) {
|
||||
if (v != null) {
|
||||
|
|
@ -623,9 +682,9 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
);
|
||||
_profileTouched = true;
|
||||
}),
|
||||
title: const Text(
|
||||
'Paginanummers tonen (rechtsonder)',
|
||||
style: TextStyle(fontSize: 13),
|
||||
title: Text(
|
||||
l10n.d('Paginanummers tonen (rechtsonder)'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
|
|
@ -688,6 +747,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
}
|
||||
|
||||
Widget _stylePreview() {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
|
|
@ -699,7 +759,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Voorvertoning',
|
||||
l10n.d('Voorvertoning'),
|
||||
style: _fontStyle(
|
||||
_themeProfile.fontFamily,
|
||||
TextStyle(
|
||||
|
|
@ -711,7 +771,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'De snelle bruine vos springt over de luie hond.',
|
||||
l10n.d('De snelle bruine vos springt over de luie hond.'),
|
||||
style: _fontStyle(
|
||||
_themeProfile.fontFamily,
|
||||
TextStyle(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:path/path.dart' as p;
|
|||
import '../../models/slide.dart';
|
||||
import '../../services/file_service.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../slides/slide_preview.dart';
|
||||
|
||||
/// A single search hit: one slide from a scanned presentation.
|
||||
|
|
@ -92,7 +93,7 @@ class _SlideFinderDialogState extends State<SlideFinderDialog> {
|
|||
|
||||
Future<void> _pickDirectory() async {
|
||||
final result = await FilePicker.getDirectoryPath(
|
||||
dialogTitle: 'Map met presentaties kiezen',
|
||||
dialogTitle: context.l10n.d('Map met presentaties kiezen'),
|
||||
initialDirectory: _directory,
|
||||
);
|
||||
if (result != null) {
|
||||
|
|
@ -160,6 +161,7 @@ class _SlideFinderDialogState extends State<SlideFinderDialog> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final hits = _hits();
|
||||
|
||||
return AlertDialog(
|
||||
|
|
@ -167,11 +169,11 @@ class _SlideFinderDialogState extends State<SlideFinderDialog> {
|
|||
children: [
|
||||
const Icon(Icons.travel_explore_outlined, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Slide zoeken'),
|
||||
Text(l10n.d('Slide zoeken')),
|
||||
const Spacer(),
|
||||
if (_addedCount > 0)
|
||||
Text(
|
||||
'$_addedCount toegevoegd',
|
||||
'$_addedCount ${l10n.d('toegevoegd')}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.accent,
|
||||
|
|
@ -196,34 +198,39 @@ class _SlideFinderDialogState extends State<SlideFinderDialog> {
|
|||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Klaar'),
|
||||
child: Text(l10n.d('Klaar')),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _toolbar() {
|
||||
final l10n = context.l10n;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
prefixIcon: Icon(Icons.search, size: 18),
|
||||
hintText: 'Zoek slides op tekst, titel, onderschrift, pad…',
|
||||
prefixIcon: const Icon(Icons.search, size: 18),
|
||||
hintText: l10n.d(
|
||||
'Zoek slides op tekst, titel, onderschrift, pad…',
|
||||
),
|
||||
),
|
||||
onChanged: (v) => setState(() => _query = v),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Tooltip(
|
||||
message: _directory ?? 'Geen map gekozen',
|
||||
message: _directory ?? l10n.d('Geen map gekozen'),
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _pickDirectory,
|
||||
icon: const Icon(Icons.folder_open_outlined, size: 16),
|
||||
label: Text(
|
||||
_directory == null ? 'Map kiezen' : p.basename(_directory!),
|
||||
_directory == null
|
||||
? l10n.d('Map kiezen')
|
||||
: p.basename(_directory!),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
|
@ -233,25 +240,26 @@ class _SlideFinderDialogState extends State<SlideFinderDialog> {
|
|||
}
|
||||
|
||||
Widget _body(List<_Hit> hits) {
|
||||
final l10n = context.l10n;
|
||||
if (_loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (_directory == null) {
|
||||
return _empty(
|
||||
Icons.folder_off_outlined,
|
||||
'Kies een map met presentaties om te beginnen.',
|
||||
l10n.d('Kies een map met presentaties om te beginnen.'),
|
||||
);
|
||||
}
|
||||
if (_query.trim().isEmpty) {
|
||||
return _empty(
|
||||
Icons.travel_explore_outlined,
|
||||
'Typ zoektermen om slides uit al je presentaties te vinden.',
|
||||
l10n.d('Typ zoektermen om slides uit al je presentaties te vinden.'),
|
||||
);
|
||||
}
|
||||
if (hits.isEmpty) {
|
||||
return _empty(
|
||||
Icons.search_off_outlined,
|
||||
'Geen slides gevonden voor "${_query.trim()}".',
|
||||
'${l10n.d('Geen slides gevonden voor')} "${_query.trim()}".',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -262,8 +270,8 @@ class _SlideFinderDialogState extends State<SlideFinderDialog> {
|
|||
padding: const EdgeInsets.only(bottom: 8, left: 2),
|
||||
child: Text(
|
||||
hits.length >= _maxResults
|
||||
? 'Eerste $_maxResults treffers — verfijn je zoekopdracht'
|
||||
: '${hits.length} treffer(s)',
|
||||
? '${l10n.d('Eerste')} $_maxResults ${l10n.d('treffers — verfijn je zoekopdracht')}'
|
||||
: '${hits.length} ${l10n.d('treffer(s)')}',
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
),
|
||||
|
|
@ -320,6 +328,7 @@ class _SlideHitCard extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final deck = hit.source.deck;
|
||||
final sourceName = deck.title.isEmpty ? hit.source.fileName : deck.title;
|
||||
|
||||
|
|
@ -351,7 +360,7 @@ class _SlideHitCard extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$sourceName · slide ${hit.slideIndex + 1}',
|
||||
'$sourceName · ${l10n.d('slide')} ${hit.slideIndex + 1}',
|
||||
style: const TextStyle(fontSize: 10.5, color: Color(0xFF94A3B8)),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
|
@ -363,7 +372,7 @@ class _SlideHitCard extends StatelessWidget {
|
|||
? OutlinedButton.icon(
|
||||
onPressed: onAdd,
|
||||
icon: const Icon(Icons.check, size: 14),
|
||||
label: const Text('Toegevoegd'),
|
||||
label: Text(l10n.d('Toegevoegd')),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppTheme.accent,
|
||||
side: const BorderSide(color: AppTheme.accent),
|
||||
|
|
@ -374,7 +383,7 @@ class _SlideHitCard extends StatelessWidget {
|
|||
: ElevatedButton.icon(
|
||||
onPressed: onAdd,
|
||||
icon: const Icon(Icons.add, size: 14),
|
||||
label: const Text('Toevoegen'),
|
||||
label: Text(l10n.d('Toevoegen')),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.accent,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import '../../services/caption_service.dart';
|
|||
import '../../services/description_service.dart';
|
||||
import '../../services/image_service.dart';
|
||||
import '../../state/tabs_provider.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../dialogs/image_carousel_picker.dart';
|
||||
|
||||
/// Shared layout helpers for slide editors.
|
||||
|
|
@ -25,11 +26,12 @@ class EditorField extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
l10n.d(label),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
|
@ -41,7 +43,9 @@ class EditorField extends StatelessWidget {
|
|||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
minLines: 1,
|
||||
decoration: InputDecoration(hintText: hint),
|
||||
decoration: InputDecoration(
|
||||
hintText: hint.isEmpty ? '' : l10n.d(hint),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -87,18 +91,20 @@ class ImageZoomControl extends StatelessWidget {
|
|||
// Effectieve sliderwaarde: 0 behandelen als 100
|
||||
int get _effective => value == 0 ? 100 : value.clamp(minValue, maxValue);
|
||||
|
||||
String get _label {
|
||||
String _label(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final v = _effective;
|
||||
if (maxValue <= 100) return '$v%'; // paneelbreedte-modus
|
||||
if (v == 100) return 'Volledig zichtbaar (100%)';
|
||||
if (v == 100) return l10n.d('Volledig zichtbaar (100%)');
|
||||
if (v > 100) {
|
||||
return 'Ingezoomd $v% — ${((1 / (v / 100)) * 100).round()}% van de foto zichtbaar';
|
||||
return '${l10n.d('Ingezoomd')} $v% — ${((1 / (v / 100)) * 100).round()}% ${l10n.d('van de foto zichtbaar')}';
|
||||
}
|
||||
return 'Uitgezoomd $v%';
|
||||
return '${l10n.d('Uitgezoomd')} $v%';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final zoomed = _effective != 100;
|
||||
|
||||
return Column(
|
||||
|
|
@ -106,9 +112,13 @@ class ImageZoomControl extends StatelessWidget {
|
|||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Tooltip(
|
||||
message: 'Uitzoomen (meer van de foto zichtbaar)',
|
||||
child: Icon(Icons.zoom_out, size: 16, color: Color(0xFF94A3B8)),
|
||||
Tooltip(
|
||||
message: l10n.d('Uitzoomen (meer van de foto zichtbaar)'),
|
||||
child: const Icon(
|
||||
Icons.zoom_out,
|
||||
size: 16,
|
||||
color: Color(0xFF94A3B8),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
|
|
@ -116,7 +126,7 @@ class ImageZoomControl extends StatelessWidget {
|
|||
min: minValue.toDouble(),
|
||||
max: maxValue.toDouble(),
|
||||
divisions: (maxValue - minValue) ~/ step,
|
||||
label: _label,
|
||||
label: _label(context),
|
||||
onChanged: (v) {
|
||||
final snapped = ((v.round() / step).round() * step).clamp(
|
||||
minValue,
|
||||
|
|
@ -126,9 +136,13 @@ class ImageZoomControl extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
),
|
||||
const Tooltip(
|
||||
message: 'Inzoomen (minder van de foto zichtbaar)',
|
||||
child: Icon(Icons.zoom_in, size: 16, color: Color(0xFF94A3B8)),
|
||||
Tooltip(
|
||||
message: l10n.d('Inzoomen (minder van de foto zichtbaar)'),
|
||||
child: const Icon(
|
||||
Icons.zoom_in,
|
||||
size: 16,
|
||||
color: Color(0xFF94A3B8),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
|
|
@ -147,7 +161,7 @@ class ImageZoomControl extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 4),
|
||||
Tooltip(
|
||||
message: 'Terugzetten (volledige afbeelding zichtbaar)',
|
||||
message: l10n.d('Terugzetten (volledige afbeelding zichtbaar)'),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.refresh, size: 16),
|
||||
onPressed: zoomed ? () => onChanged(100) : null,
|
||||
|
|
@ -161,7 +175,7 @@ class ImageZoomControl extends StatelessWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8, bottom: 4),
|
||||
child: Text(
|
||||
_label,
|
||||
_label(context),
|
||||
style: const TextStyle(fontSize: 10, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
),
|
||||
|
|
@ -250,6 +264,7 @@ class ImagePickerBar extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = context.l10n;
|
||||
final captions = ref.read(captionServiceProvider);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -263,7 +278,7 @@ class ImagePickerBar extends ConsumerWidget {
|
|||
color: Colors.white,
|
||||
),
|
||||
child: Text(
|
||||
imagePath.isEmpty ? label : imagePath,
|
||||
imagePath.isEmpty ? l10n.d(label) : imagePath,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: imagePath.isEmpty
|
||||
|
|
@ -283,17 +298,17 @@ class ImagePickerBar extends ConsumerWidget {
|
|||
ElevatedButton.icon(
|
||||
onPressed: () => _openCarousel(context, ref, captions),
|
||||
icon: const Icon(Icons.photo_library_outlined, size: 16),
|
||||
label: const Text('Uit bibliotheek…'),
|
||||
label: Text(l10n.d('Uit bibliotheek…')),
|
||||
),
|
||||
if (onBrowse != null)
|
||||
OutlinedButton.icon(
|
||||
onPressed: onBrowse,
|
||||
icon: const Icon(Icons.folder_open_outlined, size: 16),
|
||||
label: const Text('Van computer…'),
|
||||
label: Text(l10n.d('Van computer…')),
|
||||
),
|
||||
if (onPaste != null)
|
||||
Tooltip(
|
||||
message: 'Afbeelding plakken uit klembord',
|
||||
message: l10n.d('Afbeelding plakken uit klembord'),
|
||||
child: IconButton(
|
||||
onPressed: onPaste,
|
||||
icon: const Icon(Icons.content_paste, size: 18),
|
||||
|
|
@ -302,7 +317,7 @@ class ImagePickerBar extends ConsumerWidget {
|
|||
),
|
||||
if (imagePath.isNotEmpty)
|
||||
Tooltip(
|
||||
message: 'Kopieer afbeelding naar klembord',
|
||||
message: l10n.d('Kopieer afbeelding naar klembord'),
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
final ok = await ImageService().copyImageToClipboard(
|
||||
|
|
@ -313,8 +328,8 @@ class ImagePickerBar extends ConsumerWidget {
|
|||
SnackBar(
|
||||
content: Text(
|
||||
ok
|
||||
? 'Afbeelding gekopieerd naar klembord.'
|
||||
: 'Kopiëren naar klembord mislukt.',
|
||||
? l10n.d('Afbeelding gekopieerd naar klembord.')
|
||||
: l10n.d('Kopiëren naar klembord mislukt.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -326,7 +341,7 @@ class ImagePickerBar extends ConsumerWidget {
|
|||
),
|
||||
if (onClear != null && imagePath.isNotEmpty)
|
||||
Tooltip(
|
||||
message: 'Verwijder afbeelding',
|
||||
message: l10n.d('Verwijder afbeelding'),
|
||||
child: IconButton(
|
||||
onPressed: onClear,
|
||||
icon: const Icon(Icons.clear, size: 18),
|
||||
|
|
@ -423,10 +438,11 @@ class _CaptionFieldState extends State<_CaptionField> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return TextField(
|
||||
controller: _ctrl,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Caption / bronvermelding (bijv. © Naam Fotograaf)',
|
||||
hintText: l10n.d('Caption / bronvermelding (bijv. © Naam Fotograaf)'),
|
||||
hintStyle: const TextStyle(fontSize: 12, color: Color(0xFFB0BEC5)),
|
||||
prefixIcon: const Icon(
|
||||
Icons.copyright_outlined,
|
||||
|
|
@ -457,10 +473,11 @@ class SectionLabel extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Text(
|
||||
text,
|
||||
l10n.d(text),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../models/slide.dart';
|
||||
import '../../services/image_service.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '_editor_field.dart';
|
||||
|
||||
class AudioAttachmentEditor extends StatelessWidget {
|
||||
|
|
@ -22,6 +23,7 @@ class AudioAttachmentEditor extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -41,7 +43,7 @@ class AudioAttachmentEditor extends StatelessWidget {
|
|||
),
|
||||
child: Text(
|
||||
slide.audioPath.isEmpty
|
||||
? 'Geen audiobestand gekozen'
|
||||
? l10n.d('Geen audiobestand gekozen')
|
||||
: slide.audioPath,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
|
|
@ -57,7 +59,7 @@ class AudioAttachmentEditor extends StatelessWidget {
|
|||
ElevatedButton.icon(
|
||||
onPressed: _pickAudio,
|
||||
icon: const Icon(Icons.audio_file_outlined, size: 16),
|
||||
label: const Text('Kiezen'),
|
||||
label: Text(l10n.d('Kiezen')),
|
||||
),
|
||||
if (slide.audioPath.isNotEmpty)
|
||||
IconButton(
|
||||
|
|
@ -65,7 +67,7 @@ class AudioAttachmentEditor extends StatelessWidget {
|
|||
slide.copyWith(audioPath: '', audioAutoplay: false),
|
||||
),
|
||||
icon: const Icon(Icons.clear, size: 18),
|
||||
tooltip: 'Audio verwijderen',
|
||||
tooltip: l10n.d('Audio verwijderen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -77,7 +79,7 @@ class AudioAttachmentEditor extends StatelessWidget {
|
|||
? null
|
||||
: (value) =>
|
||||
onUpdate(slide.copyWith(audioAutoplay: value ?? false)),
|
||||
title: const Text('Audio automatisch afspelen'),
|
||||
title: Text(l10n.d('Audio automatisch afspelen')),
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../models/slide.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '_editor_field.dart';
|
||||
|
||||
class BulletsEditor extends StatefulWidget {
|
||||
|
|
@ -161,6 +162,7 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
|
|
@ -182,7 +184,7 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
child: TextButton.icon(
|
||||
onPressed: () => _addBulletAfter(_bullets.length - 1),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Bullet toevoegen'),
|
||||
label: Text(l10n.d('Bullet toevoegen')),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -190,6 +192,7 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
}
|
||||
|
||||
Widget _buildBulletRow(int i) {
|
||||
final l10n = context.l10n;
|
||||
final level = _levels[i];
|
||||
return Padding(
|
||||
key: ValueKey(_bullets[i]),
|
||||
|
|
@ -250,7 +253,7 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
controller: _bullets[i],
|
||||
focusNode: _focusNodes[i],
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Bullet ${i + 1}',
|
||||
hintText: '${l10n.d('Bullet')} ${i + 1}',
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
|
|
@ -265,7 +268,7 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
onPressed: _bullets.length > 1
|
||||
? () => _removeBulletAndFocus(i)
|
||||
: null,
|
||||
tooltip: 'Verwijder',
|
||||
tooltip: l10n.d('Verwijder'),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
constraints: const BoxConstraints(minWidth: 28),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import '../../models/slide.dart';
|
||||
import '../../services/image_service.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '_editor_field.dart';
|
||||
|
||||
class BulletsImageEditor extends StatefulWidget {
|
||||
|
|
@ -175,6 +176,7 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final imagePath = widget.slide.imagePath;
|
||||
|
||||
return ListView(
|
||||
|
|
@ -197,7 +199,7 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
child: TextButton.icon(
|
||||
onPressed: () => _addBulletAfter(_bullets.length - 1),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Bullet toevoegen'),
|
||||
label: Text(l10n.d('Bullet toevoegen')),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -235,6 +237,7 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
}
|
||||
|
||||
Widget _buildBulletRow(int i) {
|
||||
final l10n = context.l10n;
|
||||
final level = _levels[i];
|
||||
return Padding(
|
||||
key: ValueKey(_bullets[i]),
|
||||
|
|
@ -291,7 +294,7 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
controller: _bullets[i],
|
||||
focusNode: _focusNodes[i],
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Bullet ${i + 1}',
|
||||
hintText: '${l10n.d('Bullet')} ${i + 1}',
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../models/slide.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
class FreeMarkdownEditor extends StatefulWidget {
|
||||
final Slide slide;
|
||||
|
|
@ -37,14 +38,15 @@ class _FreeMarkdownEditorState extends State<FreeMarkdownEditor> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Markdown inhoud',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
l10n.d('Markdown inhoud'),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF64748B),
|
||||
|
|
@ -58,8 +60,8 @@ class _FreeMarkdownEditorState extends State<FreeMarkdownEditor> {
|
|||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 13),
|
||||
decoration: const InputDecoration(
|
||||
hintText: '# Slide\n\nInhoud hier...',
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.d('# Slide\n\nInhoud hier...'),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../models/slide.dart';
|
||||
import '../../state/deck_provider.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '_editor_field.dart';
|
||||
|
||||
class QuoteEditor extends ConsumerStatefulWidget {
|
||||
|
|
@ -70,6 +71,7 @@ class _QuoteEditorState extends ConsumerState<QuoteEditor> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final imagePath = widget.slide.imagePath;
|
||||
|
||||
return ListView(
|
||||
|
|
@ -93,10 +95,11 @@ class _QuoteEditorState extends ConsumerState<QuoteEditor> {
|
|||
// ── Background image ──────────────────────────────────────────────
|
||||
const SectionLabel('Achtergrondafbeelding (optioneel)'),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'De afbeelding wordt schermvullend als achtergrond getoond '
|
||||
'met verminderde opaciteit zodat de tekst leesbaar blijft.',
|
||||
style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
Text(
|
||||
l10n.d(
|
||||
'De afbeelding wordt schermvullend als achtergrond getoond met verminderde opaciteit zodat de tekst leesbaar blijft.',
|
||||
),
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ImagePickerBar(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../models/slide.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '_editor_field.dart';
|
||||
|
||||
/// Editor for a table slide. Stores cells as a rectangular grid of
|
||||
|
|
@ -120,17 +121,18 @@ class _TableEditorState extends State<TableEditor> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
EditorField(label: 'Titel', controller: _title, hint: 'Slide titel'),
|
||||
const SizedBox(height: 16),
|
||||
const SectionLabel('Tabel'),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 6),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Text(
|
||||
'Tip: druk op Enter binnen een cel voor een nieuwe regel.',
|
||||
style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
l10n.d('Tip: druk op Enter binnen een cel voor een nieuwe regel.'),
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
),
|
||||
_buildColumnControls(),
|
||||
|
|
@ -141,13 +143,13 @@ class _TableEditorState extends State<TableEditor> {
|
|||
TextButton.icon(
|
||||
onPressed: _addRow,
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Rij toevoegen'),
|
||||
label: Text(l10n.d('Rij toevoegen')),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton.icon(
|
||||
onPressed: _addColumn,
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Kolom toevoegen'),
|
||||
label: Text(l10n.d('Kolom toevoegen')),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -156,6 +158,7 @@ class _TableEditorState extends State<TableEditor> {
|
|||
}
|
||||
|
||||
Widget _buildColumnControls() {
|
||||
final l10n = context.l10n;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
|
|
@ -170,7 +173,8 @@ class _TableEditorState extends State<TableEditor> {
|
|||
color: Color(0xFF94A3B8),
|
||||
),
|
||||
onPressed: _colCount > 1 ? () => _removeColumn(c) : null,
|
||||
tooltip: 'Kolom ${c + 1} verwijderen',
|
||||
tooltip:
|
||||
'${l10n.d('Kolom')} ${c + 1} ${l10n.d('verwijderen')}',
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: const BoxConstraints(
|
||||
|
|
@ -187,6 +191,7 @@ class _TableEditorState extends State<TableEditor> {
|
|||
}
|
||||
|
||||
Widget _buildRow(int r) {
|
||||
final l10n = context.l10n;
|
||||
final isHeader = r == 0;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
|
|
@ -214,7 +219,7 @@ class _TableEditorState extends State<TableEditor> {
|
|||
isDense: true,
|
||||
filled: isHeader,
|
||||
fillColor: isHeader ? const Color(0xFFF1F5F9) : null,
|
||||
hintText: isHeader ? 'Kolom ${c + 1}' : null,
|
||||
hintText: isHeader ? '${l10n.d('Kolom')} ${c + 1}' : null,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8,
|
||||
|
|
@ -234,7 +239,9 @@ class _TableEditorState extends State<TableEditor> {
|
|||
color: Color(0xFF94A3B8),
|
||||
),
|
||||
onPressed: _cells.length > 1 ? () => _removeRow(r) : null,
|
||||
tooltip: isHeader ? 'Koprij verwijderen' : 'Rij verwijderen',
|
||||
tooltip: isHeader
|
||||
? l10n.d('Koprij verwijderen')
|
||||
: l10n.d('Rij verwijderen'),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 28),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../models/slide.dart';
|
||||
import '../../state/deck_provider.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '_editor_field.dart';
|
||||
|
||||
class TitleEditor extends ConsumerStatefulWidget {
|
||||
|
|
@ -70,6 +71,7 @@ class _TitleEditorState extends ConsumerState<TitleEditor> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final imagePath = widget.slide.imagePath;
|
||||
|
||||
return ListView(
|
||||
|
|
@ -93,10 +95,11 @@ class _TitleEditorState extends ConsumerState<TitleEditor> {
|
|||
// ── Background image ─────────────────────────────────────────────
|
||||
const SectionLabel('Achtergrondafbeelding (optioneel)'),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'De afbeelding wordt schermvullend als achtergrond getoond '
|
||||
'met verminderde opaciteit zodat de tekst leesbaar blijft.',
|
||||
style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
Text(
|
||||
l10n.d(
|
||||
'De afbeelding wordt schermvullend als achtergrond getoond met verminderde opaciteit zodat de tekst leesbaar blijft.',
|
||||
),
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ImagePickerBar(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../models/slide.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '_editor_field.dart';
|
||||
|
||||
typedef _Mutate = void Function(VoidCallback fn);
|
||||
|
|
@ -217,6 +218,7 @@ class _BulletColumnState extends State<_BulletColumn> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -228,13 +230,14 @@ class _BulletColumnState extends State<_BulletColumn> {
|
|||
onPressed: () =>
|
||||
set.addAfter((fn) => setState(fn), set.controllers.length - 1),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Bullet toevoegen'),
|
||||
label: Text(l10n.d('Bullet toevoegen')),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRow(int i) {
|
||||
final l10n = context.l10n;
|
||||
final level = set.levels[i];
|
||||
return Padding(
|
||||
key: ValueKey(set.controllers[i]),
|
||||
|
|
@ -284,7 +287,7 @@ class _BulletColumnState extends State<_BulletColumn> {
|
|||
controller: set.controllers[i],
|
||||
focusNode: set.focusNodes[i],
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Bullet ${i + 1}',
|
||||
hintText: '${l10n.d('Bullet')} ${i + 1}',
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
|
|
@ -299,7 +302,7 @@ class _BulletColumnState extends State<_BulletColumn> {
|
|||
onPressed: set.controllers.length > 1
|
||||
? () => set.removeAndFocus((fn) => setState(fn), i)
|
||||
: null,
|
||||
tooltip: 'Verwijder',
|
||||
tooltip: l10n.d('Verwijder'),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
constraints: const BoxConstraints(minWidth: 28),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../models/slide.dart';
|
||||
import '../../services/image_service.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '_editor_field.dart';
|
||||
import 'audio_attachment_editor.dart';
|
||||
|
||||
|
|
@ -47,6 +48,7 @@ class _VideoSlideEditorState extends State<VideoSlideEditor> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
|
|
@ -65,7 +67,7 @@ class _VideoSlideEditorState extends State<VideoSlideEditor> {
|
|||
ElevatedButton.icon(
|
||||
onPressed: _pickVideo,
|
||||
icon: const Icon(Icons.movie_outlined, size: 16),
|
||||
label: const Text('Kiezen'),
|
||||
label: Text(l10n.d('Kiezen')),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -77,7 +79,7 @@ class _VideoSlideEditorState extends State<VideoSlideEditor> {
|
|||
onChanged: (value) => widget.onUpdate(
|
||||
widget.slide.copyWith(videoAutoplay: value ?? false),
|
||||
),
|
||||
title: const Text('Video automatisch afspelen'),
|
||||
title: Text(l10n.d('Video automatisch afspelen')),
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
|
|
@ -101,6 +103,7 @@ class _PathBox extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -109,7 +112,7 @@ class _PathBox extends StatelessWidget {
|
|||
color: Colors.white,
|
||||
),
|
||||
child: Text(
|
||||
path.isEmpty ? 'Geen video gekozen' : path,
|
||||
path.isEmpty ? l10n.d('Geen video gekozen') : path,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: path.isEmpty
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import '../../state/deck_provider.dart';
|
|||
import '../../state/editor_provider.dart';
|
||||
import '../../state/settings_provider.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../editors/bullets_editor.dart';
|
||||
import '../editors/bullets_image_editor.dart';
|
||||
import '../editors/audio_attachment_editor.dart';
|
||||
|
|
@ -324,6 +325,7 @@ class _EditorToolbar extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
// Make sure the active profile is always selectable, even when it was
|
||||
// loaded from a file and is not part of the saved profile list.
|
||||
final profileItems = <ThemeProfile>[
|
||||
|
|
@ -365,7 +367,7 @@ class _EditorToolbar extends StatelessWidget {
|
|||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
type.label,
|
||||
l10n.d(type.label),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
|
@ -435,7 +437,8 @@ class _EditorToolbar extends StatelessWidget {
|
|||
if (activeProfile.name != defaultProfile.name) ...[
|
||||
const SizedBox(width: 2),
|
||||
Tooltip(
|
||||
message: "Terug naar standaardstijl '${defaultProfile.name}'",
|
||||
message:
|
||||
'${context.l10n.d('Terug naar standaardstijl')} ${defaultProfile.name}',
|
||||
child: IconButton(
|
||||
onPressed: onDefaultProfileRequested,
|
||||
icon: const Icon(Icons.restart_alt, size: 16),
|
||||
|
|
@ -460,10 +463,11 @@ class _ToolbarField extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
l10n.d(label),
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w700,
|
||||
|
|
@ -502,6 +506,7 @@ class _SlideTimingControl extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final enabled = slide.advanceDuration > 0;
|
||||
final duration = slide.advanceDuration;
|
||||
|
||||
|
|
@ -519,9 +524,9 @@ class _SlideTimingControl extends StatelessWidget {
|
|||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'Automatisch doorgaan na',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF0369A1)),
|
||||
Text(
|
||||
l10n.d('Automatisch doorgaan na'),
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF0369A1)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Minus knop
|
||||
|
|
@ -578,6 +583,7 @@ class _SlideLogoControl extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
|
|
@ -596,9 +602,9 @@ class _SlideLogoControl extends StatelessWidget {
|
|||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'Logo tonen op deze slide',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF475569)),
|
||||
Text(
|
||||
l10n.d('Logo tonen op deze slide'),
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF475569)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -615,6 +621,7 @@ class _SlideFooterControl extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
|
|
@ -633,9 +640,9 @@ class _SlideFooterControl extends StatelessWidget {
|
|||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'Footer tonen op deze slide',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF475569)),
|
||||
Text(
|
||||
l10n.d('Footer tonen op deze slide'),
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF475569)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -684,6 +691,7 @@ class _NotesFieldState extends State<_NotesField> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
color: const Color(0xFFFFFBEB),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
|
|
@ -701,12 +709,15 @@ class _NotesFieldState extends State<_NotesField> {
|
|||
maxLines: 3,
|
||||
minLines: 1,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Sprekersnotities...',
|
||||
hintStyle: TextStyle(fontSize: 12, color: Color(0xFFD97706)),
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.d('Sprekersnotities...'),
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFFD97706),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 8),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -753,6 +764,7 @@ class _MarkdownModeEditorState extends State<_MarkdownModeEditor> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
|
|
@ -764,10 +776,15 @@ class _MarkdownModeEditorState extends State<_MarkdownModeEditor> {
|
|||
children: [
|
||||
const Icon(Icons.code, size: 14, color: Color(0xFF92400E)),
|
||||
const SizedBox(width: 6),
|
||||
const Expanded(
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Markdown modus — bewerk de volledige presentatie als Marp Markdown',
|
||||
style: TextStyle(fontSize: 11, color: Color(0xFF92400E)),
|
||||
l10n.d(
|
||||
'Markdown modus — bewerk de volledige presentatie als Marp Markdown',
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF92400E),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
|
|
@ -775,11 +792,11 @@ class _MarkdownModeEditorState extends State<_MarkdownModeEditor> {
|
|||
final ok = widget.onApply(_ctrl.text);
|
||||
if (ok) widget.onExitMarkdown();
|
||||
},
|
||||
child: const Text('Toepassen'),
|
||||
child: Text(l10n.d('Toepassen')),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: widget.onExitMarkdown,
|
||||
child: const Text('Annuleren'),
|
||||
child: Text(l10n.t('cancel')),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -788,14 +805,20 @@ class _MarkdownModeEditorState extends State<_MarkdownModeEditor> {
|
|||
Container(
|
||||
color: const Color(0xFFFEE2E2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_outlined, size: 14, color: Colors.red),
|
||||
SizedBox(width: 6),
|
||||
const Icon(
|
||||
Icons.warning_amber_outlined,
|
||||
size: 14,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Markdown kon niet worden verwerkt. Controleer de syntax.',
|
||||
style: TextStyle(fontSize: 11, color: Colors.red),
|
||||
l10n.d(
|
||||
'Markdown kon niet worden verwerkt. Controleer de syntax.',
|
||||
),
|
||||
style: const TextStyle(fontSize: 11, color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import '../../state/deck_provider.dart';
|
|||
import '../../state/editor_provider.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../utils/url_launcher_util.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../slides/slide_preview.dart';
|
||||
|
||||
/// Of het preview-paneel ingeklapt is (UI-voorkeur, app-breed).
|
||||
|
|
@ -85,6 +86,7 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final deckState = ref.watch(deckProvider);
|
||||
final deck = deckState.deck!;
|
||||
final editor = ref.watch(editorProvider);
|
||||
|
|
@ -117,9 +119,9 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
|||
color: Color(0xFF64748B),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'Preview',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
l10n.d('Preview'),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
color: Color(0xFF334155),
|
||||
|
|
@ -128,7 +130,7 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
|||
const Spacer(),
|
||||
// ── Zoom controls ──────────────────────────────────────
|
||||
Tooltip(
|
||||
message: 'Uitzoomen',
|
||||
message: l10n.d('Uitzoomen'),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.remove, size: 16),
|
||||
onPressed: _zoom > _minZoom ? _zoomOut : null,
|
||||
|
|
@ -143,7 +145,7 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
|||
GestureDetector(
|
||||
onTap: _zoom != _minZoom ? _resetZoom : null,
|
||||
child: Tooltip(
|
||||
message: 'Zoom resetten',
|
||||
message: l10n.d('Zoom resetten'),
|
||||
child: Text(
|
||||
'${(_zoom * 100).round()}%',
|
||||
style: TextStyle(
|
||||
|
|
@ -159,7 +161,7 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
|||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: 'Inzoomen',
|
||||
message: l10n.d('Inzoomen'),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
onPressed: _zoom < _maxZoom ? _zoomIn : null,
|
||||
|
|
@ -181,7 +183,7 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
|||
),
|
||||
const SizedBox(width: 4),
|
||||
Tooltip(
|
||||
message: 'Preview inklappen',
|
||||
message: l10n.d('Preview inklappen'),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.chevron_right, size: 18),
|
||||
onPressed: () =>
|
||||
|
|
@ -266,12 +268,12 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
|||
: null,
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
iconSize: 20,
|
||||
tooltip: 'Vorige slide',
|
||||
tooltip: l10n.d('Vorige slide'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
slide.type.label,
|
||||
l10n.d(slide.type.label),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF64748B),
|
||||
|
|
@ -286,7 +288,7 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
|||
: null,
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
iconSize: 20,
|
||||
tooltip: 'Volgende slide',
|
||||
tooltip: l10n.d('Volgende slide'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -308,7 +310,7 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
|||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Thema: ${deck.theme}',
|
||||
'${l10n.d('Thema')}: ${deck.theme}',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF94A3B8),
|
||||
|
|
@ -318,9 +320,9 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
|||
const SizedBox(width: 10),
|
||||
const Icon(Icons.tag, size: 12, color: Color(0xFF94A3B8)),
|
||||
const SizedBox(width: 2),
|
||||
const Text(
|
||||
'paginering aan',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
l10n.d('paginering aan'),
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF94A3B8),
|
||||
),
|
||||
|
|
@ -351,10 +353,11 @@ class FullDeckPreview extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF1E2028),
|
||||
appBar: AppBar(
|
||||
title: Text('${deck.title} — volledig deck'),
|
||||
title: Text('${deck.title} — ${l10n.d('volledig deck')}'),
|
||||
backgroundColor: AppTheme.navy,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
|
|
@ -371,7 +374,7 @@ class FullDeckPreview extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Slide ${i + 1}',
|
||||
'${l10n.d('Slide')} ${i + 1}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF64748B),
|
||||
fontSize: 11,
|
||||
|
|
@ -416,6 +419,7 @@ class CollapsedPreviewBar extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
width: 34,
|
||||
color: Colors.white,
|
||||
|
|
@ -423,7 +427,7 @@ class CollapsedPreviewBar extends ConsumerWidget {
|
|||
children: [
|
||||
const SizedBox(height: 6),
|
||||
Tooltip(
|
||||
message: 'Preview uitklappen',
|
||||
message: l10n.d('Preview uitklappen'),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.chevron_left, size: 18),
|
||||
onPressed: () =>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import '../../services/image_service.dart';
|
|||
import '../../services/slide_rasterizer.dart';
|
||||
import '../../state/slide_clipboard_provider.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../dialogs/add_slide_dialog.dart';
|
||||
import '../dialogs/import_slides_dialog.dart';
|
||||
import '../dialogs/slide_finder_dialog.dart';
|
||||
|
|
@ -157,9 +158,9 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
if (deck == null) return;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Slide renderen…'),
|
||||
duration: Duration(milliseconds: 700),
|
||||
SnackBar(
|
||||
content: Text(context.l10n.d('Slide renderen…')),
|
||||
duration: const Duration(milliseconds: 700),
|
||||
),
|
||||
);
|
||||
Uint8List? bytes;
|
||||
|
|
@ -181,7 +182,9 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
ok ? 'Slide gekopieerd naar klembord.' : 'Kopiëren mislukt.',
|
||||
ok
|
||||
? context.l10n.d('Slide gekopieerd naar klembord.')
|
||||
: context.l10n.d('Kopiëren mislukt.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -214,8 +217,12 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
final messenger = ScaffoldMessenger.of(context);
|
||||
if (targets.isEmpty) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Geen ander deck open. Open eerst een ander tabblad.'),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.d(
|
||||
'Geen ander deck open. Open eerst een ander tabblad.',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
|
|
@ -223,26 +230,29 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
|
||||
final target = await showDialog<TabInfo>(
|
||||
context: context,
|
||||
builder: (ctx) => SimpleDialog(
|
||||
title: Text(
|
||||
slides.length == 1
|
||||
? '1 slide kopiëren naar…'
|
||||
: '${slides.length} slides kopiëren naar…',
|
||||
),
|
||||
children: [
|
||||
for (final t in targets)
|
||||
SimpleDialogOption(
|
||||
onPressed: () => Navigator.pop(ctx, t),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.slideshow_outlined, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(t.label)),
|
||||
],
|
||||
builder: (ctx) {
|
||||
final l10n = ctx.l10n;
|
||||
return SimpleDialog(
|
||||
title: Text(
|
||||
slides.length == 1
|
||||
? l10n.d('1 slide kopiëren naar…')
|
||||
: '${slides.length} ${l10n.d('slides kopiëren naar…')}',
|
||||
),
|
||||
children: [
|
||||
for (final t in targets)
|
||||
SimpleDialogOption(
|
||||
onPressed: () => Navigator.pop(ctx, t),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.slideshow_outlined, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(t.label)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (target == null || !mounted) return;
|
||||
|
||||
|
|
@ -252,8 +262,8 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
SnackBar(
|
||||
content: Text(
|
||||
at >= 0
|
||||
? '${slides.length} slide(s) gekopieerd naar “${target.label}”.'
|
||||
: 'Kopiëren mislukt.',
|
||||
? '${slides.length} ${context.l10n.d('slide(s) gekopieerd naar')} “${target.label}”.'
|
||||
: context.l10n.d('Kopiëren mislukt.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -365,14 +375,15 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
SnackBar(
|
||||
content: Text(
|
||||
slides.length == 1
|
||||
? '1 slide geïmporteerd.'
|
||||
: '${slides.length} slides geïmporteerd.',
|
||||
? context.l10n.d('1 slide geïmporteerd.')
|
||||
: '${slides.length} ${context.l10n.d('slides geïmporteerd.')}',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchField() {
|
||||
final l10n = context.l10n;
|
||||
return SizedBox(
|
||||
height: 30,
|
||||
child: TextField(
|
||||
|
|
@ -381,7 +392,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
hintText: 'Zoek in slides…',
|
||||
hintText: l10n.d('Zoek in slides…'),
|
||||
hintStyle: const TextStyle(color: Color(0xFF6B7280), fontSize: 12),
|
||||
prefixIcon: const Icon(
|
||||
Icons.search,
|
||||
|
|
@ -431,6 +442,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
DeckNotifier notifier,
|
||||
EditorNotifier editorNotifier,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
final matches = <int>[
|
||||
for (var i = 0; i < deck.slides.length; i++)
|
||||
if (_matches(deck.slides[i], query)) i,
|
||||
|
|
@ -450,7 +462,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Geen slides met "$query"',
|
||||
'${l10n.d('Geen slides met')} "$query"',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
|
||||
),
|
||||
|
|
@ -496,6 +508,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final deckState = ref.watch(deckProvider);
|
||||
final deck = deckState.deck!;
|
||||
_pruneSlideKeys(deck);
|
||||
|
|
@ -533,9 +546,9 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'SLIDES',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
l10n.d('SLIDES'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF94A3B8),
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
|
|
@ -667,9 +680,11 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
if (path == null) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Geen afbeelding op het klembord gevonden.',
|
||||
l10n.d(
|
||||
'Geen afbeelding op het klembord gevonden.',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -687,7 +702,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
editorNotifier.select(newIdx);
|
||||
},
|
||||
icon: const Icon(Icons.image_outlined, size: 14),
|
||||
label: const Text('Afbeelding plakken'),
|
||||
label: Text(l10n.d('Afbeelding plakken')),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white70,
|
||||
side: const BorderSide(color: Color(0xFF4A4F5B)),
|
||||
|
|
@ -710,7 +725,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
}
|
||||
},
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Slide toevoegen'),
|
||||
label: Text(l10n.d('Slide toevoegen')),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.accent,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
|
|
@ -728,7 +743,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
Icons.travel_explore_outlined,
|
||||
size: 14,
|
||||
),
|
||||
label: const Text('Slide zoeken'),
|
||||
label: Text(l10n.d('Slide zoeken')),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white70,
|
||||
side: const BorderSide(color: Color(0xFF4A4F5B)),
|
||||
|
|
@ -744,7 +759,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
child: OutlinedButton.icon(
|
||||
onPressed: () => _importSlides(context, ref, deckState),
|
||||
icon: const Icon(Icons.library_add_outlined, size: 14),
|
||||
label: const Text('Slides importeren'),
|
||||
label: Text(l10n.d('Slides importeren')),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white70,
|
||||
side: const BorderSide(color: Color(0xFF4A4F5B)),
|
||||
|
|
@ -771,7 +786,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
editorNotifier.select(newIdx);
|
||||
},
|
||||
icon: const Icon(Icons.content_paste, size: 14),
|
||||
label: const Text('Slide plakken'),
|
||||
label: Text(l10n.d('Slide plakken')),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white70,
|
||||
side: const BorderSide(color: Color(0xFF4A4F5B)),
|
||||
|
|
@ -802,6 +817,7 @@ class _SkipBanner extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(8, 5, 4, 5),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -820,8 +836,8 @@ class _SkipBanner extends StatelessWidget {
|
|||
Expanded(
|
||||
child: Text(
|
||||
count == 1
|
||||
? '1 slide overgeslagen'
|
||||
: '$count slides overgeslagen',
|
||||
? l10n.d('1 slide overgeslagen')
|
||||
: '$count ${l10n.d('slides overgeslagen')}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFE3C281),
|
||||
fontSize: 11,
|
||||
|
|
@ -841,7 +857,7 @@ class _SkipBanner extends StatelessWidget {
|
|||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
child: const Text('Alles tonen'),
|
||||
child: Text(l10n.d('Alles tonen')),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -870,6 +886,7 @@ class _BulkActionBar extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 4, 4),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -881,7 +898,7 @@ class _BulkActionBar extends StatelessWidget {
|
|||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'$count geselecteerd',
|
||||
'$count ${l10n.d('geselecteerd')}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFE2E8F0),
|
||||
fontSize: 11,
|
||||
|
|
@ -891,28 +908,28 @@ class _BulkActionBar extends StatelessWidget {
|
|||
),
|
||||
_BulkIcon(
|
||||
icon: Icons.drive_file_move_outline,
|
||||
tooltip: 'Kopiëren naar ander deck',
|
||||
tooltip: l10n.d('Kopiëren naar ander deck'),
|
||||
onTap: onCopyToDeck,
|
||||
),
|
||||
_BulkIcon(
|
||||
icon: Icons.visibility_off_outlined,
|
||||
tooltip: 'Overslaan bij presenteren/exporteren',
|
||||
tooltip: l10n.d('Overslaan bij presenteren/exporteren'),
|
||||
onTap: onSkip,
|
||||
),
|
||||
_BulkIcon(
|
||||
icon: Icons.visibility_outlined,
|
||||
tooltip: 'Weer tonen',
|
||||
tooltip: l10n.d('Weer tonen'),
|
||||
onTap: onShow,
|
||||
),
|
||||
_BulkIcon(
|
||||
icon: Icons.delete_outline,
|
||||
tooltip: 'Verwijderen',
|
||||
tooltip: l10n.d('Verwijderen'),
|
||||
color: const Color(0xFFE5746E),
|
||||
onTap: onDelete,
|
||||
),
|
||||
_BulkIcon(
|
||||
icon: Icons.close,
|
||||
tooltip: 'Selectie opheffen',
|
||||
tooltip: l10n.d('Selectie opheffen'),
|
||||
onTap: onDeselect,
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import '../../models/deck.dart';
|
|||
import '../../models/settings.dart';
|
||||
import '../../models/slide.dart';
|
||||
import '../../utils/url_launcher_util.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../slides/slide_preview.dart';
|
||||
|
||||
/// Blanco-schermstand tijdens het presenteren (zoals B/W in PowerPoint).
|
||||
|
|
@ -578,20 +579,21 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
|
||||
/// Sneltoets-overzicht (cheatsheet).
|
||||
Widget _buildHelpOverlay() {
|
||||
const rows = <(String, String)>[
|
||||
('→ · spatie · klik', 'Volgende slide'),
|
||||
('←', 'Vorige slide'),
|
||||
('cijfers + Enter', 'Naar slidenummer'),
|
||||
('Home · End', 'Eerste · laatste slide'),
|
||||
('G', 'Slide-overzicht (pijltjes + Enter)'),
|
||||
('P', 'Presenter view (notities, klok)'),
|
||||
('B · W', 'Zwart · wit scherm'),
|
||||
('R', 'Verstreken tijd resetten'),
|
||||
('A', 'Automatische modus aan/uit'),
|
||||
('L', 'Herhalen (loop) aan/uit'),
|
||||
('M', 'Na audio automatisch doorgaan'),
|
||||
('? · H', 'Dit overzicht'),
|
||||
('Esc', 'Terug / afsluiten'),
|
||||
final l10n = context.l10n;
|
||||
final rows = <(String, String)>[
|
||||
('→ · ${l10n.d('spatie')} · ${l10n.d('klik')}', l10n.d('Volgende slide')),
|
||||
('←', l10n.d('Vorige slide')),
|
||||
('${l10n.d('cijfers')} + Enter', l10n.d('Naar slidenummer')),
|
||||
('Home · End', l10n.d('Eerste · laatste slide')),
|
||||
('G', l10n.d('Slide-overzicht (pijltjes + Enter)')),
|
||||
('P', l10n.d('Presenter view (notities, klok)')),
|
||||
('B · W', l10n.d('Zwart · wit scherm')),
|
||||
('R', l10n.d('Verstreken tijd resetten')),
|
||||
('A', l10n.d('Automatische modus aan/uit')),
|
||||
('L', l10n.d('Herhalen (loop) aan/uit')),
|
||||
('M', l10n.d('Na audio automatisch doorgaan')),
|
||||
('? · H', l10n.d('Dit overzicht')),
|
||||
('Esc', l10n.d('Terug / afsluiten')),
|
||||
];
|
||||
return GestureDetector(
|
||||
onTap: _toggleHelp,
|
||||
|
|
@ -616,17 +618,17 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
const Icon(
|
||||
Icons.keyboard_outlined,
|
||||
color: Colors.white70,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'Sneltoetsen',
|
||||
style: TextStyle(
|
||||
l10n.d('Sneltoetsen'),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
|
@ -664,10 +666,13 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Center(
|
||||
Center(
|
||||
child: Text(
|
||||
'Klik of druk op ? / H / Esc om te sluiten',
|
||||
style: TextStyle(color: Colors.white30, fontSize: 12),
|
||||
l10n.d('Klik of druk op ? / H / Esc om te sluiten'),
|
||||
style: const TextStyle(
|
||||
color: Colors.white30,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -682,14 +687,15 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
/// Subtiele statusindicator (linksonder) voor de automatische modus. Toont
|
||||
/// of auto-play, herhalen en 'na audio doorgaan' actief zijn.
|
||||
Widget _autoPlayStatus() {
|
||||
final l10n = context.l10n;
|
||||
final items = <(IconData, String, bool)>[
|
||||
(
|
||||
_autoPlay ? Icons.play_circle_outline : Icons.pause_circle_outline,
|
||||
_autoPlay ? 'Auto (A)' : 'Handmatig (A)',
|
||||
_autoPlay ? l10n.d('Auto (A)') : l10n.d('Handmatig (A)'),
|
||||
_autoPlay,
|
||||
),
|
||||
(Icons.repeat, 'Herhalen (L)', _loop),
|
||||
(Icons.graphic_eq, 'Na audio (M)', _advanceOnAudioEnd),
|
||||
(Icons.repeat, l10n.d('Herhalen (L)'), _loop),
|
||||
(Icons.graphic_eq, l10n.d('Na audio (M)'), _advanceOnAudioEnd),
|
||||
];
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -868,7 +874,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
child: Row(
|
||||
children: [
|
||||
Tooltip(
|
||||
message: 'Sneltoetsen (?)',
|
||||
message: context.l10n.d('Sneltoetsen (?)'),
|
||||
child: IconButton(
|
||||
onPressed: _toggleHelp,
|
||||
icon: const Icon(Icons.help_outline),
|
||||
|
|
@ -880,7 +886,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
Tooltip(
|
||||
message: 'Slide-overzicht (G)',
|
||||
message: context.l10n.d('Slide-overzicht (G)'),
|
||||
child: IconButton(
|
||||
onPressed: _toggleGrid,
|
||||
icon: const Icon(Icons.grid_view_rounded),
|
||||
|
|
@ -892,7 +898,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
Tooltip(
|
||||
message: 'Presenter view (P)',
|
||||
message: context.l10n.d('Presenter view (P)'),
|
||||
child: IconButton(
|
||||
onPressed: _togglePresenterView,
|
||||
icon: const Icon(Icons.co_present_outlined),
|
||||
|
|
@ -904,7 +910,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
),
|
||||
const SizedBox(width: 8),
|
||||
Tooltip(
|
||||
message: 'Afsluiten (Escape)',
|
||||
message: context.l10n.d('Afsluiten (Escape)'),
|
||||
child: IconButton(
|
||||
onPressed: _exit,
|
||||
icon: const Icon(Icons.close),
|
||||
|
|
@ -925,6 +931,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
// ── Presenter view (slide + volgende + notities + tijd) ──────────────────
|
||||
|
||||
Widget _buildPresenterView(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final total = widget.slides.length;
|
||||
final slide = widget.slides[_index.clamp(0, total - 1)];
|
||||
final hasNext = _index < total - 1;
|
||||
|
|
@ -942,7 +949,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const _SectionLabel('HUIDIGE SLIDE'),
|
||||
_SectionLabel(l10n.d('HUIDIGE SLIDE')),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
|
|
@ -988,7 +995,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
children: [
|
||||
_buildClockBar(),
|
||||
const SizedBox(height: 16),
|
||||
const _SectionLabel('VOLGENDE'),
|
||||
_SectionLabel(l10n.d('VOLGENDE')),
|
||||
const SizedBox(height: 8),
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
|
|
@ -1006,9 +1013,9 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
: Container(
|
||||
color: const Color(0xFF161616),
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
'Einde van de presentatie',
|
||||
style: TextStyle(
|
||||
child: Text(
|
||||
l10n.d('Einde van de presentatie'),
|
||||
style: const TextStyle(
|
||||
color: Colors.white38,
|
||||
fontSize: 13,
|
||||
),
|
||||
|
|
@ -1017,7 +1024,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const _SectionLabel('NOTITIES'),
|
||||
_SectionLabel(l10n.d('NOTITIES')),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: _buildNotes(slide)),
|
||||
],
|
||||
|
|
@ -1029,6 +1036,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
}
|
||||
|
||||
Widget _buildClockBar() {
|
||||
final l10n = context.l10n;
|
||||
final elapsed = DateTime.now().difference(_startTime);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
|
|
@ -1044,9 +1052,9 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Verstreken',
|
||||
style: TextStyle(color: Colors.white38, fontSize: 10),
|
||||
Text(
|
||||
l10n.d('Verstreken'),
|
||||
style: const TextStyle(color: Colors.white38, fontSize: 10),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
|
|
@ -1063,7 +1071,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
),
|
||||
// Reset-knop
|
||||
Tooltip(
|
||||
message: 'Tijd resetten (R)',
|
||||
message: l10n.d('Tijd resetten (R)'),
|
||||
child: IconButton(
|
||||
onPressed: _resetTimer,
|
||||
icon: const Icon(Icons.restart_alt, size: 18),
|
||||
|
|
@ -1076,9 +1084,9 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const Text(
|
||||
'Klok',
|
||||
style: TextStyle(color: Colors.white38, fontSize: 10),
|
||||
Text(
|
||||
l10n.d('Klok'),
|
||||
style: const TextStyle(color: Colors.white38, fontSize: 10),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
|
|
@ -1098,6 +1106,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
}
|
||||
|
||||
Widget _buildNotes(Slide slide) {
|
||||
final l10n = context.l10n;
|
||||
final notes = slide.notes.trim();
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
|
|
@ -1108,11 +1117,11 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
border: Border.all(color: const Color(0xFF262626)),
|
||||
),
|
||||
child: notes.isEmpty
|
||||
? const Align(
|
||||
? Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
'Geen notities voor deze slide.',
|
||||
style: TextStyle(
|
||||
l10n.d('Geen notities voor deze slide.'),
|
||||
style: const TextStyle(
|
||||
color: Colors.white30,
|
||||
fontSize: 14,
|
||||
fontStyle: FontStyle.italic,
|
||||
|
|
@ -1133,6 +1142,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
}
|
||||
|
||||
Widget _buildPresenterControls(int total) {
|
||||
final l10n = context.l10n;
|
||||
return Row(
|
||||
children: [
|
||||
_NavButton(icon: Icons.chevron_left, onTap: _index > 0 ? _prev : null),
|
||||
|
|
@ -1143,7 +1153,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
'Slide ${_index + 1} / $total',
|
||||
'${l10n.d('Slide')} ${_index + 1} / $total',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
|
|
@ -1151,9 +1161,11 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Expanded(
|
||||
Expanded(
|
||||
child: Text(
|
||||
'P publiek · G overzicht · B/W zwart/wit · R tijd · Esc stop',
|
||||
l10n.d(
|
||||
'P publiek · G overzicht · B/W zwart/wit · R tijd · Esc stop',
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
|
@ -1162,7 +1174,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
),
|
||||
const SizedBox(width: 12),
|
||||
Tooltip(
|
||||
message: 'Afsluiten (Escape)',
|
||||
message: l10n.d('Afsluiten (Escape)'),
|
||||
child: IconButton(
|
||||
onPressed: _exit,
|
||||
icon: const Icon(Icons.close),
|
||||
|
|
@ -1177,6 +1189,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
// ── Rasteroverzicht (snel naar een slide springen) ───────────────────────
|
||||
|
||||
Widget _buildGridOverlay() {
|
||||
final l10n = context.l10n;
|
||||
final total = widget.slides.length;
|
||||
return Container(
|
||||
color: Colors.black.withValues(alpha: 0.94),
|
||||
|
|
@ -1187,9 +1200,9 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
padding: const EdgeInsets.fromLTRB(24, 16, 16, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text(
|
||||
'Slide-overzicht',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
l10n.d('Slide-overzicht'),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
|
@ -1197,12 +1210,12 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'pijltjes + Enter of klik om te springen · $total slides',
|
||||
'${l10n.d('pijltjes + Enter of klik om te springen')} · $total ${l10n.t('slides')}',
|
||||
style: const TextStyle(color: Colors.white38, fontSize: 12),
|
||||
),
|
||||
const Spacer(),
|
||||
Tooltip(
|
||||
message: 'Sluiten (G of Esc)',
|
||||
message: l10n.d('Sluiten (G of Esc)'),
|
||||
child: IconButton(
|
||||
onPressed: _toggleGrid,
|
||||
icon: const Icon(Icons.close),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import '../../models/settings.dart';
|
|||
import '../../models/slide.dart';
|
||||
import '../../state/slide_clipboard_provider.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import 'slide_preview.dart';
|
||||
|
||||
class SlideThumbnail extends ConsumerWidget {
|
||||
|
|
@ -44,6 +45,7 @@ class SlideThumbnail extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = context.l10n;
|
||||
final skipped = slide.skipped;
|
||||
final borderColor = isSelected
|
||||
? AppTheme.accent
|
||||
|
|
@ -100,18 +102,18 @@ class SlideThumbnail extends ConsumerWidget {
|
|||
color: const Color(0xCC8A6D3B),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
const Icon(
|
||||
Icons.visibility_off_outlined,
|
||||
size: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(width: 3),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
'Overgeslagen',
|
||||
style: TextStyle(
|
||||
l10n.d('Overgeslagen'),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
|
@ -153,7 +155,7 @@ class SlideThumbnail extends ConsumerWidget {
|
|||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
slide.type.label,
|
||||
l10n.d(slide.type.label),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF94A3B8),
|
||||
fontSize: 9,
|
||||
|
|
@ -182,8 +184,8 @@ class SlideThumbnail extends ConsumerWidget {
|
|||
iconSize: 14,
|
||||
splashRadius: 12,
|
||||
tooltip: skipped
|
||||
? 'Weer tonen bij presenteren/exporteren'
|
||||
: 'Overslaan bij presenteren/exporteren',
|
||||
? l10n.d('Weer tonen bij presenteren/exporteren')
|
||||
: l10n.d('Overslaan bij presenteren/exporteren'),
|
||||
icon: Icon(
|
||||
skipped
|
||||
? Icons.visibility_off
|
||||
|
|
@ -207,29 +209,31 @@ class SlideThumbnail extends ConsumerWidget {
|
|||
),
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (_) => [
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'copy',
|
||||
child: Text('Kopiëren'),
|
||||
child: Text(l10n.d('Kopiëren')),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'copy_image',
|
||||
child: Text('Kopieer als afbeelding'),
|
||||
child: Text(l10n.d('Kopieer als afbeelding')),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'duplicate',
|
||||
child: Text('Dupliceren'),
|
||||
child: Text(l10n.d('Dupliceren')),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'skip',
|
||||
child: Text(
|
||||
skipped ? 'Niet meer overslaan' : 'Overslaan',
|
||||
skipped
|
||||
? l10n.d('Niet meer overslaan')
|
||||
: l10n.d('Overslaan'),
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text(
|
||||
'Verwijderen',
|
||||
style: TextStyle(color: Colors.red),
|
||||
l10n.d('Verwijderen'),
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
13
pubspec.lock
13
pubspec.lock
|
|
@ -230,6 +230,11 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_math_fork:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -344,6 +349,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
cupertino_icons: ^1.0.8
|
||||
flutter_riverpod: ^3.3.1
|
||||
file_picker: ^11.0.2
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ocideck/models/settings.dart';
|
||||
import 'package:ocideck/services/marp_html_service.dart';
|
||||
|
||||
/// Reads the vendored libraries straight from the repo (tests run at the root).
|
||||
Future<String> _diskLoader(String asset) => File(asset).readAsString();
|
||||
Future<Uint8List> _diskBytes(String asset) => File(asset).readAsBytes();
|
||||
|
||||
void main() {
|
||||
group('marpSlides', () {
|
||||
|
|
@ -73,4 +76,38 @@ void main() {}
|
|||
expect(html, isNot(contains('foo </script> bar')));
|
||||
expect(html, contains(r'<\/script'));
|
||||
});
|
||||
|
||||
test('a theme colours the slides with the profile palette', () async {
|
||||
final service = MarpHtmlService(
|
||||
loadAsset: _diskLoader,
|
||||
loadBytes: _diskBytes,
|
||||
);
|
||||
const theme = ThemeProfile(
|
||||
slideBackgroundColor: '#102030',
|
||||
textColor: '#EEF1F4',
|
||||
accentColor: '#33CC99',
|
||||
fontFamily: 'Arial',
|
||||
);
|
||||
final html = await service.build('# Titel', theme: theme);
|
||||
|
||||
expect(html, contains('background:#102030'));
|
||||
expect(html, contains('color:#EEF1F4'));
|
||||
expect(html, contains('#33CC99'));
|
||||
expect(html, contains("'Arial'"));
|
||||
// A system font is not embedded as base64.
|
||||
expect(html, isNot(contains('data:font/ttf;base64,')));
|
||||
});
|
||||
|
||||
test('EB Garamond theme embeds the font for offline rendering', () async {
|
||||
final service = MarpHtmlService(
|
||||
loadAsset: _diskLoader,
|
||||
loadBytes: _diskBytes,
|
||||
);
|
||||
const theme = ThemeProfile(fontFamily: 'EB Garamond');
|
||||
final html = await service.build('# Titel', theme: theme);
|
||||
|
||||
expect(html, contains('@font-face'));
|
||||
expect(html, contains('data:font/ttf;base64,'));
|
||||
expect(html, contains("'EB Garamond'"));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue