This commit is contained in:
nyne
2025-01-21 16:02:01 +08:00
committed by GitHub
15 changed files with 469 additions and 136 deletions

View File

@@ -1205,6 +1205,10 @@ class Image {
} }
} }
/**
* UI related apis
* @since 1.2.0
*/
let UI = { let UI = {
/** /**
* Show a message * Show a message
@@ -1222,7 +1226,9 @@ let UI = {
* Show a dialog. Any action will close the dialog. * Show a dialog. Any action will close the dialog.
* @param title {string} * @param title {string}
* @param content {string} * @param content {string}
* @param actions {{text:string, callback: () => void}[]} * @param actions {{text:string, callback: () => void | Promise<void>, style: "text"|"filled"|"danger"}[]} - If callback returns a promise, the button will show a loading indicator until the promise is resolved.
* @returns {Promise<void>} - Resolved when the dialog is closed.
* @since 1.2.1
*/ */
showDialog: (title, content, actions) => { showDialog: (title, content, actions) => {
sendMessage({ sendMessage({
@@ -1245,4 +1251,97 @@ let UI = {
url: url, url: url,
}) })
}, },
/**
* Show a loading dialog.
* @param onCancel {() => void | null | undefined} - Called when the loading dialog is canceled. If [onCancel] is null, the dialog cannot be canceled by the user.
* @returns {number} - A number that can be used to cancel the loading dialog.
* @since 1.2.1
*/
showLoading: (onCancel) => {
return sendMessage({
method: 'UI',
function: 'showLoading',
onCancel: onCancel
})
},
/**
* Cancel a loading dialog.
* @param id {number} - returned by [showLoading]
* @since 1.2.1
*/
cancelLoading: (id) => {
sendMessage({
method: 'UI',
function: 'cancelLoading',
id: id
})
},
/**
* Show an input dialog
* @param title {string}
* @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message.
* @returns {Promise<string | null>} - The input value. If the dialog is canceled, return null.
*/
showInputDialog: (title, validator) => {
return sendMessage({
method: 'UI',
function: 'showInputDialog',
title: title,
validator: validator
})
},
/**
* Show a select dialog
* @param title {string}
* @param options {string[]}
* @param initialIndex {number?}
* @returns {Promise<number | null>} - The selected index. If the dialog is canceled, return null.
*/
showSelectDialog: (title, options, initialIndex) => {
return sendMessage({
method: 'UI',
function: 'showSelectDialog',
title: title,
options: options,
initialIndex: initialIndex
})
}
}
/**
* App related apis
* @since 1.2.1
*/
let APP = {
/**
* Get the app version
* @returns {string} - The app version
*/
get version() {
return appVersion // defined in the engine
},
/**
* Get current app locale
* @returns {string} - The app locale, in the format of [languageCode]_[countryCode]
*/
get locale() {
return sendMessage({
method: 'getLocale'
})
},
/**
* Get current running platform
* @returns {string} - The platform name, "android", "ios", "windows", "macos", "linux"
*/
get platform() {
return sendMessage({
method: 'getPlatform'
})
}
} }

View File

@@ -148,11 +148,6 @@
"Size in MB": "大小MB", "Size in MB": "大小MB",
"Select a directory which contains the comic directories." : "选择一个包含漫画文件夹的目录", "Select a directory which contains the comic directories." : "选择一个包含漫画文件夹的目录",
"Help": "帮助", "Help": "帮助",
"A directory is considered as a comic only if it matches one of the following conditions:" : "只有当目录满足以下条件之一时,才被视为漫画:",
"1. The directory only contains image files." : "1. 目录只包含图片文件。",
"2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目录包含多个包含图片文件的目录。每个目录被视为一个章节。",
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。",
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。\n",
"Export as cbz": "导出为cbz", "Export as cbz": "导出为cbz",
"Select an archive file (cbz, zip, 7z, cb7)" : "选择一个归档文件 (cbz, zip, 7z, cb7)", "Select an archive file (cbz, zip, 7z, cb7)" : "选择一个归档文件 (cbz, zip, 7z, cb7)",
"An archive file" : "一个归档文件", "An archive file" : "一个归档文件",
@@ -318,7 +313,8 @@
"Imported @a comics": "已导入 @a 本漫画", "Imported @a comics": "已导入 @a 本漫画",
"New Version": "新版本", "New Version": "新版本",
"@c updates": "@c 项更新", "@c updates": "@c 项更新",
"No updates": "无更新" "No updates": "无更新",
"Set comic source list url": "设置漫画源列表URL"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -468,11 +464,6 @@
"Size in MB": "大小MB", "Size in MB": "大小MB",
"Select a directory which contains the comic directories." : "選擇一個包含漫畫文件夾的目錄", "Select a directory which contains the comic directories." : "選擇一個包含漫畫文件夾的目錄",
"Help": "幫助", "Help": "幫助",
"A directory is considered as a comic only if it matches one of the following conditions:" : "只有當目錄滿足以下條件之一時,才被視為漫畫:",
"1. The directory only contains image files." : "1. 目錄只包含圖片文件。",
"2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目錄包含多個包含圖片文件的目錄。每個目錄被視為一個章節。",
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。",
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。\n",
"Export as cbz": "匯出為cbz", "Export as cbz": "匯出為cbz",
"Select an archive file (cbz, zip, 7z, cb7)" : "選擇一個歸檔文件 (cbz, zip, 7z, cb7)", "Select an archive file (cbz, zip, 7z, cb7)" : "選擇一個歸檔文件 (cbz, zip, 7z, cb7)",
"An archive file" : "一個歸檔文件", "An archive file" : "一個歸檔文件",
@@ -639,6 +630,7 @@
"Imported @a comics": "已匯入 @a 部漫畫", "Imported @a comics": "已匯入 @a 部漫畫",
"New Version": "新版本", "New Version": "新版本",
"@c updates": "@c 項更新", "@c updates": "@c 項更新",
"No updates": "無更新" "No updates": "無更新",
"Set comic source list url": "設置漫畫源列表URL"
} }
} }

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
platform :ios, '15.0' platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'

