Untitled
unknown
dart
a year ago
40 kB
4
Indexable
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; }
Editor is loading...