flutter_epub_viewer_kit 0.3.0 copy "flutter_epub_viewer_kit: ^0.3.0" to clipboard
flutter_epub_viewer_kit: ^0.3.0 copied to clipboard

A customizable EPUB reader widget for Flutter. Supports iOS, Android, and Web platforms with features like pagination, scrolling, bookmarks, and customizable themes.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_epub_viewer_kit/flutter_epub_viewer_kit.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'EPUB Viewer Kit Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2E7CE0)),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

// --- Source type enum ---

enum SourceType { asset, url, bytes }

// --- Configuration data class ---

class ReaderConfig {
  final SourceType sourceType;
  final String assetPath;
  final EpubReaderLocalization localization;
  final Color? progressBarColor;
  final bool showWatermark;
  final bool useCustomBars;
  final bool enablePersistence;
  final bool enableMaxPages;
  final int maxPages;
  final bool isPageMode;
  final double resumeProgress;
  final bool loadInitialBookmarks;

  const ReaderConfig({
    required this.sourceType,
    required this.assetPath,
    required this.localization,
    required this.progressBarColor,
    required this.showWatermark,
    required this.useCustomBars,
    required this.enablePersistence,
    required this.enableMaxPages,
    required this.maxPages,
    required this.isPageMode,
    required this.resumeProgress,
    required this.loadInitialBookmarks,
  });
}

/// Asset books bundled with the example, used for the source picker and the
/// in-reader dynamic source swap demo.
const assetBooks = [
  'assets/example.epub',
];

const localizationPresets = <String, EpubReaderLocalization>{
  '한국어': EpubReaderLocalization.korean,
  'English': EpubReaderLocalization.english,
  '日本語': EpubReaderLocalization.japanese,
};

const progressBarColorPresets = <String, Color?>{
  'Default': null,
  'Indigo': Colors.indigo,
  'Orange': Colors.orange,
};

// ============================================================
// Screen 1: Feature Configuration Home
// ============================================================

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

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

class _HomePageState extends State<HomePage> {
  SourceType _sourceType = SourceType.asset;
  String _assetPath = assetBooks.first;
  String _localizationName = '한국어';
  String _progressBarColorName = 'Default';
  bool _showWatermark = false;
  bool _useCustomBars = true;
  bool _enablePersistence = true;
  bool _enableMaxPages = false;
  final TextEditingController _maxPagesController = TextEditingController(
    text: '5',
  );
  bool _isPageMode = true;
  double _resumeProgress = 0.0;
  bool _loadInitialBookmarks = false;

  @override
  void dispose() {
    _maxPagesController.dispose();
    super.dispose();
  }

  void _openReader() {
    final maxPages = int.tryParse(_maxPagesController.text) ?? 5;

    final config = ReaderConfig(
      sourceType: _sourceType,
      assetPath: _assetPath,
      localization: localizationPresets[_localizationName]!,
      progressBarColor: progressBarColorPresets[_progressBarColorName],
      showWatermark: _showWatermark,
      useCustomBars: _useCustomBars,
      enablePersistence: _enablePersistence,
      enableMaxPages: _enableMaxPages,
      maxPages: maxPages.clamp(1, 9999),
      isPageMode: _isPageMode,
      resumeProgress: _resumeProgress,
      loadInitialBookmarks: _loadInitialBookmarks,
    );

    Navigator.of(
      context,
    ).push(MaterialPageRoute(builder: (_) => ReaderPage(config: config)));
  }

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

