Compare commits

...

33 Commits

Author SHA1 Message Date
ynyx631
49abf92724 Update main.yml 2025-11-01 14:48:06 +08:00
ynyx631
38376c5b2e Merge pull request #600 from liulifox233/master
Fix editor page gesture confict
2025-11-01 13:55:00 +08:00
LiuliFox
4053faa186 Fix editor page gesture confict 2025-11-01 13:51:48 +08:00
ynyx631
17fd9b3606 Merge pull request #598 from luckyray-fan/feat-search-use-lower
feat: 本地收藏搜索支持转小写匹配
2025-11-01 13:29:05 +08:00
ynyx631
792c41fdc3 Update main.yml 2025-11-01 13:28:39 +08:00
Yoshiro_fan
05e661b101 feat: 本地收藏搜索支持转小写匹配 2025-11-01 13:08:43 +08:00
ynyx631
46131fcf41 Merge pull request #597 from venera-app/fix/deb-depends
Fix missing depends in deb package. Close #587
2025-11-01 12:30:04 +08:00
ynyx631
59750332cd Merge pull request #596 from venera-app/version-1.6.0
Update version code
2025-11-01 12:29:52 +08:00
ynyx631
fd017a35f9 Merge pull request #594 from lings03/favorite
Optimize favorite page and home page.
2025-11-01 12:29:42 +08:00
ynyx631
3834d0211f Merge pull request #593 from lings03/comment
Chapter comments.
2025-11-01 12:29:15 +08:00
ynyx631
10bec09c80 Merge pull request #592 from lings03/patch
Save data when mark all as read
2025-11-01 12:28:43 +08:00
ynyx631
62dd742280 Merge pull request #588 from venera-app/fix/comic-list-loading
Fix the issue of the comic list loading infinitely. Close #584
2025-11-01 12:28:30 +08:00
ynyx631
03603a53e1 Merge pull request #586 from venera-app/feat/login-webview-localstorage
Added support for localstorage when logging in via webview.
2025-11-01 12:28:17 +08:00
ynyx631
2847af91ff Merge pull request #585 from venera-app/feat/js-dialog-img
Add support for ArrayBuffer to showInputDialog.
2025-11-01 12:27:58 +08:00
ynyx631
0bc01f718a Merge pull request #583 from venera-app/fix/zip-chinese
Fix chinese character issue when compressing files. Close #565
2025-11-01 12:27:36 +08:00
ynyx631
b60119170a Merge pull request #582 from luckyray-fan/feat-add-filter-local-favorites
feat: 支持过滤阅读完成情况
2025-11-01 12:27:12 +08:00
ynyx631
f4af6f3954 Merge branch 'master' into feat-add-filter-local-favorites 2025-11-01 12:25:16 +08:00
ynyx631
9e9d1ac3b1 Merge pull request #578 from 4b1tQu4ntN3k0/normalize-protocol-relative-urls
[linux] Fix linux nhentai cover image
2025-11-01 12:06:49 +08:00
ynyx631
b3b9199cc3 Merge pull request #575 from liulifox233/master
[iOS] Enable full screen swipe back gesture
2025-11-01 12:06:16 +08:00
dd00ba11c8 Fix missing depends in deb package. Close #587 2025-11-01 12:04:14 +08:00
e87fb535b8 Update version code 2025-11-01 11:50:00 +08:00
角砂糖
df1649def6 Home page shared item 2025-11-01 04:17:29 +08:00
角砂糖
99559eaff8 Remove wrong showBarrier false 2025-11-01 02:59:50 +08:00
角砂糖
39a834815d Optimize favorite page. 2025-11-01 02:34:43 +08:00
角砂糖
a9e76201f3 Chapter comments. 2025-11-01 02:34:08 +08:00
角砂糖
0044d95e97 Save data when mark all as read 2025-11-01 00:37:25 +08:00
ynyx631
9636cf62cb Fix the issue of the comic list loading infinitely. Close #584 2025-10-29 19:39:18 +08:00
5ccf0eea43 Added support for localstorage when logging in via webview. 2025-10-28 19:21:28 +08:00
ynyx631
d22501198a Fix chinese character issue when compressing files. Close #565 2025-10-26 20:38:58 +08:00
Yoshiro_fan
be23c4fe68 feat: 支持过滤阅读完成情况 2025-10-26 16:30:52 +08:00
4b1tQu4ntN3k0
a8422780a0 fix linux nhentai cover image 2025-10-25 07:33:51 +08:00
LiuliFox
75c2a3a417 Fix some gesture conflicts 2025-10-20 15:44:07 +08:00
LiuliFox
3d194d7f6a [iOS] Enable full screen swipe back gesture 2025-10-20 10:08:25 +08:00
36 changed files with 1473 additions and 253 deletions

View File

@@ -143,7 +143,7 @@ jobs:
- run: |
sudo apt-get update -y
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: dart run flutter_to_arch
- run: |
@@ -171,7 +171,7 @@ jobs:
flutter pub get
sudo apt-get update -y
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"
run: |
dart run patch/font.dart

View File

