Improve init. Close #236

This commit is contained in:
2025-03-04 15:30:40 +08:00
parent 0f6874f8d7
commit 7b5c13200d
14 changed files with 184 additions and 105 deletions

View File

@@ -640,5 +640,5 @@ TransitionBuilder VirtualWindowFrameInit() {
} }
void debug() { void debug() {
ComicSource.reload(); ComicSourceManager().reload();
} }

View File

@@ -77,10 +77,15 @@ class _App {
Future<void> init() async { Future<void> init() async {
cachePath = (await getApplicationCacheDirectory()).path; cachePath = (await getApplicationCacheDirectory()).path;
dataPath = (await getApplicationSupportDirectory()).path; dataPath = (await getApplicationSupportDirectory()).path;
await data.init(); }
await history.init();
await favorites.init(); Future<void> initComponents() async {
await local.init(); await Future.wait([
data.init(),
history.init(),
favorites.init(),
local.init(),
]);
} }
Function? _forceRebuildHandler; Function? _forceRebuildHandler;

View File

@@ -145,7 +145,7 @@ class RandomCategoryPartWithRuntimeData extends BaseCategoryPart {
} }
CategoryData getCategoryDataWithKey(String key) { CategoryData getCategoryDataWithKey(String key) {
for (var source in ComicSource._sources) { for (var source in ComicSource.all()) {
if (source.categoryData?.key == key) { if (source.categoryData?.key == key) {
return source.categoryData!; return source.categoryData!;
} }

View File

@@ -13,6 +13,7 @@ import 'package:venera/foundation/history.dart';
import 'package:venera/foundation/res.dart'; import 'package:venera/foundation/res.dart';
import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/data_sync.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/init.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
@@ -27,81 +28,29 @@ part 'parser.dart';
part 'models.dart'; part 'models.dart';
/// build comic list, [Res.subData] should be maxPage or null if there is no limit. part 'types.dart';
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
/// build comic list with next param, [Res.subData] should be next page param or null if there is no next page. class ComicSourceManager with ChangeNotifier, Init {
typedef ComicListBuilderWithNext = Future<Res<List<Comic>>> Function( final List<ComicSource> _sources = [];
String? next);
typedef LoginFunction = Future<Res<bool>> Function(String, String); static ComicSourceManager? _instance;
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id); ComicSourceManager._create();
typedef LoadComicPagesFunc = Future<Res<List<String>>> Function( factory ComicSourceManager() => _instance ??= ComicSourceManager._create();
String id, String? ep);
typedef CommentsLoader = Future<Res<List<Comment>>> Function( List<ComicSource> all() => List.from(_sources);
String id, String? subId, int page, String? replyTo);
typedef SendCommentFunc = Future<Res<bool>> Function( ComicSource? find(String key) =>
String id, String? subId, String content, String? replyTo);
typedef GetImageLoadingConfigFunc = Future<Map<String, dynamic>> Function(
String imageKey, String comicId, String epId)?;
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
String imageKey)?;
typedef ComicThumbnailLoader = Future<Res<List<String>>> Function(
String comicId, String? next);
typedef LikeOrUnlikeComicFunc = Future<Res<bool>> Function(
String comicId, bool isLiking);
/// [isLiking] is true if the user is liking the comment, false if unliking.
/// return the new likes count or null.
typedef LikeCommentFunc = Future<Res<int?>> Function(
String comicId, String? subId, String commentId, bool isLiking);
/// [isUp] is true if the user is upvoting the comment, false if downvoting.
/// return the new vote count or null.
typedef VoteCommentFunc = Future<Res<int?>> Function(
String comicId, String? subId, String commentId, bool isUp, bool isCancel);
typedef HandleClickTagEvent = Map<String, String> Function(
String namespace, String tag);
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating);
class ComicSource {
static final List<ComicSource> _sources = [];
static final List<Function> _listeners = [];
static void addListener(Function listener) {
_listeners.add(listener);
}
static void removeListener(Function listener) {
_listeners.remove(listener);
}
static void notifyListeners() {
for (var listener in _listeners) {
listener();
}
}
static List<ComicSource> all() => List.from(_sources);
static ComicSource? find(String key) =>
_sources.firstWhereOrNull((element) => element.key == key); _sources.firstWhereOrNull((element) => element.key == key);
static ComicSource? fromIntKey(int key) => ComicSource? fromIntKey(int key) =>
_sources.firstWhereOrNull((element) => element.key.hashCode == key); _sources.firstWhereOrNull((element) => element.key.hashCode == key);
static Future<void> init() async { @override
@protected
Future<void> doInit() async {
await JsEngine().ensureInit();
final path = "${App.dataPath}/comic_source"; final path = "${App.dataPath}/comic_source";
if (!(await Directory(path).exists())) { if (!(await Directory(path).exists())) {
Directory(path).create(); Directory(path).create();
@@ -120,26 +69,49 @@ class ComicSource {
} }
} }
static Future reload() async { Future reload() async {
_sources.clear(); _sources.clear();
JsEngine().runCode("ComicSource.sources = {};"); JsEngine().runCode("ComicSource.sources = {};");
await init(); await doInit();
notifyListeners(); notifyListeners();
} }
static void add(ComicSource source) { void add(ComicSource source) {
_sources.add(source); _sources.add(source);
notifyListeners(); notifyListeners();
} }
static void remove(String key) { void remove(String key) {
_sources.removeWhere((element) => element.key == key); _sources.removeWhere((element) => element.key == key);
notifyListeners(); notifyListeners();
} }
static final availableUpdates = <String, String>{}; bool get isEmpty => _sources.isEmpty;
static bool get isEmpty => _sources.isEmpty; /// Key is the source key, value is the version.
final _availableUpdates = <String, String>{};
void updateAvailableUpdates(Map<String, String> updates) {
_availableUpdates.addAll(updates);
notifyListeners();
}
Map<String, String> get availableUpdates => Map.from(_availableUpdates);
void notifyStateChange() {
notifyListeners();
}
}
class ComicSource {
static List<ComicSource> all() => ComicSourceManager().all();
static ComicSource? find(String key) => ComicSourceManager().find(key);
static ComicSource? fromIntKey(int key) =>
ComicSourceManager().fromIntKey(key);
static bool get isEmpty => ComicSourceManager().isEmpty;
/// Name of this source. /// Name of this source.
final String name; final String name;
@@ -321,7 +293,7 @@ class AccountConfig {
this.onLoginWithWebviewSuccess, this.onLoginWithWebviewSuccess,
this.cookieFields, this.cookieFields,
this.validateCookies, this.validateCookies,
) : infoItems = const []; ) : infoItems = const [];
} }
class AccountInfoItem { class AccountInfoItem {
@@ -478,4 +450,4 @@ class ArchiveDownloader {
final Future<Res<String>> Function(String cid, String aid) getDownloadUrl; final Future<Res<String>> Function(String cid, String aid) getDownloadUrl;
const ArchiveDownloader(this.getArchives, this.getDownloadUrl); const ArchiveDownloader(this.getArchives, this.getDownloadUrl);
} }

View File

@@ -0,0 +1,48 @@
part of 'comic_source.dart';
/// build comic list, [Res.subData] should be maxPage or null if there is no limit.
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
/// build comic list with next param, [Res.subData] should be next page param or null if there is no next page.
typedef ComicListBuilderWithNext = Future<Res<List<Comic>>> Function(
String? next);
typedef LoginFunction = Future<Res<bool>> Function(String, String);
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
typedef LoadComicPagesFunc = Future<Res<List<String>>> Function(
String id, String? ep);
typedef CommentsLoader = Future<Res<List<Comment>>> Function(
String id, String? subId, int page, String? replyTo);
typedef SendCommentFunc = Future<Res<bool>> Function(
String id, String? subId, String content, String? replyTo);
typedef GetImageLoadingConfigFunc = Future<Map<String, dynamic>> Function(
String imageKey, String comicId, String epId)?;
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
String imageKey)?;
typedef ComicThumbnailLoader = Future<Res<List<String>>> Function(
String comicId, String? next);
typedef LikeOrUnlikeComicFunc = Future<Res<bool>> Function(
String comicId, bool isLiking);
/// [isLiking] is true if the user is liking the comment, false if unliking.
/// return the new likes count or null.
typedef LikeCommentFunc = Future<Res<int?>> Function(
String comicId, String? subId, String commentId, bool isLiking);
/// [isUp] is true if the user is upvoting the comment, false if downvoting.
/// return the new vote count or null.
typedef VoteCommentFunc = Future<Res<int?>> Function(
String comicId, String? subId, String commentId, bool isUp, bool isCancel);
typedef HandleClickTagEvent = Map<String, String> Function(
String namespace, String tag);
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating);

