diff --git a/lib/components/window_frame.dart b/lib/components/window_frame.dart index 3453aed..6525970 100644 --- a/lib/components/window_frame.dart +++ b/lib/components/window_frame.dart @@ -640,5 +640,5 @@ TransitionBuilder VirtualWindowFrameInit() { } void debug() { - ComicSource.reload(); + ComicSourceManager().reload(); } diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 7415bbc..b5a541a 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -77,10 +77,15 @@ class _App { Future init() async { cachePath = (await getApplicationCacheDirectory()).path; dataPath = (await getApplicationSupportDirectory()).path; - await data.init(); - await history.init(); - await favorites.init(); - await local.init(); + } + + Future initComponents() async { + await Future.wait([ + data.init(), + history.init(), + favorites.init(), + local.init(), + ]); } Function? _forceRebuildHandler; diff --git a/lib/foundation/comic_source/category.dart b/lib/foundation/comic_source/category.dart index ed62432..7aae515 100644 --- a/lib/foundation/comic_source/category.dart +++ b/lib/foundation/comic_source/category.dart @@ -145,7 +145,7 @@ class RandomCategoryPartWithRuntimeData extends BaseCategoryPart { } CategoryData getCategoryDataWithKey(String key) { - for (var source in ComicSource._sources) { + for (var source in ComicSource.all()) { if (source.categoryData?.key == key) { return source.categoryData!; } diff --git a/lib/foundation/comic_source/comic_source.dart b/lib/foundation/comic_source/comic_source.dart index a0d3cc7..1ef748c 100644 --- a/lib/foundation/comic_source/comic_source.dart +++ b/lib/foundation/comic_source/comic_source.dart @@ -13,6 +13,7 @@ import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/ext.dart'; +import 'package:venera/utils/init.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; @@ -27,81 +28,29 @@ part 'parser.dart'; part 'models.dart'; -/// build comic list, [Res.subData] should be maxPage or null if there is no limit. -typedef ComicListBuilder = Future>> Function(int page); +part 'types.dart'; -/// build comic list with next param, [Res.subData] should be next page param or null if there is no next page. -typedef ComicListBuilderWithNext = Future>> Function( - String? next); +class ComicSourceManager with ChangeNotifier, Init { + final List _sources = []; -typedef LoginFunction = Future> Function(String, String); + static ComicSourceManager? _instance; -typedef LoadComicFunc = Future> Function(String id); + ComicSourceManager._create(); -typedef LoadComicPagesFunc = Future>> Function( - String id, String? ep); + factory ComicSourceManager() => _instance ??= ComicSourceManager._create(); -typedef CommentsLoader = Future>> Function( - String id, String? subId, int page, String? replyTo); + List all() => List.from(_sources); -typedef SendCommentFunc = Future> Function( - String id, String? subId, String content, String? replyTo); - -typedef GetImageLoadingConfigFunc = Future> Function( - String imageKey, String comicId, String epId)?; -typedef GetThumbnailLoadingConfigFunc = Map Function( - String imageKey)?; - -typedef ComicThumbnailLoader = Future>> Function( - String comicId, String? next); - -typedef LikeOrUnlikeComicFunc = Future> 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> 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> Function( - String comicId, String? subId, String commentId, bool isUp, bool isCancel); - -typedef HandleClickTagEvent = Map Function( - String namespace, String tag); - -/// [rating] is the rating value, 0-10. 1 represents 0.5 star. -typedef StarRatingFunc = Future> Function(String comicId, int rating); - -class ComicSource { - static final List _sources = []; - - static final List _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 all() => List.from(_sources); - - static ComicSource? find(String key) => + ComicSource? find(String key) => _sources.firstWhereOrNull((element) => element.key == key); - static ComicSource? fromIntKey(int key) => + ComicSource? fromIntKey(int key) => _sources.firstWhereOrNull((element) => element.key.hashCode == key); - static Future init() async { + @override + @protected + Future doInit() async { + await JsEngine().ensureInit(); final path = "${App.dataPath}/comic_source"; if (!(await Directory(path).exists())) { Directory(path).create(); @@ -120,26 +69,49 @@ class ComicSource { } } - static Future reload() async { + Future reload() async { _sources.clear(); JsEngine().runCode("ComicSource.sources = {};"); - await init(); + await doInit(); notifyListeners(); } - static void add(ComicSource source) { + void add(ComicSource source) { _sources.add(source); notifyListeners(); } - static void remove(String key) { + void remove(String key) { _sources.removeWhere((element) => element.key == key); notifyListeners(); } - static final availableUpdates = {}; + bool get isEmpty => _sources.isEmpty; - static bool get isEmpty => _sources.isEmpty; + /// Key is the source key, value is the version. + final _availableUpdates = {}; + + void updateAvailableUpdates(Map updates) { + _availableUpdates.addAll(updates); + notifyListeners(); + } + + Map get availableUpdates => Map.from(_availableUpdates); + + void notifyStateChange() { + notifyListeners(); + } +} + +class ComicSource { + static List 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. final String name; @@ -321,7 +293,7 @@ class AccountConfig { this.onLoginWithWebviewSuccess, this.cookieFields, this.validateCookies, - ) : infoItems = const []; + ) : infoItems = const []; } class AccountInfoItem { @@ -478,4 +450,4 @@ class ArchiveDownloader { final Future> Function(String cid, String aid) getDownloadUrl; const ArchiveDownloader(this.getArchives, this.getDownloadUrl); -} \ No newline at end of file +} diff --git a/lib/foundation/comic_source/types.dart b/lib/foundation/comic_source/types.dart new file mode 100644 index 0000000..fbacc8f --- /dev/null +++ b/lib/foundation/comic_source/types.dart @@ -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>> 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>> Function( + String? next); + +typedef LoginFunction = Future> Function(String, String); + +typedef LoadComicFunc = Future> Function(String id); + +typedef LoadComicPagesFunc = Future>> Function( + String id, String? ep); + +typedef CommentsLoader = Future>> Function( + String id, String? subId, int page, String? replyTo); + +typedef SendCommentFunc = Future> Function( + String id, String? subId, String content, String? replyTo); + +typedef GetImageLoadingConfigFunc = Future> Function( + String imageKey, String comicId, String epId)?; +typedef GetThumbnailLoadingConfigFunc = Map Function( + String imageKey)?; + +typedef ComicThumbnailLoader = Future>> Function( + String comicId, String? next); + +typedef LikeOrUnlikeComicFunc = Future> 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> 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> Function( + String comicId, String? subId, String commentId, bool isUp, bool isCancel); + +typedef HandleClickTagEvent = Map Function( + String namespace, String tag); + +/// [rating] is the rating value, 0-10. 1 represents 0.5 star. +typedef StarRatingFunc = Future> Function(String comicId, int rating); \ No newline at end of file diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart index 574c69c..3772491 100644 --- a/lib/foundation/js_engine.dart +++ b/lib/foundation/js_engine.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:math' as math; import 'package:crypto/crypto.dart'; import 'package:dio/io.dart'; +import 'package:flutter/foundation.dart' show protected; import 'package:flutter/services.dart'; import 'package:html/parser.dart' as html; 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/network/app_dio.dart'; import 'package:venera/network/cookie_jar.dart'; +import 'package:venera/utils/init.dart'; import 'comic_source/comic_source.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()); static JsEngine? _cache; @@ -64,7 +66,9 @@ class JsEngine with _JSEngineApi, JsUiApi { responseType: ResponseType.plain, validateStatus: (status) => true)); } - Future init() async { + @override + @protected + Future doInit() async { if (!_closed) { return; } diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index c2b462c..be0efe0 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -265,6 +265,7 @@ class LocalManager with ChangeNotifier { } _checkPathValidation(); _checkNoMedia(); + await ComicSourceManager().ensureInit(); restoreDownloadingTasks(); } diff --git a/lib/init.dart b/lib/init.dart index 8435771..939ea70 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -30,13 +30,15 @@ extension _FutureInit on Future { Future init() async { await App.init().wait(); - SingleInstanceCookieJar("${App.dataPath}/cookie.db"); + await SingleInstanceCookieJar.createInstance(); var futures = [ Rhttp.init(), + App.initComponents(), SAFTaskWorker().init().wait(), AppTranslation.init().wait(), TagsTranslation.readData().wait(), - JsEngine().init().then((_) => ComicSource.init()).wait(), + JsEngine().init().wait(), + ComicSourceManager().init().wait(), ]; await Future.wait(futures); CacheManager().setLimitSize(appdata.settings['cacheSize']); diff --git a/lib/network/cookie_jar.dart b/lib/network/cookie_jar.dart index a8c073a..d06cc1e 100644 --- a/lib/network/cookie_jar.dart +++ b/lib/network/cookie_jar.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/utils/ext.dart'; @@ -200,6 +201,11 @@ class SingleInstanceCookieJar extends CookieJarSql { SingleInstanceCookieJar._create(super.path); static SingleInstanceCookieJar? instance; + + static Future createInstance() async { + var dataPath = (await getApplicationSupportDirectory()).path; + instance = SingleInstanceCookieJar("$dataPath/cookie.db"); + } } class CookieManagerSql extends Interceptor { diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 49d7d03..96fd888 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -40,10 +40,11 @@ class ComicSourcePage extends StatelessWidget { } } if (shouldUpdate.isNotEmpty) { + var updates = {}; for (var key in shouldUpdate) { - ComicSource.availableUpdates[key] = versions[key]!; + updates[key] = versions[key]!; } - ComicSource.notifyListeners(); + ComicSourceManager().updateAvailableUpdates(updates); } return shouldUpdate.length; } @@ -73,13 +74,13 @@ class _BodyState extends State<_Body> { @override void initState() { super.initState(); - ComicSource.addListener(updateUI); + ComicSourceManager().addListener(updateUI); } @override void dispose() { super.dispose(); - ComicSource.removeListener(updateUI); + ComicSourceManager().removeListener(updateUI); } @override @@ -115,7 +116,7 @@ class _BodyState extends State<_Body> { onConfirm: () { var file = File(source.filePath); file.delete(); - ComicSource.remove(source.key); + ComicSourceManager().remove(source.key); _validatePages(); App.forceRebuild(); }, @@ -136,7 +137,7 @@ class _BodyState extends State<_Body> { child: const Text("cancel")), TextButton( onPressed: () async { - await ComicSource.reload(); + await ComicSourceManager().reload(); App.forceRebuild(); }, child: const Text("continue")), @@ -150,7 +151,7 @@ class _BodyState extends State<_Body> { } context.to( () => _EditFilePage(source.filePath, () async { - await ComicSource.reload(); + await ComicSourceManager().reload(); setState(() {}); }), ); @@ -162,7 +163,7 @@ class _BodyState extends State<_Body> { App.rootContext.showMessage(message: "Invalid url config"); return; } - ComicSource.remove(source.key); + ComicSourceManager().remove(source.key); bool cancel = false; LoadingDialogController? controller; if (showLoading) { @@ -179,14 +180,14 @@ class _BodyState extends State<_Body> { controller?.close(); await ComicSourceParser().parse(res.data!, source.filePath); await File(source.filePath).writeAsString(res.data!); - if (ComicSource.availableUpdates.containsKey(source.key)) { - ComicSource.availableUpdates.remove(source.key); + if (ComicSourceManager().availableUpdates.containsKey(source.key)) { + ComicSourceManager().availableUpdates.remove(source.key); } } catch (e) { if (cancel) return; App.rootContext.showMessage(message: e.toString()); } - await ComicSource.reload(); + await ComicSourceManager().reload(); App.forceRebuild(); } @@ -304,7 +305,7 @@ class _BodyState extends State<_Body> { Future addSource(String js, String fileName) async { var comicSource = await ComicSourceParser().createAndParse(js, fileName); - ComicSource.add(comicSource); + ComicSourceManager().add(comicSource); _addAllPagesWithComicSource(comicSource); appdata.saveData(); App.forceRebuild(); @@ -563,7 +564,7 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> { } 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}"; }).join("\n"); bool doUpdate = false; @@ -592,9 +593,9 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> { withProgress: true, ); int current = 0; - int total = ComicSource.availableUpdates.length; + int total = ComicSourceManager().availableUpdates.length; try { - var shouldUpdate = ComicSource.availableUpdates.keys.toList(); + var shouldUpdate = ComicSourceManager().availableUpdates.keys.toList(); for (var key in shouldUpdate) { var source = ComicSource.find(key)!; await _BodyState.update(source, false); @@ -692,7 +693,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> { @override Widget build(BuildContext context) { - var newVersion = ComicSource.availableUpdates[source.key]; + var newVersion = ComicSourceManager().availableUpdates[source.key]; bool hasUpdate = newVersion != null && compareSemVer(newVersion, source.version); @@ -960,7 +961,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> { source.data["account"] = null; source.account?.logout(); source.saveData(); - ComicSource.notifyListeners(); + ComicSourceManager().notifyStateChange(); setState(() {}); }, trailing: const Icon(Icons.logout), diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index b513911..94a281f 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -630,19 +630,19 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> { @override void initState() { comicSources = ComicSource.all().map((e) => e.name).toList(); - ComicSource.addListener(onComicSourceChange); + ComicSourceManager().addListener(onComicSourceChange); super.initState(); } @override void dispose() { - ComicSource.removeListener(onComicSourceChange); + ComicSourceManager().removeListener(onComicSourceChange); super.dispose(); } int get _availableUpdates { int c = 0; - ComicSource.availableUpdates.forEach((key, version) { + ComicSourceManager().availableUpdates.forEach((key, version) { var source = ComicSource.find(key); if (source != null) { if (compareSemVer(version, source.version)) { diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 08ab2e7..56f14c9 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -103,7 +103,7 @@ Future importAppData(File file, [bool checkVersion = false]) async { await file.copy(targetFile); } } - await ComicSource.reload(); + await ComicSourceManager().reload(); } } finally { cacheDir.deleteIgnoreError(recursive: true); diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index 193ce5a..721ddf6 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -19,7 +19,7 @@ class DataSync with ChangeNotifier { downloadData(); } LocalFavoritesManager().addListener(onDataChanged); - ComicSource.addListener(onDataChanged); + ComicSourceManager().addListener(onDataChanged); } void onDataChanged() { diff --git a/lib/utils/init.dart b/lib/utils/init.dart new file mode 100644 index 0000000..96d4bc7 --- /dev/null +++ b/lib/utils/init.dart @@ -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 = >[]; + + /// Ensure the class is initialized. + Future ensureInit() async { + if (_isInit) { + return; + } + var completer = Completer(); + _initCompleter.add(completer); + return completer.future; + } + + Future _markInit() async { + _isInit = true; + for (var completer in _initCompleter) { + completer.complete(); + } + _initCompleter.clear(); + } + + @protected + Future doInit(); + + /// Initialize the class. + Future init() async { + if (_isInit) { + return; + } + await doInit(); + await _markInit(); + } +} \ No newline at end of file