@@ -47,6 +47,7 @@
"Move to folder": "移动到文件夹",
"Copy to folder": "复制到文件夹",
"Delete Comic": "删除漫画",
"Jump to Detail": "跳转详情",
"Delete @c comics?": "删除 @c 本漫画?",
"Add comic source": "添加漫画源",
"Delete comic source '@n' ?": "删除漫画源 '@n' ",
@@ -69,6 +70,9 @@
"Next": "前进",
"Login with webview": "通过网页登录",
"Read": "阅读",
"Completed": "已完成",
"UnCompleted": "未完成",
"Filter reading status": "过滤阅读状态",
"Download": "下载",
"Favorite": "收藏",
"Comments": "评论",
@@ -379,6 +383,8 @@
"Continuous": "连续",
"Display mode of comic list": "漫画列表的显示模式",
"Show Page Number": "显示页码",
"Show Chapter Comments": "显示章节评论",
"Chapter Comments": "章节评论",
"Jump to page": "跳转到页面",
"Page": "页面",
"Jump": "跳转",
@@ -464,6 +470,7 @@
"Move": "移動",
"Move to folder": "移動到資料夾",
"Copy to folder": "複製到資料夾",
"Jump to Detail": "跳轉詳情​​",
"Delete Comic": "刪除漫畫",
"Delete @c comics?": "刪除 @c 本漫畫?",
"Add comic source": "添加漫畫源",
@@ -487,6 +494,9 @@
"Next": "前進",
"Login with webview": "透過網頁登入",
"Read": "閱讀",
"Completed": "已完成",
"UnCompleted": "未完成",
"Filter reading status": "過濾閱讀狀態",
"Download": "下載",
"Favorite": "收藏",
"Comments": "評論",
@@ -796,6 +806,8 @@
"Continuous": "連續",
"Display mode of comic list": "漫畫列表的顯示模式",
"Show Page Number": "顯示頁碼",
"Show Chapter Comments": "顯示章節評論",
"Chapter Comments": "章節評論",
"Jump to page": "跳轉到頁面",
"Page": "頁面",
"Jump": "跳轉",

View File

@@ -553,6 +553,51 @@ If `load` function is implemented, `loadNext` function will be ignored.
*/
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

View File

