flutter_desktop_notifications 1.1.2 copy "flutter_desktop_notifications: ^1.1.2" to clipboard
flutter_desktop_notifications: ^1.1.2 copied to clipboard

Native desktop notifications for Flutter on Windows, macOS, and Linux. One unified API for title, body, images, buttons, replies, and click/dismiss callbacks.

example/lib/main.dart

import 'dart:io' show Platform;
import 'dart:ui' show ImageFilter;

import 'package:flutter/material.dart';
import 'package:flutter_desktop_notifications/flutter_desktop_notifications.dart';

const _aumid = 'io.luminest.flutter_desktop_notifications.example';
const _displayName = 'Windows Notification Demo';
const _heroSize = Size(364, 180);

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // Unpackaged Windows apps need a registered AUMID or Windows drops the toast.
  // No-op on macOS and Linux, where the API isn't available.
  if (Platform.isWindows) {
    await WindowsNotification.registerAumid(
      aumid: _aumid,
      displayName: _displayName,
    );
  }
  runApp(const ExampleApp());
}

class ExampleApp extends StatefulWidget {
  const ExampleApp({super.key});

  @override
  State<ExampleApp> createState() => _ExampleAppState();
}

class _ExampleAppState extends State<ExampleApp> {
  /// Selectable accent colors for the demo's theme.
  static const List<(String, Color)> swatches = [
    ('Violet', Color(0xFF7C4DFF)),
    ('Indigo', Color(0xFF3F51B5)),
    ('Blue', Color(0xFF2196F3)),
    ('Teal', Color(0xFF009688)),
    ('Green', Color(0xFF43A047)),
    ('Amber', Color(0xFFFFB300)),
    ('Orange', Color(0xFFFB8C00)),
    ('Rose', Color(0xFFEC407A)),
  ];

  Color _seed = swatches.first.$2;
  ThemeMode _mode = ThemeMode.light;