242
lib/components/js_ui.dart Normal file
View File

@@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:flutter_qjs/flutter_qjs.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/js_engine.dart';
import 'components.dart';
mixin class JsUiApi {
final Map<int, LoadingDialogController> _loadingDialogControllers = {};
dynamic handleUIMessage(Map<String, dynamic> message) {
switch (message['function']) {
case 'showMessage':
var m = message['message'];
if (m.toString().isNotEmpty) {
App.rootContext.showMessage(message: m.toString());
}
case 'showDialog':
return _showDialog(message);
case 'launchUrl':
var url = message['url'];
if (url.toString().isNotEmpty) {
launchUrlString(url.toString());
}
case 'showLoading':
var onCancel = message['onCancel'];
if (onCancel != null && onCancel is! JSInvokable) {
return;
}
return _showLoading(onCancel);
case 'cancelLoading':
var id = message['id'];
if (id is int) {
_cancelLoading(id);
}
case 'showInputDialog':
var title = message['title'];
var validator = message['validator'];
if (title is! String) return;
if (validator != null && validator is! JSInvokable) return;
return _showInputDialog(title, validator);
case 'showSelectDialog':
var title = message['title'];
var options = message['options'];
var initialIndex = message['initialIndex'];
if (title is! String) return;
if (options is! List) return;
if (initialIndex != null && initialIndex is! int) return;
return _showSelectDialog(
title,
options.whereType<String>().toList(),
initialIndex,
);
}
}
Future<void> _showDialog(Map<String, dynamic> message) {
BuildContext? dialogContext;
var title = message['title'];
var content = message['content'];
var actions = <Widget>[];
for (var action in message['actions']) {
if (action['callback'] is! JSInvokable) {
continue;
}
var callback = action['callback'] as JSInvokable;
var text = action['text'].toString();
var style = (action['style'] ?? 'text').toString();
actions.add(_JSCallbackButton(
text: text,
callback: JSAutoFreeFunction(callback),
style: style,
onCallbackFinished: () {
dialogContext?.pop();
},
));
}
if (actions.isEmpty) {
actions.add(TextButton(
onPressed: () {
dialogContext?.pop();
},
child: Text('OK'),
));
}
return showDialog(
context: App.rootContext,
builder: (context) {
dialogContext = context;
return ContentDialog(
title: title,
content: Text(content).paddingHorizontal(16),
actions: actions,
);
},
).then((value) {
dialogContext = null;
});
}
int _showLoading(JSInvokable? onCancel) {
var func = onCancel == null ? null : JSAutoFreeFunction(onCancel);
var controller = showLoadingDialog(
App.rootContext,
barrierDismissible: onCancel != null,
allowCancel: onCancel != null,
onCancel: onCancel == null
? null
: () {
func?.call([]);
},
);
var i = 0;
while (_loadingDialogControllers.containsKey(i)) {
i++;
}
_loadingDialogControllers[i] = controller;
return i;
}
void _cancelLoading(int id) {
var controller = _loadingDialogControllers.remove(id);
controller?.close();
}
Future<String?> _showInputDialog(String title, JSInvokable? validator) async {
String? result;
var func = validator == null ? null : JSAutoFreeFunction(validator);
await showInputDialog(
context: App.rootContext,
title: title,
onConfirm: (v) {
if (func != null) {
var res = func.call([v]);
if (res != null) {
return res.toString();
} else {
result = v;
}
} else {
result = v;
}
return null;
},
);
return result;
}
Future<int?> _showSelectDialog(
String title,
List<String> options,
int? initialIndex,
) {
if (options.isEmpty) {
return Future.value(null);
}
if (initialIndex != null &&
(initialIndex >= options.length || initialIndex < 0)) {
initialIndex = null;
}
return showSelectDialog(
title: title,
options: options,
initialIndex: initialIndex,
);
}
}
class _JSCallbackButton extends StatefulWidget {
const _JSCallbackButton({
required this.text,
required this.callback,
required this.style,
this.onCallbackFinished,
});
final JSAutoFreeFunction callback;
final String text;
final String style;
final void Function()? onCallbackFinished;
@override
State<_JSCallbackButton> createState() => _JSCallbackButtonState();
}
class _JSCallbackButtonState extends State<_JSCallbackButton> {
bool isLoading = false;
void onClick() async {
if (isLoading) {
return;
}
var res = widget.callback.call([]);
if (res is Future) {
setState(() {
isLoading = true;
});
await res;
setState(() {
isLoading = false;
});
}
widget.onCallbackFinished?.call();
}
@override
Widget build(BuildContext context) {
return switch (widget.style) {
"filled" => FilledButton(
onPressed: onClick,
child: isLoading
? CircularProgressIndicator(strokeWidth: 1.4)
.fixWidth(18)
.fixHeight(18)
: Text(widget.text),
),
"danger" => FilledButton(
onPressed: onClick,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(context.colorScheme.error),
),
child: isLoading
? CircularProgressIndicator(strokeWidth: 1.4)
.fixWidth(18)
.fixHeight(18)
: Text(widget.text),
),
_ => TextButton(
onPressed: onClick,
child: isLoading
? CircularProgressIndicator(strokeWidth: 1.4)
.fixWidth(18)
.fixHeight(18)
: Text(widget.text),
),
};
}
}

