mirror of
https://github.com/venera-app/venera.git
synced 2025-12-13 05:51:15 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
068d6148ad | ||
|
|
0b261f81ba | ||
|
|
781ff2553d | ||
|
|
0ce18cd738 | ||
| 40ef8a63b0 | |||
| 053293839e | |||
|
|
f0be40c6d7 | ||
|
|
da5b64abb0 | ||
|
|
7e3addf7a6 | ||
|
|
b9c06779ad | ||
|
|
7e928d2c9c | ||
|
|
b3239757a8 | ||
|
|
bdaa10fa06 | ||
|
|
4296768c8d |
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
105
assets/init.js
105
assets/init.js
@@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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": "已添加",
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -153,9 +153,7 @@ class LocalComic with HistoryMixin implements Comic {
|
||||
),
|
||||
author: subtitle,
|
||||
tags: tags,
|
||||
),
|
||||
enableIOSGesture: false,
|
||||
iosFullScreenGesture: false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -170,7 +170,6 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
|
||||
text: widget.keyword,
|
||||
sourceKey: widget.source.key,
|
||||
),
|
||||
iosFullScreenGesture: false,
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -200,7 +200,6 @@ class _BodyState extends State<_Body> {
|
||||
await ComicSourceManager().reload();
|
||||
setState(() {});
|
||||
}),
|
||||
iosFullScreenGesture: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -37,8 +37,6 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> {
|
||||
initialEp: ep,
|
||||
initialPage: page,
|
||||
),
|
||||
enableIOSGesture: false,
|
||||
iosFullScreenGesture: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -243,9 +243,7 @@ class _ImageFavoritesPhotoViewState extends State<ImageFavoritesPhotoView> {
|
||||
sourceKey: comic.sourceKey,
|
||||
initialEp: ep,
|
||||
initialPage: page,
|
||||
),
|
||||
enableIOSGesture: false,
|
||||
iosFullScreenGesture: false,
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -362,7 +362,7 @@ Future<void> saveFile(
|
||||
}
|
||||
}
|
||||
|
||||
class _IOOverrides extends IOOverrides {
|
||||
final class _IOOverrides extends IOOverrides {
|
||||
@override
|
||||
Directory createDirectory(String path) {
|
||||
if (App.isAndroid) {
|
||||
|
||||
10
pubspec.lock
10
pubspec.lock
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user