View File

@@ -3,6 +3,7 @@ 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/foundation.dart' show protected;
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;
@@ -24,6 +25,7 @@ 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';
import 'package:venera/utils/init.dart';
import 'comic_source/comic_source.dart'; import 'comic_source/comic_source.dart';
import 'consts.dart'; import 'consts.dart';
@@ -40,7 +42,7 @@ class JavaScriptRuntimeException implements Exception {
} }
} }
class JsEngine with _JSEngineApi, JsUiApi { class JsEngine with _JSEngineApi, JsUiApi, Init {
factory JsEngine() => _cache ?? (_cache = JsEngine._create()); factory JsEngine() => _cache ?? (_cache = JsEngine._create());
static JsEngine? _cache; static JsEngine? _cache;
@@ -64,7 +66,9 @@ class JsEngine with _JSEngineApi, JsUiApi {
responseType: ResponseType.plain, validateStatus: (status) => true)); responseType: ResponseType.plain, validateStatus: (status) => true));
} }
Future<void> init() async { @override
@protected
Future<void> doInit() async {
if (!_closed) { if (!_closed) {
return; return;
} }

View File

@@ -265,6 +265,7 @@ class LocalManager with ChangeNotifier {
} }
_checkPathValidation(); _checkPathValidation();
_checkNoMedia(); _checkNoMedia();
await ComicSourceManager().ensureInit();
restoreDownloadingTasks(); restoreDownloadingTasks();
} }

