library venera_reader; import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/image_provider/reader_image.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/file_type.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; import 'package:venera/utils/volume.dart'; import 'package:window_manager/window_manager.dart'; import 'package:battery_plus/battery_plus.dart'; part 'scaffold.dart'; part 'images.dart'; part 'gesture.dart'; part 'comic_image.dart'; extension _ReaderContext on BuildContext { _ReaderState get reader => findAncestorStateOfType<_ReaderState>()!; _ReaderScaffoldState get readerScaffold => findAncestorStateOfType<_ReaderScaffoldState>()!; } class Reader extends StatefulWidget { const Reader({ super.key, required this.type, required this.cid, required this.name, required this.chapters, required this.history, this.initialPage, this.initialChapter, }); final ComicType type; final String cid; final String name; /// Map. /// null if the comic is a gallery final Map? chapters; /// Starts from 1, invalid values equal to 1 final int? initialPage; /// Starts from 1, invalid values equal to 1 final int? initialChapter; final History history; @override State createState() => _ReaderState(); } class _ReaderState extends State with _ReaderLocation, _ReaderWindow { @override void update() { setState(() {}); } @override int get maxPage => images?.length ?? 1; ComicType get type => widget.type; String get cid => widget.cid; String get eid => widget.chapters?.keys.elementAt(chapter - 1) ?? '0'; List? images; late ReaderMode mode; History? history; @override bool isLoading = false; var focusNode = FocusNode(); VolumeListener? volumeListener; @override void initState() { page = widget.initialPage ?? 1; chapter = widget.initialChapter ?? 1; mode = ReaderMode.fromKey(appdata.settings['readerMode']); history = widget.history; Future.microtask(() { updateHistory(); }); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); if(appdata.settings['enableTurnPageByVolumeKey']) { handleVolumeEvent(); } super.initState(); } @override void dispose() { autoPageTurningTimer?.cancel(); focusNode.dispose(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); stopVolumeEvent(); Future.microtask(() { DataSync().onDataChanged(); }); super.dispose(); } @override Widget build(BuildContext context) { return KeyboardListener( focusNode: focusNode, autofocus: true, onKeyEvent: onKeyEvent, child: _ReaderScaffold( child: _ReaderGestureDetector( child: _ReaderImages(key: Key(chapter.toString())), ), ), ); } void onKeyEvent(KeyEvent event) { _imageViewController?.handleKeyEvent(event); } @override int get maxChapter => widget.chapters?.length ?? 1; @override void onPageChanged() { updateHistory(); } void updateHistory() { if(history != null) { history!.page = page; history!.ep = chapter; history!.readEpisode.add(chapter); HistoryManager().addHistory(history!); } } void handleVolumeEvent() { if(!App.isAndroid) { // Currently only support Android return; } if(volumeListener != null) { volumeListener?.cancel(); } volumeListener = VolumeListener( onDown: () { toNextPage(); }, onUp: () { toPrevPage(); }, )..listen(); } void stopVolumeEvent() { if(volumeListener != null) { volumeListener?.cancel(); volumeListener = null; } } } abstract mixin class _ReaderLocation { int _page = 1; int get page => _page; set page(int value) { _page = value; onPageChanged(); } int chapter = 1; int get maxPage; int get maxChapter; bool get isLoading; void update(); bool get enablePageAnimation => appdata.settings['enablePageAnimation']; _ImageViewController? _imageViewController; void onPageChanged(); void setPage(int page) { // Prevent page change during animation if (_animationCount > 0) { return; } this.page = page; } bool _validatePage(int page) { return page >= 1 && page <= maxPage; } /// Returns true if the page is changed bool toNextPage() { return toPage(page + 1); } /// Returns true if the page is changed bool toPrevPage() { return toPage(page - 1); } int _animationCount = 0; bool toPage(int page) { if (_validatePage(page)) { if (page == this.page) { if(!(chapter == 1 && page == 1) && !(chapter == maxChapter && page == maxPage)) { return false; } } this.page = page; update(); if (enablePageAnimation) { _animationCount++; _imageViewController!.animateToPage(page).then((_) { _animationCount--; }); } else { _imageViewController!.toPage(page); } return true; } return false; } bool _validateChapter(int chapter) { return chapter >= 1 && chapter <= maxChapter; } /// Returns true if the chapter is changed bool toNextChapter() { return toChapter(chapter + 1); } /// Returns true if the chapter is changed bool toPrevChapter() { return toChapter(chapter - 1); } bool toChapter(int c) { if (_validateChapter(c) && !isLoading) { chapter = c; page = 1; update(); return true; } return false; } Timer? autoPageTurningTimer; void autoPageTurning() { if (autoPageTurningTimer != null) { autoPageTurningTimer!.cancel(); autoPageTurningTimer = null; } else { int interval = appdata.settings['autoPageTurningInterval']; autoPageTurningTimer = Timer.periodic(Duration(seconds: interval), (_) { if (page == maxPage) { autoPageTurningTimer!.cancel(); } toNextPage(); }); } } } mixin class _ReaderWindow { bool isFullscreen = false; void fullscreen() { windowManager.setFullScreen(!isFullscreen); isFullscreen = !isFullscreen; } } enum ReaderMode { galleryLeftToRight('galleryLeftToRight'), galleryRightToLeft('galleryRightToLeft'), galleryTopToBottom('galleryTopToBottom'), continuousTopToBottom('continuousTopToBottom'), continuousLeftToRight('continuousLeftToRight'), continuousRightToLeft('continuousRightToLeft'); final String key; bool get isGallery => key.startsWith('gallery'); bool get isContinuous => key.startsWith('continuous'); const ReaderMode(this.key); static ReaderMode fromKey(String key) { for (var mode in values) { if (mode.key == key) { return mode; } } return galleryLeftToRight; } } abstract interface class _ImageViewController { void toPage(int page); Future animateToPage(int page); void handleDoubleTap(Offset location); void handleLongPressDown(Offset location); void handleLongPressUp(Offset location); void handleKeyEvent(KeyEvent event); }