Merge pull request #130 from venera-app/dev

v1.1.4
This commit is contained in:
nyne
2025-01-06 22:42:09 +08:00
committed by GitHub
29 changed files with 1069 additions and 218 deletions

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip

View File

@@ -18,7 +18,7 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.2.1' apply false id "com.android.application" version '8.3.2' apply false
id "org.jetbrains.kotlin.android" version "1.8.10" apply false id "org.jetbrains.kotlin.android" version "1.8.10" apply false
} }

View File

@@ -218,7 +218,6 @@
"Create Folder": "新建文件夹", "Create Folder": "新建文件夹",
"Select an image on screen": "选择屏幕上的图片", "Select an image on screen": "选择屏幕上的图片",
"Added @count comics to download queue.": "已添加 @count 本漫画到下载队列", "Added @count comics to download queue.": "已添加 @count 本漫画到下载队列",
"Ignore Certificate Errors": "忽略证书错误",
"Authorization Required": "需要身份验证", "Authorization Required": "需要身份验证",
"Sync": "同步", "Sync": "同步",
"The folder is Linked to @source": "文件夹已关联到 @source", "The folder is Linked to @source": "文件夹已关联到 @source",
@@ -258,7 +257,15 @@
"View Detail": "查看详情", "View Detail": "查看详情",
"Select a directory which contains multiple cbz/zip files." : "选择一个包含多个cbz/zip文件的目录", "Select a directory which contains multiple cbz/zip files." : "选择一个包含多个cbz/zip文件的目录",
"Multiple cbz files" : "多个cbz文件", "Multiple cbz files" : "多个cbz文件",
"No valid comics found" : "未找到有效的漫画" "No valid comics found" : "未找到有效的漫画",
"Enable DNS Overrides": "启用DNS覆写",
"DNS Overrides": "DNS覆写",
"Custom Image Processing": "自定义图片处理",
"Enable": "启用",
"Aggregated": "聚合",
"Default Search Target": "默认搜索目标",
"Auto Language Filters": "自动语言筛选",
"Check for updates on startup": "启动时检查更新"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -479,7 +486,6 @@
"Create Folder": "新建文件夾", "Create Folder": "新建文件夾",
"Select an image on screen": "選擇屏幕上的圖片", "Select an image on screen": "選擇屏幕上的圖片",
"Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列", "Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列",
"Ignore Certificate Errors": "忽略證書錯誤",
"Authorization Required": "需要身份驗證", "Authorization Required": "需要身份驗證",
"Sync": "同步", "Sync": "同步",
"The folder is Linked to @source": "文件夾已關聯到 @source", "The folder is Linked to @source": "文件夾已關聯到 @source",
@@ -519,6 +525,14 @@
"View Detail": "查看詳情", "View Detail": "查看詳情",
"Select a directory which contains multiple cbz/zip files." : "選擇一個包含多個cbz/zip文件的目錄", "Select a directory which contains multiple cbz/zip files." : "選擇一個包含多個cbz/zip文件的目錄",
"Multiple cbz files" : "多個cbz文件", "Multiple cbz files" : "多個cbz文件",
"No valid comics found" : "未找到有效的漫畫" "No valid comics found" : "未找到有效的漫畫",
"Enable DNS Overrides": "啟用DNS覆寫",
"DNS Overrides": "DNS覆寫",
"Custom Image Processing": "自定義圖片處理",
"Enable": "啟用",
"Aggregated": "聚合",
"Default Search Target": "默認搜索目標",
"Auto Language Filters": "自動語言篩選",
"Check for updates on startup": "啟動時檢查更新"
} }
} }

383
lib/components/code.dart Normal file
View File