@@ -753,9 +753,9 @@ class SliverGridComics extends StatefulWidget {
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
State<SliverGridComics> createState() => _SliverGridComicsState();
@@ -856,52 +856,51 @@ class _SliverGridComics extends StatelessWidget {
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
Widget build(BuildContext context) {
return SliverGrid(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index == comics.length - 1) {
onLastItemBuild?.call();
}
var badge = badgeBuilder?.call(comics[index]);
var isSelected =
selection == null ? false : selection![comics[index]] ?? false;
var comic = ComicTile(
comic: comics[index],
badge: badge,
menuOptions: menuBuilder?.call(comics[index]),
onTap: onTap != null ? () => onTap!(comics[index]) : null,
onLongPressed: onLongPressed != null
? () => onLongPressed!(comics[index])
delegate: SliverChildBuilderDelegate((context, index) {
if (index == comics.length - 1) {
onLastItemBuild?.call();
}
var badge = badgeBuilder?.call(comics[index]);
var isSelected = selection == null
? false
: selection![comics[index]] ?? false;
var comic = ComicTile(
comic: comics[index],
badge: badge,
menuOptions: menuBuilder?.call(comics[index]),
onTap: onTap != null
? () => onTap!(comics[index], heroIDs[index])
: null,
onLongPressed: onLongPressed != null
? () => onLongPressed!(comics[index], heroIDs[index])
: null,
heroID: heroIDs[index],
);
if (selection == null) {
return comic;
}
return AnimatedContainer(
key: ValueKey(comics[index].id),
duration: const Duration(milliseconds: 150),
decoration: BoxDecoration(
color: isSelected
? Theme.of(
context,
).colorScheme.secondaryContainer.toOpacity(0.72)
: null,
heroID: heroIDs[index],
);
if (selection == null) {
return comic;
}
return AnimatedContainer(
key: ValueKey(comics[index].id),
duration: const Duration(milliseconds: 150),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context)
.colorScheme
.secondaryContainer
.toOpacity(0.72)
: null,
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(4),
child: comic,
);
},
childCount: comics.length,
),
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(4),
child: comic,
);
}, childCount: comics.length),
gridDelegate: SliverGridDelegateWithComics(),
);
}
@@ -1159,7 +1158,7 @@ class ComicListState extends State<ComicList> {
if (res.data.isEmpty) {
setState(() {
_data[page] = const [];
_maxPage = page;
_maxPage ??= page;
});
} else {
setState(() {
@@ -1282,8 +1281,8 @@ class ComicListState extends State<ComicList> {
],
);
}
if (_data[_page] == null) {
_loadPage(_page);
if (_data[1] == null) {
_loadPage(1);
return Column(
children: [
if (widget.errorLeading != null) widget.errorLeading!,
@@ -1304,7 +1303,7 @@ class ComicListState extends State<ComicList> {
comics: _data.values.expand((element) => element).toList(),
menuBuilder: widget.menuBuilder,
onLastItemBuild: () {
if (_error == null && (_maxPage == null || _page < _maxPage!)) {
if (_error == null && (_maxPage == null || _data.length < _maxPage!)) {
_loadPage(_data.length + 1);
}
},
@@ -1334,7 +1333,7 @@ class ComicListState extends State<ComicList> {
],
).paddingHorizontal(16).paddingVertical(8),
)
else if (_maxPage == null || _page < _maxPage!)
else if (_maxPage == null || _data.length < _maxPage!)
const SliverListLoadingIndicator(),
if (widget.trailingSliver != null) widget.trailingSliver!,
],
@@ -1627,7 +1626,7 @@ class _SMClipper extends CustomClipper<Rect> {
class SimpleComicTile extends StatelessWidget {
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;
@@ -1635,6 +1634,8 @@ class SimpleComicTile extends StatelessWidget {
final bool withTitle;
final int? heroID;
@override
Widget build(BuildContext context) {
var image = _findImageProvider(comic);
@@ -1660,6 +1661,13 @@ class SimpleComicTile extends StatelessWidget {
child: child,
);
if (heroID != null) {
child = Hero(
tag: "cover$heroID",
child: child,
);
}
child = AnimatedTapRegion(
borderRadius: 8,
onTap: onTap ??
@@ -1668,6 +1676,9 @@ class SimpleComicTile extends StatelessWidget {
() => ComicPage(
id: comic.id,
sourceKey: comic.sourceKey,
cover: comic.cover,
title: comic.title,
heroID: heroID,
),
);
},

View File

@@ -241,6 +241,10 @@ class _AppScrollBarState extends State<AppScrollBar> {
late final VerticalDragGestureRecognizer _dragGestureRecognizer;
bool _isVisible = false;
Timer? _hideTimer;
static const _hideDuration = Duration(seconds: 2);
@override
void initState() {
super.initState();
@@ -248,7 +252,41 @@ class _AppScrollBarState extends State<AppScrollBar> {
_scrollController.addListener(onChanged);
Future.microtask(onChanged);
_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) {
@@ -269,14 +307,24 @@ class _AppScrollBarState extends State<AppScrollBar> {
void onChanged() {
if (_scrollController.positions.isEmpty) return;
var position = _scrollController.position;
bool hasChanged = false;
if (position.minScrollExtent != minExtent ||
position.maxScrollExtent != maxExtent ||
position.pixels != this.position) {
setState(() {
minExtent = position.minScrollExtent;
maxExtent = position.maxScrollExtent;
this.position = position.pixels;
});
hasChanged = true;
minExtent = position.minScrollExtent;
maxExtent = position.maxScrollExtent;
this.position = position.pixels;
}
if (hasChanged) {
_showScrollbar();
_scheduleHide();
}
if (hasChanged && mounted) {
setState(() {});
}
}
@@ -300,29 +348,35 @@ class _AppScrollBarState extends State<AppScrollBar> {
Positioned(
top: top + widget.topPadding,
right: 0,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (event) {
_dragGestureRecognizer.addPointer(event);
},
child: SizedBox(
width: _scrollIndicatorSize/2,
height: _scrollIndicatorSize,
child: CustomPaint(
painter: _ScrollIndicatorPainter(
backgroundColor: context.colorScheme.surface,
shadowColor: context.colorScheme.shadow,
child: AnimatedOpacity(
opacity: _isVisible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => _showScrollbar(),
onExit: (_) => _scheduleHide(),
child: Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (event) {
_dragGestureRecognizer.addPointer(event);
},
child: SizedBox(
width: _scrollIndicatorSize / 2,
height: _scrollIndicatorSize,
child: CustomPaint(
painter: _ScrollIndicatorPainter(
backgroundColor: context.colorScheme.surface,
shadowColor: context.colorScheme.shadow,
),
child: Column(
children: [
const Spacer(),
Icon(Icons.arrow_drop_up, size: 18),
Icon(Icons.arrow_drop_down, size: 18),
const Spacer(),
],
).paddingLeft(4),
),
child: Column(
children: [
const Spacer(),
Icon(Icons.arrow_drop_up, size: 18),
Icon(Icons.arrow_drop_down, size: 18),
const Spacer(),
],
).paddingLeft(4),
),
),
),

View File

@@ -13,7 +13,7 @@ export "widget_utils.dart";
export "context.dart";
class _App {
final version = "1.5.3";
final version = "1.6.0";
bool get isAndroid => Platform.isAndroid;

View File

@@ -2,6 +2,7 @@ import 'dart:math';
import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:venera/foundation/app.dart';
const double _kBackGestureWidth = 20.0;
@@ -19,6 +20,7 @@ class AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin{
super.allowSnapshotting = true,
super.barrierDismissible = false,
this.enableIOSGesture = true,
this.iosFullScreenPopGesture = true,
this.preventRebuild = true,
}) {
assert(opaque);
@@ -48,6 +50,9 @@ class AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin{
@override
final bool enableIOSGesture;
@override
final bool iosFullScreenPopGesture;
@override
final bool preventRebuild;
}
@@ -74,6 +79,8 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
bool get enableIOSGesture;
bool get iosFullScreenPopGesture;
bool get preventRebuild;
Widget? _child;
@@ -121,20 +128,22 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
builder = PredictiveBackPageTransitionsBuilder();
} else {
builder = SlidePageTransitionBuilder();
}
}
return builder.buildTransitions(
return builder.buildTransitions(
this,
context,
animation,
secondaryAnimation,
enableIOSGesture && App.isIOS
? IOSBackGestureDetector(
gestureWidth: _kBackGestureWidth,
enabledCallback: () => _isPopGestureEnabled<T>(this),
onStartPopGesture: () => _startPopGesture(this),
child: child)
: child);
enableIOSGesture && App.isIOS
? IOSBackGestureDetector(
gestureWidth: _kBackGestureWidth,
enabledCallback: () => _isPopGestureEnabled<T>(this),
onStartPopGesture: () => _startPopGesture(this),
fullScreen: iosFullScreenPopGesture,
child: child,
)
: child);
}
IOSBackGestureController _startPopGesture(PageRoute<T> route) {
@@ -206,6 +215,7 @@ class IOSBackGestureDetector extends StatefulWidget {
required this.child,
required this.gestureWidth,
required this.onStartPopGesture,
this.fullScreen = false,
super.key});
final double gestureWidth;
@@ -216,6 +226,8 @@ class IOSBackGestureDetector extends StatefulWidget {
final Widget child;
final bool fullScreen;
@override
State<IOSBackGestureDetector> createState() => _IOSBackGestureDetectorState();
}
@@ -247,26 +259,40 @@ class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
? MediaQuery.of(context).padding.left
: MediaQuery.of(context).padding.right;
dragAreaWidth = max(dragAreaWidth, widget.gestureWidth);
final Widget gestureListener = widget.fullScreen
? Positioned.fill(
child: Listener(
onPointerDown: _handlePointerDown,
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,
Positioned(
width: dragAreaWidth,
top: 0.0,
bottom: 0.0,
left: 0,
child: Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.translucent,
),
),
gestureListener,
],
);
}
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() {
@@ -304,6 +330,28 @@ class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
_backGestureController!.dragUpdate(
_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 {
@@ -314,30 +362,31 @@ class SlidePageTransitionBuilder extends PageTransitionsBuilder {
Animation<double> animation,
Animation<double> secondaryAnimation,
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(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(primaryAnimation),
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.ease,
)),
child: SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: const Offset(-0.4, 0),
).animate(CurvedAnimation(
parent: secondaryAnimation,
curve: Curves.ease,
)),
child: PhysicalModel(
color: Colors.transparent,
borderRadius: BorderRadius.zero,
clipBehavior: Clip.hardEdge,
elevation: 6,
child: Material(child: child,),
),
)
begin: Offset.zero,
end: const Offset(-0.4, 0),
).animate(secondaryCurve),
child: PhysicalModel(
color: Colors.transparent,
borderRadius: BorderRadius.zero,
clipBehavior: Clip.hardEdge,
elevation: 6,
child: Material(child: child),
),
),
);
}
}

View File

@@ -194,6 +194,7 @@ class Settings with ChangeNotifier {
'readerScrollSpeed': 1.0, // 0.5 - 3.0
'localFavoritesFirst': true,
'autoCloseFavoritePanel': false,
'showChapterComments': true, // show chapter comments in reader
};
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);
}
@@ -215,7 +220,8 @@ class Settings with ChangeNotifier {
if (comicId == null || sourceKey == null) {
return false;
}
return _data['comicSpecificSettings']["$comicId@$sourceKey"]?["enabled"] == true;
return _data['comicSpecificSettings']["$comicId@$sourceKey"]?["enabled"] ==
true;
}
dynamic getReaderSetting(String comicId, String sourceKey, String key) {

View File

@@ -61,8 +61,10 @@ class ComicSourceManager with ChangeNotifier, Init {
await for (var entity in Directory(path).list()) {
if (entity is File && entity.path.endsWith(".js")) {
try {
var source = await ComicSourceParser()
.parse(await entity.readAsString(), entity.absolute.path);
var source = await ComicSourceParser().parse(
await entity.readAsString(),
entity.absolute.path,
);
_sources.add(source);
} catch (e, s) {
Log.error("ComicSource", "$e\n$s");
@@ -154,7 +156,7 @@ class ComicSource {
final GetImageLoadingConfigFunc? getImageLoadingConfig;
final Map<String, dynamic> Function(String imageKey)?
getThumbnailLoadingConfig;
getThumbnailLoadingConfig;
var data = <String, dynamic>{};
@@ -170,6 +172,10 @@ class ComicSource {
final SendCommentFunc? sendCommentFunc;
final ChapterCommentsLoader? chapterCommentsLoader;
final SendChapterCommentFunc? sendChapterCommentFunc;
final RegExp? idMatcher;
final LikeOrUnlikeComicFunc? likeOrUnlikeComic;
@@ -256,6 +262,8 @@ class ComicSource {
this.version,
this.commentsLoader,
this.sendCommentFunc,
this.chapterCommentsLoader,
this.sendChapterCommentFunc,
this.likeOrUnlikeComic,
this.voteCommentFunc,
this.likeCommentFunc,
@@ -367,11 +375,19 @@ enum ExplorePageType {
override,
}
typedef SearchFunction = Future<Res<List<Comic>>> Function(
String keyword, int page, List<String> searchOption);
typedef SearchFunction =
Future<Res<List<Comic>>> Function(
String keyword,
int page,
List<String> searchOption,
);
typedef SearchNextFunction = Future<Res<List<Comic>>> Function(
String keyword, String? next, List<String> searchOption);
typedef SearchNextFunction =
Future<Res<List<Comic>>> Function(
String keyword,
String? next,
List<String> searchOption,
);
class SearchPageData {
/// 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 ?? "";
}
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
String category, String? param, List<String> options, int page);
typedef CategoryComicsLoader =
Future<Res<List<Comic>>> Function(
String category,
String? param,
List<String> options,
int page,
);
typedef CategoryOptionsLoader = Future<Res<List<CategoryComicsOptions>>> Function(
String category, String? param);
typedef CategoryOptionsLoader =
Future<Res<List<CategoryComicsOptions>>> Function(
String category,
String? param,
);
class CategoryComicsData {
/// options
@@ -419,7 +443,12 @@ class CategoryComicsData {
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 {
@@ -428,7 +457,7 @@ class RankingData {
final Future<Res<List<Comic>>> Function(String option, int page)? load;
final Future<Res<List<Comic>>> Function(String option, String? next)?
loadWithNext;
loadWithNext;
const RankingData(this.options, this.load, this.loadWithNext);
}
@@ -447,7 +476,12 @@ class CategoryComicsOptions {
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 {

View File

@@ -542,6 +542,7 @@ class PageJumpTarget {
sourceKey: sourceKey,
options: List.from(attributes?["options"] ?? []),
),
iosFullScreenGesture: false,
);
} else if (page == "category") {
var key = ComicSource.find(sourceKey)!.categoryData!.key;

View File

@@ -151,6 +151,8 @@ class ComicSourceParser {
version ?? "1.0.0",
_parseCommentsLoader(),
_parseSendCommentFunc(),
_parseChapterCommentsLoader(),
_parseSendChapterCommentFunc(),
_parseLikeFunc(),
_parseVoteCommentFunc(),
_parseLikeCommentFunc(),
@@ -560,12 +562,16 @@ class ComicSourceParser {
res = await res;
}
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>[];
for (var element in res) {
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>();
for (var option in element["options"] ?? []) {
@@ -582,13 +588,14 @@ class ComicSourceParser {
element["label"] ?? "",
map,
List.from(element["notShowWhen"] ?? []),
element["showWhen"] == null ? null : List.from(element["showWhen"]),
element["showWhen"] == null
? null
: List.from(element["showWhen"]),
),
);
}
return Res(options);
}
catch(e) {
} catch (e) {
Log.error("Data Analysis", "Failed to load category options.\n$e");
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() {
if (!_checkExists("comic.onImageLoad")) {
return null;

View File

@@ -4,50 +4,90 @@ part of 'comic_source.dart';
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.
typedef ComicListBuilderWithNext = Future<Res<List<Comic>>> Function(
String? next);
typedef ComicListBuilderWithNext =
Future<Res<List<Comic>>> Function(String? next);
typedef LoginFunction = Future<Res<bool>> Function(String, String);
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
typedef LoadComicPagesFunc = Future<Res<List<String>>> Function(
String id, String? ep);
typedef LoadComicPagesFunc =
Future<Res<List<String>>> Function(String id, String? ep);
typedef CommentsLoader = Future<Res<List<Comment>>> Function(
String id, String? subId, int page, String? replyTo);
typedef CommentsLoader =
Future<Res<List<Comment>>> Function(
String id,
String? subId,
int page,
String? replyTo,
);
typedef SendCommentFunc = Future<Res<bool>> Function(
String id, String? subId, String content, String? replyTo);
typedef ChapterCommentsLoader =
Future<Res<List<Comment>>> Function(
String comicId,
String epId,
int page,
String? replyTo,
);
typedef GetImageLoadingConfigFunc = Future<Map<String, dynamic>> Function(
String imageKey, String comicId, String epId)?;
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
String imageKey)?;
typedef SendCommentFunc =
Future<Res<bool>> Function(
String id,
String? subId,
String content,
String? replyTo,
);
typedef ComicThumbnailLoader = Future<Res<List<String>>> Function(
String comicId, String? next);
typedef SendChapterCommentFunc =
Future<Res<bool>> Function(
String comicId,
String epId,
String content,
String? replyTo,
);
typedef LikeOrUnlikeComicFunc = Future<Res<bool>> Function(
String comicId, bool isLiking);
typedef GetImageLoadingConfigFunc =
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.
/// return the new likes count or null.
typedef LikeCommentFunc = Future<Res<int?>> Function(
String comicId, String? subId, String commentId, bool isLiking);
typedef LikeCommentFunc =
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.
/// return the new vote count or null.
typedef VoteCommentFunc = Future<Res<int?>> Function(
String comicId, String? subId, String commentId, bool isUp, bool isCancel);
typedef VoteCommentFunc =
Future<Res<int?>> Function(
String comicId,
String? subId,
String commentId,
bool isUp,
bool isCancel,
);
typedef HandleClickTagEvent = PageJumpTarget? Function(
String namespace, String tag);
typedef HandleClickTagEvent =
PageJumpTarget? Function(String namespace, String tag);
/// Handle tag suggestion selection event. Should return the text to insert
/// into the search field.
typedef TagSuggestionSelectFunc = String Function(
String namespace, String tag);
typedef TagSuggestionSelectFunc = String Function(String namespace, String tag);
/// [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);

View File

@@ -14,14 +14,20 @@ extension Navigation on BuildContext {
return Navigator.of(this).canPop();
}
Future<T?> to<T>(Widget Function() builder) {
return Navigator.of(this)
.push<T>(AppPageRoute(builder: (context) => builder()));
Future<T?> to<T>(Widget Function() builder,
{bool enableIOSGesture = true, bool iosFullScreenGesture = true}) {
return Navigator.of(this).push<T>(AppPageRoute(
builder: (context) => builder(),
enableIOSGesture: enableIOSGesture,
iosFullScreenPopGesture: iosFullScreenGesture));
}
Future<void> toReplacement<T>(Widget Function() builder) {
return Navigator.of(this)
.pushReplacement(AppPageRoute(builder: (context) => builder()));
Future<void> toReplacement<T>(Widget Function() builder,
{bool enableIOSGesture = true, bool iosFullScreenGesture = true}) {
return Navigator.of(this).pushReplacement(AppPageRoute(
builder: (context) => builder(),
enableIOSGesture: enableIOSGesture,
iosFullScreenPopGesture: iosFullScreenGesture));
}
double get width => MediaQuery.of(this).size.width;

View File

@@ -441,7 +441,7 @@ class ImageFavoriteManager with ChangeNotifier {
for (var comic in comics) {
count += comic.images.length;
for (var tag in comic.tags) {
String finalTag = tag;
String finalTag = tag.split(":").last;
tagCount[finalTag] = (tagCount[finalTag] ?? 0) + 1;
}

View File

@@ -154,6 +154,8 @@ class LocalComic with HistoryMixin implements Comic {
author: subtitle,
tags: tags,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
);
}

View File

@@ -52,7 +52,11 @@ abstract class ImageDownloader {
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']);
var stream = req.data?.stream ?? (throw "Error: Empty response body.");
int? expectedBytes = req.data!.contentLength;

View File

@@ -170,6 +170,7 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
text: widget.keyword,
sourceKey: widget.source.key,
),
iosFullScreenGesture: false,
);
},
child: Column(

View File

@@ -116,6 +116,8 @@ abstract mixin class _ComicPageActions {
author: comic.findAuthor() ?? '',
tags: comic.plainTags,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
)
.then((_) {
onReadEnd();

View File

@@ -236,7 +236,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
author: localComic.subTitle ?? '',
tags: localComic.tags,
);
});
}, enableIOSGesture: false, iosFullScreenGesture: false);
App.mainNavigatorKey!.currentContext!.pop();
});
isFirst = false;

View File

@@ -200,6 +200,7 @@ class _BodyState extends State<_Body> {
await ComicSourceManager().reload();
setState(() {});
}),
iosFullScreenGesture: false,
);
}
@@ -1245,6 +1246,15 @@ class _LoginPageState extends State<_LoginPage> {
if (widget.config.checkLoginStatus != null &&
widget.config.checkLoginStatus!(url, title)) {
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(
Uri.parse(url),
cookies,
@@ -1306,6 +1316,20 @@ class _LoginPageState extends State<_LoginPage> {
Uri.parse(url),
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;
widget.config.onLoginWithWebviewSuccess?.call();
webview.close();

View File

@@ -11,6 +11,7 @@ import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/consts.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/foundation/res.dart';

View File

@@ -30,6 +30,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
late List<String> added = [];
String keyword = "";
bool searchHasUpper = false;
bool searchMode = false;
@@ -43,6 +44,8 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
bool isLoading = false;
late String readFilterSelect;
var searchResults = <FavoriteItem>[];
void updateSearchResult() {
@@ -104,27 +107,40 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
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) {
var list = keyword.split(" ");
for (var k in list) {
if (k.isEmpty) continue;
if (comic.title.contains(k)) {
if (checkKeyWordMatch(k, comic.title, false)) {
continue;
} else if (comic.subtitle != null && comic.subtitle!.contains(k)) {
} else if (comic.subtitle != null && checkKeyWordMatch(k, comic.subtitle!, false)) {
continue;
} else if (comic.tags.any((tag) {
if (tag == k) {
if (checkKeyWordMatch(k, tag, true)) {
return true;
} else if (tag.contains(':') && tag.split(':')[1] == k) {
} else if (tag.contains(':') && checkKeyWordMatch(k, tag.split(':')[1], true)) {
return true;
} else if (App.locale.languageCode != 'en' &&
tag.translateTagsToCN == k) {
checkKeyWordMatch(k, tag.translateTagsToCN, true)) {
return true;
}
return false;
})) {
continue;
} else if (comic.author == k) {
} else if (checkKeyWordMatch(k, comic.author, true)) {
continue;
}
return false;
@@ -132,6 +148,17 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
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
bool matchKeywordT(String keyword, FavoriteItem comic) {
if (!OpenCC.hasChineseSimplified(keyword)) {
@@ -149,9 +176,10 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
keyword = OpenCC.traditionalToSimplified(keyword);
return matchKeyword(keyword, comic);
}
@override
void initState() {
readFilterSelect = appdata.implicitData["local_favorites_read_filter"] ??
readFilterList[0];
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
if (!isAllFolder) {
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(
message: "Search".tl,
child: IconButton(
@@ -454,15 +507,15 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
actions: [
MenuButton(entries: [
if (!isAllFolder)
MenuEntry(
icon: Icons.drive_file_move,
text: "Move to folder".tl,
onClick: () => favoriteOption('move')),
MenuEntry(
icon: Icons.drive_file_move,
text: "Move to folder".tl,
onClick: () => favoriteOption('move')),
if (!isAllFolder)
MenuEntry(
icon: Icons.copy,
text: "Copy to folder".tl,
onClick: () => favoriteOption('add')),
MenuEntry(
icon: Icons.copy,
text: "Copy to folder".tl,
onClick: () => favoriteOption('add')),
MenuEntry(
icon: Icons.select_all,
text: "Select All".tl,
@@ -519,9 +572,27 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
onClick: () {
final c = selectedComics.keys.first as FavoriteItem;
App.rootContext.to(() => ReaderWithLoading(
id: c.id,
sourceKey: c.sourceKey,
));
id: c.id,
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) {
keyword = v;
searchHasUpper = keyword.contains(RegExp(r'[A-Z]'));
updateSearchResult();
},
).paddingBottom(8).paddingRight(8),
@@ -568,7 +640,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
)
else
SliverGridComics(
comics: searchMode ? searchResults : comics,
comics: searchMode ? searchResults : filterComics(comics),
selections: selectedComics,
menuBuilder: (c) {
return [
@@ -622,12 +694,14 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
id: c.id,
sourceKey: c.sourceKey,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
);
},
),
];
},
onTap: (c) {
onTap: (c, heroID) {
if (multiSelectMode) {
setState(() {
if (selectedComics.containsKey(c as FavoriteItem)) {
@@ -639,18 +713,24 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
lastSelectedIndex = comics.indexOf(c);
});
} else if (appdata.settings["onClickFavorite"] == "viewDetail") {
App.mainNavigatorKey?.currentContext
?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
} else {
App.mainNavigatorKey?.currentContext?.to(
() => ReaderWithLoading(
() => ComicPage(
id: c.id,
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(() {
if (!multiSelectMode) {
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),
),
],
);
}
}

View File

@@ -299,6 +299,7 @@ class _FollowUpdatesPageState extends AutomaticGlobalState<FollowUpdatesPage> {
);
}
updateFollowUpdatesUI();
appdata.saveData();
},
);
},

View File

@@ -211,7 +211,7 @@ class _HistoryPageState extends State<HistoryPage> {
selections: selectedComics,
onLongPressed: null,
onTap: multiSelectMode
? (c) {
? (c, heroID) {
setState(() {
if (selectedComics.containsKey(c as History)) {
selectedComics.remove(c);

View File

@@ -302,13 +302,18 @@ class _HistoryState extends State<_History> {
scrollDirection: Axis.horizontal,
itemCount: history.length,
itemBuilder: (context, index) {
final heroID = history[index].id.hashCode;
return SimpleComicTile(
comic: history[index],
heroID: heroID,
onTap: () {
context.to(
() => ComicPage(
id: history[index].id,
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(
margin: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(8),
@@ -405,9 +412,22 @@ class _LocalState extends State<_Local> {
scrollDirection: Axis.horizontal,
itemCount: local.length,
itemBuilder: (context, index) {
return SimpleComicTile(comic: local[index])
.paddingHorizontal(8)
.paddingVertical(2);
final heroID = local[index].id.hashCode;
return SimpleComicTile(
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),
@@ -874,7 +894,10 @@ class _ImageFavoritesState extends State<ImageFavorites> {
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
context.to(() => const ImageFavoritesPage());
context.to(
() => const ImageFavoritesPage(),
iosFullScreenGesture: false,
);
},
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -993,7 +1016,10 @@ class _ImageFavoritesState extends State<ImageFavorites> {
maxCount: maxCount,
enableTranslation: displayType != 2,
onTap: (text) {
context.to(() => ImageFavoritesPage(initialKeyword: text));
context.to(
() => ImageFavoritesPage(initialKeyword: text),
iosFullScreenGesture: false,
);
},
);
}).toList(),

View File

@@ -37,6 +37,8 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> {
initialEp: ep,
initialPage: page,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
);
}

View File

@@ -244,6 +244,8 @@ class _ImageFavoritesPhotoViewState extends State<ImageFavoritesPhotoView> {
initialEp: ep,
initialPage: page,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
);
},
),

View File

@@ -285,13 +285,13 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
SliverGridComics(
comics: comics,
selections: selectedComics,
onLongPressed: (c) {
onLongPressed: (c, heroID) {
setState(() {
multiSelectMode = true;
selectedComics[c as LocalComic] = true;
});
},
onTap: (c) {
onTap: (c, heroID) {
if (multiSelectMode) {
setState(() {
if (selectedComics.containsKey(c as LocalComic)) {

View 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);
}
}
}

View File

@@ -25,6 +25,7 @@ import 'package:venera/foundation/consts.dart';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/global_state.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/local.dart';
import 'package:venera/foundation/log.dart';
@@ -54,6 +55,8 @@ part 'loading.dart';
part 'chapters.dart';
part 'chapter_comments.dart';
extension _ReaderContext on BuildContext {
_ReaderState get reader => findAncestorStateOfType<_ReaderState>()!;
@@ -163,14 +166,27 @@ class _ReaderState extends State<Reader>
}
if (widget.initialPage != null) {
page = widget.initialPage!;
if (page < 1) {
page = 1;
}
}
// 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;
if (!appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSystemStatusBar')) {
if (!appdata.settings.getReaderSetting(
cid,
type.sourceKey,
'showSystemStatusBar',
)) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
if (appdata.settings.getReaderSetting(cid, type.sourceKey, 'enableTurnPageByVolumeKey')) {
if (appdata.settings.getReaderSetting(
cid,
type.sourceKey,
'enableTurnPageByVolumeKey',
)) {
handleVolumeEvent();
}
setImageCacheSize();
@@ -208,8 +224,10 @@ class _ReaderState extends State<Reader>
} else {
maxImageCacheSize = 500 << 20;
}
Log.info("Reader",
"Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize");
Log.info(
"Reader",
"Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize",
);
PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize;
}
@@ -239,13 +257,15 @@ class _ReaderState extends State<Reader>
onKeyEvent: onKeyEvent,
child: Overlay(
initialEntries: [
OverlayEntry(builder: (context) {
return _ReaderScaffold(
child: _ReaderGestureDetector(
child: _ReaderImages(key: Key(chapter.toString())),
),
);
})
OverlayEntry(
builder: (context) {
return _ReaderScaffold(
child: _ReaderGestureDetector(
child: _ReaderImages(key: Key(chapter.toString())),
),
);
},
),
],
),
);
@@ -382,16 +402,29 @@ abstract mixin class _ImagePerPageHandler {
}
}
bool showSingleImageOnFirstPage() =>
appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSingleImageOnFirstPage');
bool showSingleImageOnFirstPage() => appdata.settings.getReaderSetting(
cid,
type.sourceKey,
'showSingleImageOnFirstPage',
);
/// The number of images displayed on one screen
int get imagesPerPage {
if (mode.isContinuous) return 1;
if (isPortrait) {
return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForPortrait') ?? 1;
return appdata.settings.getReaderSetting(
cid,
type.sourceKey,
'readerScreenPicNumberForPortrait',
) ??
1;
} 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;
bool currentOrientation = isPortrait;
if (_lastImagesPerPage != currentImagesPerPage || _lastOrientation != currentOrientation) {
_adjustPageForImagesPerPageChange(_lastImagesPerPage, currentImagesPerPage);
if (_lastImagesPerPage != currentImagesPerPage ||
_lastOrientation != currentOrientation) {
_adjustPageForImagesPerPageChange(
_lastImagesPerPage,
currentImagesPerPage,
);
_lastImagesPerPage = currentImagesPerPage;
_lastOrientation = currentOrientation;
}
}
/// 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;
if (!showSingleImageOnFirstPage() || oldImagesPerPage == 1) {
previousImageIndex = (page - 1) * oldImagesPerPage + 1;
@@ -431,7 +471,7 @@ abstract mixin class _ImagePerPageHandler {
newPage = previousImageIndex;
}
page = newPage>0 ? newPage : 1;
page = newPage > 0 ? newPage : 1;
}
}
@@ -466,10 +506,7 @@ abstract mixin class _VolumeListener {
if (volumeListener != null) {
volumeListener?.cancel();
}
volumeListener = VolumeListener(
onDown: onDown,
onUp: onUp,
)..listen();
volumeListener = VolumeListener(onDown: onDown, onUp: onUp)..listen();
}
void stopVolumeEvent() {
@@ -504,7 +541,8 @@ abstract mixin class _ReaderLocation {
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;
@@ -585,7 +623,11 @@ abstract mixin class _ReaderLocation {
autoPageTurningTimer!.cancel();
autoPageTurningTimer = null;
} 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), (_) {
if (page == maxPage) {
autoPageTurningTimer!.cancel();

View File

@@ -183,6 +183,14 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
),
),
const SizedBox(width: 8),
if (shouldShowChapterComments())
Tooltip(
message: "Chapter Comments".tl,
child: IconButton(
icon: const Icon(Icons.comment),
onPressed: openChapterComments,
),
),
Tooltip(
message: "Settings".tl,
child: IconButton(
@@ -605,7 +613,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
}
var (imageIndex, data) = result;
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);
}
@@ -616,7 +625,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
}
var (imageIndex, data) = result;
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);
}
@@ -650,6 +660,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
if (key == "quickCollectImage") {
addDragListener();
}
if (key == "showChapterComments") {
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() {
if (context.reader.widget.chapters == null) return const SizedBox();
switch (showFloatingButtonValue) {

View File

@@ -49,7 +49,10 @@ class _SearchPageState extends State<SearchPage> {
void search([String? text]) {
if (aggregatedSearch) {
context
.to(() => AggregatedSearchPage(keyword: text ?? controller.text))
.to(
() => AggregatedSearchPage(keyword: text ?? controller.text),
iosFullScreenGesture: false,
)
.then((_) => update());
} else {
context
@@ -59,6 +62,7 @@ class _SearchPageState extends State<SearchPage> {
sourceKey: searchTarget,
options: options,
),
iosFullScreenGesture: false,
)
.then((_) => update());
}

View File

@@ -282,7 +282,7 @@ class _ReaderSettingsState extends State<ReaderSettings> {
).toSliver(),
_CallbackSetting(
title: "Custom Image Processing".tl,
callback: () => context.to(() => _CustomImageProcessing()),
callback: () => context.to(() => _CustomImageProcessing(), iosFullScreenGesture: false),
actionTitle: "Edit".tl,
).toSliver(),
_SliderSetting(
@@ -303,6 +303,15 @@ class _ReaderSettingsState extends State<ReaderSettings> {
comicId: isEnabledSpecificSettings ? widget.comicId : null,
comicSource: isEnabledSpecificSettings ? widget.comicSource : null,
).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(),
],
);
}

View File

@@ -252,9 +252,10 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
if (!App.isIOS) {
return;
}
if (event.position.dx < 20) {
gestureRecognizer.addPointer(event);
if (currentPage == -1) {
return;
}
gestureRecognizer.addPointer(event);
}
Widget buildLeft() {

View File

@@ -478,10 +478,11 @@ packages:
flutter_to_debian:
dependency: "direct dev"
description:
name: flutter_to_debian
sha256: d23534407334b331ce20fbaa8395b9ecc255d0c047136b8998715f36933ee696
url: "https://pub.dev"
source: hosted
path: "."
ref: HEAD
resolved-ref: "3777c91b6b1cc0b7c03357c67ca216d4313c3db5"
url: "https://github.com/venera-app/flutter_to_debian.git"
source: git
version: "2.0.2"
flutter_web_plugins:
dependency: transitive
@@ -1126,10 +1127,10 @@ packages:
dependency: "direct main"
description:
name: zip_flutter
sha256: c4d5a34c5803def866bc550926bb16fe89717c9b7304695d5b2ede30964eb8a8
sha256: baecf8deb6bf53a50e5ab513707ab56cc0c25f5b43333aa56ef562e8e7057357
url: "https://pub.dev"
source: hosted
version: "0.0.12"
version: "0.0.13"
sdks:
dart: ">=3.8.0 <4.0.0"
flutter: ">=3.35.5"
flutter: ">=3.35.7"

View File

@@ -2,11 +2,11 @@ name: venera
description: "A comic app."
publish_to: 'none'
version: 1.5.3+153
version: 1.6.0+160
environment:
sdk: '>=3.8.0 <4.0.0'
flutter: 3.35.5
flutter: 3.35.7
dependencies:
flutter:
@@ -53,7 +53,7 @@ dependencies:
sliver_tools: ^0.2.12
flutter_file_dialog: ^3.0.2
file_selector: ^1.0.3
zip_flutter: ^0.0.12
zip_flutter: ^0.0.13
lodepng_flutter:
git:
url: https://github.com/venera-app/lodepng_flutter
@@ -93,7 +93,9 @@ dev_dependencies:
sdk: flutter
flutter_lints: ^5.0.0
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
flutter: