mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
357 lines
8.0 KiB
Dart
357 lines
8.0 KiB
Dart
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/components/custom_slider.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<Chapter ID, Chapter Name>.
|
|
/// null if the comic is a gallery
|
|
final Map<String, String>? 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<Reader> createState() => _ReaderState();
|
|
}
|
|
|
|
class _ReaderState extends State<Reader> 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<String>? 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<void> animateToPage(int page);
|
|
|
|
void handleDoubleTap(Offset location);
|
|
|
|
void handleLongPressDown(Offset location);
|
|
|
|
void handleLongPressUp(Offset location);
|
|
|
|
void handleKeyEvent(KeyEvent event);
|
|
}
|