    return Scaffold(
      appBar: AppBar(
        title: const Text('EPUB Viewer Kit Demo'),
        centerTitle: true,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          // --- Source ---
          _SectionCard(
            title: 'Source',
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                SegmentedButton<SourceType>(
                  segments: const [
                    ButtonSegment(
                      value: SourceType.asset,
                      label: Text('Asset'),
                    ),
                    ButtonSegment(value: SourceType.url, label: Text('URL')),
                    ButtonSegment(
                      value: SourceType.bytes,
                      label: Text('Bytes'),
                    ),
                  ],
                  selected: {_sourceType},
                  onSelectionChanged: (v) =>
                      setState(() => _sourceType = v.first),
                ),
                if (_sourceType == SourceType.asset)
                  Padding(
                    padding: const EdgeInsets.only(top: 12),
                    child: Wrap(
                      spacing: 8,
                      children: assetBooks
                          .map(
                            (path) => ChoiceChip(
                              label: Text(path.split('/').last),
                              selected: _assetPath == path,
                              onSelected: (_) =>
                                  setState(() => _assetPath = path),
                            ),
                          )
                          .toList(),
                    ),
                  ),
              ],
            ),
          ),

          // --- Localization ---
          _SectionCard(
            title: 'Localization',
            child: Wrap(
              spacing: 8,
              children: localizationPresets.keys
                  .map(
                    (name) => ChoiceChip(
                      label: Text(name),
                      selected: _localizationName == name,
                      onSelected: (_) =>
                          setState(() => _localizationName = name),
                    ),
                  )
                  .toList(),
            ),
          ),

          // --- Progress Bar Color ---
          _SectionCard(
            title: 'Progress Bar Color',
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Shown at the top when bars are hidden (tap center to hide)',
                  style: theme.textTheme.bodySmall,
                ),
                const SizedBox(height: 8),
                Wrap(
                  spacing: 8,
                  children: progressBarColorPresets.keys
                      .map(
                        (name) => ChoiceChip(
                          label: Text(name),
                          selected: _progressBarColorName == name,
                          avatar: progressBarColorPresets[name] != null
                              ? CircleAvatar(
                                  backgroundColor:
                                      progressBarColorPresets[name],
                                )
                              : null,
                          onSelected: (_) =>
                              setState(() => _progressBarColorName = name),
                        ),
                      )
                      .toList(),
                ),
              ],
            ),
          ),

          // --- Reading Mode ---
          _SectionCard(
            title: 'Reading Mode',
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                SegmentedButton<bool>(
                  segments: const [
                    ButtonSegment(value: true, label: Text('Page')),
                    ButtonSegment(value: false, label: Text('Scroll')),
                  ],
                  selected: {_isPageMode},
                  onSelectionChanged: (v) =>
                      setState(() => _isPageMode = v.first),
                ),
                const SizedBox(height: 4),
                Text(
                  'Saved settings override this when persistence is on',
                  style: theme.textTheme.bodySmall,
                ),
              ],
            ),
          ),

          // --- Watermark ---
          _SectionCard(
            title: 'Watermark',
            child: SwitchListTile(
              title: const Text('Show watermark overlay'),
              value: _showWatermark,
              onChanged: (v) => setState(() => _showWatermark = v),
              contentPadding: EdgeInsets.zero,
            ),
          ),

          // --- Custom Bars ---
          _SectionCard(
            title: 'Custom Top/Bottom Bars',
            child: SwitchListTile(
              title: const Text('Use custom bars'),
              subtitle: const Text('Off = use default built-in bars'),
              value: _useCustomBars,
              onChanged: (v) => setState(() => _useCustomBars = v),
              contentPadding: EdgeInsets.zero,
            ),
          ),

          // --- Settings Persistence ---
          _SectionCard(
            title: 'Settings Persistence',
            child: SwitchListTile(
              title: const Text('Persist settings to device'),
              subtitle: const Text('Uses SharedPreferences when enabled'),
              value: _enablePersistence,
              onChanged: (v) => setState(() => _enablePersistence = v),
              contentPadding: EdgeInsets.zero,
            ),
          ),

          // --- Max Readable Pages ---
          _SectionCard(
            title: 'Max Readable Pages',
            child: Column(
              children: [
                SwitchListTile(
                  title: const Text('Limit readable pages'),
                  subtitle: const Text('Simulates preview/trial mode'),
                  value: _enableMaxPages,
                  onChanged: (v) => setState(() => _enableMaxPages = v),
                  contentPadding: EdgeInsets.zero,
                ),
                if (_enableMaxPages)
                  Padding(
                    padding: const EdgeInsets.only(top: 8),
                    child: TextField(
                      controller: _maxPagesController,
                      decoration: const InputDecoration(
                        labelText: 'Max pages',
                        border: OutlineInputBorder(),
                      ),
                      keyboardType: TextInputType.number,
                      inputFormatters: [FilteringTextInputFormatter.digitsOnly],
                    ),
                  ),
              ],
            ),
          ),

          // --- Resume Position ---
          _SectionCard(
            title: 'Resume Position',
            child: Column(
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    const Text('Start reading from'),
                    Text(
                      '${(_resumeProgress * 100).toStringAsFixed(0)}%',
                      style: theme.textTheme.titleMedium,
                    ),
                  ],
                ),
                Slider(
                  value: _resumeProgress,
                  onChanged: (v) => setState(() => _resumeProgress = v),
                  divisions: 20,
                  label: '${(_resumeProgress * 100).toStringAsFixed(0)}%',
                ),
              ],
            ),
          ),

          // --- Initial Bookmarks ---
          _SectionCard(
            title: 'Initial Bookmarks',
            child: SwitchListTile(
              title: const Text('Pre-load sample bookmarks'),
              subtitle: const Text('Adds bookmarks at pages 1, 3, 5'),
              value: _loadInitialBookmarks,
              onChanged: (v) => setState(() => _loadInitialBookmarks = v),
              contentPadding: EdgeInsets.zero,
            ),
          ),

          const SizedBox(height: 24),

          // --- Open Reader ---
          FilledButton.icon(
            onPressed: _openReader,
            icon: const Icon(Icons.menu_book),
            label: const Text('Open Reader'),
            style: FilledButton.styleFrom(
              minimumSize: const Size.fromHeight(56),
              textStyle: const TextStyle(fontSize: 18),
            ),
          ),
          const SizedBox(height: 32),
        ],
      ),
    );
  }
}

