printing_ffi 0.0.6 copy "printing_ffi: ^0.0.6" to clipboard
printing_ffi: ^0.0.6 copied to clipboard

A Flutter plugin for direct printer communication using native FFI bindings for macOS, Windows, and Linux.

example/lib/main.dart

import 'dart:io';
import 'dart:async';
import 'dart:typed_data';

import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:printing_ffi/printing_ffi.dart';

extension ColorExt on Color {
  /// Flutter 3.29, Migration helper for withOpacity Function
  Color withOpacityx(double value) {
    return withValues(alpha: value);
  }
}

void main() {
  runApp(const PrintingFfiExampleApp());
}

class PrintingFfiExampleApp extends StatelessWidget {
  const PrintingFfiExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Printing FFI Example',
      theme: ThemeData(
        primarySwatch: Colors.blueGrey,
        useMaterial3: true,
        brightness: Brightness.light,
        cardTheme: CardThemeData(
          elevation: 2,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      ),
      home: const PrintingScreen(),
    );
  }
}

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

  @override
  State<PrintingScreen> createState() => _PrintingScreenState();
}

class _PrintingScreenState extends State<PrintingScreen> {
  List<Printer> _printers = [];
  Printer? _selectedPrinter;
  List<PrintJob> _jobs = [];
  StreamSubscription<List<PrintJob>>? _jobsSubscription;
  List<CupsOption>? _cupsOptions;
  Map<String, String> _selectedCupsOptions = {};
  WindowsPrinterCapabilities? _windowsCapabilities;
  WindowsPaperSize? _selectedPaperSize;
  WindowsPaperSource? _selectedPaperSource;
  WindowsOrientation _selectedOrientation = WindowsOrientation.portrait;
  ColorMode _selectedColorMode = ColorMode.color;
  PrintQuality _selectedPrintQuality = PrintQuality.normal;

  bool _isLoadingPrinters = false;
  bool _isLoadingJobs = false;
  bool _isLoadingCupsOptions = false;
  bool _isLoadingWindowsCaps = false;

  final TextEditingController _rawDataController = TextEditingController(
    text: 'Hello, FFI!',
  );
  PdfPrintScaling _selectedScaling = PdfPrintScaling.fitPage;
  final TextEditingController _copiesController = TextEditingController(
    text: '1',
  );
  final TextEditingController _pageRangeController = TextEditingController();

  ///int _tabIndex = 0;

  @override
  void initState() {
    super.initState();
    _refreshPrinters();
  }

  @override
  void dispose() {
    _rawDataController.dispose();
    _jobsSubscription?.cancel();
    _copiesController.dispose();
    _pageRangeController.dispose();
    super.dispose();
  }