@@ -0,0 +1,383 @@
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 _CodeTextEditingController _controller;
late FocusNode _focusNode;
var horizontalScrollController = ScrollController();
var verticalScrollController = ScrollController();
int lineCount = 1;
@override
void initState() {
super.initState();
_controller = _CodeTextEditingController(text: widget.initialValue);
_focusNode = FocusNode()
..onKeyEvent = (node, event) {
if (event.logicalKey == LogicalKeyboardKey.tab) {
if (event is KeyDownEvent) {
handleTab();
}
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
};
lineCount = calculateLineCount(widget.initialValue ?? '');
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
future = _controller.init(context.brightness);
}
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);
}
int calculateLineCount(String text) {
return text.split('\n').length;
}
Widget buildLineNumbers() {
return SizedBox(
width: 32,
child: Column(
children: [
for (var i = 1; i <= lineCount; i++)
SizedBox(
height: 14 * 1.5,
child: Center(
child: Text(
i.toString(),
style: TextStyle(
color: context.colorScheme.outline,
fontSize: 13,
height: 1.0,
fontFamily: 'Consolas',
fontFamilyFallback: ['Courier New', 'monospace'],
),
),
),
),
],
),
).paddingVertical(8);
}
late Future future;
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, value) {
if (value.connectionState == ConnectionState.waiting) {
return const SizedBox();
}
return GestureDetector(
onTap: () {
_controller.selection = TextSelection.collapsed(
offset: _controller.text.length,
);
_focusNode.requestFocus();
},
child: 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: SingleChildScrollView(
scrollDirection: Axis.vertical,
controller: verticalScrollController,
child: Row(
children: [
buildLineNumbers(),
IntrinsicWidth(
stepWidth: 100,
child: TextField(
controller: _controller,
focusNode: _focusNode,
maxLines: null,
cursorHeight: 1.5 * 14,
style: TextStyle(height: 1.5, fontSize: 14),
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.all(8),
),
onChanged: (value) {
widget.onChanged?.call(value);
if (lineCount != calculateLineCount(value)) {
setState(() {
lineCount = calculateLineCount(value);
});
}
},
),
),
],
),
),
),
),
),
),
),
);
},
);
}
}
class _CustomScrollBehavior extends MaterialScrollBehavior {
const _CustomScrollBehavior();
@override
Widget buildScrollbar(
BuildContext context, Widget child, ScrollableDetails details) {
return child;
}
}
class _CodeTextEditingController extends TextEditingController {
_CodeTextEditingController({super.text});
HighlighterTheme? _theme;
Future<void> init(Brightness brightness) async {
Highlighter.addLanguage('js', _jsGrammer);
_theme = await HighlighterTheme.loadForBrightness(brightness);
}
@override
TextSpan buildTextSpan(
{required BuildContext context,
TextStyle? style,
required bool withComposing}) {
var highlighter = Highlighter(
language: 'js',
theme: _theme!,
);
var result = highlighter.highlight(text);
style = TextStyle(
height: 1.5,
fontSize: 14,
fontFamily: 'Consolas',
fontFamilyFallback: ['Courier New', 'Roboto Mono', 'monospace'],
);
return mergeTextStyle(result, style);
}
TextSpan mergeTextStyle(TextSpan span, TextStyle style) {
var result = TextSpan(
style: style.merge(span.style),
children: span.children
?.whereType()
.map((e) => mergeTextStyle(e, style))
.toList(),
text: span.text,
);
return result;
}
}
const _jsGrammer = r'''
{
"name": "JavaScript",
"version": "1.0.0",
"fileTypes": ["js", "mjs", "cjs"],
"scopeName": "source.js",
"foldingStartMarker": "\\{\\s*$",
"foldingStopMarker": "^\\s*\\}",
"patterns": [
{
"name": "meta.preprocessor.script.js",
"match": "^(#!.*)$"
},
{
"name": "meta.import-export.js",
"begin": "\\b(import|export)\\b",
"beginCaptures": {
"0": {
"name": "keyword.control.import.js"
}
},
"end": ";",
"endCaptures": {
"0": {
"name": "punctuation.terminator.js"
}
},
"patterns": [
{
"include": "#strings"
},
{
"include": "#comments"
},
{
"name": "keyword.control.import.js",
"match": "\\b(as|from)\\b"
}
]
},
{
"include": "#comments"
},
{
"include": "#keywords"
},
{
"include": "#constants-and-special-vars"
},
{
"include": "#operators"
},
{
"include": "#strings"
}
],
"repository": {
"comments": {
"patterns": [
{
"name": "comment.block.js",
"begin": "/\\*",
"end": "\\*/"
},
{
"name": "comment.line.double-slash.js",
"match": "//.*$"
}
]
},
"keywords": {
"patterns": [
{
"name": "keyword.control.js",
"match": "\\b(if|else|for|while|do|switch|case|default|break|continue|return|throw|try|catch|finally)\\b"
},
{
"name": "keyword.operator.js",
"match": "\\b(instanceof|typeof|new|delete|in|void)\\b"
},
{
"name": "storage.type.js",
"match": "\\b(var|let|const|function|class|extends)\\b"
},
{
"name": "keyword.declaration.js",
"match": "\\b(export|import|default)\\b"
}
]
},
"constants-and-special-vars": {
"patterns": [
{
"name": "constant.language.js",
"match": "\\b(true|false|null|undefined|NaN|Infinity)\\b"
},
{
"name": "constant.numeric.js",
"match": "\\b(0x[0-9A-Fa-f]+|[0-9]+\\.?[0-9]*(e[+-]?[0-9]+)?)\\b"
}
]
},
"operators": {
"patterns": [
{
"name": "keyword.operator.assignment.js",
"match": "(=|\\+=|-=|\\*=|/=|%=|\\|=|&=|\\^=|<<=|>>=|>>>=)"
},
{
"name": "keyword.operator.comparison.js",
"match": "(==|!=|===|!==|<|<=|>|>=)"
},
{
"name": "keyword.operator.logical.js",
"match": "(&&|\\|\\||!)"
},
{
"name": "keyword.operator.arithmetic.js",
"match": "(-|\\+|\\*|/|%)"
},
{
"name": "keyword.operator.bitwise.js",
"match": "(\\||&|\\^|~|<<|>>|>>>)"
}
]
},
"strings": {
"patterns": [
{
"name": "string.quoted.double.js",
"begin": "\"",
"end": "\"",
"patterns": [
{
"include": "#string-interpolation"
}
]
},
{
"name": "string.quoted.single.js",
"begin": "'",
"end": "'",
"patterns": [
{
"include": "#string-interpolation"
}
]
},
{
"name": "string.template.js",
"begin": "`",
"end": "`",
"patterns": [
{
"include": "#string-interpolation"
}
]
}
]
},
"string-interpolation": {
"patterns": [
{
"name": "variable.parameter.js",
"begin": "\\$\\{",
"end": "\\}"
}
]
}
}
}
''';

View File

