Ocideck/lib/widgets/shell/status_bar.dart

317 lines
9.5 KiB
Dart
Raw Normal View History

// Part of the app_shell library — see ../app_shell.dart.
// Split out for navigability; all imports live in the main library file.
part of '../app_shell.dart';
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!)}';
final theme = Theme.of(context);
return Material(
color: theme.colorScheme.surface,
child: Container(
height: 30,
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: theme.colorScheme.outlineVariant),
),
),
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 ?? Theme.of(context).colorScheme.onSurfaceVariant;
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 ?? Theme.of(context).colorScheme.secondary)
: Theme.of(context).disabledColor;
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: Theme.of(context).colorScheme.outlineVariant,
);
}
}
/// Dunne verticale scheiding tussen groepen AppBar-knoppen.
class _ActionsDivider extends StatelessWidget {
const _ActionsDivider();
@override
Widget build(BuildContext context) {
return Container(
width: 1,
height: 20,
margin: const EdgeInsets.symmetric(horizontal: 6),
color: Colors.white24,
);
}
}
/// TLP-classificatie als altijd zichtbare, direct instelbare chip in de
/// AppBar-titel. Toont de huidige status in de officiële TLP-kleur en opent
/// bij klikken een keuzelijst met alle niveaus (incl. "Geen").
class _TlpChip extends StatelessWidget {
final TlpLevel tlp;
final ValueChanged<TlpLevel> onSelected;
const _TlpChip({required this.tlp, required this.onSelected});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final isSet = tlp != TlpLevel.none;
final fg = Color(tlp.foreground);
final child = Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
decoration: BoxDecoration(
color: isSet ? Colors.black : Colors.transparent,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: isSet ? fg.withValues(alpha: 0.7) : Colors.white24,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!isSet)
const Icon(Icons.shield_outlined, size: 14, color: Colors.white70),
if (!isSet) const SizedBox(width: 5),
Text(
isSet ? tlp.label : 'TLP',
style: TextStyle(
color: isSet ? fg : Colors.white70,
fontSize: 11.5,
fontWeight: FontWeight.w700,
fontFamily: 'monospace',
fontFamilyFallback: const ['Menlo', 'Consolas', 'Courier New'],
letterSpacing: 0.3,
),
),
Icon(
Icons.arrow_drop_down,
size: 16,
color: isSet ? fg : Colors.white54,
),
],
),
);
return PopupMenuButton<TlpLevel>(
tooltip: l10n.d('TLP-classificatie (Traffic Light Protocol)'),
position: PopupMenuPosition.under,
onSelected: onSelected,
itemBuilder: (_) => [
for (final level in TlpLevel.values)
PopupMenuItem<TlpLevel>(
value: level,
child: Row(
children: [
Container(
width: 14,
height: 14,
decoration: BoxDecoration(
color: level == TlpLevel.none
? Colors.transparent
: Color(level.foreground),
border: Border.all(color: const Color(0xFF94A3B8)),
borderRadius: BorderRadius.circular(3),
),
),
const SizedBox(width: 10),
Text(level == TlpLevel.none ? l10n.d('Geen') : level.label),
if (level == tlp) ...[
const SizedBox(width: 12),
const Spacer(),
const Icon(Icons.check, size: 16, color: Color(0xFF475569)),
],
],
),
),
],
child: child,
);
}
}