View File

@@ -402,3 +402,59 @@ void showInfoDialog({
}, },
); );
} }
Future<int?> showSelectDialog({
required String title,
required List<String> options,
int? initialIndex,
}) async {
int? current = initialIndex;
await showDialog(
context: App.rootContext,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return ContentDialog(
title: title,
content: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Select(
current: current == null ? "" : options[current!],
values: options,
minWidth: 156,
onTap: (i) {
setState(() {
current = i;
});
},
)
],
),
),
actions: [
TextButton(
onPressed: () {
current = null;
context.pop();
},
child: Text('Cancel'.tl),
),
FilledButton(
onPressed: current == null
? null
: context.pop,
child: Text('Confirm'.tl),
),
],
);
},
);
},
);
return current;
}

View File

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

View File

@@ -90,13 +90,15 @@ class _Appdata {
/// Sync data from another device /// Sync data from another device
void syncData(Map<String, dynamic> data) { void syncData(Map<String, dynamic> data) {
for (var key in data.keys) { if (data['settings'] is Map) {
if (_disableSync.contains(key)) { var settings = data['settings'] as Map<String, dynamic>;
continue; for (var key in settings.keys) {
if (!_disableSync.contains(key)) {
this.settings[key] = settings[key];
}
} }
settings[key] = data[key];
} }
searchHistory = List.from(data['searchHistory']); searchHistory = List.from(data['searchHistory'] ?? []);
saveData(); saveData();
} }
@@ -153,6 +155,7 @@ class _Settings with ChangeNotifier {
'customImageProcessing': defaultCustomImageProcessing, 'customImageProcessing': defaultCustomImageProcessing,
'sni': true, 'sni': true,
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese 'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
'comicSourceListUrl': "https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json",
}; };
operator [](String key) { operator [](String key) {

View File

@@ -63,7 +63,8 @@ class ReaderImageProvider
})() })()
'''); ''');
if (func is JSInvokable) { if (func is JSInvokable) {
var result = func.invoke([imageBytes, cid, eid, page, sourceKey]); var autoFreeFunc = JSAutoFreeFunction(func);
var result = autoFreeFunc([imageBytes, cid, eid, page, sourceKey]);
if (result is Uint8List) { if (result is Uint8List) {
imageBytes = result; imageBytes = result;
} else if (result is Future) { } else if (result is Future) {
@@ -76,9 +77,9 @@ class ReaderImageProvider
if (image is Uint8List) { if (image is Uint8List) {
imageBytes = image; imageBytes = image;
} else if (image is Future) { } else if (image is Future) {
JSInvokable? onCancel; JSAutoFreeFunction? onCancel;
if (result['onCancel'] is JSInvokable) { if (result['onCancel'] is JSInvokable) {
onCancel = result['onCancel']; onCancel = JSAutoFreeFunction(result['onCancel']);
} }
if (onCancel == null) { if (onCancel == null) {
var futureImage = await image; var futureImage = await image;
@@ -96,9 +97,7 @@ class ReaderImageProvider
checkStop(); checkStop();
} }
catch(e) { catch(e) {
onCancel.invoke([]); onCancel([]);
onCancel.free();
func.free();
rethrow; rethrow;
} }
await Future.delayed(Duration(milliseconds: 50)); await Future.delayed(Duration(milliseconds: 50));
@@ -107,10 +106,8 @@ class ReaderImageProvider
imageBytes = futureImage; imageBytes = futureImage;
} }
} }
onCancel?.free();
} }
} }
func.free();
} }
} }
return imageBytes!; return imageBytes!;

