flutter_epub_viewer 2.2.0
flutter_epub_viewer: ^2.2.0 copied to clipboard
A Flutter package for viewing Epub documents developed by combining the power of Epubjs and flutter_inappwebview
import 'package:flutter_epub_viewer/flutter_epub_viewer.dart';
import 'search_page.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Epub Viewer Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final epubController = EpubController();
var textSelectionCfi = '';
bool isLoading = true;
double progress = 0.0;
List<EpubChapter> chapters = [];
/// Current flow, toggled from the app bar.
EpubFlow flow = EpubFlow.paginated;
/// Page info from the last relocation, shown under the progress bar.
String pageInfo = '';
/// Web only: whether the beside-the-viewer chapters panel is visible.
bool _showChapters = false;
@override
Widget build(BuildContext context) {
// On mobile the epub is a native platform view we can freely overlay, so we
// use the standard Drawer for chapters and epub.js swipe for page turning.
//
// On the web the epub <iframe> composites above the Flutter scene, so
// overlays (Drawer / routes) placed over it don't receive taps and there is
// no swipe with a mouse. There we use a side panel beside the viewer plus
// AppBar page-turn buttons — all outside the iframe's rectangle.
return kIsWeb ? _buildWebLayout(context) : _buildMobileLayout(context);
}
// ---------------------------------------------------------------------------
// Mobile: Drawer + swipe (original behavior)
// ---------------------------------------------------------------------------
Widget _buildMobileLayout(BuildContext context) {
return Scaffold(
drawer: Drawer(
child: SafeArea(
child: _chapterList(onTap: () => Navigator.pop(context)),
),
),
appBar: _appBar(context, leading: null, showPageButtons: false),
body: SafeArea(
child: Column(
children: [
_progressBar(),
Expanded(child: _viewerStack()),
],
),
),
);
}
// ---------------------------------------------------------------------------
// Web: side panel + AppBar page-turn buttons
// ---------------------------------------------------------------------------
Widget _buildWebLayout(BuildContext context) {
return Scaffold(
appBar: _appBar(
context,
leading: IconButton(
tooltip: 'Chapters',
icon: const Icon(Icons.menu),
onPressed: () => setState(() => _showChapters = !_showChapters),
),
showPageButtons: true,
),
body: SafeArea(
child: Column(
children: [
_progressBar(),
Expanded(
child: Row(
children: [
if (_showChapters)
Container(
width: 280,
decoration: BoxDecoration(
border: Border(
right:
BorderSide(color: Theme.of(context).dividerColor),
),
),
child: _chapterList(),
),
Expanded(child: _viewerStack()),
],
),
),
],
),
),
);
}
// ---------------------------------------------------------------------------
// Shared pieces
// ---------------------------------------------------------------------------
AppBar _appBar(
BuildContext context, {
required Widget? leading,
required bool showPageButtons,
}) {
return AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
leading: leading,
title: Text(widget.title),
actions: [
IconButton(
tooltip: 'Toggle paginated / scrolled',
icon: Icon(flow == EpubFlow.paginated
? Icons.view_day_outlined
: Icons.auto_stories_outlined),
onPressed: () {
setState(() {
flow = flow == EpubFlow.paginated
? EpubFlow.scrolled
: EpubFlow.paginated;
});
epubController.setFlow(flow: flow);
},
),
if (showPageButtons) ...[
IconButton(
tooltip: 'Previous page',
icon: const Icon(Icons.chevron_left),
onPressed: () => epubController.prev(),
),
IconButton(
tooltip: 'Next page',
icon: const Icon(Icons.chevron_right),
onPressed: () => epubController.next(),
),
],
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
SearchPage(epubController: epubController),
),
);
},
),
],
);
}
Widget _progressBar() => Column(
children: [
LinearProgressIndicator(
value: progress,
backgroundColor: Colors.transparent,
),
Text(
'${(progress * 100).toStringAsFixed(1)}% $pageInfo',
style: Theme.of(context).textTheme.bodySmall,
),
],
);
void _showFootnote(EpubFootnote footnote) {
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Note ${footnote.linkText}',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text(footnote.text),
TextButton(
onPressed: () {
Navigator.pop(context);
epubController.display(cfi: footnote.href);
},
child: const Text('Go to note'),
),
],
),
),
),
);
}
Widget _chapterList({VoidCallback? onTap}) {
if (chapters.isEmpty) {
return const Center(child: Text('Chapters not loaded yet'));
}
return ListView.builder(
itemCount: chapters.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(chapters[index].title),
onTap: () {
epubController.display(cfi: chapters[index].href);
onTap?.call();
},
);
},
);
}
Widget _viewerStack() {
return Stack(
children: [
EpubViewer(
initialCfi: 'epubcfi(/6/20!/4/2[introduction]/2[c1_h]/1:0)',
// Bundled asset so it works on every platform (including web, where
// cross-origin URLs are blocked by CORS). On web you can also use
// EpubSource.fromData(bytes).
epubSource: EpubSource.fromAsset('assets/sample.epub'),
epubController: epubController,
displaySettings: EpubDisplaySettings(
flow: EpubFlow.paginated,
useSnapAnimationAndroid: false,
snap: true,
theme: EpubTheme.light(),
// Safe here because the bundled sample is trusted. Keep this `false`
// for user-provided / untrusted EPUBs (see EpubDisplaySettings docs).
allowScriptedContent: true,
),
selectionContextMenu: EpubContextMenu(
hideDefaultSystemItems: true,
items: [
EpubContextMenuItem(
title: "Highlight",
id: 1,
action: () {
epubController.addHighlight(cfi: textSelectionCfi);
},
),
],
),
onChaptersLoaded: (loaded) {
setState(() {
chapters = loaded;
isLoading = false;
});
},
onRelocated: (value) {
setState(() {
progress = value.progress;
pageInfo = [
if (value.chapterPage != null)
'page ${value.chapterPage}/${value.chapterTotalPages} in chapter',
if (value.printedPage != null) 'printed p. ${value.printedPage}',
].join(' · ');
});
},
onTextSelected: (epubTextSelection) {
textSelectionCfi = epubTextSelection.selectionCfi;
},
onFootnoteTapped: _showFootnote,
onLoadError: (error) {
setState(() => isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not open the book: ${error.message}')),
);
},
selectAnnotationRange: true,
),
Visibility(
visible: isLoading,
child: const Center(child: CircularProgressIndicator()),
),
],
);
}
}