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