View File

@@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:dio/io.dart'; import 'package:dio/io.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:html/parser.dart' as html; import 'package:html/parser.dart' as html;
import 'package:html/dom.dart' as dom; import 'package:html/dom.dart' as dom;
@@ -20,9 +19,8 @@ import 'package:pointycastle/block/modes/cbc.dart';
import 'package:pointycastle/block/modes/cfb.dart'; import 'package:pointycastle/block/modes/cfb.dart';
import 'package:pointycastle/block/modes/ecb.dart'; import 'package:pointycastle/block/modes/ecb.dart';
import 'package:pointycastle/block/modes/ofb.dart'; import 'package:pointycastle/block/modes/ofb.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:venera/components/components.dart'; import 'package:venera/components/js_ui.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/network/app_dio.dart'; import 'package:venera/network/app_dio.dart';
import 'package:venera/network/cookie_jar.dart'; import 'package:venera/network/cookie_jar.dart';
@@ -42,7 +40,7 @@ class JavaScriptRuntimeException implements Exception {
} }
} }
class JsEngine with _JSEngineApi, _JsUiApi { class JsEngine with _JSEngineApi, JsUiApi {
factory JsEngine() => _cache ?? (_cache = JsEngine._create()); factory JsEngine() => _cache ?? (_cache = JsEngine._create());
static JsEngine? _cache; static JsEngine? _cache;
@@ -156,7 +154,11 @@ class JsEngine with _JSEngineApi, _JsUiApi {
case "delay": case "delay":
return Future.delayed(Duration(milliseconds: message["time"])); return Future.delayed(Duration(milliseconds: message["time"]));
case "UI": case "UI":
handleUIMessage(Map.from(message)); return handleUIMessage(Map.from(message));
case "getLocale":
return "${App.locale.languageCode}-${App.locale.countryCode}";
case "getPlatform":
return Platform.operatingSystem;
} }
} }
return null; return null;
@@ -679,6 +681,7 @@ class JSAutoFreeFunction {
/// Automatically free the function when it's not used anymore /// Automatically free the function when it's not used anymore
JSAutoFreeFunction(this.func) { JSAutoFreeFunction(this.func) {
func.dup();
finalizer.attach(this, func); finalizer.attach(this, func);
} }
@@ -687,48 +690,6 @@ class JSAutoFreeFunction {
} }
static final finalizer = Finalizer<JSInvokable>((func) { static final finalizer = Finalizer<JSInvokable>((func) {
func.free(); func.destroy();
}); });
} }
mixin class _JsUiApi {
void handleUIMessage(Map<String, dynamic> message) {
switch (message['function']) {
case 'showMessage':
var m = message['message'];
if (m.toString().isNotEmpty) {
App.rootContext.showMessage(message: m.toString());
}
case 'showDialog':
_showDialog(message);
case 'launchUrl':
var url = message['url'];
if (url.toString().isNotEmpty) {
launchUrlString(url.toString());
}
}
}
void _showDialog(Map<String, dynamic> message) {
var title = message['title'];
var content = message['content'];
var actions = <String, JSAutoFreeFunction>{};
for (var action in message['actions']) {
actions[action['text']] = JSAutoFreeFunction(action['callback']);
}
showDialog(context: App.rootContext, builder: (context) {
return ContentDialog(
title: title,
content: Text(content).paddingHorizontal(16),
actions: actions.entries.map((entry) {
return TextButton(
onPressed: () {
entry.value.call([]);
},
child: Text(entry.key),
);
}).toList(),
);
});
}
}