// --- Section card helper ---

class _SectionCard extends StatelessWidget {
  final String title;
  final Widget child;

  const _SectionCard({required this.title, required this.child});

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
        side: BorderSide(color: Theme.of(context).colorScheme.outlineVariant),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: Theme.of(context).textTheme.titleSmall),
            const SizedBox(height: 8),
            child,
          ],
        ),
      ),
    );
  }
}

// ============================================================
// Screen 2: EPUB Reader
// ============================================================

class ReaderPage extends StatefulWidget {
  final ReaderConfig config;

  const ReaderPage({super.key, required this.config});

  @override
  State<ReaderPage> createState() => _ReaderPageState();
}

class _ReaderPageState extends State<ReaderPage> {
  late final EpubReaderController _controller;
  EpubSource? _source;
  bool _loadingSource = false;

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

    final initialBookmarks = widget.config.loadInitialBookmarks
        ? [
            Bookmark(
              pageIndex: 0,
              progress: 0.0,
              title: 'Sample Bookmark 1',
              excerpt: 'Beginning of the book',
              createdAt: DateTime.now(),
            ),
            Bookmark(
              pageIndex: 2,
              progress: 0.0,
              title: 'Sample Bookmark 2',
              excerpt: 'A few pages in',
              createdAt: DateTime.now(),
            ),
            Bookmark(
              pageIndex: 4,
              progress: 0.0,
              title: 'Sample Bookmark 3',
              excerpt: 'Further along',
              createdAt: DateTime.now(),
            ),
          ]
        : <Bookmark>[];