  void _showSnackbar(String message, {bool isError = false}) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        backgroundColor: isError ? Colors.redAccent : Colors.green,
      ),
    );
  }

  Future<void> _refreshPrinters() async {
    setState(() {
      _isLoadingPrinters = true;
      _printers = [];
      _selectedPrinter = null;
      _jobs = [];
      _cupsOptions = null;
      _selectedCupsOptions = {};
      _windowsCapabilities = null;
      _selectedPaperSize = null;
      _selectedPaperSource = null;
      _selectedOrientation = WindowsOrientation.portrait;
      _selectedColorMode = ColorMode.color;
      _selectedPrintQuality = PrintQuality.normal;
    });
    try {
      final printers = listPrinters();
      setState(() {
        _printers = printers;
        if (printers.isNotEmpty) {
          _selectedPrinter = printers.firstWhere(
            (p) => p.isDefault,
            orElse: () => printers.first,
          );
          _onPrinterSelected(_selectedPrinter);
        }
      });
    } catch (e) {
      _showSnackbar('Failed to get printers: $e', isError: true);
    } finally {
      setState(() {
        _isLoadingPrinters = false;
      });
    }
  }

  void _onPrinterSelected(Printer? printer) {
    if (printer == null) return;
    setState(() {
      _jobsSubscription?.cancel();
      _jobs = [];
      _selectedPrinter = printer;
      _subscribeToJobs();
      _fetchCupsOptions();
      _fetchWindowsCapabilities();
    });
  }

  void _subscribeToJobs() {
    if (_selectedPrinter == null) return;
    _jobsSubscription?.cancel();
    setState(() => _isLoadingJobs = true);
    _jobsSubscription = listPrintJobsStream(_selectedPrinter!.name).listen(
      (jobs) {
        if (!mounted) return;
        setState(() {
          _jobs = jobs;
          _isLoadingJobs = false;
        });
      },
      onError: (e) {
        if (!mounted) return;
        _showSnackbar('Error fetching jobs: $e', isError: true);
        setState(() => _isLoadingJobs = false);
      },
    );
  }

  Future<void> _fetchCupsOptions() async {
    if (_selectedPrinter == null) return;
    setState(() {
      _isLoadingCupsOptions = true;
      _cupsOptions = null;
    });

    try {
      final options = await getSupportedCupsOptions(_selectedPrinter!.name);
      if (!mounted) return;
      final defaultOptions = <String, String>{};
      for (final option in options) {
        defaultOptions[option.name] = option.defaultValue;
      }
      setState(() {
        _cupsOptions = options;
        _selectedCupsOptions = defaultOptions;
      });
    } catch (e) {
      _showSnackbar('Failed to get CUPS options: $e', isError: true);
    } finally {
      if (mounted) setState(() => _isLoadingCupsOptions = false);
    }
  }

  Future<void> _fetchWindowsCapabilities() async {
    if (_selectedPrinter == null || !Platform.isWindows) return;
    setState(() => _isLoadingWindowsCaps = true);
    try {
      final caps = await getWindowsPrinterCapabilities(_selectedPrinter!.name);
      if (!mounted) return;
      setState(() {
        _windowsCapabilities = caps;
        // Set defaults
        if (caps?.paperSizes.isNotEmpty ?? false) {
          _selectedPaperSize = caps!.paperSizes.first;
        }
        if (caps?.paperSources.isNotEmpty ?? false) {
          _selectedPaperSource = caps!.paperSources.first;
        }
        _selectedOrientation = WindowsOrientation.portrait;
      });
    } catch (e) {
      _showSnackbar('Failed to get Windows capabilities: $e', isError: true);
    } finally {
      if (mounted) setState(() => _isLoadingWindowsCaps = false);
    }
  }

  List<PrintOption> _buildPrintOptions({Map<String, String>? cupsOptions}) {
    final options = <PrintOption>[];
    if (Platform.isWindows) {
      if (_selectedPaperSize != null) {
        options.add(WindowsPaperSizeOption(_selectedPaperSize!.id));
      }
      if (_selectedPaperSource != null) {
        options.add(WindowsPaperSourceOption(_selectedPaperSource!.id));
      }
    }
    options.add(OrientationOption(_selectedOrientation));
    options.add(ColorModeOption(_selectedColorMode));
    options.add(PrintQualityOption(_selectedPrintQuality));

    if (Platform.isWindows &&
        (_windowsCapabilities?.mediaTypes.any((t) => t.name == 'Photo') ??
            false)) {
      // Example of setting a specific media type if available
    }

    if (cupsOptions != null) {
      cupsOptions.forEach((key, value) {
        options.add(GenericCupsOption(key, value));
      });
    }
    return options;
  }

  Future<void> _printPdf({
    Map<String, String>? cupsOptions,
    required PdfPrintScaling scaling,
    required int copies,
    required String pageRangeString,
  }) async {
    if (_selectedPrinter == null) {
      _showSnackbar('No printer selected!', isError: true);
      return;
    }
    PageRange? pageRange;
    if (pageRangeString.trim().isNotEmpty) {
      try {
        pageRange = PageRange.parse(pageRangeString);
      } on ArgumentError catch (e) {
        _showSnackbar('Invalid page range: ${e.message}', isError: true);
        return;
      }
    }
    final result = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: ['pdf'],
    );

    if (result != null && result.files.single.path != null) {
      final path = result.files.single.path!;
      try {
        final options = _buildPrintOptions(cupsOptions: cupsOptions);
        _showSnackbar('Printing PDF...');
        final success = await printPdf(
          _selectedPrinter!.name,
          path,
          docName: 'My Flutter PDF',
          options: options,
          scaling: scaling,
          copies: copies,
          pageRange: pageRange,
        );
        if (!mounted) return;
        if (success) {
          _showSnackbar('PDF sent to printer successfully!');
        } else {
          _showSnackbar(
            'Failed to print PDF. The printer may be offline or the page range may be invalid for the document.',
            isError: true,
          );
        }
      } on ArgumentError catch (e) {
        _showSnackbar('Invalid argument: ${e.message}', isError: true);
      } catch (e) {
        _showSnackbar(
          'An unexpected error occurred while printing: $e',
          isError: true,
        );
      }
    }
  }

  Future<void> _printPdfAndTrack() async {
    if (_selectedPrinter == null) {
      _showSnackbar('No printer selected!', isError: true);
      return;
    }
    final result = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: ['pdf'],
    );

    if (result != null && result.files.single.path != null) {
      final copies = int.tryParse(_copiesController.text) ?? 1;
      final pageRangeString = _pageRangeController.text;
      final path = result.files.single.path!;
      PageRange? pageRange;
      if (pageRangeString.trim().isNotEmpty) {
        try {
          pageRange = PageRange.parse(pageRangeString);
        } on ArgumentError catch (e) {
          _showSnackbar('Invalid page range: ${e.message}', isError: true);
          return;
        }
      }
      final options = _buildPrintOptions();
      if (!mounted) return;
      showDialog(
        context: context,
        barrierDismissible: false,
        builder: (context) => _PrintStatusDialog(
          printerName: _selectedPrinter!.name,
          jobStream: printPdfAndStreamStatus(
            _selectedPrinter!.name,
            path,
            options: options,
            scaling: _selectedScaling,
            copies: copies,
            pageRange: pageRange,
          ),
        ),
      );
    }
  }

  Future<void> _printRawDataAndTrack() async {
    if (_selectedPrinter == null) {
      _showSnackbar('No printer selected!', isError: true);
      return;
    }
    // Construct ZPL data with the text from the input field.
    final textToPrint = _rawDataController.text;
    if (textToPrint.isEmpty) {
      _showSnackbar('Please enter some text to print.', isError: true);
      return;
    }
    final zplData = '^XA^FO50,50^A0N,50,50^FD$textToPrint^FS^XZ';
    final data = Uint8List.fromList(zplData.codeUnits);

    final options = _buildPrintOptions(cupsOptions: _selectedCupsOptions);
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => _PrintStatusDialog(
        printerName: _selectedPrinter!.name,
        jobStream: rawDataToPrinterAndStreamStatus(
          _selectedPrinter!.name,
          data,
          docName: 'My Tracked ZPL Label',
          options: options,
        ),
      ),
    );
  }

  Future<void> _printRawData() async {
    if (_selectedPrinter == null) {
      _showSnackbar('No printer selected!', isError: true);
      return;
    }
    // Construct ZPL data with the text from the input field.
    final textToPrint = _rawDataController.text;
    if (textToPrint.isEmpty) {
      _showSnackbar('Please enter some text to print.', isError: true);
      return;
    }
    final zplData = '^XA^FO50,50^A0N,50,50^FD$textToPrint^FS^XZ';
    final data = Uint8List.fromList(zplData.codeUnits);

    final options = _buildPrintOptions(cupsOptions: _selectedCupsOptions);
    _showSnackbar('Sending raw ZPL data...');
    final success = await rawDataToPrinter(
      _selectedPrinter!.name,
      data,
      docName: 'My ZPL Label',
      options: options,
    );
    if (!mounted) return;
    if (success) {
      _showSnackbar('Raw data sent successfully!');
    } else {
      _showSnackbar('Failed to send raw data.', isError: true);
    }
  }

  Future<void> _manageJob(int jobId, String action) async {
    if (_selectedPrinter == null) return;
    bool success = false;
    try {
      switch (action) {
        case 'pause':
          success = await pausePrintJob(_selectedPrinter!.name, jobId);
          break;
        case 'resume':
          success = await resumePrintJob(_selectedPrinter!.name, jobId);
          break;
        case 'cancel':
          success = await cancelPrintJob(_selectedPrinter!.name, jobId);
          break;
      }
      if (!mounted) return;
      _showSnackbar(
        'Job $action ${success ? 'succeeded' : 'failed'}.',
        isError: !success,
      );
    } catch (e) {
      _showSnackbar('Error managing job: $e', isError: true);
    }
  }

  Future<void> _showWindowsCapabilities() async {
    if (_selectedPrinter == null || !Platform.isWindows) return;

    final capabilities = await getWindowsPrinterCapabilities(
      _selectedPrinter!.name,
    );

    if (!mounted) return;

    if (capabilities == null) {
      _showSnackbar(
        'Could not retrieve capabilities for this printer.',
        isError: true,
      );
      return;
    }

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Capabilities for ${_selectedPrinter!.name}'),
        content: SizedBox(
          width: 400,
          height: 500,
          child: ListView(
            children: [
              Text(
                'Paper Sizes (${capabilities.paperSizes.length})',
                style: Theme.of(context).textTheme.titleMedium,
              ),
              for (final paper in capabilities.paperSizes)
                ListTile(
                  dense: true,
                  title: Text(paper.name),
                  subtitle: Text(
                    'ID: ${paper.id}, ${paper.widthMillimeters.toStringAsFixed(1)} x ${paper.heightMillimeters.toStringAsFixed(1)} mm',
                  ),
                ),
              const Divider(),
              Text(
                'Paper Sources (${capabilities.paperSources.length})',
                style: Theme.of(context).textTheme.titleMedium,
              ),
              for (final paper in capabilities.paperSources)
                ListTile(
                  dense: true,
                  title: Text(paper.name),
                  subtitle: Text(paper.toString()),
                ),
              const Divider(),
              Text(
                'Media Types (${capabilities.mediaTypes.length})',
                style: Theme.of(context).textTheme.titleMedium,
              ),
              for (final media in capabilities.mediaTypes)
                ListTile(
                  dense: true,
                  title: Text(media.name),
                  subtitle: Text('ID: ${media.id}'),
                ),
              const Divider(),
              Text(
                'Supported Resolutions',
                style: Theme.of(context).textTheme.titleMedium,
              ),
              for (final res in capabilities.resolutions)
                ListTile(title: Text(res.toString())),
            ],
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('Close'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Printing FFI Example'),
          actions: [
            IconButton(
              icon: const Icon(Icons.refresh_outlined),
              onPressed: _refreshPrinters,
            ),
          ],
          bottom: _selectedPrinter != null
              ? TabBar(
                  // onTap: (index) => setState(() => _tabIndex = index),
                  tabs: const [
                    Tab(icon: Icon(Icons.print_outlined), text: 'Standard'),
                    Tab(
                      icon: Icon(Icons.settings_applications),
                      text: 'Advanced (CUPS)',
                    ),
                  ],
                )
              : null,
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              _buildPrinterSelector(),
              const SizedBox(height: 20),
              if (_selectedPrinter != null)
                Expanded(
                  child: TabBarView(
                    physics: const NeverScrollableScrollPhysics(),
                    children: [_buildSimpleTab(), _buildAdvancedTab()],
                  ),
                ),
              if (_isLoadingPrinters)
                const Center(child: CircularProgressIndicator()),
              if (!_isLoadingPrinters && _printers.isEmpty)
                const Center(
                  child: Text('No printers found. Press refresh to try again.'),
                ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildSimpleTab() {
    return ListView(
      children: [
        _buildStandardActions(),
        const SizedBox(height: 20),
        _buildJobsList(),
      ],
    );
  }

  Widget _buildPrinterSelector() {
    return Row(
      children: [
        const Text('Printer:', style: TextStyle(fontSize: 16)),
        const SizedBox(width: 10),
        Expanded(
          child: DropdownButton<Printer>(
            value: _selectedPrinter,
            isExpanded: true,
            items: _printers
                .map((p) => DropdownMenuItem(value: p, child: Text(p.name)))
                .toList(),
            onChanged: _onPrinterSelected,
            hint: const Text('Select a printer'),
          ),
        ),
      ],
    );
  }

  Widget _buildStandardActions() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Standard Actions',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            _buildPlatformSettings(),
            const SizedBox(height: 16),
            Center(
              child: Column(
                children: [
                  if (Platform.isWindows) ...[
                    SegmentedButton<PdfPrintScaling>(
                      segments: const [
                        ButtonSegment(
                          value: PdfPrintScaling.fitPage,
                          label: Text('Fit to Page'),
                        ),
                        ButtonSegment(
                          value: PdfPrintScaling.actualSize,
                          label: Text('Actual Size'),
                        ),
                      ],
                      selected: {_selectedScaling},
                      onSelectionChanged: (newSelection) {
                        setState(() {
                          _selectedScaling = newSelection.first;
                        });
                      },
                    ),
                    const SizedBox(height: 12),
                  ],
                  ElevatedButton.icon(
                    icon: const Icon(Icons.picture_as_pdf),
                    label: const Text('Print a PDF File'),
                    onPressed: () => _printPdf(
                      scaling: _selectedScaling,
                      copies: int.tryParse(_copiesController.text) ?? 1,
                      pageRangeString: _pageRangeController.text,
                    ),
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 24,
                        vertical: 12,
                      ),
                    ),
                  ),
                  const SizedBox(height: 12),
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16.0),
                    child: Row(
                      children: [
                        Expanded(
                          child: TextField(
                            controller: _copiesController,
                            decoration: const InputDecoration(
                              border: OutlineInputBorder(),
                              labelText: 'Copies',
                            ),
                            keyboardType: TextInputType.number,
                          ),
                        ),
                        const SizedBox(width: 16),
                        Expanded(
                          flex: 2,
                          child: TextField(
                            controller: _pageRangeController,
                            decoration: const InputDecoration(
                              border: OutlineInputBorder(),
                              labelText: 'Page Range',
                              hintText: 'e.g. 1-3, 5, 7-9',
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                  const SizedBox(height: 4),
                  const Text(
                    'Leave page range blank to print all pages.',
                    style: TextStyle(fontSize: 12, color: Colors.grey),
                  ),
                  const SizedBox(height: 12),
                  ElevatedButton.icon(
                    icon: const Icon(Icons.track_changes),
                    label: const Text('Print PDF and Track Status'),
                    onPressed: _printPdfAndTrack,
                  ),
                  if (Platform.isWindows) ...[
                    const SizedBox(height: 12),
                    ElevatedButton.icon(
                      icon: const Icon(Icons.inventory_2_outlined),
                      label: const Text('Show Printer Capabilities'),
                      onPressed: _showWindowsCapabilities,
                    ),
                  ],
                ],
              ),
            ),
            const Divider(height: 32),
            Text(
              'Raw Data (ZPL Example)',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 8),
            TextField(
              controller: _rawDataController,
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
                labelText: 'Text to print',
              ),
            ),
            const SizedBox(height: 12),
            Center(
              child: ElevatedButton.icon(
                icon: const Icon(Icons.data_object),
                label: const Text('Print Raw Data'),
                onPressed: _printRawData,
              ),
            ),
            const SizedBox(height: 12),
            Center(
              child: ElevatedButton.icon(
                icon: const Icon(Icons.track_changes),
                label: const Text('Print Raw Data and Track'),
                onPressed: _printRawDataAndTrack,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildPlatformSettings() {
    return Padding(
      padding: const EdgeInsets.only(top: 16.0),
      child: Card(
        elevation: 1,
        color: Theme.of(
          context,
        ).colorScheme.secondaryContainer.withOpacityx(0.3),
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                'Platform Settings',
                style: Theme.of(context).textTheme.titleMedium,
              ),
              const SizedBox(height: 12),
              if (Platform.isWindows) ...[
                if (_isLoadingWindowsCaps)
                  const Center(child: CircularProgressIndicator())
                else if (_windowsCapabilities != null) ...[
                  DropdownButtonFormField<WindowsPaperSize>(
                    initialValue: _selectedPaperSize,
                    decoration: const InputDecoration(
                      labelText: 'Paper Size (Windows)',
                      border: OutlineInputBorder(),
                    ),
                    items: _windowsCapabilities!.paperSizes
                        .map(
                          (p) =>
                              DropdownMenuItem(value: p, child: Text(p.name)),
                        )
                        .toList(),
                    onChanged: (p) => setState(() => _selectedPaperSize = p),
                  ),
                  const SizedBox(height: 12),
                  DropdownButtonFormField<WindowsPaperSource>(
                    initialValue: _selectedPaperSource,
                    decoration: const InputDecoration(
                      labelText: 'Paper Source (Windows)',
                      border: OutlineInputBorder(),
                    ),
                    items: _windowsCapabilities!.paperSources
                        .map(
                          (s) =>
                              DropdownMenuItem(value: s, child: Text(s.name)),
                        )
                        .toList(),
                    onChanged: (s) => setState(() => _selectedPaperSource = s),
                  ),
                  const SizedBox(height: 12),
                ],
              ],
              DropdownButtonFormField<PrintQuality>(
                initialValue: _selectedPrintQuality,
                decoration: const InputDecoration(
                  labelText: 'Print Quality',
                  border: OutlineInputBorder(),
                ),
                items: PrintQuality.values
                    .map(
                      (q) => DropdownMenuItem(
                        value: q,
                        child: Text(
                          q.name[0].toUpperCase() + q.name.substring(1),
                        ),
                      ),
                    )
                    .toList(),
                onChanged: (q) => setState(
                  () => _selectedPrintQuality = q ?? PrintQuality.normal,
                ),
              ),
              const SizedBox(height: 12),
              Tooltip(
                message:
                    'Options may be disabled if the printer does not report support. If capabilities are unknown, all options are enabled.',
                child: DropdownButtonFormField<ColorMode>(
                  initialValue: _selectedColorMode,
                  decoration: const InputDecoration(
                    labelText: 'Color Mode',
                    border: OutlineInputBorder(),
                  ),
                  items: ColorMode.values
                      .map(
                        (c) => DropdownMenuItem(
                          value: c,
                          enabled:
                              (c == ColorMode.color &&
                                  (_windowsCapabilities?.isColorSupported ??
                                      true)) ||
                              (c == ColorMode.monochrome &&
                                  (_windowsCapabilities
                                          ?.isMonochromeSupported ??
                                      true)),
                          child: Text(
                            c.name[0].toUpperCase() + c.name.substring(1),
                          ),
                        ),
                      )
                      .toList(),
                  onChanged: (c) =>
                      setState(() => _selectedColorMode = c ?? ColorMode.color),
                ),
              ),
              const SizedBox(height: 12),
              Tooltip(
                message:
                    'Options may be disabled if the printer does not report support. If capabilities are unknown, all options are enabled.',
                child: DropdownButtonFormField<WindowsOrientation>(
                  initialValue: _selectedOrientation,
                  decoration: const InputDecoration(
                    labelText: 'Orientation',
                    border: OutlineInputBorder(),
                  ),
                  items: WindowsOrientation.values
                      .map(
                        (o) => DropdownMenuItem(
                          value: o,
                          enabled:
                              o == WindowsOrientation.portrait ||
                              (_windowsCapabilities?.supportsLandscape ??
                                  true), // Only enable landscape if supported
                          child: Text(
                            o.name[0].toUpperCase() + o.name.substring(1),
                          ),
                        ),
                      )
                      .toList(),
                  onChanged: (o) => setState(
                    () =>
                        _selectedOrientation = o ?? WindowsOrientation.portrait,
                  ),
                ),
              ),
              const SizedBox(height: 16),
              Center(
                child: ElevatedButton.icon(
                  icon: const Icon(Icons.settings_outlined),
                  label: const Text('Open Printer Properties'),
                  onPressed: () async {
                    if (_selectedPrinter == null) return;
                    try {
                      // For a real app, you might use the `win32` package
                      // to get the handle of the main window. For this
                      // example, 0 (NULL) is sufficient.
                      final result = await openPrinterProperties(
                        _selectedPrinter!.name,
                        hwnd: 0,
                      );
                      if (!mounted) return;
                      switch (result) {
                        case PrinterPropertiesResult.ok:
                          _showSnackbar(
                            'Printer properties updated successfully.',
                          );
                          // Refresh capabilities to reflect any changes made.
                          _fetchWindowsCapabilities();
                          break;
                        case PrinterPropertiesResult.cancel:
                          _showSnackbar(
                            'Printer properties dialog was cancelled.',
                            isError: false,
                          );
                          break;
                        case PrinterPropertiesResult.error:
                          _showSnackbar(
                            'Could not open printer properties.',
                            isError: true,
                          );
                          break;
                      }
                    } catch (e) {
                      _showSnackbar(
                        'Error opening properties: $e',
                        isError: true,
                      );
                    }
                  },
                ),
              ),
              const SizedBox(height: 12),
              if (Platform.isWindows)
                Center(
                  child: ElevatedButton.icon(
                    icon: const Icon(Icons.inventory_2_outlined),
                    label: const Text('Show All Capabilities'),
                    onPressed: _showWindowsCapabilities,
                  ),
                ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildJobsList() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          children: [
            Text(
              'Print Queue',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
          ],
        ),
        if (_isLoadingJobs) const Center(child: CircularProgressIndicator()),
        if (!_isLoadingJobs && _jobs.isEmpty)
          const Text('No active print jobs.'),
        if (!_isLoadingJobs && _jobs.isNotEmpty)
          SizedBox(
            height: 200,
            child: ListView.builder(
              itemCount: _jobs.length,
              itemBuilder: (context, index) {
                final job = _jobs[index];
                return Card(
                  child: ListTile(
                    title: Text(job.title),
                    subtitle: Text(
                      'ID: ${job.id} - Status: ${job.statusDescription}',
                    ),
                    trailing: Wrap(
                      spacing: 0,
                      children: [
                        IconButton(
                          icon: const Icon(Icons.pause),
                          onPressed: () => _manageJob(job.id, 'pause'),
                        ),
                        IconButton(
                          icon: const Icon(Icons.play_arrow),
                          onPressed: () => _manageJob(job.id, 'resume'),
                        ),
                        IconButton(
                          icon: const Icon(Icons.cancel),
                          onPressed: () => _manageJob(job.id, 'cancel'),
                        ),
                      ],
                    ),
                  ),
                );
              },
            ),
          ),
      ],
    );
  }

  Widget _buildAdvancedTab() {
    if (!Platform.isMacOS && !Platform.isLinux) {
      return const Center(
        child: Text(
          'Advanced CUPS options are only available on macOS and Linux.',
        ),
      );
    }
    return ListView(
      children: [
        Text('CUPS Options', style: Theme.of(context).textTheme.headlineSmall),
        const SizedBox(height: 10),
        if (_isLoadingCupsOptions)
          const Center(child: CircularProgressIndicator()),
        if (!_isLoadingCupsOptions &&
            (_cupsOptions == null || _cupsOptions!.isEmpty))
          const Text('No CUPS options found for this printer.'),
        if (!_isLoadingCupsOptions &&
            _cupsOptions != null &&
            _cupsOptions!.isNotEmpty) ...[
          ..._buildCupsOptionWidgets(),
          const SizedBox(height: 20),
          Center(
            child: ElevatedButton.icon(
              icon: const Icon(Icons.picture_as_pdf_outlined),
              label: const Text('Print PDF with Selected Options'),
              onPressed: () => _printPdf(
                cupsOptions: _selectedCupsOptions,
                scaling: _selectedScaling,
                copies: int.tryParse(_copiesController.text) ?? 1,
                pageRangeString: _pageRangeController.text,
              ),
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(
                  horizontal: 24,
                  vertical: 12,
                ),
              ),
            ),
          ),
        ],
      ],
    );
  }

  List<Widget> _buildCupsOptionWidgets() {
    if (_cupsOptions == null) return [];
    return _cupsOptions!.map((option) {
      final currentValue = _selectedCupsOptions[option.name];
      return Card(
        margin: const EdgeInsets.symmetric(vertical: 4),
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Expanded(
                flex: 2,
                child: Text(
                  option.name,
                  style: const TextStyle(fontWeight: FontWeight.bold),
                ),
              ),
              const SizedBox(width: 16),
              Expanded(
                flex: 3,
                child: DropdownButton<String>(
                  value: currentValue,
                  isExpanded: true,
                  underline: const SizedBox.shrink(),
                  items: option.supportedValues.map((choice) {
                    return DropdownMenuItem<String>(
                      value: choice.choice,
                      child: Tooltip(
                        message: choice.text,
                        child: Text(
                          choice.text,
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                    );
                  }).toList(),
                  onChanged: (newValue) {
                    if (newValue != null) {
                      setState(
                        () => _selectedCupsOptions[option.name] = newValue,
                      );
                    }
                  },
                ),
              ),
            ],
          ),
        ),
      );
    }).toList();
  }
}

class _PrintStatusDialog extends StatefulWidget {
  const _PrintStatusDialog({
    required this.jobStream,
    required this.printerName,
  });

  final Stream<PrintJob> jobStream;
  final String printerName;

  @override
  State<_PrintStatusDialog> createState() => _PrintStatusDialogState();
}

class _PrintStatusDialogState extends State<_PrintStatusDialog> {
  StreamSubscription<PrintJob>? _subscription;
  PrintJob? _job;
  Object? _error;
  bool _isDone = false;
  bool _isCancelling = false;

  @override
  void initState() {
    super.initState();
    _subscription = widget.jobStream.listen(
      (job) {
        if (mounted) setState(() => _job = job);
      },
      onError: (error) {
        if (mounted) setState(() => _error = error);
      },
      onDone: () {
        if (mounted) {
          setState(() => _isDone = true);
        }
      },
    );
  }

  Future<void> _cancelJob() async {
    if (_job == null || !mounted) return;
    final navigator = Navigator.of(context);
    final messenger = ScaffoldMessenger.of(context);
    setState(() => _isCancelling = true);
    try {
      final success = await cancelPrintJob(widget.printerName, _job!.id);

      // After the await, the widget might have been disposed.
      if (!mounted) return;

      if (success) {
        // If successful, pop the dialog and show a confirmation snackbar.
        navigator.pop();
        messenger.showSnackBar(
          const SnackBar(
            content: Text('Cancel command sent successfully.'),
            backgroundColor: Colors.blue,
          ),
        );
      } else {
        navigator.pop();
        // If failed, stay on the dialog and show an error.
        setState(() => _isCancelling = false);
        messenger.showSnackBar(
          const SnackBar(
            content: Text('Failed to send cancel command.'),
            backgroundColor: Colors.orange,
          ),
        );
      }
    } catch (e) {
      if (!mounted) return;
      setState(() => _isCancelling = false);
      messenger.showSnackBar(
        SnackBar(content: Text('Error cancelling job: $e')),
      );
    }
  }

  @override
  void dispose() {
    _subscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final isJobTerminal =
        _job != null &&
        (_job!.status == PrintJobStatus.completed ||
            _job!.status == PrintJobStatus.canceled ||
            _job!.status == PrintJobStatus.aborted ||
            _job!.status == PrintJobStatus.error);

    // If the stream is done but we never got a job object, it means the job
    // completed so quickly it was never seen in the queue. We can treat this
    // as a successful completion.
    final isImplicitlyComplete = _isDone && _job == null && _error == null;

    final canCancel = !_isCancelling && !isJobTerminal && !_isDone;

    Widget content;
    if (_error != null) {
      content = Text(
        'Error: $_error',
        style: const TextStyle(color: Colors.red),
      );
    } else if (isImplicitlyComplete) {
      content = Text(
        'Job Completed',
        style: Theme.of(context).textTheme.titleMedium,
      );
    } else if (_job == null) {
      content = const CircularProgressIndicator();
    } else {
      content = Text(
        'Job #${_job!.id}: ${_job!.statusDescription}',
        style: Theme.of(context).textTheme.titleMedium,
      );
    }

    return AlertDialog(
      title: const Text('Tracking Print Job...'),
      content: SizedBox(width: 250, height: 100, child: Center(child: content)),
      actions: <Widget>[
        if (isJobTerminal || _error != null || isImplicitlyComplete)
          TextButton(
            child: const Text('Close'),
            onPressed: () => Navigator.of(context).pop(),
          )
        else
          TextButton(
            onPressed: canCancel ? _cancelJob : null,
            child: _isCancelling
                ? const SizedBox(
                    width: 24,
                    height: 24,
                    child: CircularProgressIndicator(strokeWidth: 3),
                  )
                : const Text('Cancel'),
          ),
      ],
    );
  }
}
8
likes
160
points
247
downloads

Publisher

verified publishershreeman.dev

Weekly Downloads

A Flutter plugin for direct printer communication using native FFI bindings for macOS, Windows, and Linux.

Homepage
Repository (GitHub)
View/report issues

Topics

#printing #print #thermal-printer #ffi #pos-printer

Documentation

API reference

License

MIT (license)

Dependencies

ffi, flutter, plugin_platform_interface

More

Packages that depend on printing_ffi

Packages that implement printing_ffi