docx_viewer_plus 1.1.3 copy "docx_viewer_plus: ^1.1.3" to clipboard
docx_viewer_plus: ^1.1.3 copied to clipboard

A Flutter package for viewing and editing .docx files. Supports rich text editing, customizable toolbar, and cross-platform save.

example/lib/main.dart

import 'package:flutter/material.dart';

import 'dart:io';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:share_plus/share_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:docx_viewer_plus/docx_viewer_plus.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'DOCX Viewer',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFF1565C0),
        useMaterial3: true,
      ),
      home: const ExampleHomeScreen(),
    );
  }
}

/// Example home screen demonstrating ALL possible use cases of DocxViewerWidget.
///
/// This file shows:
///   1. Read-only viewer (embedded in a card)
///   2. Full editor with all toolbar options
///   3. Minimal editor (selected formatting only)
///   4. Custom-styled toolbar (color, icons, layout)
///   5. RTL / Arabic viewer
///   6. Dialog-based viewer (bottom sheet)
///   7. Using callbacks (onSave, onContentChanged)
///   8. Using DocxService directly for advanced control
///   9. Loading from bytes instead of file path
///  10. Programmatic save + share
class ExampleHomeScreen extends StatefulWidget {
  const ExampleHomeScreen({super.key});

  @override
  State<ExampleHomeScreen> createState() => _ExampleHomeScreenState();
}

class _ExampleHomeScreenState extends State<ExampleHomeScreen> {
  String? _pickedFilePath;

  // Keys for programmatic access
  final _editorKey = GlobalKey<DocxViewerWidgetState>();
  final _minimalKey = GlobalKey<DocxViewerWidgetState>();

  // For "Use Case 8: Direct Service"
  final _service = DocxService();

  @override
  void initState() {
    super.initState();
    // Load service when file is picked
    // (In a real app you'd load after file_picker)
  }

