Compare commits
No commits in common. "d59c6ee761f41e3b86371575f707d725cfa9ff12" and "3e664193ced97d51720c487acc1ae2bf6177b60f" have entirely different histories.
d59c6ee761
...
3e664193ce
41 changed files with 871 additions and 3491 deletions
20
lib/app.dart
20
lib/app.dart
|
|
@ -1,32 +1,16 @@
|
||||||
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 ConsumerWidget {
|
class OciDeckApp extends StatelessWidget {
|
||||||
const OciDeckApp({super.key});
|
const OciDeckApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
final languageCode = ref.watch(
|
|
||||||
settingsProvider.select((s) => s.languageCode),
|
|
||||||
);
|
|
||||||
AppLocalizations.setActiveLanguageCode(languageCode);
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'OciDeck',
|
title: 'OciDeck',
|
||||||
theme: AppTheme.light,
|
theme: AppTheme.light,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
locale: AppLocalizations.materialLocaleFor(languageCode),
|
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
|
||||||
localizationsDelegates: const [
|
|
||||||
AppLocalizations.delegate,
|
|
||||||
GlobalMaterialLocalizations.delegate,
|
|
||||||
GlobalCupertinoLocalizations.delegate,
|
|
||||||
GlobalWidgetsLocalizations.delegate,
|
|
||||||
],
|
|
||||||
home: const AppShell(),
|
home: const AppShell(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -145,7 +145,6 @@ 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
|
||||||
|
|
@ -156,7 +155,6 @@ 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()],
|
||||||
|
|
@ -186,7 +184,6 @@ class AppSettings {
|
||||||
];
|
];
|
||||||
|
|
||||||
AppSettings copyWith({
|
AppSettings copyWith({
|
||||||
String? languageCode,
|
|
||||||
String? homeDirectory,
|
String? homeDirectory,
|
||||||
String? exportDirectory,
|
String? exportDirectory,
|
||||||
ThemeProfile? themeProfile,
|
ThemeProfile? themeProfile,
|
||||||
|
|
@ -198,7 +195,6 @@ 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,7 +8,6 @@ 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 }
|
||||||
|
|
@ -102,7 +101,6 @@ 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) {
|
||||||
|
|
@ -130,9 +128,7 @@ 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(
|
bytes = Uint8List.fromList(utf8.encode(await _html.build(markdown!)));
|
||||||
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,7 +6,6 @@ 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';
|
||||||
|
|
@ -41,20 +40,12 @@ 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(
|
FileService(this._md, this._img, this._themeProfile);
|
||||||
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',
|
||||||
|
|
@ -126,7 +117,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: _d('Presentatie openen'),
|
dialogTitle: 'Presentatie openen',
|
||||||
type: FileType.custom,
|
type: FileType.custom,
|
||||||
allowedExtensions: ['md'],
|
allowedExtensions: ['md'],
|
||||||
initialDirectory: initialDirectory,
|
initialDirectory: initialDirectory,
|
||||||
|
|
@ -153,7 +144,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: _d('Opslaan als'),
|
dialogTitle: 'Opslaan als',
|
||||||
fileName: '$safeName.md',
|
fileName: '$safeName.md',
|
||||||
initialDirectory: initialDirectory,
|
initialDirectory: initialDirectory,
|
||||||
);
|
);
|
||||||
|
|
@ -369,7 +360,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: _d('Pakket importeren'),
|
dialogTitle: 'Pakket importeren',
|
||||||
type: FileType.custom,
|
type: FileType.custom,
|
||||||
allowedExtensions: [packageExtension, 'zip'],
|
allowedExtensions: [packageExtension, 'zip'],
|
||||||
initialDirectory: initialDirectory,
|
initialDirectory: initialDirectory,
|
||||||
|
|
@ -379,7 +370,7 @@ class FileService {
|
||||||
|
|
||||||
Future<String?> pickPackageDestination(Deck deck) async {
|
Future<String?> pickPackageDestination(Deck deck) async {
|
||||||
return FilePicker.saveFile(
|
return FilePicker.saveFile(
|
||||||
dialogTitle: _d('Pakket exporteren'),
|
dialogTitle: 'Pakket exporteren',
|
||||||
fileName: '${_safeName(deck.title)}.$packageExtension',
|
fileName: '${_safeName(deck.title)}.$packageExtension',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,13 @@ 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: _d('Kies een afbeelding'),
|
dialogTitle: 'Kies een afbeelding',
|
||||||
);
|
);
|
||||||
return result?.files.single.path;
|
return result?.files.single.path;
|
||||||
}
|
}
|
||||||
|
|
@ -26,7 +18,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: _d('Kies een video'),
|
dialogTitle: 'Kies een video',
|
||||||
);
|
);
|
||||||
return result?.files.single.path;
|
return result?.files.single.path;
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +26,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: _d('Kies een audiobestand'),
|
dialogTitle: 'Kies een audiobestand',
|
||||||
);
|
);
|
||||||
return result?.files.single.path;
|
return result?.files.single.path;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
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,
|
||||||
|
|
@ -15,32 +10,21 @@ import '../models/settings.dart';
|
||||||
/// 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 text asset (defaults to the Flutter asset bundle).
|
/// Reads a bundled asset (defaults to the Flutter asset bundle). Injectable so
|
||||||
/// Injectable so the builder can be unit-tested against the on-disk files.
|
/// the builder can be unit-tested against the on-disk asset files.
|
||||||
final Future<String> Function(String asset) loadAsset;
|
final Future<String> Function(String asset) loadAsset;
|
||||||
|
|
||||||
/// Reads a bundled binary asset (used to embed the EB Garamond font).
|
MarpHtmlService({Future<String> Function(String asset)? loadAsset})
|
||||||
final Future<Uint8List> Function(String asset) loadBytes;
|
: loadAsset = loadAsset ?? rootBundle.loadString;
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
/// Builds the HTML. When [theme] is given, the slides take that profile's
|
Future<String> build(String deckMarkdown) async {
|
||||||
/// 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)) {
|
||||||
|
|
@ -56,7 +40,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>$css\n$hljsCss</style>'
|
'<style>$_baseCss\n$hljsCss</style>'
|
||||||
'<script>$_mathjaxConfig</script>'
|
'<script>$_mathjaxConfig</script>'
|
||||||
'${inline(marked)}'
|
'${inline(marked)}'
|
||||||
'${inline(hljs)}'
|
'${inline(hljs)}'
|
||||||
|
|
@ -101,61 +85,6 @@ 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,17 +13,12 @@ import 'settings_provider.dart';
|
||||||
final markdownServiceProvider = Provider<MarkdownService>(
|
final markdownServiceProvider = Provider<MarkdownService>(
|
||||||
(_) => MarkdownService(),
|
(_) => MarkdownService(),
|
||||||
);
|
);
|
||||||
final imageServiceProvider = Provider<ImageService>((ref) {
|
final imageServiceProvider = Provider<ImageService>((_) => ImageService());
|
||||||
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,7 +29,6 @@ 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,
|
||||||
|
|
@ -49,12 +48,6 @@ 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,7 +15,6 @@ 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';
|
||||||
|
|
@ -51,23 +50,20 @@ 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: Text(l10n.d('Importeren via URL')),
|
title: const Text('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: [
|
||||||
Text(
|
const 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(
|
||||||
|
|
@ -88,12 +84,12 @@ Future<String?> _showUrlDialog(BuildContext context) {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx),
|
onPressed: () => Navigator.pop(ctx),
|
||||||
child: Text(l10n.t('cancel')),
|
child: const Text('Annuleren'),
|
||||||
),
|
),
|
||||||
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: Text(l10n.d('Ophalen')),
|
label: const Text('Ophalen'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -164,20 +160,19 @@ 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) {
|
builder: (ctx) => AlertDialog(
|
||||||
final l10n = ctx.l10n;
|
title: const Text('Niet-opgeslagen werk herstellen?'),
|
||||||
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
|
||||||
? l10n.d(
|
? 'Er is een presentatie met niet-opgeslagen wijzigingen '
|
||||||
'Er is een presentatie met niet-opgeslagen wijzigingen gevonden van een vorige sessie:',
|
'gevonden van een vorige sessie:'
|
||||||
)
|
: 'Er zijn ${snapshots.length} presentaties met '
|
||||||
: '${l10n.d('Er zijn')} ${snapshots.length} ${l10n.d('presentaties met niet-opgeslagen wijzigingen gevonden van een vorige sessie:')}',
|
'niet-opgeslagen wijzigingen gevonden van een vorige '
|
||||||
|
'sessie:',
|
||||||
style: const TextStyle(fontSize: 13),
|
style: const TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
@ -197,15 +192,14 @@ class _AppShellState extends ConsumerState<AppShell> with WindowListener {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx, false),
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
child: Text(l10n.d('Verwijderen')),
|
child: const Text('Verwijderen'),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => Navigator.pop(ctx, true),
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
child: Text(l10n.d('Herstellen')),
|
child: const Text('Herstellen'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (restore == true) {
|
if (restore == true) {
|
||||||
|
|
@ -230,9 +224,8 @@ 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(
|
||||||
context.l10n.d(
|
'Er zijn presentaties met niet-opgeslagen wijzigingen. '
|
||||||
'Er zijn presentaties met niet-opgeslagen wijzigingen. Sla ze op voordat de app sluit.',
|
'Sla ze op voordat de app sluit.',
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (!shouldSave) return;
|
if (!shouldSave) return;
|
||||||
final saved = await _saveAllDirtyTabs();
|
final saved = await _saveAllDirtyTabs();
|
||||||
|
|
@ -253,23 +246,20 @@ 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) {
|
builder: (ctx) => AlertDialog(
|
||||||
final l10n = ctx.l10n;
|
title: const Text('Niet-opgeslagen wijzigingen'),
|
||||||
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: Text(l10n.t('cancel')),
|
child: const Text('Annuleren'),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => Navigator.pop(ctx, true),
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
child: Text(l10n.d('Opslaan en sluiten')),
|
child: const Text('Opslaan en sluiten'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
},
|
|
||||||
) ??
|
) ??
|
||||||
false;
|
false;
|
||||||
}
|
}
|
||||||
|
|
@ -288,9 +278,8 @@ 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(
|
||||||
context.l10n.d(
|
'Deze presentatie heeft niet-opgeslagen wijzigingen. '
|
||||||
'Deze presentatie heeft niet-opgeslagen wijzigingen. Sla de presentatie op voordat het tabblad sluit.',
|
'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(
|
||||||
|
|
@ -354,11 +343,10 @@ 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(
|
||||||
SnackBar(
|
const SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
context.l10n.d(
|
'Open eerst een presentatie om afbeeldingen toe te '
|
||||||
'Open eerst een presentatie om afbeeldingen toe te voegen.',
|
'voegen.',
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -514,7 +502,6 @@ 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,
|
||||||
|
|
@ -538,7 +525,7 @@ class _AppTabBar extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.t('newTab'),
|
message: 'Nieuw tabblad',
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onAdd,
|
onTap: onAdd,
|
||||||
child: const SizedBox(
|
child: const SizedBox(
|
||||||
|
|
@ -648,7 +635,6 @@ 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),
|
||||||
|
|
@ -681,7 +667,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: Text(l10n.t('newPresentation')),
|
label: const Text('Nieuwe presentatie'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
@ -690,7 +676,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: Text(l10n.t('open')),
|
label: const Text('Openen...'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -708,11 +694,11 @@ class _WelcomeScreen extends ConsumerWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
const Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 10),
|
padding: EdgeInsets.fromLTRB(16, 20, 16, 10),
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.t('recentPresentations'),
|
'Recente presentaties',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: Color(0xFF94A3B8),
|
color: Color(0xFF94A3B8),
|
||||||
|
|
@ -816,7 +802,6 @@ 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);
|
||||||
|
|
||||||
|
|
@ -883,13 +868,11 @@ 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(
|
||||||
SnackBar(
|
const 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;
|
||||||
}
|
}
|
||||||
|
|
@ -906,10 +889,8 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
];
|
];
|
||||||
if (visible.isEmpty) {
|
if (visible.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
const SnackBar(
|
||||||
content: Text(
|
content: Text('Alle slides zijn overgeslagen — niets om te tonen.'),
|
||||||
l10n.d('Alle slides zijn overgeslagen — niets om te tonen.'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
@ -930,9 +911,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(
|
||||||
SnackBar(
|
const SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
l10n.d('Alle slides zijn overgeslagen — niets om te exporteren.'),
|
'Alle slides zijn overgeslagen — niets om te exporteren.',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -951,13 +932,6 @@ 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);
|
||||||
|
|
@ -1007,15 +981,13 @@ 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(
|
SnackBar(content: Text('Pakket geëxporteerd naar:\n$dest')),
|
||||||
content: Text('${l10n.d('Pakket geëxporteerd naar:')}\n$dest'),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
SnackBar(content: Text('${l10n.d('Export mislukt:')} $e')),
|
context,
|
||||||
);
|
).showSnackBar(SnackBar(content: Text('Export mislukt: $e')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1030,7 +1002,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(
|
||||||
SnackBar(content: Text(l10n.d('Kon dit pakket niet importeren.'))),
|
const SnackBar(content: Text('Kon dit pakket niet importeren.')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1043,8 +1015,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(
|
||||||
SnackBar(
|
const SnackBar(
|
||||||
content: Text(l10n.d('Kon van deze URL geen presentatie ophalen.')),
|
content: Text('Kon van deze URL geen presentatie ophalen.'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1134,14 +1106,14 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
actions: [
|
actions: [
|
||||||
// ── Bewerken ────────────────────────────────────────────────
|
// ── Bewerken ────────────────────────────────────────────────
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.t('undo'),
|
message: 'Ongedaan maken (Ctrl/Cmd+Z)',
|
||||||
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: l10n.t('redo'),
|
message: 'Opnieuw uitvoeren (Ctrl/Cmd+Shift+Z)',
|
||||||
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,
|
||||||
|
|
@ -1150,7 +1122,7 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
const _ActionsDivider(),
|
const _ActionsDivider(),
|
||||||
// ── Inhoud ──────────────────────────────────────────────────
|
// ── Inhoud ──────────────────────────────────────────────────
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.t('imageLibrary'),
|
message: 'Afbeeldingenbibliotheek',
|
||||||
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,
|
||||||
|
|
@ -1159,16 +1131,15 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
const _ActionsDivider(),
|
const _ActionsDivider(),
|
||||||
// ── Presenteren & uitvoer ───────────────────────────────────
|
// ── Presenteren & uitvoer ───────────────────────────────────
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.t('presentFullscreen'),
|
message:
|
||||||
|
'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
|
message: isMarkdownMode ? 'Visuele modus' : 'Markdown modus',
|
||||||
? 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,
|
||||||
|
|
@ -1178,16 +1149,25 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.t('saveShortcut'),
|
message: 'Opslaan (Ctrl/Cmd+S)',
|
||||||
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(
|
||||||
|
message: 'Exporteren (PDF/PPTX)',
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.upload_file_outlined, size: 18),
|
||||||
|
onPressed: (deckState.filePath != null && !deckState.isDirty)
|
||||||
|
? exportDeck
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
const _ActionsDivider(),
|
const _ActionsDivider(),
|
||||||
// ── Overig (minder vaak gebruikt) ───────────────────────────
|
// ── Overig (minder vaak gebruikt) ───────────────────────────
|
||||||
PopupMenuButton<String>(
|
PopupMenuButton<String>(
|
||||||
tooltip: l10n.t('more'),
|
tooltip: 'Meer',
|
||||||
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) {
|
||||||
|
|
@ -1225,31 +1205,27 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
menuItem(
|
menuItem(
|
||||||
'new_tab',
|
'new_tab',
|
||||||
Icons.add_circle_outline,
|
Icons.add_circle_outline,
|
||||||
l10n.t('newPresentationTab'),
|
'Nieuwe presentatie (tab)',
|
||||||
),
|
|
||||||
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,
|
||||||
l10n.t('exportPackage'),
|
'Pakket exporteren…',
|
||||||
),
|
),
|
||||||
menuItem(
|
menuItem(
|
||||||
'import_package',
|
'import_package',
|
||||||
Icons.unarchive_outlined,
|
Icons.unarchive_outlined,
|
||||||
l10n.t('importPackage'),
|
'Pakket importeren…',
|
||||||
),
|
),
|
||||||
menuItem('import_url', Icons.link, l10n.t('importUrl')),
|
menuItem('import_url', Icons.link, 'Importeren via URL…'),
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
menuItem('find', Icons.find_replace, l10n.t('findReplace')),
|
menuItem('find', Icons.find_replace, 'Zoeken en vervangen'),
|
||||||
menuItem(
|
menuItem(
|
||||||
'full_preview',
|
'full_preview',
|
||||||
Icons.preview_outlined,
|
Icons.preview_outlined,
|
||||||
l10n.t('fullDeckPreview'),
|
'Volledig deck bekijken',
|
||||||
),
|
),
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
for (final profile in settings.themeProfiles)
|
for (final profile in settings.themeProfiles)
|
||||||
|
|
@ -1267,7 +1243,7 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
'${l10n.t('styleProfile')}: ${profile.name}',
|
'Stijl: ${profile.name}',
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -1278,26 +1254,14 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
menuItem(
|
menuItem(
|
||||||
'properties',
|
'properties',
|
||||||
Icons.info_outline,
|
Icons.info_outline,
|
||||||
l10n.t('presentationProperties'),
|
'Presentatie-eigenschappen',
|
||||||
),
|
|
||||||
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) {
|
||||||
|
|
@ -1359,212 +1323,6 @@ 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();
|
||||||
|
|
@ -1595,7 +1353,6 @@ 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,
|
||||||
|
|
@ -1608,9 +1365,7 @@ 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: l10n.d(
|
message: 'Sleep om de slide-preview breder of smaller te maken',
|
||||||
'Sleep om de slide-preview breder of smaller te maken',
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 9,
|
width: 9,
|
||||||
child: Center(
|
child: Center(
|
||||||
|
|
@ -1638,7 +1393,6 @@ 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);
|
||||||
|
|
||||||
|
|
@ -1678,7 +1432,7 @@ class _TlpChip extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
return PopupMenuButton<TlpLevel>(
|
return PopupMenuButton<TlpLevel>(
|
||||||
tooltip: l10n.d('TLP-classificatie (Traffic Light Protocol)'),
|
tooltip: 'TLP-classificatie (Traffic Light Protocol)',
|
||||||
position: PopupMenuPosition.under,
|
position: PopupMenuPosition.under,
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
itemBuilder: (_) => [
|
itemBuilder: (_) => [
|
||||||
|
|
@ -1699,7 +1453,7 @@ class _TlpChip extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(level == TlpLevel.none ? l10n.d('Geen') : level.label),
|
Text(level.menuLabel),
|
||||||
if (level == tlp) ...[
|
if (level == tlp) ...[
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ 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});
|
||||||
|
|
@ -34,7 +33,6 @@ 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): () =>
|
||||||
|
|
@ -43,7 +41,7 @@ class AddSlideDialog extends StatelessWidget {
|
||||||
child: Focus(
|
child: Focus(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
child: AlertDialog(
|
child: AlertDialog(
|
||||||
title: Text(l10n.d('Slide type kiezen')),
|
title: const Text('Slide type kiezen'),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 400,
|
width: 400,
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
|
|
@ -53,7 +51,7 @@ class AddSlideDialog extends StatelessWidget {
|
||||||
final (type, icon, label) = entry;
|
final (type, icon, label) = entry;
|
||||||
return _TypeCard(
|
return _TypeCard(
|
||||||
icon: icon,
|
icon: icon,
|
||||||
label: l10n.d(label),
|
label: label,
|
||||||
onTap: () => Navigator.pop(context, type),
|
onTap: () => Navigator.pop(context, type),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
|
@ -62,7 +60,7 @@ class AddSlideDialog extends StatelessWidget {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text(l10n.t('cancel')),
|
child: const Text('Annuleren'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ 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).
|
||||||
|
|
@ -80,13 +79,12 @@ 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 ? l10n.t('renderingSlides') : l10n.t('buildingHtml');
|
_phase = needsRaster ? 'Slides renderen…' : 'HTML samenstellen…';
|
||||||
_done = 0;
|
_done = 0;
|
||||||
_total = needsRaster ? widget.slides.length : 0;
|
_total = needsRaster ? widget.slides.length : 0;
|
||||||
});
|
});
|
||||||
|
|
@ -105,7 +103,7 @@ class _ExportDialogState extends State<ExportDialog> {
|
||||||
: const <Uint8List>[];
|
: const <Uint8List>[];
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _phase = '${format.label} ${l10n.t('buildingExport')}');
|
setState(() => _phase = '${format.label} samenstellen…');
|
||||||
|
|
||||||
final r = await widget.exportService.export(
|
final r = await widget.exportService.export(
|
||||||
widget.deckPath,
|
widget.deckPath,
|
||||||
|
|
@ -116,42 +114,37 @@ 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
|
_result = r.success ? 'Geëxporteerd naar:\n${r.outputPath}' : r.error;
|
||||||
? '${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: Text(l10n.t('exportDialogTitle')),
|
title: const Text('Exporteren'),
|
||||||
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: Text(l10n.t('exportAgain')),
|
child: const Text('Nogmaals exporteren'),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _loading ? null : () => Navigator.pop(context),
|
onPressed: _loading ? null : () => Navigator.pop(context),
|
||||||
child: Text(l10n.t('close')),
|
child: const Text('Sluiten'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
||||||
|
|
@ -169,9 +162,7 @@ class _ExportDialogState extends State<ExportDialog> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
_total == 0
|
_total == 0 ? '' : 'Slide $_done van $_total',
|
||||||
? ''
|
|
||||||
: '${l10n.t('slideOf')} $_done ${l10n.t('of')} $_total',
|
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -204,18 +195,19 @@ class _ExportDialogState extends State<ExportDialog> {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
const Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: EdgeInsets.only(bottom: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.t('exportIntro'),
|
'De export gebruikt exact de weergave uit de editor, inclusief je '
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFF64748B)),
|
'stijlprofiel.',
|
||||||
|
style: TextStyle(fontSize: 12, color: Color(0xFF64748B)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
const Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 6),
|
padding: EdgeInsets.only(bottom: 6),
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.t('imageQualityPdf'),
|
'Afbeeldingskwaliteit (PDF)',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF475569),
|
color: Color(0xFF475569),
|
||||||
|
|
@ -223,16 +215,16 @@ class _ExportDialogState extends State<ExportDialog> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SegmentedButton<bool>(
|
SegmentedButton<bool>(
|
||||||
segments: [
|
segments: const [
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
value: false,
|
value: false,
|
||||||
icon: const Icon(Icons.image_outlined),
|
icon: Icon(Icons.image_outlined),
|
||||||
label: Text(l10n.t('normal')),
|
label: Text('Normaal'),
|
||||||
),
|
),
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
value: true,
|
value: true,
|
||||||
icon: const Icon(Icons.compress),
|
icon: Icon(Icons.compress),
|
||||||
label: Text(l10n.t('compressed')),
|
label: Text('Gecomprimeerd'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
selected: {_compress},
|
selected: {_compress},
|
||||||
|
|
@ -243,23 +235,26 @@ 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 ? l10n.t('compressedHelp') : l10n.t('losslessHelp'),
|
_compress
|
||||||
|
? '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: l10n.t('exportAsPdf'),
|
label: 'Exporteer als PDF',
|
||||||
onPressed: () => _export(ExportFormat.pdf, compress: _compress),
|
onPressed: () => _export(ExportFormat.pdf, compress: _compress),
|
||||||
),
|
),
|
||||||
_exportButton(
|
_exportButton(
|
||||||
icon: _formatIcon(ExportFormat.pptx),
|
icon: _formatIcon(ExportFormat.pptx),
|
||||||
label: l10n.t('exportAsPptx'),
|
label: 'Exporteer als ${ExportFormat.pptx.label}',
|
||||||
onPressed: () => _export(ExportFormat.pptx),
|
onPressed: () => _export(ExportFormat.pptx),
|
||||||
),
|
),
|
||||||
_exportButton(
|
_exportButton(
|
||||||
icon: _formatIcon(ExportFormat.html),
|
icon: _formatIcon(ExportFormat.html),
|
||||||
label: l10n.t('exportAsHtml'),
|
label: 'Exporteer als HTML (Marp, offline)',
|
||||||
onPressed: () => _export(ExportFormat.html),
|
onPressed: () => _export(ExportFormat.html),
|
||||||
),
|
),
|
||||||
const Padding(
|
const Padding(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
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);
|
||||||
|
|
@ -79,10 +78,9 @@ 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: Text(l10n.d('Zoeken en vervangen')),
|
title: const Text('Zoeken en vervangen'),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 420,
|
width: 420,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -93,9 +91,9 @@ class _FindReplaceDialogState extends State<FindReplaceDialog> {
|
||||||
controller: _find,
|
controller: _find,
|
||||||
focusNode: _findFocus,
|
focusNode: _findFocus,
|
||||||
onChanged: (_) => _recount(),
|
onChanged: (_) => _recount(),
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: l10n.d('Zoeken naar'),
|
labelText: 'Zoeken naar',
|
||||||
prefixIcon: const Icon(Icons.search, size: 18),
|
prefixIcon: Icon(Icons.search, size: 18),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -103,9 +101,9 @@ class _FindReplaceDialogState extends State<FindReplaceDialog> {
|
||||||
TextField(
|
TextField(
|
||||||
controller: _replace,
|
controller: _replace,
|
||||||
onChanged: (_) => setState(() => _replaced = null),
|
onChanged: (_) => setState(() => _replaced = null),
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: l10n.d('Vervangen door'),
|
labelText: 'Vervangen door',
|
||||||
prefixIcon: const Icon(Icons.edit_outlined, size: 18),
|
prefixIcon: Icon(Icons.edit_outlined, size: 18),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -121,9 +119,9 @@ class _FindReplaceDialogState extends State<FindReplaceDialog> {
|
||||||
_recount();
|
_recount();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Text(
|
const Text(
|
||||||
l10n.d('Hoofdlettergevoelig'),
|
'Hoofdlettergevoelig',
|
||||||
style: const TextStyle(fontSize: 13),
|
style: TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
_statusText(hasQuery),
|
_statusText(hasQuery),
|
||||||
|
|
@ -135,26 +133,25 @@ class _FindReplaceDialogState extends State<FindReplaceDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text(l10n.t('close')),
|
child: const Text('Sluiten'),
|
||||||
),
|
),
|
||||||
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: Text(l10n.d('Vervang alles')),
|
label: const Text('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
|
||||||
? l10n.d('Niets vervangen')
|
? 'Niets vervangen'
|
||||||
: _replaced == 1
|
: _replaced == 1
|
||||||
? '1 ${l10n.d('vervangen')}'
|
? '1 vervangen'
|
||||||
: '$_replaced ${l10n.d('vervangen')}',
|
: '$_replaced vervangen',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Color(0xFF15803D),
|
color: Color(0xFF15803D),
|
||||||
|
|
@ -165,10 +162,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
|
||||||
? l10n.d('Geen resultaten')
|
? 'Geen resultaten'
|
||||||
: _matches == 1
|
: _matches == 1
|
||||||
? '1 ${l10n.d('resultaat')}'
|
? '1 resultaat'
|
||||||
: '$_matches ${l10n.d('resultaten')}',
|
: '$_matches resultaten',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: _matches == 0
|
color: _matches == 0
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ 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 {
|
||||||
|
|
@ -291,7 +290,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: context.l10n.d('Kies een afbeelding'),
|
dialogTitle: '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!;
|
||||||
|
|
@ -379,9 +378,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
const SnackBar(content: Text('Kopiëren naar klembord mislukt.')),
|
||||||
content: Text(context.l10n.d('Kopiëren naar klembord mislukt.')),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -421,9 +418,7 @@ 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) {
|
builder: (ctx) => AlertDialog(
|
||||||
final l10n = ctx.l10n;
|
|
||||||
return AlertDialog(
|
|
||||||
backgroundColor: const Color(0xFF161B22),
|
backgroundColor: const Color(0xFF161B22),
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -437,10 +432,10 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
const Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.d('Afbeelding verwijderen?'),
|
'Afbeelding verwijderen?',
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 16),
|
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -459,18 +454,15 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
if (usages.isEmpty)
|
if (usages.isEmpty)
|
||||||
Text(
|
const Text(
|
||||||
l10n.d(
|
'Het bestand wordt permanent van schijf verwijderd. '
|
||||||
'Het bestand wordt permanent van schijf verwijderd. Deze actie kan niet ongedaan worden gemaakt.',
|
'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(
|
||||||
'${l10n.d('Let op: deze afbeelding wordt nog gebruikt in')} ${usages.length} ${usages.length == 1 ? l10n.d("slide") : l10n.t("slides")}:',
|
'Let op: deze afbeelding wordt nog gebruikt in '
|
||||||
|
'${usages.length} ${usages.length == 1 ? "slide" : "slides"}:',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Color(0xFFF0B429),
|
color: Color(0xFFF0B429),
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|
@ -500,14 +492,10 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
const Text(
|
||||||
l10n.d(
|
'Verwijderen maakt die slides leeg. Dit kan niet ongedaan '
|
||||||
'Verwijderen maakt die slides leeg. Dit kan niet ongedaan worden gemaakt.',
|
'worden gemaakt.',
|
||||||
),
|
style: TextStyle(color: Color(0xFF8B949E), fontSize: 13),
|
||||||
style: const TextStyle(
|
|
||||||
color: Color(0xFF8B949E),
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
@ -518,20 +506,19 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFF8B949E),
|
foregroundColor: const Color(0xFF8B949E),
|
||||||
),
|
),
|
||||||
child: Text(l10n.t('cancel')),
|
child: const Text('Annuleren'),
|
||||||
),
|
),
|
||||||
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: Text(l10n.d('Verwijderen')),
|
label: const Text('Verwijderen'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFFB62324),
|
backgroundColor: const Color(0xFFB62324),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -608,16 +595,15 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLoading() {
|
Widget _buildLoading() {
|
||||||
final l10n = context.l10n;
|
return const Center(
|
||||||
return Center(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const CircularProgressIndicator(color: Color(0xFF3B82F6)),
|
CircularProgressIndicator(color: Color(0xFF3B82F6)),
|
||||||
const SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
l10n.d('Afbeeldingen laden…'),
|
'Afbeeldingen laden…',
|
||||||
style: const TextStyle(color: Color(0xFF8B949E), fontSize: 14),
|
style: TextStyle(color: Color(0xFF8B949E), fontSize: 14),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -625,7 +611,6 @@ 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),
|
||||||
|
|
@ -647,9 +632,9 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 14),
|
const SizedBox(width: 14),
|
||||||
Text(
|
const Text(
|
||||||
l10n.d('Afbeelding kiezen'),
|
'Afbeelding kiezen',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
@ -682,7 +667,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: l10n.d('Sluiten (Esc)'),
|
tooltip: 'Sluiten (Esc)',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -690,7 +675,6 @@ 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(
|
||||||
|
|
@ -698,7 +682,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: l10n.d('Zoek op naam of beschrijving…'),
|
hintText: '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,
|
||||||
|
|
@ -741,7 +725,6 @@ 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(
|
||||||
|
|
@ -775,13 +758,9 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
seg(_ViewMode.grid, Icons.grid_view_rounded, l10n.d('Raster')),
|
seg(_ViewMode.grid, Icons.grid_view_rounded, 'Raster'),
|
||||||
const SizedBox(width: 3),
|
const SizedBox(width: 3),
|
||||||
seg(
|
seg(_ViewMode.cover, Icons.view_carousel_rounded, 'Coverflow'),
|
||||||
_ViewMode.cover,
|
|
||||||
Icons.view_carousel_rounded,
|
|
||||||
l10n.d('Coverflow'),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -789,7 +768,6 @@ 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,
|
||||||
|
|
@ -812,8 +790,8 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text(
|
Text(
|
||||||
filtering
|
filtering
|
||||||
? '${l10n.d('Geen resultaten voor')} "${_query.trim()}"'
|
? 'Geen resultaten voor "${_query.trim()}"'
|
||||||
: l10n.d('Geen afbeeldingen gevonden'),
|
: 'Geen afbeeldingen gevonden',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Color(0xFFCDD9E5),
|
color: Color(0xFFCDD9E5),
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|
@ -823,10 +801,8 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
filtering
|
filtering
|
||||||
? l10n.d('Pas je zoekterm aan of voeg een beschrijving toe.')
|
? 'Pas je zoekterm aan of voeg een beschrijving toe.'
|
||||||
: l10n.d(
|
: 'Gebruik "Bladeren" om afbeeldingen van elke locatie te kiezen.',
|
||||||
'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),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -1285,26 +1261,25 @@ 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
|
||||||
? Center(
|
? const Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.touch_app_outlined,
|
Icons.touch_app_outlined,
|
||||||
size: 40,
|
size: 40,
|
||||||
color: Color(0xFF30363D),
|
color: Color(0xFF30363D),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
l10n.d('Selecteer een\nafbeelding'),
|
'Selecteer een\nafbeelding',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF6E7681),
|
color: Color(0xFF6E7681),
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
|
|
@ -1388,7 +1363,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: l10n.d('Caption / bronvermelding'),
|
hintText: 'Caption / bronvermelding',
|
||||||
hintStyle: const TextStyle(
|
hintStyle: const TextStyle(
|
||||||
color: Color(0xFF6E7681),
|
color: Color(0xFF6E7681),
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -1433,7 +1408,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: l10n.d('Beschrijving (doorzoekbaar)'),
|
hintText: 'Beschrijving (doorzoekbaar)',
|
||||||
hintStyle: const TextStyle(
|
hintStyle: const TextStyle(
|
||||||
color: Color(0xFF6E7681),
|
color: Color(0xFF6E7681),
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -1480,9 +1455,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
_justCopied
|
_justCopied ? 'Gekopieerd' : 'Kopiëren',
|
||||||
? l10n.d('Gekopieerd')
|
|
||||||
: l10n.d('Kopiëren'),
|
|
||||||
),
|
),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: _justCopied
|
foregroundColor: _justCopied
|
||||||
|
|
@ -1504,7 +1477,7 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
Icons.delete_outline,
|
Icons.delete_outline,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
label: Text(l10n.d('Verwijderen')),
|
label: const Text('Verwijderen'),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFFE5746E),
|
foregroundColor: const Color(0xFFE5746E),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
|
@ -1526,7 +1499,6 @@ 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),
|
||||||
|
|
@ -1539,7 +1511,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: Text(l10n.d('Bladeren…')),
|
label: const Text('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)),
|
||||||
|
|
@ -1548,9 +1520,9 @@ class _ImageCarouselPickerState extends State<ImageCarouselPicker> {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
// Hint
|
// Hint
|
||||||
Text(
|
const Text(
|
||||||
l10n.d('↑↓←→ navigeren · Enter kiezen · Dubbelklik selecteert'),
|
'↑↓←→ navigeren · Enter kiezen · Dubbelklik selecteert',
|
||||||
style: const TextStyle(color: Color(0xFF484F58), fontSize: 11),
|
style: TextStyle(color: Color(0xFF484F58), fontSize: 11),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
// Annuleren
|
// Annuleren
|
||||||
|
|
@ -1560,14 +1532,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: Text(l10n.t('cancel')),
|
child: const Text('Annuleren'),
|
||||||
),
|
),
|
||||||
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: Text(l10n.d('Kiezen')),
|
label: const Text('Kiezen'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF238636),
|
backgroundColor: const Color(0xFF238636),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ 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
|
||||||
|
|
@ -75,7 +74,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: context.l10n.d('Map met presentaties kiezen'),
|
dialogTitle: 'Map met presentaties kiezen',
|
||||||
initialDirectory: _directory,
|
initialDirectory: _directory,
|
||||||
);
|
);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
|
|
@ -149,7 +148,6 @@ 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;
|
||||||
|
|
||||||
|
|
@ -158,11 +156,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),
|
||||||
Text(l10n.d('Slides importeren')),
|
const Text('Slides importeren'),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (selectedCount > 0)
|
if (selectedCount > 0)
|
||||||
Text(
|
Text(
|
||||||
'$selectedCount ${l10n.d('geselecteerd')}',
|
'$selectedCount geselecteerd',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppTheme.accent,
|
color: AppTheme.accent,
|
||||||
|
|
@ -187,7 +185,7 @@ class _ImportSlidesDialogState extends State<ImportSlidesDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text(l10n.t('cancel')),
|
child: const Text('Annuleren'),
|
||||||
),
|
),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: selectedCount == 0
|
onPressed: selectedCount == 0
|
||||||
|
|
@ -195,9 +193,7 @@ 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
|
selectedCount == 0 ? 'Importeren' : 'Importeren ($selectedCount)',
|
||||||
? l10n.d('Importeren')
|
|
||||||
: '${l10n.d('Importeren')} ($selectedCount)',
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -205,30 +201,27 @@ 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: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
prefixIcon: const Icon(Icons.search, size: 18),
|
prefixIcon: Icon(Icons.search, size: 18),
|
||||||
hintText: l10n.d('Zoek op presentatie, titel of tekst…'),
|
hintText: '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 ?? l10n.d('Geen map gekozen'),
|
message: _directory ?? '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
|
_directory == null ? 'Map kiezen' : p.basename(_directory!),
|
||||||
? l10n.d('Map kiezen')
|
|
||||||
: p.basename(_directory!),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -238,26 +231,25 @@ 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,
|
||||||
l10n.d('Kies een map met presentaties om te beginnen.'),
|
'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,
|
||||||
l10n.d('Geen andere presentaties (.md) in deze map gevonden.'),
|
'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,
|
||||||
'${l10n.d('Geen slides gevonden voor')} "$_query".',
|
'Geen slides gevonden voor "$_query".',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,7 +315,6 @@ 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;
|
||||||
|
|
@ -374,9 +365,7 @@ class _PresentationSection extends StatelessWidget {
|
||||||
textStyle: const TextStyle(fontSize: 11),
|
textStyle: const TextStyle(fontSize: 11),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
allSelected
|
allSelected ? 'Deselecteer alles' : 'Selecteer alles',
|
||||||
? l10n.d('Deselecteer alles')
|
|
||||||
: l10n.d('Selecteer alles'),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
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});
|
||||||
|
|
@ -29,14 +28,13 @@ 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: Text(l10n.d('Nieuwe presentatie')),
|
title: const Text('Nieuwe presentatie'),
|
||||||
content: Form(
|
content: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
|
|
@ -44,13 +42,12 @@ class _NewDeckDialogState extends State<NewDeckDialog> {
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _ctrl,
|
controller: _ctrl,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: l10n.d('Titel'),
|
labelText: 'Titel',
|
||||||
hintText: l10n.d('Bijv. Kwartaalupdate Q4'),
|
hintText: 'Bijv. Kwartaalupdate Q4',
|
||||||
),
|
),
|
||||||
validator: (v) => (v == null || v.trim().isEmpty)
|
validator: (v) =>
|
||||||
? l10n.d('Vul een titel in')
|
(v == null || v.trim().isEmpty) ? 'Vul een titel in' : null,
|
||||||
: null,
|
|
||||||
onFieldSubmitted: (_) => _submit(),
|
onFieldSubmitted: (_) => _submit(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -58,9 +55,9 @@ class _NewDeckDialogState extends State<NewDeckDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text(l10n.t('cancel')),
|
child: const Text('Annuleren'),
|
||||||
),
|
),
|
||||||
ElevatedButton(onPressed: _submit, child: Text(l10n.d('Aanmaken'))),
|
ElevatedButton(onPressed: _submit, child: const Text('Aanmaken')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ 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).
|
||||||
|
|
@ -72,7 +71,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: context.l10n.d('Map met presentaties kiezen'),
|
dialogTitle: 'Map met presentaties kiezen',
|
||||||
initialDirectory: _directory,
|
initialDirectory: _directory,
|
||||||
);
|
);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
|
|
@ -159,15 +158,14 @@ 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: [
|
children: const [
|
||||||
const Icon(Icons.folder_open_outlined, size: 20),
|
Icon(Icons.folder_open_outlined, size: 20),
|
||||||
const SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(l10n.d('Presentatie openen')),
|
Text('Presentatie openen'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.fromLTRB(24, 12, 24, 0),
|
contentPadding: const EdgeInsets.fromLTRB(24, 12, 24, 0),
|
||||||
|
|
@ -187,11 +185,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: Text(l10n.d('Bladeren…')),
|
label: const Text('Bladeren…'),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text(l10n.t('cancel')),
|
child: const Text('Annuleren'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
// Knoppen uit elkaar: Bladeren links, Annuleren rechts. (Geen Spacer in
|
// Knoppen uit elkaar: Bladeren links, Annuleren rechts. (Geen Spacer in
|
||||||
|
|
@ -201,32 +199,27 @@ 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: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
prefixIcon: const Icon(Icons.search, size: 18),
|
prefixIcon: Icon(Icons.search, size: 18),
|
||||||
hintText: l10n.d(
|
hintText: 'Zoek op bestandsnaam, titel of tekst in de slides…',
|
||||||
'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 ?? l10n.d('Geen map gekozen'),
|
message: _directory ?? '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
|
_directory == null ? 'Map kiezen' : p.basename(_directory!),
|
||||||
? l10n.d('Map kiezen')
|
|
||||||
: p.basename(_directory!),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -236,26 +229,25 @@ 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,
|
||||||
l10n.d('Kies een map met presentaties om te beginnen.'),
|
'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,
|
||||||
l10n.d('Geen presentaties (.md) in deze map gevonden.'),
|
'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,
|
||||||
'${l10n.d('Geen presentaties gevonden voor')} "$_query".',
|
'Geen presentaties gevonden voor "$_query".',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,7 +308,6 @@ 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;
|
||||||
|
|
||||||
|
|
@ -352,7 +343,7 @@ class _PresentationRow extends StatelessWidget {
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${presentation.fileName} · ${deck.slides.length} ${l10n.t('slides')}',
|
'${presentation.fileName} · ${deck.slides.length} slides',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Color(0xFF94A3B8),
|
color: Color(0xFF94A3B8),
|
||||||
|
|
@ -387,7 +378,7 @@ class _PresentationRow extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${l10n.d('Slide')} ${hit.index + 1}',
|
'Slide ${hit.index + 1}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
@ -414,7 +405,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} ${l10n.d('meer treffer(s)')}',
|
'+ ${hits.length - 4} meer treffer(s)',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Color(0xFF94A3B8),
|
color: Color(0xFF94A3B8),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
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 {
|
||||||
|
|
@ -87,7 +86,6 @@ 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): () =>
|
||||||
|
|
@ -95,10 +93,10 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
||||||
},
|
},
|
||||||
child: AlertDialog(
|
child: AlertDialog(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: const [
|
||||||
const Icon(Icons.info_outline, size: 20),
|
Icon(Icons.info_outline, size: 20),
|
||||||
const SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(l10n.d('Presentatie-eigenschappen')),
|
Text('Presentatie-eigenschappen'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
|
|
@ -162,14 +160,10 @@ 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),
|
||||||
Text(
|
const Text(
|
||||||
l10n.d(
|
'Deze gegevens worden in de markdown opgeslagen en zijn '
|
||||||
'Deze gegevens worden in de markdown opgeslagen en zijn doorzoekbaar bij het openen.',
|
'doorzoekbaar bij het openen.',
|
||||||
),
|
style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Color(0xFF94A3B8),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -178,9 +172,9 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text(l10n.t('cancel')),
|
child: const Text('Annuleren'),
|
||||||
),
|
),
|
||||||
ElevatedButton(onPressed: _save, child: Text(l10n.t('save'))),
|
ElevatedButton(onPressed: _save, child: const Text('Opslaan')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -192,13 +186,12 @@ 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: l10n.d(label),
|
labelText: label,
|
||||||
hintText: l10n.d(hint),
|
hintText: hint,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ 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);
|
||||||
|
|
@ -94,7 +93,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: context.l10n.d('Standaard map voor presentaties'),
|
dialogTitle: 'Standaard map voor presentaties',
|
||||||
initialDirectory: _homeDirectory,
|
initialDirectory: _homeDirectory,
|
||||||
);
|
);
|
||||||
if (result != null) setState(() => _homeDirectory = result);
|
if (result != null) setState(() => _homeDirectory = result);
|
||||||
|
|
@ -102,7 +101,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: context.l10n.d('Map voor exports'),
|
dialogTitle: 'Map voor exports',
|
||||||
initialDirectory: _exportDirectory ?? _homeDirectory,
|
initialDirectory: _exportDirectory ?? _homeDirectory,
|
||||||
);
|
);
|
||||||
if (result != null) setState(() => _exportDirectory = result);
|
if (result != null) setState(() => _exportDirectory = result);
|
||||||
|
|
@ -110,7 +109,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: context.l10n.d('Logo kiezen'),
|
dialogTitle: 'Logo kiezen',
|
||||||
type: FileType.image,
|
type: FileType.image,
|
||||||
);
|
);
|
||||||
final path = result?.files.single.path;
|
final path = result?.files.single.path;
|
||||||
|
|
@ -159,7 +158,6 @@ 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
|
||||||
|
|
@ -168,7 +166,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: 3,
|
length: 3,
|
||||||
child: AlertDialog(
|
child: AlertDialog(
|
||||||
title: Text(l10n.t('settings')),
|
title: const Text('Instellingen'),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 520,
|
width: 520,
|
||||||
height: 560,
|
height: 560,
|
||||||
|
|
@ -179,20 +177,11 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_profileNameField(),
|
_profileNameField(),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TabBar(
|
const TabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(
|
Tab(icon: Icon(Icons.tune), text: 'Algemeen'),
|
||||||
icon: const Icon(Icons.tune),
|
Tab(icon: Icon(Icons.palette_outlined), text: 'Kleuren'),
|
||||||
text: l10n.t('settingsGeneral'),
|
Tab(icon: Icon(Icons.image_outlined), text: 'Logo'),
|
||||||
),
|
|
||||||
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),
|
||||||
|
|
@ -211,24 +200,23 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text(l10n.t('cancel')),
|
child: const Text('Annuleren'),
|
||||||
),
|
),
|
||||||
ElevatedButton(onPressed: _save, child: Text(l10n.t('saveSettings'))),
|
ElevatedButton(onPressed: _save, child: const Text('Opslaan')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _profileNameField() {
|
Widget _profileNameField() {
|
||||||
final l10n = context.l10n;
|
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: _profileName,
|
controller: _profileName,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: l10n.d('Profielnaam'),
|
labelText: 'Profielnaam',
|
||||||
hintText: l10n.d('Naam van het stijlprofiel'),
|
hintText: 'Naam van het stijlprofiel',
|
||||||
isDense: true,
|
isDense: true,
|
||||||
prefixIcon: const Icon(Icons.badge_outlined, size: 18),
|
prefixIcon: Icon(Icons.badge_outlined, size: 18),
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
final name = value.trim();
|
final name = value.trim();
|
||||||
|
|
@ -241,13 +229,12 @@ 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: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: l10n.d('Stijlprofiel'),
|
labelText: 'Stijlprofiel',
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
child: DropdownButtonHideUnderline(
|
child: DropdownButtonHideUnderline(
|
||||||
|
|
@ -271,17 +258,17 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: l10n.d('Nieuw profiel'),
|
tooltip: 'Nieuw profiel',
|
||||||
onPressed: _createProfile,
|
onPressed: _createProfile,
|
||||||
icon: const Icon(Icons.add, size: 18),
|
icon: const Icon(Icons.add, size: 18),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: l10n.d('Standaardprofiel laden'),
|
tooltip: '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: l10n.d('Profiel verwijderen'),
|
tooltip: 'Profiel verwijderen',
|
||||||
onPressed: profiles.length <= 1
|
onPressed: profiles.length <= 1
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
|
|
@ -341,50 +328,15 @@ 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(l10n.t('language')),
|
_sectionTitle('Presentatiemap'),
|
||||||
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 ?? l10n.t('notSet'),
|
_homeDirectory ?? 'Niet ingesteld',
|
||||||
muted: _homeDirectory == null,
|
muted: _homeDirectory == null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -392,23 +344,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: Text(l10n.t('choose')),
|
label: const Text('Kiezen'),
|
||||||
),
|
),
|
||||||
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: l10n.t('removeDefaultFolder'),
|
tooltip: 'Verwijder standaard map',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_sectionTitle(l10n.t('exportFolderSetting')),
|
_sectionTitle('Exportmap'),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _pathBox(
|
child: _pathBox(
|
||||||
_exportDirectory ?? l10n.t('nextToPresentationFile'),
|
_exportDirectory ?? 'Naast het presentatiebestand',
|
||||||
muted: _exportDirectory == null,
|
muted: _exportDirectory == null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -416,21 +368,22 @@ 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: Text(l10n.t('choose')),
|
label: const Text('Kiezen'),
|
||||||
),
|
),
|
||||||
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: l10n.t('removeExportFolder'),
|
tooltip: 'Verwijder exportmap',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Padding(
|
const Padding(
|
||||||
padding: const EdgeInsets.only(top: 6),
|
padding: EdgeInsets.only(top: 6),
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.t('exportFolderHelp'),
|
'Alle exports (PDF/PPTX) worden hier opgeslagen. Niet ingesteld? '
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
'Dan komt de export naast het presentatiebestand te staan.',
|
||||||
|
style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -493,61 +446,60 @@ 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(l10n.d('Lettertype')),
|
_sectionTitle('Lettertype'),
|
||||||
_fontSection(),
|
_fontSection(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_sectionTitle(l10n.d('Kleuren')),
|
_sectionTitle('Kleuren'),
|
||||||
_colorSetting(
|
_colorSetting(
|
||||||
l10n.d('Achtergrond slides'),
|
'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(
|
||||||
l10n.d('Tekst'),
|
'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(
|
||||||
l10n.d('Accent / bullets'),
|
'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(
|
||||||
l10n.d('Tabeltekst'),
|
'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(
|
||||||
l10n.d('Tabel koptekst'),
|
'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(
|
||||||
l10n.d('Titelachtergrond'),
|
'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(
|
||||||
l10n.d('Titeltekst'),
|
'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(
|
||||||
l10n.d('Sectieachtergrond'),
|
'Sectieachtergrond',
|
||||||
_themeProfile.sectionBackgroundColor,
|
_themeProfile.sectionBackgroundColor,
|
||||||
(v) =>
|
(v) =>
|
||||||
_themeProfile = _themeProfile.copyWith(sectionBackgroundColor: v),
|
_themeProfile = _themeProfile.copyWith(sectionBackgroundColor: v),
|
||||||
|
|
@ -559,16 +511,15 @@ 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(l10n.d('Logo')),
|
_sectionTitle('Logo'),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _pathBox(
|
child: _pathBox(
|
||||||
_themeProfile.logoPath ?? l10n.d('Geen logo ingesteld'),
|
_themeProfile.logoPath ?? 'Geen logo ingesteld',
|
||||||
muted: _themeProfile.logoPath == null,
|
muted: _themeProfile.logoPath == null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -576,7 +527,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: Text(l10n.d('Kiezen')),
|
label: const Text('Kiezen'),
|
||||||
),
|
),
|
||||||
if (_themeProfile.logoPath != null)
|
if (_themeProfile.logoPath != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
@ -585,34 +536,22 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
_profileTouched = true;
|
_profileTouched = true;
|
||||||
}),
|
}),
|
||||||
icon: const Icon(Icons.clear, size: 18),
|
icon: const Icon(Icons.clear, size: 18),
|
||||||
tooltip: l10n.d('Verwijder logo'),
|
tooltip: 'Verwijder logo',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
initialValue: _themeProfile.logoPosition,
|
initialValue: _themeProfile.logoPosition,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: l10n.d('Logo positie'),
|
labelText: 'Logo positie',
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
items: [
|
items: const [
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(value: 'top-left', child: Text('Linksboven')),
|
||||||
value: 'top-left',
|
DropdownMenuItem(value: 'top-right', child: Text('Rechtsboven')),
|
||||||
child: Text(l10n.d('Linksboven')),
|
DropdownMenuItem(value: 'bottom-left', child: Text('Linksonder')),
|
||||||
),
|
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) {
|
||||||
|
|
@ -628,7 +567,10 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
width: 160,
|
width: 160,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _logoSize,
|
controller: _logoSize,
|
||||||
decoration: InputDecoration(labelText: 'Logo px', isDense: true),
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Logo px',
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
onChanged: (_) => _profileTouched = true,
|
onChanged: (_) => _profileTouched = true,
|
||||||
|
|
@ -638,31 +580,30 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
_sectionTitle('Footer'),
|
_sectionTitle('Footer'),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _footerText,
|
controller: _footerText,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: l10n.d('Footertekst'),
|
labelText: 'Footertekst',
|
||||||
hintText: l10n.d('bijv. Vertrouwelijk · {title} · {date}'),
|
hintText: 'bijv. Vertrouwelijk · {title} · {date}',
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
onChanged: (_) => _profileTouched = true,
|
onChanged: (_) => _profileTouched = true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
const Text(
|
||||||
l10n.d(
|
'Tokens: {page}, {total}, {date}, {title}. Footer verschijnt op alle '
|
||||||
'Tokens: {page}, {total}, {date}, {title}. Footer verschijnt op alle slides behalve titel- en sectieslides, tenzij je hem per slide uitzet.',
|
'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: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: l10n.d('Footerpositie'),
|
labelText: 'Footerpositie',
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
items: [
|
items: const [
|
||||||
DropdownMenuItem(value: 'left', child: Text(l10n.d('Links'))),
|
DropdownMenuItem(value: 'left', child: Text('Links')),
|
||||||
DropdownMenuItem(value: 'center', child: Text(l10n.d('Midden'))),
|
DropdownMenuItem(value: 'center', child: Text('Midden')),
|
||||||
DropdownMenuItem(value: 'right', child: Text(l10n.d('Rechts'))),
|
DropdownMenuItem(value: 'right', child: Text('Rechts')),
|
||||||
],
|
],
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
if (v != null) {
|
if (v != null) {
|
||||||
|
|
@ -682,9 +623,9 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
);
|
);
|
||||||
_profileTouched = true;
|
_profileTouched = true;
|
||||||
}),
|
}),
|
||||||
title: Text(
|
title: const Text(
|
||||||
l10n.d('Paginanummers tonen (rechtsonder)'),
|
'Paginanummers tonen (rechtsonder)',
|
||||||
style: const TextStyle(fontSize: 13),
|
style: TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
|
|
@ -747,7 +688,6 @@ 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),
|
||||||
|
|
@ -759,7 +699,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
l10n.d('Voorvertoning'),
|
'Voorvertoning',
|
||||||
style: _fontStyle(
|
style: _fontStyle(
|
||||||
_themeProfile.fontFamily,
|
_themeProfile.fontFamily,
|
||||||
TextStyle(
|
TextStyle(
|
||||||
|
|
@ -771,7 +711,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
l10n.d('De snelle bruine vos springt over de luie hond.'),
|
'De snelle bruine vos springt over de luie hond.',
|
||||||
style: _fontStyle(
|
style: _fontStyle(
|
||||||
_themeProfile.fontFamily,
|
_themeProfile.fontFamily,
|
||||||
TextStyle(
|
TextStyle(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ 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.
|
||||||
|
|
@ -93,7 +92,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: context.l10n.d('Map met presentaties kiezen'),
|
dialogTitle: 'Map met presentaties kiezen',
|
||||||
initialDirectory: _directory,
|
initialDirectory: _directory,
|
||||||
);
|
);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
|
|
@ -161,7 +160,6 @@ 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(
|
||||||
|
|
@ -169,11 +167,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),
|
||||||
Text(l10n.d('Slide zoeken')),
|
const Text('Slide zoeken'),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (_addedCount > 0)
|
if (_addedCount > 0)
|
||||||
Text(
|
Text(
|
||||||
'$_addedCount ${l10n.d('toegevoegd')}',
|
'$_addedCount toegevoegd',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppTheme.accent,
|
color: AppTheme.accent,
|
||||||
|
|
@ -198,39 +196,34 @@ class _SlideFinderDialogState extends State<SlideFinderDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text(l10n.d('Klaar')),
|
child: const Text('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: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
prefixIcon: const Icon(Icons.search, size: 18),
|
prefixIcon: Icon(Icons.search, size: 18),
|
||||||
hintText: l10n.d(
|
hintText: 'Zoek slides op tekst, titel, onderschrift, pad…',
|
||||||
'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 ?? l10n.d('Geen map gekozen'),
|
message: _directory ?? '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
|
_directory == null ? 'Map kiezen' : p.basename(_directory!),
|
||||||
? l10n.d('Map kiezen')
|
|
||||||
: p.basename(_directory!),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -240,26 +233,25 @@ 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,
|
||||||
l10n.d('Kies een map met presentaties om te beginnen.'),
|
'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,
|
||||||
l10n.d('Typ zoektermen om slides uit al je presentaties te vinden.'),
|
'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,
|
||||||
'${l10n.d('Geen slides gevonden voor')} "${_query.trim()}".',
|
'Geen slides gevonden voor "${_query.trim()}".',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,8 +262,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
|
||||||
? '${l10n.d('Eerste')} $_maxResults ${l10n.d('treffers — verfijn je zoekopdracht')}'
|
? 'Eerste $_maxResults treffers — verfijn je zoekopdracht'
|
||||||
: '${hits.length} ${l10n.d('treffer(s)')}',
|
: '${hits.length} treffer(s)',
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -328,7 +320,6 @@ 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;
|
||||||
|
|
||||||
|
|
@ -360,7 +351,7 @@ class _SlideHitCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'$sourceName · ${l10n.d('slide')} ${hit.slideIndex + 1}',
|
'$sourceName · 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,
|
||||||
|
|
@ -372,7 +363,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: Text(l10n.d('Toegevoegd')),
|
label: const Text('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),
|
||||||
|
|
@ -383,7 +374,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: Text(l10n.d('Toevoegen')),
|
label: const Text('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,7 +5,6 @@ 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.
|
||||||
|
|
@ -26,12 +25,11 @@ 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(
|
||||||
l10n.d(label),
|
label,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
@ -43,9 +41,7 @@ class EditorField extends StatelessWidget {
|
||||||
controller: controller,
|
controller: controller,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(hintText: hint),
|
||||||
hintText: hint.isEmpty ? '' : l10n.d(hint),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -91,20 +87,18 @@ 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 _label(BuildContext context) {
|
String get _label {
|
||||||
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 l10n.d('Volledig zichtbaar (100%)');
|
if (v == 100) return 'Volledig zichtbaar (100%)';
|
||||||
if (v > 100) {
|
if (v > 100) {
|
||||||
return '${l10n.d('Ingezoomd')} $v% — ${((1 / (v / 100)) * 100).round()}% ${l10n.d('van de foto zichtbaar')}';
|
return 'Ingezoomd $v% — ${((1 / (v / 100)) * 100).round()}% van de foto zichtbaar';
|
||||||
}
|
}
|
||||||
return '${l10n.d('Uitgezoomd')} $v%';
|
return '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(
|
||||||
|
|
@ -112,13 +106,9 @@ class ImageZoomControl extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Tooltip(
|
const Tooltip(
|
||||||
message: l10n.d('Uitzoomen (meer van de foto zichtbaar)'),
|
message: 'Uitzoomen (meer van de foto zichtbaar)',
|
||||||
child: const Icon(
|
child: Icon(Icons.zoom_out, size: 16, color: Color(0xFF94A3B8)),
|
||||||
Icons.zoom_out,
|
|
||||||
size: 16,
|
|
||||||
color: Color(0xFF94A3B8),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Slider(
|
child: Slider(
|
||||||
|
|
@ -126,7 +116,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(context),
|
label: _label,
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
final snapped = ((v.round() / step).round() * step).clamp(
|
final snapped = ((v.round() / step).round() * step).clamp(
|
||||||
minValue,
|
minValue,
|
||||||
|
|
@ -136,13 +126,9 @@ class ImageZoomControl extends StatelessWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Tooltip(
|
const Tooltip(
|
||||||
message: l10n.d('Inzoomen (minder van de foto zichtbaar)'),
|
message: 'Inzoomen (minder van de foto zichtbaar)',
|
||||||
child: const Icon(
|
child: Icon(Icons.zoom_in, size: 16, color: Color(0xFF94A3B8)),
|
||||||
Icons.zoom_in,
|
|
||||||
size: 16,
|
|
||||||
color: Color(0xFF94A3B8),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|
@ -161,7 +147,7 @@ class ImageZoomControl extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.d('Terugzetten (volledige afbeelding zichtbaar)'),
|
message: '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,
|
||||||
|
|
@ -175,7 +161,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(context),
|
_label,
|
||||||
style: const TextStyle(fontSize: 10, color: Color(0xFF94A3B8)),
|
style: const TextStyle(fontSize: 10, color: Color(0xFF94A3B8)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -264,7 +250,6 @@ 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,
|
||||||
|
|
@ -278,7 +263,7 @@ class ImagePickerBar extends ConsumerWidget {
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
imagePath.isEmpty ? l10n.d(label) : imagePath,
|
imagePath.isEmpty ? label : imagePath,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: imagePath.isEmpty
|
color: imagePath.isEmpty
|
||||||
|
|
@ -298,17 +283,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: Text(l10n.d('Uit bibliotheek…')),
|
label: const Text('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: Text(l10n.d('Van computer…')),
|
label: const Text('Van computer…'),
|
||||||
),
|
),
|
||||||
if (onPaste != null)
|
if (onPaste != null)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.d('Afbeelding plakken uit klembord'),
|
message: '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),
|
||||||
|
|
@ -317,7 +302,7 @@ class ImagePickerBar extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
if (imagePath.isNotEmpty)
|
if (imagePath.isNotEmpty)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.d('Kopieer afbeelding naar klembord'),
|
message: 'Kopieer afbeelding naar klembord',
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final ok = await ImageService().copyImageToClipboard(
|
final ok = await ImageService().copyImageToClipboard(
|
||||||
|
|
@ -328,8 +313,8 @@ class ImagePickerBar extends ConsumerWidget {
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
ok
|
ok
|
||||||
? l10n.d('Afbeelding gekopieerd naar klembord.')
|
? 'Afbeelding gekopieerd naar klembord.'
|
||||||
: l10n.d('Kopiëren naar klembord mislukt.'),
|
: 'Kopiëren naar klembord mislukt.',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -341,7 +326,7 @@ class ImagePickerBar extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
if (onClear != null && imagePath.isNotEmpty)
|
if (onClear != null && imagePath.isNotEmpty)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.d('Verwijder afbeelding'),
|
message: 'Verwijder afbeelding',
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: onClear,
|
onPressed: onClear,
|
||||||
icon: const Icon(Icons.clear, size: 18),
|
icon: const Icon(Icons.clear, size: 18),
|
||||||
|
|
@ -438,11 +423,10 @@ 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: l10n.d('Caption / bronvermelding (bijv. © Naam Fotograaf)'),
|
hintText: '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,
|
||||||
|
|
@ -473,11 +457,10 @@ 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(
|
||||||
l10n.d(text),
|
text,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
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 {
|
||||||
|
|
@ -23,7 +22,6 @@ 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: [
|
||||||
|
|
@ -43,7 +41,7 @@ class AudioAttachmentEditor extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
slide.audioPath.isEmpty
|
slide.audioPath.isEmpty
|
||||||
? l10n.d('Geen audiobestand gekozen')
|
? 'Geen audiobestand gekozen'
|
||||||
: slide.audioPath,
|
: slide.audioPath,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -59,7 +57,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: Text(l10n.d('Kiezen')),
|
label: const Text('Kiezen'),
|
||||||
),
|
),
|
||||||
if (slide.audioPath.isNotEmpty)
|
if (slide.audioPath.isNotEmpty)
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
@ -67,7 +65,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: l10n.d('Audio verwijderen'),
|
tooltip: 'Audio verwijderen',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -79,7 +77,7 @@ class AudioAttachmentEditor extends StatelessWidget {
|
||||||
? null
|
? null
|
||||||
: (value) =>
|
: (value) =>
|
||||||
onUpdate(slide.copyWith(audioAutoplay: value ?? false)),
|
onUpdate(slide.copyWith(audioAutoplay: value ?? false)),
|
||||||
title: Text(l10n.d('Audio automatisch afspelen')),
|
title: const Text('Audio automatisch afspelen'),
|
||||||
dense: true,
|
dense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
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 {
|
||||||
|
|
@ -162,7 +161,6 @@ 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: [
|
||||||
|
|
@ -184,7 +182,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: Text(l10n.d('Bullet toevoegen')),
|
label: const Text('Bullet toevoegen'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -192,7 +190,6 @@ 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]),
|
||||||
|
|
@ -253,7 +250,7 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
controller: _bullets[i],
|
controller: _bullets[i],
|
||||||
focusNode: _focusNodes[i],
|
focusNode: _focusNodes[i],
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '${l10n.d('Bullet')} ${i + 1}',
|
hintText: 'Bullet ${i + 1}',
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -268,7 +265,7 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
onPressed: _bullets.length > 1
|
onPressed: _bullets.length > 1
|
||||||
? () => _removeBulletAndFocus(i)
|
? () => _removeBulletAndFocus(i)
|
||||||
: null,
|
: null,
|
||||||
tooltip: l10n.d('Verwijder'),
|
tooltip: 'Verwijder',
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
constraints: const BoxConstraints(minWidth: 28),
|
constraints: const BoxConstraints(minWidth: 28),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ 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 {
|
||||||
|
|
@ -176,7 +175,6 @@ 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(
|
||||||
|
|
@ -199,7 +197,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: Text(l10n.d('Bullet toevoegen')),
|
label: const Text('Bullet toevoegen'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -237,7 +235,6 @@ 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]),
|
||||||
|
|
@ -294,7 +291,7 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
||||||
controller: _bullets[i],
|
controller: _bullets[i],
|
||||||
focusNode: _focusNodes[i],
|
focusNode: _focusNodes[i],
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: '${l10n.d('Bullet')} ${i + 1}',
|
hintText: 'Bullet ${i + 1}',
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
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;
|
||||||
|
|
@ -38,15 +37,14 @@ 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: [
|
||||||
Text(
|
const Text(
|
||||||
l10n.d('Markdown inhoud'),
|
'Markdown inhoud',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF64748B),
|
color: Color(0xFF64748B),
|
||||||
|
|
@ -60,8 +58,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: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: l10n.d('# Slide\n\nInhoud hier...'),
|
hintText: '# Slide\n\nInhoud hier...',
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ 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 {
|
||||||
|
|
@ -71,7 +70,6 @@ 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(
|
||||||
|
|
@ -95,11 +93,10 @@ 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),
|
||||||
Text(
|
const Text(
|
||||||
l10n.d(
|
'De afbeelding wordt schermvullend als achtergrond getoond '
|
||||||
'De afbeelding wordt schermvullend als achtergrond getoond met verminderde opaciteit zodat de tekst leesbaar blijft.',
|
'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,5 @@
|
||||||
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
|
||||||
|
|
@ -121,18 +120,17 @@ 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'),
|
||||||
Padding(
|
const Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 6),
|
padding: EdgeInsets.only(bottom: 6),
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.d('Tip: druk op Enter binnen een cel voor een nieuwe regel.'),
|
'Tip: druk op Enter binnen een cel voor een nieuwe regel.',
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildColumnControls(),
|
_buildColumnControls(),
|
||||||
|
|
@ -143,13 +141,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: Text(l10n.d('Rij toevoegen')),
|
label: const Text('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: Text(l10n.d('Kolom toevoegen')),
|
label: const Text('Kolom toevoegen'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -158,7 +156,6 @@ 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(
|
||||||
|
|
@ -173,8 +170,7 @@ class _TableEditorState extends State<TableEditor> {
|
||||||
color: Color(0xFF94A3B8),
|
color: Color(0xFF94A3B8),
|
||||||
),
|
),
|
||||||
onPressed: _colCount > 1 ? () => _removeColumn(c) : null,
|
onPressed: _colCount > 1 ? () => _removeColumn(c) : null,
|
||||||
tooltip:
|
tooltip: 'Kolom ${c + 1} verwijderen',
|
||||||
'${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(
|
||||||
|
|
@ -191,7 +187,6 @@ 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),
|
||||||
|
|
@ -219,7 +214,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 ? '${l10n.d('Kolom')} ${c + 1}' : null,
|
hintText: isHeader ? 'Kolom ${c + 1}' : null,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8,
|
horizontal: 8,
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
|
|
@ -239,9 +234,7 @@ 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
|
tooltip: isHeader ? 'Koprij verwijderen' : 'Rij verwijderen',
|
||||||
? 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,7 +2,6 @@ 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 {
|
||||||
|
|
@ -71,7 +70,6 @@ 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(
|
||||||
|
|
@ -95,11 +93,10 @@ 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),
|
||||||
Text(
|
const Text(
|
||||||
l10n.d(
|
'De afbeelding wordt schermvullend als achtergrond getoond '
|
||||||
'De afbeelding wordt schermvullend als achtergrond getoond met verminderde opaciteit zodat de tekst leesbaar blijft.',
|
'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,7 +1,6 @@
|
||||||
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);
|
||||||
|
|
@ -218,7 +217,6 @@ 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: [
|
||||||
|
|
@ -230,14 +228,13 @@ 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: Text(l10n.d('Bullet toevoegen')),
|
label: const Text('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]),
|
||||||
|
|
@ -287,7 +284,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: '${l10n.d('Bullet')} ${i + 1}',
|
hintText: 'Bullet ${i + 1}',
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -302,7 +299,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: l10n.d('Verwijder'),
|
tooltip: 'Verwijder',
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
constraints: const BoxConstraints(minWidth: 28),
|
constraints: const BoxConstraints(minWidth: 28),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
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';
|
||||||
|
|
||||||
|
|
@ -48,7 +47,6 @@ 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: [
|
||||||
|
|
@ -67,7 +65,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: Text(l10n.d('Kiezen')),
|
label: const Text('Kiezen'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -79,7 +77,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: Text(l10n.d('Video automatisch afspelen')),
|
title: const Text('Video automatisch afspelen'),
|
||||||
dense: true,
|
dense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
|
@ -103,7 +101,6 @@ 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(
|
||||||
|
|
@ -112,7 +109,7 @@ class _PathBox extends StatelessWidget {
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
path.isEmpty ? l10n.d('Geen video gekozen') : path,
|
path.isEmpty ? 'Geen video gekozen' : path,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: path.isEmpty
|
color: path.isEmpty
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ 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';
|
||||||
|
|
@ -325,7 +324,6 @@ 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>[
|
||||||
|
|
@ -367,7 +365,7 @@ class _EditorToolbar extends StatelessWidget {
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.d(type.label),
|
type.label,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -437,8 +435,7 @@ 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:
|
message: "Terug naar standaardstijl '${defaultProfile.name}'",
|
||||||
'${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),
|
||||||
|
|
@ -463,11 +460,10 @@ 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(
|
||||||
l10n.d(label),
|
label,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
|
|
@ -506,7 +502,6 @@ 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;
|
||||||
|
|
||||||
|
|
@ -524,9 +519,9 @@ class _SlideTimingControl extends StatelessWidget {
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
const Text(
|
||||||
l10n.d('Automatisch doorgaan na'),
|
'Automatisch doorgaan na',
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFF0369A1)),
|
style: TextStyle(fontSize: 12, color: Color(0xFF0369A1)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
// Minus knop
|
// Minus knop
|
||||||
|
|
@ -583,7 +578,6 @@ 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),
|
||||||
|
|
@ -602,9 +596,9 @@ class _SlideLogoControl extends StatelessWidget {
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
const Text(
|
||||||
l10n.d('Logo tonen op deze slide'),
|
'Logo tonen op deze slide',
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFF475569)),
|
style: TextStyle(fontSize: 12, color: Color(0xFF475569)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -621,7 +615,6 @@ 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),
|
||||||
|
|
@ -640,9 +633,9 @@ class _SlideFooterControl extends StatelessWidget {
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
const Text(
|
||||||
l10n.d('Footer tonen op deze slide'),
|
'Footer tonen op deze slide',
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFF475569)),
|
style: TextStyle(fontSize: 12, color: Color(0xFF475569)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -691,7 +684,6 @@ 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),
|
||||||
|
|
@ -709,15 +701,12 @@ class _NotesFieldState extends State<_NotesField> {
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: l10n.d('Sprekersnotities...'),
|
hintText: 'Sprekersnotities...',
|
||||||
hintStyle: const TextStyle(
|
hintStyle: TextStyle(fontSize: 12, color: Color(0xFFD97706)),
|
||||||
fontSize: 12,
|
|
||||||
color: Color(0xFFD97706),
|
|
||||||
),
|
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
contentPadding: EdgeInsets.symmetric(vertical: 8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -764,7 +753,6 @@ 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: [
|
||||||
|
|
@ -776,15 +764,10 @@ 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),
|
||||||
Expanded(
|
const 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(
|
||||||
|
|
@ -792,11 +775,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: Text(l10n.d('Toepassen')),
|
child: const Text('Toepassen'),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: widget.onExitMarkdown,
|
onPressed: widget.onExitMarkdown,
|
||||||
child: Text(l10n.t('cancel')),
|
child: const Text('Annuleren'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -805,20 +788,14 @@ 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: Row(
|
child: const Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(Icons.warning_amber_outlined, size: 14, color: Colors.red),
|
||||||
Icons.warning_amber_outlined,
|
SizedBox(width: 6),
|
||||||
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,7 +9,6 @@ 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).
|
||||||
|
|
@ -86,7 +85,6 @@ 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);
|
||||||
|
|
@ -119,9 +117,9 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
||||||
color: Color(0xFF64748B),
|
color: Color(0xFF64748B),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
const Text(
|
||||||
l10n.d('Preview'),
|
'Preview',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF334155),
|
color: Color(0xFF334155),
|
||||||
|
|
@ -130,7 +128,7 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
// ── Zoom controls ──────────────────────────────────────
|
// ── Zoom controls ──────────────────────────────────────
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.d('Uitzoomen'),
|
message: '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,
|
||||||
|
|
@ -145,7 +143,7 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _zoom != _minZoom ? _resetZoom : null,
|
onTap: _zoom != _minZoom ? _resetZoom : null,
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: l10n.d('Zoom resetten'),
|
message: 'Zoom resetten',
|
||||||
child: Text(
|
child: Text(
|
||||||
'${(_zoom * 100).round()}%',
|
'${(_zoom * 100).round()}%',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|
@ -161,7 +159,7 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.d('Inzoomen'),
|
message: '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,
|
||||||
|
|
@ -183,7 +181,7 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.d('Preview inklappen'),
|
message: 'Preview inklappen',
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.chevron_right, size: 18),
|
icon: const Icon(Icons.chevron_right, size: 18),
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
|
|
@ -268,12 +266,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: l10n.d('Vorige slide'),
|
tooltip: 'Vorige slide',
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.d(slide.type.label),
|
slide.type.label,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Color(0xFF64748B),
|
color: Color(0xFF64748B),
|
||||||
|
|
@ -288,7 +286,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: l10n.d('Volgende slide'),
|
tooltip: 'Volgende slide',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -310,7 +308,7 @@ class _PreviewPanelState extends ConsumerState<PreviewPanel> {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'${l10n.d('Thema')}: ${deck.theme}',
|
'Thema: ${deck.theme}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Color(0xFF94A3B8),
|
color: Color(0xFF94A3B8),
|
||||||
|
|
@ -320,9 +318,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),
|
||||||
Text(
|
const Text(
|
||||||
l10n.d('paginering aan'),
|
'paginering aan',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Color(0xFF94A3B8),
|
color: Color(0xFF94A3B8),
|
||||||
),
|
),
|
||||||
|
|
@ -353,11 +351,10 @@ 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} — ${l10n.d('volledig deck')}'),
|
title: Text('${deck.title} — volledig deck'),
|
||||||
backgroundColor: AppTheme.navy,
|
backgroundColor: AppTheme.navy,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
|
|
@ -374,7 +371,7 @@ class FullDeckPreview extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${l10n.d('Slide')} ${i + 1}',
|
'Slide ${i + 1}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Color(0xFF64748B),
|
color: Color(0xFF64748B),
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
|
|
@ -419,7 +416,6 @@ 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,
|
||||||
|
|
@ -427,7 +423,7 @@ class CollapsedPreviewBar extends ConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.d('Preview uitklappen'),
|
message: '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,7 +12,6 @@ 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';
|
||||||
|
|
@ -158,9 +157,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(
|
||||||
SnackBar(
|
const SnackBar(
|
||||||
content: Text(context.l10n.d('Slide renderen…')),
|
content: Text('Slide renderen…'),
|
||||||
duration: const Duration(milliseconds: 700),
|
duration: Duration(milliseconds: 700),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Uint8List? bytes;
|
Uint8List? bytes;
|
||||||
|
|
@ -182,9 +181,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
||||||
messenger.showSnackBar(
|
messenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
ok
|
ok ? 'Slide gekopieerd naar klembord.' : 'Kopiëren mislukt.',
|
||||||
? context.l10n.d('Slide gekopieerd naar klembord.')
|
|
||||||
: context.l10n.d('Kopiëren mislukt.'),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -217,12 +214,8 @@ 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(
|
||||||
SnackBar(
|
const SnackBar(
|
||||||
content: Text(
|
content: Text('Geen ander deck open. Open eerst een ander tabblad.'),
|
||||||
context.l10n.d(
|
|
||||||
'Geen ander deck open. Open eerst een ander tabblad.',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
@ -230,13 +223,11 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
||||||
|
|
||||||
final target = await showDialog<TabInfo>(
|
final target = await showDialog<TabInfo>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) {
|
builder: (ctx) => SimpleDialog(
|
||||||
final l10n = ctx.l10n;
|
|
||||||
return SimpleDialog(
|
|
||||||
title: Text(
|
title: Text(
|
||||||
slides.length == 1
|
slides.length == 1
|
||||||
? l10n.d('1 slide kopiëren naar…')
|
? '1 slide kopiëren naar…'
|
||||||
: '${slides.length} ${l10n.d('slides kopiëren naar…')}',
|
: '${slides.length} slides kopiëren naar…',
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
for (final t in targets)
|
for (final t in targets)
|
||||||
|
|
@ -251,8 +242,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
if (target == null || !mounted) return;
|
if (target == null || !mounted) return;
|
||||||
|
|
||||||
|
|
@ -262,8 +252,8 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
at >= 0
|
at >= 0
|
||||||
? '${slides.length} ${context.l10n.d('slide(s) gekopieerd naar')} “${target.label}”.'
|
? '${slides.length} slide(s) gekopieerd naar “${target.label}”.'
|
||||||
: context.l10n.d('Kopiëren mislukt.'),
|
: 'Kopiëren mislukt.',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -375,15 +365,14 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
slides.length == 1
|
slides.length == 1
|
||||||
? context.l10n.d('1 slide geïmporteerd.')
|
? '1 slide geïmporteerd.'
|
||||||
: '${slides.length} ${context.l10n.d('slides geïmporteerd.')}',
|
: '${slides.length} slides geïmporteerd.',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSearchField() {
|
Widget _buildSearchField() {
|
||||||
final l10n = context.l10n;
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 30,
|
height: 30,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
|
@ -392,7 +381,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: l10n.d('Zoek in slides…'),
|
hintText: '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,
|
||||||
|
|
@ -442,7 +431,6 @@ 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,
|
||||||
|
|
@ -462,7 +450,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
'${l10n.d('Geen slides met')} "$query"',
|
'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),
|
||||||
),
|
),
|
||||||
|
|
@ -508,7 +496,6 @@ 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);
|
||||||
|
|
@ -546,9 +533,9 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
const Text(
|
||||||
l10n.d('SLIDES'),
|
'SLIDES',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF94A3B8),
|
color: Color(0xFF94A3B8),
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
|
|
@ -680,13 +667,11 @@ 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(
|
||||||
SnackBar(
|
const SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
l10n.d(
|
|
||||||
'Geen afbeelding op het klembord gevonden.',
|
'Geen afbeelding op het klembord gevonden.',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -702,7 +687,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: Text(l10n.d('Afbeelding plakken')),
|
label: const Text('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)),
|
||||||
|
|
@ -725,7 +710,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.add, size: 16),
|
icon: const Icon(Icons.add, size: 16),
|
||||||
label: Text(l10n.d('Slide toevoegen')),
|
label: const Text('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),
|
||||||
|
|
@ -743,7 +728,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
||||||
Icons.travel_explore_outlined,
|
Icons.travel_explore_outlined,
|
||||||
size: 14,
|
size: 14,
|
||||||
),
|
),
|
||||||
label: Text(l10n.d('Slide zoeken')),
|
label: const Text('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)),
|
||||||
|
|
@ -759,7 +744,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: Text(l10n.d('Slides importeren')),
|
label: const Text('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)),
|
||||||
|
|
@ -786,7 +771,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: Text(l10n.d('Slide plakken')),
|
label: const Text('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)),
|
||||||
|
|
@ -817,7 +802,6 @@ 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(
|
||||||
|
|
@ -836,8 +820,8 @@ class _SkipBanner extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
count == 1
|
count == 1
|
||||||
? l10n.d('1 slide overgeslagen')
|
? '1 slide overgeslagen'
|
||||||
: '$count ${l10n.d('slides overgeslagen')}',
|
: '$count slides overgeslagen',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Color(0xFFE3C281),
|
color: Color(0xFFE3C281),
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
|
|
@ -857,7 +841,7 @@ class _SkipBanner extends StatelessWidget {
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(l10n.d('Alles tonen')),
|
child: const Text('Alles tonen'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -886,7 +870,6 @@ 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(
|
||||||
|
|
@ -898,7 +881,7 @@ class _BulkActionBar extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'$count ${l10n.d('geselecteerd')}',
|
'$count geselecteerd',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Color(0xFFE2E8F0),
|
color: Color(0xFFE2E8F0),
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
|
|
@ -908,28 +891,28 @@ class _BulkActionBar extends StatelessWidget {
|
||||||
),
|
),
|
||||||
_BulkIcon(
|
_BulkIcon(
|
||||||
icon: Icons.drive_file_move_outline,
|
icon: Icons.drive_file_move_outline,
|
||||||
tooltip: l10n.d('Kopiëren naar ander deck'),
|
tooltip: 'Kopiëren naar ander deck',
|
||||||
onTap: onCopyToDeck,
|
onTap: onCopyToDeck,
|
||||||
),
|
),
|
||||||
_BulkIcon(
|
_BulkIcon(
|
||||||
icon: Icons.visibility_off_outlined,
|
icon: Icons.visibility_off_outlined,
|
||||||
tooltip: l10n.d('Overslaan bij presenteren/exporteren'),
|
tooltip: 'Overslaan bij presenteren/exporteren',
|
||||||
onTap: onSkip,
|
onTap: onSkip,
|
||||||
),
|
),
|
||||||
_BulkIcon(
|
_BulkIcon(
|
||||||
icon: Icons.visibility_outlined,
|
icon: Icons.visibility_outlined,
|
||||||
tooltip: l10n.d('Weer tonen'),
|
tooltip: 'Weer tonen',
|
||||||
onTap: onShow,
|
onTap: onShow,
|
||||||
),
|
),
|
||||||
_BulkIcon(
|
_BulkIcon(
|
||||||
icon: Icons.delete_outline,
|
icon: Icons.delete_outline,
|
||||||
tooltip: l10n.d('Verwijderen'),
|
tooltip: 'Verwijderen',
|
||||||
color: const Color(0xFFE5746E),
|
color: const Color(0xFFE5746E),
|
||||||
onTap: onDelete,
|
onTap: onDelete,
|
||||||
),
|
),
|
||||||
_BulkIcon(
|
_BulkIcon(
|
||||||
icon: Icons.close,
|
icon: Icons.close,
|
||||||
tooltip: l10n.d('Selectie opheffen'),
|
tooltip: 'Selectie opheffen',
|
||||||
onTap: onDeselect,
|
onTap: onDeselect,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:screen_retriever/screen_retriever.dart';
|
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
import '../../models/deck.dart';
|
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).
|
||||||
|
|
@ -108,12 +106,6 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
/// Met M te wisselen.
|
/// Met M te wisselen.
|
||||||
bool _advanceOnAudioEnd = true;
|
bool _advanceOnAudioEnd = true;
|
||||||
|
|
||||||
/// Known displays for moving the fullscreen presentation window. This is not
|
|
||||||
/// a second presenter window; it keeps the current output movable between
|
|
||||||
/// screens with S or the presenter-view button.
|
|
||||||
List<Display> _displays = const [];
|
|
||||||
int _displayIndex = 0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -126,7 +118,6 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
});
|
});
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
_loadDisplays();
|
|
||||||
_scheduleAdvance();
|
_scheduleAdvance();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -217,55 +208,6 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
_scheduleAdvance();
|
_scheduleAdvance();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadDisplays() async {
|
|
||||||
try {
|
|
||||||
final displays = await screenRetriever.getAllDisplays();
|
|
||||||
if (!mounted || displays.isEmpty) return;
|
|
||||||
final bounds = await windowManager.getBounds();
|
|
||||||
final center = bounds.center;
|
|
||||||
final current = displays.indexWhere((d) {
|
|
||||||
final p = d.visiblePosition ?? Offset.zero;
|
|
||||||
final s = d.visibleSize ?? d.size;
|
|
||||||
return Rect.fromLTWH(p.dx, p.dy, s.width, s.height).contains(center);
|
|
||||||
});
|
|
||||||
setState(() {
|
|
||||||
_displays = displays;
|
|
||||||
_displayIndex = current < 0 ? 0 : current;
|
|
||||||
});
|
|
||||||
} catch (_) {
|
|
||||||
// Screen detection is best-effort; presenting should still work.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _moveToDisplay(int index) async {
|
|
||||||
if (_displays.length < 2) return;
|
|
||||||
final display = _displays[index.clamp(0, _displays.length - 1)];
|
|
||||||
final position = display.visiblePosition ?? Offset.zero;
|
|
||||||
final size = display.visibleSize ?? display.size;
|
|
||||||
try {
|
|
||||||
await windowManager.setFullScreen(false);
|
|
||||||
await windowManager.setBounds(
|
|
||||||
Rect.fromLTWH(position.dx, position.dy, size.width, size.height),
|
|
||||||
);
|
|
||||||
await windowManager.setFullScreen(true);
|
|
||||||
if (mounted) setState(() => _displayIndex = index);
|
|
||||||
} catch (_) {
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(context.l10n.d('Kon niet van scherm wisselen.')),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _cycleDisplay() async {
|
|
||||||
if (_displays.isEmpty) await _loadDisplays();
|
|
||||||
if (_displays.length < 2) return;
|
|
||||||
await _moveToDisplay((_displayIndex + 1) % _displays.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _exit() async {
|
Future<void> _exit() async {
|
||||||
_advanceTimer?.cancel();
|
_advanceTimer?.cancel();
|
||||||
await windowManager.setFullScreen(false);
|
await windowManager.setFullScreen(false);
|
||||||
|
|
@ -509,9 +451,6 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
case LogicalKeyboardKey.keyM:
|
case LogicalKeyboardKey.keyM:
|
||||||
_toggleAudioAdvance();
|
_toggleAudioAdvance();
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
case LogicalKeyboardKey.keyS:
|
|
||||||
_cycleDisplay();
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
case LogicalKeyboardKey.escape:
|
case LogicalKeyboardKey.escape:
|
||||||
// Gelaagd: getypt nummer wissen, dan blanco scherm, dan pas afsluiten.
|
// Gelaagd: getypt nummer wissen, dan blanco scherm, dan pas afsluiten.
|
||||||
if (_typed.isNotEmpty) {
|
if (_typed.isNotEmpty) {
|
||||||
|
|
@ -639,22 +578,20 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
|
|
||||||
/// Sneltoets-overzicht (cheatsheet).
|
/// Sneltoets-overzicht (cheatsheet).
|
||||||
Widget _buildHelpOverlay() {
|
Widget _buildHelpOverlay() {
|
||||||
final l10n = context.l10n;
|
const rows = <(String, String)>[
|
||||||
final rows = <(String, String)>[
|
('→ · spatie · klik', 'Volgende slide'),
|
||||||
('→ · ${l10n.d('spatie')} · ${l10n.d('klik')}', l10n.d('Volgende slide')),
|
('←', 'Vorige slide'),
|
||||||
('←', l10n.d('Vorige slide')),
|
('cijfers + Enter', 'Naar slidenummer'),
|
||||||
('${l10n.d('cijfers')} + Enter', l10n.d('Naar slidenummer')),
|
('Home · End', 'Eerste · laatste slide'),
|
||||||
('Home · End', l10n.d('Eerste · laatste slide')),
|
('G', 'Slide-overzicht (pijltjes + Enter)'),
|
||||||
('G', l10n.d('Slide-overzicht (pijltjes + Enter)')),
|
('P', 'Presenter view (notities, klok)'),
|
||||||
('P', l10n.d('Presenter view (notities, klok)')),
|
('B · W', 'Zwart · wit scherm'),
|
||||||
('S', l10n.d('Scherm wisselen (meerdere schermen)')),
|
('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('Deze legenda')),
|
|
||||||
('Esc', l10n.d('Terug / afsluiten')),
|
|
||||||
];
|
];
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: _toggleHelp,
|
onTap: _toggleHelp,
|
||||||
|
|
@ -679,17 +616,17 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
const Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.keyboard_outlined,
|
Icons.keyboard_outlined,
|
||||||
color: Colors.white70,
|
color: Colors.white70,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
l10n.d('Toetsenlegenda'),
|
'Sneltoetsen',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
@ -727,13 +664,10 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Center(
|
const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.d('Klik of druk op H / Esc om te sluiten'),
|
'Klik of druk op ? / H / Esc om te sluiten',
|
||||||
style: const TextStyle(
|
style: TextStyle(color: Colors.white30, fontSize: 12),
|
||||||
color: Colors.white30,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -745,6 +679,43 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Subtiele statusindicator (linksonder) voor de automatische modus. Toont
|
||||||
|
/// of auto-play, herhalen en 'na audio doorgaan' actief zijn.
|
||||||
|
Widget _autoPlayStatus() {
|
||||||
|
final items = <(IconData, String, bool)>[
|
||||||
|
(
|
||||||
|
_autoPlay ? Icons.play_circle_outline : Icons.pause_circle_outline,
|
||||||
|
_autoPlay ? 'Auto (A)' : 'Handmatig (A)',
|
||||||
|
_autoPlay,
|
||||||
|
),
|
||||||
|
(Icons.repeat, 'Herhalen (L)', _loop),
|
||||||
|
(Icons.graphic_eq, 'Na audio (M)', _advanceOnAudioEnd),
|
||||||
|
];
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
for (final (icon, label, active) in items)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 12),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: active ? 0.7 : 0.28,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 14, color: Colors.white),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 11),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Vol-vlak zwart/wit scherm dat met een klik weer verdwijnt.
|
/// Vol-vlak zwart/wit scherm dat met een klik weer verdwijnt.
|
||||||
Widget _blankFill() {
|
Widget _blankFill() {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
|
@ -806,14 +777,154 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: _next,
|
onTap: _next,
|
||||||
onSecondaryTap: _prev,
|
onSecondaryTap: _prev,
|
||||||
child: SizedBox.expand(child: _slideCanvas(slide)),
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// ── Slide canvas ─────────────────────────────────────────────────
|
||||||
|
Positioned.fill(child: _slideCanvas(slide)),
|
||||||
|
|
||||||
|
// ── Voortgangsbalk (auto-advance) ────────────────────────────────
|
||||||
|
if (_progress > 0)
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: _progress,
|
||||||
|
backgroundColor: Colors.white12,
|
||||||
|
color: Colors.white54,
|
||||||
|
minHeight: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Slide counter ────────────────────────────────────────────────
|
||||||
|
Positioned(
|
||||||
|
right: 24,
|
||||||
|
bottom: 10,
|
||||||
|
child: Text(
|
||||||
|
'${_index + 1} / $total',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white38,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Auto-play status (linksonder) ────────────────────────────────
|
||||||
|
Positioned(left: 24, bottom: 10, child: _autoPlayStatus()),
|
||||||
|
|
||||||
|
// ── Navigation arrows ────────────────────────────────────────────
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: _index > 0 ? SystemMouseCursors.click : MouseCursor.defer,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _prev,
|
||||||
|
child: Container(
|
||||||
|
width: 60,
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: _index > 0
|
||||||
|
? const Icon(
|
||||||
|
Icons.chevron_left,
|
||||||
|
color: Colors.white24,
|
||||||
|
size: 40,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: _index < total - 1
|
||||||
|
? SystemMouseCursors.click
|
||||||
|
: MouseCursor.defer,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _next,
|
||||||
|
child: Container(
|
||||||
|
width: 60,
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: _index < total - 1
|
||||||
|
? const Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Colors.white24,
|
||||||
|
size: 40,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Top-right controls (presenter view + afsluiten) ──────────────
|
||||||
|
Positioned(
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Tooltip(
|
||||||
|
message: 'Sneltoetsen (?)',
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: _toggleHelp,
|
||||||
|
icon: const Icon(Icons.help_outline),
|
||||||
|
color: Colors.white,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.black45,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Tooltip(
|
||||||
|
message: 'Slide-overzicht (G)',
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: _toggleGrid,
|
||||||
|
icon: const Icon(Icons.grid_view_rounded),
|
||||||
|
color: Colors.white,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.black45,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Tooltip(
|
||||||
|
message: 'Presenter view (P)',
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: _togglePresenterView,
|
||||||
|
icon: const Icon(Icons.co_present_outlined),
|
||||||
|
color: Colors.white,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.black45,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Tooltip(
|
||||||
|
message: 'Afsluiten (Escape)',
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: _exit,
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
color: Colors.white,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.black45,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 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;
|
||||||
|
|
@ -831,7 +942,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_SectionLabel(l10n.d('HUIDIGE SLIDE')),
|
const _SectionLabel('HUIDIGE SLIDE'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
|
|
@ -877,7 +988,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
children: [
|
children: [
|
||||||
_buildClockBar(),
|
_buildClockBar(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_SectionLabel(l10n.d('VOLGENDE')),
|
const _SectionLabel('VOLGENDE'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
|
|
@ -895,9 +1006,9 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
: Container(
|
: Container(
|
||||||
color: const Color(0xFF161616),
|
color: const Color(0xFF161616),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: const Text(
|
||||||
l10n.d('Einde van de presentatie'),
|
'Einde van de presentatie',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white38,
|
color: Colors.white38,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
|
|
@ -906,7 +1017,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_SectionLabel(l10n.d('NOTITIES')),
|
const _SectionLabel('NOTITIES'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Expanded(child: _buildNotes(slide)),
|
Expanded(child: _buildNotes(slide)),
|
||||||
],
|
],
|
||||||
|
|
@ -918,7 +1029,6 @@ 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),
|
||||||
|
|
@ -934,9 +1044,9 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
const Text(
|
||||||
l10n.d('Verstreken'),
|
'Verstreken',
|
||||||
style: const TextStyle(color: Colors.white38, fontSize: 10),
|
style: TextStyle(color: Colors.white38, fontSize: 10),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -953,7 +1063,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
),
|
),
|
||||||
// Reset-knop
|
// Reset-knop
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.d('Tijd resetten (R)'),
|
message: '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),
|
||||||
|
|
@ -966,9 +1076,9 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
const Text(
|
||||||
l10n.d('Klok'),
|
'Klok',
|
||||||
style: const TextStyle(color: Colors.white38, fontSize: 10),
|
style: TextStyle(color: Colors.white38, fontSize: 10),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -988,7 +1098,6 @@ 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,
|
||||||
|
|
@ -999,11 +1108,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
|
||||||
? Align(
|
? const Align(
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.d('Geen notities voor deze slide.'),
|
'Geen notities voor deze slide.',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white30,
|
color: Colors.white30,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
|
|
@ -1024,7 +1133,6 @@ 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),
|
||||||
|
|
@ -1033,19 +1141,9 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
icon: Icons.chevron_right,
|
icon: Icons.chevron_right,
|
||||||
onTap: _index < total - 1 ? _next : null,
|
onTap: _index < total - 1 ? _next : null,
|
||||||
),
|
),
|
||||||
if (_displays.length > 1) ...[
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Tooltip(
|
|
||||||
message: l10n.d('Wissel scherm (S)'),
|
|
||||||
child: _NavButton(
|
|
||||||
icon: Icons.screen_share_outlined,
|
|
||||||
onTap: _cycleDisplay,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Text(
|
Text(
|
||||||
'${l10n.d('Slide')} ${_index + 1} / $total',
|
'Slide ${_index + 1} / $total',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
|
|
@ -1053,13 +1151,9 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
const Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.d(
|
'P publiek · G overzicht · B/W zwart/wit · R tijd · Esc stop',
|
||||||
_displays.length > 1
|
|
||||||
? 'P publiek · H legenda · S scherm · G overzicht · B/W zwart/wit · R tijd · Esc stop'
|
|
||||||
: 'P publiek · H legenda · 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,
|
||||||
|
|
@ -1068,7 +1162,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.d('Afsluiten (Escape)'),
|
message: 'Afsluiten (Escape)',
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: _exit,
|
onPressed: _exit,
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
|
|
@ -1083,7 +1177,6 @@ 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),
|
||||||
|
|
@ -1094,9 +1187,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: [
|
||||||
Text(
|
const Text(
|
||||||
l10n.d('Slide-overzicht'),
|
'Slide-overzicht',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
@ -1104,12 +1197,12 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
'${l10n.d('pijltjes + Enter of klik om te springen')} · $total ${l10n.t('slides')}',
|
'pijltjes + Enter of klik om te springen · $total slides',
|
||||||
style: const TextStyle(color: Colors.white38, fontSize: 12),
|
style: const TextStyle(color: Colors.white38, fontSize: 12),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: l10n.d('Sluiten (G of Esc)'),
|
message: 'Sluiten (G of Esc)',
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: _toggleGrid,
|
onPressed: _toggleGrid,
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ 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 {
|
||||||
|
|
@ -45,7 +44,6 @@ 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
|
||||||
|
|
@ -102,18 +100,18 @@ class SlideThumbnail extends ConsumerWidget {
|
||||||
color: const Color(0xCC8A6D3B),
|
color: const Color(0xCC8A6D3B),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: const Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.visibility_off_outlined,
|
Icons.visibility_off_outlined,
|
||||||
size: 10,
|
size: 10,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 3),
|
SizedBox(width: 3),
|
||||||
Text(
|
Text(
|
||||||
l10n.d('Overgeslagen'),
|
'Overgeslagen',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
@ -155,7 +153,7 @@ class SlideThumbnail extends ConsumerWidget {
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.d(slide.type.label),
|
slide.type.label,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Color(0xFF94A3B8),
|
color: Color(0xFF94A3B8),
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
|
|
@ -184,8 +182,8 @@ class SlideThumbnail extends ConsumerWidget {
|
||||||
iconSize: 14,
|
iconSize: 14,
|
||||||
splashRadius: 12,
|
splashRadius: 12,
|
||||||
tooltip: skipped
|
tooltip: skipped
|
||||||
? l10n.d('Weer tonen bij presenteren/exporteren')
|
? 'Weer tonen bij presenteren/exporteren'
|
||||||
: l10n.d('Overslaan bij presenteren/exporteren'),
|
: 'Overslaan bij presenteren/exporteren',
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
skipped
|
skipped
|
||||||
? Icons.visibility_off
|
? Icons.visibility_off
|
||||||
|
|
@ -209,31 +207,29 @@ class SlideThumbnail extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
itemBuilder: (_) => [
|
itemBuilder: (_) => [
|
||||||
PopupMenuItem(
|
const PopupMenuItem(
|
||||||
value: 'copy',
|
value: 'copy',
|
||||||
child: Text(l10n.d('Kopiëren')),
|
child: Text('Kopiëren'),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
const PopupMenuItem(
|
||||||
value: 'copy_image',
|
value: 'copy_image',
|
||||||
child: Text(l10n.d('Kopieer als afbeelding')),
|
child: Text('Kopieer als afbeelding'),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
const PopupMenuItem(
|
||||||
value: 'duplicate',
|
value: 'duplicate',
|
||||||
child: Text(l10n.d('Dupliceren')),
|
child: Text('Dupliceren'),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'skip',
|
value: 'skip',
|
||||||
child: Text(
|
child: Text(
|
||||||
skipped
|
skipped ? 'Niet meer overslaan' : 'Overslaan',
|
||||||
? l10n.d('Niet meer overslaan')
|
|
||||||
: l10n.d('Overslaan'),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
const PopupMenuItem(
|
||||||
value: 'delete',
|
value: 'delete',
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.d('Verwijderen'),
|
'Verwijderen',
|
||||||
style: const TextStyle(color: Colors.red),
|
style: TextStyle(color: Colors.red),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
15
pubspec.lock
15
pubspec.lock
|
|
@ -230,11 +230,6 @@ 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:
|
||||||
|
|
@ -349,14 +344,6 @@ 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:
|
||||||
|
|
@ -654,7 +641,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.2.1"
|
||||||
screen_retriever:
|
screen_retriever:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: screen_retriever
|
name: screen_retriever
|
||||||
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
|
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,12 @@ 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
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
path: ^1.9.1
|
path: ^1.9.1
|
||||||
uuid: ^4.5.1
|
uuid: ^4.5.1
|
||||||
screen_retriever: ^0.2.0
|
|
||||||
window_manager: ^0.5.1
|
window_manager: ^0.5.1
|
||||||
shared_preferences: ^2.3.3
|
shared_preferences: ^2.3.3
|
||||||
pasteboard: ^0.5.0
|
pasteboard: ^0.5.0
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:ocideck/l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
tearDown(() => AppLocalizations.setActiveLanguageCode('nl'));
|
|
||||||
|
|
||||||
test('supports Frisian and Papiamento language choices', () {
|
|
||||||
expect(AppLocalizations.languageNames['fy'], 'Frysk');
|
|
||||||
expect(AppLocalizations.languageNames['pap'], 'Papiamento');
|
|
||||||
expect(AppLocalizations.supportedLocales, contains(const Locale('fy')));
|
|
||||||
expect(AppLocalizations.supportedLocales, contains(const Locale('pap')));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('uses app translations while Material falls back safely', () {
|
|
||||||
AppLocalizations.setActiveLanguageCode('fy');
|
|
||||||
expect(const AppLocalizations(Locale('en')).t('settings'), 'Ynstellingen');
|
|
||||||
expect(
|
|
||||||
const AppLocalizations(Locale('en')).d('Toetsenlegenda'),
|
|
||||||
'Toetsleginda',
|
|
||||||
);
|
|
||||||
expect(AppLocalizations.materialLocaleFor('fy'), const Locale('en'));
|
|
||||||
|
|
||||||
AppLocalizations.setActiveLanguageCode('pap');
|
|
||||||
expect(
|
|
||||||
const AppLocalizations(Locale('en')).t('settings'),
|
|
||||||
'Preferensianan',
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
const AppLocalizations(Locale('en')).d('Toetsenlegenda'),
|
|
||||||
'Legenda di tekla',
|
|
||||||
);
|
|
||||||
expect(AppLocalizations.materialLocaleFor('pap'), const Locale('en'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -29,9 +29,9 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('free Markdown renders display math', (tester) async {
|
testWidgets('free Markdown renders display math', (tester) async {
|
||||||
final slide = Slide.create(
|
final slide = Slide.create(SlideType.freeMarkdown).copyWith(
|
||||||
SlideType.freeMarkdown,
|
customMarkdown: 'Stelling:\n\n\$\$E = mc^2\$\$\n',
|
||||||
).copyWith(customMarkdown: 'Stelling:\n\n\$\$E = mc^2\$\$\n');
|
);
|
||||||
await tester.pumpWidget(_host(slide));
|
await tester.pumpWidget(_host(slide));
|
||||||
|
|
||||||
expect(find.byType(Math), findsOneWidget);
|
expect(find.byType(Math), findsOneWidget);
|
||||||
|
|
@ -40,9 +40,9 @@ void main() {
|
||||||
testWidgets('an unknown code language falls back without throwing', (
|
testWidgets('an unknown code language falls back without throwing', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
final slide = Slide.create(
|
final slide = Slide.create(SlideType.freeMarkdown).copyWith(
|
||||||
SlideType.freeMarkdown,
|
customMarkdown: '```nonexistentlang\nsome code\n```\n',
|
||||||
).copyWith(customMarkdown: '```nonexistentlang\nsome code\n```\n');
|
);
|
||||||
await tester.pumpWidget(_host(slide));
|
await tester.pumpWidget(_host(slide));
|
||||||
|
|
||||||
// No HighlightView (unknown language) and no exception during build.
|
// No HighlightView (unknown language) and no exception during build.
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,7 @@ void main() {
|
||||||
await tester.pumpWidget(_host(slides));
|
await tester.pumpWidget(_host(slides));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(find.text('Eerste'), findsOneWidget);
|
expect(find.text('1 / 2'), findsOneWidget); // slide counter
|
||||||
expect(find.text('1 / 2'), findsNothing); // no audience chrome
|
|
||||||
expect(find.byIcon(Icons.help_outline), findsNothing);
|
|
||||||
expect(find.byIcon(Icons.grid_view_rounded), findsNothing);
|
|
||||||
expect(find.byIcon(Icons.co_present_outlined), findsNothing);
|
|
||||||
expect(find.byIcon(Icons.close), findsNothing);
|
|
||||||
expect(find.text('NOTITIES'), findsNothing); // presenter-only
|
expect(find.text('NOTITIES'), findsNothing); // presenter-only
|
||||||
expect(find.text('VOLGENDE'), findsNothing);
|
expect(find.text('VOLGENDE'), findsNothing);
|
||||||
|
|
||||||
|
|
@ -93,17 +88,17 @@ void main() {
|
||||||
testWidgets('B blanks the audience screen and toggles back', (tester) async {
|
testWidgets('B blanks the audience screen and toggles back', (tester) async {
|
||||||
await tester.pumpWidget(_host(slides));
|
await tester.pumpWidget(_host(slides));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Eerste'), findsOneWidget);
|
expect(find.text('1 / 2'), findsOneWidget);
|
||||||
|
|
||||||
// Blank to black: the slide disappears.
|
// Blank to black: the slide (and its counter) disappears.
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
|
await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Eerste'), findsNothing);
|
expect(find.text('1 / 2'), findsNothing);
|
||||||
|
|
||||||
// Same key restores the slide.
|
// Same key restores the slide.
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
|
await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Eerste'), findsOneWidget);
|
expect(find.text('1 / 2'), findsOneWidget);
|
||||||
|
|
||||||
await tester.pumpWidget(const SizedBox());
|
await tester.pumpWidget(const SizedBox());
|
||||||
});
|
});
|
||||||
|
|
@ -116,12 +111,12 @@ void main() {
|
||||||
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
|
await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Eerste'), findsNothing);
|
expect(find.text('1 / 2'), findsNothing);
|
||||||
|
|
||||||
// First arrow un-blanks without advancing (still on slide 1).
|
// First arrow un-blanks without advancing (still on slide 1).
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Eerste'), findsOneWidget);
|
expect(find.text('1 / 2'), findsOneWidget);
|
||||||
|
|
||||||
await tester.pumpWidget(const SizedBox());
|
await tester.pumpWidget(const SizedBox());
|
||||||
});
|
});
|
||||||
|
|
@ -147,7 +142,7 @@ void main() {
|
||||||
|
|
||||||
// Grid closed and we jumped to slide 2.
|
// Grid closed and we jumped to slide 2.
|
||||||
expect(find.text('Slide-overzicht'), findsNothing);
|
expect(find.text('Slide-overzicht'), findsNothing);
|
||||||
expect(find.text('Tweede'), findsOneWidget);
|
expect(find.text('2 / 2'), findsOneWidget);
|
||||||
|
|
||||||
await tester.pumpWidget(const SizedBox());
|
await tester.pumpWidget(const SizedBox());
|
||||||
});
|
});
|
||||||
|
|
@ -157,15 +152,15 @@ void main() {
|
||||||
) async {
|
) async {
|
||||||
await tester.pumpWidget(_host(slides));
|
await tester.pumpWidget(_host(slides));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Eerste'), findsOneWidget);
|
expect(find.text('1 / 2'), findsOneWidget);
|
||||||
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.end);
|
await tester.sendKeyEvent(LogicalKeyboardKey.end);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Tweede'), findsOneWidget);
|
expect(find.text('2 / 2'), findsOneWidget);
|
||||||
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.home);
|
await tester.sendKeyEvent(LogicalKeyboardKey.home);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Eerste'), findsOneWidget);
|
expect(find.text('1 / 2'), findsOneWidget);
|
||||||
|
|
||||||
await tester.pumpWidget(const SizedBox());
|
await tester.pumpWidget(const SizedBox());
|
||||||
});
|
});
|
||||||
|
|
@ -192,7 +187,7 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(find.text('Slide-overzicht'), findsNothing);
|
expect(find.text('Slide-overzicht'), findsNothing);
|
||||||
expect(find.text('Tweede'), findsOneWidget);
|
expect(find.text('2 / 2'), findsOneWidget);
|
||||||
|
|
||||||
await tester.pumpWidget(const SizedBox());
|
await tester.pumpWidget(const SizedBox());
|
||||||
});
|
});
|
||||||
|
|
@ -204,7 +199,7 @@ void main() {
|
||||||
];
|
];
|
||||||
await tester.pumpWidget(_host(three));
|
await tester.pumpWidget(_host(three));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Eerste'), findsOneWidget);
|
expect(find.text('1 / 3'), findsOneWidget);
|
||||||
|
|
||||||
// Type "3" → a badge appears, Enter jumps to slide 3.
|
// Type "3" → a badge appears, Enter jumps to slide 3.
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.digit3);
|
await tester.sendKeyEvent(LogicalKeyboardKey.digit3);
|
||||||
|
|
@ -213,9 +208,8 @@ void main() {
|
||||||
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
// Badge gone, now actually on slide 3.
|
// Badge gone, now actually on slide 3 (counter shows it).
|
||||||
expect(find.text('Derde'), findsOneWidget);
|
expect(find.text('3 / 3'), findsOneWidget); // slide counter now
|
||||||
expect(find.text('3 / 3'), findsNothing);
|
|
||||||
expect(find.byIcon(Icons.south_east), findsNothing); // badge icon gone
|
expect(find.byIcon(Icons.south_east), findsNothing); // badge icon gone
|
||||||
|
|
||||||
await tester.pumpWidget(const SizedBox());
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
|
@ -224,18 +218,17 @@ void main() {
|
||||||
testWidgets('? toggles the shortcut cheatsheet', (tester) async {
|
testWidgets('? toggles the shortcut cheatsheet', (tester) async {
|
||||||
await tester.pumpWidget(_host(slides));
|
await tester.pumpWidget(_host(slides));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Toetsenlegenda'), findsNothing);
|
expect(find.text('Sneltoetsen'), findsNothing);
|
||||||
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyH);
|
await tester.sendKeyEvent(LogicalKeyboardKey.keyH);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Toetsenlegenda'), findsOneWidget);
|
expect(find.text('Sneltoetsen'), findsOneWidget);
|
||||||
expect(find.text('Scherm wisselen (meerdere schermen)'), findsOneWidget);
|
|
||||||
|
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Toetsenlegenda'), findsNothing);
|
expect(find.text('Sneltoetsen'), findsNothing);
|
||||||
// Esc closed the help, not the presentation.
|
// Esc closed the help, not the presentation.
|
||||||
expect(find.text('Eerste'), findsOneWidget);
|
expect(find.text('1 / 2'), findsOneWidget);
|
||||||
|
|
||||||
await tester.pumpWidget(const SizedBox());
|
await tester.pumpWidget(const SizedBox());
|
||||||
});
|
});
|
||||||
|
|
@ -257,7 +250,7 @@ void main() {
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Slide-overzicht'), findsNothing);
|
expect(find.text('Slide-overzicht'), findsNothing);
|
||||||
expect(find.text('Eerste'), findsOneWidget);
|
expect(find.text('1 / 2'), findsOneWidget);
|
||||||
|
|
||||||
await tester.pumpWidget(const SizedBox());
|
await tester.pumpWidget(const SizedBox());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
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', () {
|
||||||
|
|
@ -76,38 +73,4 @@ 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