    _controller = EpubReaderController(
      initialProgress: widget.config.resumeProgress,
      initialBookmarks: initialBookmarks,
      onPositionChanged: (position) {
        debugPrint(
          'Position: page ${position.pageIndex + 1}/${position.totalPages}, '
          'progress: ${(position.progress * 100).toStringAsFixed(1)}%',
        );
      },
      onSettingsChanged: (settings) {
        debugPrint(
          'Settings changed: fontSize=${settings.fontSize}, '
          'isPageMode=${settings.isPageMode}',
        );
      },
      onBookmarkAdded: (bookmark) {
        debugPrint('Bookmark added: page ${bookmark.pageIndex + 1}');
        setState(() {});
      },
      onBookmarkRemoved: (bookmark) {
        debugPrint('Bookmark removed: page ${bookmark.pageIndex + 1}');
        setState(() {});
      },
    );
    _controller.addListener(_onControllerChanged);

    _prepareSource();
  }

  Future<void> _prepareSource() async {
    switch (widget.config.sourceType) {
      case SourceType.asset:
        setState(() {
          _source = EpubSourceAsset(widget.config.assetPath);
        });
      case SourceType.url:
        setState(() {
          _source = const EpubSourceUrl(
            'https://www.gutenberg.org/ebooks/11.epub.images',
          );
        });
      case SourceType.bytes:
        setState(() => _loadingSource = true);
        try {
          final data = await rootBundle.load('assets/example.epub');
          if (mounted) {
            setState(() {
              _source = EpubSourceBytes(data.buffer.asUint8List());
              _loadingSource = false;
            });
          }
        } catch (e) {
          if (mounted) {
            setState(() => _loadingSource = false);
            ScaffoldMessenger.of(
              context,
            ).showSnackBar(SnackBar(content: Text('Failed to load bytes: $e')));
          }
        }
    }
  }

  void _onControllerChanged() {
    if (mounted) setState(() {});
  }

  /// Swaps to the next bundled asset book without recreating the widget,
  /// demonstrating dynamic source swap via didUpdateWidget.
  void _swapBook() {
    final current = _source;
    if (current is! EpubSourceAsset) return;
    final index = assetBooks.indexOf(current.assetPath);
    final next = assetBooks[(index + 1) % assetBooks.length];
    setState(() => _source = EpubSourceAsset(next));
  }

  @override
  void dispose() {
    _controller.removeListener(_onControllerChanged);
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_loadingSource || _source == null) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    final config = widget.config;

    // The reader is an embeddable widget — the app owns the Scaffold.
    return Scaffold(
      body: EpubReaderWidget(
        source: _source!,
        controller: _controller,
        settingsStorageKey: config.enablePersistence
            ? 'epub_reader_settings'
            : null,
        initialSettings: ReaderSettings(isPageMode: config.isPageMode),
        localization: config.localization,
        progressBarColor: config.progressBarColor,
        showTopBar: true,
        showBottomBar: true,
        topBarBuilder: config.useCustomBars
            ? (context, settings, position) => _buildTopBar(settings)
            : null,
        bottomBarBuilder: config.useCustomBars
            ? (context, settings, position) =>
                  _buildBottomBar(settings, position)
            : null,
        watermark: config.showWatermark ? _buildWatermark() : null,
        maxReadablePages: config.enableMaxPages ? config.maxPages : null,
        onMaxPageReached: (maxPage, totalPages) {
          _showMaxPageDialog(maxPage, totalPages);
        },
        onPageChanged: (current, total) {
          debugPrint('Page: $current / $total');
        },
        onBookLoaded: (title, author) {
          debugPrint('Book loaded: $title by $author');
          if (mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text(
                  'Loaded: ${title ?? "Unknown"} by ${author ?? "Unknown"}',
                ),
                duration: const Duration(seconds: 2),
              ),
            );
          }
        },
        onError: (error) {
          debugPrint('Error: $error');
          if (mounted) {
            ScaffoldMessenger.of(
              context,
            ).showSnackBar(SnackBar(content: Text('Error: $error')));
          }
        },
      ),
    );
  }

  // --- Custom top bar ---

  Widget _buildTopBar(ReaderSettings settings) {
    final isBookmarked = _controller.isCurrentPageBookmarked;

    return Material(
      color: settings.surfaceColor.withValues(alpha: 0.96),
      child: SafeArea(
        bottom: false,
        child: Container(
          height: kToolbarHeight,
          padding: const EdgeInsets.symmetric(horizontal: 8),
          decoration: BoxDecoration(
            border: Border(bottom: BorderSide(color: settings.dividerColor)),
          ),
          child: Row(
            children: [
              IconButton(
                icon: Icon(Icons.arrow_back, color: settings.textColor),
                onPressed: () => Navigator.maybePop(context),
              ),
              Expanded(
                child: Text(
                  'EPUB Reader',
                  style: TextStyle(
                    color: settings.textColor,
                    fontSize: 16,
                    fontWeight: FontWeight.w500,
                  ),
                ),
              ),
              if (widget.config.sourceType == SourceType.asset)
                IconButton(
                  tooltip: 'Swap book',
                  icon: Icon(Icons.swap_horiz, color: settings.textColor),
                  onPressed: _swapBook,
                ),
              IconButton(
                icon: Icon(
                  isBookmarked ? Icons.bookmark : Icons.bookmark_border,
                  color:
                      isBookmarked ? settings.accentColor : settings.textColor,
                ),
                onPressed: () => _controller.toggleBookmark(),
              ),
              IconButton(
                icon: Icon(Icons.list, color: settings.textColor),
                onPressed: _showBookmarksList,
              ),
              IconButton(
                icon: Icon(Icons.settings, color: settings.textColor),
                onPressed: () => _controller.showSettings(),
              ),
            ],
          ),
        ),
      ),
    );
  }

  // --- Custom bottom bar ---

  Widget _buildBottomBar(ReaderSettings settings, ReadingPosition position) {
    final currentPage = position.pageIndex + 1;
    final totalPages = position.totalPages;
    final progress = position.progress;

    return Material(
      color: settings.surfaceColor.withValues(alpha: 0.96),
      child: SafeArea(
        top: false,
        child: Container(
          decoration: BoxDecoration(
            border: Border(top: BorderSide(color: settings.dividerColor)),
          ),
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              LinearProgressIndicator(
                value: progress,
                minHeight: 3,
                backgroundColor: settings.dividerColor,
                valueColor:
                    AlwaysStoppedAnimation<Color>(settings.accentColor),
              ),
              const SizedBox(height: 8),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(
                    '$currentPage / $totalPages (${(progress * 100).toStringAsFixed(1)}%)',
                    style: TextStyle(
                      color: settings.mutedColor,
                      fontSize: 12,
                    ),
                  ),
                  Row(
                    children: [
                      IconButton(
                        icon: Icon(
                          Icons.chevron_left,
                          color: settings.textColor,
                        ),
                        onPressed: () => _controller.previousPage(),
                      ),
                      IconButton(
                        icon: Icon(
                          Icons.chevron_right,
                          color: settings.textColor,
                        ),
                        onPressed: () => _controller.nextPage(),
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  // --- Bookmarks list ---

  void _showBookmarksList() {
    final bookmarks = _controller.bookmarks;
    final settings = _controller.currentSettings;

    showModalBottomSheet(
      context: context,
      backgroundColor: Colors.transparent,
      builder: (context) {
        return Container(
          constraints: BoxConstraints(
            maxHeight: MediaQuery.of(context).size.height * 0.6,
          ),
          decoration: BoxDecoration(
            color: settings.surfaceColor,
            borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Container(
                width: 36,
                height: 4,
                margin: const EdgeInsets.only(top: 8, bottom: 14),
                decoration: BoxDecoration(
                  color: settings.textColor.withValues(alpha: 0.2),
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
              Padding(
                padding: const EdgeInsets.fromLTRB(20, 0, 20, 8),
                child: Row(
                  children: [
                    Text(
                      'BOOKMARKS',
                      style: TextStyle(
                        color: settings.mutedColor,
                        fontSize: 12,
                        fontWeight: FontWeight.w600,
                        letterSpacing: 0.8,
                      ),
                    ),
                    const SizedBox(width: 8),
                    Text(
                      '${bookmarks.length}',
                      style: TextStyle(
                        color: settings.accentColor,
                        fontSize: 12,
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                  ],
                ),
              ),
              if (bookmarks.isEmpty)
                Padding(
                  padding: const EdgeInsets.fromLTRB(20, 28, 20, 56),
                  child: Column(
                    children: [
                      Icon(
                        Icons.bookmark_border,
                        size: 32,
                        color: settings.mutedColor,
                      ),
                      const SizedBox(height: 10),
                      Text(
                        'No bookmarks yet',
                        style: TextStyle(
                          color: settings.mutedColor,
                          fontSize: 13,
                        ),
                      ),
                    ],
                  ),
                )
              else
                Flexible(
                  child: ListView.separated(
                    shrinkWrap: true,
                    padding: const EdgeInsets.fromLTRB(20, 4, 20, 24),
                    itemCount: bookmarks.length,
                    separatorBuilder: (_, __) =>
                        Divider(height: 1, color: settings.dividerColor),
                    itemBuilder: (context, index) =>
                        _buildBookmarkRow(bookmarks[index], settings),
                  ),
                ),
            ],
          ),
        );
      },
    );
  }

  Widget _buildBookmarkRow(Bookmark bookmark, ReaderSettings settings) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: () {
        _controller.goToBookmark(bookmark);
        Navigator.pop(context);
      },
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 12),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
              decoration: BoxDecoration(
                color: settings.accentColor.withValues(alpha: 0.10),
                borderRadius: BorderRadius.circular(6),
              ),
              child: Text(
                'p.${bookmark.pageIndex + 1}',
                style: TextStyle(
                  color: settings.accentColor,
                  fontSize: 11,
                  fontWeight: FontWeight.w600,
                ),
              ),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    bookmark.title ?? 'Page ${bookmark.pageIndex + 1}',
                    style: TextStyle(
                      color: settings.textColor,
                      fontSize: 14,
                      fontWeight: FontWeight.w500,
                    ),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                  if (bookmark.excerpt != null) ...[
                    const SizedBox(height: 3),
                    Text(
                      bookmark.excerpt!,
                      style: TextStyle(
                        color: settings.mutedColor,
                        fontSize: 12,
                        height: 1.4,
                      ),
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ],
                ],
              ),
            ),
            const SizedBox(width: 12),
            GestureDetector(
              behavior: HitTestBehavior.opaque,
              onTap: () {
                _controller.removeBookmark(bookmark);
                Navigator.pop(context);
                _showBookmarksList();
              },
              child: Padding(
                padding: const EdgeInsets.all(4),
                child: Icon(
                  Icons.close_rounded,
                  size: 16,
                  color: settings.mutedColor,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  // --- Watermark ---

  Widget _buildWatermark() {
    return Opacity(
      opacity: 0.08,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(
            Icons.menu_book_outlined,
            size: 160,
            color: Colors.grey.shade600,
          ),
          const SizedBox(height: 12),
          Text(
            'Flutter EPUB Viewer',
            style: TextStyle(
              fontSize: 20,
              fontWeight: FontWeight.w600,
              color: Colors.grey.shade600,
              letterSpacing: 1.1,
            ),
          ),
        ],
      ),
    );
  }

  // --- Max page reached dialog ---

  void _showMaxPageDialog(int maxPage, int totalPages) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Preview Limit Reached'),
        content: Text(
          'You have reached the preview limit of $maxPage pages '
          '(out of $totalPages total). '
          'Purchase the full book to continue reading.',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }
}
7
likes
150
points
127
downloads

Documentation

API reference

Publisher

verified publisherpbbz.co.kr

Weekly Downloads

A customizable EPUB reader widget for Flutter. Supports iOS, Android, and Web platforms with features like pagination, scrolling, bookmarks, and customizable themes.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

epub_view, epubx, flutter, flutter_html, google_fonts, html, http, scrollable_positioned_list, shared_preferences

More

Packages that depend on flutter_epub_viewer_kit