From 6c8a7d62a626f69e69681881e460d3621daae391 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 17 Oct 2024 12:27:20 +0800 Subject: [PATCH] add loginWithWebview, mixed explore page, app links, html node api; improve ui --- android/app/src/main/AndroidManifest.xml | 18 ++ assets/init.js | 135 ++++++++++++--- lib/components/layout.dart | 58 ++++--- lib/components/loading.dart | 6 +- lib/foundation/comic_source/category.dart | 3 +- lib/foundation/comic_source/comic_source.dart | 37 ++-- lib/foundation/comic_source/models.dart | 35 +++- lib/foundation/comic_source/parser.dart | 119 +++++++++++-- lib/foundation/js_engine.dart | 36 +++- lib/foundation/local.dart | 3 + lib/main.dart | 4 + lib/network/cloudflare.dart | 22 +-- lib/pages/accounts_page.dart | 116 ++++++++++--- lib/pages/comic_page.dart | 19 ++- lib/pages/explore_page.dart | 7 - lib/pages/favorites/local_favorites_page.dart | 2 + lib/pages/history_page.dart | 1 + lib/pages/reader/scaffold.dart | 9 +- lib/pages/search_result_page.dart | 14 +- lib/pages/webview.dart | 159 +++++++++++------- lib/utils/app_links.dart | 30 ++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 40 +++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 28 files changed, 686 insertions(+), 199 deletions(-) create mode 100644 lib/utils/app_links.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a48a7ff..b2c3459 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -25,6 +25,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/init.js b/assets/init.js index 33e4dfe..eb4ca22 100644 --- a/assets/init.js +++ b/assets/init.js @@ -1,5 +1,7 @@ /* Venera JavaScript Library + +This library provides a set of APIs for interacting with the Venera app. */ /// encode, decode, hash, decrypt @@ -305,7 +307,7 @@ let Network = { * @param {string} url - The URL to send the request to. * @param {Object} headers - The headers to include in the request. * @param data - The data to send with the request. - * @returns {Promise} The response from the request. + * @returns {Promise<{status: number, headers: {}, body: ArrayBuffer}>} The response from the request. */ async fetchBytes(method, url, headers, data) { let result = await sendMessage({ @@ -330,7 +332,7 @@ let Network = { * @param {string} url - The URL to send the request to. * @param {Object} headers - The headers to include in the request. * @param data - The data to send with the request. - * @returns {Promise} The response from the request. + * @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request. */ async sendRequest(method, url, headers, data) { let result = await sendMessage({ @@ -352,7 +354,7 @@ let Network = { * Sends an HTTP GET request. * @param {string} url - The URL to send the request to. * @param {Object} headers - The headers to include in the request. - * @returns {Promise} The response from the request. + * @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request. */ async get(url, headers) { return this.sendRequest('GET', url, headers); @@ -363,7 +365,7 @@ let Network = { * @param {string} url - The URL to send the request to. * @param {Object} headers - The headers to include in the request. * @param data - The data to send with the request. - * @returns {Promise} The response from the request. + * @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request. */ async post(url, headers, data) { return this.sendRequest('POST', url, headers, data); @@ -374,7 +376,7 @@ let Network = { * @param {string} url - The URL to send the request to. * @param {Object} headers - The headers to include in the request. * @param data - The data to send with the request. - * @returns {Promise} The response from the request. + * @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request. */ async put(url, headers, data) { return this.sendRequest('PUT', url, headers, data); @@ -385,7 +387,7 @@ let Network = { * @param {string} url - The URL to send the request to. * @param {Object} headers - The headers to include in the request. * @param data - The data to send with the request. - * @returns {Promise} The response from the request. + * @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request. */ async patch(url, headers, data) { return this.sendRequest('PATCH', url, headers, data); @@ -395,7 +397,7 @@ let Network = { * Sends an HTTP DELETE request. * @param {string} url - The URL to send the request to. * @param {Object} headers - The headers to include in the request. - * @returns {Promise} The response from the request. + * @returns {Promise<{status: number, headers: {}, body: string}>} The response from the request. */ async delete(url, headers) { return this.sendRequest('DELETE', url, headers); @@ -577,6 +579,91 @@ class HtmlElement { }) return ks.map(k => new HtmlElement(k)); } + + /** + * Get the nodes of the current element. + * @returns {HtmlNode[]} An array of nodes. + */ + get nodes() { + let ks = sendMessage({ + method: "html", + function: "getNodes", + key: this.key + }) + return ks.map(k => new HtmlNode(k)); + } + + /** + * Get inner HTML of the element. + * @returns {string} The inner HTML. + */ + get innerHTML() { + return sendMessage({ + method: "html", + function: "getInnerHTML", + key: this.key + }) + } + + /** + * Get parent element of the element. If the element has no parent, return null. + * @returns {HtmlElement|null} + */ + get parent() { + let k = sendMessage({ + method: "html", + function: "getParent", + key: this.key + }) + if(!k) return null; + return new HtmlElement(k); + } +} + +class HtmlNode { + key = 0; + + constructor(k) { + this.key = k; + } + + /** + * Get the text content of the node. + * @returns {string} The text content. + */ + get text() { + return sendMessage({ + method: "html", + function: "node_text", + key: this.key + }) + } + + /** + * Get the type of the node. + * @returns {string} The type of the node. ("text", "element", "comment", "document", "unknown") + */ + get type() { + return sendMessage({ + method: "html", + function: "node_type", + key: this.key + }) + } + + /** + * Convert the node to an HtmlElement. If the node is not an element, return null. + * @returns {HtmlElement|null} + */ + toElement() { + let k = sendMessage({ + method: "html", + function: "node_toElement", + key: this.key + }) + if(!k) return null; + return new HtmlElement(k); + } } function log(level, title, content) { @@ -608,10 +695,11 @@ let console = { * @param cover {string} * @param tags {string[]} * @param description {string} - * @param maxPage {number | null} + * @param maxPage {number?} + * @param language {string?} * @constructor */ -function Comic({id, title, subtitle, cover, tags, description, maxPage}) { +function Comic({id, title, subtitle, cover, tags, description, maxPage, language}) { this.id = id; this.title = title; this.subtitle = subtitle; @@ -619,26 +707,27 @@ function Comic({id, title, subtitle, cover, tags, description, maxPage}) { this.tags = tags; this.description = description; this.maxPage = maxPage; + this.language = language; } /** * Create a comic details object * @param title {string} * @param cover {string} - * @param description {string | null} - * @param tags {Map | {} | null} - * @param chapters {Map | {} | null} - key: chapter id, value: chapter title - * @param isFavorite {boolean | null} - favorite status. If the comic source supports multiple folders, this field should be null - * @param subId {string | null} - a param which is passed to comments api - * @param thumbnails {string[] | null} - for multiple page thumbnails, set this to null, and use `loadThumbnails` api to load thumbnails - * @param recommend {Comic[] | null} - related comics - * @param commentCount {number | null} - * @param likesCount {number | null} - * @param isLiked {boolean | null} - * @param uploader {string | null} - * @param updateTime {string | null} - * @param uploadTime {string | null} - * @param url {string | null} + * @param description {string?} + * @param tags {Map | {} | null | undefined} + * @param chapters {Map | {} | null | undefined}} - key: chapter id, value: chapter title + * @param isFavorite {boolean | null | undefined}} - favorite status. If the comic source supports multiple folders, this field should be null + * @param subId {string?} - a param which is passed to comments api + * @param thumbnails {string[]? - for multiple page thumbnails, set this to null, and use `loadThumbnails` api to load thumbnails + * @param recommend {Comic[]?} - related comics + * @param commentCount {number?} + * @param likesCount {number?} + * @param isLiked {boolean?} + * @param uploader {string?} + * @param updateTime {string?} + * @param uploadTime {string?} + * @param url {string?} * @constructor */ function ComicDetails({title, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url}) { diff --git a/lib/components/layout.dart b/lib/components/layout.dart index 6cab85a..04d08ea 100644 --- a/lib/components/layout.dart +++ b/lib/components/layout.dart @@ -3,9 +3,9 @@ part of 'components.dart'; class SliverGridViewWithFixedItemHeight extends StatelessWidget { const SliverGridViewWithFixedItemHeight( {required this.delegate, - required this.maxCrossAxisExtent, - required this.itemHeight, - super.key}); + required this.maxCrossAxisExtent, + required this.itemHeight, + super.key}); final SliverChildDelegate delegate; @@ -16,13 +16,14 @@ class SliverGridViewWithFixedItemHeight extends StatelessWidget { @override Widget build(BuildContext context) { return SliverLayoutBuilder( - builder: ((context, constraints) => SliverGrid( - delegate: delegate, - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: maxCrossAxisExtent, - childAspectRatio: - calcChildAspectRatio(constraints.crossAxisExtent)), - ))); + builder: (context, constraints) => SliverGrid( + delegate: delegate, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: maxCrossAxisExtent, + childAspectRatio: calcChildAspectRatio(constraints.crossAxisExtent), + ), + ), + ); } double calcChildAspectRatio(double width) { @@ -35,7 +36,7 @@ class SliverGridViewWithFixedItemHeight extends StatelessWidget { } } -class SliverGridDelegateWithFixedHeight extends SliverGridDelegate{ +class SliverGridDelegateWithFixedHeight extends SliverGridDelegate { const SliverGridDelegateWithFixedHeight({ required this.maxCrossAxisExtent, required this.itemHeight, @@ -58,23 +59,21 @@ class SliverGridDelegateWithFixedHeight extends SliverGridDelegate{ crossAxisStride: width / crossItems, childMainAxisExtent: itemHeight, childCrossAxisExtent: width / crossItems, - reverseCrossAxis: false - ); + reverseCrossAxis: false); } @override bool shouldRelayout(covariant SliverGridDelegate oldDelegate) { - if(oldDelegate is! SliverGridDelegateWithFixedHeight) return true; - if(oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent - || oldDelegate.itemHeight != itemHeight){ + if (oldDelegate is! SliverGridDelegateWithFixedHeight) return true; + if (oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent || + oldDelegate.itemHeight != itemHeight) { return true; } return false; } - } -class SliverGridDelegateWithComics extends SliverGridDelegate{ +class SliverGridDelegateWithComics extends SliverGridDelegate { SliverGridDelegateWithComics([this.useBriefMode = false, this.scale]); final bool useBriefMode; @@ -83,14 +82,17 @@ class SliverGridDelegateWithComics extends SliverGridDelegate{ @override SliverGridLayout getLayout(SliverConstraints constraints) { - if(appdata.settings['comicDisplayMode'] == 'brief' || useBriefMode){ - return getBriefModeLayout(constraints, scale ?? appdata.settings['comicTileScale']); + if (appdata.settings['comicDisplayMode'] == 'brief' || useBriefMode) { + return getBriefModeLayout( + constraints, scale ?? appdata.settings['comicTileScale']); } else { - return getDetailedModeLayout(constraints, scale ?? appdata.settings['comicTileScale']); + return getDetailedModeLayout(constraints, + scale ?? (appdata.settings['comicTileScale'] as num).toDouble()); } } - SliverGridLayout getDetailedModeLayout(SliverConstraints constraints, double scale){ + SliverGridLayout getDetailedModeLayout( + SliverConstraints constraints, double scale) { const minCrossAxisExtent = 360; final itemHeight = 152 * scale; final width = constraints.crossAxisExtent; @@ -102,15 +104,17 @@ class SliverGridDelegateWithComics extends SliverGridDelegate{ crossAxisStride: width / crossItems, childMainAxisExtent: itemHeight, childCrossAxisExtent: width / crossItems, - reverseCrossAxis: false - ); + reverseCrossAxis: false); } - SliverGridLayout getBriefModeLayout(SliverConstraints constraints, double scale){ + SliverGridLayout getBriefModeLayout( + SliverConstraints constraints, double scale) { final maxCrossAxisExtent = 192.0 * scale; const childAspectRatio = 0.72; const crossAxisSpacing = 0.0; - int crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil(); + int crossAxisCount = + (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)) + .ceil(); // Ensure a minimum count of 1, can be zero and result in an infinite extent // below when the window size is 0. crossAxisCount = math.max(1, crossAxisCount); @@ -134,4 +138,4 @@ class SliverGridDelegateWithComics extends SliverGridDelegate{ bool shouldRelayout(covariant SliverGridDelegate oldDelegate) { return true; } -} \ No newline at end of file +} diff --git a/lib/components/loading.dart b/lib/components/loading.dart index 6bbd367..4b3b2d7 100644 --- a/lib/components/loading.dart +++ b/lib/components/loading.dart @@ -199,7 +199,7 @@ abstract class MultiPageLoadingState int _page = 1; - int _maxPage = 1; + int? _maxPage; Future>> loadData(int page); @@ -211,10 +211,10 @@ abstract class MultiPageLoadingState bool get isFirstLoading => _isFirstLoading; - bool get haveNextPage => _page <= _maxPage; + bool get haveNextPage => _maxPage == null || _page <= _maxPage!; void nextPage() { - if (_page > _maxPage) return; + if (_maxPage != null && _page > _maxPage!) return; if (_isLoading) return; _isLoading = true; loadData(_page).then((value) { diff --git a/lib/foundation/comic_source/category.dart b/lib/foundation/comic_source/category.dart index 3cb8e7c..6bc91dc 100644 --- a/lib/foundation/comic_source/category.dart +++ b/lib/foundation/comic_source/category.dart @@ -88,7 +88,8 @@ class RandomCategoryPart extends BaseCategoryPart { if (randomNumber >= tags.length) { return tags; } - return tags.sublist(math.Random().nextInt(tags.length - randomNumber)); + var start = math.Random().nextInt(tags.length - randomNumber); + return tags.sublist(start, start + randomNumber); } @override diff --git a/lib/foundation/comic_source/comic_source.dart b/lib/foundation/comic_source/comic_source.dart index e2ff7a6..f945e6a 100644 --- a/lib/foundation/comic_source/comic_source.dart +++ b/lib/foundation/comic_source/comic_source.dart @@ -197,6 +197,8 @@ class ComicSource { final HandleClickTagEvent? handleClickTagEvent; + final LinkHandler? linkHandler; + Future loadData() async { var file = File("${App.dataPath}/comic_source/$key.data"); if (await file.exists()) { @@ -261,14 +263,13 @@ class ComicSource { this.idMatcher, this.translations, this.handleClickTagEvent, + this.linkHandler, ); } class AccountConfig { final LoginFunction? login; - final FutureOr Function(BuildContext)? onLogin; - final String? loginWebsite; final String? registerWebsite; @@ -279,10 +280,15 @@ class AccountConfig { final List infoItems; + final bool Function(String url, String title)? checkLoginStatus; + const AccountConfig( - this.login, this.loginWebsite, this.registerWebsite, this.logout, - {this.onLogin}) - : allowReLogin = true, + this.login, + this.loginWebsite, + this.registerWebsite, + this.logout, + this.checkLoginStatus, + ) : allowReLogin = true, infoItems = const []; } @@ -315,11 +321,13 @@ class ExplorePageData { /// return a `List` contains `List` or `ExplorePagePart` final Future>> Function(int index)? loadMixed; - final WidgetBuilder? overridePageBuilder; - - ExplorePageData(this.title, this.type, this.loadPage, this.loadMultiPart) - : loadMixed = null, - overridePageBuilder = null; + ExplorePageData( + this.title, + this.type, + this.loadPage, + this.loadMultiPart, + this.loadMixed, + ); } class ExplorePagePart { @@ -422,3 +430,12 @@ class CategoryComicsOptions { const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen); } + + +class LinkHandler { + final List domains; + + final String? Function(String url) linkToId; + + const LinkHandler(this.domains, this.linkToId); +} \ No newline at end of file diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 2d2554b..a4d6a44 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -11,11 +11,23 @@ class Comment { final bool? isLiked; final int? voteStatus; // 1: upvote, -1: downvote, 0: none + static String? parseTime(dynamic value) { + if(value == null) return null; + if(value is int) { + if(value < 10000000000) { + return DateTime.fromMillisecondsSinceEpoch(value * 1000).toString().substring(0, 19); + } else { + return DateTime.fromMillisecondsSinceEpoch(value).toString().substring(0, 19); + } + } + return value.toString(); + } + Comment.fromJson(Map json) : userName = json["userName"], avatar = json["avatar"], content = json["content"], - time = json["time"], + time = parseTime(json["time"]), replyCount = json["replyCount"], id = json["id"].toString(), score = json["score"], @@ -40,8 +52,19 @@ class Comic { final int? maxPage; - const Comic(this.title, this.cover, this.id, this.subtitle, this.tags, - this.description, this.sourceKey, this.maxPage); + final String? language; + + const Comic( + this.title, + this.cover, + this.id, + this.subtitle, + this.tags, + this.description, + this.sourceKey, + this.maxPage, + this.language, + ); Map toJson() { return { @@ -53,6 +76,7 @@ class Comic { "description": description, "sourceKey": sourceKey, "maxPage": maxPage, + "language": language, }; } @@ -63,7 +87,8 @@ class Comic { id = json["id"], tags = List.from(json["tags"] ?? []), description = json["description"] ?? "", - maxPage = json["maxPage"]; + maxPage = json["maxPage"], + language = json["language"]; } class ComicDetails with HistoryMixin { @@ -109,7 +134,7 @@ class ComicDetails with HistoryMixin { final String? url; - static Map> _generateMap(Map map) { + static Map> _generateMap(Map map) { var res = >{}; map.forEach((key, value) { res[key] = List.from(value); diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart index f881c8b..3bd9f80 100644 --- a/lib/foundation/comic_source/parser.dart +++ b/lib/foundation/comic_source/parser.dart @@ -147,6 +147,7 @@ class ComicSourceParser { _parseIdMatch(), _parseTranslation(), _parseClickTagEvent(), + _parseLinkHandler(), ); await source.loadData(); @@ -199,8 +200,28 @@ class ComicSourceParser { JsEngine().runCode("ComicSource.sources.$_key.account.logout()"); } - return AccountConfig(login, _getValue("account.login.website"), - _getValue("account.registerWebsite"), logout); + if(!_checkExists('account.loginWithWebview')) { + return AccountConfig( + login, + null, + _getValue("account.registerWebsite"), + logout, + null, + ); + } else { + return AccountConfig( + null, + _getValue("account.loginWithWebview.url"), + _getValue("account.registerWebsite"), + logout, + (url, title) { + return JsEngine().runCode(""" + ComicSource.sources.$_key.account.loginWithWebview.checkStatus( + ${jsonEncode(url)}, ${jsonEncode(title)}) + """); + }, + ); + } } List _loadExploreData() { @@ -214,6 +235,7 @@ class ComicSourceParser { final String type = _getValue("explore[$i].type"); Future>> Function()? loadMultiPart; Future>> Function(int page)? loadPage; + Future>> Function(int index)? loadMixed; if (type == "singlePageWithMultiPart") { loadMultiPart = () async { try { @@ -246,18 +268,69 @@ class ComicSourceParser { return Res.error(e.toString()); } }; + } else if (type == "multiPartPage") { + loadMultiPart = () async { + try { + var res = await JsEngine() + .runCode("ComicSource.sources.$_key.explore[$i].load()"); + return Res( + List.from( + (res as List).map((e) { + return ExplorePagePart( + e['title'], + (e['comics'] as List).map((e) { + return Comic.fromJson(e, _key!); + }).toList(), + e['viewMore'], + ); + }), + ), + ); + } catch (e, s) { + Log.error("Data Analysis", "$e\n$s"); + return Res.error(e.toString()); + } + }; + } else if (type == 'mixed') { + loadMixed = (index) async { + try { + var res = await JsEngine().runCode( + "ComicSource.sources.$_key.explore[$i].load(${jsonEncode(index)})"); + var list = []; + for (var data in (res['data'] as List)) { + if (data is List) { + list.add(data.map((e) => Comic.fromJson(e, _key!)).toList()); + } else if (data is Map) { + list.add(ExplorePagePart( + data['title'], + (data['comics'] as List).map((e) { + return Comic.fromJson(e, _key!); + }).toList(), + data['viewMore'], + )); + } + } + return Res(list, subData: res['maxPage']); + } catch (e, s) { + Log.error("Network", "$e\n$s"); + return Res.error(e.toString()); + } + }; } pages.add(ExplorePageData( - title, - switch (type) { - "singlePageWithMultiPart" => - ExplorePageType.singlePageWithMultiPart, - "multiPageComicList" => ExplorePageType.multiPageComicList, - _ => - throw ComicSourceParseException("Unknown explore page type $type") - }, - loadPage, - loadMultiPart)); + title, + switch (type) { + "singlePageWithMultiPart" => ExplorePageType.singlePageWithMultiPart, + "multiPartPage" => ExplorePageType.singlePageWithMultiPart, + "multiPageComicList" => ExplorePageType.multiPageComicList, + "mixed" => ExplorePageType.mixed, + _ => + throw ComicSourceParseException("Unknown explore page type $type") + }, + loadPage, + loadMultiPart, + loadMixed, + )); } return pages; } @@ -279,8 +352,11 @@ class ComicSourceParser { final String type = c["type"]; final List tags = List.from(c["categories"]); final String itemType = c["itemType"]; - final List? categoryParams = - c["categoryParams"] == null ? null : List.from(c["categoryParams"]); + List? categoryParams = ListOrNull.from(c["categoryParams"]); + final String? groupParam = c["groupParam"]; + if (groupParam != null) { + categoryParams = List.filled(tags.length, groupParam); + } if (type == "fixed") { categoryParts .add(FixedCategoryPart(name, tags, itemType, categoryParams)); @@ -407,6 +483,7 @@ class ComicSourceParser { if (res is! Map) throw "Invalid data"; res['comicId'] = id; res['sourceKey'] = _key; + JsEngine().clearHtml(); return Res(ComicDetails.fromJson(res)); } catch (e, s) { Log.error("Network", "$e\n$s"); @@ -728,4 +805,18 @@ class ComicSourceParser { return Map.from(r); }; } + + LinkHandler? _parseLinkHandler() { + if (!_checkExists("linkHandler")) { + return null; + } + List domains = List.from(_getValue("link.domains")); + linkToId(String link) { + var res = JsEngine().runCode(""" + ComicSource.sources.$_key.link.linkToId(${jsonEncode(link)}) + """); + return res as String?; + } + return LinkHandler(domains, linkToId); + } } diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart index 3192d0a..52f2e11 100644 --- a/lib/foundation/js_engine.dart +++ b/lib/foundation/js_engine.dart @@ -225,6 +225,7 @@ class JsEngine with _JSEngineApi{ mixin class _JSEngineApi{ final Map _documents = {}; final Map _elements = {}; + final Map _nodes = {}; CookieJarSql? _cookieJar; dynamic handleHtmlCallback(Map data) { @@ -270,6 +271,38 @@ mixin class _JSEngineApi{ keys.add(_elements.length - 1); } return keys; + case "getNodes": + var res = _elements[data["key"]]!.nodes; + var keys = []; + for (var node in res) { + _nodes[_nodes.length] = node; + keys.add(_nodes.length - 1); + } + return keys; + case "getInnerHTML": + return _elements[data["key"]]!.innerHtml; + case "getParent": + var res = _elements[data["key"]]!.parent; + if(res == null) return null; + _elements[_elements.length] = res; + return _elements.length - 1; + case "node_text": + return _nodes[data["key"]]!.text; + case "node_type": + return switch(_nodes[data["key"]]!.nodeType) { + dom.Node.ELEMENT_NODE => "element", + dom.Node.TEXT_NODE => "text", + dom.Node.COMMENT_NODE => "comment", + dom.Node.DOCUMENT_NODE => "document", + _ => "unknown" + }; + case "node_to_element": + var node = _nodes[data["key"]]!; + if(node is dom.Element){ + _elements[_elements.length] = node; + return _elements.length - 1; + } + return null; } } @@ -305,9 +338,10 @@ mixin class _JSEngineApi{ } } - void clear(){ + void clearHtml(){ _documents.clear(); _elements.clear(); + _nodes.clear(); } void clearCookies(List domains) async{ diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 7cd221f..988311f 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -120,6 +120,9 @@ class LocalComic with HistoryMixin implements Comic { @override String? get subTitle => subtitle; + + @override + String? get language => null; } class LocalManager with ChangeNotifier { diff --git a/lib/main.dart b/lib/main.dart index 4619dc6..858b97e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/pages/main_page.dart'; +import 'package:venera/utils/app_links.dart'; import 'package:window_manager/window_manager.dart'; import 'components/components.dart'; import 'components/window_frame.dart'; @@ -19,6 +20,9 @@ void main(List args) { runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); await init(); + if(App.isAndroid) { + handleLinks(); + } FlutterError.onError = (details) { Log.error( "Unhandled Exception", "${details.exception}\n${details.stack}"); diff --git a/lib/network/cloudflare.dart b/lib/network/cloudflare.dart index e69f0b7..7d8c5f2 100644 --- a/lib/network/cloudflare.dart +++ b/lib/network/cloudflare.dart @@ -1,10 +1,12 @@ import 'dart:io' as io; import 'package:dio/dio.dart'; +import 'package:flutter_qjs/flutter_qjs.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/consts.dart'; import 'package:venera/pages/webview.dart'; +import 'package:venera/utils/ext.dart'; import 'cookie_jar.dart'; @@ -113,7 +115,9 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async { ); } - if (App.isDesktop && (await DesktopWebview.isAvailable())) { + // windows version of package `flutter_inappwebview` cannot get some cookies + // Using DesktopWebview instead + if (App.isLinux || App.isWindows) { var webview = DesktopWebview( initialUrl: url, onTitleChange: (title, controller) async { @@ -136,12 +140,12 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async { }, ); webview.open(); - } else if (App.isMobile) { + } else { await App.rootContext.to( () => AppWebview( initialUrl: url, singlePage: true, - onTitleChange: (title, controller) async { + onLoadStop: (controller) async { var res = await controller.platform.evaluateJavascript( source: "document.head.innerHTML.includes('#challenge-success-text')"); @@ -151,11 +155,11 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async { appdata.implicitData['ua'] = ua; appdata.writeImplicitData(); } - var cookiesMap = await controller.getCookies(url) ?? {}; - if(cookiesMap['cf_clearance'] == null) { + var cookies = await controller.getCookies(url) ?? []; + if(cookies.firstWhereOrNull((element) => element.name == 'cf_clearance') == null) { return; } - saveCookies(cookiesMap); + SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies); App.rootPop(); } }, @@ -165,13 +169,11 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async { appdata.implicitData['ua'] = ua; appdata.writeImplicitData(); } - var cookiesMap = await controller.getCookies(url) ?? {}; - saveCookies(cookiesMap); + var cookies = await controller.getCookies(url) ?? []; + SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies); }, ), ); onFinished(); - } else { - App.rootContext.showMessage(message: "Unsupported device"); } } diff --git a/lib/pages/accounts_page.dart b/lib/pages/accounts_page.dart index 1816c4e..73c241e 100644 --- a/lib/pages/accounts_page.dart +++ b/lib/pages/accounts_page.dart @@ -5,6 +5,8 @@ import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/state_controller.dart'; +import 'package:venera/network/cookie_jar.dart'; +import 'package:venera/pages/webview.dart'; import 'package:venera/utils/translations.dart'; class AccountsPageLogic extends StateController { @@ -60,18 +62,13 @@ class AccountsPage extends StatelessWidget { title: Text("Log in".tl), trailing: const Icon(Icons.arrow_right), onTap: () async { - if (element.account!.onLogin != null) { - await element.account!.onLogin!(context); - } - if (element.account!.login != null && context.mounted) { - await context.to( - () => _LoginPage( - login: element.account!.login!, - registerWebsite: element.account!.registerWebsite, - ), - ); - element.saveData(); - } + await context.to( + () => _LoginPage( + config: element.account!, + source: element, + ), + ); + element.saveData(); logic.update(); }, ); @@ -121,7 +118,7 @@ class AccountsPage extends StatelessWidget { ); } yield ListTile( - title: Text("Exit".tl), + title: Text("Log out".tl), onTap: () { element.data["account"] = null; element.account?.logout(); @@ -146,11 +143,11 @@ class AccountsPage extends StatelessWidget { } class _LoginPage extends StatefulWidget { - const _LoginPage({required this.login, this.registerWebsite}); + const _LoginPage({required this.config, required this.source}); - final LoginFunction login; + final AccountConfig config; - final String? registerWebsite; + final ComicSource source; @override State<_LoginPage> createState() => _LoginPageState(); @@ -181,6 +178,7 @@ class _LoginPageState extends State<_LoginPage> { labelText: "Username".tl, border: const OutlineInputBorder(), ), + enabled: widget.config.login != null, onChanged: (s) { username = s; }, @@ -192,21 +190,39 @@ class _LoginPageState extends State<_LoginPage> { border: const OutlineInputBorder(), ), obscureText: true, + enabled: widget.config.login != null, onChanged: (s) { password = s; }, onSubmitted: (s) => login(), ), const SizedBox(height: 32), - Button.filled( - isLoading: loading, - onPressed: login, - child: Text("Continue".tl), - ), - const SizedBox(height: 32), - if (widget.registerWebsite != null) + if (widget.config.login == null) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline), + const SizedBox(width: 8), + Text("Login with password is disabled".tl), + ], + ) + else + Button.filled( + isLoading: loading, + onPressed: login, + child: Text("Continue".tl), + ), + const SizedBox(height: 24), + if (widget.config.loginWebsite != null) + FilledButton( + onPressed: loginWithWebview, + child: Text("Login with webview".tl), + ), + const SizedBox(height: 8), + if (widget.config.registerWebsite != null) TextButton( - onPressed: () => launchUrlString(widget.registerWebsite!), + onPressed: () => + launchUrlString(widget.config.registerWebsite!), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -235,7 +251,7 @@ class _LoginPageState extends State<_LoginPage> { setState(() { loading = true; }); - widget.login(username, password).then((value) { + widget.config.login!(username, password).then((value) { if (value.error) { context.showMessage(message: value.errorMessage!); setState(() { @@ -248,4 +264,54 @@ class _LoginPageState extends State<_LoginPage> { } }); } + + void loginWithWebview() async { + var url = widget.config.loginWebsite!; + var title = ''; + bool success = false; + await context.to( + () => AppWebview( + initialUrl: widget.config.loginWebsite!, + onNavigation: (u, c) { + url = u; + print(url); + () async { + if (widget.config.checkLoginStatus != null) { + if (widget.config.checkLoginStatus!(url, title)) { + var cookies = (await c.getCookies(url)) ?? []; + SingleInstanceCookieJar.instance?.saveFromResponse( + Uri.parse(url), + cookies, + ); + success = true; + App.mainNavigatorKey?.currentContext?.pop(); + } + } + }(); + return false; + }, + onTitleChange: (t, c) { + () async { + if (widget.config.checkLoginStatus != null) { + if (widget.config.checkLoginStatus!(url, title)) { + var cookies = (await c.getCookies(url)) ?? []; + SingleInstanceCookieJar.instance?.saveFromResponse( + Uri.parse(url), + cookies, + ); + success = true; + App.mainNavigatorKey?.currentContext?.pop(); + } + } + }(); + title = t; + }, + ), + ); + if (success) { + widget.source.data['account'] = 'ok'; + widget.source.saveData(); + context.pop(); + } + } } diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index eeddbdf..b0d72e4 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -134,6 +134,7 @@ class _ComicPageState extends LoadingState const SizedBox(width: 16), Container( decoration: BoxDecoration( + color: context.colorScheme.primaryContainer, borderRadius: BorderRadius.circular(8), ), height: 144, @@ -369,11 +370,11 @@ class _ComicPageState extends LoadingState buildTag(text: comic.uploadTime!), ], ), - if (comic.uploadTime != null) + if (comic.updateTime != null) buildWrap( children: [ buildTag(text: 'Update Time'.tl, isTitle: true), - buildTag(text: comicSource.name), + buildTag(text: comic.updateTime!), ], ), const SizedBox(height: 12), @@ -1120,13 +1121,18 @@ class _FavoritePanelState extends State<_FavoritePanel> { cid: widget.cid, comicSource: comicSource, isFavorite: widget.isFavorite, + onFavorite: widget.onFavorite, ); } } class _NetworkFavorites extends StatefulWidget { - const _NetworkFavorites( - {required this.cid, required this.comicSource, required this.isFavorite}); + const _NetworkFavorites({ + required this.cid, + required this.comicSource, + required this.isFavorite, + required this.onFavorite, + }); final String cid; @@ -1134,6 +1140,8 @@ class _NetworkFavorites extends StatefulWidget { final bool? isFavorite; + final void Function(bool) onFavorite; + @override State<_NetworkFavorites> createState() => _NetworkFavoritesState(); } @@ -1167,7 +1175,10 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> { var res = await widget.comicSource.favoriteData! .addOrDelFavorite!(widget.cid, '', !isFavorite); if (res.success) { + widget.onFavorite(!isFavorite); context.pop(); + App.rootContext.showMessage( + message: isFavorite ? "Removed".tl : "Added".tl); } else { setState(() { isLoading = false; diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart index 84c51f7..ae1c18b 100644 --- a/lib/pages/explore_page.dart +++ b/lib/pages/explore_page.dart @@ -187,13 +187,6 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> { comicSourceKey, key: ValueKey(key), ); - } else if (data.overridePageBuilder != null) { - return Builder( - builder: (context) { - return data.overridePageBuilder!(context); - }, - key: ValueKey(key), - ); } else { return const Center( child: Text("Empty Page"), diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index 8c6bcf4..877c7d2 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -121,6 +121,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { "${e.time} | ${comicSource?.name ?? "Unknown"}", comicSource?.key ?? "Unknown", null, + null, ); }).toList(), menuBuilder: (c) { @@ -202,6 +203,7 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> { "${e.time} | ${comicSource?.name ?? "Unknown"}", comicSource?.key ?? "Unknown", null, + null, ), ); }, diff --git a/lib/pages/history_page.dart b/lib/pages/history_page.dart index fde879f..dd6fa2f 100644 --- a/lib/pages/history_page.dart +++ b/lib/pages/history_page.dart @@ -87,6 +87,7 @@ class _HistoryPageState extends State { getDescription(e), e.type.comicSource?.key ?? "Invalid", null, + null, ); }, ).toList(), diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index f911b43..a2429f8 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -94,7 +94,12 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { const BackButton(), const SizedBox(width: 8), Expanded( - child: Text(context.reader.widget.name, style: ts.s18), + child: Text( + context.reader.widget.name, + style: ts.s18, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), const SizedBox(width: 8), Tooltip( @@ -356,7 +361,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { context, ReaderSettings( onChanged: (key) { - if(key == "readerMode") { + if (key == "readerMode") { context.reader.mode = ReaderMode.fromKey(appdata.settings[key]); App.rootContext.pop(); } diff --git a/lib/pages/search_result_page.dart b/lib/pages/search_result_page.dart index f26b361..84f9abb 100644 --- a/lib/pages/search_result_page.dart +++ b/lib/pages/search_result_page.dart @@ -27,7 +27,15 @@ class _SearchResultPageState extends State { late List options; - void search([String? text]) {} + late String text; + + void search([String? text]) { + if (text != null) { + setState(() { + this.text = text; + }); + } + } @override void initState() { @@ -37,12 +45,14 @@ class _SearchResultPageState extends State { ); sourceKey = widget.sourceKey; options = widget.options; + text = widget.text; super.initState(); } @override Widget build(BuildContext context) { return ComicList( + key: Key(text + options.toString()), errorLeading: AppSearchBar( controller: controller, ), @@ -52,7 +62,7 @@ class _SearchResultPageState extends State { loadPage: (i) { var source = ComicSource.find(sourceKey); return source!.searchPageData!.loadPage!( - controller.initialText, + text, i, options, ); diff --git a/lib/pages/webview.dart b/lib/pages/webview.dart index 644440d..c16c3b4 100644 --- a/lib/pages/webview.dart +++ b/lib/pages/webview.dart @@ -11,11 +11,13 @@ import 'package:venera/foundation/app.dart'; import 'package:venera/network/app_dio.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/translations.dart'; +import 'dart:io' as io; -export 'package:flutter_inappwebview/flutter_inappwebview.dart' show WebUri, URLRequest; +export 'package:flutter_inappwebview/flutter_inappwebview.dart' + show WebUri, URLRequest; -extension WebviewExtension on InAppWebViewController{ - Future?> getCookies(String url) async{ +extension WebviewExtension on InAppWebViewController { + Future?> getCookies(String url) async { if(url.contains("https://")){ url.replaceAll("https://", ""); } @@ -24,18 +26,20 @@ extension WebviewExtension on InAppWebViewController{ } CookieManager cookieManager = CookieManager.instance(); final cookies = await cookieManager.getCookies(url: WebUri(url)); - Map res = {}; - for(var cookie in cookies){ - res[cookie.name] = cookie.value; + var res = []; + for (var cookie in cookies) { + var c = io.Cookie(cookie.name, cookie.value); + c.domain = cookie.domain; + res.add(c); } return res; } - Future getUA() async{ + Future getUA() async { var res = await evaluateJavascript(source: "navigator.userAgent"); - if(res is String){ - if(res[0] == "'" || res[0] == "\"") { - res = res.substring(1, res.length-1); + if (res is String) { + if (res[0] == "'" || res[0] == "\"") { + res = res.substring(1, res.length - 1); } } return res is String ? res : null; @@ -43,17 +47,27 @@ extension WebviewExtension on InAppWebViewController{ } class AppWebview extends StatefulWidget { - const AppWebview({required this.initialUrl, this.onTitleChange, - this.onNavigation, this.singlePage = false, this.onStarted, super.key}); + const AppWebview( + {required this.initialUrl, + this.onTitleChange, + this.onNavigation, + this.singlePage = false, + this.onStarted, + this.onLoadStop, + super.key}); final String initialUrl; - final void Function(String title, InAppWebViewController controller)? onTitleChange; + final void Function(String title, InAppWebViewController controller)? + onTitleChange; - final bool Function(String url)? onNavigation; + final bool Function(String url, InAppWebViewController controller)? + onNavigation; final void Function(InAppWebViewController controller)? onStarted; + final void Function(InAppWebViewController controller)? onLoadStop; + final bool singlePage; @override @@ -74,35 +88,42 @@ class _AppWebviewState extends State { message: "More", child: IconButton( icon: const Icon(Icons.more_horiz), - onPressed: (){ - showMenu(context: context, position: RelativeRect.fromLTRB( - MediaQuery.of(context).size.width, - 0, - MediaQuery.of(context).size.width, - 0 - ), items: [ - PopupMenuItem( - child: Text("Open in browser".tl), - onTap: () async => launchUrlString((await controller?.getUrl())!.path), - ), - PopupMenuItem( - child: Text("Copy link".tl), - onTap: () async => Clipboard.setData(ClipboardData(text: (await controller?.getUrl())!.path)), - ), - PopupMenuItem( - child: Text("Reload".tl), - onTap: () => controller?.reload(), - ), - ]); + onPressed: () { + showMenu( + context: context, + position: RelativeRect.fromLTRB( + MediaQuery.of(context).size.width, + 0, + MediaQuery.of(context).size.width, + 0), + items: [ + PopupMenuItem( + child: Text("Open in browser".tl), + onTap: () async => + launchUrlString((await controller?.getUrl())!.path), + ), + PopupMenuItem( + child: Text("Copy link".tl), + onTap: () async => Clipboard.setData(ClipboardData( + text: (await controller?.getUrl())!.path)), + ), + PopupMenuItem( + child: Text("Reload".tl), + onTap: () => controller?.reload(), + ), + ]); }, ), ) ]; Widget body = InAppWebView( + initialSettings: InAppWebViewSettings( + isInspectable: true, + ), initialUrlRequest: URLRequest(url: WebUri(widget.initialUrl)), - onTitleChanged: (c, t){ - if(mounted){ + onTitleChanged: (c, t) { + if (mounted) { setState(() { title = t ?? "Webview"; }); @@ -110,19 +131,24 @@ class _AppWebviewState extends State { widget.onTitleChange?.call(title, controller!); }, shouldOverrideUrlLoading: (c, r) async { - var res = widget.onNavigation?.call(r.request.url?.toString() ?? "") ?? false; - if(res) { + var res = + widget.onNavigation?.call(r.request.url?.toString() ?? "", c) ?? + false; + if (res) { return NavigationActionPolicy.CANCEL; } else { return NavigationActionPolicy.ALLOW; } }, - onWebViewCreated: (c){ + onWebViewCreated: (c) { controller = c; widget.onStarted?.call(c); }, - onProgressChanged: (c, p){ - if(mounted){ + onLoadStop: (c, r) { + widget.onLoadStop?.call(c); + }, + onProgressChanged: (c, p) { + if (mounted) { setState(() { _progress = p / 100; }); @@ -133,19 +159,22 @@ class _AppWebviewState extends State { body = Stack( children: [ Positioned.fill(child: body), - if(_progress < 1.0) - const Positioned.fill(child: Center( - child: CircularProgressIndicator())) + if (_progress < 1.0) + const Positioned.fill( + child: Center(child: CircularProgressIndicator())) ], ); return Scaffold( - appBar: Appbar( - title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis,), - actions: actions, - ), - body: body - ); + appBar: Appbar( + title: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + actions: actions, + ), + body: body); } } @@ -162,13 +191,12 @@ class DesktopWebview { final void Function()? onClose; - DesktopWebview({ - required this.initialUrl, - this.onTitleChange, - this.onNavigation, - this.onStarted, - this.onClose - }); + DesktopWebview( + {required this.initialUrl, + this.onTitleChange, + this.onNavigation, + this.onStarted, + this.onClose}); Webview? _webview; @@ -178,8 +206,8 @@ class DesktopWebview { void onMessage(String message) { var json = jsonDecode(message); - if(json is Map){ - if(json["id"] == "document_created"){ + if (json is Map) { + if (json["id"] == "document_created") { title = json["data"]["title"]; _ua = json["data"]["ua"]; onTitleChange?.call(title!, this); @@ -210,14 +238,15 @@ class DesktopWebview { } collect(); '''; - if(_webview != null) { + if (_webview != null) { onMessage(await evaluateJavascript(js) ?? ''); } }); } void open() async { - _webview = await WebviewWindow.create(configuration: CreateConfiguration( + _webview = await WebviewWindow.create( + configuration: CreateConfiguration( useWindowPositionAndSize: true, userDataFolderWindows: "${App.dataPath}\\webview", title: "webview", @@ -242,11 +271,11 @@ class DesktopWebview { return _webview!.evaluateJavaScript(source); } - Future> getCookies(String url) async{ + Future> getCookies(String url) async { var allCookies = await _webview!.getAllCookies(); var res = {}; - for(var c in allCookies) { - if(_cookieMatch(url, c.domain)){ + for (var c in allCookies) { + if (_cookieMatch(url, c.domain)) { res[_removeCode0(c.name)] = _removeCode0(c.value); } } @@ -279,4 +308,4 @@ class DesktopWebview { _webview?.close(); _webview = null; } -} \ No newline at end of file +} diff --git a/lib/utils/app_links.dart b/lib/utils/app_links.dart new file mode 100644 index 0000000..2e36be3 --- /dev/null +++ b/lib/utils/app_links.dart @@ -0,0 +1,30 @@ +import 'package:app_links/app_links.dart'; +import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; +import 'package:venera/pages/comic_page.dart'; + +void handleLinks() { + final appLinks = AppLinks(); + appLinks.uriLinkStream.listen((uri) { + handleAppLink(uri); + }); +} + +void handleAppLink(Uri uri) async { + for(var source in ComicSource.all()) { + if(source.linkHandler != null) { + if(source.linkHandler!.domains.contains(uri.host)) { + var id = source.linkHandler!.linkToId(uri.toString()); + if(id != null) { + if(App.mainNavigatorKey == null) { + await Future.delayed(const Duration(milliseconds: 200)); + } + App.mainNavigatorKey!.currentContext?.to(() { + return ComicPage(id: id, sourceKey: source.key); + }); + } + return; + } + } + } +} \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 8bc6f9c..bee88c4 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_qjs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterQjsPlugin"); flutter_qjs_plugin_register_with_registrar(flutter_qjs_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) screen_retriever_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 1ddbe25..a37a6c8 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_webview_window flutter_qjs + gtk screen_retriever sqlite3_flutter_libs url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 3e07b38..786de6e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import app_links import desktop_webview_window import flutter_inappwebview_macos import path_provider_foundation @@ -15,6 +16,7 @@ import url_launcher_macos import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/pubspec.lock b/pubspec.lock index e2171d3..93862a8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,38 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + app_links: + dependency: "direct main" + description: + name: app_links + sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" async: dependency: transitive description: @@ -255,6 +287,14 @@ packages: description: flutter source: sdk version: "0.0.0" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" html: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 8c905f8..0911462 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: url: https://github.com/wgh136/flutter_desktop_webview path: packages/desktop_webview_window flutter_inappwebview: ^6.1.5 + app_links: ^6.3.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index fe07201..c807ed8 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -16,6 +17,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); DesktopWebviewWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b64cb9b..e0556be 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links desktop_webview_window flutter_inappwebview_windows flutter_qjs