mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
fix #107
This commit is contained in:
@@ -259,7 +259,9 @@
|
|||||||
"Multiple cbz files" : "多个cbz文件",
|
"Multiple cbz files" : "多个cbz文件",
|
||||||
"No valid comics found" : "未找到有效的漫画",
|
"No valid comics found" : "未找到有效的漫画",
|
||||||
"Enable DNS Overrides": "启用DNS覆写",
|
"Enable DNS Overrides": "启用DNS覆写",
|
||||||
"DNS Overrides": "DNS覆写"
|
"DNS Overrides": "DNS覆写",
|
||||||
|
"Custom Image Processing": "自定义图片处理",
|
||||||
|
"Enable": "启用"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -521,6 +523,8 @@
|
|||||||
"Multiple cbz files" : "多個cbz文件",
|
"Multiple cbz files" : "多個cbz文件",
|
||||||
"No valid comics found" : "未找到有效的漫畫",
|
"No valid comics found" : "未找到有效的漫畫",
|
||||||
"Enable DNS Overrides": "啟用DNS覆寫",
|
"Enable DNS Overrides": "啟用DNS覆寫",
|
||||||
"DNS Overrides": "DNS覆寫"
|
"DNS Overrides": "DNS覆寫",
|
||||||
|
"Custom Image Processing": "自定義圖片處理",
|
||||||
|
"Enable": "啟用"
|
||||||
}
|
}
|
||||||
}
|
}
|
96
lib/components/code.dart
Normal file
96
lib/components/code.dart
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
part of 'components.dart';
|
||||||
|
|
||||||
|
class CodeEditor extends StatefulWidget {
|
||||||
|
const CodeEditor({super.key, this.initialValue, this.onChanged});
|
||||||
|
|
||||||
|
final String? initialValue;
|
||||||
|
|
||||||
|
final void Function(String value)? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CodeEditor> createState() => _CodeEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CodeEditorState extends State<CodeEditor> {
|
||||||
|
late TextEditingController _controller;
|
||||||
|
late FocusNode _focusNode;
|
||||||
|
var horizontalScrollController = ScrollController();
|
||||||
|
var verticalScrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(text: widget.initialValue);
|
||||||
|
_focusNode = FocusNode()
|
||||||
|
..onKeyEvent = (node, event) {
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.tab) {
|
||||||
|
if (event is KeyDownEvent) {
|
||||||
|
handleTab();
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleTab() {
|
||||||
|
var text = _controller.text;
|
||||||
|
var start = _controller.selection.start;
|
||||||
|
var end = _controller.selection.end;
|
||||||
|
_controller.text = '${text.substring(0, start)} ${text.substring(end)}';
|
||||||
|
_controller.selection = TextSelection.collapsed(offset: start + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scrollbar(
|
||||||
|
thumbVisibility: true,
|
||||||
|
controller: verticalScrollController,
|
||||||
|
notificationPredicate: (notif) => notif.metrics.axis == Axis.vertical,
|
||||||
|
child: Scrollbar(
|
||||||
|
thumbVisibility: true,
|
||||||
|
controller: horizontalScrollController,
|
||||||
|
notificationPredicate: (notif) => notif.metrics.axis == Axis.horizontal,
|
||||||
|
child: SizedBox.expand(
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: _CustomScrollBehavior(),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
controller: horizontalScrollController,
|
||||||
|
child: IntrinsicWidth(
|
||||||
|
stepWidth: 100,
|
||||||
|
child: TextField(
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'consolas',
|
||||||
|
fontFamilyFallback: ['Courier New', 'monospace'],
|
||||||
|
),
|
||||||
|
controller: _controller,
|
||||||
|
focusNode: _focusNode,
|
||||||
|
maxLines: null,
|
||||||
|
expands: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.all(8),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
widget.onChanged?.call(value);
|
||||||
|
},
|
||||||
|
scrollController: verticalScrollController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomScrollBehavior extends MaterialScrollBehavior {
|
||||||
|
const _CustomScrollBehavior();
|
||||||
|
@override
|
||||||
|
Widget buildScrollbar(
|
||||||
|
BuildContext context, Widget child, ScrollableDetails details) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
@@ -45,3 +45,4 @@ part 'side_bar.dart';
|
|||||||
part 'comic.dart';
|
part 'comic.dart';
|
||||||
part 'effects.dart';
|
part 'effects.dart';
|
||||||
part 'gesture.dart';
|
part 'gesture.dart';
|
||||||
|
part 'code.dart';
|
@@ -155,6 +155,9 @@ class _PopUpWidgetScaffoldState extends State<PopUpWidgetScaffold> {
|
|||||||
),
|
),
|
||||||
NotificationListener<ScrollNotification>(
|
NotificationListener<ScrollNotification>(
|
||||||
onNotification: (notifications) {
|
onNotification: (notifications) {
|
||||||
|
if (notifications.metrics.axisDirection != AxisDirection.down) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (notifications.metrics.pixels ==
|
if (notifications.metrics.pixels ==
|
||||||
notifications.metrics.minScrollExtent &&
|
notifications.metrics.minScrollExtent &&
|
||||||
!top) {
|
!top) {
|
||||||
|
@@ -13,7 +13,7 @@ class _Appdata {
|
|||||||
|
|
||||||
bool _isSavingData = false;
|
bool _isSavingData = false;
|
||||||
|
|
||||||
Future<void> saveData() async {
|
Future<void> saveData([bool sync = true]) async {
|
||||||
if (_isSavingData) {
|
if (_isSavingData) {
|
||||||
await Future.doWhile(() async {
|
await Future.doWhile(() async {
|
||||||
await Future.delayed(const Duration(milliseconds: 20));
|
await Future.delayed(const Duration(milliseconds: 20));
|
||||||
@@ -25,8 +25,10 @@ class _Appdata {
|
|||||||
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
||||||
await file.writeAsString(data);
|
await file.writeAsString(data);
|
||||||
_isSavingData = false;
|
_isSavingData = false;
|
||||||
|
if (sync) {
|
||||||
DataSync().uploadData();
|
DataSync().uploadData();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void addSearchHistory(String keyword) {
|
void addSearchHistory(String keyword) {
|
||||||
if (searchHistory.contains(keyword)) {
|
if (searchHistory.contains(keyword)) {
|
||||||
@@ -78,6 +80,25 @@ class _Appdata {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Following fields are related to device-specific data and should not be synced.
|
||||||
|
static const _disableSync = [
|
||||||
|
"proxy",
|
||||||
|
"authorizationRequired",
|
||||||
|
"customImageProcessing",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Sync data from another device
|
||||||
|
void syncData(Map<String, dynamic> data) {
|
||||||
|
for (var key in data.keys) {
|
||||||
|
if (_disableSync.contains(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
settings[key] = data[key];
|
||||||
|
}
|
||||||
|
searchHistory = List.from(data['searchHistory']);
|
||||||
|
saveData();
|
||||||
|
}
|
||||||
|
|
||||||
var implicitData = <String, dynamic>{};
|
var implicitData = <String, dynamic>{};
|
||||||
|
|
||||||
void writeImplicitData() {
|
void writeImplicitData() {
|
||||||
@@ -126,6 +147,8 @@ class _Settings with ChangeNotifier {
|
|||||||
'onClickFavorite': 'viewDetail', // viewDetail, read
|
'onClickFavorite': 'viewDetail', // viewDetail, read
|
||||||
'enableDnsOverrides': false,
|
'enableDnsOverrides': false,
|
||||||
'dnsOverrides': {},
|
'dnsOverrides': {},
|
||||||
|
'enableCustomImageProcessing': false,
|
||||||
|
'customImageProcessing': _defaultCustomImageProcessing,
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
@@ -142,3 +165,16 @@ class _Settings with ChangeNotifier {
|
|||||||
return _data.toString();
|
return _data.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _defaultCustomImageProcessing = '''
|
||||||
|
/**
|
||||||
|
* Process an image
|
||||||
|
* @param image {ArayBuffer} - The image to process
|
||||||
|
* @param cid {string} - The comic ID
|
||||||
|
* @param eid {string} - The episode ID
|
||||||
|
* @returns {Promise<ArrayBuffer>} - The processed image
|
||||||
|
*/
|
||||||
|
async function processImage(image, cid, eid) {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
''';
|
@@ -1,10 +1,13 @@
|
|||||||
import 'dart:async' show Future, StreamController;
|
import 'dart:async' show Future, StreamController;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
|
import 'package:venera/foundation/js_engine.dart';
|
||||||
import 'package:venera/network/images.dart';
|
import 'package:venera/network/images.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'base_image_provider.dart';
|
import 'base_image_provider.dart';
|
||||||
import 'reader_image.dart' as image_provider;
|
import 'reader_image.dart' as image_provider;
|
||||||
|
import 'package:venera/foundation/appdata.dart';
|
||||||
|
|
||||||
class ReaderImageProvider
|
class ReaderImageProvider
|
||||||
extends BaseImageProvider<image_provider.ReaderImageProvider> {
|
extends BaseImageProvider<image_provider.ReaderImageProvider> {
|
||||||
@@ -21,14 +24,15 @@ class ReaderImageProvider
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
|
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
|
||||||
|
Uint8List? imageBytes;
|
||||||
if (imageKey.startsWith('file://')) {
|
if (imageKey.startsWith('file://')) {
|
||||||
var file = File(imageKey);
|
var file = File(imageKey);
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
return file.readAsBytes();
|
imageBytes = await file.readAsBytes();
|
||||||
}
|
} else {
|
||||||
throw "Error: File not found.";
|
throw "Error: File not found.";
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
await for (var event
|
await for (var event
|
||||||
in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) {
|
in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) {
|
||||||
chunkEvents.add(ImageChunkEvent(
|
chunkEvents.add(ImageChunkEvent(
|
||||||
@@ -36,11 +40,35 @@ class ReaderImageProvider
|
|||||||
expectedTotalBytes: event.totalBytes,
|
expectedTotalBytes: event.totalBytes,
|
||||||
));
|
));
|
||||||
if (event.imageBytes != null) {
|
if (event.imageBytes != null) {
|
||||||
return event.imageBytes!;
|
imageBytes = event.imageBytes;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (imageBytes == null) {
|
||||||
throw "Error: Empty response body.";
|
throw "Error: Empty response body.";
|
||||||
}
|
}
|
||||||
|
if (appdata.settings['enableCustomImageProcessing']) {
|
||||||
|
var script = appdata.settings['customImageProcessing'].toString();
|
||||||
|
if (!script.contains('async function processImage')) {
|
||||||
|
return imageBytes;
|
||||||
|
}
|
||||||
|
var func = JsEngine().runCode('''
|
||||||
|
(() => {
|
||||||
|
$script
|
||||||
|
return processImage;
|
||||||
|
})()
|
||||||
|
''');
|
||||||
|
if (func is JSInvokable) {
|
||||||
|
var result = await func.invoke([imageBytes, cid, eid]);
|
||||||
|
func.free();
|
||||||
|
if (result is Uint8List) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imageBytes;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ReaderImageProvider> obtainKey(ImageConfiguration configuration) {
|
Future<ReaderImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
@@ -61,9 +61,17 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
).toSliver(),
|
).toSliver(),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: AbsorbPointer(
|
child: AbsorbPointer(
|
||||||
absorbing: (appdata.settings['readerMode']?.toLowerCase().startsWith('continuous') ?? false),
|
absorbing: (appdata.settings['readerMode']
|
||||||
|
?.toLowerCase()
|
||||||
|
.startsWith('continuous') ??
|
||||||
|
false),
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
opacity: (appdata.settings['readerMode']?.toLowerCase().startsWith('continuous') ?? false) ? 0.5 : 1.0,
|
opacity: (appdata.settings['readerMode']
|
||||||
|
?.toLowerCase()
|
||||||
|
.startsWith('continuous') ??
|
||||||
|
false)
|
||||||
|
? 0.5
|
||||||
|
: 1.0,
|
||||||
duration: Duration(milliseconds: 300),
|
duration: Duration(milliseconds: 300),
|
||||||
child: _SliderSetting(
|
child: _SliderSetting(
|
||||||
title: "The number of pic in screen (Only Gallery Mode)".tl,
|
title: "The number of pic in screen (Only Gallery Mode)".tl,
|
||||||
@@ -108,7 +116,67 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
widget.onChanged?.call("enableClockAndBatteryInfoInReader");
|
widget.onChanged?.call("enableClockAndBatteryInfoInReader");
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
_PopupWindowSetting(
|
||||||
|
title: "Custom Image Processing".tl,
|
||||||
|
builder: () => _CustomImageProcessing(),
|
||||||
|
).toSliver(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CustomImageProcessing extends StatefulWidget {
|
||||||
|
const _CustomImageProcessing();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CustomImageProcessing> createState() => __CustomImageProcessingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __CustomImageProcessingState extends State<_CustomImageProcessing> {
|
||||||
|
var current = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
current = appdata.settings['customImageProcessing'];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
appdata.settings['customImageProcessing'] = current;
|
||||||
|
appdata.saveData();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopUpWidgetScaffold(
|
||||||
|
title: "Custom Image Processing".tl,
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Enable".tl,
|
||||||
|
settingKey: "enableCustomImageProcessing",
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: context.colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
child: SizedBox.expand(
|
||||||
|
child: CodeEditor(
|
||||||
|
initialValue: appdata.settings['customImageProcessing'],
|
||||||
|
onChanged: (value) {
|
||||||
|
current = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -80,15 +80,9 @@ Future<void> importAppData(File file, [bool checkVersion = false]) async {
|
|||||||
LocalFavoritesManager().init();
|
LocalFavoritesManager().init();
|
||||||
}
|
}
|
||||||
if (await appdataFile.exists()) {
|
if (await appdataFile.exists()) {
|
||||||
// proxy settings & authorization setting should be kept
|
var content = await appdataFile.readAsString();
|
||||||
var proxySettings = appdata.settings["proxy"];
|
var data = jsonDecode(content);
|
||||||
var authSettings = appdata.settings["authorizationRequired"];
|
appdata.syncData(data);
|
||||||
File(FilePath.join(App.dataPath, "appdata.json")).deleteIfExistsSync();
|
|
||||||
appdataFile.renameSync(FilePath.join(App.dataPath, "appdata.json"));
|
|
||||||
await appdata.init();
|
|
||||||
appdata.settings["proxy"] = proxySettings;
|
|
||||||
appdata.settings["authorizationRequired"] = authSettings;
|
|
||||||
appdata.saveData();
|
|
||||||
}
|
}
|
||||||
if (await cookieFile.exists()) {
|
if (await cookieFile.exists()) {
|
||||||
SingleInstanceCookieJar.instance?.dispose();
|
SingleInstanceCookieJar.instance?.dispose();
|
||||||
|
@@ -99,7 +99,7 @@ class DataSync with ChangeNotifier {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
appdata.settings['dataVersion']++;
|
appdata.settings['dataVersion']++;
|
||||||
await appdata.saveData();
|
await appdata.saveData(false);
|
||||||
var data = await exportAppData();
|
var data = await exportAppData();
|
||||||
var time =
|
var time =
|
||||||
(DateTime.now().millisecondsSinceEpoch ~/ 86400000).toString();
|
(DateTime.now().millisecondsSinceEpoch ~/ 86400000).toString();
|
||||||
|
Reference in New Issue
Block a user