View File

@@ -30,13 +30,15 @@ extension _FutureInit<T> on Future<T> {
Future<void> init() async { Future<void> init() async {
await App.init().wait(); await App.init().wait();
SingleInstanceCookieJar("${App.dataPath}/cookie.db"); await SingleInstanceCookieJar.createInstance();
var futures = [ var futures = [
Rhttp.init(), Rhttp.init(),
App.initComponents(),
SAFTaskWorker().init().wait(), SAFTaskWorker().init().wait(),
AppTranslation.init().wait(), AppTranslation.init().wait(),
TagsTranslation.readData().wait(), TagsTranslation.readData().wait(),
JsEngine().init().then((_) => ComicSource.init()).wait(), JsEngine().init().wait(),
ComicSourceManager().init().wait(),
]; ];
await Future.wait(futures); await Future.wait(futures);
CacheManager().setLimitSize(appdata.settings['cacheSize']); CacheManager().setLimitSize(appdata.settings['cacheSize']);

View File

@@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3/sqlite3.dart';
import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/log.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
@@ -200,6 +201,11 @@ class SingleInstanceCookieJar extends CookieJarSql {
SingleInstanceCookieJar._create(super.path); SingleInstanceCookieJar._create(super.path);
static SingleInstanceCookieJar? instance; static SingleInstanceCookieJar? instance;
static Future<void> createInstance() async {
var dataPath = (await getApplicationSupportDirectory()).path;
instance = SingleInstanceCookieJar("$dataPath/cookie.db");
}
} }
class CookieManagerSql extends Interceptor { class CookieManagerSql extends Interceptor {

View File

@@ -40,10 +40,11 @@ class ComicSourcePage extends StatelessWidget {
} }
} }
if (shouldUpdate.isNotEmpty) { if (shouldUpdate.isNotEmpty) {
var updates = <String, String>{};
for (var key in shouldUpdate) { for (var key in shouldUpdate) {
ComicSource.availableUpdates[key] = versions[key]!; updates[key] = versions[key]!;
} }
ComicSource.notifyListeners(); ComicSourceManager().updateAvailableUpdates(updates);
} }
return shouldUpdate.length; return shouldUpdate.length;
} }
@@ -73,13 +74,13 @@ class _BodyState extends State<_Body> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
ComicSource.addListener(updateUI); ComicSourceManager().addListener(updateUI);
} }
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
ComicSource.removeListener(updateUI); ComicSourceManager().removeListener(updateUI);
} }
@override @override
@@ -115,7 +116,7 @@ class _BodyState extends State<_Body> {
onConfirm: () { onConfirm: () {
var file = File(source.filePath); var file = File(source.filePath);
file.delete(); file.delete();
ComicSource.remove(source.key); ComicSourceManager().remove(source.key);
_validatePages(); _validatePages();
App.forceRebuild(); App.forceRebuild();
}, },
@@ -136,7 +137,7 @@ class _BodyState extends State<_Body> {
child: const Text("cancel")), child: const Text("cancel")),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await ComicSource.reload(); await ComicSourceManager().reload();
App.forceRebuild(); App.forceRebuild();
}, },
child: const Text("continue")), child: const Text("continue")),
@@ -150,7 +151,7 @@ class _BodyState extends State<_Body> {
} }
context.to( context.to(
() => _EditFilePage(source.filePath, () async { () => _EditFilePage(source.filePath, () async {
await ComicSource.reload(); await ComicSourceManager().reload();
setState(() {}); setState(() {});
}), }),
); );
@@ -162,7 +163,7 @@ class _BodyState extends State<_Body> {
App.rootContext.showMessage(message: "Invalid url config"); App.rootContext.showMessage(message: "Invalid url config");
return; return;
} }
ComicSource.remove(source.key); ComicSourceManager().remove(source.key);
bool cancel = false; bool cancel = false;
LoadingDialogController? controller; LoadingDialogController? controller;
if (showLoading) { if (showLoading) {
@@ -179,14 +180,14 @@ class _BodyState extends State<_Body> {
controller?.close(); controller?.close();
await ComicSourceParser().parse(res.data!, source.filePath); await ComicSourceParser().parse(res.data!, source.filePath);
await File(source.filePath).writeAsString(res.data!); await File(source.filePath).writeAsString(res.data!);
if (ComicSource.availableUpdates.containsKey(source.key)) { if (ComicSourceManager().availableUpdates.containsKey(source.key)) {
ComicSource.availableUpdates.remove(source.key); ComicSourceManager().availableUpdates.remove(source.key);
} }
} catch (e) { } catch (e) {
if (cancel) return; if (cancel) return;
App.rootContext.showMessage(message: e.toString()); App.rootContext.showMessage(message: e.toString());
} }
await ComicSource.reload(); await ComicSourceManager().reload();
App.forceRebuild(); App.forceRebuild();
} }
@@ -304,7 +305,7 @@ class _BodyState extends State<_Body> {
Future<void> addSource(String js, String fileName) async { Future<void> addSource(String js, String fileName) async {
var comicSource = await ComicSourceParser().createAndParse(js, fileName); var comicSource = await ComicSourceParser().createAndParse(js, fileName);
ComicSource.add(comicSource); ComicSourceManager().add(comicSource);
_addAllPagesWithComicSource(comicSource); _addAllPagesWithComicSource(comicSource);
appdata.saveData(); appdata.saveData();
App.forceRebuild(); App.forceRebuild();
@@ -563,7 +564,7 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
} }
void showUpdateDialog() async { void showUpdateDialog() async {
var text = ComicSource.availableUpdates.entries.map((e) { var text = ComicSourceManager().availableUpdates.entries.map((e) {
return "${ComicSource.find(e.key)!.name}: ${e.value}"; return "${ComicSource.find(e.key)!.name}: ${e.value}";
}).join("\n"); }).join("\n");
bool doUpdate = false; bool doUpdate = false;
@@ -592,9 +593,9 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
withProgress: true, withProgress: true,
); );
int current = 0; int current = 0;
int total = ComicSource.availableUpdates.length; int total = ComicSourceManager().availableUpdates.length;
try { try {
var shouldUpdate = ComicSource.availableUpdates.keys.toList(); var shouldUpdate = ComicSourceManager().availableUpdates.keys.toList();
for (var key in shouldUpdate) { for (var key in shouldUpdate) {
var source = ComicSource.find(key)!; var source = ComicSource.find(key)!;
await _BodyState.update(source, false); await _BodyState.update(source, false);
@@ -692,7 +693,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var newVersion = ComicSource.availableUpdates[source.key]; var newVersion = ComicSourceManager().availableUpdates[source.key];
bool hasUpdate = bool hasUpdate =
newVersion != null && compareSemVer(newVersion, source.version); newVersion != null && compareSemVer(newVersion, source.version);
@@ -960,7 +961,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
source.data["account"] = null; source.data["account"] = null;
source.account?.logout(); source.account?.logout();
source.saveData(); source.saveData();
ComicSource.notifyListeners(); ComicSourceManager().notifyStateChange();
setState(() {}); setState(() {});
}, },
trailing: const Icon(Icons.logout), trailing: const Icon(Icons.logout),

View File

@@ -630,19 +630,19 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> {
@override @override
void initState() { void initState() {
comicSources = ComicSource.all().map((e) => e.name).toList(); comicSources = ComicSource.all().map((e) => e.name).toList();
ComicSource.addListener(onComicSourceChange); ComicSourceManager().addListener(onComicSourceChange);
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
ComicSource.removeListener(onComicSourceChange); ComicSourceManager().removeListener(onComicSourceChange);
super.dispose(); super.dispose();
} }
int get _availableUpdates { int get _availableUpdates {
int c = 0; int c = 0;
ComicSource.availableUpdates.forEach((key, version) { ComicSourceManager().availableUpdates.forEach((key, version) {
var source = ComicSource.find(key); var source = ComicSource.find(key);
if (source != null) { if (source != null) {
if (compareSemVer(version, source.version)) { if (compareSemVer(version, source.version)) {

View File

@@ -103,7 +103,7 @@ Future<void> importAppData(File file, [bool checkVersion = false]) async {
await file.copy(targetFile); await file.copy(targetFile);
} }
} }
await ComicSource.reload(); await ComicSourceManager().reload();
} }
} finally { } finally {
cacheDir.deleteIgnoreError(recursive: true); cacheDir.deleteIgnoreError(recursive: true);

View File

@@ -19,7 +19,7 @@ class DataSync with ChangeNotifier {
downloadData(); downloadData();
} }
LocalFavoritesManager().addListener(onDataChanged); LocalFavoritesManager().addListener(onDataChanged);
ComicSource.addListener(onDataChanged); ComicSourceManager().addListener(onDataChanged);
} }
void onDataChanged() { void onDataChanged() {

40
lib/utils/init.dart Normal file
View File

@@ -0,0 +1,40 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
/// A mixin class that provides a way to ensure the class is initialized.
abstract mixin class Init {
bool _isInit = false;
final _initCompleter = <Completer<void>>[];
/// Ensure the class is initialized.
Future<void> ensureInit() async {
if (_isInit) {
return;
}
var completer = Completer<void>();
_initCompleter.add(completer);
return completer.future;
}
Future<void> _markInit() async {
_isInit = true;
for (var completer in _initCompleter) {
completer.complete();
}
_initCompleter.clear();
}
@protected
Future<void> doInit();
/// Initialize the class.
Future<void> init() async {
if (_isInit) {
return;
}
await doInit();
await _markInit();
}
}