  ThemeData _theme(Brightness brightness) => ThemeData(
        colorScheme:
            ColorScheme.fromSeed(seedColor: _seed, brightness: brightness),
        useMaterial3: true,
      );

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'flutter_desktop_notifications demo',
      theme: _theme(Brightness.light),
      darkTheme: _theme(Brightness.dark),
      themeMode: _mode,
      home: HomePage(
        swatches: swatches,
        seed: _seed,
        isDark: _mode == ThemeMode.dark,
        onSeedChanged: (c) => setState(() => _seed = c),
        onDarkChanged: (d) =>
            setState(() => _mode = d ? ThemeMode.dark : ThemeMode.light),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  final List<(String, Color)> swatches;
  final Color seed;
  final bool isDark;
  final ValueChanged<Color> onSeedChanged;
  final ValueChanged<bool> onDarkChanged;

  const HomePage({
    super.key,
    required this.swatches,
    required this.seed,
    required this.isDark,
    required this.onSeedChanged,
    required this.onDarkChanged,
  });

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  // Windows-only rich API (scenarios, audio, progress, custom XML, hero images).
  final _notifier = WindowsNotification(applicationId: _aumid);
  // Cross-platform API (Windows, macOS, Linux).
  final _desktop = DesktopNotifier(appName: _displayName, appId: _aumid);
  String _status = 'No event yet — fire a notification above, then click it.';

  @override
  void initState() {
    super.initState();
    // On Windows both notifiers share the same native callback channel, so one
    // handler covers everything.
    _desktop.requestPermission();
    _desktop.setCallback(_onEvent);
  }

  void _onEvent(NotificationCallbackDetails details) {
    final parts = <String>['id=${details.message.id}'];
    if (details.arguments != null) parts.add('args=${details.arguments}');
    if (details.userInput.isNotEmpty) parts.add('input=${details.userInput}');
    setState(() => _status = '${_eventLabel(details.event)}  ·  '
        '${parts.join("  ·  ")}');

    // "Open" buttons should pull the app back to the front.
    if (details.event == NotificationEvent.activated &&
        details.arguments == 'action:open') {
      WindowsNotification.bringAppToForeground();
    }
  }

  String _eventLabel(NotificationEvent e) => switch (e) {
        NotificationEvent.activated => 'ACTIVATED',
        NotificationEvent.dismissedByUser => 'DISMISSED',
        NotificationEvent.dismissedByApp => 'HIDDEN',
        NotificationEvent.dismissedByTimeout => 'TIMED OUT',
      };

  /// Sets a status line, runs [action], and reports any failure.
  Future<void> _run(String fired, Future<void> Function() action) async {
    setState(() => _status = fired);
    try {
      await action();
    } catch (e) {
      if (mounted) setState(() => _status = 'Error: $e');
    }
  }

  // ---- Cross-platform (DesktopNotifier): Windows, macOS, Linux ----

  Future<void> _xSimple() => _run(
        'Fired a cross-platform notification.',
        () => _desktop.show(
          NotificationMessage.fromPluginTemplate(
            'x-simple',
            'Build complete',
            'Works the same on Windows, macOS, and Linux.',
          ),
        ),
      );

  Future<void> _xButtons() => _run(
        'Fired a notification with action buttons.',
        () => _desktop.show(
          NotificationMessage.fromPluginTemplate(
            'x-buttons',
            'Update available',
            'Version 2.0 is ready to install.',
            actions: const [
              NotificationAction(
                  content: 'Install', arguments: 'action:install'),
              NotificationAction(content: 'Later', arguments: 'action:later'),
            ],
          ),
        ),
      );

  Future<void> _xReply() => _run(
        'Fired a notification with a reply field (Windows and macOS).',
        () => _desktop.show(
          NotificationMessage.fromPluginTemplate(
            'x-reply',
            'Ada Lovelace',
            'Want to grab lunch at 12:30?',
            inputs: const [
              NotificationInput.text(id: 'reply', placeholder: 'Reply…'),
            ],
            actions: const [
              NotificationAction(
                content: 'Reply',
                arguments: 'action:reply',
                inputId: 'reply',
              ),
            ],
          ),
        ),
      );

  Future<void> _xCancelAll() =>
      _run('Cleared delivered notifications.', _desktop.cancelAll);

  // ---- Windows-only (WindowsNotification) ----

  Future<void> _simpleToast() => _run(
        'Fired the Simple toast — title and body only.',
        () => _notifier.showNotificationPluginTemplate(
          NotificationMessage.fromPluginTemplate(
            'simple',
            'Build complete',
            'flutter build windows finished in 34.0s.',
          ),
        ),
      );

  Future<void> _replyToast() => _run(
        'Fired the Reply toast — type something, then click Reply.',
        () => _notifier.showNotificationPluginTemplate(
          NotificationMessage.fromPluginTemplate(
            'reply',
            'Ada Lovelace',
            'Want to grab lunch at 12:30?',
            inputs: const [
              NotificationInput.text(id: 'reply', placeholder: 'Quick reply…'),
            ],
            actions: const [
              NotificationAction(
                content: 'Reply',
                arguments: 'action:reply',
                inputId: 'reply',
              ),
              NotificationAction(
                content: 'Dismiss',
                arguments: 'action:dismiss',
                buttonStyle: NotificationButtonStyle.critical,
              ),
            ],
          ),
        ),
      );

  Future<void> _linkToast() => _run(
        'Fired a toast that opens flutter.dev in your browser.',
        () => _notifier.showNotificationPluginTemplate(
          NotificationMessage.fromPluginTemplate(
            'link',
            'Flutter',
            'Tap the toast body to open flutter.dev.',
            launch: 'https://flutter.dev',
            activationType: NotificationActivationType.protocol,
          ),
        ),
      );

  Future<void> _reminderToast() => _run(
        'Fired a Reminder — it stays until you act on it.',
        () => _notifier.showNotificationPluginTemplate(
          NotificationMessage.fromPluginTemplate(
            'reminder',
            'Leave for meeting',
            'Design review · Room 2001',
            scenario: NotificationScenario.reminder,
            extraTexts: const [
              NotificationText('10:30 to 11:00 AM',
                  style: NotificationTextStyle.captionSubtle),
            ],
            attribution: 'Calendar',
          ),
        ),
      );

  Future<void> _alarmToast() => _run(
        'Fired an Alarm — loops its sound until dismissed.',
        () => _notifier.showNotificationPluginTemplate(
          NotificationMessage.fromPluginTemplate(
            'alarm',
            'Morning alarm',
            '7:00 AM',
            scenario: NotificationScenario.alarm,
            duration: NotificationDuration.long,
            audio: const NotificationAudio(
              sound: NotificationSound.Alarm,
              loop: true,
            ),
            actions: const [
              NotificationAction(content: 'Snooze', arguments: 'action:snooze'),
              NotificationAction(
                content: 'Dismiss',
                arguments: 'action:dismiss',
                buttonStyle: NotificationButtonStyle.critical,
              ),
            ],
          ),
        ),
      );

  Future<void> _urgentToast() => _run(
        'Fired an Urgent toast — bypasses Focus Assist.',
        () => _notifier.showNotificationPluginTemplate(
          NotificationMessage.fromPluginTemplate(
            'urgent',
            'Production down',
            'api.example.com · 503 for 2 minutes.',
            scenario: NotificationScenario.urgent,
            audio: const NotificationAudio(sound: NotificationSound.Alarm2),
            actions: const [
              NotificationAction(
                content: 'Open dashboard',
                arguments: 'action:open',
                buttonStyle: NotificationButtonStyle.success,
              ),
              NotificationAction(
                content: 'Acknowledge',
                arguments: 'action:ack',
              ),
            ],
          ),
        ),
      );

  Future<void> _silentToast() => _run(
        'Fired a Silent toast — no sound, with an attribution line.',
        () => _notifier.showNotificationPluginTemplate(
          NotificationMessage.fromPluginTemplate(
            'silent',
            'Backup complete',
            '12.4 GB synced to OneDrive.',
            audio: const NotificationAudio.silent(),
            attribution: 'OneDrive',
          ),
        ),
      );

  Future<void> _progressToast() => _run(
        'Fired a Progress toast — shows a determinate progress bar.',
        () => _notifier.showNotificationPluginTemplate(
          NotificationMessage.fromPluginTemplate(
            'progress',
            'Downloading update',
            'v1.0.0 · 1.2 GB',
            progress: const NotificationProgress(
              title: 'Update installer',
              value: 0.42,
              valueStringOverride: '504 MB of 1.2 GB',
              status: 'Downloading…',
            ),
          ),
        ),
      );

  Future<void> _customXmlToast() => _run(
        'Fired a toast from hand-written toast XML.',
        () => _notifier.showNotificationCustomTemplate(
          NotificationMessage.fromCustomTemplate('meeting-xml', group: 'demos'),
          _meetingTemplate,
        ),
      );

  Future<void> _heroToast() => _run(
        'Rendered a Flutter widget and used it as the hero image.',
        () async {
          final theme = Theme.of(context);
          final path = await WidgetToImage.toPngFile(
            widget: const _MessageHeroCard(),
            size: _heroSize,
            pixelRatio: 2.0,
            theme: theme,
          );
          await _notifier.showNotificationPluginTemplate(
            NotificationMessage.fromPluginTemplate(
              'hero',
              'Ada Lovelace',
              'Want to grab lunch at 12:30?',
              heroImage: path,
              group: 'demos',
              actions: const [
                NotificationAction(
                  content: 'Open',
                  arguments: 'action:open',
                  buttonStyle: NotificationButtonStyle.success,
                ),
                NotificationAction(
                  content: 'Dismiss',
                  arguments: 'action:dismiss',
                ),
              ],
            ),
          );
        },
      );

  Future<void> _removeGroup() => _run(
        'Removed the "demos" group from the Action Center.',
        () => _notifier.removeNotificationGroup('demos'),
      );

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    final dark = Theme.of(context).brightness == Brightness.dark;
    final gradient = dark
        ? [
            Color.alphaBlend(cs.primary.withValues(alpha: 0.30), cs.surface),
            Color.alphaBlend(cs.tertiary.withValues(alpha: 0.26), cs.surface),
          ]
        : [cs.primary, cs.tertiary];

    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: gradient,
          ),
        ),
        child: SafeArea(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: SizedBox.expand(child: _panel(cs)),
          ),
        ),
      ),
    );
  }

  Widget _panel(ColorScheme cs) {
    return ClipRRect(
      borderRadius: BorderRadius.circular(28),
      child: BackdropFilter(
        filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
        child: Container(
          decoration: BoxDecoration(
            color: cs.surface.withValues(alpha: 0.82),
            borderRadius: BorderRadius.circular(28),
            border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.6)),
            boxShadow: [
              BoxShadow(
                color: Colors.black.withValues(alpha: 0.16),
                blurRadius: 32,
                offset: const Offset(0, 14),
              ),
            ],
          ),
          padding: const EdgeInsets.all(28),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Expanded(
                child: SingleChildScrollView(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      _header(cs),
                      const SizedBox(height: 28),
                      _themeControls(cs),
                      const SizedBox(height: 28),
                      _buttons(cs),
                      const SizedBox(height: 18),
                      _maintenanceRow(cs),
                      const SizedBox(height: 22),
                      _helpCard(cs),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 18),
              _resultCard(cs),
            ],
          ),
        ),
      ),
    );
  }

  Widget _header(ColorScheme cs) {
    return Row(
      children: [
        Container(
          padding: const EdgeInsets.all(14),
          decoration: BoxDecoration(
            color: cs.primaryContainer,
            borderRadius: BorderRadius.circular(18),
          ),
          child: Icon(Icons.notifications_active_rounded,
              color: cs.onPrimaryContainer, size: 30),
        ),
        const SizedBox(width: 18),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                'flutter_desktop_notifications',
                style: Theme.of(context)
                    .textTheme
                    .headlineSmall
                    ?.copyWith(fontWeight: FontWeight.w700),
              ),
              const SizedBox(height: 2),
              Text(
                'Native desktop notifications for Windows, macOS, and Linux.',
                style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                      color: cs.onSurfaceVariant,
                    ),
              ),
            ],
          ),
        ),
      ],
    );
  }

  Widget _themeControls(ColorScheme cs) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('Accent color',
                  style: Theme.of(context).textTheme.labelMedium?.copyWith(
                        color: cs.onSurfaceVariant,
                      )),
              const SizedBox(height: 10),
              Wrap(
                spacing: 10,
                runSpacing: 10,
                children: [
                  for (final (name, color) in widget.swatches)
                    _SwatchDot(
                      name: name,
                      color: color,
                      selected: widget.seed.toARGB32() == color.toARGB32(),
                      onTap: () => widget.onSeedChanged(color),
                    ),
                ],
              ),
            ],
          ),
        ),
        const SizedBox(width: 20),
        Column(
          crossAxisAlignment: CrossAxisAlignment.end,
          children: [
            Text('Appearance',
                style: Theme.of(context).textTheme.labelMedium?.copyWith(
                      color: cs.onSurfaceVariant,
                    )),
            const SizedBox(height: 10),
            SegmentedButton<bool>(
              showSelectedIcon: false,
              segments: const [
                ButtonSegment(
                  value: false,
                  icon: Icon(Icons.light_mode_outlined, size: 18),
                  label: Text('Light'),
                ),
                ButtonSegment(
                  value: true,
                  icon: Icon(Icons.dark_mode_outlined, size: 18),
                  label: Text('Dark'),
                ),
              ],
              selected: {widget.isDark},
              onSelectionChanged: (s) => widget.onDarkChanged(s.first),
            ),
          ],
        ),
      ],
    );
  }

  Widget _buttons(ColorScheme cs) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Text('Cross-platform',
            style: Theme.of(context)
                .textTheme
                .titleMedium
                ?.copyWith(fontWeight: FontWeight.w600)),
        const SizedBox(height: 4),
        Text('DesktopNotifier — one API for Windows, macOS, and Linux.',
            style: Theme.of(context).textTheme.bodySmall),
        const SizedBox(height: 14),
        _crossPlatformButtons(cs),
        if (Platform.isWindows) ...[
          const SizedBox(height: 26),
          Text('Windows extras',
              style: Theme.of(context)
                  .textTheme
                  .titleMedium
                  ?.copyWith(fontWeight: FontWeight.w600)),
          const SizedBox(height: 4),
          Text(
            'WindowsNotification — scenarios, audio, progress, custom XML, '
            'and hero images.',
            style: Theme.of(context).textTheme.bodySmall,
          ),
          const SizedBox(height: 14),
          LayoutBuilder(
            builder: (context, constraints) {
              final cols = (constraints.maxWidth / 300).floor().clamp(1, 4);
              final cellWidth = (constraints.maxWidth - (cols - 1) * 14) / cols;
              return Wrap(
                spacing: 14,
                runSpacing: 14,
                children: [
                  _ActionButton(
                    width: cellWidth,
                    icon: Icons.notifications_active_outlined,
                    label: 'Simple',
                    description: 'Title and body, nothing else',
                    onTap: _simpleToast,
                  ),
                  _ActionButton(
                    width: cellWidth,
                    icon: Icons.reply_outlined,
                    label: 'Reply input',
                    description: 'Text box plus two buttons',
                    onTap: _replyToast,
                  ),
                  _ActionButton(
                    width: cellWidth,
                    icon: Icons.open_in_browser_outlined,
                    label: 'Open link',
                    description: 'Protocol-launch to the browser',
                    onTap: _linkToast,
                  ),
                  _ActionButton(
                    width: cellWidth,
                    icon: Icons.schedule_outlined,
                    label: 'Reminder',
                    description: 'Persistent · adds a snooze menu',
                    onTap: _reminderToast,
                  ),
                  _ActionButton(
                    width: cellWidth,
                    icon: Icons.alarm_outlined,
                    label: 'Alarm',
                    description: 'Loops until acted on · long',
                    onTap: _alarmToast,
                  ),
                  _ActionButton(
                    width: cellWidth,
                    icon: Icons.priority_high_rounded,
                    label: 'Urgent',
                    description: 'Bypasses Focus Assist',
                    onTap: _urgentToast,
                  ),
                  _ActionButton(
                    width: cellWidth,
                    icon: Icons.notifications_off_outlined,
                    label: 'Silent',
                    description: 'No sound · attribution line',
                    onTap: _silentToast,
                  ),
                  _ActionButton(
                    width: cellWidth,
                    icon: Icons.downloading_outlined,
                    label: 'Progress bar',
                    description: 'Value, override label, status',
                    onTap: _progressToast,
                  ),
                  _ActionButton(
                    width: cellWidth,
                    icon: Icons.code_outlined,
                    label: 'Custom XML',
                    description: 'Hand-written toast XML',
                    onTap: _customXmlToast,
                  ),
                  _ActionButton(
                    width: constraints.maxWidth,
                    icon: Icons.image_outlined,
                    label: 'Hero image from a widget',
                    description:
                        'Rasterizes a Flutter widget with WidgetToImage and '
                        'drops it into the toast',
                    primary: true,
                    onTap: _heroToast,
                  ),
                ],
              );
            },
          ),
        ],
      ],
    );
  }

  Widget _crossPlatformButtons(ColorScheme cs) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final cols = (constraints.maxWidth / 300).floor().clamp(1, 4);
        final cellWidth = (constraints.maxWidth - (cols - 1) * 14) / cols;
        return Wrap(
          spacing: 14,
          runSpacing: 14,
          children: [
            _ActionButton(
              width: cellWidth,
              icon: Icons.notifications_active_outlined,
              label: 'Simple',
              description: 'Title and body',
              onTap: _xSimple,
            ),
            _ActionButton(
              width: cellWidth,
              icon: Icons.smart_button_outlined,
              label: 'With buttons',
              description: 'Two action buttons',
              onTap: _xButtons,
            ),
            _ActionButton(
              width: cellWidth,
              icon: Icons.reply_outlined,
              label: 'With reply',
              description: 'Reply field (Windows, macOS)',
              onTap: _xReply,
            ),
          ],
        );
      },
    );
  }

  Widget _maintenanceRow(ColorScheme cs) {
    return Wrap(
      spacing: 12,
      runSpacing: 12,
      children: [
        OutlinedButton.icon(
          onPressed: _xCancelAll,
          icon: const Icon(Icons.layers_clear_outlined, size: 18),
          label: const Text('Clear all'),
        ),
        if (Platform.isWindows)
          OutlinedButton.icon(
            onPressed: _removeGroup,
            icon: const Icon(Icons.delete_sweep_outlined, size: 18),
            label: const Text('Remove "demos" group'),
          ),
      ],
    );
  }

  Widget _helpCard(ColorScheme cs) {
    const tips = [
      'The top row uses DesktopNotifier — it runs on Windows, macOS, and Linux',
      'Click a notification or its buttons — the event shows up below',
      'Type in the reply field, then send, to read the text back',
      'Windows extras add scenarios, audio, progress bars, and custom XML',
      'Hero image renders a live Flutter widget into a Windows toast',
      'Recolor everything from the accent swatches above',
    ];
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: cs.surfaceContainerLowest.withValues(alpha: 0.5),
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: cs.outlineVariant),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Icon(Icons.lightbulb_outline, color: cs.primary, size: 20),
              const SizedBox(width: 8),
              Text('Things to try',
                  style: Theme.of(context)
                      .textTheme
                      .titleSmall
                      ?.copyWith(fontWeight: FontWeight.w700)),
            ],
          ),
          const SizedBox(height: 14),
          for (final tip in tips)
            Padding(
              padding: const EdgeInsets.only(bottom: 10),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Padding(
                    padding: const EdgeInsets.only(top: 1),
                    child: Icon(Icons.check_circle_outline,
                        size: 17, color: cs.primary),
                  ),
                  const SizedBox(width: 10),
                  Expanded(
                    child: Text(tip,
                        style: Theme.of(context).textTheme.bodyMedium),
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }

  Widget _resultCard(ColorScheme cs) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: cs.surfaceContainerHigh,
        borderRadius: BorderRadius.circular(14),
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Icon(Icons.bolt_rounded, size: 18, color: cs.onSurfaceVariant),
          const SizedBox(width: 10),
          Expanded(
            child: SelectableText(
              _status,
              style: const TextStyle(fontFamily: 'monospace', fontSize: 13),
            ),
          ),
        ],
      ),
    );
  }
}