@@ -235,110 +235,109 @@ class ComicTile extends StatelessWidget {
} }
Widget _buildBriefMode(BuildContext context) { Widget _buildBriefMode(BuildContext context) {
return Padding( return LayoutBuilder(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), builder: (context, constraints) {
child: LayoutBuilder( return InkWell(
builder: (context, constraints) { borderRadius: BorderRadius.circular(8),
return InkWell( onTap: _onTap,
borderRadius: BorderRadius.circular(8), onLongPress: enableLongPressed ? () => _onLongPressed(context) : null,
onTap: _onTap, onSecondaryTapDown: (detail) => onSecondaryTap(detail, context),
onLongPress: child: Column(
enableLongPressed ? () => _onLongPressed(context) : null, children: [
onSecondaryTapDown: (detail) => onSecondaryTap(detail, context), Expanded(
child: Column( child: Stack(
children: [ children: [
Expanded( Positioned.fill(
child: SizedBox( child: Container(
child: Stack( decoration: BoxDecoration(
children: [ color: context.colorScheme.secondaryContainer,
Positioned.fill( borderRadius: BorderRadius.circular(8),
child: Container( boxShadow: [
decoration: BoxDecoration( BoxShadow(
color: Theme.of(context) color: Colors.black.toOpacity(0.2),
.colorScheme blurRadius: 2,
.secondaryContainer, offset: const Offset(0, 2),
borderRadius: BorderRadius.circular(8),
),
clipBehavior: Clip.antiAlias,
child: buildImage(context),
), ),
), ],
Align( ),
alignment: Alignment.bottomRight, clipBehavior: Clip.antiAlias,
child: (() { child: buildImage(context),
final subtitle =
comic.subtitle?.replaceAll('\n', '').trim();
final text = comic.description.isNotEmpty
? comic.description.split('|').join('\n')
: (subtitle?.isNotEmpty == true
? subtitle
: null);
final scale =
(appdata.settings['comicTileScale'] as num)
.toDouble();
final fortSize = scale < 0.85
? 8.0 // 小尺寸
: (scale < 1.0 ? 10.0 : 12.0);
if (text == null) {
return const SizedBox
.shrink(); // 如果没有文本,则不显示任何内容
}
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 2, vertical: 2),
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(10.0),
),
child: Container(
color: Colors.black.toOpacity(0.5),
child: Padding(
padding:
const EdgeInsets.fromLTRB(8, 6, 8, 6),
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth,
),
child: Text(
text,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: fortSize,
color: Colors.white,
),
textAlign: TextAlign.right,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
),
),
);
})(),
),
],
), ),
), ),
), Align(
Padding( alignment: Alignment.bottomRight,
padding: const EdgeInsets.fromLTRB(8, 4, 8, 0), child: (() {
child: Text( final subtitle =
comic.title.replaceAll('\n', ''), comic.subtitle?.replaceAll('\n', '').trim();
style: const TextStyle( final text = comic.description.isNotEmpty
fontWeight: FontWeight.w500, ? comic.description.split('|').join('\n')
), : (subtitle?.isNotEmpty == true ? subtitle : null);
maxLines: 1, final fortSize = constraints.maxWidth < 80
overflow: TextOverflow.ellipsis, ? 8.0
: constraints.maxWidth < 150
? 10.0
: 12.0;
if (text == null) {
return const SizedBox();
}
var children = <Widget>[];
for (var line in text.split('\n')) {
children.add(Container(
margin: const EdgeInsets.fromLTRB(2, 0, 2, 2),
padding: constraints.maxWidth < 80
? const EdgeInsets.fromLTRB(3, 1, 3, 1)
: constraints.maxWidth < 150
? const EdgeInsets.fromLTRB(4, 2, 4, 2)
: const EdgeInsets.fromLTRB(5, 2, 5, 2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.black.toOpacity(0.5),
),
constraints: BoxConstraints(
maxWidth: constraints.maxWidth,
),
child: Text(
line,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: fortSize,
color: Colors.white,
),
textAlign: TextAlign.right,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
));
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: children,
);
})(),
), ),
), ],
], ),
), ),
); Padding(
}, padding: const EdgeInsets.fromLTRB(4, 4, 4, 0),
)); child: TextScroll(
comic.title.replaceAll('\n', ''),
mode: TextScrollMode.endless,
style: const TextStyle(
fontWeight: FontWeight.w500,
),
delayBefore: Duration(milliseconds: 500),
velocity: const Velocity(pixelsPerSecond: Offset(40, 0)),
),
),
],
).paddingHorizontal(6).paddingVertical(8),
);
},
);
} }
List<String> _splitText(String text) { List<String> _splitText(String text) {
@@ -807,7 +806,10 @@ class _SliverGridComics extends StatelessWidget {
duration: const Duration(milliseconds: 150), duration: const Duration(milliseconds: 150),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? Theme.of(context).colorScheme.secondaryContainer.toOpacity(0.72) ? Theme.of(context)
.colorScheme
.secondaryContainer
.toOpacity(0.72)
: null, : null,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
@@ -901,13 +903,13 @@ class ComicListState extends State<ComicList> {
late bool enablePageStorage = widget.enablePageStorage; late bool enablePageStorage = widget.enablePageStorage;
Map<String, dynamic> get state => { Map<String, dynamic> get state => {
'maxPage': _maxPage, 'maxPage': _maxPage,
'data': _data, 'data': _data,
'page': _page, 'page': _page,
'error': _error, 'error': _error,
'loading': _loading, 'loading': _loading,
'nextUrl': _nextUrl, 'nextUrl': _nextUrl,
}; };
void restoreState(Map<String, dynamic>? state) { void restoreState(Map<String, dynamic>? state) {
if (state == null || !enablePageStorage) { if (state == null || !enablePageStorage) {
@@ -924,7 +926,7 @@ class ComicListState extends State<ComicList> {
} }
void storeState() { void storeState() {
if(enablePageStorage) { if (enablePageStorage) {
PageStorage.of(context).writeState(context, state); PageStorage.of(context).writeState(context, state);
} }
} }
@@ -1094,11 +1096,11 @@ class ComicListState extends State<ComicList> {
while (_data[page] == null) { while (_data[page] == null) {
await _fetchNext(); await _fetchNext();
} }
if(mounted) { if (mounted) {
setState(() {}); setState(() {});
} }
} catch (e) { } catch (e) {
if(mounted) { if (mounted) {
setState(() { setState(() {
_error = e.toString(); _error = e.toString();
}); });

View File

@@ -8,6 +8,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:syntax_highlight/syntax_highlight.dart';
import 'package:text_scroll/text_scroll.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/app_page_route.dart'; import 'package:venera/foundation/app_page_route.dart';
import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/appdata.dart';
@@ -44,4 +46,5 @@ part 'select.dart';
part 'side_bar.dart'; 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';

View File

@@ -28,6 +28,9 @@ class _MenuRoute<T> extends PopupRoute<T> {
var width = entries.first.icon == null ? 216.0 : 242.0; var width = entries.first.icon == null ? 216.0 : 242.0;
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
var left = location.dx; var left = location.dx;
if (left < 10) {
left = 10;
}
if (left + width > size.width - 10) { if (left + width > size.width - 10) {
left = size.width - width - 10; left = size.width - width - 10;
} }

View File

@@ -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) {

View File

@@ -10,7 +10,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.1.3"; final version = "1.1.4";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
class _Appdata { class _Appdata {
@@ -12,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));
@@ -24,6 +25,9 @@ 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();
}
} }
void addSearchHistory(String keyword) { void addSearchHistory(String keyword) {
@@ -76,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() {
@@ -120,9 +143,14 @@ class _Settings with ChangeNotifier {
'quickFavorite': null, 'quickFavorite': null,
'enableTurnPageByVolumeKey': true, 'enableTurnPageByVolumeKey': true,
'enableClockAndBatteryInfoInReader': true, 'enableClockAndBatteryInfoInReader': true,
'ignoreCertificateErrors': false,
'authorizationRequired': false, 'authorizationRequired': false,
'onClickFavorite': 'viewDetail', // viewDetail, read 'onClickFavorite': 'viewDetail', // viewDetail, read
'enableDnsOverrides': false,
'dnsOverrides': {},
'enableCustomImageProcessing': false,
'customImageProcessing': _defaultCustomImageProcessing,
'sni': true,
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
}; };
operator [](String key) { operator [](String key) {
@@ -139,3 +167,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;
}
''';

View File

@@ -73,6 +73,7 @@ class FavoriteItem implements Comic {
@override @override
String get description { String get description {
var time = this.time.substring(0, 10);
return appdata.settings['comicDisplayMode'] == 'detailed' return appdata.settings['comicDisplayMode'] == 'detailed'
? "$time | ${type == ComicType.local ? 'local' : type.comicSource?.name ?? "Unknown"}" ? "$time | ${type == ComicType.local ? 'local' : type.comicSource?.name ?? "Unknown"}"
: "${type.comicSource?.name ?? "Unknown"} | $time"; : "${type.comicSource?.name ?? "Unknown"} | $time";

View File

@@ -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,25 +24,50 @@ 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(
cumulativeBytesLoaded: event.currentBytes, cumulativeBytesLoaded: event.currentBytes,
expectedTotalBytes: event.totalBytes, expectedTotalBytes: event.totalBytes,
)); ));
if (event.imageBytes != null) { if (event.imageBytes != null) {
return event.imageBytes!; 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 @override

View File

@@ -58,6 +58,11 @@ class JsEngine with _JSEngineApi {
JsEngine().init(); JsEngine().init();
} }
void resetDio() {
_dio = AppDio(BaseOptions(
responseType: ResponseType.plain, validateStatus: (status) => true));
}
Future<void> init() async { Future<void> init() async {
if (!_closed) { if (!_closed) {
return; return;
@@ -198,7 +203,8 @@ class JsEngine with _JSEngineApi {
..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy"; ..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
}, },
); );
dio.interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!)); dio.interceptors
.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
dio.interceptors.add(LogInterceptor()); dio.interceptors.add(LogInterceptor());
} }
response = await dio!.request(req["url"], response = await dio!.request(req["url"],

View File

@@ -108,7 +108,6 @@ class MyLogInterceptor implements Interceptor {
class AppDio with DioMixin { class AppDio with DioMixin {
String? _proxy = proxy; String? _proxy = proxy;
static bool get ignoreCertificateErrors => appdata.settings['ignoreCertificateErrors'] == true;
AppDio([BaseOptions? options]) { AppDio([BaseOptions? options]) {
this.options = options ?? BaseOptions(); this.options = options ?? BaseOptions();
@@ -116,9 +115,6 @@ class AppDio with DioMixin {
proxySettings: proxy == null proxySettings: proxy == null
? const rhttp.ProxySettings.noProxy() ? const rhttp.ProxySettings.noProxy()
: rhttp.ProxySettings.proxy(proxy!), : rhttp.ProxySettings.proxy(proxy!),
tlsSettings: rhttp.TlsSettings(
verifyCertificates: !ignoreCertificateErrors,
),
)); ));
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!)); interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
interceptors.add(NetworkCacheManager()); interceptors.add(NetworkCacheManager());
@@ -196,9 +192,6 @@ class AppDio with DioMixin {
proxySettings: proxy == null proxySettings: proxy == null
? const rhttp.ProxySettings.noProxy() ? const rhttp.ProxySettings.noProxy()
: rhttp.ProxySettings.proxy(proxy!), : rhttp.ProxySettings.proxy(proxy!),
tlsSettings: rhttp.TlsSettings(
verifyCertificates: !ignoreCertificateErrors,
),
)); ));
} }
try { try {
@@ -222,6 +215,22 @@ class AppDio with DioMixin {
class RHttpAdapter implements HttpClientAdapter { class RHttpAdapter implements HttpClientAdapter {
rhttp.ClientSettings settings; rhttp.ClientSettings settings;
static Map<String, List<String>> _getOverrides() {
if (!appdata.settings['enableDnsOverrides'] == true) {
return {};
}
var config = appdata.settings["dnsOverrides"];
var result = <String, List<String>>{};
if (config is Map) {
for (var entry in config.entries) {
if (entry.key is String && entry.value is String) {
result[entry.key] = [entry.value];
}
}
}
return result;
}
RHttpAdapter([this.settings = const rhttp.ClientSettings()]) { RHttpAdapter([this.settings = const rhttp.ClientSettings()]) {
settings = settings.copyWith( settings = settings.copyWith(
redirectSettings: const rhttp.RedirectSettings.limited(5), redirectSettings: const rhttp.RedirectSettings.limited(5),
@@ -231,8 +240,9 @@ class RHttpAdapter implements HttpClientAdapter {
keepAlivePing: Duration(seconds: 30), keepAlivePing: Duration(seconds: 30),
), ),
throwOnStatusCode: false, throwOnStatusCode: false,
dnsSettings: rhttp.DnsSettings.static(overrides: _getOverrides()),
tlsSettings: rhttp.TlsSettings( tlsSettings: rhttp.TlsSettings(
verifyCertificates: !AppDio.ignoreCertificateErrors, sni: appdata.settings['sni'] != false,
), ),
); );
} }

View File

@@ -111,13 +111,12 @@ class _BodyState extends State<_Body> {
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (App.isDesktop) Tooltip(
Tooltip( message: "Edit".tl,
message: "Edit".tl, child: IconButton(
child: IconButton( onPressed: () => edit(source),
onPressed: () => edit(source), icon: const Icon(Icons.edit_note)),
icon: const Icon(Icons.edit_note)), ),
),
Tooltip( Tooltip(
message: "Update".tl, message: "Update".tl,
child: IconButton( child: IconButton(
@@ -165,7 +164,8 @@ class _BodyState extends State<_Body> {
} }
} else { } else {
current = item.value['options'] current = item.value['options']
.firstWhere((e) => e['value'] == current)['text'] ?? current; .firstWhere((e) => e['value'] == current)['text'] ??
current;
} }
yield ListTile( yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)), title: Text((item.value['title'] as String).ts(source.key)),
@@ -249,27 +249,35 @@ class _BodyState extends State<_Body> {
} }
void edit(ComicSource source) async { void edit(ComicSource source) async {
try { if (App.isDesktop) {
await Process.run("code", [source.filePath], runInShell: true); try {
await showDialog( await Process.run("code", [source.filePath], runInShell: true);
await showDialog(
context: App.rootContext, context: App.rootContext,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text("Reload Configs"), title: const Text("Reload Configs"),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text("cancel")), child: const Text("cancel")),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await ComicSource.reload(); await ComicSource.reload();
App.forceRebuild(); App.forceRebuild();
}, },
child: const Text("continue")), child: const Text("continue")),
], ],
)); ),
} catch (e) { );
context.showMessage(message: "Failed to launch vscode"); return;
} catch (e) {
//
}
} }
context.to(() => _EditFilePage(source.filePath)).then((value) async {
await ComicSource.reload();
setState(() {});
});
} }
static Future<void> update(ComicSource source) async { static Future<void> update(ComicSource source) async {
@@ -300,12 +308,14 @@ class _BodyState extends State<_Body> {
} }
Widget buildCard(BuildContext context) { Widget buildCard(BuildContext context) {
Widget buildButton({required Widget child, required VoidCallback onPressed}) { Widget buildButton(
{required Widget child, required VoidCallback onPressed}) {
return Button.normal( return Button.normal(
onPressed: onPressed, onPressed: onPressed,
child: child, child: child,
).fixHeight(32); ).fixHeight(32);
} }
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
@@ -561,3 +571,51 @@ void _addAllPagesWithComicSource(ComicSource source) {
appdata.saveData(); appdata.saveData();
} }
class _EditFilePage extends StatefulWidget {
const _EditFilePage(this.path);
final String path;
@override
State<_EditFilePage> createState() => __EditFilePageState();
}
class __EditFilePageState extends State<_EditFilePage> {
var current = '';
@override
void initState() {
super.initState();
current = File(widget.path).readAsStringSync();
}
@override
void dispose() {
File(widget.path).writeAsStringSync(current);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: Appbar(
title: Text("Edit".tl),
),
body: Column(
children: [
Container(
height: 0.6,
color: context.colorScheme.outlineVariant,
),
Expanded(
child: CodeEditor(
initialValue: current,
onChanged: (value) => current = value,
),
),
],
),
);
}
}

View File

@@ -404,8 +404,9 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
var file = await CBZ.export(c); var file = await CBZ.export(c);
await saveFile(filename: file.name, file: file); await saveFile(filename: file.name, file: file);
await file.delete(); await file.delete();
} catch (e) { } catch (e, s) {
context.showMessage(message: e.toString()); context.showMessage(message: e.toString());
Log.error("CBZ Export", e, s);
} }
controller.close(); controller.close();
}), }),

View File

@@ -139,7 +139,9 @@ class _SearchPageState extends State<SearchPage> {
@override @override
void initState() { void initState() {
var defaultSearchTarget = appdata.settings['defaultSearchTarget']; var defaultSearchTarget = appdata.settings['defaultSearchTarget'];
if (defaultSearchTarget != null && if (defaultSearchTarget == "_aggregated_") {
aggregatedSearch = true;
} else if (defaultSearchTarget != null &&
ComicSource.find(defaultSearchTarget) != null) { ComicSource.find(defaultSearchTarget) != null) {
searchTarget = defaultSearchTarget; searchTarget = defaultSearchTarget;
} else { } else {

View File

@@ -45,8 +45,9 @@ class _SearchResultPageState extends State<SearchResultPage> {
if (suggestionsController.entry != null) { if (suggestionsController.entry != null) {
suggestionsController.remove(); suggestionsController.remove();
} }
text = checkAutoLanguage(text);
setState(() { setState(() {
this.text = text; this.text = text!;
}); });
appdata.addSearchHistory(text); appdata.addSearchHistory(text);
controller.currentText = text; controller.currentText = text;
@@ -92,13 +93,33 @@ class _SearchResultPageState extends State<SearchResultPage> {
super.dispose(); super.dispose();
} }
String checkAutoLanguage(String text) {
var setting = appdata.settings["autoAddLanguageFilter"] ?? 'none';
if (setting == 'none') {
return text;
}
var searchSource = sourceKey;
// TODO: Move it to a better place
const enabledSources = [
'nhentai',
'ehentai',
];
if (!enabledSources.contains(searchSource)) {
return text;
}
if (!text.contains('language:')) {
return '$text language:$setting';
}
return text;
}
@override @override
void initState() { void initState() {
sourceKey = widget.sourceKey;
controller = SearchBarController( controller = SearchBarController(
currentText: widget.text, currentText: checkAutoLanguage(widget.text),
onSearch: search, onSearch: search,
); );
sourceKey = widget.sourceKey;
options = widget.options ?? const []; options = widget.options ?? const [];
validateOptions(); validateOptions();
text = widget.text; text = widget.text;
@@ -162,6 +183,12 @@ class _SearchResultPageState extends State<SearchResultPage> {
child: IconButton( child: IconButton(
icon: const Icon(Icons.tune), icon: const Icon(Icons.tune),
onPressed: () async { onPressed: () async {
if (suggestionOverlay != null) {
suggestionsController.remove();
}
var previousOptions = options;
var previousSourceKey = sourceKey;
await showDialog( await showDialog(
context: context, context: context,
useRootNavigator: true, useRootNavigator: true,
@@ -169,7 +196,11 @@ class _SearchResultPageState extends State<SearchResultPage> {
return _SearchSettingsDialog(state: this); return _SearchSettingsDialog(state: this);
}, },
); );
setState(() {}); if (previousOptions != options || previousSourceKey != sourceKey) {
text = checkAutoLanguage(controller.text);
controller.currentText = text;
setState(() {});
}
}, },
), ),
); );

View File

@@ -61,6 +61,10 @@ class _AboutSettingsState extends State<AboutSettings> {
}, },
).fixHeight(32), ).fixHeight(32),
).toSliver(), ).toSliver(),
_SwitchSetting(
title: "Check for updates on startup".tl,
settingKey: "checkUpdateOnStart",
).toSliver(),
ListTile( ListTile(
title: const Text("Github"), title: const Text("Github"),
trailing: const Icon(Icons.open_in_new), trailing: const Icon(Icons.open_in_new),
@@ -102,7 +106,9 @@ Future<void> checkUpdateUi([bool showMessageIfNoUpdate = true]) async {
return ContentDialog( return ContentDialog(
title: "New version available".tl, title: "New version available".tl,
content: Text( content: Text(
"A new version is available. Do you want to update now?".tl), "A new version is available. Do you want to update now?"
.tl)
.paddingHorizontal(8),
actions: [ actions: [
Button.text( Button.text(
onPressed: () { onPressed: () {

View File

@@ -88,6 +88,30 @@ class _ExploreSettingsState extends State<ExploreSettings> {
title: "Keyword blocking".tl, title: "Keyword blocking".tl,
builder: () => const _ManageBlockingWordView(), builder: () => const _ManageBlockingWordView(),
).toSliver(), ).toSliver(),
SelectSetting(
title: "Default Search Target".tl,
settingKey: "defaultSearchTarget",
optionTranslation: {
'_aggregated_': "Aggregated".tl,
...((){
var map = <String, String>{};
for (var c in ComicSource.all()) {
map[c.key] = c.name;
}
return map;
}()),
},
).toSliver(),
SelectSetting(
title: "Auto Language Filters".tl,
settingKey: "autoAddLanguageFilter",
optionTranslation: {
'none': "None".tl,
'chinese': "Chinese",
'english': "English",
'japanese': "Japanese",
},
).toSliver(),
], ],
); );
} }
@@ -150,7 +174,7 @@ class _ManageBlockingWordViewState extends State<_ManageBlockingWordView> {
errorText: error, errorText: error,
), ),
onChanged: (s) { onChanged: (s) {
if(error != null){ if (error != null) {
setState(() { setState(() {
error = null; error = null;
}); });
@@ -160,7 +184,8 @@ class _ManageBlockingWordViewState extends State<_ManageBlockingWordView> {
actions: [ actions: [
Button.filled( Button.filled(
onPressed: () { onPressed: () {
if(appdata.settings["blockedWords"].contains(controller.text)){ if (appdata.settings["blockedWords"]
.contains(controller.text)) {
setState(() { setState(() {
error = "Keyword already exists".tl; error = "Keyword already exists".tl;
}); });

View File

@@ -17,6 +17,10 @@ class _NetworkSettingsState extends State<NetworkSettings> {
title: "Proxy".tl, title: "Proxy".tl,
builder: () => const _ProxySettingView(), builder: () => const _ProxySettingView(),
).toSliver(), ).toSliver(),
_PopupWindowSetting(
title: "DNS Overrides".tl,
builder: () => const _DNSOverrides(),
).toSliver(),
_SliderSetting( _SliderSetting(
title: "Download Threads".tl, title: "Download Threads".tl,
settingsIndex: 'downloadThreads', settingsIndex: 'downloadThreads',
@@ -42,7 +46,6 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
String port = ''; String port = '';
String username = ''; String username = '';
String password = ''; String password = '';
bool ignoreCertificateErrors = false;
// USERNAME:PASSWORD@HOST:PORT // USERNAME:PASSWORD@HOST:PORT
String toProxyStr() { String toProxyStr() {
@@ -100,7 +103,6 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
void initState() { void initState() {
var proxy = appdata.settings['proxy']; var proxy = appdata.settings['proxy'];
parseProxyString(proxy); parseProxyString(proxy);
ignoreCertificateErrors = appdata.settings['ignoreCertificateErrors'] ?? false;
super.initState(); super.initState();
} }
@@ -146,17 +148,6 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
}, },
), ),
if (type == 'manual') buildManualProxy(), if (type == 'manual') buildManualProxy(),
SwitchListTile(
title: Text("Ignore Certificate Errors".tl),
value: ignoreCertificateErrors,
onChanged: (v) {
setState(() {
ignoreCertificateErrors = v;
});
appdata.settings['ignoreCertificateErrors'] = ignoreCertificateErrors;
appdata.saveData();
},
),
], ],
), ),
), ),
@@ -250,3 +241,139 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
).paddingHorizontal(16).paddingTop(16); ).paddingHorizontal(16).paddingTop(16);
} }
} }
class _DNSOverrides extends StatefulWidget {
const _DNSOverrides();
@override
State<_DNSOverrides> createState() => __DNSOverridesState();
}
class __DNSOverridesState extends State<_DNSOverrides> {
var overrides = <(String, String)>[];
@override
void initState() {
for (var entry in (appdata.settings['dnsOverrides'] as Map).entries) {
if (entry.key is String && entry.value is String) {
overrides.add((entry.key, entry.value));
}
}
super.initState();
}
@override
void dispose() {
var map = <String, String>{};
for (var entry in overrides) {
map[entry.$1] = entry.$2;
}
appdata.settings['dnsOverrides'] = map;
appdata.saveData();
JsEngine().resetDio();
super.dispose();
}
@override
Widget build(BuildContext context) {
return PopUpWidgetScaffold(
title: "DNS Overrides".tl,
body: SingleChildScrollView(
child: Column(
children: [
_SwitchSetting(
title: "Enable DNS Overrides".tl,
settingKey: "enableDnsOverrides",
),
_SwitchSetting(
title: "Server Name Indication",
settingKey: "sni",
),
const SizedBox(height: 8),
Container(
height: 1,
margin: EdgeInsets.symmetric(horizontal: 8),
color: context.colorScheme.outlineVariant,
),
for (var i = 0; i < overrides.length; i++) buildOverride(i),
const SizedBox(height: 8),
TextButton.icon(
onPressed: () {
setState(() {
overrides.add(('', ''));
});
},
icon: const Icon(Icons.add),
label: Text("Add".tl),
),
],
),
),
);
}
Widget buildOverride(int index) {
var entry = overrides[index];
return Container(
height: 48,
margin: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: context.colorScheme.outlineVariant,
),
left: BorderSide(
color: context.colorScheme.outlineVariant,
),
right: BorderSide(
color: context.colorScheme.outlineVariant,
),
),
),
child: Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Domain".tl,
),
controller: TextEditingController(text: entry.$1),
onChanged: (v) {
overrides[index] = (v, entry.$2);
},
).paddingHorizontal(8),
),
Container(
width: 1,
color: context.colorScheme.outlineVariant,
),
Expanded(
child: TextField(
decoration: InputDecoration(
border: InputBorder.none,
hintText: "IP".tl,
),
controller: TextEditingController(text: entry.$2),
onChanged: (v) {
overrides[index] = (entry.$1, v);
},
).paddingHorizontal(8),
),
Container(
width: 1,
color: context.colorScheme.outlineVariant,
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () {
setState(() {
overrides.removeAt(index);
});
},
),
],
),
);
}
}

View File

@@ -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,
@@ -93,7 +101,7 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call('limitImageWidth'); widget.onChanged?.call('limitImageWidth');
}, },
).toSliver(), ).toSliver(),
if(App.isAndroid) if (App.isAndroid)
_SwitchSetting( _SwitchSetting(
title: 'Turn page by volume keys'.tl, title: 'Turn page by volume keys'.tl,
settingKey: 'enableTurnPageByVolumeKey', settingKey: 'enableTurnPageByVolumeKey',
@@ -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;
},
),
),
),
)
],
),
);
}
}

View File

@@ -12,6 +12,7 @@ import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/js_engine.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/log.dart';
import 'package:venera/network/app_dio.dart'; import 'package:venera/network/app_dio.dart';

View File

@@ -8,6 +8,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:venera/components/components.dart'; import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/network/app_dio.dart'; import 'package:venera/network/app_dio.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
@@ -83,6 +84,33 @@ class _AppWebviewState extends State<AppWebview> {
double _progress = 0; double _progress = 0;
late var future = _createWebviewEnvironment();
Future<WebViewEnvironment> _createWebviewEnvironment() async {
var proxy = appdata.settings['proxy'].toString();
if (proxy != "system" && proxy != "direct") {
var proxyAvailable = await WebViewFeature.isFeatureSupported(
WebViewFeature.PROXY_OVERRIDE);
if (proxyAvailable) {
ProxyController proxyController = ProxyController.instance();
await proxyController.clearProxyOverride();
if (!proxy.contains("://")) {
proxy = "http://$proxy";
}
await proxyController.setProxyOverride(
settings: ProxySettings(
proxyRules: [ProxyRule(url: proxy)],
),
);
}
}
return WebViewEnvironment.create(
settings: WebViewEnvironmentSettings(
userDataFolder: "${App.dataPath}\\webview",
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final actions = [ final actions = [
@@ -121,20 +149,17 @@ class _AppWebviewState extends State<AppWebview> {
Widget body = (App.isWindows && AppWebview.webViewEnvironment == null) Widget body = (App.isWindows && AppWebview.webViewEnvironment == null)
? FutureBuilder( ? FutureBuilder(
future: WebViewEnvironment.create( future: future,
settings: WebViewEnvironmentSettings(
userDataFolder: "${App.dataPath}\\webview",
),
),
builder: (context, e) { builder: (context, e) {
if(e.error != null) { if (e.error != null) {
return Center(child: Text("Error: ${e.error}")); return Center(child: Text("Error: ${e.error}"));
} }
if(e.data == null) { if (e.data == null) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
AppWebview.webViewEnvironment = e.data; AppWebview.webViewEnvironment = e.data;
return createWebviewWithEnvironment(AppWebview.webViewEnvironment); return createWebviewWithEnvironment(
AppWebview.webViewEnvironment);
}, },
) )
: createWebviewWithEnvironment(AppWebview.webViewEnvironment); : createWebviewWithEnvironment(AppWebview.webViewEnvironment);

View File

@@ -208,6 +208,7 @@ abstract class CBZ {
), ),
); );
var cbz = File(FilePath.join(App.cachePath, sanitizeFileName('${comic.title}.cbz'))); var cbz = File(FilePath.join(App.cachePath, sanitizeFileName('${comic.title}.cbz')));
if (cbz.existsSync()) cbz.deleteSync();
await _compress(cache.path, cbz.path); await _compress(cache.path, cbz.path);
cache.deleteSync(recursive: true); cache.deleteSync(recursive: true);
return cbz; return cbz;

View File

@@ -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();

View File

@@ -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();

View File

@@ -408,8 +408,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "1657f62fe7545ac43a339e0a5ee2b82bacd81e9f" ref: "9c99ac258a11f8e91761a5466a190efba3ca64af"
resolved-ref: "1657f62fe7545ac43a339e0a5ee2b82bacd81e9f" resolved-ref: "9c99ac258a11f8e91761a5466a190efba3ca64af"
url: "https://github.com/wgh136/flutter_qjs" url: "https://github.com/wgh136/flutter_qjs"
source: git source: git
version: "0.3.7" version: "0.3.7"
@@ -944,6 +944,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
syntax_highlight:
dependency: "direct main"
description:
name: syntax_highlight
sha256: ee33b6aa82cc722bb9b40152a792181dee222353b486c0255fde666a3e3a4997
url: "https://pub.dev"
source: hosted
version: "0.4.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -960,6 +968,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.3" version: "0.7.3"
text_scroll:
dependency: "direct main"
description:
name: text_scroll
sha256: "7869d86a6fdd725dee56bdd150216a99f0372b82fbfcac319214dbd5f36e1908"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -1125,10 +1141,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: zip_flutter name: zip_flutter
sha256: "955b53d58709fcd9feefbed3d41b5522bc5273e677603e9fc67017a81e568d24" sha256: be21152c35fcb6d0ef4ce89fc3aed681f7adc0db5490ca3eb5893f23fd20e646
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.5" version: "0.0.6"
sdks: sdks:
dart: ">=3.6.0 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.1" flutter: ">=3.27.1"

View File

@@ -2,7 +2,7 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.1.3+113 version: 1.1.4+114
environment: environment:
sdk: '>=3.6.0 <4.0.0' sdk: '>=3.6.0 <4.0.0'
@@ -21,7 +21,7 @@ dependencies:
flutter_qjs: flutter_qjs:
git: git:
url: https://github.com/wgh136/flutter_qjs url: https://github.com/wgh136/flutter_qjs
ref: 1657f62fe7545ac43a339e0a5ee2b82bacd81e9f ref: 9c99ac258a11f8e91761a5466a190efba3ca64af
crypto: ^3.0.6 crypto: ^3.0.6
dio: ^5.7.0 dio: ^5.7.0
html: ^0.15.5 html: ^0.15.5
@@ -51,7 +51,7 @@ dependencies:
sliver_tools: ^0.2.12 sliver_tools: ^0.2.12
flutter_file_dialog: ^3.0.2 flutter_file_dialog: ^3.0.2
file_selector: ^1.0.3 file_selector: ^1.0.3
zip_flutter: ^0.0.5 zip_flutter: ^0.0.6
lodepng_flutter: lodepng_flutter:
git: git:
url: https://github.com/venera-app/lodepng_flutter url: https://github.com/venera-app/lodepng_flutter
@@ -71,6 +71,8 @@ dependencies:
dynamic_color: ^1.7.0 dynamic_color: ^1.7.0
shimmer: ^3.0.0 shimmer: ^3.0.0
flutter_memory_info: ^0.0.1 flutter_memory_info: ^0.0.1
syntax_highlight: ^0.4.0
text_scroll: ^0.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: