This commit is contained in:
2024-12-31 12:05:56 +08:00
parent a88bbe9ea6
commit 3a320feda9
9 changed files with 260 additions and 30 deletions

96
lib/components/code.dart Normal file
View 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;
}
}

View File

@@ -44,4 +44,5 @@ part 'select.dart';
part 'side_bar.dart';
part 'comic.dart';
part 'effects.dart';
part 'gesture.dart';
part 'gesture.dart';
part 'code.dart';

View File

@@ -155,6 +155,9 @@ class _PopUpWidgetScaffoldState extends State<PopUpWidgetScaffold> {
),
NotificationListener<ScrollNotification>(
onNotification: (notifications) {
if (notifications.metrics.axisDirection != AxisDirection.down) {
return false;
}
if (notifications.metrics.pixels ==
notifications.metrics.minScrollExtent &&
!top) {

View File

@@ -13,7 +13,7 @@ class _Appdata {
bool _isSavingData = false;
Future<void> saveData() async {
Future<void> saveData([bool sync = true]) async {
if (_isSavingData) {
await Future.doWhile(() async {
await Future.delayed(const Duration(milliseconds: 20));
@@ -25,7 +25,9 @@ class _Appdata {
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
await file.writeAsString(data);
_isSavingData = false;
DataSync().uploadData();
if (sync) {
DataSync().uploadData();
}
}
void addSearchHistory(String 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>{};
void writeImplicitData() {
@@ -126,6 +147,8 @@ class _Settings with ChangeNotifier {
'onClickFavorite': 'viewDetail', // viewDetail, read
'enableDnsOverrides': false,
'dnsOverrides': {},
'enableCustomImageProcessing': false,
'customImageProcessing': _defaultCustomImageProcessing,
};
operator [](String key) {
@@ -142,3 +165,16 @@ class _Settings with ChangeNotifier {
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;
}
''';

View File

@@ -1,10 +1,13 @@
import 'dart:async' show Future, StreamController;
import 'package:flutter/foundation.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/utils/io.dart';
import 'base_image_provider.dart';
import 'reader_image.dart' as image_provider;
import 'package:venera/foundation/appdata.dart';
class ReaderImageProvider
extends BaseImageProvider<image_provider.ReaderImageProvider> {
@@ -21,25 +24,50 @@ class ReaderImageProvider
@override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
Uint8List? imageBytes;
if (imageKey.startsWith('file://')) {
var file = File(imageKey);
if (await file.exists()) {
return file.readAsBytes();
imageBytes = await file.readAsBytes();
} else {
throw "Error: File not found.";
}
throw "Error: File not found.";
}
await for (var event
} else {
await for (var event
in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: event.currentBytes,
expectedTotalBytes: event.totalBytes,
));
if (event.imageBytes != null) {
return event.imageBytes!;
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: event.currentBytes,
expectedTotalBytes: event.totalBytes,
));
if (event.imageBytes != null) {
imageBytes = event.imageBytes;
break;
}
}
}
throw "Error: Empty response body.";
if (imageBytes == null) {
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

View File

@@ -61,9 +61,17 @@ class _ReaderSettingsState extends State<ReaderSettings> {
).toSliver(),
SliverToBoxAdapter(
child: AbsorbPointer(
absorbing: (appdata.settings['readerMode']?.toLowerCase().startsWith('continuous') ?? false),
absorbing: (appdata.settings['readerMode']
?.toLowerCase()
.startsWith('continuous') ??
false),
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),
child: _SliderSetting(
title: "The number of pic in screen (Only Gallery Mode)".tl,
@@ -93,7 +101,7 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call('limitImageWidth');
},
).toSliver(),
if(App.isAndroid)
if (App.isAndroid)
_SwitchSetting(
title: 'Turn page by volume keys'.tl,
settingKey: 'enableTurnPageByVolumeKey',
@@ -108,7 +116,67 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call("enableClockAndBatteryInfoInReader");
},
).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;
},
),
),
),
)
],
),
);
}
}

View File

@@ -80,15 +80,9 @@ Future<void> importAppData(File file, [bool checkVersion = false]) async {
LocalFavoritesManager().init();
}
if (await appdataFile.exists()) {
// proxy settings & authorization setting should be kept
var proxySettings = appdata.settings["proxy"];
var authSettings = appdata.settings["authorizationRequired"];
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();
var content = await appdataFile.readAsString();
var data = jsonDecode(content);
appdata.syncData(data);
}
if (await cookieFile.exists()) {
SingleInstanceCookieJar.instance?.dispose();

View File

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