/// A circular accent-color swatch with a selection ring + check.
class _SwatchDot extends StatelessWidget {
  final String name;
  final Color color;
  final bool selected;
  final VoidCallback onTap;

  const _SwatchDot({
    required this.name,
    required this.color,
    required this.selected,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    final checkColor =
        ThemeData.estimateBrightnessForColor(color) == Brightness.dark
            ? Colors.white
            : Colors.black;
    return Tooltip(
      message: name,
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(20),
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 150),
          width: 34,
          height: 34,
          decoration: BoxDecoration(
            color: color,
            shape: BoxShape.circle,
            border: Border.all(
              color: selected ? cs.onSurface : Colors.transparent,
              width: 2.5,
            ),
            boxShadow: [
              BoxShadow(
                color: color.withValues(alpha: 0.45),
                blurRadius: 8,
                offset: const Offset(0, 2),
              ),
            ],
          ),
          child:
              selected ? Icon(Icons.check, size: 18, color: checkColor) : null,
        ),
      ),
    );
  }
}

/// A large, card-style button with icon, title, and description.
class _ActionButton extends StatelessWidget {
  final double width;
  final IconData icon;
  final String label;
  final String description;
  final VoidCallback onTap;
  final bool primary;

  const _ActionButton({
    required this.width,
    required this.icon,
    required this.label,
    required this.description,
    required this.onTap,
    this.primary = false,
  });

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    final fg = primary ? cs.onPrimary : cs.onSurface;
    final sub =
        primary ? cs.onPrimary.withValues(alpha: 0.85) : cs.onSurfaceVariant;
    final iconBg =
        primary ? cs.onPrimary.withValues(alpha: 0.18) : cs.primaryContainer;
    final iconFg = primary ? cs.onPrimary : cs.onPrimaryContainer;

