diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index b5fc5a7..5e6b542 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/android/settings.gradle b/android/settings.gradle index 0d0d669..3a1cb3b 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -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 } diff --git a/assets/translation.json b/assets/translation.json index 61cb8bc..24a5e6c 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -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": "啟動時檢查更新" } } \ No newline at end of file diff --git a/lib/components/code.dart b/lib/components/code.dart new file mode 100644 index 0000000..11d4b13 --- /dev/null +++ b/lib/components/code.dart @@ -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 createState() => _CodeEditorState(); +} + +class _CodeEditorState extends State { + 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 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": "\\}" + } + ] + } + } +} +'''; diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 344cab7..95c8818 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -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 = []; + 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 _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 { late bool enablePageStorage = widget.enablePageStorage; Map 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? state) { if (state == null || !enablePageStorage) { @@ -924,7 +926,7 @@ class ComicListState extends State { } void storeState() { - if(enablePageStorage) { + if (enablePageStorage) { PageStorage.of(context).writeState(context, state); } } @@ -1094,11 +1096,11 @@ class ComicListState extends State { while (_data[page] == null) { await _fetchNext(); } - if(mounted) { + if (mounted) { setState(() {}); } } catch (e) { - if(mounted) { + if (mounted) { setState(() { _error = e.toString(); }); diff --git a/lib/components/components.dart b/lib/components/components.dart index ac55f9b..420309f 100644 --- a/lib/components/components.dart +++ b/lib/components/components.dart @@ -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'; @@ -44,4 +46,5 @@ part 'select.dart'; part 'side_bar.dart'; part 'comic.dart'; part 'effects.dart'; -part 'gesture.dart'; \ No newline at end of file +part 'gesture.dart'; +part 'code.dart'; \ No newline at end of file diff --git a/lib/components/menu.dart b/lib/components/menu.dart index 86b06a4..7293625 100644 --- a/lib/components/menu.dart +++ b/lib/components/menu.dart @@ -28,6 +28,9 @@ class _MenuRoute extends PopupRoute { 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; } diff --git a/lib/components/pop_up_widget.dart b/lib/components/pop_up_widget.dart index 3436af5..bf36812 100644 --- a/lib/components/pop_up_widget.dart +++ b/lib/components/pop_up_widget.dart @@ -155,6 +155,9 @@ class _PopUpWidgetScaffoldState extends State { ), NotificationListener( onNotification: (notifications) { + if (notifications.metrics.axisDirection != AxisDirection.down) { + return false; + } if (notifications.metrics.pixels == notifications.metrics.minScrollExtent && !top) { diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index acd8cdf..91b7f81 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -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; diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index dc49623..bc4a4c3 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -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 saveData() async { + Future 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 data) { + for (var key in data.keys) { + if (_disableSync.contains(key)) { + continue; + } + settings[key] = data[key]; + } + searchHistory = List.from(data['searchHistory']); + saveData(); + } + var implicitData = {}; 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} - The processed image + */ +async function processImage(image, cid, eid) { + return image; +} +'''; \ No newline at end of file diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index 5353ad0..6671aba 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -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"; diff --git a/lib/foundation/image_provider/reader_image.dart b/lib/foundation/image_provider/reader_image.dart index b92b524..0afc72a 100644 --- a/lib/foundation/image_provider/reader_image.dart +++ b/lib/foundation/image_provider/reader_image.dart @@ -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 { @@ -21,25 +24,50 @@ class ReaderImageProvider @override Future load(StreamController 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 diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart index 206ffa2..f7e935a 100644 --- a/lib/foundation/js_engine.dart +++ b/lib/foundation/js_engine.dart @@ -58,6 +58,11 @@ class JsEngine with _JSEngineApi { JsEngine().init(); } + void resetDio() { + _dio = AppDio(BaseOptions( + responseType: ResponseType.plain, validateStatus: (status) => true)); + } + Future 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"], diff --git a/lib/network/app_dio.dart b/lib/network/app_dio.dart index 540960c..816b226 100644 --- a/lib/network/app_dio.dart +++ b/lib/network/app_dio.dart @@ -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> _getOverrides() { + if (!appdata.settings['enableDnsOverrides'] == true) { + return {}; + } + var config = appdata.settings["dnsOverrides"]; + var result = >{}; + 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, ), ); } diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 28c3bcc..9e7c3f6 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -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 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, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index d82f09b..a8a0794 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -404,8 +404,9 @@ class _LocalComicsPageState extends State { 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(); }), diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart index 44bf020..bcaeb5b 100644 --- a/lib/pages/search_page.dart +++ b/lib/pages/search_page.dart @@ -139,7 +139,9 @@ class _SearchPageState extends State { @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 { diff --git a/lib/pages/search_result_page.dart b/lib/pages/search_result_page.dart index 9543c0b..b5beac7 100644 --- a/lib/pages/search_result_page.dart +++ b/lib/pages/search_result_page.dart @@ -45,8 +45,9 @@ class _SearchResultPageState extends State { 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 { 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 { 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 { return _SearchSettingsDialog(state: this); }, ); - setState(() {}); + if (previousOptions != options || previousSourceKey != sourceKey) { + text = checkAutoLanguage(controller.text); + controller.currentText = text; + setState(() {}); + } }, ), ); diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart index c65b87e..b7c2bef 100644 --- a/lib/pages/settings/about.dart +++ b/lib/pages/settings/about.dart @@ -61,6 +61,10 @@ class _AboutSettingsState extends State { }, ).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 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: () { diff --git a/lib/pages/settings/explore_settings.dart b/lib/pages/settings/explore_settings.dart index 8670927..459678b 100644 --- a/lib/pages/settings/explore_settings.dart +++ b/lib/pages/settings/explore_settings.dart @@ -88,6 +88,30 @@ class _ExploreSettingsState extends State { title: "Keyword blocking".tl, builder: () => const _ManageBlockingWordView(), ).toSliver(), + SelectSetting( + title: "Default Search Target".tl, + settingKey: "defaultSearchTarget", + optionTranslation: { + '_aggregated_': "Aggregated".tl, + ...((){ + var map = {}; + 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; }); diff --git a/lib/pages/settings/network.dart b/lib/pages/settings/network.dart index 9396b7e..ea86985 100644 --- a/lib/pages/settings/network.dart +++ b/lib/pages/settings/network.dart @@ -17,6 +17,10 @@ class _NetworkSettingsState extends State { 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 = {}; + 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); + }); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index 810734a..1c6f05d 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -61,9 +61,17 @@ class _ReaderSettingsState extends State { ).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 { 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 { 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; + }, + ), + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index bad2e09..3827206 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -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'; diff --git a/lib/pages/webview.dart b/lib/pages/webview.dart index e0c6349..e6a4320 100644 --- a/lib/pages/webview.dart +++ b/lib/pages/webview.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 { double _progress = 0; + late var future = _createWebviewEnvironment(); + + Future _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 { 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); diff --git a/lib/utils/cbz.dart b/lib/utils/cbz.dart index 4116217..7001da9 100644 --- a/lib/utils/cbz.dart +++ b/lib/utils/cbz.dart @@ -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; diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 788191d..a427af2 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -80,15 +80,9 @@ Future 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(); diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index 3dd1474..285dd8d 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -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(); diff --git a/pubspec.lock b/pubspec.lock index 0fe101a..d6e8e6d 100644 --- a/pubspec.lock +++ b/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" diff --git a/pubspec.yaml b/pubspec.yaml index 32e6075..3c67428 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: