Untitled

 avatar
unknown
dart
a year ago
40 kB
3
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;
}