    return SizedBox(
      width: width,
      child: Material(
        color: primary ? cs.primary : cs.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(16),
        clipBehavior: Clip.antiAlias,
        child: InkWell(
          onTap: onTap,
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
            child: Row(
              children: [
                Container(
                  width: 46,
                  height: 46,
                  decoration: BoxDecoration(
                    color: iconBg,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Icon(icon, color: iconFg, size: 24),
                ),
                const SizedBox(width: 14),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Text(
                        label,
                        style: TextStyle(
                          fontSize: 16,
                          fontWeight: FontWeight.w600,
                          color: fg,
                        ),
                      ),
                      const SizedBox(height: 2),
                      Text(
                        description,
                        style: TextStyle(fontSize: 12.5, color: sub),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ],
                  ),
                ),
                Icon(Icons.chevron_right, color: sub),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

/// Rendered off-screen by [WidgetToImage] and used as a toast hero image.
/// Sized for the 364 x 180 hero slot; picks up the app's accent via [Theme].
class _MessageHeroCard extends StatelessWidget {
  const _MessageHeroCard();

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return DecoratedBox(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [cs.primary, cs.tertiary],
        ),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
              width: 56,
              height: 56,
              alignment: Alignment.center,
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: cs.onPrimary.withValues(alpha: 0.18),
                border: Border.all(
                  color: cs.onPrimary.withValues(alpha: 0.6),
                  width: 2,
                ),
              ),
              child: Text(
                'AL',
                style: TextStyle(
                  color: cs.onPrimary,
                  fontSize: 22,
                  fontWeight: FontWeight.w700,
                ),
              ),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      Expanded(
                        child: Text(
                          'Ada Lovelace',
                          style: TextStyle(
                            color: cs.onPrimary,
                            fontSize: 18,
                            fontWeight: FontWeight.w700,
                          ),
                        ),
                      ),
                      Text(
                        '12:24',
                        style: TextStyle(
                          color: cs.onPrimary.withValues(alpha: 0.8),
                          fontSize: 12,
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'Want to grab lunch at 12:30? I found a new ramen place '
                    'around the corner.',
                    maxLines: 3,
                    overflow: TextOverflow.ellipsis,
                    style: TextStyle(
                      color: cs.onPrimary.withValues(alpha: 0.92),
                      fontSize: 14,
                      height: 1.3,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

/// Hand-written toast XML, shown by the "Custom XML" button. Anything the
/// structured builder doesn't cover can be sent as a raw template like this.
const _meetingTemplate = '''
<toast scenario="reminder" launch="open=event&amp;id=1983">
  <visual>
    <binding template="ToastGeneric">
      <text>Design review</text>
      <text>Room 2001 · Building 135</text>
      <text>10:00 AM - 10:30 AM</text>
    </binding>
  </visual>
  <actions>
    <input id="snoozeTime" type="selection" defaultInput="15">
      <selection id="1" content="1 minute"/>
      <selection id="15" content="15 minutes"/>
      <selection id="60" content="1 hour"/>
    </input>
    <action activationType="system" arguments="snooze" hint-inputId="snoozeTime" content=""/>
    <action activationType="system" arguments="dismiss" content=""/>
  </actions>
</toast>
''';
1
likes
150
points
99
downloads
screenshot

Documentation

API reference

Publisher

verified publisherluminest.io

Weekly Downloads

Native desktop notifications for Flutter on Windows, macOS, and Linux. One unified API for title, body, images, buttons, replies, and click/dismiss callbacks.

Homepage
Repository (GitHub)
View/report issues

Topics

#notifications #desktop #windows #macos #linux

License

MIT (license)

Dependencies

desktop_notifications, flutter, plugin_platform_interface

More

Packages that depend on flutter_desktop_notifications

Packages that implement flutter_desktop_notifications