mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
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
|
||||
|
@@ -18,7 +18,7 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
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
|
||||
}
|
||||
|
||||
|
@@ -218,7 +218,6 @@
|
||||
"Create Folder": "新建文件夹",
|
||||
"Select an image on screen": "选择屏幕上的图片",
|
||||
"Added @count comics to download queue.": "已添加 @count 本漫画到下载队列",
|
||||
"Ignore Certificate Errors": "忽略证书错误",
|
||||
"Authorization Required": "需要身份验证",
|
||||
"Sync": "同步",
|
||||
"The folder is Linked to @source": "文件夹已关联到 @source",
|
||||
@@ -258,7 +257,15 @@
|
||||
"View Detail": "查看详情",
|
||||
"Select a directory which contains multiple cbz/zip files." : "选择一个包含多个cbz/zip文件的目录",
|
||||
"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": {
|
||||
"Home": "首頁",
|
||||
@@ -479,7 +486,6 @@
|
||||
"Create Folder": "新建文件夾",
|
||||
"Select an image on screen": "選擇屏幕上的圖片",
|
||||
"Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列",
|
||||
"Ignore Certificate Errors": "忽略證書錯誤",
|
||||
"Authorization Required": "需要身份驗證",
|
||||
"Sync": "同步",
|
||||
"The folder is Linked to @source": "文件夾已關聯到 @source",
|
||||
@@ -519,6 +525,14 @@
|
||||
"View Detail": "查看詳情",
|
||||
"Select a directory which contains multiple cbz/zip files." : "選擇一個包含多個cbz/zip文件的目錄",
|
||||
"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
383
lib/components/code.dart
Normal 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": "\\}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
@@ -235,110 +235,109 @@ class ComicTile extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildBriefMode(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: _onTap,
|
||||
onLongPress:
|
||||
enableLongPressed ? () => _onLongPressed(context) : null,
|
||||
onSecondaryTapDown: (detail) => onSecondaryTap(detail, context),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: buildImage(context),
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: _onTap,
|
||||
onLongPress: enableLongPressed ? () => _onLongPressed(context) : null,
|
||||
onSecondaryTapDown: (detail) => onSecondaryTap(detail, context),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.toOpacity(0.2),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: (() {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: buildImage(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 0),
|
||||
child: Text(
|
||||
comic.title.replaceAll('\n', ''),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: (() {
|
||||
final subtitle =
|
||||
comic.subtitle?.replaceAll('\n', '').trim();
|
||||
final text = comic.description.isNotEmpty
|
||||
? comic.description.split('|').join('\n')
|
||||
: (subtitle?.isNotEmpty == true ? subtitle : null);
|
||||
final fortSize = constraints.maxWidth < 80
|
||||
? 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) {
|
||||
@@ -807,7 +806,10 @@ class _SliverGridComics extends StatelessWidget {
|
||||
duration: const Duration(milliseconds: 150),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.secondaryContainer.toOpacity(0.72)
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
.toOpacity(0.72)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@@ -901,13 +903,13 @@ class ComicListState extends State<ComicList> {
|
||||
late bool enablePageStorage = widget.enablePageStorage;
|
||||
|
||||
Map<String, dynamic> get state => {
|
||||
'maxPage': _maxPage,
|
||||
'data': _data,
|
||||
'page': _page,
|
||||
'error': _error,
|
||||
'loading': _loading,
|
||||
'nextUrl': _nextUrl,
|
||||
};
|
||||
'maxPage': _maxPage,
|
||||
'data': _data,
|
||||
'page': _page,
|
||||
'error': _error,
|
||||
'loading': _loading,
|
||||
'nextUrl': _nextUrl,
|
||||
};
|
||||
|
||||
void restoreState(Map<String, dynamic>? state) {
|
||||
if (state == null || !enablePageStorage) {
|
||||
@@ -924,7 +926,7 @@ class ComicListState extends State<ComicList> {
|
||||
}
|
||||
|
||||
void storeState() {
|
||||
if(enablePageStorage) {
|
||||
if (enablePageStorage) {
|
||||
PageStorage.of(context).writeState(context, state);
|
||||
}
|
||||
}
|
||||
@@ -1094,11 +1096,11 @@ class ComicListState extends State<ComicList> {
|
||||
while (_data[page] == null) {
|
||||
await _fetchNext();
|
||||
}
|
||||
if(mounted) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
} catch (e) {
|
||||
if(mounted) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
});
|
||||
|
@@ -8,6 +8,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.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_page_route.dart';
|
||||
import 'package:venera/foundation/appdata.dart';
|
||||
@@ -45,3 +47,4 @@ part 'side_bar.dart';
|
||||
part 'comic.dart';
|
||||
part 'effects.dart';
|
||||
part 'gesture.dart';
|
||||
part 'code.dart';
|
@@ -28,6 +28,9 @@ class _MenuRoute<T> extends PopupRoute<T> {
|
||||
var width = entries.first.icon == null ? 216.0 : 242.0;
|
||||
final size = MediaQuery.of(context).size;
|
||||
var left = location.dx;
|
||||
if (left < 10) {
|
||||
left = 10;
|
||||
}
|
||||
if (left + width > size.width - 10) {
|
||||
left = size.width - width - 10;
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -10,7 +10,7 @@ export "widget_utils.dart";
|
||||
export "context.dart";
|
||||
|
||||
class _App {
|
||||
final version = "1.1.3";
|
||||
final version = "1.1.4";
|
||||
|
||||
bool get isAndroid => Platform.isAndroid;
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/utils/data_sync.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
|
||||
class _Appdata {
|
||||
@@ -12,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));
|
||||
@@ -24,6 +25,9 @@ class _Appdata {
|
||||
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
||||
await file.writeAsString(data);
|
||||
_isSavingData = false;
|
||||
if (sync) {
|
||||
DataSync().uploadData();
|
||||
}
|
||||
}
|
||||
|
||||
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>{};
|
||||
|
||||
void writeImplicitData() {
|
||||
@@ -120,9 +143,14 @@ class _Settings with ChangeNotifier {
|
||||
'quickFavorite': null,
|
||||
'enableTurnPageByVolumeKey': true,
|
||||
'enableClockAndBatteryInfoInReader': true,
|
||||
'ignoreCertificateErrors': false,
|
||||
'authorizationRequired': false,
|
||||
'onClickFavorite': 'viewDetail', // viewDetail, read
|
||||
'enableDnsOverrides': false,
|
||||
'dnsOverrides': {},
|
||||
'enableCustomImageProcessing': false,
|
||||
'customImageProcessing': _defaultCustomImageProcessing,
|
||||
'sni': true,
|
||||
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
||||
};
|
||||
|
||||
operator [](String key) {
|
||||
@@ -139,3 +167,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;
|
||||
}
|
||||
''';
|
@@ -73,6 +73,7 @@ class FavoriteItem implements Comic {
|
||||
|
||||
@override
|
||||
String get description {
|
||||
var time = this.time.substring(0, 10);
|
||||
return appdata.settings['comicDisplayMode'] == 'detailed'
|
||||
? "$time | ${type == ComicType.local ? 'local' : type.comicSource?.name ?? "Unknown"}"
|
||||
: "${type.comicSource?.name ?? "Unknown"} | $time";
|
||||
|
@@ -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
|
||||
|
@@ -58,6 +58,11 @@ class JsEngine with _JSEngineApi {
|
||||
JsEngine().init();
|
||||
}
|
||||
|
||||
void resetDio() {
|
||||
_dio = AppDio(BaseOptions(
|
||||
responseType: ResponseType.plain, validateStatus: (status) => true));
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
if (!_closed) {
|
||||
return;
|
||||
@@ -198,7 +203,8 @@ class JsEngine with _JSEngineApi {
|
||||
..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
|
||||
},
|
||||
);
|
||||
dio.interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
||||
dio.interceptors
|
||||
.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
||||
dio.interceptors.add(LogInterceptor());
|
||||
}
|
||||
response = await dio!.request(req["url"],
|
||||
|
@@ -108,7 +108,6 @@ class MyLogInterceptor implements Interceptor {
|
||||
|
||||
class AppDio with DioMixin {
|
||||
String? _proxy = proxy;
|
||||
static bool get ignoreCertificateErrors => appdata.settings['ignoreCertificateErrors'] == true;
|
||||
|
||||
AppDio([BaseOptions? options]) {
|
||||
this.options = options ?? BaseOptions();
|
||||
@@ -116,9 +115,6 @@ class AppDio with DioMixin {
|
||||
proxySettings: proxy == null
|
||||
? const rhttp.ProxySettings.noProxy()
|
||||
: rhttp.ProxySettings.proxy(proxy!),
|
||||
tlsSettings: rhttp.TlsSettings(
|
||||
verifyCertificates: !ignoreCertificateErrors,
|
||||
),
|
||||
));
|
||||
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
||||
interceptors.add(NetworkCacheManager());
|
||||
@@ -196,9 +192,6 @@ class AppDio with DioMixin {
|
||||
proxySettings: proxy == null
|
||||
? const rhttp.ProxySettings.noProxy()
|
||||
: rhttp.ProxySettings.proxy(proxy!),
|
||||
tlsSettings: rhttp.TlsSettings(
|
||||
verifyCertificates: !ignoreCertificateErrors,
|
||||
),
|
||||
));
|
||||
}
|
||||
try {
|
||||
@@ -222,6 +215,22 @@ class AppDio with DioMixin {
|
||||
class RHttpAdapter implements HttpClientAdapter {
|
||||
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()]) {
|
||||
settings = settings.copyWith(
|
||||
redirectSettings: const rhttp.RedirectSettings.limited(5),
|
||||
@@ -231,8 +240,9 @@ class RHttpAdapter implements HttpClientAdapter {
|
||||
keepAlivePing: Duration(seconds: 30),
|
||||
),
|
||||
throwOnStatusCode: false,
|
||||
dnsSettings: rhttp.DnsSettings.static(overrides: _getOverrides()),
|
||||
tlsSettings: rhttp.TlsSettings(
|
||||
verifyCertificates: !AppDio.ignoreCertificateErrors,
|
||||
sni: appdata.settings['sni'] != false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -111,13 +111,12 @@ class _BodyState extends State<_Body> {
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (App.isDesktop)
|
||||
Tooltip(
|
||||
message: "Edit".tl,
|
||||
child: IconButton(
|
||||
onPressed: () => edit(source),
|
||||
icon: const Icon(Icons.edit_note)),
|
||||
),
|
||||
Tooltip(
|
||||
message: "Edit".tl,
|
||||
child: IconButton(
|
||||
onPressed: () => edit(source),
|
||||
icon: const Icon(Icons.edit_note)),
|
||||
),
|
||||
Tooltip(
|
||||
message: "Update".tl,
|
||||
child: IconButton(
|
||||
@@ -165,7 +164,8 @@ class _BodyState extends State<_Body> {
|
||||
}
|
||||
} else {
|
||||
current = item.value['options']
|
||||
.firstWhere((e) => e['value'] == current)['text'] ?? current;
|
||||
.firstWhere((e) => e['value'] == current)['text'] ??
|
||||
current;
|
||||
}
|
||||
yield ListTile(
|
||||
title: Text((item.value['title'] as String).ts(source.key)),
|
||||
@@ -249,27 +249,35 @@ class _BodyState extends State<_Body> {
|
||||
}
|
||||
|
||||
void edit(ComicSource source) async {
|
||||
try {
|
||||
await Process.run("code", [source.filePath], runInShell: true);
|
||||
await showDialog(
|
||||
if (App.isDesktop) {
|
||||
try {
|
||||
await Process.run("code", [source.filePath], runInShell: true);
|
||||
await showDialog(
|
||||
context: App.rootContext,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("Reload Configs"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("cancel")),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ComicSource.reload();
|
||||
App.forceRebuild();
|
||||
},
|
||||
child: const Text("continue")),
|
||||
],
|
||||
));
|
||||
} catch (e) {
|
||||
context.showMessage(message: "Failed to launch vscode");
|
||||
title: const Text("Reload Configs"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("cancel")),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ComicSource.reload();
|
||||
App.forceRebuild();
|
||||
},
|
||||
child: const Text("continue")),
|
||||
],
|
||||
),
|
||||
);
|
||||
return;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
context.to(() => _EditFilePage(source.filePath)).then((value) async {
|
||||
await ComicSource.reload();
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> update(ComicSource source) async {
|
||||
@@ -300,12 +308,14 @@ class _BodyState extends State<_Body> {
|
||||
}
|
||||
|
||||
Widget buildCard(BuildContext context) {
|
||||
Widget buildButton({required Widget child, required VoidCallback onPressed}) {
|
||||
Widget buildButton(
|
||||
{required Widget child, required VoidCallback onPressed}) {
|
||||
return Button.normal(
|
||||
onPressed: onPressed,
|
||||
child: child,
|
||||
).fixHeight(32);
|
||||
}
|
||||
|
||||
return SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -561,3 +571,51 @@ void _addAllPagesWithComicSource(ComicSource source) {
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -404,8 +404,9 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
||||
var file = await CBZ.export(c);
|
||||
await saveFile(filename: file.name, file: file);
|
||||
await file.delete();
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
context.showMessage(message: e.toString());
|
||||
Log.error("CBZ Export", e, s);
|
||||
}
|
||||
controller.close();
|
||||
}),
|
||||
|
@@ -139,7 +139,9 @@ class _SearchPageState extends State<SearchPage> {
|
||||
@override
|
||||
void initState() {
|
||||
var defaultSearchTarget = appdata.settings['defaultSearchTarget'];
|
||||
if (defaultSearchTarget != null &&
|
||||
if (defaultSearchTarget == "_aggregated_") {
|
||||
aggregatedSearch = true;
|
||||
} else if (defaultSearchTarget != null &&
|
||||
ComicSource.find(defaultSearchTarget) != null) {
|
||||
searchTarget = defaultSearchTarget;
|
||||
} else {
|
||||
|
@@ -45,8 +45,9 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
if (suggestionsController.entry != null) {
|
||||
suggestionsController.remove();
|
||||
}
|
||||
text = checkAutoLanguage(text);
|
||||
setState(() {
|
||||
this.text = text;
|
||||
this.text = text!;
|
||||
});
|
||||
appdata.addSearchHistory(text);
|
||||
controller.currentText = text;
|
||||
@@ -92,13 +93,33 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
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
|
||||
void initState() {
|
||||
sourceKey = widget.sourceKey;
|
||||
controller = SearchBarController(
|
||||
currentText: widget.text,
|
||||
currentText: checkAutoLanguage(widget.text),
|
||||
onSearch: search,
|
||||
);
|
||||
sourceKey = widget.sourceKey;
|
||||
options = widget.options ?? const [];
|
||||
validateOptions();
|
||||
text = widget.text;
|
||||
@@ -162,6 +183,12 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.tune),
|
||||
onPressed: () async {
|
||||
if (suggestionOverlay != null) {
|
||||
suggestionsController.remove();
|
||||
}
|
||||
|
||||
var previousOptions = options;
|
||||
var previousSourceKey = sourceKey;
|
||||
await showDialog(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
@@ -169,7 +196,11 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
return _SearchSettingsDialog(state: this);
|
||||
},
|
||||
);
|
||||
setState(() {});
|
||||
if (previousOptions != options || previousSourceKey != sourceKey) {
|
||||
text = checkAutoLanguage(controller.text);
|
||||
controller.currentText = text;
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@@ -61,6 +61,10 @@ class _AboutSettingsState extends State<AboutSettings> {
|
||||
},
|
||||
).fixHeight(32),
|
||||
).toSliver(),
|
||||
_SwitchSetting(
|
||||
title: "Check for updates on startup".tl,
|
||||
settingKey: "checkUpdateOnStart",
|
||||
).toSliver(),
|
||||
ListTile(
|
||||
title: const Text("Github"),
|
||||
trailing: const Icon(Icons.open_in_new),
|
||||
@@ -102,7 +106,9 @@ Future<void> checkUpdateUi([bool showMessageIfNoUpdate = true]) async {
|
||||
return ContentDialog(
|
||||
title: "New version available".tl,
|
||||
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: [
|
||||
Button.text(
|
||||
onPressed: () {
|
||||
|
@@ -88,6 +88,30 @@ class _ExploreSettingsState extends State<ExploreSettings> {
|
||||
title: "Keyword blocking".tl,
|
||||
builder: () => const _ManageBlockingWordView(),
|
||||
).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,
|
||||
),
|
||||
onChanged: (s) {
|
||||
if(error != null){
|
||||
if (error != null) {
|
||||
setState(() {
|
||||
error = null;
|
||||
});
|
||||
@@ -160,7 +184,8 @@ class _ManageBlockingWordViewState extends State<_ManageBlockingWordView> {
|
||||
actions: [
|
||||
Button.filled(
|
||||
onPressed: () {
|
||||
if(appdata.settings["blockedWords"].contains(controller.text)){
|
||||
if (appdata.settings["blockedWords"]
|
||||
.contains(controller.text)) {
|
||||
setState(() {
|
||||
error = "Keyword already exists".tl;
|
||||
});
|
||||
|
@@ -17,6 +17,10 @@ class _NetworkSettingsState extends State<NetworkSettings> {
|
||||
title: "Proxy".tl,
|
||||
builder: () => const _ProxySettingView(),
|
||||
).toSliver(),
|
||||
_PopupWindowSetting(
|
||||
title: "DNS Overrides".tl,
|
||||
builder: () => const _DNSOverrides(),
|
||||
).toSliver(),
|
||||
_SliderSetting(
|
||||
title: "Download Threads".tl,
|
||||
settingsIndex: 'downloadThreads',
|
||||
@@ -42,7 +46,6 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
|
||||
String port = '';
|
||||
String username = '';
|
||||
String password = '';
|
||||
bool ignoreCertificateErrors = false;
|
||||
|
||||
// USERNAME:PASSWORD@HOST:PORT
|
||||
String toProxyStr() {
|
||||
@@ -100,7 +103,6 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
|
||||
void initState() {
|
||||
var proxy = appdata.settings['proxy'];
|
||||
parseProxyString(proxy);
|
||||
ignoreCertificateErrors = appdata.settings['ignoreCertificateErrors'] ?? false;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -146,17 +148,6 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
|
||||
},
|
||||
),
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import 'package:venera/foundation/appdata.dart';
|
||||
import 'package:venera/foundation/cache_manager.dart';
|
||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||
import 'package:venera/foundation/favorites.dart';
|
||||
import 'package:venera/foundation/js_engine.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/foundation/log.dart';
|
||||
import 'package:venera/network/app_dio.dart';
|
||||
|
@@ -8,6 +8,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:venera/components/components.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/appdata.dart';
|
||||
import 'package:venera/network/app_dio.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
@@ -83,6 +84,33 @@ class _AppWebviewState extends State<AppWebview> {
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final actions = [
|
||||
@@ -121,20 +149,17 @@ class _AppWebviewState extends State<AppWebview> {
|
||||
|
||||
Widget body = (App.isWindows && AppWebview.webViewEnvironment == null)
|
||||
? FutureBuilder(
|
||||
future: WebViewEnvironment.create(
|
||||
settings: WebViewEnvironmentSettings(
|
||||
userDataFolder: "${App.dataPath}\\webview",
|
||||
),
|
||||
),
|
||||
future: future,
|
||||
builder: (context, e) {
|
||||
if(e.error != null) {
|
||||
if (e.error != null) {
|
||||
return Center(child: Text("Error: ${e.error}"));
|
||||
}
|
||||
if(e.data == null) {
|
||||
if (e.data == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
AppWebview.webViewEnvironment = e.data;
|
||||
return createWebviewWithEnvironment(AppWebview.webViewEnvironment);
|
||||
return createWebviewWithEnvironment(
|
||||
AppWebview.webViewEnvironment);
|
||||
},
|
||||
)
|
||||
: createWebviewWithEnvironment(AppWebview.webViewEnvironment);
|
||||
|
@@ -208,6 +208,7 @@ abstract class CBZ {
|
||||
),
|
||||
);
|
||||
var cbz = File(FilePath.join(App.cachePath, sanitizeFileName('${comic.title}.cbz')));
|
||||
if (cbz.existsSync()) cbz.deleteSync();
|
||||
await _compress(cache.path, cbz.path);
|
||||
cache.deleteSync(recursive: true);
|
||||
return cbz;
|
||||
|
@@ -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();
|
||||
|
@@ -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();
|
||||
|
24
pubspec.lock
24
pubspec.lock
@@ -408,8 +408,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "1657f62fe7545ac43a339e0a5ee2b82bacd81e9f"
|
||||
resolved-ref: "1657f62fe7545ac43a339e0a5ee2b82bacd81e9f"
|
||||
ref: "9c99ac258a11f8e91761a5466a190efba3ca64af"
|
||||
resolved-ref: "9c99ac258a11f8e91761a5466a190efba3ca64af"
|
||||
url: "https://github.com/wgh136/flutter_qjs"
|
||||
source: git
|
||||
version: "0.3.7"
|
||||
@@ -944,6 +944,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -960,6 +968,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1125,10 +1141,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: zip_flutter
|
||||
sha256: "955b53d58709fcd9feefbed3d41b5522bc5273e677603e9fc67017a81e568d24"
|
||||
sha256: be21152c35fcb6d0ef4ce89fc3aed681f7adc0db5490ca3eb5893f23fd20e646
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.5"
|
||||
version: "0.0.6"
|
||||
sdks:
|
||||
dart: ">=3.6.0 <4.0.0"
|
||||
flutter: ">=3.27.1"
|
||||
|
@@ -2,7 +2,7 @@ name: venera
|
||||
description: "A comic app."
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.1.3+113
|
||||
version: 1.1.4+114
|
||||
|
||||
environment:
|
||||
sdk: '>=3.6.0 <4.0.0'
|
||||
@@ -21,7 +21,7 @@ dependencies:
|
||||
flutter_qjs:
|
||||
git:
|
||||
url: https://github.com/wgh136/flutter_qjs
|
||||
ref: 1657f62fe7545ac43a339e0a5ee2b82bacd81e9f
|
||||
ref: 9c99ac258a11f8e91761a5466a190efba3ca64af
|
||||
crypto: ^3.0.6
|
||||
dio: ^5.7.0
|
||||
html: ^0.15.5
|
||||
@@ -51,7 +51,7 @@ dependencies:
|
||||
sliver_tools: ^0.2.12
|
||||
flutter_file_dialog: ^3.0.2
|
||||
file_selector: ^1.0.3
|
||||
zip_flutter: ^0.0.5
|
||||
zip_flutter: ^0.0.6
|
||||
lodepng_flutter:
|
||||
git:
|
||||
url: https://github.com/venera-app/lodepng_flutter
|
||||
@@ -71,6 +71,8 @@ dependencies:
|
||||
dynamic_color: ^1.7.0
|
||||
shimmer: ^3.0.0
|
||||
flutter_memory_info: ^0.0.1
|
||||
syntax_highlight: ^0.4.0
|
||||
text_scroll: ^0.2.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user