import 'dart:async';
import 'dart:developer';
import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:base_architecture/core/constants/assets.dart';
import 'package:base_architecture/core/constants/theme.dart';
import 'package:base_architecture/core/shared_widgets/arab_textfield_widget.dart';
import 'package:base_architecture/epub/epubx/entities/epub_book.dart';
import 'package:base_architecture/epub/epubx/entities/epub_chapter.dart';
import 'package:base_architecture/epub/src/data/epub_cfi_reader.dart';
import 'package:base_architecture/epub/src/data/epub_parser.dart';
import 'package:base_architecture/epub/src/data/models/chapter.dart';
import 'package:base_architecture/epub/src/data/models/chapter_view_value.dart';
import 'package:base_architecture/epub/src/data/models/paragraph.dart';
import 'package:base_architecture/epub/src/ui/substring_highlight.dart';
import 'package:base_architecture/presentation/notifiers/book_view_notifier.dart';
import 'package:base_architecture/presentation/notifiers/theme_notifier.dart';
import 'package:base_architecture/presentation/pages/main/main_page.dart';
import 'package:base_architecture/presentation/resources/color_manager.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_uxcam/flutter_uxcam.dart';
import 'package:flutter_widget_offset/flutter_widget_offset.dart';
import 'package:html/parser.dart';
import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:url_launcher/url_launcher.dart';
part '../epub_controller.dart';
part '../helpers/epub_view_builders.dart';
const double _minTrailingEdge = 0.55;
const double _minLeadingEdge = -0.05;
typedef ExternalLinkPressed = void Function(String href);
class EpubView extends StatefulWidget {
const EpubView({
required this.controller,
this.onExternalLinkPressed,
this.onChapterChanged,
this.onDocumentLoaded,
this.onDocumentError,
required this.lang,
this.builders = const EpubViewBuilders<DefaultBuilderOptions>(
options: DefaultBuilderOptions(),
),
this.shrinkWrap = false,
this.animatoTo,
Key? key,
}) : super(key: key);
final EpubController controller;
final ExternalLinkPressed? onExternalLinkPressed;
final bool shrinkWrap;
final void Function(EpubChapterViewValue? value)? onChapterChanged;
final int? animatoTo;
final String lang;
/// Called when a document is loaded
final void Function(EpubBook document)? onDocumentLoaded;
/// Called when a document loading error
final void Function(Exception? error)? onDocumentError;
/// Builders
final EpubViewBuilders builders;
@override
State<EpubView> createState() => _EpubViewState();
}
class _EpubViewState extends State<EpubView> {
Exception? _loadingError;
static ScrollController scrollController = ScrollController();
ItemScrollController? _itemScrollController;
ItemPositionsListener? _itemPositionListener;
List<EpubChapter> _chapters = [];
List<Paragraph> _paragraphs = [];
EpubCfiReader? _epubCfiReader;
EpubChapterViewValue? _currentValue;
final List<int> _chapterIndexes = <int>[];
static String search = '';
late PageController controller;
BookViewNotifier bookViewNotifier = Provider.of<BookViewNotifier>(
navigatorKey.currentContext!,
listen: false,
);
EpubController get _controller => widget.controller;
static final OffsetDetectorController? _offsetDetectorController =
OffsetDetectorController();
@override
void initState() {
super.initState();
search = '';
_itemScrollController = ItemScrollController();
_itemPositionListener = ItemPositionsListener.create();
_controller._attach(this);
_controller.loadingState.addListener(() {
switch (_controller.loadingState.value) {
case EpubViewLoadingState.loading:
break;
case EpubViewLoadingState.success:
widget.onDocumentLoaded?.call(_controller._document!);
break;
case EpubViewLoadingState.error:
widget.onDocumentError?.call(_loadingError);
break;
}
if (mounted) {
setState(() {});
}
});
}
@override
void dispose() {
bookViewNotifier.resetReadAppBar();
_itemPositionListener!.itemPositions.removeListener(_changeListener);
_controller._detach();
super.dispose();
}
Future<bool> _init() async {
if (_controller.isBookLoaded.value) {
return true;
}
_chapters = parseChapters(_controller._document!);
final ParseParagraphsResult parseParagraphsResult =
parseParagraphs(_chapters, _controller._document!.Content);
_paragraphs = parseParagraphsResult.flatParagraphs;
bookViewNotifier.checkBookTotalPages(_chapters.length);
if (_chapters.length == 1) {
bookViewNotifier.addToLibrary();
}
_chapterIndexes.addAll(parseParagraphsResult.chapterIndexes);
_epubCfiReader = EpubCfiReader.parser(
cfiInput: _controller.epubCfi,
chapters: _chapters,
paragraphs: _paragraphs,
);
_itemPositionListener!.itemPositions.addListener(_changeListener);
_controller.isBookLoaded.value = true;
_currentValue = EpubChapterViewValue(
// start index of chapter page
chapter: _chapters[0],
chapterNumber: 1,
paragraphNumber: 1,
position: ItemPosition(
// start index of chapter page
index: widget.animatoTo ?? 0,
itemLeadingEdge: 10,
itemTrailingEdge: 10,
),
);
bookViewNotifier.setBookPageNumber(_currentValue?.position.index ?? 0);
_controller.currentValueListenable.value = _currentValue;
// start index of chapter page
controller = PageController(initialPage: widget.animatoTo ?? 0);
return true;
}
void _changeListener() {
if (_paragraphs.isEmpty ||
_itemPositionListener!.itemPositions.value.isEmpty) {
return;
}
final ItemPosition position =
_itemPositionListener!.itemPositions.value.first;
final int chapterIndex = _getChapterIndexBy(
positionIndex: position.index,
trailingEdge: position.itemTrailingEdge,
leadingEdge: position.itemLeadingEdge,
);
final int paragraphIndex = _getParagraphIndexBy(
positionIndex: position.index,
trailingEdge: position.itemTrailingEdge,
leadingEdge: position.itemLeadingEdge,
);
_currentValue = EpubChapterViewValue(
chapter: chapterIndex >= 0 ? _chapters[chapterIndex] : null,
chapterNumber: chapterIndex + 1,
paragraphNumber: paragraphIndex + 1,
position: position,
);
_controller.currentValueListenable.value = _currentValue;
widget.onChapterChanged?.call(_currentValue);
if (mounted) {
setState(() {});
}
}
void _gotoEpubCfi(
String? epubCfi, {
double alignment = 0,
Duration duration = const Duration(milliseconds: 250),
Curve curve = Curves.linear,
}) {
_epubCfiReader?.epubCfi = epubCfi;
final int? index = _epubCfiReader?.paragraphIndexByCfiFragment;
if (index == null) {
return;
}
controller.animateToPage(
index,
duration: duration,
curve: curve,
);
}
void _onLinkPressed(String href1) {
String url = href1
.replaceAll('<span style="color: yellow">', '')
.replaceAll('<span/>', '');
if (href1.contains('://')) {
_launchUrl(url);
// widget.onExternalLinkPressed?.call(href);
return;
}
// Chapter01.xhtml#ph1_1 -> [ph1_1, Chapter01.xhtml] || [ph1_1]
String? hrefIdRef;
String? hrefFileName;
if (url.contains('#')) {
final List<String> dividedHref = url.split('#');
if (dividedHref.length == 1) {
hrefIdRef = url;
} else {
hrefFileName = dividedHref[0];
hrefIdRef = dividedHref[1];
}
} else {
hrefFileName = url;
}
if (hrefIdRef == null) {
final EpubChapter? chapter = _chapterByFileName(hrefFileName);
if (chapter != null) {
final String? cfi = _epubCfiReader?.generateCfiChapter(
book: _controller._document,
chapter: chapter,
additional: ['/4/2'],
);
_gotoEpubCfi(cfi);
}
return;
} else {
final Paragraph? paragraph = _paragraphByIdRef(hrefIdRef);
final EpubChapter? chapter =
paragraph != null ? _chapters[paragraph.chapterIndex] : null;
if (chapter != null && paragraph != null) {
final int? paragraphIndex =
_epubCfiReader?.getParagraphIndexByElement(paragraph.element);
final String? cfi = _epubCfiReader?.generateCfi(
book: _controller._document,
chapter: chapter,
paragraphIndex: paragraphIndex,
);
_gotoEpubCfi(cfi);
}
return;
}
}
Paragraph? _paragraphByIdRef(String idRef) =>
_paragraphs.firstWhereOrNull((Paragraph paragraph) {
if (paragraph.element.id == idRef) {
return true;
}
return paragraph.element.children.isNotEmpty &&
paragraph.element.children[0].id == idRef;
});
EpubChapter? _chapterByFileName(String? fileName) =>
_chapters.firstWhereOrNull((EpubChapter chapter) {
if (fileName != null) {
if (chapter.ContentFileName!.contains(fileName)) {
return true;
} else {
return false;
}
}
return false;
});
int _getChapterIndexBy({
required int positionIndex,
double? trailingEdge,
double? leadingEdge,
}) {
final int posIndex = _getAbsParagraphIndexBy(
positionIndex: positionIndex,
trailingEdge: trailingEdge,
leadingEdge: leadingEdge,
);
final int index = posIndex >= _chapterIndexes.last
? _chapterIndexes.length
: _chapterIndexes.indexWhere((int chapterIndex) {
if (posIndex < chapterIndex) {
return true;
}
return false;
});
return index - 1;
}
int _getParagraphIndexBy({
required int positionIndex,
double? trailingEdge,
double? leadingEdge,
}) {
final int posIndex = _getAbsParagraphIndexBy(
positionIndex: positionIndex,
trailingEdge: trailingEdge,
leadingEdge: leadingEdge,
);
final int index = _getChapterIndexBy(positionIndex: posIndex);
if (index == -1) {
return posIndex;
}
return posIndex - _chapterIndexes[index];
}
int _getAbsParagraphIndexBy({
required int positionIndex,
double? trailingEdge,
double? leadingEdge,
}) {
int posIndex = positionIndex;
if (trailingEdge != null &&
leadingEdge != null &&
trailingEdge < _minTrailingEdge &&
leadingEdge < _minLeadingEdge) {
posIndex += 1;
}
return posIndex;
}
static Widget _chapterDividerBuilder(EpubChapter chapter) => Container(
height: 56,
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color(0x24000000),
),
alignment: Alignment.centerLeft,
child: Text(
chapter.Title ?? '',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
),
),
);
static void _launchUrl(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
static Widget _chapterBuilder(
BuildContext context,
EpubViewBuilders builders,
EpubBook document,
List<EpubChapter> chapters,
List<Paragraph> paragraphs,
int index,
int chapterIndex,
int paragraphIndex,
ExternalLinkPressed onExternalLinkPressed,
) {
if (paragraphs.isEmpty) {
return Container();
}
final EpubViewBuilders<DefaultBuilderOptions> defaultBuilder =
builders as EpubViewBuilders<DefaultBuilderOptions>;
final DefaultBuilderOptions options = defaultBuilder.options;
BookViewNotifier _bookViewNotifier =
Provider.of<BookViewNotifier>(context, listen: true);
return SingleChildScrollView(
controller: scrollController,
physics: MediaQuery.of(context).accessibleNavigation
? NeverScrollableScrollPhysics()
: BouncingScrollPhysics(),
child: Column(
children: <Widget>[
if (chapterIndex >= 0 && paragraphIndex == 0)
builders.chapterDividerBuilder(chapters[chapterIndex]),
Html(
data: search.isEmpty
? chapters[index].HtmlContent!
: chapters[index].HtmlContent!.replaceAll(
search.toLowerCase().trim(),
'<span style="color: yellow">$search<span/>',
),
shrinkWrap: true,
onLinkTap: (String? href, _, __, ___) {
onExternalLinkPressed(href!);
},
style: {
'html': Style(
width: Width(MediaQuery.of(context).size.width),
padding: options.paragraphPadding as EdgeInsets?,
).merge(
Style.fromTextStyle(
options.textStyle,
),
),
},
customRenders: {
tagMatcher('img'): CustomRender.widget(
widget: (RenderContext context, buildChildren) {
final String url = context.tree.element!.attributes['src']!
.replaceAll('../', '');
return Image(
image: MemoryImage(
Uint8List.fromList(
document.Content!.Images![url]!.Content!,
),
),
);
},
),
tagMatcher('span'): _customRender(options, chapterIndex),
tagMatcher('p'): CustomRender.widget(
widget: (RenderContext contextRender, buildChildren) =>
Semantics(
onDidGainAccessibilityFocus: () {
if (_bookViewNotifier.readAppBar == false) {
log('focused');
_bookViewNotifier.resetReadAppBar();
}
},
focused: true,
container: true,
child: Text(
contextRender.tree.element!.text,
style: options.textStyle,
),
),
),
// tagMatcher('h1'): _customRender(options, chapterIndex),
// tagMatcher('h2'): _customRender(options, chapterIndex),
// tagMatcher('h3'): _customRender(options, chapterIndex),
// tagMatcher('h4'): _customRender(options, chapterIndex),
// tagMatcher('a'):
// _customRender(options, chapterIndex, isLink: true),
},
),
],
),
);
}
String chapterName = '';
AppBar _appBar(
BuildContext context,
ThemeNotifier themeNotifier,
) =>
AppBar(
backgroundColor: themeNotifier.getTheme().primaryColor,
elevation: 0.0,
automaticallyImplyLeading: false,
centerTitle: false,
leadingWidth: 70,
toolbarHeight: 100,
leading: InkWell(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
onLongPress: () {
while (Navigator.canPop(context)) {
Navigator.pop(context);
}
},
onTap: () {
Navigator.of(context).pop();
bookViewNotifier
.postReadingPage(_currentValue!.position.index.toString());
mixpanel?.track('Pressed back ');
FlutterUxcam.logEvent('Pressed back ');
},
child: Semantics(
label: 'Back'.tr(),
button: true,
excludeSemantics: true,
child: Container(
width: 45,
height: 45,
decoration: BoxDecoration(
color: themeNotifier.getTheme().focusColor,
border: Border.all(
color: themeNotifier.getTheme() == AppThemes().darkTheme
? Colors.transparent
: HexColor.fromHex('#E9EBEF'),
),
borderRadius: const BorderRadius.all(
Radius.circular(10),
),
),
margin: const EdgeInsetsDirectional.only(
start: 24,
bottom: 35,
top: 20,
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: context.locale != const Locale('en')
? SvgPicture.asset(
Assets.icArrowBack,
color: themeNotifier.getTheme().hoverColor,
)
: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(math.pi),
child: SvgPicture.asset(
Assets.icArrowBack,
color: themeNotifier.getTheme().hoverColor,
),
),
),
),
),
title: Semantics(
explicitChildNodes: true,
child: Row(
children: [
Expanded(
flex: 3,
child: Text(
'Chapter:'.tr() +
' ' +
getChapterName(_chapters, _currentValue, chapterName),
//'${_chapters[0].HtmlContent}',
// '${_currentValue?.chapter?.Title?.replaceAll('\n', '').trim() ?? ''}',
textAlign: TextAlign.center,
maxLines: 2,
style: TextStyle(
color: themeNotifier.getTheme().hoverColor,
),
),
),
Expanded(
child: Semantics(
label: ''.tr(),
button: true,
child: _serach(context, themeNotifier),
),
),
],
),
),
);
TextEditingController myController = TextEditingController();
Widget _buildLoaded(BuildContext context) {
ThemeNotifier themeNotifier = Provider.of<ThemeNotifier>(
context,
listen: false,
);
return Column(
children: <Widget>[
if (bookViewNotifier.readAppBar == true)
_appBar(context, themeNotifier)
else
ExcludeSemantics(child: _appBar(context, themeNotifier)),
Expanded(
child: PageView(
allowImplicitScrolling: false,
children: List<Widget>.generate(
_chapters.length,
(int index) => Directionality(
textDirection: widget.lang == 'en'
? ui.TextDirection.ltr
: ui.TextDirection.rtl,
child: widget.builders.chapterBuilder(
context,
widget.builders,
widget.controller._document!,
_chapters,
_paragraphs,
index,
_getChapterIndexBy(positionIndex: index),
_getParagraphIndexBy(positionIndex: index),
_onLinkPressed,
),
),
),
scrollDirection: Axis.horizontal,
// reverse: true,
physics: const BouncingScrollPhysics(),
controller: controller,
onPageChanged: (int num) {
if (_paragraphs.isEmpty) {
return;
}
final int chapterIndex = _getChapterIndexBy(
positionIndex: num,
);
final int paragraphIndex = _getParagraphIndexBy(
positionIndex: num,
);
_currentValue = EpubChapterViewValue(
chapter: chapterIndex >= 0 ? _chapters[num] : null,
chapterNumber: num + 1,
paragraphNumber: paragraphIndex + 1,
position: ItemPosition(
index: num,
itemLeadingEdge: controller.position.extentInside,
itemTrailingEdge: controller.position.extentInside,
),
);
bookViewNotifier
.postReadingPage(_currentValue!.position.index.toString());
bookViewNotifier
.setBookPageNumber(_currentValue?.position.index ?? 0);
int chapter50 = (_chapters.length ~/ 2);
if (_currentValue!.chapterNumber == chapter50) {
bookViewNotifier.addToLibrary();
}
print(_currentValue?.position.index ?? 0);
_controller.currentValueListenable.value = _currentValue;
widget.onChapterChanged?.call(_currentValue);
if (mounted) {
setState(() {});
}
},
),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(top: 20, left: 20, right: 20),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(10),
child: SizedBox(
width: 80,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () {
if (context.locale == const Locale('ar')) {
controller.nextPage(
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
);
} else {
controller.previousPage(
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
);
}
},
child: Icon(
Icons.arrow_back,
size: 20,
color: themeNotifier.getTheme().hintColor,
),
),
Text(
((_currentValue?.position.index ?? 0) + 1).toString() +
'/' +
_chapters.length.toString(),
style: themeNotifier.getTheme().textTheme.headlineSmall,
),
Expanded(
child: InkWell(
onTap: () {
if (context.locale == const Locale('ar')) {
controller.previousPage(
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
);
} else {
controller.nextPage(
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
);
}
},
child: Icon(
Icons.arrow_forward,
size: 20,
color: themeNotifier.getTheme().hintColor,
),
),
),
],
),
),
),
),
),
),
],
);
}
Widget _swiper(BuildContext context, ThemeNotifier themeNotifier) => Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
width: 240,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
iconSize: 40,
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
onPressed: () {
if (context.locale == const Locale('ar')) {
controller.nextPage(
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
);
} else {
controller.previousPage(
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
);
}
},
icon: SvgPicture.asset(
context.locale == const Locale('ar')
? 'assets/svg/next.svg'
: 'assets/svg/back.svg',
),
),
Flexible(
child: Text(
_currentValue?.chapter?.Title ?? '',
style: themeNotifier
.getTheme()
.textTheme
.headline5!
.copyWith(fontSize: 20),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
IconButton(
iconSize: 40,
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
onPressed: () {
if (context.locale == const Locale('ar')) {
controller.previousPage(
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
);
} else {
controller.nextPage(
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
);
}
},
icon: SvgPicture.asset(
context.locale == const Locale('ar')
? 'assets/svg/back.svg'
: 'assets/svg/next.svg',
),
),
],
),
),
);
Widget _serach(BuildContext context, ThemeNotifier themeNotifier) =>
GestureDetector(
onTap: () {
showModal(context);
},
child: Semantics(
button: true,
label: 'search here'.tr(),
child: Icon(
Icons.search,
color: themeNotifier.getTheme().hoverColor,
),
),
);
void showModal(context) {
ThemeNotifier themeNotifier = Provider.of<ThemeNotifier>(
context,
listen: false,
);
List<Paragraph> data = search.isNotEmpty
? _paragraphs
.where(
(Paragraph element) => element.element.text
.trim()
.toLowerCase()
.contains(search.trim().toLowerCase()),
)
.toList()
: [];
showModalBottomSheet(
isScrollControlled: true,
useSafeArea: true,
backgroundColor: themeNotifier.getTheme().primaryColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(15.0)),
),
context: context,
builder: (BuildContext context) => StatefulBuilder(
builder: (BuildContext context, StateSetter setState) => Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
InkWell(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
onTap: () {
Navigator.of(context).pop();
mixpanel?.track('Pressed back ');
FlutterUxcam.logEvent('Pressed back ');
},
child: Semantics(
label: 'back'.tr(),
button: true,
excludeSemantics: true,
child: Align(
alignment: AlignmentDirectional.topStart,
child: Container(
width: 45,
height: 45,
decoration: BoxDecoration(
color: themeNotifier.getTheme().focusColor,
border: Border.all(
color:
themeNotifier.getTheme() == AppThemes().darkTheme
? Colors.transparent
: HexColor.fromHex('#E9EBEF'),
),
borderRadius: const BorderRadius.all(
Radius.circular(10),
),
),
margin: const EdgeInsetsDirectional.only(
start: 14,
bottom: 15,
top: 10,
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: context.locale != const Locale('en')
? SvgPicture.asset(
Assets.icArrowBack,
color: themeNotifier.getTheme().hoverColor,
)
: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(math.pi),
child: SvgPicture.asset(
Assets.icArrowBack,
color: themeNotifier.getTheme().hoverColor,
),
),
),
),
),
),
Semantics(
label: 'search here'.tr(),
child: ArabTextField(
hintText: ''.tr(),
isRequired: false,
labelText: '',
initialValue: search,
onChanged: (String p0) {
search = p0.trim();
setState(() {
if (search.isNotEmpty) {
data = _paragraphs
.where(
(Paragraph element) => element.element.text
.trim()
.replaceAll(' ', '')
.toLowerCase()
.contains(search.trim().toLowerCase()),
)
.toList();
} else {
data.clear();
}
});
},
prefixIcon: Icon(
Icons.search,
color: themeNotifier.getTheme().hoverColor,
),
),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: data.length,
itemBuilder: (BuildContext context, int index) {
String input = data[index].element.text;
List<String> words = input.split(' ');
String startWord = search;
int startIndex = input.indexOf(startWord);
words.forEach((String element) {
if (element
.trim()
.toLowerCase()
.contains(search.trim().toLowerCase())) {
startIndex = input.indexOf(element);
}
});
String output =
input.substring(startIndex == -1 ? 0 : startIndex);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
),
child: InkWell(
onTap: () async {
await _onSearchSelect(
context,
index,
setState,
output,
);
},
child: Column(
children: [
SubstringHighlight(
scrollController: scrollController,
text: output.trim().replaceAll(' ', ''),
maxLines: 2,
terms: [search],
caseSensitive: false,
textStyle: widget.builders.options.textStyle,
textStyleHighlight: const TextStyle(
// highlight style
color: Colors.amber,
decoration: TextDecoration.underline,
),
),
Divider(
color: themeNotifier.getTheme().hoverColor,
)
],
),
),
),
);
},
),
)
],
),
),
),
);
}
Future<void> _onSearchSelect(
BuildContext context,
int index,
StateSetter setState1,
String output,
) async {
Navigator.of(context).pop();
lastOffest = 0;
////////////////////////////////////////////////////////////////////////////////////
if (search.isNotEmpty) {
bookViewNotifier.setReadAppBar();
/// check in current page first
int index = _paragraphs.indexWhere(
(Paragraph element) =>
element.element.text.toLowerCase().trim().contains(
output.toLowerCase().trim(),
),
);
await controller
.animateToPage(
_paragraphs[index].chapterIndex,
duration: const Duration(milliseconds: 300),
curve: Curves.linear,
)
.whenComplete(() {
currentIndex = _currentValue!.position.index;
// _customRender(widget.builders.options, currentIndex, scroll: true);
if (mounted) {
setState(() {});
}
});
}
////////////////////////////////////////////////////////////////////////////////////
}
static BuildContext? targetContext;
static double? lastOffest;
static int currentIndex = 0;
static CustomRender _customRender(DefaultBuilderOptions options, int index,
{int? postion, bool? scroll}) =>
CustomRender.widget(
widget: (RenderContext contextRender, buildChildren) =>
_textView(contextRender, index, options, scroll));
static OffsetDetector _textView(
RenderContext contextRender, int index, DefaultBuilderOptions options,
[bool? scroll]) =>
OffsetDetector(
controller: _offsetDetectorController,
onChanged:
(Size size, EdgeInsets offset, EdgeInsets rootPadding) async {
if (contextRender.tree.element!.text
.trim()
.toLowerCase()
.contains(search.trim().toLowerCase()) &&
lastOffest == 0 &&
currentIndex == index) {
debugPrint(
'The offset to edge of root(ltrb): ${offset.left}, ${offset.top}, ${offset.right}, ${offset.bottom}',
);
lastOffest = offset.top.isNegative || offset.top < 200
? 0
: offset.top * 2.4;
/// zoom to word
if (scroll ?? false) {
await scrollController.animateTo(
lastOffest ?? 0,
duration: const Duration(seconds: 1),
curve: Curves.linear,
);
}
}
},
child: SubstringHighlight(
scrollController: scrollController,
// key: GlobalObjectKey(search),
text: contextRender.tree.element!.text,
term: search,
caseSensitive: false,
textStyle: options.textStyle.copyWith(
fontWeight: contextRender.tree.style.fontWeight,
backgroundColor: contextRender.tree.style.backgroundColor,
fontStyle: contextRender.tree.style.fontStyle,
fontFamily: contextRender.tree.style.fontFamily,
letterSpacing: contextRender.tree.style.letterSpacing,
overflow: contextRender.tree.style.textOverflow),
textStyleHighlight: const TextStyle(
// highlight style
color: Colors.amber,
decoration: TextDecoration.underline,
),
),
);
// int _index() => _chapters.indexWhere(
// (EpubChapter element) =>
// element.HtmlContent!.toLowerCase().contains(search.toLowerCase()),
// );
static Widget _builder(
BuildContext context,
EpubViewBuilders builders,
EpubViewLoadingState state,
WidgetBuilder loadedBuilder,
Exception? loadingError,
) {
final Widget content = () {
switch (state) {
case EpubViewLoadingState.loading:
return KeyedSubtree(
key: const Key('epubx.root.loading'),
child: builders.loaderBuilder?.call(context) ?? const SizedBox(),
);
case EpubViewLoadingState.error:
return KeyedSubtree(
key: const Key('epubx.root.error'),
child: Padding(
padding: const EdgeInsets.all(32),
child: builders.errorBuilder?.call(context, loadingError!) ??
Center(child: Text(loadingError.toString())),
),
);
case EpubViewLoadingState.success:
return KeyedSubtree(
key: const Key('epubx.root.success'),
child: loadedBuilder(context),
);
}
}();
final EpubViewBuilders<DefaultBuilderOptions> defaultBuilder =
builders as EpubViewBuilders<DefaultBuilderOptions>;
final DefaultBuilderOptions options = defaultBuilder.options;
return AnimatedSwitcher(
duration: options.loaderSwitchDuration,
transitionBuilder: options.transitionBuilder,
child: content,
);
}
@override
Widget build(BuildContext context) => widget.builders.builder(
context,
widget.builders,
_controller.loadingState.value,
_buildLoaded,
_loadingError,
);
}
String getChapterName(
List<EpubChapter> chapters,
EpubChapterViewValue? _currentValue,
String chapterName,
) {
var document = parse(
chapters[_currentValue!.chapterNumber - 1].HtmlContent,
);
var x = document.getElementsByTagName('h1').firstOrNull?.innerHtml;
x ??= document.getElementsByTagName('h2').firstOrNull?.innerHtml ?? '';
//getElementsByTagName('h1')[0].innerHtml ?? '';
chapterName = x;
return x;
}