View File

@@ -135,6 +135,8 @@ class NetworkCacheManager implements Interceptor {
} }
static bool compareHeaders(Map<String, dynamic> a, Map<String, dynamic> b) { static bool compareHeaders(Map<String, dynamic> a, Map<String, dynamic> b) {
a = Map.from(a);
b = Map.from(b);
const shouldIgnore = [ const shouldIgnore = [
'cache-time', 'cache-time',
'prevent-parallel', 'prevent-parallel',

View File

@@ -246,7 +246,7 @@ class _BodyState extends State<_Body> {
), ),
); );
} else if (type == "callback") { } else if (type == "callback") {
yield _CallbackSetting(setting: item); yield _CallbackSetting(setting: item, sourceKey: source.key);
} }
} catch (e, s) { } catch (e, s) {
Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s"); Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s");
@@ -419,7 +419,7 @@ class _BodyState extends State<_Body> {
} }
void help() { void help() {
launchUrlString("https://github.com/venera-app/venera-configs"); launchUrlString("https://github.com/venera-app/venera/blob/master/doc/comic_source.md");
} }
Future<void> handleAddSource(String url) async { Future<void> handleAddSource(String url) async {
@@ -469,8 +469,7 @@ class _ComicSourceListState extends State<_ComicSourceList> {
void load() async { void load() async {
var dio = AppDio(); var dio = AppDio();
var res = await dio.get<String>( var res = await dio.get<String>(appdata.settings['comicSourceListUrl']);
"https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json");
if (res.statusCode != 200) { if (res.statusCode != 200) {
context.showMessage(message: "Network error".tl); context.showMessage(message: "Network error".tl);
return; return;
@@ -485,6 +484,27 @@ class _ComicSourceListState extends State<_ComicSourceList> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopUpWidgetScaffold( return PopUpWidgetScaffold(
title: "Comic Source".tl, title: "Comic Source".tl,
tailing: [
IconButton(
icon: Icon(Icons.settings),
onPressed: () async {
await showInputDialog(
context: context,
title: "Set comic source list url".tl,
initialValue: appdata.settings['comicSourceListUrl'],
onConfirm: (value) {
appdata.settings['comicSourceListUrl'] = value;
appdata.saveData();
setState(() {
loading = true;
json = null;
});
return null;
},
);
},
)
],
body: buildBody(), body: buildBody(),
); );
} }
@@ -682,10 +702,12 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
} }
class _CallbackSetting extends StatefulWidget { class _CallbackSetting extends StatefulWidget {
const _CallbackSetting({required this.setting}); const _CallbackSetting({required this.setting, required this.sourceKey});
final MapEntry<String, Map<String, dynamic>> setting; final MapEntry<String, Map<String, dynamic>> setting;
final String sourceKey;
@override @override
State<_CallbackSetting> createState() => _CallbackSettingState(); State<_CallbackSetting> createState() => _CallbackSettingState();
} }
@@ -719,11 +741,11 @@ class _CallbackSettingState extends State<_CallbackSetting> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
title: Text(title.ts(key)), title: Text(title.ts(widget.sourceKey)),
trailing: Button.normal( trailing: Button.normal(
onPressed: onClick, onPressed: onClick,
isLoading: isLoading, isLoading: isLoading,
child: Text(buttonText.ts(key)), child: Text(buttonText.ts(widget.sourceKey)),
).fixHeight(32), ).fixHeight(32),
); );
} }

