Improve presentation settings and localization
This commit is contained in:
parent
20906ddb65
commit
ee66721de6
12 changed files with 1612 additions and 126 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -384,6 +384,7 @@ class DeckNotifier extends StateNotifier<DeckState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateInfo({
|
void updateInfo({
|
||||||
|
String? title,
|
||||||
String? author,
|
String? author,
|
||||||
String? organization,
|
String? organization,
|
||||||
String? version,
|
String? version,
|
||||||
|
|
@ -396,6 +397,7 @@ class DeckNotifier extends StateNotifier<DeckState> {
|
||||||
if (deck == null) return;
|
if (deck == null) return;
|
||||||
_mutate(
|
_mutate(
|
||||||
deck.copyWith(
|
deck.copyWith(
|
||||||
|
title: title,
|
||||||
author: author,
|
author: author,
|
||||||
organization: organization,
|
organization: organization,
|
||||||
version: version,
|
version: version,
|
||||||
|
|
|
||||||
|
|
@ -477,27 +477,32 @@ class _DropOverlay extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
border: Border.all(color: const Color(0xFF60A5FA), width: 2),
|
border: Border.all(color: const Color(0xFF60A5FA), width: 2),
|
||||||
),
|
),
|
||||||
child: const Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
const Icon(
|
||||||
Icons.file_download_outlined,
|
Icons.file_download_outlined,
|
||||||
size: 40,
|
size: 40,
|
||||||
color: Color(0xFF2563EB),
|
color: Color(0xFF2563EB),
|
||||||
),
|
),
|
||||||
SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
'Laat los om toe te voegen',
|
context.l10n.d('Laat los om toe te voegen'),
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1E293B),
|
color: Color(0xFF1E293B),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
|
context.l10n.d(
|
||||||
'Afbeeldingen → nieuwe slides · .md / .ocideck → openen',
|
'Afbeeldingen → nieuwe slides · .md / .ocideck → openen',
|
||||||
style: TextStyle(fontSize: 12, color: Color(0xFF64748B)),
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF64748B),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -1007,6 +1012,7 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
final info = await PresentationInfoDialog.show(context, deck);
|
final info = await PresentationInfoDialog.show(context, deck);
|
||||||
if (info == null) return;
|
if (info == null) return;
|
||||||
deckNotifier.updateInfo(
|
deckNotifier.updateInfo(
|
||||||
|
title: info.title,
|
||||||
author: info.author,
|
author: info.author,
|
||||||
organization: info.organization,
|
organization: info.organization,
|
||||||
version: info.version,
|
version: info.version,
|
||||||
|
|
@ -1146,6 +1152,14 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
tlp: deck.tlp,
|
tlp: deck.tlp,
|
||||||
onSelected: (level) => deckNotifier.updateInfo(tlp: level),
|
onSelected: (level) => deckNotifier.updateInfo(tlp: level),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Tooltip(
|
||||||
|
message: l10n.t('presentationProperties'),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.info_outline, size: 18),
|
||||||
|
onPressed: openProperties,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
|
@ -1292,11 +1306,6 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
menuItem(
|
|
||||||
'properties',
|
|
||||||
Icons.info_outline,
|
|
||||||
l10n.t('presentationProperties'),
|
|
||||||
),
|
|
||||||
menuItem(
|
menuItem(
|
||||||
'settings',
|
'settings',
|
||||||
Icons.settings_outlined,
|
Icons.settings_outlined,
|
||||||
|
|
|
||||||
|
|
@ -293,12 +293,13 @@ class _ExportDialogState extends State<ExportDialog> {
|
||||||
label: l10n.t('exportAsHtml'),
|
label: l10n.t('exportAsHtml'),
|
||||||
onPressed: () => _export(ExportFormat.html),
|
onPressed: () => _export(ExportFormat.html),
|
||||||
),
|
),
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
'HTML opent in elke browser zonder internet en rendert codeblokken, '
|
l10n.d(
|
||||||
'wiskunde en mermaid-diagrammen.',
|
'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.',
|
||||||
style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
),
|
||||||
|
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import '../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
/// The editable general metadata of a presentation.
|
/// The editable general metadata of a presentation.
|
||||||
class PresentationInfo {
|
class PresentationInfo {
|
||||||
|
final String title;
|
||||||
final String author;
|
final String author;
|
||||||
final String organization;
|
final String organization;
|
||||||
final String version;
|
final String version;
|
||||||
|
|
@ -13,6 +14,7 @@ class PresentationInfo {
|
||||||
final String keywords;
|
final String keywords;
|
||||||
|
|
||||||
const PresentationInfo({
|
const PresentationInfo({
|
||||||
|
required this.title,
|
||||||
required this.author,
|
required this.author,
|
||||||
required this.organization,
|
required this.organization,
|
||||||
required this.version,
|
required this.version,
|
||||||
|
|
@ -42,6 +44,7 @@ class PresentationInfoDialog extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
||||||
|
late final TextEditingController _title;
|
||||||
late final TextEditingController _author;
|
late final TextEditingController _author;
|
||||||
late final TextEditingController _organization;
|
late final TextEditingController _organization;
|
||||||
late final TextEditingController _version;
|
late final TextEditingController _version;
|
||||||
|
|
@ -52,6 +55,7 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_title = TextEditingController(text: widget.deck.title);
|
||||||
_author = TextEditingController(text: widget.deck.author);
|
_author = TextEditingController(text: widget.deck.author);
|
||||||
_organization = TextEditingController(text: widget.deck.organization);
|
_organization = TextEditingController(text: widget.deck.organization);
|
||||||
_version = TextEditingController(text: widget.deck.version);
|
_version = TextEditingController(text: widget.deck.version);
|
||||||
|
|
@ -62,6 +66,7 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_title.dispose();
|
||||||
_author.dispose();
|
_author.dispose();
|
||||||
_organization.dispose();
|
_organization.dispose();
|
||||||
_version.dispose();
|
_version.dispose();
|
||||||
|
|
@ -75,6 +80,7 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
context,
|
context,
|
||||||
PresentationInfo(
|
PresentationInfo(
|
||||||
|
title: _title.text.trim(),
|
||||||
author: _author.text.trim(),
|
author: _author.text.trim(),
|
||||||
organization: _organization.text.trim(),
|
organization: _organization.text.trim(),
|
||||||
version: _version.text.trim(),
|
version: _version.text.trim(),
|
||||||
|
|
@ -108,14 +114,7 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
_field(_title, 'Titel', 'Titel van de presentatie'),
|
||||||
widget.deck.title,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF64748B),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
|
||||||
|
|
@ -173,25 +173,26 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
: profiles.first.name;
|
: profiles.first.name;
|
||||||
|
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: 3,
|
length: 4,
|
||||||
child: AlertDialog(
|
child: AlertDialog(
|
||||||
title: Text(l10n.t('settings')),
|
title: Text(l10n.t('settings')),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 520,
|
width: 520,
|
||||||
height: 560,
|
height: 600,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_profileSelector(profiles, dropdownValue),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_profileNameField(),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
TabBar(
|
TabBar(
|
||||||
|
isScrollable: true,
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(
|
Tab(
|
||||||
icon: const Icon(Icons.tune),
|
icon: const Icon(Icons.tune),
|
||||||
text: l10n.t('settingsGeneral'),
|
text: l10n.t('settingsGeneral'),
|
||||||
),
|
),
|
||||||
|
Tab(
|
||||||
|
icon: const Icon(Icons.style_outlined),
|
||||||
|
text: l10n.t('styleProfile'),
|
||||||
|
),
|
||||||
Tab(
|
Tab(
|
||||||
icon: const Icon(Icons.palette_outlined),
|
icon: const Icon(Icons.palette_outlined),
|
||||||
text: l10n.t('settingsColors'),
|
text: l10n.t('settingsColors'),
|
||||||
|
|
@ -207,6 +208,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
children: [
|
children: [
|
||||||
_tabBody(_generalTab()),
|
_tabBody(_generalTab()),
|
||||||
|
_tabBody(_styleTab(profiles, dropdownValue)),
|
||||||
_tabBody(_colorsTab()),
|
_tabBody(_colorsTab()),
|
||||||
_tabBody(_logoTab()),
|
_tabBody(_logoTab()),
|
||||||
],
|
],
|
||||||
|
|
@ -350,6 +352,24 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _styleTab(List<ThemeProfile> profiles, String dropdownValue) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_sectionTitle(l10n.t('styleProfile')),
|
||||||
|
_profileSelector(profiles, dropdownValue),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_profileNameField(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_sectionTitle(l10n.d('Lettertype')),
|
||||||
|
_fontSection(),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
_stylePreview(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _generalTab() {
|
Widget _generalTab() {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final languageCode = ref.watch(
|
final languageCode = ref.watch(
|
||||||
|
|
@ -507,9 +527,6 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_sectionTitle(l10n.d('Lettertype')),
|
|
||||||
_fontSection(),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_sectionTitle(l10n.d('Kleuren')),
|
_sectionTitle(l10n.d('Kleuren')),
|
||||||
_colorSetting(
|
_colorSetting(
|
||||||
l10n.d('Achtergrond slides'),
|
l10n.d('Achtergrond slides'),
|
||||||
|
|
@ -638,7 +655,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: InputDecoration(
|
||||||
|
labelText: context.l10n.d('Logo px'),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
onChanged: (_) => _profileTouched = true,
|
onChanged: (_) => _profileTouched = true,
|
||||||
|
|
@ -754,7 +774,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
'$label $value',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
@ -767,33 +787,84 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
runSpacing: 6,
|
runSpacing: 6,
|
||||||
children: [
|
children: [
|
||||||
for (final color in _colorPresets)
|
for (final color in _colorPresets)
|
||||||
Tooltip(
|
_colorSwatch(
|
||||||
message: color,
|
color,
|
||||||
child: InkWell(
|
selected: value == color,
|
||||||
onTap: () => setState(() {
|
onTap: () => setState(() {
|
||||||
onChanged(color);
|
onChanged(color);
|
||||||
_profileTouched = true;
|
_profileTouched = true;
|
||||||
}),
|
}),
|
||||||
borderRadius: BorderRadius.circular(12),
|
),
|
||||||
child: Container(
|
],
|
||||||
width: 24,
|
),
|
||||||
height: 24,
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _colorSwatch(
|
||||||
|
String color, {
|
||||||
|
required bool selected,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
final parsed = _parseColor(color);
|
||||||
|
final checkColor = parsed.computeLuminance() > 0.55
|
||||||
|
? const Color(0xFF0F172A)
|
||||||
|
: Colors.white;
|
||||||
|
return Tooltip(
|
||||||
|
message: selected ? '${context.l10n.d('Geselecteerd')}: $color' : color,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 120),
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _parseColor(color),
|
color: selected
|
||||||
shape: BoxShape.circle,
|
? AppTheme.accent.withValues(alpha: 0.12)
|
||||||
|
: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: value == color
|
color: selected ? AppTheme.accent : const Color(0xFFCBD5E1),
|
||||||
? AppTheme.accent
|
width: selected ? 2 : 1,
|
||||||
: const Color(0xFFCBD5E1),
|
|
||||||
width: value == color ? 2 : 1,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: parsed,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Color(0x330F172A),
|
||||||
|
blurRadius: 2,
|
||||||
|
offset: Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (selected)
|
||||||
|
Icon(
|
||||||
|
Icons.check,
|
||||||
|
size: 16,
|
||||||
|
color: checkColor,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
color: checkColor == Colors.white
|
||||||
|
? Colors.black54
|
||||||
|
: Colors.white70,
|
||||||
|
blurRadius: 2,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,7 @@ class CollapsedPreviewBar extends ConsumerWidget {
|
||||||
RotatedBox(
|
RotatedBox(
|
||||||
quarterTurns: 1,
|
quarterTurns: 1,
|
||||||
child: Text(
|
child: Text(
|
||||||
'PREVIEW',
|
context.l10n.d('PREVIEW'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
letterSpacing: 1.5,
|
letterSpacing: 1.5,
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
required int initialIndex,
|
required int initialIndex,
|
||||||
TlpLevel tlp = TlpLevel.none,
|
TlpLevel tlp = TlpLevel.none,
|
||||||
}) async {
|
}) async {
|
||||||
|
final hadWakeLock = await _wakeLockEnabled();
|
||||||
|
await _enableWakeLock();
|
||||||
|
try {
|
||||||
await windowManager.setFullScreen(true);
|
await windowManager.setFullScreen(true);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
|
|
@ -57,12 +60,43 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
await _restoreWakeLock(hadWakeLock);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FullscreenPresenter> createState() => _FullscreenPresenterState();
|
State<FullscreenPresenter> createState() => _FullscreenPresenterState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> _wakeLockEnabled() async {
|
||||||
|
try {
|
||||||
|
return await WakelockPlus.enabled;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _enableWakeLock() async {
|
||||||
|
try {
|
||||||
|
await WakelockPlus.enable();
|
||||||
|
} catch (_) {
|
||||||
|
// Best-effort: unsupported platforms should not interrupt presenting.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _restoreWakeLock(bool enabledBeforePresentation) async {
|
||||||
|
try {
|
||||||
|
if (enabledBeforePresentation) {
|
||||||
|
await WakelockPlus.enable();
|
||||||
|
} else {
|
||||||
|
await WakelockPlus.disable();
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Best-effort cleanup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
late int _index;
|
late int _index;
|
||||||
late FocusNode _focusNode;
|
late FocusNode _focusNode;
|
||||||
|
|
@ -125,7 +159,6 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
_clockTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
_clockTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
if (mounted && _presenterView) setState(() {});
|
if (mounted && _presenterView) setState(() {});
|
||||||
});
|
});
|
||||||
_enableWakeLock();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
_loadDisplays();
|
_loadDisplays();
|
||||||
|
|
@ -138,28 +171,11 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
_advanceTimer?.cancel();
|
_advanceTimer?.cancel();
|
||||||
_clockTimer?.cancel();
|
_clockTimer?.cancel();
|
||||||
_typedTimer?.cancel();
|
_typedTimer?.cancel();
|
||||||
_disableWakeLock();
|
|
||||||
_gridScroll.dispose();
|
_gridScroll.dispose();
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _enableWakeLock() async {
|
|
||||||
try {
|
|
||||||
await WakelockPlus.enable();
|
|
||||||
} catch (_) {
|
|
||||||
// Best-effort: unsupported platforms should not interrupt presenting.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _disableWakeLock() async {
|
|
||||||
try {
|
|
||||||
await WakelockPlus.disable();
|
|
||||||
} catch (_) {
|
|
||||||
// Best-effort cleanup.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _scheduleAdvance() {
|
void _scheduleAdvance() {
|
||||||
_advanceTimer?.cancel();
|
_advanceTimer?.cancel();
|
||||||
_advanceTimer = null;
|
_advanceTimer = null;
|
||||||
|
|
@ -287,7 +303,6 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
|
|
||||||
Future<void> _exit() async {
|
Future<void> _exit() async {
|
||||||
_advanceTimer?.cancel();
|
_advanceTimer?.cancel();
|
||||||
await _disableWakeLock();
|
|
||||||
await windowManager.setFullScreen(false);
|
await windowManager.setFullScreen(false);
|
||||||
if (mounted) Navigator.pop(context);
|
if (mounted) Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:flutter_math_fork/flutter_math.dart';
|
||||||
import 'package:highlight/highlight.dart' show highlight;
|
import 'package:highlight/highlight.dart' show highlight;
|
||||||
import 'package:highlight/languages/all.dart' show allLanguages;
|
import 'package:highlight/languages/all.dart' show allLanguages;
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
import '../../l10n/app_localizations.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';
|
||||||
|
|
@ -154,6 +155,10 @@ class SlidePreviewWidget extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final hasBottomRightTlp =
|
||||||
|
tlp != TlpLevel.none &&
|
||||||
|
!((themeProfile.logoPath?.isNotEmpty == true && slide.showLogo) &&
|
||||||
|
themeProfile.logoPosition == 'bottom-right');
|
||||||
// Make the widget self-sufficient for text rendering. On screen it sits
|
// Make the widget self-sufficient for text rendering. On screen it sits
|
||||||
// inside a Material (which supplies a clean DefaultTextStyle), but the
|
// inside a Material (which supplies a clean DefaultTextStyle), but the
|
||||||
// export rasterizer mounts it in a bare Overlay subtree. Without an
|
// export rasterizer mounts it in a bare Overlay subtree. Without an
|
||||||
|
|
@ -172,7 +177,7 @@ class SlidePreviewWidget extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child: _SlideLinkScope(
|
child: _SlideLinkScope(
|
||||||
onTapLink: onLinkTap,
|
onTapLink: onLinkTap,
|
||||||
hasBottomTlp: tlp != TlpLevel.none,
|
hasBottomTlp: hasBottomRightTlp,
|
||||||
child: _buildSlide(),
|
child: _buildSlide(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -199,7 +204,14 @@ class SlidePreviewWidget extends StatelessWidget {
|
||||||
tlp: tlp,
|
tlp: tlp,
|
||||||
),
|
),
|
||||||
if (tlp != TlpLevel.none)
|
if (tlp != TlpLevel.none)
|
||||||
_TlpOverlay(tlp: tlp, w: w, profile: themeProfile),
|
_TlpOverlay(
|
||||||
|
tlp: tlp,
|
||||||
|
w: w,
|
||||||
|
profile: themeProfile,
|
||||||
|
hasLogo:
|
||||||
|
themeProfile.logoPath?.isNotEmpty == true &&
|
||||||
|
slide.showLogo,
|
||||||
|
),
|
||||||
if (themeProfile.logoPath?.isNotEmpty == true && slide.showLogo)
|
if (themeProfile.logoPath?.isNotEmpty == true && slide.showLogo)
|
||||||
_LogoOverlay(
|
_LogoOverlay(
|
||||||
logoPath: themeProfile.logoPath!,
|
logoPath: themeProfile.logoPath!,
|
||||||
|
|
@ -502,6 +514,7 @@ class _TitlePreview extends StatelessWidget {
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
_zoomedImage(
|
_zoomedImage(
|
||||||
|
context,
|
||||||
slide.imagePath,
|
slide.imagePath,
|
||||||
projectPath,
|
projectPath,
|
||||||
slide.imageSize,
|
slide.imageSize,
|
||||||
|
|
@ -1065,13 +1078,8 @@ class _BulletsImagePreview extends StatelessWidget {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
_resolvedImage(slide.imagePath, projectPath),
|
_resolvedImage(context, slide.imagePath, projectPath),
|
||||||
_captionOverlay(
|
_captionOverlay(context, slide.imageCaption, w),
|
||||||
context,
|
|
||||||
slide.imageCaption,
|
|
||||||
w,
|
|
||||||
right: w * 0.018,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -1449,7 +1457,7 @@ class _TwoImagesPreview extends StatelessWidget {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
_resolvedImage(slide.imagePath, projectPath),
|
_resolvedImage(context, slide.imagePath, projectPath),
|
||||||
_captionOverlay(context, slide.imageCaption, w),
|
_captionOverlay(context, slide.imageCaption, w),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -1459,7 +1467,7 @@ class _TwoImagesPreview extends StatelessWidget {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
_resolvedImage(slide.imagePath2, projectPath),
|
_resolvedImage(context, slide.imagePath2, projectPath),
|
||||||
_captionOverlay(context, slide.imageCaption2, w),
|
_captionOverlay(context, slide.imageCaption2, w),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -1524,6 +1532,7 @@ class _ImagePreview extends StatelessWidget {
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
_zoomedImage(
|
_zoomedImage(
|
||||||
|
context,
|
||||||
slide.imagePath,
|
slide.imagePath,
|
||||||
projectPath,
|
projectPath,
|
||||||
slide.imageSize,
|
slide.imageSize,
|
||||||
|
|
@ -1792,6 +1801,7 @@ class _QuotePreview extends StatelessWidget {
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
_zoomedImage(
|
_zoomedImage(
|
||||||
|
context,
|
||||||
slide.imagePath,
|
slide.imagePath,
|
||||||
projectPath,
|
projectPath,
|
||||||
slide.imageSize,
|
slide.imageSize,
|
||||||
|
|
@ -1831,7 +1841,12 @@ class _LogoOverlay extends StatelessWidget {
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
child: _resolvedImage(logoPath, projectPath, fit: BoxFit.contain),
|
child: _resolvedImage(
|
||||||
|
context,
|
||||||
|
logoPath,
|
||||||
|
projectPath,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -2047,6 +2062,7 @@ void _ensureHighlightLanguages() {
|
||||||
/// imageSize > 100 → inzoomen: groter dan contain, bijgesneden door ClipRect
|
/// imageSize > 100 → inzoomen: groter dan contain, bijgesneden door ClipRect
|
||||||
/// imageSize < 100 → nog meer uitzoomen: afbeelding kleiner dan contain
|
/// imageSize < 100 → nog meer uitzoomen: afbeelding kleiner dan contain
|
||||||
Widget _zoomedImage(
|
Widget _zoomedImage(
|
||||||
|
BuildContext context,
|
||||||
String imagePath,
|
String imagePath,
|
||||||
String? projectPath,
|
String? projectPath,
|
||||||
int imageSize, {
|
int imageSize, {
|
||||||
|
|
@ -2054,7 +2070,11 @@ Widget _zoomedImage(
|
||||||
Alignment alignment = Alignment.center,
|
Alignment alignment = Alignment.center,
|
||||||
}) {
|
}) {
|
||||||
if (imageSize == 0) {
|
if (imageSize == 0) {
|
||||||
return _resolvedImage(imagePath, projectPath); // BoxFit.cover standaard
|
return _resolvedImage(
|
||||||
|
context,
|
||||||
|
imagePath,
|
||||||
|
projectPath,
|
||||||
|
); // BoxFit.cover standaard
|
||||||
}
|
}
|
||||||
final scale = imageSize / 100.0;
|
final scale = imageSize / 100.0;
|
||||||
// Size the image box to `scale` × the available area and let BoxFit.contain
|
// Size the image box to `scale` × the available area and let BoxFit.contain
|
||||||
|
|
@ -2076,6 +2096,7 @@ Widget _zoomedImage(
|
||||||
height: boxH,
|
height: boxH,
|
||||||
// BoxFit.contain: toont de volledige afbeelding zonder bijsnijden
|
// BoxFit.contain: toont de volledige afbeelding zonder bijsnijden
|
||||||
child: _resolvedImage(
|
child: _resolvedImage(
|
||||||
|
context,
|
||||||
imagePath,
|
imagePath,
|
||||||
projectPath,
|
projectPath,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
|
|
@ -2089,11 +2110,12 @@ Widget _zoomedImage(
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _resolvedImage(
|
Widget _resolvedImage(
|
||||||
|
BuildContext context,
|
||||||
String imagePath,
|
String imagePath,
|
||||||
String? projectPath, {
|
String? projectPath, {
|
||||||
BoxFit fit = BoxFit.cover,
|
BoxFit fit = BoxFit.cover,
|
||||||
}) {
|
}) {
|
||||||
if (imagePath.isEmpty) return _imagePlaceholder();
|
if (imagePath.isEmpty) return _imagePlaceholder(context);
|
||||||
|
|
||||||
final String resolved;
|
final String resolved;
|
||||||
if (imagePath.startsWith('/') || imagePath.contains(':\\')) {
|
if (imagePath.startsWith('/') || imagePath.contains(':\\')) {
|
||||||
|
|
@ -2109,7 +2131,7 @@ Widget _resolvedImage(
|
||||||
fit: fit,
|
fit: fit,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
errorBuilder: (context, error, stackTrace) => _imagePlaceholder(),
|
errorBuilder: (context, error, stackTrace) => _imagePlaceholder(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2128,8 +2150,8 @@ Widget _captionOverlay(
|
||||||
? _tlpVerticalReserve(w)
|
? _tlpVerticalReserve(w)
|
||||||
: 0.0;
|
: 0.0;
|
||||||
return Positioned(
|
return Positioned(
|
||||||
right: right ?? w * 0.018,
|
right: right ?? w * _kTlpEdge,
|
||||||
bottom: (bottom ?? w * 0.014) + lift,
|
bottom: (bottom ?? _tlpBottomInset(w)) + lift,
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxWidth: w * 0.5),
|
constraints: BoxConstraints(maxWidth: w * 0.5),
|
||||||
padding: EdgeInsets.symmetric(horizontal: w * 0.008, vertical: w * 0.005),
|
padding: EdgeInsets.symmetric(horizontal: w * 0.008, vertical: w * 0.005),
|
||||||
|
|
@ -2165,13 +2187,15 @@ const double _kTlpEdge = 0.025; // afstand tot de slidehoek (× breedte)
|
||||||
const double _kTlpHPad = 0.011;
|
const double _kTlpHPad = 0.011;
|
||||||
const double _kTlpVPad = 0.005;
|
const double _kTlpVPad = 0.005;
|
||||||
|
|
||||||
|
double _tlpBottomInset(double w) => w * 0.022;
|
||||||
|
|
||||||
/// Geschatte breedte van de TLP-badge, zodat de footer ervoor kan uitwijken.
|
/// Geschatte breedte van de TLP-badge, zodat de footer ervoor kan uitwijken.
|
||||||
double _tlpBadgeWidth(double w, TlpLevel tlp) =>
|
double _tlpBadgeWidth(double w, TlpLevel tlp) =>
|
||||||
tlp.label.length * w * _kTlpFont * 0.62 + 2 * (w * _kTlpHPad);
|
tlp.label.length * w * _kTlpFont * 0.62 + 2 * (w * _kTlpHPad);
|
||||||
|
|
||||||
/// Verticale ruimte die een TLP-badge rechtsonder inneemt (voor bijschriften).
|
/// Verticale ruimte die een TLP-badge rechtsonder inneemt (voor bijschriften).
|
||||||
double _tlpVerticalReserve(double w) =>
|
double _tlpVerticalReserve(double w) =>
|
||||||
w * _kTlpFont + 2 * (w * _kTlpVPad) + w * 0.014;
|
w * _kTlpFont + 2 * (w * _kTlpVPad) + _tlpBottomInset(w);
|
||||||
|
|
||||||
/// Officiële TLP 2.0-markering (FIRST): de gekleurde label op een zwart vlak,
|
/// Officiële TLP 2.0-markering (FIRST): de gekleurde label op een zwart vlak,
|
||||||
/// rechtsonder. Wijkt uit naar linksonder als het logo rechtsonder staat.
|
/// rechtsonder. Wijkt uit naar linksonder als het logo rechtsonder staat.
|
||||||
|
|
@ -2179,18 +2203,20 @@ class _TlpOverlay extends StatelessWidget {
|
||||||
final TlpLevel tlp;
|
final TlpLevel tlp;
|
||||||
final double w;
|
final double w;
|
||||||
final ThemeProfile profile;
|
final ThemeProfile profile;
|
||||||
|
final bool hasLogo;
|
||||||
|
|
||||||
const _TlpOverlay({
|
const _TlpOverlay({
|
||||||
required this.tlp,
|
required this.tlp,
|
||||||
required this.w,
|
required this.w,
|
||||||
required this.profile,
|
required this.profile,
|
||||||
|
required this.hasLogo,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final toLeft = profile.logoPosition == 'bottom-right';
|
final toLeft = hasLogo && profile.logoPosition == 'bottom-right';
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: w * 0.022,
|
bottom: _tlpBottomInset(w),
|
||||||
left: toLeft ? w * _kTlpEdge : null,
|
left: toLeft ? w * _kTlpEdge : null,
|
||||||
right: toLeft ? null : w * _kTlpEdge,
|
right: toLeft ? null : w * _kTlpEdge,
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|
@ -2306,7 +2332,7 @@ class _FooterOverlay extends StatelessWidget {
|
||||||
final logoOnLeft = profile.logoPosition.endsWith('left');
|
final logoOnLeft = profile.logoPosition.endsWith('left');
|
||||||
final logoSpan = w * (profile.logoSize / 1280) * 1.28 + w * 0.012;
|
final logoSpan = w * (profile.logoSize / 1280) * 1.28 + w * 0.012;
|
||||||
final logoLeftEdge = w * (profile.logoSize / 1280) * 0.28;
|
final logoLeftEdge = w * (profile.logoSize / 1280) * 0.28;
|
||||||
final tlpOnRight = profile.logoPosition != 'bottom-right';
|
final tlpOnRight = !(hasLogo && profile.logoPosition == 'bottom-right');
|
||||||
final tlpSpan = tlp == TlpLevel.none
|
final tlpSpan = tlp == TlpLevel.none
|
||||||
? 0.0
|
? 0.0
|
||||||
: w * _kTlpEdge + _tlpBadgeWidth(w, tlp) + w * 0.012;
|
: w * _kTlpEdge + _tlpBadgeWidth(w, tlp) + w * 0.012;
|
||||||
|
|
@ -2403,18 +2429,18 @@ Widget _mediaPlaceholder(IconData icon, String label) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _imagePlaceholder() {
|
Widget _imagePlaceholder(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
color: const Color(0xFFE2E8F0),
|
color: const Color(0xFFE2E8F0),
|
||||||
child: const Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.image_outlined, color: Color(0xFF94A3B8), size: 24),
|
const Icon(Icons.image_outlined, color: Color(0xFF94A3B8), size: 24),
|
||||||
SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Afbeelding',
|
context.l10n.d('Afbeelding'),
|
||||||
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
|
style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:ocideck/l10n/app_localizations.dart';
|
import 'package:ocideck/l10n/app_localizations.dart';
|
||||||
|
|
@ -32,4 +34,96 @@ void main() {
|
||||||
);
|
);
|
||||||
expect(AppLocalizations.materialLocaleFor('pap'), const Locale('en'));
|
expect(AppLocalizations.materialLocaleFor('pap'), const Locale('en'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('all literal Dutch source strings have an English fallback', () {
|
||||||
|
AppLocalizations.setActiveLanguageCode('en');
|
||||||
|
|
||||||
|
const unchangedInEnglish = {
|
||||||
|
'Accent / bullets',
|
||||||
|
'Bullet',
|
||||||
|
'Coverflow',
|
||||||
|
'Logo',
|
||||||
|
'Logo px',
|
||||||
|
'PREVIEW',
|
||||||
|
'Preview',
|
||||||
|
'SLIDES',
|
||||||
|
'Slide',
|
||||||
|
'slide',
|
||||||
|
};
|
||||||
|
final expression = RegExp(r'''\.d\(\s*('(?:\\.|[^'])*'|"(?:\\.|[^"])*")''');
|
||||||
|
final files = Directory('lib')
|
||||||
|
.listSync(recursive: true)
|
||||||
|
.whereType<File>()
|
||||||
|
.where((file) => file.path.endsWith('.dart'));
|
||||||
|
final sources = <String>{};
|
||||||
|
|
||||||
|
for (final file in files) {
|
||||||
|
final content = file.readAsStringSync();
|
||||||
|
for (final match in expression.allMatches(content)) {
|
||||||
|
sources.add(_unquoteDartString(match.group(1)!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final english = const AppLocalizations(Locale('en'));
|
||||||
|
final missing = sources.where((source) {
|
||||||
|
final translated = english.d(source);
|
||||||
|
return translated == source && !unchangedInEnglish.contains(source);
|
||||||
|
}).toList()..sort();
|
||||||
|
|
||||||
|
expect(missing, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('all literal Dutch source strings are translated in every language', () {
|
||||||
|
const unchangedInAllLanguages = {
|
||||||
|
'Accent / bullets',
|
||||||
|
'Bullet',
|
||||||
|
'Coverflow',
|
||||||
|
'Logo',
|
||||||
|
'Logo px',
|
||||||
|
'PREVIEW',
|
||||||
|
'Preview',
|
||||||
|
'SLIDES',
|
||||||
|
'Slide',
|
||||||
|
'slide',
|
||||||
|
};
|
||||||
|
final expression = RegExp(r'''\.d\(\s*('(?:\\.|[^'])*'|"(?:\\.|[^"])*")''');
|
||||||
|
final files = Directory('lib')
|
||||||
|
.listSync(recursive: true)
|
||||||
|
.whereType<File>()
|
||||||
|
.where((file) => file.path.endsWith('.dart'));
|
||||||
|
final sources = <String>{};
|
||||||
|
|
||||||
|
for (final file in files) {
|
||||||
|
final content = file.readAsStringSync();
|
||||||
|
for (final match in expression.allMatches(content)) {
|
||||||
|
sources.add(_unquoteDartString(match.group(1)!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final missingByLanguage = <String, List<String>>{};
|
||||||
|
for (final languageCode in AppLocalizations.languageNames.keys) {
|
||||||
|
if (languageCode == 'nl') continue;
|
||||||
|
final missing = sources.where((source) {
|
||||||
|
if (unchangedInAllLanguages.contains(source)) return false;
|
||||||
|
return !AppLocalizations.hasDirectDutchSourceTranslation(
|
||||||
|
languageCode,
|
||||||
|
source,
|
||||||
|
);
|
||||||
|
}).toList()..sort();
|
||||||
|
if (missing.isNotEmpty) missingByLanguage[languageCode] = missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(missingByLanguage, isEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _unquoteDartString(String value) {
|
||||||
|
final quote = value[0];
|
||||||
|
final body = value.substring(1, value.length - 1);
|
||||||
|
return body
|
||||||
|
.replaceAll(r'\\', r'\')
|
||||||
|
.replaceAll('\\$quote', quote)
|
||||||
|
.replaceAll(r'\n', '\n')
|
||||||
|
.replaceAll(r'\r', '\r')
|
||||||
|
.replaceAll(r'\t', '\t');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,13 @@ void main() {
|
||||||
expect(n.state.deck!.paginate, isFalse);
|
expect(n.state.deck!.paginate, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('updateInfo can update the presentation title', () {
|
||||||
|
final n = _notifier()..newDeck('D');
|
||||||
|
n.updateInfo(title: 'Nieuwe presentatietitel', author: 'Auteur');
|
||||||
|
expect(n.state.deck!.title, 'Nieuwe presentatietitel');
|
||||||
|
expect(n.state.deck!.author, 'Auteur');
|
||||||
|
});
|
||||||
|
|
||||||
test('generateMarkdown and applyMarkdown round-trip the deck', () {
|
test('generateMarkdown and applyMarkdown round-trip the deck', () {
|
||||||
final n = _notifier()..newDeck('D');
|
final n = _notifier()..newDeck('D');
|
||||||
n.addSlide(SlideType.bulletsImage, afterIndex: 0);
|
n.addSlide(SlideType.bulletsImage, afterIndex: 0);
|
||||||
|
|
|
||||||
|
|
@ -63,5 +63,39 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.textContaining('TLP:'), findsNothing);
|
expect(find.textContaining('TLP:'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('right-side image caption aligns with the TLP badge', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
const caption = 'Foto: iemand';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 800,
|
||||||
|
height: 450,
|
||||||
|
child: SlidePreviewWidget(
|
||||||
|
slide: Slide.create(
|
||||||
|
SlideType.bulletsImage,
|
||||||
|
).copyWith(title: 'T', bullets: ['a'], imageCaption: caption),
|
||||||
|
tlp: TlpLevel.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final captionRight = tester.getTopRight(find.text(caption)).dx;
|
||||||
|
final tlpRight = tester.getTopRight(find.text('TLP:RED')).dx;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(captionRight - tlpRight).abs(),
|
||||||
|
lessThan(4),
|
||||||
|
reason: 'Caption and TLP badge should share the same right edge.',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue