mirror of
https://github.com/venera-app/venera.git
synced 2025-12-17 07:21:15 +00:00
Compare commits
33 Commits
fix/comic-
...
v1.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49abf92724 | ||
|
|
38376c5b2e | ||
|
|
4053faa186 | ||
|
|
17fd9b3606 | ||
|
|
792c41fdc3 | ||
|
|
05e661b101 | ||
|
|
46131fcf41 | ||
|
|
59750332cd | ||
|
|
fd017a35f9 | ||
|
|
3834d0211f | ||
|
|
10bec09c80 | ||
|
|
62dd742280 | ||
|
|
03603a53e1 | ||
|
|
2847af91ff | ||
|
|
0bc01f718a | ||
|
|
b60119170a | ||
|
|
f4af6f3954 | ||
|
|
9e9d1ac3b1 | ||
|
|
b3b9199cc3 | ||
| dd00ba11c8 | |||
| e87fb535b8 | |||
|
|
df1649def6 | ||
|
|
99559eaff8 | ||
|
|
39a834815d | ||
|
|
a9e76201f3 | ||
|
|
0044d95e97 | ||
| 5ccf0eea43 | |||
| e8d98e8274 | |||
|
|
d22501198a | ||
|
|
be23c4fe68 | ||
|
|
a8422780a0 | ||
|
|
75c2a3a417 | ||
|
|
3d194d7f6a |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -143,7 +143,7 @@ jobs:
|
|||||||
- run: |
|
- run: |
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
|
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
|
||||||
dart pub global activate flutter_to_debian
|
dart pub global activate -s git https://github.com/venera-app/flutter_to_debian.git
|
||||||
- run: python3 debian/build.py x64
|
- run: python3 debian/build.py x64
|
||||||
- run: dart run flutter_to_arch
|
- run: dart run flutter_to_arch
|
||||||
- run: |
|
- run: |
|
||||||
@@ -171,7 +171,7 @@ jobs:
|
|||||||
flutter pub get
|
flutter pub get
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
|
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
|
||||||
dart pub global activate flutter_to_debian
|
dart pub global activate -s git https://github.com/venera-app/flutter_to_debian.git
|
||||||
- name: "Patch font"
|
- name: "Patch font"
|
||||||
run: |
|
run: |
|
||||||
dart run patch/font.dart
|
dart run patch/font.dart
|
||||||
|
|||||||
@@ -1334,7 +1334,7 @@ let UI = {
|
|||||||
* Show an input dialog
|
* Show an input dialog
|
||||||
* @param title {string}
|
* @param title {string}
|
||||||
* @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message.
|
* @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message.
|
||||||
* @param image {string?} - Available since 1.4.6. An optional image to show in the dialog. You can use this to show a captcha.
|
* @param image {string | ArrayBuffer | null | undefined} - Since 1.4.6, you can pass an image url to show an image in the dialog. Since 1.5.3, you can also pass an ArrayBuffer to show a custom image.
|
||||||
* @returns {Promise<string | null>} - The input value. If the dialog is canceled, return null.
|
* @returns {Promise<string | null>} - The input value. If the dialog is canceled, return null.
|
||||||
*/
|
*/
|
||||||
showInputDialog: (title, validator, image) => {
|
showInputDialog: (title, validator, image) => {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
"Move to folder": "移动到文件夹",
|
"Move to folder": "移动到文件夹",
|
||||||
"Copy to folder": "复制到文件夹",
|
"Copy to folder": "复制到文件夹",
|
||||||
"Delete Comic": "删除漫画",
|
"Delete Comic": "删除漫画",
|
||||||
|
"Jump to Detail": "跳转详情",
|
||||||
"Delete @c comics?": "删除 @c 本漫画?",
|
"Delete @c comics?": "删除 @c 本漫画?",
|
||||||
"Add comic source": "添加漫画源",
|
"Add comic source": "添加漫画源",
|
||||||
"Delete comic source '@n' ?": "删除漫画源 '@n' ?",
|
"Delete comic source '@n' ?": "删除漫画源 '@n' ?",
|
||||||
@@ -69,6 +70,9 @@
|
|||||||
"Next": "前进",
|
"Next": "前进",
|
||||||
"Login with webview": "通过网页登录",
|
"Login with webview": "通过网页登录",
|
||||||
"Read": "阅读",
|
"Read": "阅读",
|
||||||
|
"Completed": "已完成",
|
||||||
|
"UnCompleted": "未完成",
|
||||||
|
"Filter reading status": "过滤阅读状态",
|
||||||
"Download": "下载",
|
"Download": "下载",
|
||||||
"Favorite": "收藏",
|
"Favorite": "收藏",
|
||||||
"Comments": "评论",
|
"Comments": "评论",
|
||||||
@@ -379,6 +383,8 @@
|
|||||||
"Continuous": "连续",
|
"Continuous": "连续",
|
||||||
"Display mode of comic list": "漫画列表的显示模式",
|
"Display mode of comic list": "漫画列表的显示模式",
|
||||||
"Show Page Number": "显示页码",
|
"Show Page Number": "显示页码",
|
||||||
|
"Show Chapter Comments": "显示章节评论",
|
||||||
|
"Chapter Comments": "章节评论",
|
||||||
"Jump to page": "跳转到页面",
|
"Jump to page": "跳转到页面",
|
||||||
"Page": "页面",
|
"Page": "页面",
|
||||||
"Jump": "跳转",
|
"Jump": "跳转",
|
||||||
@@ -464,6 +470,7 @@
|
|||||||
"Move": "移動",
|
"Move": "移動",
|
||||||
"Move to folder": "移動到資料夾",
|
"Move to folder": "移動到資料夾",
|
||||||
"Copy to folder": "複製到資料夾",
|
"Copy to folder": "複製到資料夾",
|
||||||
|
"Jump to Detail": "跳轉詳情",
|
||||||
"Delete Comic": "刪除漫畫",
|
"Delete Comic": "刪除漫畫",
|
||||||
"Delete @c comics?": "刪除 @c 本漫畫?",
|
"Delete @c comics?": "刪除 @c 本漫畫?",
|
||||||
"Add comic source": "添加漫畫源",
|
"Add comic source": "添加漫畫源",
|
||||||
@@ -487,6 +494,9 @@
|
|||||||
"Next": "前進",
|
"Next": "前進",
|
||||||
"Login with webview": "透過網頁登入",
|
"Login with webview": "透過網頁登入",
|
||||||
"Read": "閱讀",
|
"Read": "閱讀",
|
||||||
|
"Completed": "已完成",
|
||||||
|
"UnCompleted": "未完成",
|
||||||
|
"Filter reading status": "過濾閱讀狀態",
|
||||||
"Download": "下載",
|
"Download": "下載",
|
||||||
"Favorite": "收藏",
|
"Favorite": "收藏",
|
||||||
"Comments": "評論",
|
"Comments": "評論",
|
||||||
@@ -796,6 +806,8 @@
|
|||||||
"Continuous": "連續",
|
"Continuous": "連續",
|
||||||
"Display mode of comic list": "漫畫列表的顯示模式",
|
"Display mode of comic list": "漫畫列表的顯示模式",
|
||||||
"Show Page Number": "顯示頁碼",
|
"Show Page Number": "顯示頁碼",
|
||||||
|
"Show Chapter Comments": "顯示章節評論",
|
||||||
|
"Chapter Comments": "章節評論",
|
||||||
"Jump to page": "跳轉到頁面",
|
"Jump to page": "跳轉到頁面",
|
||||||
"Page": "頁面",
|
"Page": "頁面",
|
||||||
"Jump": "跳轉",
|
"Jump": "跳轉",
|
||||||
|
|||||||
@@ -553,6 +553,51 @@ If `load` function is implemented, `loadNext` function will be ignored.
|
|||||||
*/
|
*/
|
||||||
sendComment: async (comicId, subId, content, replyTo) => {
|
sendComment: async (comicId, subId, content, replyTo) => {
|
||||||
|
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* [Optional] load chapter comments
|
||||||
|
*
|
||||||
|
* Chapter comments are displayed in the reader.
|
||||||
|
* Same rich text support as loadComments.
|
||||||
|
*
|
||||||
|
* Note: To control reply functionality:
|
||||||
|
* - If a comment does not support replies, set its `id` to null/undefined
|
||||||
|
* - Or set its `replyCount` to null/undefined
|
||||||
|
* - The reply button will only show when both `id` and `replyCount` are present
|
||||||
|
*
|
||||||
|
* @param comicId {string}
|
||||||
|
* @param epId {string} - chapter id
|
||||||
|
* @param page {number}
|
||||||
|
* @param replyTo {string?} - commentId to reply, not null when reply to a comment
|
||||||
|
* @returns {Promise<{comments: Comment[], maxPage: number?}>}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Example for comments without reply support:
|
||||||
|
* return {
|
||||||
|
* comments: data.list.map(e => ({
|
||||||
|
* userName: e.user_name,
|
||||||
|
* avatar: e.user_avatar,
|
||||||
|
* content: e.comment,
|
||||||
|
* time: e.create_at,
|
||||||
|
* replyCount: null, // or undefined - no reply support
|
||||||
|
* id: null, // or undefined - no reply support
|
||||||
|
* })),
|
||||||
|
* maxPage: Math.ceil(total / 20)
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
loadChapterComments: async (comicId, epId, page, replyTo) => {
|
||||||
|
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* [Optional] send a chapter comment, return any value to indicate success
|
||||||
|
* @param comicId {string}
|
||||||
|
* @param epId {string} - chapter id
|
||||||
|
* @param content {string}
|
||||||
|
* @param replyTo {string?} - commentId to reply, not null when reply to a comment
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
sendChapterComment: async (comicId, epId, content, replyTo) => {
|
||||||
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* [Optional] like or unlike a comment
|
* [Optional] like or unlike a comment
|
||||||
|
|||||||
@@ -753,9 +753,9 @@ class SliverGridComics extends StatefulWidget {
|
|||||||
|
|
||||||
final List<MenuEntry> Function(Comic)? menuBuilder;
|
final List<MenuEntry> Function(Comic)? menuBuilder;
|
||||||
|
|
||||||
final void Function(Comic)? onTap;
|
final void Function(Comic, int heroID)? onTap;
|
||||||
|
|
||||||
final void Function(Comic)? onLongPressed;
|
final void Function(Comic, int heroID)? onLongPressed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SliverGridComics> createState() => _SliverGridComicsState();
|
State<SliverGridComics> createState() => _SliverGridComicsState();
|
||||||
@@ -856,28 +856,30 @@ class _SliverGridComics extends StatelessWidget {
|
|||||||
|
|
||||||
final List<MenuEntry> Function(Comic)? menuBuilder;
|
final List<MenuEntry> Function(Comic)? menuBuilder;
|
||||||
|
|
||||||
final void Function(Comic)? onTap;
|
final void Function(Comic, int heroID)? onTap;
|
||||||
|
|
||||||
final void Function(Comic)? onLongPressed;
|
final void Function(Comic, int heroID)? onLongPressed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SliverGrid(
|
return SliverGrid(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate((context, index) {
|
||||||
(context, index) {
|
|
||||||
if (index == comics.length - 1) {
|
if (index == comics.length - 1) {
|
||||||
onLastItemBuild?.call();
|
onLastItemBuild?.call();
|
||||||
}
|
}
|
||||||
var badge = badgeBuilder?.call(comics[index]);
|
var badge = badgeBuilder?.call(comics[index]);
|
||||||
var isSelected =
|
var isSelected = selection == null
|
||||||
selection == null ? false : selection![comics[index]] ?? false;
|
? false
|
||||||
|
: selection![comics[index]] ?? false;
|
||||||
var comic = ComicTile(
|
var comic = ComicTile(
|
||||||
comic: comics[index],
|
comic: comics[index],
|
||||||
badge: badge,
|
badge: badge,
|
||||||
menuOptions: menuBuilder?.call(comics[index]),
|
menuOptions: menuBuilder?.call(comics[index]),
|
||||||
onTap: onTap != null ? () => onTap!(comics[index]) : null,
|
onTap: onTap != null
|
||||||
|
? () => onTap!(comics[index], heroIDs[index])
|
||||||
|
: null,
|
||||||
onLongPressed: onLongPressed != null
|
onLongPressed: onLongPressed != null
|
||||||
? () => onLongPressed!(comics[index])
|
? () => onLongPressed!(comics[index], heroIDs[index])
|
||||||
: null,
|
: null,
|
||||||
heroID: heroIDs[index],
|
heroID: heroIDs[index],
|
||||||
);
|
);
|
||||||
@@ -889,19 +891,16 @@ class _SliverGridComics extends StatelessWidget {
|
|||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? Theme.of(context)
|
? Theme.of(
|
||||||
.colorScheme
|
context,
|
||||||
.secondaryContainer
|
).colorScheme.secondaryContainer.toOpacity(0.72)
|
||||||
.toOpacity(0.72)
|
|
||||||
: null,
|
: null,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
margin: const EdgeInsets.all(4),
|
margin: const EdgeInsets.all(4),
|
||||||
child: comic,
|
child: comic,
|
||||||
);
|
);
|
||||||
},
|
}, childCount: comics.length),
|
||||||
childCount: comics.length,
|
|
||||||
),
|
|
||||||
gridDelegate: SliverGridDelegateWithComics(),
|
gridDelegate: SliverGridDelegateWithComics(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1627,7 +1626,7 @@ class _SMClipper extends CustomClipper<Rect> {
|
|||||||
|
|
||||||
class SimpleComicTile extends StatelessWidget {
|
class SimpleComicTile extends StatelessWidget {
|
||||||
const SimpleComicTile(
|
const SimpleComicTile(
|
||||||
{super.key, required this.comic, this.onTap, this.withTitle = false});
|
{super.key, required this.comic, this.onTap, this.withTitle = false, this.heroID});
|
||||||
|
|
||||||
final Comic comic;
|
final Comic comic;
|
||||||
|
|
||||||
@@ -1635,6 +1634,8 @@ class SimpleComicTile extends StatelessWidget {
|
|||||||
|
|
||||||
final bool withTitle;
|
final bool withTitle;
|
||||||
|
|
||||||
|
final int? heroID;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var image = _findImageProvider(comic);
|
var image = _findImageProvider(comic);
|
||||||
@@ -1660,6 +1661,13 @@ class SimpleComicTile extends StatelessWidget {
|
|||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (heroID != null) {
|
||||||
|
child = Hero(
|
||||||
|
tag: "cover$heroID",
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
child = AnimatedTapRegion(
|
child = AnimatedTapRegion(
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
onTap: onTap ??
|
onTap: onTap ??
|
||||||
@@ -1668,6 +1676,9 @@ class SimpleComicTile extends StatelessWidget {
|
|||||||
() => ComicPage(
|
() => ComicPage(
|
||||||
id: comic.id,
|
id: comic.id,
|
||||||
sourceKey: comic.sourceKey,
|
sourceKey: comic.sourceKey,
|
||||||
|
cover: comic.cover,
|
||||||
|
title: comic.title,
|
||||||
|
heroID: heroID,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_qjs/flutter_qjs.dart';
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@@ -40,7 +42,6 @@ mixin class JsUiApi {
|
|||||||
var image = message['image'];
|
var image = message['image'];
|
||||||
if (title is! String) return;
|
if (title is! String) return;
|
||||||
if (validator != null && validator is! JSInvokable) return;
|
if (validator != null && validator is! JSInvokable) return;
|
||||||
if (image != null && image is! String) return;
|
|
||||||
return _showInputDialog(title, validator, image);
|
return _showInputDialog(title, validator, image);
|
||||||
case 'showSelectDialog':
|
case 'showSelectDialog':
|
||||||
var title = message['title'];
|
var title = message['title'];
|
||||||
@@ -126,13 +127,25 @@ mixin class JsUiApi {
|
|||||||
controller?.close();
|
controller?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> _showInputDialog(String title, JSInvokable? validator, String? image) async {
|
Future<String?> _showInputDialog(String title, JSInvokable? validator, dynamic image) async {
|
||||||
String? result;
|
String? result;
|
||||||
var func = validator == null ? null : JSAutoFreeFunction(validator);
|
var func = validator == null ? null : JSAutoFreeFunction(validator);
|
||||||
|
String? imageUrl;
|
||||||
|
Uint8List? imageData;
|
||||||
|
if (image != null) {
|
||||||
|
if (image is String) {
|
||||||
|
imageUrl = image;
|
||||||
|
} else if (image is Uint8List) {
|
||||||
|
imageData = image;
|
||||||
|
} else if (image is List<int>) {
|
||||||
|
imageData = Uint8List.fromList(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
await showInputDialog(
|
await showInputDialog(
|
||||||
context: App.rootContext,
|
context: App.rootContext,
|
||||||
title: title,
|
title: title,
|
||||||
image: image,
|
image: imageUrl,
|
||||||
|
imageData: imageData,
|
||||||
onConfirm: (v) {
|
onConfirm: (v) {
|
||||||
if (func != null) {
|
if (func != null) {
|
||||||
var res = func.call([v]);
|
var res = func.call([v]);
|
||||||
|
|||||||
@@ -360,6 +360,7 @@ Future<void> showInputDialog({
|
|||||||
String cancelText = "Cancel",
|
String cancelText = "Cancel",
|
||||||
RegExp? inputValidator,
|
RegExp? inputValidator,
|
||||||
String? image,
|
String? image,
|
||||||
|
Uint8List? imageData,
|
||||||
}) {
|
}) {
|
||||||
var controller = TextEditingController(text: initialValue);
|
var controller = TextEditingController(text: initialValue);
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
@@ -379,6 +380,11 @@ Future<void> showInputDialog({
|
|||||||
height: 108,
|
height: 108,
|
||||||
child: Image.network(image, fit: BoxFit.none),
|
child: Image.network(image, fit: BoxFit.none),
|
||||||
).paddingBottom(8),
|
).paddingBottom(8),
|
||||||
|
if (image == null && imageData != null)
|
||||||
|
SizedBox(
|
||||||
|
height: 108,
|
||||||
|
child: Image.memory(imageData, fit: BoxFit.none),
|
||||||
|
).paddingBottom(8),
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
|||||||
@@ -241,6 +241,10 @@ class _AppScrollBarState extends State<AppScrollBar> {
|
|||||||
|
|
||||||
late final VerticalDragGestureRecognizer _dragGestureRecognizer;
|
late final VerticalDragGestureRecognizer _dragGestureRecognizer;
|
||||||
|
|
||||||
|
bool _isVisible = false;
|
||||||
|
Timer? _hideTimer;
|
||||||
|
static const _hideDuration = Duration(seconds: 2);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -248,7 +252,41 @@ class _AppScrollBarState extends State<AppScrollBar> {
|
|||||||
_scrollController.addListener(onChanged);
|
_scrollController.addListener(onChanged);
|
||||||
Future.microtask(onChanged);
|
Future.microtask(onChanged);
|
||||||
_dragGestureRecognizer = VerticalDragGestureRecognizer()
|
_dragGestureRecognizer = VerticalDragGestureRecognizer()
|
||||||
..onUpdate = onUpdate;
|
..onUpdate = onUpdate
|
||||||
|
..onStart = (_) {
|
||||||
|
_showScrollbar();
|
||||||
|
}
|
||||||
|
..onEnd = (_) {
|
||||||
|
_scheduleHide();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_hideTimer?.cancel();
|
||||||
|
_scrollController.removeListener(onChanged);
|
||||||
|
_dragGestureRecognizer.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showScrollbar() {
|
||||||
|
if (!_isVisible && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isVisible = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_hideTimer?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleHide() {
|
||||||
|
_hideTimer?.cancel();
|
||||||
|
_hideTimer = Timer(_hideDuration, () {
|
||||||
|
if (mounted && _isVisible) {
|
||||||
|
setState(() {
|
||||||
|
_isVisible = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onUpdate(DragUpdateDetails details) {
|
void onUpdate(DragUpdateDetails details) {
|
||||||
@@ -269,14 +307,24 @@ class _AppScrollBarState extends State<AppScrollBar> {
|
|||||||
void onChanged() {
|
void onChanged() {
|
||||||
if (_scrollController.positions.isEmpty) return;
|
if (_scrollController.positions.isEmpty) return;
|
||||||
var position = _scrollController.position;
|
var position = _scrollController.position;
|
||||||
|
|
||||||
|
bool hasChanged = false;
|
||||||
if (position.minScrollExtent != minExtent ||
|
if (position.minScrollExtent != minExtent ||
|
||||||
position.maxScrollExtent != maxExtent ||
|
position.maxScrollExtent != maxExtent ||
|
||||||
position.pixels != this.position) {
|
position.pixels != this.position) {
|
||||||
setState(() {
|
hasChanged = true;
|
||||||
minExtent = position.minScrollExtent;
|
minExtent = position.minScrollExtent;
|
||||||
maxExtent = position.maxScrollExtent;
|
maxExtent = position.maxScrollExtent;
|
||||||
this.position = position.pixels;
|
this.position = position.pixels;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (hasChanged) {
|
||||||
|
_showScrollbar();
|
||||||
|
_scheduleHide();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanged && mounted) {
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,8 +348,13 @@ class _AppScrollBarState extends State<AppScrollBar> {
|
|||||||
Positioned(
|
Positioned(
|
||||||
top: top + widget.topPadding,
|
top: top + widget.topPadding,
|
||||||
right: 0,
|
right: 0,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: _isVisible ? 1.0 : 0.0,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
|
onEnter: (_) => _showScrollbar(),
|
||||||
|
onExit: (_) => _scheduleHide(),
|
||||||
child: Listener(
|
child: Listener(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onPointerDown: (event) {
|
onPointerDown: (event) {
|
||||||
@@ -328,6 +381,7 @@ class _AppScrollBarState extends State<AppScrollBar> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.5.3";
|
final version = "1.6.0";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:math';
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
|
|
||||||
const double _kBackGestureWidth = 20.0;
|
const double _kBackGestureWidth = 20.0;
|
||||||
@@ -19,6 +20,7 @@ class AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin{
|
|||||||
super.allowSnapshotting = true,
|
super.allowSnapshotting = true,
|
||||||
super.barrierDismissible = false,
|
super.barrierDismissible = false,
|
||||||
this.enableIOSGesture = true,
|
this.enableIOSGesture = true,
|
||||||
|
this.iosFullScreenPopGesture = true,
|
||||||
this.preventRebuild = true,
|
this.preventRebuild = true,
|
||||||
}) {
|
}) {
|
||||||
assert(opaque);
|
assert(opaque);
|
||||||
@@ -48,6 +50,9 @@ class AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin{
|
|||||||
@override
|
@override
|
||||||
final bool enableIOSGesture;
|
final bool enableIOSGesture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final bool iosFullScreenPopGesture;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final bool preventRebuild;
|
final bool preventRebuild;
|
||||||
}
|
}
|
||||||
@@ -74,6 +79,8 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
|
|||||||
|
|
||||||
bool get enableIOSGesture;
|
bool get enableIOSGesture;
|
||||||
|
|
||||||
|
bool get iosFullScreenPopGesture;
|
||||||
|
|
||||||
bool get preventRebuild;
|
bool get preventRebuild;
|
||||||
|
|
||||||
Widget? _child;
|
Widget? _child;
|
||||||
@@ -133,7 +140,9 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
|
|||||||
gestureWidth: _kBackGestureWidth,
|
gestureWidth: _kBackGestureWidth,
|
||||||
enabledCallback: () => _isPopGestureEnabled<T>(this),
|
enabledCallback: () => _isPopGestureEnabled<T>(this),
|
||||||
onStartPopGesture: () => _startPopGesture(this),
|
onStartPopGesture: () => _startPopGesture(this),
|
||||||
child: child)
|
fullScreen: iosFullScreenPopGesture,
|
||||||
|
child: child,
|
||||||
|
)
|
||||||
: child);
|
: child);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +215,7 @@ class IOSBackGestureDetector extends StatefulWidget {
|
|||||||
required this.child,
|
required this.child,
|
||||||
required this.gestureWidth,
|
required this.gestureWidth,
|
||||||
required this.onStartPopGesture,
|
required this.onStartPopGesture,
|
||||||
|
this.fullScreen = false,
|
||||||
super.key});
|
super.key});
|
||||||
|
|
||||||
final double gestureWidth;
|
final double gestureWidth;
|
||||||
@@ -216,6 +226,8 @@ class IOSBackGestureDetector extends StatefulWidget {
|
|||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
|
final bool fullScreen;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<IOSBackGestureDetector> createState() => _IOSBackGestureDetectorState();
|
State<IOSBackGestureDetector> createState() => _IOSBackGestureDetectorState();
|
||||||
}
|
}
|
||||||
@@ -247,26 +259,40 @@ class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
|
|||||||
? MediaQuery.of(context).padding.left
|
? MediaQuery.of(context).padding.left
|
||||||
: MediaQuery.of(context).padding.right;
|
: MediaQuery.of(context).padding.right;
|
||||||
dragAreaWidth = max(dragAreaWidth, widget.gestureWidth);
|
dragAreaWidth = max(dragAreaWidth, widget.gestureWidth);
|
||||||
return Stack(
|
final Widget gestureListener = widget.fullScreen
|
||||||
fit: StackFit.passthrough,
|
? Positioned.fill(
|
||||||
children: <Widget>[
|
|
||||||
widget.child,
|
|
||||||
Positioned(
|
|
||||||
width: dragAreaWidth,
|
|
||||||
top: 0.0,
|
|
||||||
bottom: 0.0,
|
|
||||||
left: 0,
|
|
||||||
child: Listener(
|
child: Listener(
|
||||||
onPointerDown: _handlePointerDown,
|
onPointerDown: _handlePointerDown,
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: Positioned(
|
||||||
|
width: dragAreaWidth,
|
||||||
|
top: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
left: Directionality.of(context) == TextDirection.ltr ? 0.0 : null,
|
||||||
|
right: Directionality.of(context) == TextDirection.rtl ? 0.0 : null,
|
||||||
|
child: Listener(
|
||||||
|
onPointerDown: _handlePointerDown,
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
fit: StackFit.passthrough,
|
||||||
|
children: <Widget>[
|
||||||
|
widget.child,
|
||||||
|
gestureListener,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handlePointerDown(PointerDownEvent event) {
|
void _handlePointerDown(PointerDownEvent event) {
|
||||||
if (widget.enabledCallback()) _recognizer.addPointer(event);
|
if (!widget.enabledCallback()) return;
|
||||||
|
if (widget.fullScreen && _isPointerOverHorizontalScrollable(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_recognizer.addPointer(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleDragCancel() {
|
void _handleDragCancel() {
|
||||||
@@ -304,6 +330,28 @@ class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
|
|||||||
_backGestureController!.dragUpdate(
|
_backGestureController!.dragUpdate(
|
||||||
_convertToLogical(details.primaryDelta! / context.size!.width));
|
_convertToLogical(details.primaryDelta! / context.size!.width));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isPointerOverHorizontalScrollable(PointerDownEvent event) {
|
||||||
|
final HitTestResult result = HitTestResult();
|
||||||
|
WidgetsBinding.instance.hitTest(result, event.position);
|
||||||
|
for (final entry in result.path) {
|
||||||
|
final target = entry.target;
|
||||||
|
if (target is RenderViewport) {
|
||||||
|
if (_isAxisHorizontal(target.axisDirection)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (target is RenderSliver) {
|
||||||
|
if (_isAxisHorizontal(target.constraints.axisDirection)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isAxisHorizontal(AxisDirection direction) {
|
||||||
|
return direction == AxisDirection.left || direction == AxisDirection.right;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SlidePageTransitionBuilder extends PageTransitionsBuilder {
|
class SlidePageTransitionBuilder extends PageTransitionsBuilder {
|
||||||
@@ -314,30 +362,31 @@ class SlidePageTransitionBuilder extends PageTransitionsBuilder {
|
|||||||
Animation<double> animation,
|
Animation<double> animation,
|
||||||
Animation<double> secondaryAnimation,
|
Animation<double> secondaryAnimation,
|
||||||
Widget child) {
|
Widget child) {
|
||||||
|
final Animation<double> primaryAnimation = App.isIOS
|
||||||
|
? animation
|
||||||
|
: CurvedAnimation(parent: animation, curve: Curves.ease);
|
||||||
|
final Animation<double> secondaryCurve = App.isIOS
|
||||||
|
? secondaryAnimation
|
||||||
|
: CurvedAnimation(parent: secondaryAnimation, curve: Curves.ease);
|
||||||
|
|
||||||
return SlideTransition(
|
return SlideTransition(
|
||||||
position: Tween<Offset>(
|
position: Tween<Offset>(
|
||||||
begin: const Offset(1, 0),
|
begin: const Offset(1, 0),
|
||||||
end: Offset.zero,
|
end: Offset.zero,
|
||||||
).animate(CurvedAnimation(
|
).animate(primaryAnimation),
|
||||||
parent: animation,
|
|
||||||
curve: Curves.ease,
|
|
||||||
)),
|
|
||||||
child: SlideTransition(
|
child: SlideTransition(
|
||||||
position: Tween<Offset>(
|
position: Tween<Offset>(
|
||||||
begin: Offset.zero,
|
begin: Offset.zero,
|
||||||
end: const Offset(-0.4, 0),
|
end: const Offset(-0.4, 0),
|
||||||
).animate(CurvedAnimation(
|
).animate(secondaryCurve),
|
||||||
parent: secondaryAnimation,
|
|
||||||
curve: Curves.ease,
|
|
||||||
)),
|
|
||||||
child: PhysicalModel(
|
child: PhysicalModel(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
borderRadius: BorderRadius.zero,
|
borderRadius: BorderRadius.zero,
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
elevation: 6,
|
elevation: 6,
|
||||||
child: Material(child: child,),
|
child: Material(child: child),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ class Settings with ChangeNotifier {
|
|||||||
'readerScrollSpeed': 1.0, // 0.5 - 3.0
|
'readerScrollSpeed': 1.0, // 0.5 - 3.0
|
||||||
'localFavoritesFirst': true,
|
'localFavoritesFirst': true,
|
||||||
'autoCloseFavoritePanel': false,
|
'autoCloseFavoritePanel': false,
|
||||||
|
'showChapterComments': true, // show chapter comments in reader
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
@@ -207,7 +208,11 @@ class Settings with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setEnabledComicSpecificSettings(String comicId, String sourceKey, bool enabled) {
|
void setEnabledComicSpecificSettings(
|
||||||
|
String comicId,
|
||||||
|
String sourceKey,
|
||||||
|
bool enabled,
|
||||||
|
) {
|
||||||
setReaderSetting(comicId, sourceKey, "enabled", enabled);
|
setReaderSetting(comicId, sourceKey, "enabled", enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +220,8 @@ class Settings with ChangeNotifier {
|
|||||||
if (comicId == null || sourceKey == null) {
|
if (comicId == null || sourceKey == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return _data['comicSpecificSettings']["$comicId@$sourceKey"]?["enabled"] == true;
|
return _data['comicSpecificSettings']["$comicId@$sourceKey"]?["enabled"] ==
|
||||||
|
true;
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamic getReaderSetting(String comicId, String sourceKey, String key) {
|
dynamic getReaderSetting(String comicId, String sourceKey, String key) {
|
||||||
|
|||||||
@@ -61,8 +61,10 @@ class ComicSourceManager with ChangeNotifier, Init {
|
|||||||
await for (var entity in Directory(path).list()) {
|
await for (var entity in Directory(path).list()) {
|
||||||
if (entity is File && entity.path.endsWith(".js")) {
|
if (entity is File && entity.path.endsWith(".js")) {
|
||||||
try {
|
try {
|
||||||
var source = await ComicSourceParser()
|
var source = await ComicSourceParser().parse(
|
||||||
.parse(await entity.readAsString(), entity.absolute.path);
|
await entity.readAsString(),
|
||||||
|
entity.absolute.path,
|
||||||
|
);
|
||||||
_sources.add(source);
|
_sources.add(source);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("ComicSource", "$e\n$s");
|
Log.error("ComicSource", "$e\n$s");
|
||||||
@@ -170,6 +172,10 @@ class ComicSource {
|
|||||||
|
|
||||||
final SendCommentFunc? sendCommentFunc;
|
final SendCommentFunc? sendCommentFunc;
|
||||||
|
|
||||||
|
final ChapterCommentsLoader? chapterCommentsLoader;
|
||||||
|
|
||||||
|
final SendChapterCommentFunc? sendChapterCommentFunc;
|
||||||
|
|
||||||
final RegExp? idMatcher;
|
final RegExp? idMatcher;
|
||||||
|
|
||||||
final LikeOrUnlikeComicFunc? likeOrUnlikeComic;
|
final LikeOrUnlikeComicFunc? likeOrUnlikeComic;
|
||||||
@@ -256,6 +262,8 @@ class ComicSource {
|
|||||||
this.version,
|
this.version,
|
||||||
this.commentsLoader,
|
this.commentsLoader,
|
||||||
this.sendCommentFunc,
|
this.sendCommentFunc,
|
||||||
|
this.chapterCommentsLoader,
|
||||||
|
this.sendChapterCommentFunc,
|
||||||
this.likeOrUnlikeComic,
|
this.likeOrUnlikeComic,
|
||||||
this.voteCommentFunc,
|
this.voteCommentFunc,
|
||||||
this.likeCommentFunc,
|
this.likeCommentFunc,
|
||||||
@@ -367,11 +375,19 @@ enum ExplorePageType {
|
|||||||
override,
|
override,
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef SearchFunction = Future<Res<List<Comic>>> Function(
|
typedef SearchFunction =
|
||||||
String keyword, int page, List<String> searchOption);
|
Future<Res<List<Comic>>> Function(
|
||||||
|
String keyword,
|
||||||
|
int page,
|
||||||
|
List<String> searchOption,
|
||||||
|
);
|
||||||
|
|
||||||
typedef SearchNextFunction = Future<Res<List<Comic>>> Function(
|
typedef SearchNextFunction =
|
||||||
String keyword, String? next, List<String> searchOption);
|
Future<Res<List<Comic>>> Function(
|
||||||
|
String keyword,
|
||||||
|
String? next,
|
||||||
|
List<String> searchOption,
|
||||||
|
);
|
||||||
|
|
||||||
class SearchPageData {
|
class SearchPageData {
|
||||||
/// If this is not null, the default value of search options will be first element.
|
/// If this is not null, the default value of search options will be first element.
|
||||||
@@ -398,11 +414,19 @@ class SearchOptions {
|
|||||||
String get defaultValue => defaultVal ?? options.keys.firstOrNull ?? "";
|
String get defaultValue => defaultVal ?? options.keys.firstOrNull ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
typedef CategoryComicsLoader =
|
||||||
String category, String? param, List<String> options, int page);
|
Future<Res<List<Comic>>> Function(
|
||||||
|
String category,
|
||||||
|
String? param,
|
||||||
|
List<String> options,
|
||||||
|
int page,
|
||||||
|
);
|
||||||
|
|
||||||
typedef CategoryOptionsLoader = Future<Res<List<CategoryComicsOptions>>> Function(
|
typedef CategoryOptionsLoader =
|
||||||
String category, String? param);
|
Future<Res<List<CategoryComicsOptions>>> Function(
|
||||||
|
String category,
|
||||||
|
String? param,
|
||||||
|
);
|
||||||
|
|
||||||
class CategoryComicsData {
|
class CategoryComicsData {
|
||||||
/// options
|
/// options
|
||||||
@@ -419,7 +443,12 @@ class CategoryComicsData {
|
|||||||
|
|
||||||
final RankingData? rankingData;
|
final RankingData? rankingData;
|
||||||
|
|
||||||
const CategoryComicsData({this.options, this.optionsLoader, required this.load, this.rankingData});
|
const CategoryComicsData({
|
||||||
|
this.options,
|
||||||
|
this.optionsLoader,
|
||||||
|
required this.load,
|
||||||
|
this.rankingData,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class RankingData {
|
class RankingData {
|
||||||
@@ -447,7 +476,12 @@ class CategoryComicsOptions {
|
|||||||
|
|
||||||
final List<String>? showWhen;
|
final List<String>? showWhen;
|
||||||
|
|
||||||
const CategoryComicsOptions(this.label, this.options, this.notShowWhen, this.showWhen);
|
const CategoryComicsOptions(
|
||||||
|
this.label,
|
||||||
|
this.options,
|
||||||
|
this.notShowWhen,
|
||||||
|
this.showWhen,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LinkHandler {
|
class LinkHandler {
|
||||||
|
|||||||
@@ -542,6 +542,7 @@ class PageJumpTarget {
|
|||||||
sourceKey: sourceKey,
|
sourceKey: sourceKey,
|
||||||
options: List.from(attributes?["options"] ?? []),
|
options: List.from(attributes?["options"] ?? []),
|
||||||
),
|
),
|
||||||
|
iosFullScreenGesture: false,
|
||||||
);
|
);
|
||||||
} else if (page == "category") {
|
} else if (page == "category") {
|
||||||
var key = ComicSource.find(sourceKey)!.categoryData!.key;
|
var key = ComicSource.find(sourceKey)!.categoryData!.key;
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ class ComicSourceParser {
|
|||||||
version ?? "1.0.0",
|
version ?? "1.0.0",
|
||||||
_parseCommentsLoader(),
|
_parseCommentsLoader(),
|
||||||
_parseSendCommentFunc(),
|
_parseSendCommentFunc(),
|
||||||
|
_parseChapterCommentsLoader(),
|
||||||
|
_parseSendChapterCommentFunc(),
|
||||||
_parseLikeFunc(),
|
_parseLikeFunc(),
|
||||||
_parseVoteCommentFunc(),
|
_parseVoteCommentFunc(),
|
||||||
_parseLikeCommentFunc(),
|
_parseLikeCommentFunc(),
|
||||||
@@ -560,12 +562,16 @@ class ComicSourceParser {
|
|||||||
res = await res;
|
res = await res;
|
||||||
}
|
}
|
||||||
if (res is! List) {
|
if (res is! List) {
|
||||||
return Res.error("Invalid data:\nExpected: List\nGot: ${res.runtimeType}");
|
return Res.error(
|
||||||
|
"Invalid data:\nExpected: List\nGot: ${res.runtimeType}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
var options = <CategoryComicsOptions>[];
|
var options = <CategoryComicsOptions>[];
|
||||||
for (var element in res) {
|
for (var element in res) {
|
||||||
if (element is! Map) {
|
if (element is! Map) {
|
||||||
return Res.error("Invalid option data:\nExpected: Map\nGot: ${element.runtimeType}");
|
return Res.error(
|
||||||
|
"Invalid option data:\nExpected: Map\nGot: ${element.runtimeType}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
|
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
|
||||||
for (var option in element["options"] ?? []) {
|
for (var option in element["options"] ?? []) {
|
||||||
@@ -582,13 +588,14 @@ class ComicSourceParser {
|
|||||||
element["label"] ?? "",
|
element["label"] ?? "",
|
||||||
map,
|
map,
|
||||||
List.from(element["notShowWhen"] ?? []),
|
List.from(element["notShowWhen"] ?? []),
|
||||||
element["showWhen"] == null ? null : List.from(element["showWhen"]),
|
element["showWhen"] == null
|
||||||
|
? null
|
||||||
|
: List.from(element["showWhen"]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Res(options);
|
return Res(options);
|
||||||
}
|
} catch (e) {
|
||||||
catch(e) {
|
|
||||||
Log.error("Data Analysis", "Failed to load category options.\n$e");
|
Log.error("Data Analysis", "Failed to load category options.\n$e");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
}
|
}
|
||||||
@@ -1005,6 +1012,54 @@ class ComicSourceParser {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChapterCommentsLoader? _parseChapterCommentsLoader() {
|
||||||
|
if (!_checkExists("comic.loadChapterComments")) return null;
|
||||||
|
return (comicId, epId, page, replyTo) async {
|
||||||
|
try {
|
||||||
|
var res = await JsEngine().runCode("""
|
||||||
|
ComicSource.sources.$_key.comic.loadChapterComments(
|
||||||
|
${jsonEncode(comicId)}, ${jsonEncode(epId)}, ${jsonEncode(page)}, ${jsonEncode(replyTo)})
|
||||||
|
""");
|
||||||
|
return Res(
|
||||||
|
(res["comments"] as List).map((e) => Comment.fromJson(e)).toList(),
|
||||||
|
subData: res["maxPage"],
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
SendChapterCommentFunc? _parseSendChapterCommentFunc() {
|
||||||
|
if (!_checkExists("comic.sendChapterComment")) return null;
|
||||||
|
return (comicId, epId, content, replyTo) async {
|
||||||
|
Future<Res<bool>> func() async {
|
||||||
|
try {
|
||||||
|
await JsEngine().runCode("""
|
||||||
|
ComicSource.sources.$_key.comic.sendChapterComment(
|
||||||
|
${jsonEncode(comicId)}, ${jsonEncode(epId)}, ${jsonEncode(content)}, ${jsonEncode(replyTo)})
|
||||||
|
""");
|
||||||
|
return const Res(true);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var res = await func();
|
||||||
|
if (res.error && res.errorMessage!.contains("Login expired")) {
|
||||||
|
var reLoginRes = await ComicSource.find(_key!)!.reLogin();
|
||||||
|
if (!reLoginRes) {
|
||||||
|
return const Res.error("Login expired and re-login failed");
|
||||||
|
} else {
|
||||||
|
return func();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
GetImageLoadingConfigFunc? _parseImageLoadingConfigFunc() {
|
GetImageLoadingConfigFunc? _parseImageLoadingConfigFunc() {
|
||||||
if (!_checkExists("comic.onImageLoad")) {
|
if (!_checkExists("comic.onImageLoad")) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -4,50 +4,90 @@ part of 'comic_source.dart';
|
|||||||
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
|
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
|
||||||
|
|
||||||
/// build comic list with next param, [Res.subData] should be next page param or null if there is no next page.
|
/// build comic list with next param, [Res.subData] should be next page param or null if there is no next page.
|
||||||
typedef ComicListBuilderWithNext = Future<Res<List<Comic>>> Function(
|
typedef ComicListBuilderWithNext =
|
||||||
String? next);
|
Future<Res<List<Comic>>> Function(String? next);
|
||||||
|
|
||||||
typedef LoginFunction = Future<Res<bool>> Function(String, String);
|
typedef LoginFunction = Future<Res<bool>> Function(String, String);
|
||||||
|
|
||||||
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
|
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
|
||||||
|
|
||||||
typedef LoadComicPagesFunc = Future<Res<List<String>>> Function(
|
typedef LoadComicPagesFunc =
|
||||||
String id, String? ep);
|
Future<Res<List<String>>> Function(String id, String? ep);
|
||||||
|
|
||||||
typedef CommentsLoader = Future<Res<List<Comment>>> Function(
|
typedef CommentsLoader =
|
||||||
String id, String? subId, int page, String? replyTo);
|
Future<Res<List<Comment>>> Function(
|
||||||
|
String id,
|
||||||
|
String? subId,
|
||||||
|
int page,
|
||||||
|
String? replyTo,
|
||||||
|
);
|
||||||
|
|
||||||
typedef SendCommentFunc = Future<Res<bool>> Function(
|
typedef ChapterCommentsLoader =
|
||||||
String id, String? subId, String content, String? replyTo);
|
Future<Res<List<Comment>>> Function(
|
||||||
|
String comicId,
|
||||||
|
String epId,
|
||||||
|
int page,
|
||||||
|
String? replyTo,
|
||||||
|
);
|
||||||
|
|
||||||
typedef GetImageLoadingConfigFunc = Future<Map<String, dynamic>> Function(
|
typedef SendCommentFunc =
|
||||||
String imageKey, String comicId, String epId)?;
|
Future<Res<bool>> Function(
|
||||||
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
|
String id,
|
||||||
String imageKey)?;
|
String? subId,
|
||||||
|
String content,
|
||||||
|
String? replyTo,
|
||||||
|
);
|
||||||
|
|
||||||
typedef ComicThumbnailLoader = Future<Res<List<String>>> Function(
|
typedef SendChapterCommentFunc =
|
||||||
String comicId, String? next);
|
Future<Res<bool>> Function(
|
||||||
|
String comicId,
|
||||||
|
String epId,
|
||||||
|
String content,
|
||||||
|
String? replyTo,
|
||||||
|
);
|
||||||
|
|
||||||
typedef LikeOrUnlikeComicFunc = Future<Res<bool>> Function(
|
typedef GetImageLoadingConfigFunc =
|
||||||
String comicId, bool isLiking);
|
Future<Map<String, dynamic>> Function(
|
||||||
|
String imageKey,
|
||||||
|
String comicId,
|
||||||
|
String epId,
|
||||||
|
)?;
|
||||||
|
typedef GetThumbnailLoadingConfigFunc =
|
||||||
|
Map<String, dynamic> Function(String imageKey)?;
|
||||||
|
|
||||||
|
typedef ComicThumbnailLoader =
|
||||||
|
Future<Res<List<String>>> Function(String comicId, String? next);
|
||||||
|
|
||||||
|
typedef LikeOrUnlikeComicFunc =
|
||||||
|
Future<Res<bool>> Function(String comicId, bool isLiking);
|
||||||
|
|
||||||
/// [isLiking] is true if the user is liking the comment, false if unliking.
|
/// [isLiking] is true if the user is liking the comment, false if unliking.
|
||||||
/// return the new likes count or null.
|
/// return the new likes count or null.
|
||||||
typedef LikeCommentFunc = Future<Res<int?>> Function(
|
typedef LikeCommentFunc =
|
||||||
String comicId, String? subId, String commentId, bool isLiking);
|
Future<Res<int?>> Function(
|
||||||
|
String comicId,
|
||||||
|
String? subId,
|
||||||
|
String commentId,
|
||||||
|
bool isLiking,
|
||||||
|
);
|
||||||
|
|
||||||
/// [isUp] is true if the user is upvoting the comment, false if downvoting.
|
/// [isUp] is true if the user is upvoting the comment, false if downvoting.
|
||||||
/// return the new vote count or null.
|
/// return the new vote count or null.
|
||||||
typedef VoteCommentFunc = Future<Res<int?>> Function(
|
typedef VoteCommentFunc =
|
||||||
String comicId, String? subId, String commentId, bool isUp, bool isCancel);
|
Future<Res<int?>> Function(
|
||||||
|
String comicId,
|
||||||
|
String? subId,
|
||||||
|
String commentId,
|
||||||
|
bool isUp,
|
||||||
|
bool isCancel,
|
||||||
|
);
|
||||||
|
|
||||||
typedef HandleClickTagEvent = PageJumpTarget? Function(
|
typedef HandleClickTagEvent =
|
||||||
String namespace, String tag);
|
PageJumpTarget? Function(String namespace, String tag);
|
||||||
|
|
||||||
/// Handle tag suggestion selection event. Should return the text to insert
|
/// Handle tag suggestion selection event. Should return the text to insert
|
||||||
/// into the search field.
|
/// into the search field.
|
||||||
typedef TagSuggestionSelectFunc = String Function(
|
typedef TagSuggestionSelectFunc = String Function(String namespace, String tag);
|
||||||
String namespace, String tag);
|
|
||||||
|
|
||||||
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
|
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
|
||||||
typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating);
|
typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating);
|
||||||
@@ -14,14 +14,20 @@ extension Navigation on BuildContext {
|
|||||||
return Navigator.of(this).canPop();
|
return Navigator.of(this).canPop();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T?> to<T>(Widget Function() builder) {
|
Future<T?> to<T>(Widget Function() builder,
|
||||||
return Navigator.of(this)
|
{bool enableIOSGesture = true, bool iosFullScreenGesture = true}) {
|
||||||
.push<T>(AppPageRoute(builder: (context) => builder()));
|
return Navigator.of(this).push<T>(AppPageRoute(
|
||||||
|
builder: (context) => builder(),
|
||||||
|
enableIOSGesture: enableIOSGesture,
|
||||||
|
iosFullScreenPopGesture: iosFullScreenGesture));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toReplacement<T>(Widget Function() builder) {
|
Future<void> toReplacement<T>(Widget Function() builder,
|
||||||
return Navigator.of(this)
|
{bool enableIOSGesture = true, bool iosFullScreenGesture = true}) {
|
||||||
.pushReplacement(AppPageRoute(builder: (context) => builder()));
|
return Navigator.of(this).pushReplacement(AppPageRoute(
|
||||||
|
builder: (context) => builder(),
|
||||||
|
enableIOSGesture: enableIOSGesture,
|
||||||
|
iosFullScreenPopGesture: iosFullScreenGesture));
|
||||||
}
|
}
|
||||||
|
|
||||||
double get width => MediaQuery.of(this).size.width;
|
double get width => MediaQuery.of(this).size.width;
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ class ImageFavoriteManager with ChangeNotifier {
|
|||||||
for (var comic in comics) {
|
for (var comic in comics) {
|
||||||
count += comic.images.length;
|
count += comic.images.length;
|
||||||
for (var tag in comic.tags) {
|
for (var tag in comic.tags) {
|
||||||
String finalTag = tag;
|
String finalTag = tag.split(":").last;
|
||||||
tagCount[finalTag] = (tagCount[finalTag] ?? 0) + 1;
|
tagCount[finalTag] = (tagCount[finalTag] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ class LocalComic with HistoryMixin implements Comic {
|
|||||||
author: subtitle,
|
author: subtitle,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
),
|
),
|
||||||
|
enableIOSGesture: false,
|
||||||
|
iosFullScreenGesture: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ abstract class ImageDownloader {
|
|||||||
responseType: ResponseType.stream,
|
responseType: ResponseType.stream,
|
||||||
));
|
));
|
||||||
|
|
||||||
var req = await dio.request<ResponseBody>(configs['url'] ?? url,
|
String requestUrl = configs['url'] ?? url;
|
||||||
|
if (requestUrl.startsWith('//')) {
|
||||||
|
requestUrl = 'https:$requestUrl';
|
||||||
|
}
|
||||||
|
var req = await dio.request<ResponseBody>(requestUrl,
|
||||||
data: configs['data']);
|
data: configs['data']);
|
||||||
var stream = req.data?.stream ?? (throw "Error: Empty response body.");
|
var stream = req.data?.stream ?? (throw "Error: Empty response body.");
|
||||||
int? expectedBytes = req.data!.contentLength;
|
int? expectedBytes = req.data!.contentLength;
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
|
|||||||
text: widget.keyword,
|
text: widget.keyword,
|
||||||
sourceKey: widget.source.key,
|
sourceKey: widget.source.key,
|
||||||
),
|
),
|
||||||
|
iosFullScreenGesture: false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ abstract mixin class _ComicPageActions {
|
|||||||
author: comic.findAuthor() ?? '',
|
author: comic.findAuthor() ?? '',
|
||||||
tags: comic.plainTags,
|
tags: comic.plainTags,
|
||||||
),
|
),
|
||||||
|
enableIOSGesture: false,
|
||||||
|
iosFullScreenGesture: false,
|
||||||
)
|
)
|
||||||
.then((_) {
|
.then((_) {
|
||||||
onReadEnd();
|
onReadEnd();
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
author: localComic.subTitle ?? '',
|
author: localComic.subTitle ?? '',
|
||||||
tags: localComic.tags,
|
tags: localComic.tags,
|
||||||
);
|
);
|
||||||
});
|
}, enableIOSGesture: false, iosFullScreenGesture: false);
|
||||||
App.mainNavigatorKey!.currentContext!.pop();
|
App.mainNavigatorKey!.currentContext!.pop();
|
||||||
});
|
});
|
||||||
isFirst = false;
|
isFirst = false;
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ class _BodyState extends State<_Body> {
|
|||||||
await ComicSourceManager().reload();
|
await ComicSourceManager().reload();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}),
|
}),
|
||||||
|
iosFullScreenGesture: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1245,6 +1246,15 @@ class _LoginPageState extends State<_LoginPage> {
|
|||||||
if (widget.config.checkLoginStatus != null &&
|
if (widget.config.checkLoginStatus != null &&
|
||||||
widget.config.checkLoginStatus!(url, title)) {
|
widget.config.checkLoginStatus!(url, title)) {
|
||||||
var cookies = (await c.getCookies(url)) ?? [];
|
var cookies = (await c.getCookies(url)) ?? [];
|
||||||
|
var localStorageItems = await c.webStorage.localStorage.getItems();
|
||||||
|
var mappedLocalStorage = <String, dynamic>{};
|
||||||
|
for (var item in localStorageItems) {
|
||||||
|
if (item.key != null) {
|
||||||
|
mappedLocalStorage[item.key!] = item.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
widget.source.data['_localStorage'] = mappedLocalStorage;
|
||||||
|
await widget.source.saveData();
|
||||||
SingleInstanceCookieJar.instance?.saveFromResponse(
|
SingleInstanceCookieJar.instance?.saveFromResponse(
|
||||||
Uri.parse(url),
|
Uri.parse(url),
|
||||||
cookies,
|
cookies,
|
||||||
@@ -1306,6 +1316,20 @@ class _LoginPageState extends State<_LoginPage> {
|
|||||||
Uri.parse(url),
|
Uri.parse(url),
|
||||||
cookies,
|
cookies,
|
||||||
);
|
);
|
||||||
|
var localStorageJson = await webview.evaluateJavascript(
|
||||||
|
"JSON.stringify(window.localStorage);",
|
||||||
|
);
|
||||||
|
var localStorage = <String, dynamic>{};
|
||||||
|
try {
|
||||||
|
var decoded = jsonDecode(localStorageJson ?? '');
|
||||||
|
if (decoded is Map<String, dynamic>) {
|
||||||
|
localStorage = decoded;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error("ComicSourcePage", "Failed to parse localStorage JSON\n$e");
|
||||||
|
}
|
||||||
|
widget.source.data['_localStorage'] = localStorage;
|
||||||
|
await widget.source.saveData();
|
||||||
success = true;
|
success = true;
|
||||||
widget.config.onLoginWithWebviewSuccess?.call();
|
widget.config.onLoginWithWebviewSuccess?.call();
|
||||||
webview.close();
|
webview.close();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:venera/foundation/comic_source/comic_source.dart';
|
|||||||
import 'package:venera/foundation/comic_type.dart';
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
import 'package:venera/foundation/consts.dart';
|
import 'package:venera/foundation/consts.dart';
|
||||||
import 'package:venera/foundation/favorites.dart';
|
import 'package:venera/foundation/favorites.dart';
|
||||||
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
late List<String> added = [];
|
late List<String> added = [];
|
||||||
|
|
||||||
String keyword = "";
|
String keyword = "";
|
||||||
|
bool searchHasUpper = false;
|
||||||
|
|
||||||
bool searchMode = false;
|
bool searchMode = false;
|
||||||
|
|
||||||
@@ -43,6 +44,8 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
|
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
|
|
||||||
|
late String readFilterSelect;
|
||||||
|
|
||||||
var searchResults = <FavoriteItem>[];
|
var searchResults = <FavoriteItem>[];
|
||||||
|
|
||||||
void updateSearchResult() {
|
void updateSearchResult() {
|
||||||
@@ -104,27 +107,40 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<FavoriteItem> filterComics(List<FavoriteItem> curComics) {
|
||||||
|
return curComics.where((comic) {
|
||||||
|
var history =
|
||||||
|
HistoryManager().find(comic.id, ComicType(comic.sourceKey.hashCode));
|
||||||
|
if (readFilterSelect == "UnCompleted") {
|
||||||
|
return history == null || history.page != history.maxPage;
|
||||||
|
} else if (readFilterSelect == "Completed") {
|
||||||
|
return history != null && history.page == history.maxPage;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
bool matchKeyword(String keyword, FavoriteItem comic) {
|
bool matchKeyword(String keyword, FavoriteItem comic) {
|
||||||
var list = keyword.split(" ");
|
var list = keyword.split(" ");
|
||||||
for (var k in list) {
|
for (var k in list) {
|
||||||
if (k.isEmpty) continue;
|
if (k.isEmpty) continue;
|
||||||
if (comic.title.contains(k)) {
|
if (checkKeyWordMatch(k, comic.title, false)) {
|
||||||
continue;
|
continue;
|
||||||
} else if (comic.subtitle != null && comic.subtitle!.contains(k)) {
|
} else if (comic.subtitle != null && checkKeyWordMatch(k, comic.subtitle!, false)) {
|
||||||
continue;
|
continue;
|
||||||
} else if (comic.tags.any((tag) {
|
} else if (comic.tags.any((tag) {
|
||||||
if (tag == k) {
|
if (checkKeyWordMatch(k, tag, true)) {
|
||||||
return true;
|
return true;
|
||||||
} else if (tag.contains(':') && tag.split(':')[1] == k) {
|
} else if (tag.contains(':') && checkKeyWordMatch(k, tag.split(':')[1], true)) {
|
||||||
return true;
|
return true;
|
||||||
} else if (App.locale.languageCode != 'en' &&
|
} else if (App.locale.languageCode != 'en' &&
|
||||||
tag.translateTagsToCN == k) {
|
checkKeyWordMatch(k, tag.translateTagsToCN, true)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
})) {
|
})) {
|
||||||
continue;
|
continue;
|
||||||
} else if (comic.author == k) {
|
} else if (checkKeyWordMatch(k, comic.author, true)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -132,6 +148,17 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool checkKeyWordMatch(String keyword, String compare, bool needEqual) {
|
||||||
|
String temp = compare;
|
||||||
|
// 没有大写的话, 就转成小写比较, 避免搜索需要注意大小写
|
||||||
|
if (!searchHasUpper) {
|
||||||
|
temp = temp.toLowerCase();
|
||||||
|
}
|
||||||
|
if (needEqual) {
|
||||||
|
return keyword == temp;
|
||||||
|
}
|
||||||
|
return temp.contains(keyword);
|
||||||
|
}
|
||||||
// Convert keyword to traditional Chinese to match comics
|
// Convert keyword to traditional Chinese to match comics
|
||||||
bool matchKeywordT(String keyword, FavoriteItem comic) {
|
bool matchKeywordT(String keyword, FavoriteItem comic) {
|
||||||
if (!OpenCC.hasChineseSimplified(keyword)) {
|
if (!OpenCC.hasChineseSimplified(keyword)) {
|
||||||
@@ -149,9 +176,10 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
keyword = OpenCC.traditionalToSimplified(keyword);
|
keyword = OpenCC.traditionalToSimplified(keyword);
|
||||||
return matchKeyword(keyword, comic);
|
return matchKeyword(keyword, comic);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
readFilterSelect = appdata.implicitData["local_favorites_read_filter"] ??
|
||||||
|
readFilterList[0];
|
||||||
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
|
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
|
||||||
if (!isAllFolder) {
|
if (!isAllFolder) {
|
||||||
var (a, b) = LocalFavoritesManager().findLinked(widget.folder);
|
var (a, b) = LocalFavoritesManager().findLinked(widget.folder);
|
||||||
@@ -320,6 +348,31 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: "Filter".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.sort_rounded),
|
||||||
|
color: readFilterSelect != readFilterList[0]
|
||||||
|
? context.colorScheme.primaryContainer
|
||||||
|
: null,
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return _LocalFavoritesFilterDialog(
|
||||||
|
initReadFilterSelect: readFilterSelect,
|
||||||
|
updateConfig: (readFilter) {
|
||||||
|
setState(() {
|
||||||
|
readFilterSelect = readFilter;
|
||||||
|
});
|
||||||
|
updateComics();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "Search".tl,
|
message: "Search".tl,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
@@ -521,7 +574,25 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
App.rootContext.to(() => ReaderWithLoading(
|
App.rootContext.to(() => ReaderWithLoading(
|
||||||
id: c.id,
|
id: c.id,
|
||||||
sourceKey: c.sourceKey,
|
sourceKey: c.sourceKey,
|
||||||
));
|
),
|
||||||
|
enableIOSGesture: false,
|
||||||
|
iosFullScreenGesture: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (selectedComics.length == 1)
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.arrow_forward_ios,
|
||||||
|
text: "Jump to Detail".tl,
|
||||||
|
onClick: () {
|
||||||
|
final c = selectedComics.keys.first as FavoriteItem;
|
||||||
|
App.mainNavigatorKey?.currentContext?.to(() => ComicPage(
|
||||||
|
id: c.id,
|
||||||
|
sourceKey: c.sourceKey,
|
||||||
|
),
|
||||||
|
enableIOSGesture: false,
|
||||||
|
iosFullScreenGesture: false,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
@@ -553,6 +624,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
),
|
),
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
keyword = v;
|
keyword = v;
|
||||||
|
searchHasUpper = keyword.contains(RegExp(r'[A-Z]'));
|
||||||
updateSearchResult();
|
updateSearchResult();
|
||||||
},
|
},
|
||||||
).paddingBottom(8).paddingRight(8),
|
).paddingBottom(8).paddingRight(8),
|
||||||
@@ -568,7 +640,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
SliverGridComics(
|
SliverGridComics(
|
||||||
comics: searchMode ? searchResults : comics,
|
comics: searchMode ? searchResults : filterComics(comics),
|
||||||
selections: selectedComics,
|
selections: selectedComics,
|
||||||
menuBuilder: (c) {
|
menuBuilder: (c) {
|
||||||
return [
|
return [
|
||||||
@@ -622,12 +694,14 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
id: c.id,
|
id: c.id,
|
||||||
sourceKey: c.sourceKey,
|
sourceKey: c.sourceKey,
|
||||||
),
|
),
|
||||||
|
enableIOSGesture: false,
|
||||||
|
iosFullScreenGesture: false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
onTap: (c) {
|
onTap: (c, heroID) {
|
||||||
if (multiSelectMode) {
|
if (multiSelectMode) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selectedComics.containsKey(c as FavoriteItem)) {
|
if (selectedComics.containsKey(c as FavoriteItem)) {
|
||||||
@@ -639,18 +713,24 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
lastSelectedIndex = comics.indexOf(c);
|
lastSelectedIndex = comics.indexOf(c);
|
||||||
});
|
});
|
||||||
} else if (appdata.settings["onClickFavorite"] == "viewDetail") {
|
} else if (appdata.settings["onClickFavorite"] == "viewDetail") {
|
||||||
App.mainNavigatorKey?.currentContext
|
|
||||||
?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
|
|
||||||
} else {
|
|
||||||
App.mainNavigatorKey?.currentContext?.to(
|
App.mainNavigatorKey?.currentContext?.to(
|
||||||
() => ReaderWithLoading(
|
() => ComicPage(
|
||||||
id: c.id,
|
id: c.id,
|
||||||
sourceKey: c.sourceKey,
|
sourceKey: c.sourceKey,
|
||||||
|
cover: c.cover,
|
||||||
|
title: c.title,
|
||||||
|
heroID: heroID,
|
||||||
),
|
),
|
||||||
|
enableIOSGesture: false,
|
||||||
|
iosFullScreenGesture: false,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
App.mainNavigatorKey?.currentContext?.to(
|
||||||
|
() => ReaderWithLoading(id: c.id, sourceKey: c.sourceKey),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPressed: (c) {
|
onLongPressed: (c, heroID) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (!multiSelectMode) {
|
if (!multiSelectMode) {
|
||||||
multiSelectMode = true;
|
multiSelectMode = true;
|
||||||
@@ -1075,3 +1155,78 @@ class _SelectUpdatePageNumState extends State<_SelectUpdatePageNum> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _LocalFavoritesFilterDialog extends StatefulWidget {
|
||||||
|
const _LocalFavoritesFilterDialog({
|
||||||
|
required this.initReadFilterSelect,
|
||||||
|
required this.updateConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String initReadFilterSelect;
|
||||||
|
final Function updateConfig;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_LocalFavoritesFilterDialog> createState() =>
|
||||||
|
_LocalFavoritesFilterDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const readFilterList = ['All', 'UnCompleted', 'Completed'];
|
||||||
|
|
||||||
|
class _LocalFavoritesFilterDialogState
|
||||||
|
extends State<_LocalFavoritesFilterDialog> {
|
||||||
|
List<String> optionTypes = ['Filter'];
|
||||||
|
late var readFilter = widget.initReadFilterSelect;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget tabBar = Material(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: AppTabBar(
|
||||||
|
key: PageStorageKey(optionTypes),
|
||||||
|
tabs: optionTypes.map((e) => Tab(text: e.tl, key: Key(e))).toList(),
|
||||||
|
),
|
||||||
|
).paddingTop(context.padding.top);
|
||||||
|
return ContentDialog(
|
||||||
|
content: DefaultTabController(
|
||||||
|
length: 2,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
tabBar,
|
||||||
|
TabViewBody(children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text("Filter reading status".tl),
|
||||||
|
trailing: Select(
|
||||||
|
current: readFilter.tl,
|
||||||
|
values: readFilterList.map((e) => e.tl).toList(),
|
||||||
|
minWidth: 64,
|
||||||
|
onTap: (index) {
|
||||||
|
setState(() {
|
||||||
|
readFilter = readFilterList[index];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
appdata.implicitData["local_favorites_read_filter"] = readFilter;
|
||||||
|
appdata.writeImplicitData();
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
widget.updateConfig(readFilter);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text("Confirm".tl),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ class _FollowUpdatesPageState extends AutomaticGlobalState<FollowUpdatesPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
updateFollowUpdatesUI();
|
updateFollowUpdatesUI();
|
||||||
|
appdata.saveData();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ class _HistoryPageState extends State<HistoryPage> {
|
|||||||
selections: selectedComics,
|
selections: selectedComics,
|
||||||
onLongPressed: null,
|
onLongPressed: null,
|
||||||
onTap: multiSelectMode
|
onTap: multiSelectMode
|
||||||
? (c) {
|
? (c, heroID) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selectedComics.containsKey(c as History)) {
|
if (selectedComics.containsKey(c as History)) {
|
||||||
selectedComics.remove(c);
|
selectedComics.remove(c);
|
||||||
|
|||||||
@@ -302,13 +302,18 @@ class _HistoryState extends State<_History> {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: history.length,
|
itemCount: history.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
final heroID = history[index].id.hashCode;
|
||||||
return SimpleComicTile(
|
return SimpleComicTile(
|
||||||
comic: history[index],
|
comic: history[index],
|
||||||
|
heroID: heroID,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.to(
|
context.to(
|
||||||
() => ComicPage(
|
() => ComicPage(
|
||||||
id: history[index].id,
|
id: history[index].id,
|
||||||
sourceKey: history[index].type.sourceKey,
|
sourceKey: history[index].type.sourceKey,
|
||||||
|
cover: history[index].cover,
|
||||||
|
title: history[index].title,
|
||||||
|
heroID: heroID,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -386,7 +391,9 @@ class _LocalState extends State<_Local> {
|
|||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8, vertical: 2),
|
horizontal: 8,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -405,9 +412,22 @@ class _LocalState extends State<_Local> {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: local.length,
|
itemCount: local.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return SimpleComicTile(comic: local[index])
|
final heroID = local[index].id.hashCode;
|
||||||
.paddingHorizontal(8)
|
return SimpleComicTile(
|
||||||
.paddingVertical(2);
|
comic: local[index],
|
||||||
|
heroID: heroID,
|
||||||
|
onTap: () {
|
||||||
|
context.to(
|
||||||
|
() => ComicPage(
|
||||||
|
id: local[index].id,
|
||||||
|
sourceKey: local[index].sourceKey,
|
||||||
|
cover: local[index].cover,
|
||||||
|
title: local[index].title,
|
||||||
|
heroID: heroID,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).paddingHorizontal(8).paddingVertical(2);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).paddingHorizontal(8),
|
).paddingHorizontal(8),
|
||||||
@@ -874,7 +894,10 @@ class _ImageFavoritesState extends State<ImageFavorites> {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.to(() => const ImageFavoritesPage());
|
context.to(
|
||||||
|
() => const ImageFavoritesPage(),
|
||||||
|
iosFullScreenGesture: false,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -993,7 +1016,10 @@ class _ImageFavoritesState extends State<ImageFavorites> {
|
|||||||
maxCount: maxCount,
|
maxCount: maxCount,
|
||||||
enableTranslation: displayType != 2,
|
enableTranslation: displayType != 2,
|
||||||
onTap: (text) {
|
onTap: (text) {
|
||||||
context.to(() => ImageFavoritesPage(initialKeyword: text));
|
context.to(
|
||||||
|
() => ImageFavoritesPage(initialKeyword: text),
|
||||||
|
iosFullScreenGesture: false,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> {
|
|||||||
initialEp: ep,
|
initialEp: ep,
|
||||||
initialPage: page,
|
initialPage: page,
|
||||||
),
|
),
|
||||||
|
enableIOSGesture: false,
|
||||||
|
iosFullScreenGesture: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,6 +244,8 @@ class _ImageFavoritesPhotoViewState extends State<ImageFavoritesPhotoView> {
|
|||||||
initialEp: ep,
|
initialEp: ep,
|
||||||
initialPage: page,
|
initialPage: page,
|
||||||
),
|
),
|
||||||
|
enableIOSGesture: false,
|
||||||
|
iosFullScreenGesture: false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -285,13 +285,13 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
SliverGridComics(
|
SliverGridComics(
|
||||||
comics: comics,
|
comics: comics,
|
||||||
selections: selectedComics,
|
selections: selectedComics,
|
||||||
onLongPressed: (c) {
|
onLongPressed: (c, heroID) {
|
||||||
setState(() {
|
setState(() {
|
||||||
multiSelectMode = true;
|
multiSelectMode = true;
|
||||||
selectedComics[c as LocalComic] = true;
|
selectedComics[c as LocalComic] = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onTap: (c) {
|
onTap: (c, heroID) {
|
||||||
if (multiSelectMode) {
|
if (multiSelectMode) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selectedComics.containsKey(c as LocalComic)) {
|
if (selectedComics.containsKey(c as LocalComic)) {
|
||||||
|
|||||||
573
lib/pages/reader/chapter_comments.dart
Normal file
573
lib/pages/reader/chapter_comments.dart
Normal file
@@ -0,0 +1,573 @@
|
|||||||
|
part of 'reader.dart';
|
||||||
|
|
||||||
|
class ChapterCommentsPage extends StatefulWidget {
|
||||||
|
const ChapterCommentsPage({
|
||||||
|
super.key,
|
||||||
|
required this.comicId,
|
||||||
|
required this.epId,
|
||||||
|
required this.source,
|
||||||
|
required this.comicTitle,
|
||||||
|
required this.chapterTitle,
|
||||||
|
this.replyComment,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String comicId;
|
||||||
|
final String epId;
|
||||||
|
final ComicSource source;
|
||||||
|
final String comicTitle;
|
||||||
|
final String chapterTitle;
|
||||||
|
final Comment? replyComment;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChapterCommentsPage> createState() => _ChapterCommentsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChapterCommentsPageState extends State<ChapterCommentsPage> {
|
||||||
|
bool _loading = true;
|
||||||
|
List<Comment>? _comments;
|
||||||
|
String? _error;
|
||||||
|
int _page = 1;
|
||||||
|
int? maxPage;
|
||||||
|
var controller = TextEditingController();
|
||||||
|
bool sending = false;
|
||||||
|
|
||||||
|
void firstLoad() async {
|
||||||
|
var res = await widget.source.chapterCommentsLoader!(
|
||||||
|
widget.comicId,
|
||||||
|
widget.epId,
|
||||||
|
1,
|
||||||
|
widget.replyComment?.id,
|
||||||
|
);
|
||||||
|
if (res.error) {
|
||||||
|
setState(() {
|
||||||
|
_error = res.errorMessage;
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
} else if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_comments = res.data;
|
||||||
|
_loading = false;
|
||||||
|
maxPage = res.subData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadMore() async {
|
||||||
|
var res = await widget.source.chapterCommentsLoader!(
|
||||||
|
widget.comicId,
|
||||||
|
widget.epId,
|
||||||
|
_page + 1,
|
||||||
|
widget.replyComment?.id,
|
||||||
|
);
|
||||||
|
if (res.error) {
|
||||||
|
context.showMessage(message: res.errorMessage ?? "Unknown Error");
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_comments!.addAll(res.data);
|
||||||
|
_page++;
|
||||||
|
if (maxPage == null && res.data.isEmpty) {
|
||||||
|
maxPage = _page;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
appBar: Appbar(
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text("Chapter Comments".tl, style: ts.s18),
|
||||||
|
Text(widget.chapterTitle, style: ts.s12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: AppbarStyle.shadow,
|
||||||
|
),
|
||||||
|
body: buildBody(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBody(BuildContext context) {
|
||||||
|
if (_loading) {
|
||||||
|
firstLoad();
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
} else if (_error != null) {
|
||||||
|
return NetworkError(
|
||||||
|
message: _error!,
|
||||||
|
retry: () {
|
||||||
|
setState(() {
|
||||||
|
_loading = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
withAppbar: false,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
var showAvatar = _comments!.any((e) {
|
||||||
|
return e.avatar != null;
|
||||||
|
});
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SmoothScrollProvider(
|
||||||
|
builder: (context, controller, physics) {
|
||||||
|
return ListView.builder(
|
||||||
|
controller: controller,
|
||||||
|
physics: physics,
|
||||||
|
primary: false,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: _comments!.length + 2,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
if (widget.replyComment != null) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_ChapterCommentTile(
|
||||||
|
comment: widget.replyComment!,
|
||||||
|
source: widget.source,
|
||||||
|
comicId: widget.comicId,
|
||||||
|
epId: widget.epId,
|
||||||
|
showAvatar: showAvatar,
|
||||||
|
showActions: false,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text("Replies".tl, style: ts.s18),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index--;
|
||||||
|
|
||||||
|
if (index == _comments!.length) {
|
||||||
|
if (_page < (maxPage ?? _page + 1)) {
|
||||||
|
loadMore();
|
||||||
|
return const ListLoadingIndicator();
|
||||||
|
} else {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _ChapterCommentTile(
|
||||||
|
comment: _comments![index],
|
||||||
|
source: widget.source,
|
||||||
|
comicId: widget.comicId,
|
||||||
|
epId: widget.epId,
|
||||||
|
showAvatar: showAvatar,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
buildBottom(context),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBottom(BuildContext context) {
|
||||||
|
if (widget.source.sendChapterCommentFunc == null) {
|
||||||
|
return const SizedBox(height: 0);
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
isCollapsed: true,
|
||||||
|
hintText: "Comment".tl,
|
||||||
|
),
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (sending)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (controller.text.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
sending = true;
|
||||||
|
});
|
||||||
|
var b = await widget.source.sendChapterCommentFunc!(
|
||||||
|
widget.comicId,
|
||||||
|
widget.epId,
|
||||||
|
controller.text,
|
||||||
|
widget.replyComment?.id,
|
||||||
|
);
|
||||||
|
if (!b.error) {
|
||||||
|
controller.text = "";
|
||||||
|
setState(() {
|
||||||
|
sending = false;
|
||||||
|
_loading = true;
|
||||||
|
_comments?.clear();
|
||||||
|
_page = 1;
|
||||||
|
maxPage = null;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: b.errorMessage ?? "Error");
|
||||||
|
setState(() {
|
||||||
|
sending = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.send,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingLeft(16).paddingRight(4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChapterCommentTile extends StatefulWidget {
|
||||||
|
const _ChapterCommentTile({
|
||||||
|
required this.comment,
|
||||||
|
required this.source,
|
||||||
|
required this.comicId,
|
||||||
|
required this.epId,
|
||||||
|
required this.showAvatar,
|
||||||
|
this.showActions = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Comment comment;
|
||||||
|
final ComicSource source;
|
||||||
|
final String comicId;
|
||||||
|
final String epId;
|
||||||
|
final bool showAvatar;
|
||||||
|
final bool showActions;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ChapterCommentTile> createState() => _ChapterCommentTileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChapterCommentTileState extends State<_ChapterCommentTile> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
likes = widget.comment.score ?? 0;
|
||||||
|
isLiked = widget.comment.isLiked ?? false;
|
||||||
|
voteStatus = widget.comment.voteStatus;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (widget.showAvatar)
|
||||||
|
Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
),
|
||||||
|
child: widget.comment.avatar == null
|
||||||
|
? null
|
||||||
|
: AnimatedImage(
|
||||||
|
image: CachedImageProvider(
|
||||||
|
widget.comment.avatar!,
|
||||||
|
sourceKey: widget.source.key,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).paddingRight(8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.comment.userName, style: ts.bold),
|
||||||
|
if (widget.comment.time != null)
|
||||||
|
Text(widget.comment.time!, style: ts.s12),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
_CommentContent(text: widget.comment.content),
|
||||||
|
buildActions(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildActions() {
|
||||||
|
if (!widget.showActions) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
if (widget.comment.score == null && widget.comment.replyCount == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
return SizedBox(
|
||||||
|
height: 36,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (widget.comment.score != null &&
|
||||||
|
widget.source.voteCommentFunc != null)
|
||||||
|
buildVote(),
|
||||||
|
if (widget.comment.score != null &&
|
||||||
|
widget.source.likeCommentFunc != null)
|
||||||
|
buildLike(),
|
||||||
|
// Only show reply button if comment has both id and replyCount
|
||||||
|
if (widget.comment.replyCount != null && widget.comment.id != null)
|
||||||
|
buildReply(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingTop(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildReply() {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(left: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
onTap: () {
|
||||||
|
// Get the parent page's widget to access comicTitle and chapterTitle
|
||||||
|
var parentState = context.findAncestorStateOfType<_ChapterCommentsPageState>();
|
||||||
|
showSideBar(
|
||||||
|
context,
|
||||||
|
ChapterCommentsPage(
|
||||||
|
comicId: widget.comicId,
|
||||||
|
epId: widget.epId,
|
||||||
|
source: widget.source,
|
||||||
|
comicTitle: parentState?.widget.comicTitle ?? '',
|
||||||
|
chapterTitle: parentState?.widget.chapterTitle ?? '',
|
||||||
|
replyComment: widget.comment,
|
||||||
|
),
|
||||||
|
showBarrier: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.insert_comment_outlined, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(widget.comment.replyCount.toString()),
|
||||||
|
],
|
||||||
|
).padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 4)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isLiking = false;
|
||||||
|
bool isLiked = false;
|
||||||
|
var likes = 0;
|
||||||
|
|
||||||
|
Widget buildLike() {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(left: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
onTap: () async {
|
||||||
|
if (isLiking) return;
|
||||||
|
setState(() {
|
||||||
|
isLiking = true;
|
||||||
|
});
|
||||||
|
var res = await widget.source.likeCommentFunc!(
|
||||||
|
widget.comicId,
|
||||||
|
widget.epId,
|
||||||
|
widget.comment.id!,
|
||||||
|
!isLiked,
|
||||||
|
);
|
||||||
|
if (res.success) {
|
||||||
|
isLiked = !isLiked;
|
||||||
|
likes += isLiked ? 1 : -1;
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: res.errorMessage ?? "Error");
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isLiking = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (isLiking)
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
else if (isLiked)
|
||||||
|
Icon(
|
||||||
|
Icons.favorite,
|
||||||
|
size: 16,
|
||||||
|
color: context.useTextColor(Colors.red),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const Icon(Icons.favorite_border, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(likes.toString()),
|
||||||
|
],
|
||||||
|
).padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 4)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int? voteStatus;
|
||||||
|
bool isVotingUp = false;
|
||||||
|
bool isVotingDown = false;
|
||||||
|
|
||||||
|
void vote(bool isUp) async {
|
||||||
|
if (isVotingUp || isVotingDown) return;
|
||||||
|
setState(() {
|
||||||
|
if (isUp) {
|
||||||
|
isVotingUp = true;
|
||||||
|
} else {
|
||||||
|
isVotingDown = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var isCancel = (isUp && voteStatus == 1) || (!isUp && voteStatus == -1);
|
||||||
|
var res = await widget.source.voteCommentFunc!(
|
||||||
|
widget.comicId,
|
||||||
|
widget.epId,
|
||||||
|
widget.comment.id!,
|
||||||
|
isUp,
|
||||||
|
isCancel,
|
||||||
|
);
|
||||||
|
if (res.success) {
|
||||||
|
if (isCancel) {
|
||||||
|
voteStatus = 0;
|
||||||
|
} else {
|
||||||
|
if (isUp) {
|
||||||
|
voteStatus = 1;
|
||||||
|
} else {
|
||||||
|
voteStatus = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
widget.comment.voteStatus = voteStatus;
|
||||||
|
widget.comment.score = res.data ?? widget.comment.score;
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: res.errorMessage ?? "Error");
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isVotingUp = false;
|
||||||
|
isVotingDown = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildVote() {
|
||||||
|
var upColor = context.colorScheme.outline;
|
||||||
|
if (voteStatus == 1) {
|
||||||
|
upColor = context.useTextColor(Colors.red);
|
||||||
|
}
|
||||||
|
var downColor = context.colorScheme.outline;
|
||||||
|
if (voteStatus == -1) {
|
||||||
|
downColor = context.useTextColor(Colors.blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(left: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Button.icon(
|
||||||
|
isLoading: isVotingUp,
|
||||||
|
icon: const Icon(Icons.arrow_upward),
|
||||||
|
size: 18,
|
||||||
|
color: upColor,
|
||||||
|
onPressed: () => vote(true),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(widget.comment.score.toString()),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Button.icon(
|
||||||
|
isLoading: isVotingDown,
|
||||||
|
icon: const Icon(Icons.arrow_downward),
|
||||||
|
size: 18,
|
||||||
|
color: downColor,
|
||||||
|
onPressed: () => vote(false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CommentContent extends StatelessWidget {
|
||||||
|
const _CommentContent({required this.text});
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!text.contains('<') && !text.contains('http')) {
|
||||||
|
return SelectableText(text);
|
||||||
|
} else {
|
||||||
|
// Use the RichCommentContent from comments_page.dart
|
||||||
|
// For simplicity, we'll just show plain text here
|
||||||
|
// In a real implementation, you'd need to import or duplicate the RichCommentContent class
|
||||||
|
return SelectableText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import 'package:venera/foundation/consts.dart';
|
|||||||
import 'package:venera/foundation/favorites.dart';
|
import 'package:venera/foundation/favorites.dart';
|
||||||
import 'package:venera/foundation/global_state.dart';
|
import 'package:venera/foundation/global_state.dart';
|
||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
|
import 'package:venera/foundation/image_provider/cached_image.dart';
|
||||||
import 'package:venera/foundation/image_provider/reader_image.dart';
|
import 'package:venera/foundation/image_provider/reader_image.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
@@ -54,6 +55,8 @@ part 'loading.dart';
|
|||||||
|
|
||||||
part 'chapters.dart';
|
part 'chapters.dart';
|
||||||
|
|
||||||
|
part 'chapter_comments.dart';
|
||||||
|
|
||||||
extension _ReaderContext on BuildContext {
|
extension _ReaderContext on BuildContext {
|
||||||
_ReaderState get reader => findAncestorStateOfType<_ReaderState>()!;
|
_ReaderState get reader => findAncestorStateOfType<_ReaderState>()!;
|
||||||
|
|
||||||
@@ -163,14 +166,27 @@ class _ReaderState extends State<Reader>
|
|||||||
}
|
}
|
||||||
if (widget.initialPage != null) {
|
if (widget.initialPage != null) {
|
||||||
page = widget.initialPage!;
|
page = widget.initialPage!;
|
||||||
|
if (page < 1) {
|
||||||
|
page = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// mode = ReaderMode.fromKey(appdata.settings['readerMode']);
|
// mode = ReaderMode.fromKey(appdata.settings['readerMode']);
|
||||||
mode = ReaderMode.fromKey(appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerMode'));
|
mode = ReaderMode.fromKey(
|
||||||
|
appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerMode'),
|
||||||
|
);
|
||||||
history = widget.history;
|
history = widget.history;
|
||||||
if (!appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSystemStatusBar')) {
|
if (!appdata.settings.getReaderSetting(
|
||||||
|
cid,
|
||||||
|
type.sourceKey,
|
||||||
|
'showSystemStatusBar',
|
||||||
|
)) {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
}
|
}
|
||||||
if (appdata.settings.getReaderSetting(cid, type.sourceKey, 'enableTurnPageByVolumeKey')) {
|
if (appdata.settings.getReaderSetting(
|
||||||
|
cid,
|
||||||
|
type.sourceKey,
|
||||||
|
'enableTurnPageByVolumeKey',
|
||||||
|
)) {
|
||||||
handleVolumeEvent();
|
handleVolumeEvent();
|
||||||
}
|
}
|
||||||
setImageCacheSize();
|
setImageCacheSize();
|
||||||
@@ -208,8 +224,10 @@ class _ReaderState extends State<Reader>
|
|||||||
} else {
|
} else {
|
||||||
maxImageCacheSize = 500 << 20;
|
maxImageCacheSize = 500 << 20;
|
||||||
}
|
}
|
||||||
Log.info("Reader",
|
Log.info(
|
||||||
"Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize");
|
"Reader",
|
||||||
|
"Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize",
|
||||||
|
);
|
||||||
PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize;
|
PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,13 +257,15 @@ class _ReaderState extends State<Reader>
|
|||||||
onKeyEvent: onKeyEvent,
|
onKeyEvent: onKeyEvent,
|
||||||
child: Overlay(
|
child: Overlay(
|
||||||
initialEntries: [
|
initialEntries: [
|
||||||
OverlayEntry(builder: (context) {
|
OverlayEntry(
|
||||||
|
builder: (context) {
|
||||||
return _ReaderScaffold(
|
return _ReaderScaffold(
|
||||||
child: _ReaderGestureDetector(
|
child: _ReaderGestureDetector(
|
||||||
child: _ReaderImages(key: Key(chapter.toString())),
|
child: _ReaderImages(key: Key(chapter.toString())),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
})
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -382,16 +402,29 @@ abstract mixin class _ImagePerPageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool showSingleImageOnFirstPage() =>
|
bool showSingleImageOnFirstPage() => appdata.settings.getReaderSetting(
|
||||||
appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSingleImageOnFirstPage');
|
cid,
|
||||||
|
type.sourceKey,
|
||||||
|
'showSingleImageOnFirstPage',
|
||||||
|
);
|
||||||
|
|
||||||
/// The number of images displayed on one screen
|
/// The number of images displayed on one screen
|
||||||
int get imagesPerPage {
|
int get imagesPerPage {
|
||||||
if (mode.isContinuous) return 1;
|
if (mode.isContinuous) return 1;
|
||||||
if (isPortrait) {
|
if (isPortrait) {
|
||||||
return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForPortrait') ?? 1;
|
return appdata.settings.getReaderSetting(
|
||||||
|
cid,
|
||||||
|
type.sourceKey,
|
||||||
|
'readerScreenPicNumberForPortrait',
|
||||||
|
) ??
|
||||||
|
1;
|
||||||
} else {
|
} else {
|
||||||
return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForLandscape') ?? 1;
|
return appdata.settings.getReaderSetting(
|
||||||
|
cid,
|
||||||
|
type.sourceKey,
|
||||||
|
'readerScreenPicNumberForLandscape',
|
||||||
|
) ??
|
||||||
|
1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,15 +433,22 @@ abstract mixin class _ImagePerPageHandler {
|
|||||||
int currentImagesPerPage = imagesPerPage;
|
int currentImagesPerPage = imagesPerPage;
|
||||||
bool currentOrientation = isPortrait;
|
bool currentOrientation = isPortrait;
|
||||||
|
|
||||||
if (_lastImagesPerPage != currentImagesPerPage || _lastOrientation != currentOrientation) {
|
if (_lastImagesPerPage != currentImagesPerPage ||
|
||||||
_adjustPageForImagesPerPageChange(_lastImagesPerPage, currentImagesPerPage);
|
_lastOrientation != currentOrientation) {
|
||||||
|
_adjustPageForImagesPerPageChange(
|
||||||
|
_lastImagesPerPage,
|
||||||
|
currentImagesPerPage,
|
||||||
|
);
|
||||||
_lastImagesPerPage = currentImagesPerPage;
|
_lastImagesPerPage = currentImagesPerPage;
|
||||||
_lastOrientation = currentOrientation;
|
_lastOrientation = currentOrientation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adjust the page number when the number of images per page changes
|
/// Adjust the page number when the number of images per page changes
|
||||||
void _adjustPageForImagesPerPageChange(int oldImagesPerPage, int newImagesPerPage) {
|
void _adjustPageForImagesPerPageChange(
|
||||||
|
int oldImagesPerPage,
|
||||||
|
int newImagesPerPage,
|
||||||
|
) {
|
||||||
int previousImageIndex = 1;
|
int previousImageIndex = 1;
|
||||||
if (!showSingleImageOnFirstPage() || oldImagesPerPage == 1) {
|
if (!showSingleImageOnFirstPage() || oldImagesPerPage == 1) {
|
||||||
previousImageIndex = (page - 1) * oldImagesPerPage + 1;
|
previousImageIndex = (page - 1) * oldImagesPerPage + 1;
|
||||||
@@ -466,10 +506,7 @@ abstract mixin class _VolumeListener {
|
|||||||
if (volumeListener != null) {
|
if (volumeListener != null) {
|
||||||
volumeListener?.cancel();
|
volumeListener?.cancel();
|
||||||
}
|
}
|
||||||
volumeListener = VolumeListener(
|
volumeListener = VolumeListener(onDown: onDown, onUp: onUp)..listen();
|
||||||
onDown: onDown,
|
|
||||||
onUp: onUp,
|
|
||||||
)..listen();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void stopVolumeEvent() {
|
void stopVolumeEvent() {
|
||||||
@@ -504,7 +541,8 @@ abstract mixin class _ReaderLocation {
|
|||||||
|
|
||||||
void update();
|
void update();
|
||||||
|
|
||||||
bool enablePageAnimation(String cid, ComicType type) => appdata.settings.getReaderSetting(cid, type.sourceKey, 'enablePageAnimation');
|
bool enablePageAnimation(String cid, ComicType type) => appdata.settings
|
||||||
|
.getReaderSetting(cid, type.sourceKey, 'enablePageAnimation');
|
||||||
|
|
||||||
_ImageViewController? _imageViewController;
|
_ImageViewController? _imageViewController;
|
||||||
|
|
||||||
@@ -585,7 +623,11 @@ abstract mixin class _ReaderLocation {
|
|||||||
autoPageTurningTimer!.cancel();
|
autoPageTurningTimer!.cancel();
|
||||||
autoPageTurningTimer = null;
|
autoPageTurningTimer = null;
|
||||||
} else {
|
} else {
|
||||||
int interval = appdata.settings.getReaderSetting(cid, type.sourceKey, 'autoPageTurningInterval');
|
int interval = appdata.settings.getReaderSetting(
|
||||||
|
cid,
|
||||||
|
type.sourceKey,
|
||||||
|
'autoPageTurningInterval',
|
||||||
|
);
|
||||||
autoPageTurningTimer = Timer.periodic(Duration(seconds: interval), (_) {
|
autoPageTurningTimer = Timer.periodic(Duration(seconds: interval), (_) {
|
||||||
if (page == maxPage) {
|
if (page == maxPage) {
|
||||||
autoPageTurningTimer!.cancel();
|
autoPageTurningTimer!.cancel();
|
||||||
|
|||||||
@@ -183,6 +183,14 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
if (shouldShowChapterComments())
|
||||||
|
Tooltip(
|
||||||
|
message: "Chapter Comments".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.comment),
|
||||||
|
onPressed: openChapterComments,
|
||||||
|
),
|
||||||
|
),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "Settings".tl,
|
message: "Settings".tl,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
@@ -605,7 +613,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
}
|
}
|
||||||
var (imageIndex, data) = result;
|
var (imageIndex, data) = result;
|
||||||
var fileType = detectFileType(data);
|
var fileType = detectFileType(data);
|
||||||
var filename = "${context.reader.widget.name}_${imageIndex + 1}${fileType.ext}";
|
var filename =
|
||||||
|
"${context.reader.widget.name}_${imageIndex + 1}${fileType.ext}";
|
||||||
saveFile(data: data, filename: filename);
|
saveFile(data: data, filename: filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +625,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
}
|
}
|
||||||
var (imageIndex, data) = result;
|
var (imageIndex, data) = result;
|
||||||
var fileType = detectFileType(data);
|
var fileType = detectFileType(data);
|
||||||
var filename = "${context.reader.widget.name}_${imageIndex + 1}${fileType.ext}";
|
var filename =
|
||||||
|
"${context.reader.widget.name}_${imageIndex + 1}${fileType.ext}";
|
||||||
Share.shareFile(data: data, filename: filename, mime: fileType.mime);
|
Share.shareFile(data: data, filename: filename, mime: fileType.mime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,6 +660,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
if (key == "quickCollectImage") {
|
if (key == "quickCollectImage") {
|
||||||
addDragListener();
|
addDragListener();
|
||||||
}
|
}
|
||||||
|
if (key == "showChapterComments") {
|
||||||
|
update();
|
||||||
|
}
|
||||||
context.reader.update();
|
context.reader.update();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -657,6 +670,48 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool shouldShowChapterComments() {
|
||||||
|
// Check if chapters exist
|
||||||
|
if (context.reader.widget.chapters == null) return false;
|
||||||
|
|
||||||
|
// Check if setting is enabled
|
||||||
|
var showChapterComments = appdata.settings.getReaderSetting(
|
||||||
|
context.reader.cid,
|
||||||
|
context.reader.type.sourceKey,
|
||||||
|
'showChapterComments',
|
||||||
|
);
|
||||||
|
if (showChapterComments != true) return false;
|
||||||
|
|
||||||
|
// Check if comic source supports chapter comments
|
||||||
|
var source = ComicSource.find(context.reader.type.sourceKey);
|
||||||
|
if (source == null || source.chapterCommentsLoader == null) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void openChapterComments() {
|
||||||
|
var source = ComicSource.find(context.reader.type.sourceKey);
|
||||||
|
if (source == null) return;
|
||||||
|
|
||||||
|
var chapters = context.reader.widget.chapters;
|
||||||
|
if (chapters == null) return;
|
||||||
|
|
||||||
|
var chapterIndex = context.reader.chapter - 1;
|
||||||
|
var epId = chapters.ids.elementAt(chapterIndex);
|
||||||
|
var chapterTitle = chapters.titles.elementAt(chapterIndex);
|
||||||
|
|
||||||
|
showSideBar(
|
||||||
|
context,
|
||||||
|
ChapterCommentsPage(
|
||||||
|
comicId: context.reader.cid,
|
||||||
|
epId: epId,
|
||||||
|
source: source,
|
||||||
|
comicTitle: context.reader.widget.name,
|
||||||
|
chapterTitle: chapterTitle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildEpChangeButton() {
|
Widget buildEpChangeButton() {
|
||||||
if (context.reader.widget.chapters == null) return const SizedBox();
|
if (context.reader.widget.chapters == null) return const SizedBox();
|
||||||
switch (showFloatingButtonValue) {
|
switch (showFloatingButtonValue) {
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
void search([String? text]) {
|
void search([String? text]) {
|
||||||
if (aggregatedSearch) {
|
if (aggregatedSearch) {
|
||||||
context
|
context
|
||||||
.to(() => AggregatedSearchPage(keyword: text ?? controller.text))
|
.to(
|
||||||
|
() => AggregatedSearchPage(keyword: text ?? controller.text),
|
||||||
|
iosFullScreenGesture: false,
|
||||||
|
)
|
||||||
.then((_) => update());
|
.then((_) => update());
|
||||||
} else {
|
} else {
|
||||||
context
|
context
|
||||||
@@ -59,6 +62,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
sourceKey: searchTarget,
|
sourceKey: searchTarget,
|
||||||
options: options,
|
options: options,
|
||||||
),
|
),
|
||||||
|
iosFullScreenGesture: false,
|
||||||
)
|
)
|
||||||
.then((_) => update());
|
.then((_) => update());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
).toSliver(),
|
).toSliver(),
|
||||||
_CallbackSetting(
|
_CallbackSetting(
|
||||||
title: "Custom Image Processing".tl,
|
title: "Custom Image Processing".tl,
|
||||||
callback: () => context.to(() => _CustomImageProcessing()),
|
callback: () => context.to(() => _CustomImageProcessing(), iosFullScreenGesture: false),
|
||||||
actionTitle: "Edit".tl,
|
actionTitle: "Edit".tl,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
_SliderSetting(
|
_SliderSetting(
|
||||||
@@ -303,6 +303,15 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Show Chapter Comments".tl,
|
||||||
|
settingKey: "showChapterComments",
|
||||||
|
onChanged: () {
|
||||||
|
widget.onChanged?.call("showChapterComments");
|
||||||
|
},
|
||||||
|
comicId: isEnabledSpecificSettings ? widget.comicId : null,
|
||||||
|
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
|
||||||
|
).toSliver(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,9 +252,10 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
|
|||||||
if (!App.isIOS) {
|
if (!App.isIOS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.position.dx < 20) {
|
if (currentPage == -1) {
|
||||||
gestureRecognizer.addPointer(event);
|
return;
|
||||||
}
|
}
|
||||||
|
gestureRecognizer.addPointer(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildLeft() {
|
Widget buildLeft() {
|
||||||
|
|||||||
15
pubspec.lock
15
pubspec.lock
@@ -478,10 +478,11 @@ packages:
|
|||||||
flutter_to_debian:
|
flutter_to_debian:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_to_debian
|
path: "."
|
||||||
sha256: d23534407334b331ce20fbaa8395b9ecc255d0c047136b8998715f36933ee696
|
ref: HEAD
|
||||||
url: "https://pub.dev"
|
resolved-ref: "3777c91b6b1cc0b7c03357c67ca216d4313c3db5"
|
||||||
source: hosted
|
url: "https://github.com/venera-app/flutter_to_debian.git"
|
||||||
|
source: git
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
flutter_web_plugins:
|
flutter_web_plugins:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
@@ -1126,10 +1127,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: zip_flutter
|
name: zip_flutter
|
||||||
sha256: c4d5a34c5803def866bc550926bb16fe89717c9b7304695d5b2ede30964eb8a8
|
sha256: baecf8deb6bf53a50e5ab513707ab56cc0c25f5b43333aa56ef562e8e7057357
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.12"
|
version: "0.0.13"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0 <4.0.0"
|
dart: ">=3.8.0 <4.0.0"
|
||||||
flutter: ">=3.35.5"
|
flutter: ">=3.35.7"
|
||||||
|
|||||||
10
pubspec.yaml
10
pubspec.yaml
@@ -2,11 +2,11 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.5.3+153
|
version: 1.6.0+160
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.8.0 <4.0.0'
|
sdk: '>=3.8.0 <4.0.0'
|
||||||
flutter: 3.35.5
|
flutter: 3.35.7
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@@ -53,7 +53,7 @@ dependencies:
|
|||||||
sliver_tools: ^0.2.12
|
sliver_tools: ^0.2.12
|
||||||
flutter_file_dialog: ^3.0.2
|
flutter_file_dialog: ^3.0.2
|
||||||
file_selector: ^1.0.3
|
file_selector: ^1.0.3
|
||||||
zip_flutter: ^0.0.12
|
zip_flutter: ^0.0.13
|
||||||
lodepng_flutter:
|
lodepng_flutter:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/venera-app/lodepng_flutter
|
url: https://github.com/venera-app/lodepng_flutter
|
||||||
@@ -93,7 +93,9 @@ dev_dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^5.0.0
|
||||||
flutter_to_arch: ^1.0.1
|
flutter_to_arch: ^1.0.1
|
||||||
flutter_to_debian: ^2.0.2
|
flutter_to_debian:
|
||||||
|
git:
|
||||||
|
url: https://github.com/venera-app/flutter_to_debian.git
|
||||||
archive: any
|
archive: any
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
|||||||
Reference in New Issue
Block a user