Compare commits

..

9 Commits

Author SHA1 Message Date
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
32 changed files with 674 additions and 456 deletions

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} value
* @param {ArrayBuffer} key * @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} value
* @param {ArrayBuffer} key * @param {ArrayBuffer} key
@@ -225,20 +257,58 @@ let Convert = {
/** /**
* @param {ArrayBuffer} value * @param {ArrayBuffer} value
* @param {ArrayBuffer} key * @param {ArrayBuffer} key
* @param {ArrayBuffer} iv
* @param {number} blockSize * @param {number} blockSize
* @returns {ArrayBuffer} * @returns {ArrayBuffer}
*/ */
decryptAesCfb: (value, key, blockSize) => { encryptAesCfb: (value, key, iv, blockSize) => {
return sendMessage({ return sendMessage({
method: "convert", method: "convert",
type: "aes-cfb", type: "aes-cfb",
value: value, value: value,
key: key, 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, blockSize: blockSize,
isEncode: false 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} value
* @param {ArrayBuffer} key * @param {ArrayBuffer} key
@@ -395,9 +465,10 @@ let Network = {
* @param {string} url - The URL to send the request to. * @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request. * @param {Object} headers - The headers to include in the request.
* @param data - The data to send with 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. * @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({ let result = await sendMessage({
method: 'http', method: 'http',
http_method: method, http_method: method,
@@ -405,6 +476,7 @@ let Network = {
url: url, url: url,
headers: headers, headers: headers,
data: data, data: data,
extra: extra,
}); });
if (result.error) { if (result.error) {
@@ -420,15 +492,17 @@ let Network = {
* @param {string} url - The URL to send the request to. * @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request. * @param {Object} headers - The headers to include in the request.
* @param data - The data to send with 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. * @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({ let result = await sendMessage({
method: 'http', method: 'http',
http_method: method, http_method: method,
url: url, url: url,
headers: headers, headers: headers,
data: data, data: data,
extra: extra,
}); });
if (result.error) { if (result.error) {
@@ -442,10 +516,11 @@ let Network = {
* Sends an HTTP GET request. * Sends an HTTP GET request.
* @param {string} url - The URL to send the request to. * @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request. * @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. * @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request.
*/ */
async get(url, headers) { async get(url, headers, extra) {
return this.sendRequest('GET', url, headers); return this.sendRequest('GET', url, headers, extra);
}, },
/** /**
@@ -453,10 +528,11 @@ let Network = {
* @param {string} url - The URL to send the request to. * @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request. * @param {Object} headers - The headers to include in the request.
* @param data - The data to send with 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. * @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request.
*/ */
async post(url, headers, data) { async post(url, headers, data, extra) {
return this.sendRequest('POST', url, headers, data); 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 {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request. * @param {Object} headers - The headers to include in the request.
* @param data - The data to send with 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. * @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request.
*/ */
async put(url, headers, data) { async put(url, headers, data, extra) {
return this.sendRequest('PUT', url, headers, data); 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 {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request. * @param {Object} headers - The headers to include in the request.
* @param data - The data to send with 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. * @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request.
*/ */
async patch(url, headers, data) { async patch(url, headers, data, extra) {
return this.sendRequest('PATCH', url, headers, data); return this.sendRequest('PATCH', url, headers, data, extra);
}, },
/** /**
* Sends an HTTP DELETE request. * Sends an HTTP DELETE request.
* @param {string} url - The URL to send the request to. * @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request. * @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. * @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request.
*/ */
async delete(url, headers) { async delete(url, headers, extra) {
return this.sendRequest('DELETE', url, headers); return this.sendRequest('DELETE', url, headers, extra);
}, },
/** /**

View File

@@ -201,6 +201,10 @@
"Sync Data": "同步数据", "Sync Data": "同步数据",
"Syncing Data": "正在同步数据", "Syncing Data": "正在同步数据",
"Data Sync": "数据同步", "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": "快速收藏", "Quick Favorite": "快速收藏",
"Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹", "Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹",
"Added": "已添加", "Added": "已添加",
@@ -624,6 +628,10 @@
"Sync Data": "同步資料", "Sync Data": "同步資料",
"Syncing Data": "正在同步資料", "Syncing Data": "正在同步資料",
"Data Sync": "資料同步", "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": "快速收藏", "Quick Favorite": "快速收藏",
"Long press on the favorite button to quickly add to this folder": "長按收藏按鈕快速添加到這個資料夾", "Long press on the favorite button to quickly add to this folder": "長按收藏按鈕快速添加到這個資料夾",
"Added": "已添加", "Added": "已添加",

View File

@@ -172,6 +172,16 @@ class NaviPaneState extends State<NaviPane>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
onRebuild(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( return _NaviPopScope(
action: () { action: () {
if (App.mainNavigatorKey!.currentState!.canPop()) { if (App.mainNavigatorKey!.currentState!.canPop()) {
@@ -185,7 +195,7 @@ class NaviPaneState extends State<NaviPane>
animation: controller, animation: controller,
builder: (context, child) { builder: (context, child) {
final value = controller.value; final value = controller.value;
return Stack( Widget content = Stack(
children: [ children: [
Positioned( Positioned(
left: _kFoldedSideBarWidth * ((value - 2.0).clamp(-1.0, 0.0)), 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

@@ -20,7 +20,6 @@ class AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin{
super.allowSnapshotting = true, super.allowSnapshotting = true,
super.barrierDismissible = false, super.barrierDismissible = false,
this.enableIOSGesture = true, this.enableIOSGesture = true,
this.iosFullScreenPopGesture = true,
this.preventRebuild = true, this.preventRebuild = true,
}) { }) {
assert(opaque); assert(opaque);
@@ -50,9 +49,6 @@ class AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin{
@override @override
final bool enableIOSGesture; final bool enableIOSGesture;
@override
final bool iosFullScreenPopGesture;
@override @override
final bool preventRebuild; final bool preventRebuild;
} }
@@ -79,8 +75,6 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
bool get enableIOSGesture; bool get enableIOSGesture;
bool get iosFullScreenPopGesture;
bool get preventRebuild; bool get preventRebuild;
Widget? _child; Widget? _child;
@@ -140,7 +134,6 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
gestureWidth: _kBackGestureWidth, gestureWidth: _kBackGestureWidth,
enabledCallback: () => _isPopGestureEnabled<T>(this), enabledCallback: () => _isPopGestureEnabled<T>(this),
onStartPopGesture: () => _startPopGesture(this), onStartPopGesture: () => _startPopGesture(this),
fullScreen: iosFullScreenPopGesture,
child: child, child: child,
) )
: child); : child);
@@ -210,32 +203,41 @@ class IOSBackGestureController {
} }
class IOSBackGestureDetector extends StatefulWidget { class IOSBackGestureDetector extends StatefulWidget {
const IOSBackGestureDetector( const IOSBackGestureDetector({
{required this.enabledCallback, required this.enabledCallback,
required this.child, required this.child,
required this.gestureWidth, required this.gestureWidth,
required this.onStartPopGesture, required this.onStartPopGesture,
this.fullScreen = false, super.key,
super.key}); });
final double gestureWidth; final double gestureWidth;
final bool Function() enabledCallback; final bool Function() enabledCallback;
final IOSBackGestureController Function() onStartPopGesture; final IOSBackGestureController Function() onStartPopGesture;
final Widget child; final Widget child;
final bool fullScreen;
@override @override
State<IOSBackGestureDetector> createState() => _IOSBackGestureDetectorState(); State<IOSBackGestureDetector> createState() => _IOSBackGestureDetectorState();
} }
class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> { class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
IOSBackGestureController? _backGestureController; 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 @override
void dispose() { void dispose() {
@@ -243,115 +245,209 @@ class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
super.dispose(); super.dispose();
} }
@override
void initState() {
super.initState();
_recognizer = HorizontalDragGestureRecognizer(debugOwner: this)
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var dragAreaWidth = Directionality.of(context) == TextDirection.ltr return RawGestureDetector(
? MediaQuery.of(context).padding.left behavior: HitTestBehavior.translucent,
: MediaQuery.of(context).padding.right; gestures: {
dragAreaWidth = max(dragAreaWidth, widget.gestureWidth); _BackSwipeRecognizer: GestureRecognizerFactoryWithHandlers<_BackSwipeRecognizer>(
final Widget gestureListener = widget.fullScreen () => _recognizer,
? Positioned.fill( (instance) {
child: Listener( instance.gestureWidth = widget.gestureWidth;
onPointerDown: _handlePointerDown, },
behavior: HitTestBehavior.translucent, ),
), },
) child: widget.child,
: Positioned(
width: dragAreaWidth,
top: 0.0,
bottom: 0.0,
left: Directionality.of(context) == TextDirection.ltr ? 0.0 : null,
right: Directionality.of(context) == TextDirection.rtl ? 0.0 : null,
child: Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.translucent,
),
);
return Stack(
fit: StackFit.passthrough,
children: <Widget>[
widget.child,
gestureListener,
],
); );
} }
void _handlePointerDown(PointerDownEvent event) { bool _isPointerInHorizontalScrollable(Offset globalPosition) {
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) {
final HitTestResult result = HitTestResult(); 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) { for (final entry in result.path) {
final target = entry.target; final target = entry.target;
if (target is RenderViewport) { if (target is RenderViewport) {
if (_isAxisHorizontal(target.axisDirection)) { if (target.axisDirection == AxisDirection.left ||
target.axisDirection == AxisDirection.right) {
return true; 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; 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; return false;
} }
bool _isAxisHorizontal(AxisDirection direction) { void _handleDragStart(DragStartDetails details) {
return direction == AxisDirection.left || direction == AxisDirection.right; 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 { class SlidePageTransitionBuilder extends PageTransitionsBuilder {
@@ -389,4 +485,4 @@ class SlidePageTransitionBuilder extends PageTransitionsBuilder {
), ),
); );
} }
} }

View File

@@ -23,9 +23,26 @@ class Appdata with Init {
} }
_isSavingData = true; _isSavingData = true;
try { try {
var data = jsonEncode(toJson()); var futures = <Future>[];
var json = toJson();
var data = jsonEncode(json);
var file = File(FilePath.join(App.dataPath, 'appdata.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 { } finally {
_isSavingData = false; _isSavingData = false;
} }
@@ -59,20 +76,33 @@ class Appdata with Init {
return {'settings': settings._data, 'searchHistory': searchHistory}; 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. /// Following fields are related to device-specific data and should not be synced.
static const _disableSync = [ static const _disableSync = [
"proxy", "proxy",
"authorizationRequired", "authorizationRequired",
"customImageProcessing", "customImageProcessing",
"webdav", "webdav",
"disableSyncFields",
]; ];
/// Sync data from another device /// Sync data from another device
void syncData(Map<String, dynamic> data) { void syncData(Map<String, dynamic> data) {
if (data['settings'] is Map) { if (data['settings'] is Map) {
var settings = data['settings'] as Map<String, dynamic>; var settings = data['settings'] as Map<String, dynamic>;
List<String> customDisableSync = splitField(this.settings["disableSyncFields"] as String);
for (var key in settings.keys) { for (var key in settings.keys) {
if (!_disableSync.contains(key)) { if (!_disableSync.contains(key) &&
!customDisableSync.contains(key)) {
this.settings[key] = settings[key]; this.settings[key] = settings[key];
} }
} }
@@ -166,6 +196,7 @@ class Settings with ChangeNotifier {
'checkUpdateOnStart': false, 'checkUpdateOnStart': false,
'limitImageWidth': true, 'limitImageWidth': true,
'webdav': [], // empty means not configured 'webdav': [], // empty means not configured
"disableSyncFields": "", // "field1, field2, ..."
'dataVersion': 0, 'dataVersion': 0,
'quickFavorite': null, 'quickFavorite': null,
'enableTurnPageByVolumeKey': true, 'enableTurnPageByVolumeKey': true,

View File

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

View File

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

View File

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

View File

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

View File

@@ -96,11 +96,28 @@ class MyLogInterceptor implements Interceptor {
@override @override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) { void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
const String headerMask = "********";
const String dataMask = "****** DATA_PROTECTED ******";
Log.info( Log.info(
"Network", "Network",
"${options.method} ${options.uri}\n" "${options.method} ${options.uri}\n"
"headers:\n${options.headers}\n" "headers:\n${
"data:\n${options.data}"); 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.connectTimeout = const Duration(seconds: 15);
options.receiveTimeout = const Duration(seconds: 15); options.receiveTimeout = const Duration(seconds: 15);
options.sendTimeout = 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 = var head =
await controller.evaluateJavascript("document.head.innerHTML") ?? await controller.evaluateJavascript("document.head.innerHTML") ??
""; "";
var body =
await controller.evaluateJavascript("document.body.innerHTML") ??
"";
Log.info("Cloudflare", "Checking head: $head"); Log.info("Cloudflare", "Checking head: $head");
var isChallenging = head.contains('#challenge-success-text') || var isChallenging = head.contains('#challenge-success-text') ||
head.contains("#challenge-error-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) { if (!isChallenging) {
Log.info( Log.info(
"Cloudflare", "Cloudflare",
@@ -159,10 +164,14 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
void check(InAppWebViewController controller) async { void check(InAppWebViewController controller) async {
var head = await controller.evaluateJavascript( var head = await controller.evaluateJavascript(
source: "document.head.innerHTML") as String; source: "document.head.innerHTML") as String;
var body = await controller.evaluateJavascript(
source: "document.body.innerHTML") as String;
Log.info("Cloudflare", "Checking head: $head"); Log.info("Cloudflare", "Checking head: $head");
var isChallenging = head.contains('#challenge-success-text') || var isChallenging = head.contains('#challenge-success-text') ||
head.contains("#challenge-error-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) { if (!isChallenging) {
Log.info( Log.info(
"Cloudflare", "Cloudflare",

View File

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

View File

@@ -115,9 +115,7 @@ abstract mixin class _ComicPageActions {
history: history ?? History.fromModel(model: comic, ep: 0, page: 0), history: history ?? History.fromModel(model: comic, ep: 0, page: 0),
author: comic.findAuthor() ?? '', author: comic.findAuthor() ?? '',
tags: comic.plainTags, tags: comic.plainTags,
), )
enableIOSGesture: false,
iosFullScreenGesture: false,
) )
.then((_) { .then((_) {
onReadEnd(); onReadEnd();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -100,7 +100,7 @@ class _AppSettingsState extends State<AppSettings> {
title: "Export App Data".tl, title: "Export App Data".tl,
callback: () async { callback: () async {
var controller = showLoadingDialog(context); var controller = showLoadingDialog(context);
var file = await exportAppData(); var file = await exportAppData(false);
await saveFile(filename: "data.venera", file: file); await saveFile(filename: "data.venera", file: file);
controller.close(); controller.close();
}, },
@@ -353,6 +353,8 @@ class _WebdavSettingState extends State<_WebdavSetting> {
String url = ""; String url = "";
String user = ""; String user = "";
String pass = ""; String pass = "";
String disableSync = "";
bool autoSync = true; bool autoSync = true;
bool isTesting = false; bool isTesting = false;
@@ -364,6 +366,9 @@ class _WebdavSettingState extends State<_WebdavSetting> {
if (appdata.settings['webdav'] is! List) { if (appdata.settings['webdav'] is! List) {
appdata.settings['webdav'] = []; appdata.settings['webdav'] = [];
} }
if (appdata.settings['disableSyncFields'].trim().isNotEmpty) {
disableSync = appdata.settings['disableSyncFields'];
}
var configs = appdata.settings['webdav'] as List; var configs = appdata.settings['webdav'] as List;
if (configs.whereType<String>().length != 3) { if (configs.whereType<String>().length != 3) {
return; return;
@@ -418,6 +423,56 @@ class _WebdavSettingState extends State<_WebdavSetting> {
onChanged: (value) => pass = value, onChanged: (value) => pass = value,
), ),
const SizedBox(height: 12), 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( ListTile(
leading: Icon(Icons.sync), leading: Icon(Icons.sync),
title: Text("Auto Sync Data".tl), title: Text("Auto Sync Data".tl),
@@ -494,6 +549,7 @@ class _WebdavSettingState extends State<_WebdavSetting> {
} }
appdata.settings['webdav'] = [url, user, pass]; appdata.settings['webdav'] = [url, user, pass];
appdata.settings['disableSyncFields'] = disableSync;
appdata.implicitData['webdavAutoSync'] = autoSync; appdata.implicitData['webdavAutoSync'] = autoSync;
appdata.writeImplicitData(); appdata.writeImplicitData();

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
@@ -41,7 +40,7 @@ class SettingsPage extends StatefulWidget {
State<SettingsPage> createState() => _SettingsPageState(); State<SettingsPage> createState() => _SettingsPageState();
} }
class _SettingsPageState extends State<SettingsPage> implements PopEntry { class _SettingsPageState extends State<SettingsPage> {
int currentPage = -1; int currentPage = -1;
ColorScheme get colors => Theme.of(context).colorScheme; ColorScheme get colors => Theme.of(context).colorScheme;
@@ -70,84 +69,14 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
Icons.bug_report, 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 @override
void initState() { void initState() {
currentPage = widget.initialPage; 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(); super.initState();
} }
@override
dispose() {
super.dispose();
gestureRecognizer.dispose();
_route?.unregisterPopEntry(this);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (currentPage != -1) {
canPop.value = false;
} else {
canPop.value = true;
}
return Material( return Material(
child: buildBody(), child: buildBody(),
); );
@@ -209,55 +138,10 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
], ],
); );
} else { } else {
return LayoutBuilder( return buildLeft();
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(),
),
),
),
)
],
);
},
);
} }
} }
void handlePointerDown(PointerDownEvent event) {
if (!App.isIOS) {
return;
}
if (currentPage == -1) {
return;
}
gestureRecognizer.addPointer(event);
}
Widget buildLeft() { Widget buildLeft() {
return Material( return Material(
child: Column( child: Column(
@@ -334,7 +218,13 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
? const EdgeInsets.fromLTRB(8, 0, 8, 0) ? const EdgeInsets.fromLTRB(8, 0, 8, 0)
: EdgeInsets.zero, : EdgeInsets.zero,
child: InkWell( child: InkWell(
onTap: () => setState(() => currentPage = id), onTap: () {
if (enableTwoViews) {
setState(() => currentPage = id);
} else {
context.to(() => _SettingsDetailPage(pageIndex: id));
}
},
child: content, child: content,
).paddingVertical(4), ).paddingVertical(4),
); );
@@ -348,8 +238,23 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
} }
Widget buildRight() { Widget buildRight() {
return switch (currentPage) { if (currentPage == -1) {
-1 => const SizedBox(), 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(), 0 => const ExploreSettings(),
1 => const ReaderSettings(), 1 => const ReaderSettings(),
2 => const AppearanceSettings(), 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 @override
ValueListenable<bool> get canPopNotifier => canPop; Widget build(BuildContext context) {
return Material(
@override child: _buildPage(),
void onPopInvokedWithResult(bool didPop, result) { );
if (currentPage != -1) {
setState(() {
currentPage = -1;
});
}
} }
@override Widget _buildPage() {
void onPopInvoked(bool didPop) { return switch (pageIndex) {
if (currentPage != -1) { 0 => const ExploreSettings(),
setState(() { 1 => const ReaderSettings(),
currentPage = -1; 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'; import 'io.dart';
Future<File> exportAppData() async { Future<File> exportAppData([bool sync = true]) async {
var time = DateTime.now().millisecondsSinceEpoch ~/ 1000; var time = DateTime.now().millisecondsSinceEpoch ~/ 1000;
var cacheFilePath = FilePath.join(App.cachePath, '$time.venera'); var cacheFilePath = FilePath.join(App.cachePath, '$time.venera');
var cacheFile = File(cacheFilePath); var cacheFile = File(cacheFilePath);
@@ -27,7 +27,7 @@ Future<File> exportAppData() async {
var zipFile = ZipFile.open(cacheFilePath); var zipFile = ZipFile.open(cacheFilePath);
var historyFile = FilePath.join(dataPath, "history.db"); var historyFile = FilePath.join(dataPath, "history.db");
var localFavoriteFile = FilePath.join(dataPath, "local_favorite.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"); var cookies = FilePath.join(dataPath, "cookie.db");
zipFile.addFile("history.db", historyFile); zipFile.addFile("history.db", historyFile);
zipFile.addFile("local_favorite.db", localFavoriteFile); zipFile.addFile("local_favorite.db", localFavoriteFile);

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ version: 1.6.0+160
environment: environment:
sdk: '>=3.8.0 <4.0.0' sdk: '>=3.8.0 <4.0.0'
flutter: 3.35.7 flutter: 3.38.3
dependencies: dependencies:
flutter: flutter: