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