  // ─── Helper: Pick a file ────────────────────────────────────────
  Future<String?> _pickFile() async {
    final result = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: ['docx'],
      withData: false, // We only need the path
    );
    if (result != null && result.files.isNotEmpty) {
      return result.files.first.path;
    }
    return null;
  }

  Future<Uint8List?> _pickFileBytes() async {
    final result = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: ['docx'],
      withData: true,
    );
    if (result != null && result.files.isNotEmpty) {
      return result.files.first.bytes;
    }
    return null;
  }

  // ─── Use Case 10: Share saved file ──────────────────────────────
  Future<void> _saveAndShare(GlobalKey<DocxViewerWidgetState> key) async {
    final state = key.currentState;

    if (state == null || !state.service.hasDocument) return;

    final bytes = await state.getDocxBytes();

    if (bytes == null || bytes.isEmpty) {
      debugPrint('Failed: HTML empty = ${state.service.html.isEmpty}');
      return;
    }

    final dir = await getTemporaryDirectory();

    final path = '${dir.path}/shared_document.docx';

    await File(path).writeAsBytes(bytes);

    await SharePlus.instance
        .share(ShareParams(files: [XFile(path)], text: 'Sharing DOCX'));
  }

  // ─── Use Case 8: Load via service + save ────────────────────────
  Future<void> _loadWithService() async {
    final path = await _pickFile();
    if (path == null) return;

    await _service.loadFromPath(path);

    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Loaded: ${_service.fileName}')),
    );

    // Later you can save:
    // final savedPath = await _service.saveDocx();
    // Or get bytes:
    // final bytes = await _service.getDocxBytes();
  }

  // ─── Use Case 9: Load from bytes ────────────────────────────────
  Future<void> _loadFromBytes() async {
    final bytes = await _pickFileBytes();
    if (bytes == null) return;

    await _service.loadFromBytes(bytes, fileName: 'from_bytes.docx');

    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Loaded from bytes successfully!')),
    );
  }

  // ─── Use Case 7: Save callback ──────────────────────────────────
  Future<String?> _customSaveHandler() async {
    // Your custom save logic: pick location, upload to server, etc.
    final dir = await getTemporaryDirectory();
    final path =
        '${dir.path}/custom_saved_${DateTime.now().millisecondsSinceEpoch}.docx';
    return path; // Return saved path, or null to cancel
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text('DocxViewerPlus — All Use Cases'),
        actions: [
          IconButton(
            icon: const Icon(Icons.folder_open),
            tooltip: 'Pick a DOCX file',
            onPressed: () async {
              final path = await _pickFile();
              if (path != null) {
                setState(() => _pickedFilePath = path);
                if (!mounted) return;
                // ignore: use_build_context_synchronously
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Selected: $path')),
                );
              }
            },
          ),
        ],
      ),
      body: _pickedFilePath == null
          ? _buildNoFileState(theme)
          : _buildUseCases(theme),
    );
  }

  // ─── Shown when no file is picked yet ───────────────────────────
  Widget _buildNoFileState(ThemeData theme) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(32),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.description_outlined,
                size: 80,
                color: theme.colorScheme.primary.withValues(alpha: 0.3)),
            const SizedBox(height: 24),
            Text(
              'Pick a .docx file to see all use cases',
              style: theme.textTheme.titleLarge?.copyWith(color: Colors.grey),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 8),
            Text(
              'Tap the folder icon in the app bar ↑',
              style: theme.textTheme.bodyMedium?.copyWith(color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }

  // ─── All use cases displayed ────────────────────────────────────
  Widget _buildUseCases(ThemeData theme) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        // ── 1. READ-ONLY VIEWER ──────────────────────────────────
        const _SectionHeader(
          title: '1. Read-Only Viewer',
          subtitle: 'No toolbar, no editing. Just display.',
        ),
        Container(
          height: 300,
          decoration: BoxDecoration(
            border: Border.all(color: Colors.grey.shade300),
            borderRadius: BorderRadius.circular(8),
          ),
          child: DocxViewerWidget(
            filePath: _pickedFilePath!,
            config: const DocxViewerConfig(
              isReadOnly: true,
              forceTextDirection: null, // auto-detect from content
            ),
          ),
        ),
        const SizedBox(height: 24),

        // ── 2. FULL EDITOR ───────────────────────────────────────
        const _SectionHeader(
          title: '2. Full Editor (All Options)',
          subtitle: 'Every toolbar button visible, scrollable layout.',
        ),
        Container(
          height: 300,
          decoration: BoxDecoration(
            border: Border.all(color: Colors.grey.shade300),
            borderRadius: BorderRadius.circular(8),
          ),
          child: DocxViewerWidget(
            key: _editorKey,
            filePath: _pickedFilePath!,
            config: const DocxViewerConfig(
              isReadOnly: false,
              toolbarPosition: ToolbarPosition.top,
              toolbarLayout: ToolbarLayout.scroll,
              enabledOptions: null, // null = show ALL
            ),
            onSave: () => _customSaveHandler(),
          ),
        ),
        const SizedBox(height: 8),
        // Save + Share buttons for this editor
        Row(
          children: [
            ElevatedButton.icon(
              onPressed: () async {
                final path = await _editorKey.currentState?.save();
                if (path != null && mounted) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                        content: Text('Saved to: $path'),
                        backgroundColor: Colors.green),
                  );
                }
              },
              icon: const Icon(Icons.save, size: 18),
              label: const Text('Save'),
            ),
            const SizedBox(width: 12),
            ElevatedButton.icon(
              onPressed: () => _saveAndShare(_editorKey),
              icon: const Icon(Icons.share, size: 18),
              label: const Text('Share'),
            ),
          ],
        ),
        const SizedBox(height: 24),

        // ── 3. MINIMAL EDITOR ───────────────────────────────────
        const _SectionHeader(
          title: '3. Minimal Editor',
          subtitle: 'Only basic formatting: bold, italic, underline, headings.',
        ),
        Container(
          height: 250,
          decoration: BoxDecoration(
            border: Border.all(color: Colors.grey.shade300),
            borderRadius: BorderRadius.circular(8),
          ),
          child: DocxViewerWidget(
            key: _minimalKey,
            filePath: _pickedFilePath!,
            config: const DocxViewerConfig(
              isReadOnly: false,
              toolbarPosition: ToolbarPosition.top,
              toolbarLayout: ToolbarLayout.wrap,
              enabledOptions: {
                ToolbarOption.bold,
                ToolbarOption.italic,
                ToolbarOption.underline,
                ToolbarOption.heading1,
                ToolbarOption.heading2,
                ToolbarOption.heading3,
                ToolbarOption.alignLeft,
                ToolbarOption.alignCenter,
                ToolbarOption.alignRight,
              },
            ),
          ),
        ),
        const SizedBox(height: 24),

        // ── 4. CUSTOM STYLED TOOLBAR ─────────────────────────────
        const _SectionHeader(
          title: '4. Custom Styled Toolbar',
          subtitle:
              'Custom background color, icon size, layout, bottom position.',
        ),
        Container(
          height: 250,
          decoration: BoxDecoration(
            border: Border.all(color: Colors.grey.shade300),
            borderRadius: BorderRadius.circular(8),
          ),
          child: DocxViewerWidget(
            filePath: _pickedFilePath!,
            config: DocxViewerConfig(
              isReadOnly: false,
              toolbarPosition: ToolbarPosition.bottom, // toolbar at bottom
              toolbarLayout: ToolbarLayout.wrap, // wrap instead of scroll
              iconSize: 24, // bigger icons
              toolbarBackgroundColor: Colors.indigo.shade50, // custom color
              enabledOptions: const {
                ToolbarOption.undo,
                ToolbarOption.redo,
                ToolbarOption.bold,
                ToolbarOption.italic,
                ToolbarOption.underline,
                ToolbarOption.strikethrough,
                ToolbarOption.textColor,
                ToolbarOption.highlightColor,
                ToolbarOption.insertLink,
                ToolbarOption.removeLink,
                ToolbarOption.clearFormatting,
              },
            ),
          ),
        ),
        const SizedBox(height: 24),

        // ── 5. RTL / ARABIC VIEWER ──────────────────────────────
        const _SectionHeader(
          title: '5. RTL / Arabic Viewer',
          subtitle: 'Force RTL direction, Arabic UI strings.',
        ),
        Container(
          height: 250,
          decoration: BoxDecoration(
            border: Border.all(color: Colors.grey.shade300),
            borderRadius: BorderRadius.circular(8),
          ),
          child: DocxViewerWidget(
            filePath: _pickedFilePath!,
            config: const DocxViewerConfig(
              isReadOnly: false,
              forceTextDirection: TextDirection.rtl,
              strings: DocxViewerStrings.arabic,
              enabledOptions: {
                ToolbarOption.bold,
                ToolbarOption.italic,
                ToolbarOption.underline,
                ToolbarOption.alignRight, // right-align is primary in RTL
                ToolbarOption.alignCenter,
                ToolbarOption.alignLeft,
                ToolbarOption.textColor,
                ToolbarOption.insertLink,
              },
            ),
          ),
        ),
        const SizedBox(height: 24),

        // ── 6. URDU LOCALIZED EDITOR ─────────────────────────────
        const _SectionHeader(
          title: '6. Urdu Localized Editor',
          subtitle: 'Urdu strings for all UI labels.',
        ),
        Container(
          height: 250,
          decoration: BoxDecoration(
            border: Border.all(color: Colors.grey.shade300),
            borderRadius: BorderRadius.circular(8),
          ),
          child: DocxViewerWidget(
            filePath: _pickedFilePath!,
            config: const DocxViewerConfig(
              isReadOnly: false,
              forceTextDirection: TextDirection.rtl,
              strings: DocxViewerStrings.urdu,
            ),
          ),
        ),
        const SizedBox(height: 24),

        // ── 7. CONTENT CHANGE CALLBACK ──────────────────────────
        const _SectionHeader(
          title: '7. Content Change Callback',
          subtitle: 'Logs every edit to a counter below.',
        ),
        _ContentChangeDemo(filePath: _pickedFilePath!),
        const SizedBox(height: 24),

        // ── 8. LOAD VIA SERVICE + SAVE ──────────────────────────
        const _SectionHeader(
          title: '8. Advanced: Direct DocxService Usage',
          subtitle: 'Load, check status, save manually.',
        ),
        Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Wrap(
                  children: [
                    ElevatedButton.icon(
                      onPressed: _loadWithService,
                      icon: const Icon(Icons.upload_file, size: 18),
                      label: const Text('Load via Service'),
                    ),
                    const SizedBox(width: 12),
                    ElevatedButton.icon(
                      onPressed: () async {
                        final path = await _service.saveDocx();
                        if (path != null && mounted) {
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(
                                content: Text('Saved: $path'),
                                backgroundColor: Colors.green),
                          );
                        }
                      },
                      icon: const Icon(Icons.save, size: 18),
                      label: const Text('Save via Service'),
                    ),
                    const SizedBox(width: 12),
                    ElevatedButton.icon(
                      onPressed: _loadFromBytes,
                      icon: const Icon(Icons.memory, size: 18),
                      label: const Text('Load from Bytes'),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                Text('Service state:', style: theme.textTheme.labelMedium),
                const SizedBox(height: 4),
                Text(
                  'fileName: ${_service.fileName}\n'
                  'hasDocument: ${_service.hasDocument}\n'
                  'isModified: ${_service.isModified}\n'
                  'isLoading: ${_service.isLoading}\n'
                  'errorMessage: ${_service.errorMessage.isEmpty ? '(none)' : _service.errorMessage}',
                  style: theme.textTheme.bodySmall
                      ?.copyWith(fontFamily: 'monospace'),
                ),
              ],
            ),
          ),
        ),
        const SizedBox(height: 24),

        // ── 9. EDITOR IN DIALOG / BOTTOM SHEET ──────────────────
        const _SectionHeader(
          title: '9. Viewer in Bottom Sheet',
          subtitle: 'Open the DOCX in a modal bottom sheet.',
        ),
        FilledButton.tonalIcon(
          onPressed: () => _openBottomSheet(context),
          icon: const Icon(Icons.open_in_full, size: 18),
          label: const Text('Open in Bottom Sheet'),
        ),
        const SizedBox(height: 24),

        // ── 10. CUSTOM ICONS ────────────────────────────────────
        const _SectionHeader(
          title: '10. Custom Toolbar Icons',
          subtitle: 'Override any toolbar button with your own widget.',
        ),
        Container(
          height: 250,
          decoration: BoxDecoration(
            border: Border.all(color: Colors.grey.shade300),
            borderRadius: BorderRadius.circular(8),
          ),
          child: DocxViewerWidget(
            filePath: _pickedFilePath!,
            config: DocxViewerConfig(
              isReadOnly: false,
              iconSize: 20,
              customIcons: {
                ToolbarOption.bold: Container(
                  width: 32,
                  height: 32,
                  decoration: BoxDecoration(
                    color: Colors.red.shade100,
                    borderRadius: BorderRadius.circular(6),
                  ),
                  child: const Icon(Icons.format_bold,
                      color: Colors.red, size: 18),
                ),
                ToolbarOption.italic: Container(
                  width: 32,
                  height: 32,
                  decoration: BoxDecoration(
                    color: Colors.blue.shade100,
                    borderRadius: BorderRadius.circular(6),
                  ),
                  child: const Icon(Icons.format_italic,
                      color: Colors.blue, size: 18),
                ),
                ToolbarOption.underline: Container(
                  width: 32,
                  height: 32,
                  decoration: BoxDecoration(
                    color: Colors.green.shade100,
                    borderRadius: BorderRadius.circular(6),
                  ),
                  child: const Icon(Icons.format_underlined,
                      color: Colors.green, size: 18),
                ),
              },
              enabledOptions: const {
                ToolbarOption.bold,
                ToolbarOption.italic,
                ToolbarOption.underline,
                ToolbarOption.strikethrough,
                ToolbarOption.heading1,
                ToolbarOption.heading2,
                ToolbarOption.heading3,
                ToolbarOption.unorderedList,
                ToolbarOption.orderedList,
                ToolbarOption.insertLink,
                ToolbarOption.clearFormatting,
              },
            ),
          ),
        ),
        const SizedBox(height: 32),
      ],
    );
  }

  // ─── Bottom Sheet demo ──────────────────────────────────────
  void _openBottomSheet(BuildContext context) {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      builder: (ctx) => DraggableScrollableSheet(
        initialChildSize: 0.85,
        maxChildSize: 0.95,
        minChildSize: 0.5,
        expand: false,
        builder: (_, scrollController) => Column(
          children: [
            // Drag handle
            Container(
              margin: const EdgeInsets.symmetric(vertical: 8),
              width: 40,
              height: 4,
              decoration: BoxDecoration(
                color: Colors.grey.shade400,
                borderRadius: BorderRadius.circular(2),
              ),
            ),
            // Title
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
              child: Row(
                children: [
                  const Text('Document Viewer',
                      style:
                          TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
                  const Spacer(),
                  IconButton(
                    icon: const Icon(Icons.close),
                    onPressed: () => Navigator.of(ctx).pop(),
                  ),
                ],
              ),
            ),
            const Divider(height: 1),
            // The viewer fills the rest
            Expanded(
              child: DocxViewerWidget(
                filePath: _pickedFilePath!,
                config: const DocxViewerConfig(
                  isReadOnly: true,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ─── Section Header Widget ─────────────────────────────────────────
class _SectionHeader extends StatelessWidget {
  final String title;
  final String subtitle;
  const _SectionHeader({required this.title, required this.subtitle});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(title,
              style:
                  const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
          Text(subtitle,
              style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
        ],
      ),
    );
  }
}

// ─── Use Case 7: Content Change Callback Demo ──────────────────────
class _ContentChangeDemo extends StatefulWidget {
  final String filePath;
  const _ContentChangeDemo({required this.filePath});

  @override
  State<_ContentChangeDemo> createState() => _ContentChangeDemoState();
}

class _ContentChangeDemoState extends State<_ContentChangeDemo> {
  int _editCount = 0;
  int _htmlLength = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Container(
          height: 200,
          decoration: BoxDecoration(
            border: Border.all(color: Colors.grey.shade300),
            borderRadius: BorderRadius.circular(8),
          ),
          child: DocxViewerWidget(
            filePath: widget.filePath,
            config: const DocxViewerConfig(
              isReadOnly: false,
              enabledOptions: {
                ToolbarOption.bold,
                ToolbarOption.italic,
                ToolbarOption.underline,
                ToolbarOption.heading1,
                ToolbarOption.heading2,
                ToolbarOption.unorderedList,
              },
            ),
            onContentChanged: (html) {
              setState(() {
                _editCount++;
                _htmlLength = html.length;
              });
            },
          ),
        ),
        const SizedBox(height: 8),
        Container(
          padding: const EdgeInsets.all(8),
          decoration: BoxDecoration(
            color: Colors.grey.shade100,
            borderRadius: BorderRadius.circular(6),
          ),
          child: Text(
            'Edits detected: $_editCount  |  HTML size: ${(_htmlLength / 1024).toStringAsFixed(1)} KB',
            style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
          ),
        ),
      ],
    );
  }
}
5
likes
160
points
213
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter package for viewing and editing .docx files. Supports rich text editing, customizable toolbar, and cross-platform save.

Repository (GitHub)
View/report issues

Topics

#docx #viewer #editor #word #rich-text

License

MIT (license)

Dependencies

archive, flutter, path_provider, webview_flutter, xml

More

Packages that depend on docx_viewer_plus