View File

@@ -124,18 +124,7 @@ class _ExplorePageState extends State<ExplorePage>
} }
return NetworkError( return NetworkError(
message: msg, message: msg,
retry: () { retry: onSettingsChanged,
setState(() {
pages = ComicSource.all()
.map((e) => e.explorePages)
.expand((e) => e.map((e) => e.title))
.toList();
controller = TabController(
length: pages.length,
vsync: this,
);
});
},
withAppbar: false, withAppbar: false,
); );
} }

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sliver_tools/sliver_tools.dart'; import 'package:sliver_tools/sliver_tools.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:venera/components/components.dart'; import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
@@ -535,38 +536,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
], ],
), ),
onPressed: () { onPressed: () {
showDialog( launchUrlString("https://github.com/venera-app/venera/blob/master/doc/import_comic.md");
context: context,
barrierColor: Colors.black.toOpacity(0.2),
builder: (context) {
var help = '';
help +=
'${"A directory is considered as a comic only if it matches one of the following conditions:".tl}\n';
help += '${'1. The directory only contains image files.'.tl}\n';
help +=
'${'2. The directory contains directories which contain image files. Each directory is considered as a chapter.'.tl}\n\n';
help +=
'${"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used.".tl}\n\n';
help +=
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n"
.tl;
help +=
"If you import an EhViewer's database, program will automatically create folders according to the download label in that database."
.tl;
return ContentDialog(
title: "Help".tl,
content: Text(help).paddingHorizontal(16),
actions: [
Button.filled(
child: Text("OK".tl),
onPressed: () {
context.pop();
},
),
],
);
},
);
}, },
).fixWidth(90).paddingRight(8), ).fixWidth(90).paddingRight(8),
Button.filled( Button.filled(

View File

@@ -417,8 +417,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "9c99ac258a11f8e91761a5466a190efba3ca64af" ref: "598d50572a658f8e04775566fe3789954d9a01e3"
resolved-ref: "9c99ac258a11f8e91761a5466a190efba3ca64af" resolved-ref: "598d50572a658f8e04775566fe3789954d9a01e3"
url: "https://github.com/wgh136/flutter_qjs" url: "https://github.com/wgh136/flutter_qjs"
source: git source: git
version: "0.3.7" version: "0.3.7"
@@ -1150,10 +1150,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: zip_flutter name: zip_flutter
sha256: be21152c35fcb6d0ef4ce89fc3aed681f7adc0db5490ca3eb5893f23fd20e646 sha256: ea7fdc86c988174ef3bb80dc26e8e8bfdf634c55930e2d18d7e77e991acf0483
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.6" version: "0.0.8"
sdks: sdks:
dart: ">=3.6.0 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.1" flutter: ">=3.27.2"

View File

@@ -2,11 +2,11 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.2.0+120 version: 1.2.1+121
environment: environment:
sdk: '>=3.6.0 <4.0.0' sdk: '>=3.6.0 <4.0.0'
flutter: 3.27.1 flutter: 3.27.2
dependencies: dependencies:
flutter: flutter:
@@ -21,7 +21,7 @@ dependencies:
flutter_qjs: flutter_qjs:
git: git:
url: https://github.com/wgh136/flutter_qjs url: https://github.com/wgh136/flutter_qjs
ref: 9c99ac258a11f8e91761a5466a190efba3ca64af ref: 598d50572a658f8e04775566fe3789954d9a01e3
crypto: ^3.0.6 crypto: ^3.0.6
dio: ^5.7.0 dio: ^5.7.0
html: ^0.15.5 html: ^0.15.5
@@ -51,7 +51,7 @@ dependencies:
sliver_tools: ^0.2.12 sliver_tools: ^0.2.12
flutter_file_dialog: ^3.0.2 flutter_file_dialog: ^3.0.2
file_selector: ^1.0.3 file_selector: ^1.0.3
zip_flutter: ^0.0.6 zip_flutter: ^0.0.8
lodepng_flutter: lodepng_flutter:
git: git:
url: https://github.com/venera-app/lodepng_flutter url: https://github.com/venera-app/lodepng_flutter