Compare commits

...

14 Commits

Author SHA1 Message Date
ynyx631
068d6148ad Update main.yml 2025-11-29 16:50:26 +08:00
ynyx631
0b261f81ba Update version code 2025-11-29 15:08:25 +08:00
nyne
781ff2553d Merge pull request #649 from venera-app/feat/comment-blocking
feat: add comment keyword blocking functionality
2025-11-29 15:04:51 +08:00
ynyx631
0ce18cd738 Merge pull request #650 from venera-app/fix/local-search-menu
fix: enable multi-select actions in local comics search mode
2025-11-29 15:04:41 +08:00
40ef8a63b0 fix: enable multi-select actions in local comics search mode 2025-11-29 15:00:30 +08:00
053293839e flutter 3.38.3 2025-11-29 14:43:15 +08:00
Pacalini
f0be40c6d7 feat: skip sync setting (#563)
* feat: skip sync setting

* fix: upload origin data if nothing to skip

* sync: optimize text
2025-11-29 14:21:56 +08:00
Pacalini
da5b64abb0 interceptor: mask log (#618) 2025-11-29 14:21:30 +08:00
Y-Ymeow
7e3addf7a6 Enhance Cloudflare challenge detection logic (#619)
添加了验证body内的,防止一些网站的漏判
2025-11-29 14:20:02 +08:00
boa
b9c06779ad Fix landscape reader layout and wrap long settings labels (#640)
* fix: handle mobile landscape safe area #604

* fix: adjust reader toolbars safe area

* fix: adjust multi-image reader layout after orientation change

* fix: item titles not fully displayed
2025-11-29 14:19:43 +08:00
RuriNyan
7e928d2c9c Optimize iOS full-screen back gesture implementation (#643)
* Optimize iOS full-screen back gesture implementation

- Fix #613 and #617

* Fix setting page
2025-11-29 14:18:44 +08:00
RuriNyan
b3239757a8 Add encryptAes for js_engine (#645) 2025-11-29 14:18:18 +08:00
ynyx631
bdaa10fa06 Merge pull request #602 from venera-app/update-altstore-20251101-075020
Update AltStore source with latest release
2025-11-02 15:58:06 +08:00
GitHub Action
4296768c8d Updated source with latest release 2025-11-01 07:50:20 +00:00
39 changed files with 830 additions and 468 deletions

View File

@@ -84,6 +84,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- run: rm -rf /opt/hostedtoolcache
- uses: subosito/flutter-action@v2
with:
channel: "stable"

View File

@@ -13,15 +13,15 @@
"bundleIdentifier": "com.github.wgh136.venera",
"developerName": "wgh136",
"subtitle": "A comic reader that supports reading local and network comics",
"version": "1.5.3",
"versionDate": "2025-10-13",
"versionDescription": "1. Fix an issue where the app freezes after swiping back on Android. 544\r\n2. Enable minification when building for Android. 547\r\n3. Prevent the app from creating an archive download task when the archive URL is an empty string.",
"downloadURL": "https://github.com/venera-app/venera/releases/download/v1.5.3/venera-ios-1.5.3%2B153.ipa",
"version": "1.6.0",
"versionDate": "2025-11-01",
"versionDescription": "What's Changed\r\n* Update AltStore source with latest release by @github-actions[bot] in https://github.com/venera-app/venera/pull/559\r\n* \u8c03\u6574\u591a\u6536\u85cf\u5939\u6f2b\u753b\u6e90\u7684\u6536\u85cf\u72b6\u6001\u663e\u793a\u903b\u8f91 by @Ftbom in https://github.com/venera-app/venera/pull/571\r\n* Enhance onResponse to support Future and validate result type by @wgh136 in https://github.com/venera-app/venera/pull/574\r\n* [iOS] Enable full screen swipe back gesture by @liulifox233 in https://github.com/venera-app/venera/pull/575\r\n* [linux] Fix linux nhentai cover image by @4b1tQu4ntN3k0 in https://github.com/venera-app/venera/pull/578\r\n* feat: \u652f\u6301\u8fc7\u6ee4\u9605\u8bfb\u5b8c\u6210\u60c5\u51b5 by @luckyray-fan in https://github.com/venera-app/venera/pull/582\r\n* Fix chinese character issue when compressing files. Close 565 by @ynyx631 in https://github.com/venera-app/venera/pull/583\r\n* Add support for ArrayBuffer to showInputDialog. by @wgh136 in https://github.com/venera-app/venera/pull/585\r\n* Added support for localstorage when logging in via webview. by @wgh136 in https://github.com/venera-app/venera/pull/586\r\n* Fix the issue of the comic list loading infinitely. Close 584 by @ynyx631 in https://github.com/venera-app/venera/pull/588\r\n* Save data when mark all as read by @lings03 in https://github.com/venera-app/venera/pull/592\r\n* Chapter comments. by @lings03 in https://github.com/venera-app/venera/pull/593\r\n* Optimize favorite page and home page. by @lings03 in https://github.com/venera-app/venera/pull/594\r\n* Update version code by @wgh136 in https://github.com/venera-app/venera/pull/596\r\n* Fix missing depends in deb package. Close 587 by @wgh136 in https://github.com/venera-app/venera/pull/597\r\n* feat: \u672c\u5730\u6536\u85cf\u641c\u7d22\u652f\u6301\u8f6c\u5c0f\u5199\u5339\u914d by @luckyray-fan in https://github.com/venera-app/venera/pull/598\r\n* Fix editor page gesture confict by @liulifox233 in https://github.com/venera-app/venera/pull/600\r \nNew Contributors\r\n* @github-actions[bot] made their first contribution in https://github.com/venera-app/venera/pull/559\r\n* @Ftbom made their first contribution in https://github.com/venera-app/venera/pull/571\r\n* @liulifox233 made their first contribution in https://github.com/venera-app/venera/pull/575\r\n* @4b1tQu4ntN3k0 made their first contribution in https://github.com/venera-app/venera/pull/578\r\n* @ynyx631 made their first contribution in https://github.com/venera-app/venera/pull/583\r \nFull Changelog: https://github.com/venera-app/venera/compare/v1.5.3...v1.6.0",
"downloadURL": "https://github.com/venera-app/venera/releases/download/v1.6.0/venera-ios-1.6.0%2B160.ipa",
"localizedDescription": "A comic reader that supports reading local and network comics",
"iconURL": "https://raw.githubusercontent.com/venera-app/venera/master/assets/app_icon.png",
"tintColor": "#0784FC",
"category": "utilities",
"size": 15047841,
"size": 15064741,
"appPermissions": {
"entitlements": [
"application-identifier",
@@ -39,6 +39,13 @@
}
},
"versions": [
{
"version": "1.6.0",
"date": "2025-11-01",
"localizedDescription": "What's Changed\r\n* Update AltStore source with latest release by @github-actions[bot] in https://github.com/venera-app/venera/pull/559\r\n* \u8c03\u6574\u591a\u6536\u85cf\u5939\u6f2b\u753b\u6e90\u7684\u6536\u85cf\u72b6\u6001\u663e\u793a\u903b\u8f91 by @Ftbom in https://github.com/venera-app/venera/pull/571\r\n* Enhance onResponse to support Future and validate result type by @wgh136 in https://github.com/venera-app/venera/pull/574\r\n* [iOS] Enable full screen swipe back gesture by @liulifox233 in https://github.com/venera-app/venera/pull/575\r\n* [linux] Fix linux nhentai cover image by @4b1tQu4ntN3k0 in https://github.com/venera-app/venera/pull/578\r\n* feat: \u652f\u6301\u8fc7\u6ee4\u9605\u8bfb\u5b8c\u6210\u60c5\u51b5 by @luckyray-fan in https://github.com/venera-app/venera/pull/582\r\n* Fix chinese character issue when compressing files. Close 565 by @ynyx631 in https://github.com/venera-app/venera/pull/583\r\n* Add support for ArrayBuffer to showInputDialog. by @wgh136 in https://github.com/venera-app/venera/pull/585\r\n* Added support for localstorage when logging in via webview. by @wgh136 in https://github.com/venera-app/venera/pull/586\r\n* Fix the issue of the comic list loading infinitely. Close 584 by @ynyx631 in https://github.com/venera-app/venera/pull/588\r\n* Save data when mark all as read by @lings03 in https://github.com/venera-app/venera/pull/592\r\n* Chapter comments. by @lings03 in https://github.com/venera-app/venera/pull/593\r\n* Optimize favorite page and home page. by @lings03 in https://github.com/venera-app/venera/pull/594\r\n* Update version code by @wgh136 in https://github.com/venera-app/venera/pull/596\r\n* Fix missing depends in deb package. Close 587 by @wgh136 in https://github.com/venera-app/venera/pull/597\r\n* feat: \u672c\u5730\u6536\u85cf\u641c\u7d22\u652f\u6301\u8f6c\u5c0f\u5199\u5339\u914d by @luckyray-fan in https://github.com/venera-app/venera/pull/598\r\n* Fix editor page gesture confict by @liulifox233 in https://github.com/venera-app/venera/pull/600\r \nNew Contributors\r\n* @github-actions[bot] made their first contribution in https://github.com/venera-app/venera/pull/559\r\n* @Ftbom made their first contribution in https://github.com/venera-app/venera/pull/571\r\n* @liulifox233 made their first contribution in https://github.com/venera-app/venera/pull/575\r\n* @4b1tQu4ntN3k0 made their first contribution in https://github.com/venera-app/venera/pull/578\r\n* @ynyx631 made their first contribution in https://github.com/venera-app/venera/pull/583\r \nFull Changelog: https://github.com/venera-app/venera/compare/v1.5.3...v1.6.0",
"downloadURL": "https://github.com/venera-app/venera/releases/download/v1.6.0/venera-ios-1.6.0%2B160.ipa",
"size": 15064741
},
{
"version": "1.5.3",
"date": "2025-10-13",
@@ -76,6 +83,16 @@
"tintColor": "#0784FC",
"title": "v1.5.3 - Venera 13/10/25",
"url": "https://github.com/venera-app/venera/releases/tag/v1.5.3"
},
{
"appID": "com.github.wgh136.venera",
"caption": "Update of Venera just got released!",
"date": "2025-11-01T07:31:38Z",
"identifier": "release-v1.6.0",
"notify": true,
"tintColor": "#0784FC",
"title": "v1.6.0 - Venera 01/11/25",
"url": "https://github.com/venera-app/venera/releases/tag/v1.6.0"
}
]
}

View File

@@ -190,6 +190,21 @@ let Convert = {
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @returns {ArrayBuffer}
*/
encryptAesEcb: (value, key) => {
return sendMessage({
method: "convert",
type: "aes-ecb",
value: value,
key: key,
isEncode: true
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
@@ -205,6 +220,23 @@ let Convert = {
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @param {ArrayBuffer} iv
* @returns {ArrayBuffer}
*/
encryptAesCbc: (value, key, iv) => {
return sendMessage({
method: "convert",
type: "aes-cbc",
value: value,
key: key,
iv: iv,
isEncode: true
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
@@ -225,20 +257,58 @@ let Convert = {
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @param {ArrayBuffer} iv
* @param {number} blockSize
* @returns {ArrayBuffer}
*/
decryptAesCfb: (value, key, blockSize) => {
encryptAesCfb: (value, key, iv, blockSize) => {
return sendMessage({
method: "convert",
type: "aes-cfb",
value: value,
key: key,
iv: iv,
blockSize: blockSize,
isEncode: true
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @param {ArrayBuffer} iv
* @param {number} blockSize
* @returns {ArrayBuffer}
*/
decryptAesCfb: (value, key, iv, blockSize) => {
return sendMessage({
method: "convert",
type: "aes-cfb",
value: value,
key: key,
iv: iv,
blockSize: blockSize,
isEncode: false
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @param {number} blockSize
* @returns {ArrayBuffer}
*/
encryptAesOfb: (value, key, blockSize) => {
return sendMessage({
method: "convert",
type: "aes-ofb",
value: value,
key: key,
blockSize: blockSize,
isEncode: true
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
@@ -395,9 +465,10 @@ let Network = {
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @param {Object} extra - Extra options to pass to the interceptor.
* @returns {Promise<{status: number, headers: {}, body: ArrayBuffer}>} The response from the request.
*/
async fetchBytes(method, url, headers, data) {
async fetchBytes(method, url, headers, data, extra) {
let result = await sendMessage({
method: 'http',
http_method: method,
@@ -405,6 +476,7 @@ let Network = {
url: url,
headers: headers,
data: data,
extra: extra,
});
if (result.error) {
@@ -420,15 +492,17 @@ let Network = {
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @param {Object} extra - Extra options to pass to the interceptor.
* @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request.
*/
async sendRequest(method, url, headers, data) {
async sendRequest(method, url, headers, data, extra) {
let result = await sendMessage({
method: 'http',
http_method: method,
url: url,
headers: headers,
data: data,
extra: extra,
});
if (result.error) {
@@ -442,10 +516,11 @@ let Network = {
* Sends an HTTP GET request.
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param {Object} extra - Extra options to pass to the interceptor.
* @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request.
*/
async get(url, headers) {
return this.sendRequest('GET', url, headers);
async get(url, headers, extra) {
return this.sendRequest('GET', url, headers, extra);
},
/**
@@ -453,10 +528,11 @@ let Network = {
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @param {Object} extra - Extra options to pass to the interceptor.
* @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request.
*/
async post(url, headers, data) {
return this.sendRequest('POST', url, headers, data);
async post(url, headers, data, extra) {
return this.sendRequest('POST', url, headers, data, extra);
},
/**
@@ -464,10 +540,11 @@ let Network = {
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @param {Object} extra - Extra options to pass to the interceptor.
* @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request.
*/
async put(url, headers, data) {
return this.sendRequest('PUT', url, headers, data);
async put(url, headers, data, extra) {
return this.sendRequest('PUT', url, headers, data, extra);
},
/**
@@ -475,20 +552,22 @@ let Network = {
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @param {Object} extra - Extra options to pass to the interceptor.
* @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request.
*/
async patch(url, headers, data) {
return this.sendRequest('PATCH', url, headers, data);
async patch(url, headers, data, extra) {
return this.sendRequest('PATCH', url, headers, data, extra);
},
/**
* Sends an HTTP DELETE request.
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param {Object} extra - Extra options to pass to the interceptor.
* @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request.
*/
async delete(url, headers) {
return this.sendRequest('DELETE', url, headers);
async delete(url, headers, extra) {
return this.sendRequest('DELETE', url, headers, extra);
},
/**

View File

@@ -103,6 +103,7 @@
"Show favorite status on comic tile": "在漫画缩略图上显示收藏状态",
"Show history on comic tile": "在漫画缩略图上显示历史记录",
"Keyword blocking": "关键词屏蔽",
"Comment keyword blocking": "评论关键词屏蔽",
"Tap to turn Pages": "点击翻页",
"Page animation": "页面动画",
"Reading mode": "阅读模式",
@@ -201,6 +202,10 @@
"Sync Data": "同步数据",
"Syncing Data": "正在同步数据",
"Data Sync": "数据同步",
"Skip Setting Fields": "跳过设置项",
"Skip Setting Fields (Optional)": "跳过设置项(可选)",
"When sync data, skip certain setting fields, which means these won't be uploaded / override.": "同步时跳过指定设置项,这些项不会被上传或覆盖。",
"See source code for available fields.": "可用的设置项名称详见源码。",
"Quick Favorite": "快速收藏",
"Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹",
"Added": "已添加",
@@ -525,6 +530,7 @@
"Show favorite status on comic tile": "在漫畫縮圖上顯示收藏狀態",
"Show history on comic tile": "在漫畫縮圖上顯示歷史記錄",
"Keyword blocking": "關鍵字封鎖",
"Comment keyword blocking": "評論關鍵字封鎖",
"Tap to turn Pages": "點擊翻頁",
"Page animation": "頁面動畫",
"Reading mode": "閱讀模式",
@@ -624,6 +630,10 @@
"Sync Data": "同步資料",
"Syncing Data": "正在同步資料",
"Data Sync": "資料同步",
"Skip Setting Fields": "跳過設定項",
"Skip Setting Fields (Optional)": "跳過設定項(可選)",
"When sync data, skip certain setting fields, which means these won't be uploaded / override.": "同步時跳過指定設定項,這些項不會被上傳或覆寫。",
"See source code for available fields.": "可用的設定項名稱詳見源碼。",
"Quick Favorite": "快速收藏",
"Long press on the favorite button to quickly add to this folder": "長按收藏按鈕快速添加到這個資料夾",
"Added": "已添加",

View File

@@ -172,6 +172,16 @@ class NaviPaneState extends State<NaviPane>
@override
Widget build(BuildContext context) {
onRebuild(context);
final mq = MediaQuery.of(context);
final sideInsets =
(App.isMobile && mq.orientation == Orientation.landscape)
? EdgeInsets.only(
left: math.max(
mq.viewPadding.left, mq.systemGestureInsets.left),
right: math.max(
mq.viewPadding.right, mq.systemGestureInsets.right),
)
: EdgeInsets.zero;
return _NaviPopScope(
action: () {
if (App.mainNavigatorKey!.currentState!.canPop()) {
@@ -185,7 +195,7 @@ class NaviPaneState extends State<NaviPane>
animation: controller,
builder: (context, child) {
final value = controller.value;
return Stack(
Widget content = Stack(
children: [
Positioned(
left: _kFoldedSideBarWidth * ((value - 2.0).clamp(-1.0, 0.0)),
@@ -202,6 +212,13 @@ class NaviPaneState extends State<NaviPane>
),
],
);
if (sideInsets != EdgeInsets.zero) {
content = Padding(
padding: sideInsets,
child: content,
);
}
return content;
},
),
);

View File

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

View File

@@ -20,7 +20,6 @@ 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);
@@ -50,9 +49,6 @@ class AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin{
@override
final bool enableIOSGesture;
@override
final bool iosFullScreenPopGesture;
@override
final bool preventRebuild;
}
@@ -79,8 +75,6 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
bool get enableIOSGesture;
bool get iosFullScreenPopGesture;
bool get preventRebuild;
Widget? _child;
@@ -140,7 +134,6 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
gestureWidth: _kBackGestureWidth,
enabledCallback: () => _isPopGestureEnabled<T>(this),
onStartPopGesture: () => _startPopGesture(this),
fullScreen: iosFullScreenPopGesture,
child: child,
)
: child);
@@ -210,32 +203,41 @@ class IOSBackGestureController {
}
class IOSBackGestureDetector extends StatefulWidget {
const IOSBackGestureDetector(
{required this.enabledCallback,
required this.child,
required this.gestureWidth,
required this.onStartPopGesture,
this.fullScreen = false,
super.key});
const IOSBackGestureDetector({
required this.enabledCallback,
required this.child,
required this.gestureWidth,
required this.onStartPopGesture,
super.key,
});
final double gestureWidth;
final bool Function() enabledCallback;
final IOSBackGestureController Function() onStartPopGesture;
final Widget child;
final bool fullScreen;
@override
State<IOSBackGestureDetector> createState() => _IOSBackGestureDetectorState();
}
class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
IOSBackGestureController? _backGestureController;
late _BackSwipeRecognizer _recognizer;
late HorizontalDragGestureRecognizer _recognizer;
@override
void initState() {
super.initState();
_recognizer = _BackSwipeRecognizer(
debugOwner: this,
gestureWidth: widget.gestureWidth,
isPointerInHorizontal: _isPointerInHorizontalScrollable,
onStart: _handleDragStart,
onUpdate: _handleDragUpdate,
onEnd: _handleDragEnd,
onCancel: _handleDragCancel,
);
}
@override
void dispose() {
@@ -243,115 +245,209 @@ class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
super.dispose();
}
@override
void initState() {
super.initState();
_recognizer = HorizontalDragGestureRecognizer(debugOwner: this)
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
}
@override
Widget build(BuildContext context) {
var dragAreaWidth = Directionality.of(context) == TextDirection.ltr
? 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,
gestureListener,
],
return RawGestureDetector(
behavior: HitTestBehavior.translucent,
gestures: {
_BackSwipeRecognizer: GestureRecognizerFactoryWithHandlers<_BackSwipeRecognizer>(
() => _recognizer,
(instance) {
instance.gestureWidth = widget.gestureWidth;
},
),
},
child: widget.child,
);
}
void _handlePointerDown(PointerDownEvent event) {
if (!widget.enabledCallback()) return;
if (widget.fullScreen && _isPointerOverHorizontalScrollable(event)) {
return;
}
_recognizer.addPointer(event);
}
void _handleDragCancel() {
assert(mounted);
_backGestureController?.dragEnd(0.0);
_backGestureController = null;
}
double _convertToLogical(double value) {
switch (Directionality.of(context)) {
case TextDirection.rtl:
return -value;
case TextDirection.ltr:
return value;
}
}
void _handleDragEnd(DragEndDetails details) {
assert(mounted);
assert(_backGestureController != null);
_backGestureController!.dragEnd(_convertToLogical(
details.velocity.pixelsPerSecond.dx / context.size!.width));
_backGestureController = null;
}
void _handleDragStart(DragStartDetails details) {
assert(mounted);
assert(_backGestureController == null);
_backGestureController = widget.onStartPopGesture();
}
void _handleDragUpdate(DragUpdateDetails details) {
assert(mounted);
assert(_backGestureController != null);
_backGestureController!.dragUpdate(
_convertToLogical(details.primaryDelta! / context.size!.width));
}
bool _isPointerOverHorizontalScrollable(PointerDownEvent event) {
bool _isPointerInHorizontalScrollable(Offset globalPosition) {
final HitTestResult result = HitTestResult();
WidgetsBinding.instance.hitTest(result, event.position);
final binding = WidgetsBinding.instance;
binding.hitTestInView(result, globalPosition, binding.platformDispatcher.implicitView!.viewId);
for (final entry in result.path) {
final target = entry.target;
if (target is RenderViewport) {
if (_isAxisHorizontal(target.axisDirection)) {
if (target.axisDirection == AxisDirection.left ||
target.axisDirection == AxisDirection.right) {
return true;
}
} else if (target is RenderSliver) {
if (_isAxisHorizontal(target.constraints.axisDirection)) {
}
else if (target is RenderSliver) {
if (target.constraints.axisDirection == AxisDirection.left ||
target.constraints.axisDirection == AxisDirection.right) {
return true;
}
}
else if (target.runtimeType.toString() == '_RenderSingleChildViewport') {
try {
final dynamic renderObject = target;
if (renderObject.axis == Axis.horizontal) {
return true;
}
} catch (e) {
// protected
}
}
else if (target is RenderEditable) {
return true;
}
}
return false;
}
bool _isAxisHorizontal(AxisDirection direction) {
return direction == AxisDirection.left || direction == AxisDirection.right;
void _handleDragStart(DragStartDetails details) {
if (!widget.enabledCallback()) return;
if (mounted && _backGestureController == null) {
_backGestureController = widget.onStartPopGesture();
}
}
void _handleDragUpdate(DragUpdateDetails details) {
if (mounted && _backGestureController != null) {
_backGestureController!.dragUpdate(
_convertToLogical(details.primaryDelta! / context.size!.width));
}
}
void _handleDragEnd(DragEndDetails details) {
if (mounted && _backGestureController != null) {
_backGestureController!.dragEnd(_convertToLogical(
details.velocity.pixelsPerSecond.dx / context.size!.width));
_backGestureController = null;
}
}
void _handleDragCancel() {
if (mounted && _backGestureController != null) {
_backGestureController?.dragEnd(0.0);
_backGestureController = null;
}
}
double _convertToLogical(double value) {
switch (Directionality.of(context)) {
case TextDirection.rtl: return -value;
case TextDirection.ltr: return value;
}
}
}
class _BackSwipeRecognizer extends OneSequenceGestureRecognizer {
_BackSwipeRecognizer({
required this.isPointerInHorizontal,
required this.gestureWidth,
required this.onStart,
required this.onUpdate,
required this.onEnd,
required this.onCancel,
super.debugOwner,
});
final bool Function(Offset globalPosition) isPointerInHorizontal;
double gestureWidth;
final ValueSetter<DragStartDetails> onStart;
final ValueSetter<DragUpdateDetails> onUpdate;
final ValueSetter<DragEndDetails> onEnd;
final VoidCallback onCancel;
Offset? _startGlobal;
bool _accepted = false;
bool _startedInHorizontal = false;
bool _startedNearLeftEdge = false;
VelocityTracker? _velocityTracker;
static const double _minDistance = 5.0;
@override
void addPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer);
_startGlobal = event.position;
_accepted = false;
_startedInHorizontal = isPointerInHorizontal(event.position);
_startedNearLeftEdge = event.position.dx <= gestureWidth;
_velocityTracker = VelocityTracker.withKind(event.kind);
_velocityTracker?.addPosition(event.timeStamp, event.position);
}
@override
void handleEvent(PointerEvent event) {
if (event is PointerMoveEvent || event is PointerUpEvent) {
_velocityTracker?.addPosition(event.timeStamp, event.position);
}
if (event is PointerMoveEvent) {
if (_startGlobal == null) return;
final delta = event.position - _startGlobal!;
final dx = delta.dx;
final dy = delta.dy.abs();
if (!_accepted) {
if (delta.distance < _minDistance) return;
final isRight = dx > 0;
final isHorizontal = dx.abs() > dy * 1.5;
final bool eligible = _startedNearLeftEdge || (!_startedInHorizontal);
if (isRight && isHorizontal && eligible) {
_accepted = true;
resolve(GestureDisposition.accepted);
onStart(DragStartDetails(
globalPosition: _startGlobal!,
localPosition: event.localPosition
));
} else {
resolve(GestureDisposition.rejected);
stopTrackingPointer(event.pointer);
_startGlobal = null;
_velocityTracker = null;
}
}
if (_accepted) {
onUpdate(DragUpdateDetails(
globalPosition: event.position,
localPosition: event.localPosition,
primaryDelta: event.delta.dx,
delta: event.delta,
));
}
} else if (event is PointerUpEvent) {
if (_accepted) {
final Velocity velocity = _velocityTracker?.getVelocity() ?? Velocity.zero;
onEnd(DragEndDetails(
velocity: velocity,
primaryVelocity: velocity.pixelsPerSecond.dx
));
}
_reset();
} else if (event is PointerCancelEvent) {
if (_accepted) {
onCancel();
}
_reset();
}
}
void _reset() {
stopTrackingPointer(0);
_accepted = false;
_startGlobal = null;
_startedInHorizontal = false;
_startedNearLeftEdge = false;
_velocityTracker = null;
}
@override
String get debugDescription => 'IOSBackSwipe';
@override
void didStopTrackingLastPointer(int pointer) {}
}
class SlidePageTransitionBuilder extends PageTransitionsBuilder {
@@ -389,4 +485,4 @@ class SlidePageTransitionBuilder extends PageTransitionsBuilder {
),
);
}
}
}

View File

@@ -23,9 +23,26 @@ class Appdata with Init {
}
_isSavingData = true;
try {
var data = jsonEncode(toJson());
var futures = <Future>[];
var json = toJson();
var data = jsonEncode(json);
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
await file.writeAsString(data);
futures.add(file.writeAsString(data));
var disableSyncFields = json["settings"]["disableSyncFields"] as String;
if (disableSyncFields.isNotEmpty){
var json4sync = jsonDecode(data);
List<String> customDisableSync = splitField(disableSyncFields);
for (var field in customDisableSync) {
json4sync["settings"].remove(field);
}
var data4sync = jsonEncode(json4sync);
var file4sync = File(FilePath.join(App.dataPath, 'syncdata.json'));
futures.add(file4sync.writeAsString(data4sync));
}
await Future.wait(futures);
} finally {
_isSavingData = false;
}
@@ -59,20 +76,33 @@ class Appdata with Init {
return {'settings': settings._data, 'searchHistory': searchHistory};
}
List<String> splitField(String merged) {
return merged
.split(',')
.map((field) => field.trim())
.where((field) => field.isNotEmpty)
.toList();
}
/// Following fields are related to device-specific data and should not be synced.
static const _disableSync = [
"proxy",
"authorizationRequired",
"customImageProcessing",
"webdav",
"disableSyncFields",
];
/// Sync data from another device
void syncData(Map<String, dynamic> data) {
if (data['settings'] is Map) {
var settings = data['settings'] as Map<String, dynamic>;
List<String> customDisableSync = splitField(this.settings["disableSyncFields"] as String);
for (var key in settings.keys) {
if (!_disableSync.contains(key)) {
if (!_disableSync.contains(key) &&
!customDisableSync.contains(key)) {
this.settings[key] = settings[key];
}
}
@@ -150,6 +180,7 @@ class Settings with ChangeNotifier {
'showFavoriteStatusOnTile': true,
'showHistoryStatusOnTile': false,
'blockedWords': [],
'blockedCommentWords': [],
'defaultSearchTarget': null,
'autoPageTurningInterval': 5, // in seconds
'readerMode': 'galleryLeftToRight', // values of [ReaderMode]
@@ -166,6 +197,7 @@ class Settings with ChangeNotifier {
'checkUpdateOnStart': false,
'limitImageWidth': true,
'webdav': [], // empty means not configured
"disableSyncFields": "", // "field1, field2, ..."
'dataVersion': 0,
'quickFavorite': null,
'enableTurnPageByVolumeKey': true,

View File

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

View File

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

View File

@@ -217,6 +217,7 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
try {
var headers = Map<String, dynamic>.from(req["headers"] ?? {});
var extra = Map<String, dynamic>.from(req["extra"] ?? {});
if (headers["user-agent"] == null && headers["User-Agent"] == null) {
headers["User-Agent"] = webUA;
}
@@ -244,7 +245,10 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
responseType: req["bytes"] == true
? ResponseType.bytes
: ResponseType.plain,
headers: headers));
headers: headers,
extra: extra,
)
);
} catch (e) {
error = e.toString();
}
@@ -436,83 +440,72 @@ mixin class _JSEngineApi {
return Uint8List.fromList(hmac.convert(value).bytes);
}
case "aes-ecb":
if (!isEncode) {
var key = data["key"];
var cipher = ECBBlockCipher(AESEngine());
cipher.init(
false,
KeyParameter(key),
var key = data["key"];
var cipher = ECBBlockCipher(AESEngine());
cipher.init(
isEncode,
KeyParameter(key),
);
var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return result;
}
return null;
return result;
case "aes-cbc":
if (!isEncode) {
var key = data["key"];
var iv = data["iv"];
var cipher = CBCBlockCipher(AESEngine());
cipher.init(false, ParametersWithIV(KeyParameter(key), iv));
var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return result;
var key = data["key"];
var iv = data["iv"];
var cipher = CBCBlockCipher(AESEngine());
cipher.init(isEncode, ParametersWithIV(KeyParameter(key), iv));
var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return null;
return result;
case "aes-cfb":
if (!isEncode) {
var key = data["key"];
var blockSize = data["blockSize"];
var cipher = CFBBlockCipher(AESEngine(), blockSize);
cipher.init(false, KeyParameter(key));
var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return result;
var key = data["key"];
var iv = data["iv"];
var blockSize = data["blockSize"];
var cipher = CFBBlockCipher(AESEngine(), blockSize);
cipher.init(isEncode, ParametersWithIV(KeyParameter(key), iv));
var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return null;
return result;
case "aes-ofb":
if (!isEncode) {
var key = data["key"];
var blockSize = data["blockSize"];
var cipher = OFBBlockCipher(AESEngine(), blockSize);
cipher.init(false, KeyParameter(key));
var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return result;
var key = data["key"];
var blockSize = data["blockSize"];
var cipher = OFBBlockCipher(AESEngine(), blockSize);
cipher.init(isEncode, KeyParameter(key));
var offset = 0;
var result = Uint8List(value.length);
while (offset < value.length) {
offset += cipher.processBlock(
value,
offset,
result,
offset,
);
}
return null;
return result;
case "rsa":
if (!isEncode) {
var key = data["key"];

View File

@@ -153,9 +153,7 @@ class LocalComic with HistoryMixin implements Comic {
),
author: subtitle,
tags: tags,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
)
);
}

View File

@@ -96,11 +96,28 @@ class MyLogInterceptor implements Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
const String headerMask = "********";
const String dataMask = "****** DATA_PROTECTED ******";
Log.info(
"Network",
"${options.method} ${options.uri}\n"
"headers:\n${options.headers}\n"
"data:\n${options.data}");
"headers:\n${
options.extra.containsKey("maskHeadersInLog")
? options.headers.map((key, value) =>
MapEntry(
key,
options.extra["maskHeadersInLog"].contains(key)
? headerMask
: value
))
: options.headers
}\n"
"data:\n${
options.extra["maskDataInLog"] == true
? dataMask
: options.data
}"
);
options.connectTimeout = const Duration(seconds: 15);
options.receiveTimeout = const Duration(seconds: 15);
options.sendTimeout = const Duration(seconds: 15);

View File

@@ -128,10 +128,15 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
var head =
await controller.evaluateJavascript("document.head.innerHTML") ??
"";
var body =
await controller.evaluateJavascript("document.body.innerHTML") ??
"";
Log.info("Cloudflare", "Checking head: $head");
var isChallenging = head.contains('#challenge-success-text') ||
head.contains("#challenge-error-text") ||
head.contains("#challenge-form");
head.contains("#challenge-form") ||
body.contains("challenge-platform") ||
body.contains("window._cf_chl_opt");
if (!isChallenging) {
Log.info(
"Cloudflare",
@@ -159,10 +164,14 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
void check(InAppWebViewController controller) async {
var head = await controller.evaluateJavascript(
source: "document.head.innerHTML") as String;
var body = await controller.evaluateJavascript(
source: "document.body.innerHTML") as String;
Log.info("Cloudflare", "Checking head: $head");
var isChallenging = head.contains('#challenge-success-text') ||
head.contains("#challenge-error-text") ||
head.contains("#challenge-form");
head.contains("#challenge-form") ||
body.contains("challenge-platform") ||
body.contains("window._cf_chl_opt");
if (!isChallenging) {
Log.info(
"Cloudflare",

View File

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

View File

@@ -115,9 +115,7 @@ abstract mixin class _ComicPageActions {
history: history ?? History.fromModel(model: comic, ep: 0, page: 0),
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

@@ -1,5 +1,18 @@
part of 'comic_page.dart';
bool _shouldBlockComment(Comment comment) {
var blockedWords = appdata.settings["blockedCommentWords"] as List;
if (blockedWords.isEmpty) return false;
var content = comment.content.toLowerCase();
for (var word in blockedWords) {
if (content.contains(word.toString().toLowerCase())) {
return true;
}
}
return false;
}
class CommentsPage extends StatefulWidget {
const CommentsPage({
super.key,
@@ -36,8 +49,9 @@ class _CommentsPageState extends State<CommentsPage> {
_loading = false;
});
} else if (mounted) {
var filteredComments = res.data.where((c) => !_shouldBlockComment(c)).toList();
setState(() {
_comments = res.data;
_comments = filteredComments;
_loading = false;
maxPage = res.subData;
});
@@ -54,8 +68,9 @@ class _CommentsPageState extends State<CommentsPage> {
if (res.error) {
context.showMessage(message: res.errorMessage ?? "Unknown Error");
} else {
var filteredComments = res.data.where((c) => !_shouldBlockComment(c)).toList();
setState(() {
_comments!.addAll(res.data);
_comments!.addAll(filteredComments);
_page++;
if (maxPage == null && res.data.isEmpty) {
maxPage = _page;

View File

@@ -21,7 +21,7 @@ class _CommentsPartState extends State<_CommentsPart> {
@override
void initState() {
comments = widget.comments;
comments = widget.comments.where((c) => !_shouldBlockComment(c)).toList();
super.initState();
}

View File

@@ -200,7 +200,6 @@ class _BodyState extends State<_Body> {
await ComicSourceManager().reload();
setState(() {});
}),
iosFullScreenGesture: false,
);
}

View File

@@ -574,9 +574,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
App.rootContext.to(() => ReaderWithLoading(
id: c.id,
sourceKey: c.sourceKey,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
)
);
},
),
@@ -589,9 +587,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
App.mainNavigatorKey?.currentContext?.to(() => ComicPage(
id: c.id,
sourceKey: c.sourceKey,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
)
);
},
),
@@ -693,9 +689,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
() => ReaderWithLoading(
id: c.id,
sourceKey: c.sourceKey,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
)
);
},
),
@@ -720,9 +714,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
cover: c.cover,
title: c.title,
heroID: heroID,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
)
);
} else {
App.mainNavigatorKey?.currentContext?.to(

View File

@@ -895,8 +895,7 @@ class _ImageFavoritesState extends State<ImageFavorites> {
borderRadius: BorderRadius.circular(8),
onTap: () {
context.to(
() => const ImageFavoritesPage(),
iosFullScreenGesture: false,
() => const ImageFavoritesPage()
);
},
child: Column(
@@ -1018,7 +1017,6 @@ class _ImageFavoritesState extends State<ImageFavorites> {
onTap: (text) {
context.to(
() => ImageFavoritesPage(initialKeyword: text),
iosFullScreenGesture: false,
);
},
);

View File

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

View File

@@ -243,9 +243,7 @@ class _ImageFavoritesPhotoViewState extends State<ImageFavoritesPhotoView> {
sourceKey: comic.sourceKey,
initialEp: ep,
initialPage: page,
),
enableIOSGesture: false,
iosFullScreenGesture: false,
)
);
},
),

View File

@@ -258,29 +258,41 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
else if (searchMode)
SliverAppbar(
leading: Tooltip(
message: "Cancel".tl,
message: multiSelectMode ? "Cancel".tl : "Cancel".tl,
child: IconButton(
icon: const Icon(Icons.close),
icon: multiSelectMode
? const Icon(Icons.close)
: const Icon(Icons.close),
onPressed: () {
setState(() {
searchMode = false;
keyword = "";
update();
});
if (multiSelectMode) {
setState(() {
multiSelectMode = false;
selectedComics.clear();
});
} else {
setState(() {
searchMode = false;
keyword = "";
update();
});
}
},
),
),
title: TextField(
autofocus: true,
decoration: InputDecoration(
hintText: "Search".tl,
border: InputBorder.none,
),
onChanged: (v) {
keyword = v;
update();
},
),
title: multiSelectMode
? Text(selectedComics.length.toString())
: TextField(
autofocus: true,
decoration: InputDecoration(
hintText: "Search".tl,
border: InputBorder.none,
),
onChanged: (v) {
keyword = v;
update();
},
),
actions: multiSelectMode ? selectActions : null,
),
SliverGridComics(
comics: comics,
@@ -344,6 +356,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
return PopScope(
canPop: !multiSelectMode && !searchMode,
onPopInvokedWithResult: (didPop, result) {
if (didPop) return;
if (multiSelectMode) {
setState(() {
multiSelectMode = false;

View File

@@ -1,5 +1,18 @@
part of 'reader.dart';
bool _shouldBlockComment(Comment comment) {
var blockedWords = appdata.settings["blockedCommentWords"] as List;
if (blockedWords.isEmpty) return false;
var content = comment.content.toLowerCase();
for (var word in blockedWords) {
if (content.contains(word.toString().toLowerCase())) {
return true;
}
}
return false;
}
class ChapterCommentsPage extends StatefulWidget {
const ChapterCommentsPage({
super.key,
@@ -44,8 +57,9 @@ class _ChapterCommentsPageState extends State<ChapterCommentsPage> {
_loading = false;
});
} else if (mounted) {
var filteredComments = res.data.where((c) => !_shouldBlockComment(c)).toList();
setState(() {
_comments = res.data;
_comments = filteredComments;
_loading = false;
maxPage = res.subData;
});
@@ -62,8 +76,9 @@ class _ChapterCommentsPageState extends State<ChapterCommentsPage> {
if (res.error) {
context.showMessage(message: res.errorMessage ?? "Unknown Error");
} else {
var filteredComments = res.data.where((c) => !_shouldBlockComment(c)).toList();
setState(() {
_comments!.addAll(res.data);
_comments!.addAll(filteredComments);
_page++;
if (maxPage == null && res.data.isEmpty) {
maxPage = _page;

View File

@@ -286,8 +286,9 @@ class _GalleryModeState extends State<_GalleryMode>
);
}
final viewportSize = MediaQuery.of(context).size;
return PhotoViewGalleryPageOptions.customChild(
childSize: reader.size * 2,
childSize: viewportSize,
controller: photoViewControllers[index],
minScale: PhotoViewComputedScale.contained * 1.0,
maxScale: PhotoViewComputedScale.covered * 10.0,

View File

@@ -166,40 +166,49 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
decoration: BoxDecoration(
color: context.colorScheme.surface.toOpacity(0.92),
border: Border(
bottom: BorderSide(color: Colors.grey.toOpacity(0.5), width: 0.5),
bottom: BorderSide(
color: Colors.grey.toOpacity(0.5),
width: 0.5,
),
),
),
child: Row(
children: [
const SizedBox(width: 8),
const BackButton(),
const SizedBox(width: 8),
Expanded(
child: Text(
context.reader.widget.name,
style: ts.s18,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
if (shouldShowChapterComments())
Tooltip(
message: "Chapter Comments".tl,
child: IconButton(
icon: const Icon(Icons.comment),
onPressed: openChapterComments,
child: Padding(
padding: EdgeInsets.only(
left: context.padding.left,
right: context.padding.right,
),
child: Row(
children: [
const SizedBox(width: 8),
const BackButton(),
const SizedBox(width: 8),
Expanded(
child: Text(
context.reader.widget.name,
style: ts.s18,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Tooltip(
message: "Settings".tl,
child: IconButton(
icon: const Icon(Icons.settings),
onPressed: openSetting,
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(
icon: const Icon(Icons.settings),
onPressed: openSetting,
),
),
),
const SizedBox(width: 8),
],
const SizedBox(width: 8),
],
),
),
),
);
@@ -520,7 +529,13 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
: null,
),
padding: EdgeInsets.only(bottom: context.padding.bottom),
child: child,
child: Padding(
padding: EdgeInsets.only(
left: context.padding.left,
right: context.padding.right,
),
child: child,
),
),
);
}
@@ -713,11 +728,12 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
}
Widget buildEpChangeButton() {
final extraWidth = context.padding.left + context.padding.right;
if (context.reader.widget.chapters == null) return const SizedBox();
switch (showFloatingButtonValue) {
case 0:
return Container(
width: 58,
width: 58 + extraWidth,
height: 58,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
@@ -735,7 +751,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
case -1:
case 1:
return SizedBox(
width: 58,
width: 58 + extraWidth,
height: 58,
child: Material(
color: Theme.of(context).colorScheme.primaryContainer,

View File

@@ -50,8 +50,7 @@ class _SearchPageState extends State<SearchPage> {
if (aggregatedSearch) {
context
.to(
() => AggregatedSearchPage(keyword: text ?? controller.text),
iosFullScreenGesture: false,
() => AggregatedSearchPage(keyword: text ?? controller.text)
)
.then((_) => update());
} else {
@@ -61,8 +60,7 @@ class _SearchPageState extends State<SearchPage> {
text: text ?? controller.text,
sourceKey: searchTarget,
options: options,
),
iosFullScreenGesture: false,
)
)
.then((_) => update());
}

View File

@@ -100,7 +100,7 @@ class _AppSettingsState extends State<AppSettings> {
title: "Export App Data".tl,
callback: () async {
var controller = showLoadingDialog(context);
var file = await exportAppData();
var file = await exportAppData(false);
await saveFile(filename: "data.venera", file: file);
controller.close();
},
@@ -353,6 +353,8 @@ class _WebdavSettingState extends State<_WebdavSetting> {
String url = "";
String user = "";
String pass = "";
String disableSync = "";
bool autoSync = true;
bool isTesting = false;
@@ -364,6 +366,9 @@ class _WebdavSettingState extends State<_WebdavSetting> {
if (appdata.settings['webdav'] is! List) {
appdata.settings['webdav'] = [];
}
if (appdata.settings['disableSyncFields'].trim().isNotEmpty) {
disableSync = appdata.settings['disableSyncFields'];
}
var configs = appdata.settings['webdav'] as List;
if (configs.whereType<String>().length != 3) {
return;
@@ -418,6 +423,56 @@ class _WebdavSettingState extends State<_WebdavSetting> {
onChanged: (value) => pass = value,
),
const SizedBox(height: 12),
TextField(
decoration: InputDecoration(
labelText: "Skip Setting Fields (Optional)".tl,
hintText: "field0, field1, field2, ...",
hintStyle: TextStyle(color: Theme.of(context).hintColor),
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(Icons.help_outline),
onPressed: () {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text("Skip Setting Fields".tl),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"When sync data, skip certain setting fields, which means these won't be uploaded / override.".tl,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Text(
"See source code for available fields.".tl,
),
),
Align(
alignment: Alignment.centerRight,
child: IconButton(
icon: const Icon(Icons.open_in_new),
onPressed: () {
launchUrlString("https://github.com/venera-app/venera/blob/b08f11f6ac49bd07d34b4fcde233ed07e86efbc9/lib/foundation/appdata.dart#L138");
},
),
),
],
),
],
),
),
);
},
),
),
controller: TextEditingController(text: disableSync),
onChanged: (value) => disableSync = value,
),
const SizedBox(height: 12),
ListTile(
leading: Icon(Icons.sync),
title: Text("Auto Sync Data".tl),
@@ -494,6 +549,7 @@ class _WebdavSettingState extends State<_WebdavSetting> {
}
appdata.settings['webdav'] = [url, user, pass];
appdata.settings['disableSyncFields'] = disableSync;
appdata.implicitData['webdavAutoSync'] = autoSync;
appdata.writeImplicitData();

View File

@@ -60,6 +60,10 @@ class _ExploreSettingsState extends State<ExploreSettings> {
title: "Keyword blocking".tl,
builder: () => const _ManageBlockingWordView(),
).toSliver(),
_PopupWindowSetting(
title: "Comment keyword blocking".tl,
builder: () => const _ManageBlockingCommentWordView(),
).toSliver(),
SelectSetting(
title: "Default Search Target".tl,
settingKey: "defaultSearchTarget",
@@ -250,4 +254,93 @@ Widget setSearchSourcesWidget() {
settingsIndex: "searchSources",
pages: pages,
);
}
class _ManageBlockingCommentWordView extends StatefulWidget {
const _ManageBlockingCommentWordView();
@override
State<_ManageBlockingCommentWordView> createState() =>
_ManageBlockingCommentWordViewState();
}
class _ManageBlockingCommentWordViewState extends State<_ManageBlockingCommentWordView> {
@override
Widget build(BuildContext context) {
assert(appdata.settings["blockedCommentWords"] is List);
return PopUpWidgetScaffold(
title: "Comment keyword blocking".tl,
tailing: [
TextButton.icon(
icon: const Icon(Icons.add),
label: Text("Add".tl),
onPressed: add,
),
],
body: ListView.builder(
itemCount: appdata.settings["blockedCommentWords"].length,
itemBuilder: (context, index) {
return ListTile(
title: Text(appdata.settings["blockedCommentWords"][index]),
trailing: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
appdata.settings["blockedCommentWords"].removeAt(index);
appdata.saveData();
setState(() {});
},
),
);
},
),
);
}
void add() {
showDialog(
context: App.rootContext,
builder: (context) {
var controller = TextEditingController();
String? error;
return StatefulBuilder(builder: (context, setState) {
return ContentDialog(
title: "Add keyword".tl,
content: TextField(
controller: controller,
decoration: InputDecoration(
border: const OutlineInputBorder(),
label: Text("Keyword".tl),
errorText: error,
),
onChanged: (s) {
if (error != null) {
setState(() {
error = null;
});
}
},
).paddingHorizontal(12),
actions: [
Button.filled(
onPressed: () {
if (appdata.settings["blockedCommentWords"]
.contains(controller.text)) {
setState(() {
error = "Keyword already exists".tl;
});
return;
}
appdata.settings["blockedCommentWords"].add(controller.text);
appdata.saveData();
this.setState(() {});
context.pop();
},
child: Text("Add".tl),
),
],
);
});
},
);
}
}

View File

@@ -282,7 +282,7 @@ class _ReaderSettingsState extends State<ReaderSettings> {
).toSliver(),
_CallbackSetting(
title: "Custom Image Processing".tl,
callback: () => context.to(() => _CustomImageProcessing(), iosFullScreenGesture: false),
callback: () => context.to(() => _CustomImageProcessing()),
actionTitle: "Edit".tl,
).toSliver(),
_SliderSetting(

View File

@@ -385,17 +385,16 @@ class _SliderSettingState extends State<_SliderSetting> {
: appdata.settings.getReaderSetting(
widget.comicId!,
widget.comicSource!,
widget.settingsIndex,
))
widget.settingsIndex,
))
.toDouble();
return ListTile(
title: Row(
children: [
Text(widget.title),
const Spacer(),
Text(value.toString(), style: ts.s12),
],
title: Text(
widget.title,
softWrap: true,
maxLines: 2,
),
trailing: Text(value.toString(), style: ts.s12),
subtitle: Slider(
value: value,
onChanged: (value) {

View File

@@ -1,6 +1,5 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
@@ -41,7 +40,7 @@ class SettingsPage extends StatefulWidget {
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> implements PopEntry {
class _SettingsPageState extends State<SettingsPage> {
int currentPage = -1;
ColorScheme get colors => Theme.of(context).colorScheme;
@@ -70,84 +69,14 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
Icons.bug_report,
];
double offset = 0;
late final HorizontalDragGestureRecognizer gestureRecognizer;
ModalRoute? _route;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final ModalRoute<dynamic>? nextRoute = ModalRoute.of(context);
if (nextRoute != _route) {
_route?.unregisterPopEntry(this);
_route = nextRoute;
_route?.registerPopEntry(this);
}
}
@override
void initState() {
currentPage = widget.initialPage;
gestureRecognizer = HorizontalDragGestureRecognizer(debugOwner: this)
..onUpdate = ((details) => setState(() => offset += details.delta.dx))
..onEnd = (details) async {
if (details.velocity.pixelsPerSecond.dx.abs() > 1 &&
details.velocity.pixelsPerSecond.dx >= 0) {
setState(() {
Future.delayed(const Duration(milliseconds: 300), () => offset = 0);
currentPage = -1;
});
} else if (offset > MediaQuery.of(context).size.width / 2) {
setState(() {
Future.delayed(const Duration(milliseconds: 300), () => offset = 0);
currentPage = -1;
});
} else {
int i = 10;
while (offset != 0) {
setState(() {
offset -= i;
i *= 10;
if (offset < 0) {
offset = 0;
}
});
await Future.delayed(const Duration(milliseconds: 10));
}
}
}
..onCancel = () async {
int i = 10;
while (offset != 0) {
setState(() {
offset -= i;
i *= 10;
if (offset < 0) {
offset = 0;
}
});
await Future.delayed(const Duration(milliseconds: 10));
}
};
super.initState();
}
@override
dispose() {
super.dispose();
gestureRecognizer.dispose();
_route?.unregisterPopEntry(this);
}
@override
Widget build(BuildContext context) {
if (currentPage != -1) {
canPop.value = false;
} else {
canPop.value = true;
}
return Material(
child: buildBody(),
);
@@ -209,55 +138,10 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
],
);
} else {
return LayoutBuilder(
builder: (context, constrains) {
return Stack(
children: [
Positioned.fill(child: buildLeft()),
Positioned(
left: offset,
width: constrains.maxWidth,
top: 0,
bottom: 0,
child: Listener(
onPointerDown: handlePointerDown,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.fastOutSlowIn,
switchOutCurve: Curves.fastOutSlowIn,
transitionBuilder: (child, animation) {
var tween = Tween<Offset>(
begin: const Offset(1, 0), end: const Offset(0, 0));
return SlideTransition(
position: tween.animate(animation),
child: child,
);
},
child: Material(
key: ValueKey(currentPage),
child: buildRight(),
),
),
),
)
],
);
},
);
return buildLeft();
}
}
void handlePointerDown(PointerDownEvent event) {
if (!App.isIOS) {
return;
}
if (currentPage == -1) {
return;
}
gestureRecognizer.addPointer(event);
}
Widget buildLeft() {
return Material(
child: Column(
@@ -334,7 +218,13 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
? const EdgeInsets.fromLTRB(8, 0, 8, 0)
: EdgeInsets.zero,
child: InkWell(
onTap: () => setState(() => currentPage = id),
onTap: () {
if (enableTwoViews) {
setState(() => currentPage = id);
} else {
context.to(() => _SettingsDetailPage(pageIndex: id));
}
},
child: content,
).paddingVertical(4),
);
@@ -348,8 +238,23 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
}
Widget buildRight() {
return switch (currentPage) {
-1 => const SizedBox(),
if (currentPage == -1) {
return const SizedBox();
}
return Navigator(
onGenerateRoute: (settings) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return _buildSettingsContent(currentPage);
},
transitionDuration: Duration.zero,
);
},
);
}
Widget _buildSettingsContent(int pageIndex) {
return switch (pageIndex) {
0 => const ExploreSettings(),
1 => const ReaderSettings(),
2 => const AppearanceSettings(),
@@ -362,26 +267,31 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
};
}
var canPop = ValueNotifier(true);
}
class _SettingsDetailPage extends StatelessWidget {
const _SettingsDetailPage({required this.pageIndex});
final int pageIndex;
@override
ValueListenable<bool> get canPopNotifier => canPop;
@override
void onPopInvokedWithResult(bool didPop, result) {
if (currentPage != -1) {
setState(() {
currentPage = -1;
});
}
Widget build(BuildContext context) {
return Material(
child: _buildPage(),
);
}
@override
void onPopInvoked(bool didPop) {
if (currentPage != -1) {
setState(() {
currentPage = -1;
});
}
Widget _buildPage() {
return switch (pageIndex) {
0 => const ExploreSettings(),
1 => const ReaderSettings(),
2 => const AppearanceSettings(),
3 => const LocalFavoritesSettings(),
4 => const AppSettings(),
5 => const NetworkSettings(),
6 => const AboutSettings(),
7 => const DebugPage(),
_ => throw UnimplementedError()
};
}
}

View File

@@ -15,7 +15,7 @@ import 'package:zip_flutter/zip_flutter.dart';
import 'io.dart';
Future<File> exportAppData() async {
Future<File> exportAppData([bool sync = true]) async {
var time = DateTime.now().millisecondsSinceEpoch ~/ 1000;
var cacheFilePath = FilePath.join(App.cachePath, '$time.venera');
var cacheFile = File(cacheFilePath);
@@ -27,7 +27,7 @@ Future<File> exportAppData() async {
var zipFile = ZipFile.open(cacheFilePath);
var historyFile = FilePath.join(dataPath, "history.db");
var localFavoriteFile = FilePath.join(dataPath, "local_favorite.db");
var appdata = FilePath.join(dataPath, "appdata.json");
var appdata = FilePath.join(dataPath, sync ? "syncdata.json" : "appdata.json");
var cookies = FilePath.join(dataPath, "cookie.db");
zipFile.addFile("history.db", historyFile);
zipFile.addFile("local_favorite.db", localFavoriteFile);

View File

@@ -130,7 +130,9 @@ class DataSync with ChangeNotifier {
try {
appdata.settings['dataVersion']++;
await appdata.saveData(false);
var data = await exportAppData();
var data = await exportAppData(
appdata.settings['disableSyncFields'].toString().isNotEmpty
);
var time =
(DateTime.now().millisecondsSinceEpoch ~/ 86400000).toString();
var filename = time;

View File

@@ -362,7 +362,7 @@ Future<void> saveFile(
}
}
class _IOOverrides extends IOOverrides {
final class _IOOverrides extends IOOverrides {
@override
Directory createDirectory(String path) {
if (App.isAndroid) {

View File

@@ -662,10 +662,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.17.0"
mime:
dependency: "direct main"
description:
@@ -958,10 +958,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.6"
version: "0.7.7"
typed_data:
dependency: transitive
description:
@@ -1133,4 +1133,4 @@ packages:
version: "0.0.13"
sdks:
dart: ">=3.8.0 <4.0.0"
flutter: ">=3.35.7"
flutter: ">=3.38.3"

View File

@@ -2,11 +2,11 @@ name: venera
description: "A comic app."
publish_to: 'none'
version: 1.6.0+160
version: 1.6.1+161
environment:
sdk: '>=3.8.0 <4.0.0'
flutter: 3.35.7
flutter: 3.38.3
dependencies:
flutter: