mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
add loginWithWebview, mixed explore page, app links, html node api;
improve ui
This commit is contained in:
@@ -25,6 +25,24 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" android:host="nhentai.net" android:pathPrefix="/g" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" android:host="e-hentai.org" android:pathPrefix="/g" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" android:host="exhentai.org" android:pathPrefix="/g" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
|
135
assets/init.js
135
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<ArrayBuffer>} 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<Object>} 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<Object>} 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<Object>} 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<Object>} 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<Object>} 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<Object>} 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<string, string[]> | {} | null}
|
||||
* @param chapters {Map<string, string> | {} | 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<string, string[]> | {} | null | undefined}
|
||||
* @param chapters {Map<string, string> | {} | 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}) {
|
||||
|
@@ -16,13 +16,14 @@ class SliverGridViewWithFixedItemHeight extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverLayoutBuilder(
|
||||
builder: ((context, constraints) => SliverGrid(
|
||||
builder: (context, constraints) => SliverGrid(
|
||||
delegate: delegate,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||
childAspectRatio:
|
||||
calcChildAspectRatio(constraints.crossAxisExtent)),
|
||||
)));
|
||||
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);
|
||||
|
@@ -199,7 +199,7 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
|
||||
|
||||
int _page = 1;
|
||||
|
||||
int _maxPage = 1;
|
||||
int? _maxPage;
|
||||
|
||||
Future<Res<List<S>>> loadData(int page);
|
||||
|
||||
@@ -211,10 +211,10 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
|
||||
|
||||
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) {
|
||||
|
@@ -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
|
||||
|
@@ -197,6 +197,8 @@ class ComicSource {
|
||||
|
||||
final HandleClickTagEvent? handleClickTagEvent;
|
||||
|
||||
final LinkHandler? linkHandler;
|
||||
|
||||
Future<void> 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<void> Function(BuildContext)? onLogin;
|
||||
|
||||
final String? loginWebsite;
|
||||
|
||||
final String? registerWebsite;
|
||||
@@ -279,10 +280,15 @@ class AccountConfig {
|
||||
|
||||
final List<AccountInfoItem> 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<Comic>` or `ExplorePagePart`
|
||||
final Future<Res<List<Object>>> 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<String> domains;
|
||||
|
||||
final String? Function(String url) linkToId;
|
||||
|
||||
const LinkHandler(this.domains, this.linkToId);
|
||||
}
|
@@ -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<String, dynamic> 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<String, dynamic> 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<String>.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<String, List<String>> _generateMap(Map<String, dynamic> map) {
|
||||
static Map<String, List<String>> _generateMap(Map<dynamic, dynamic> map) {
|
||||
var res = <String, List<String>>{};
|
||||
map.forEach((key, value) {
|
||||
res[key] = List<String>.from(value);
|
||||
|
@@ -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<ExplorePageData> _loadExploreData() {
|
||||
@@ -214,6 +235,7 @@ class ComicSourceParser {
|
||||
final String type = _getValue("explore[$i].type");
|
||||
Future<Res<List<ExplorePagePart>>> Function()? loadMultiPart;
|
||||
Future<Res<List<Comic>>> Function(int page)? loadPage;
|
||||
Future<Res<List<Object>>> 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 = <Object>[];
|
||||
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,
|
||||
"singlePageWithMultiPart" => ExplorePageType.singlePageWithMultiPart,
|
||||
"multiPartPage" => ExplorePageType.singlePageWithMultiPart,
|
||||
"multiPageComicList" => ExplorePageType.multiPageComicList,
|
||||
"mixed" => ExplorePageType.mixed,
|
||||
_ =>
|
||||
throw ComicSourceParseException("Unknown explore page type $type")
|
||||
},
|
||||
loadPage,
|
||||
loadMultiPart));
|
||||
loadMultiPart,
|
||||
loadMixed,
|
||||
));
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
@@ -279,8 +352,11 @@ class ComicSourceParser {
|
||||
final String type = c["type"];
|
||||
final List<String> tags = List.from(c["categories"]);
|
||||
final String itemType = c["itemType"];
|
||||
final List<String>? categoryParams =
|
||||
c["categoryParams"] == null ? null : List.from(c["categoryParams"]);
|
||||
List<String>? 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<String, dynamic>) 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<String> 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);
|
||||
}
|
||||
}
|
||||
|
@@ -225,6 +225,7 @@ class JsEngine with _JSEngineApi{
|
||||
mixin class _JSEngineApi{
|
||||
final Map<int, dom.Document> _documents = {};
|
||||
final Map<int, dom.Element> _elements = {};
|
||||
final Map<int, dom.Node> _nodes = {};
|
||||
CookieJarSql? _cookieJar;
|
||||
|
||||
dynamic handleHtmlCallback(Map<String, dynamic> 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 = <int>[];
|
||||
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<String> domains) async{
|
||||
|
@@ -120,6 +120,9 @@ class LocalComic with HistoryMixin implements Comic {
|
||||
|
||||
@override
|
||||
String? get subTitle => subtitle;
|
||||
|
||||
@override
|
||||
String? get language => null;
|
||||
}
|
||||
|
||||
class LocalManager with ChangeNotifier {
|
||||
|
@@ -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<String> args) {
|
||||
runZonedGuarded(() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await init();
|
||||
if(App.isAndroid) {
|
||||
handleLinks();
|
||||
}
|
||||
FlutterError.onError = (details) {
|
||||
Log.error(
|
||||
"Unhandled Exception", "${details.exception}\n${details.stack}");
|
||||
|
@@ -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");
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
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),
|
||||
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: 32),
|
||||
if (widget.registerWebsite != null)
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -134,6 +134,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
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<ComicPage, ComicDetails>
|
||||
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;
|
||||
|
@@ -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"),
|
||||
|
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@@ -87,6 +87,7 @@ class _HistoryPageState extends State<HistoryPage> {
|
||||
getDescription(e),
|
||||
e.type.comicSource?.key ?? "Invalid",
|
||||
null,
|
||||
null,
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -27,7 +27,15 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
|
||||
late List<String> 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<SearchResultPage> {
|
||||
);
|
||||
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<SearchResultPage> {
|
||||
loadPage: (i) {
|
||||
var source = ComicSource.find(sourceKey);
|
||||
return source!.searchPageData!.loadPage!(
|
||||
controller.initialText,
|
||||
text,
|
||||
i,
|
||||
options,
|
||||
);
|
||||
|
@@ -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<Map<String, String>?> getCookies(String url) async{
|
||||
extension WebviewExtension on InAppWebViewController {
|
||||
Future<List<io.Cookie>?> 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<String, String> res = {};
|
||||
for(var cookie in cookies){
|
||||
res[cookie.name] = cookie.value;
|
||||
var res = <io.Cookie>[];
|
||||
for (var cookie in cookies) {
|
||||
var c = io.Cookie(cookie.name, cookie.value);
|
||||
c.domain = cookie.domain;
|
||||
res.add(c);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<String?> getUA() async{
|
||||
Future<String?> 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,20 +88,24 @@ class _AppWebviewState extends State<AppWebview> {
|
||||
message: "More",
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
onPressed: (){
|
||||
showMenu(context: context, position: RelativeRect.fromLTRB(
|
||||
onPressed: () {
|
||||
showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(
|
||||
MediaQuery.of(context).size.width,
|
||||
0,
|
||||
MediaQuery.of(context).size.width,
|
||||
0
|
||||
), items: [
|
||||
0),
|
||||
items: [
|
||||
PopupMenuItem(
|
||||
child: Text("Open in browser".tl),
|
||||
onTap: () async => launchUrlString((await controller?.getUrl())!.path),
|
||||
onTap: () async =>
|
||||
launchUrlString((await controller?.getUrl())!.path),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text("Copy link".tl),
|
||||
onTap: () async => Clipboard.setData(ClipboardData(text: (await controller?.getUrl())!.path)),
|
||||
onTap: () async => Clipboard.setData(ClipboardData(
|
||||
text: (await controller?.getUrl())!.path)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text("Reload".tl),
|
||||
@@ -100,9 +118,12 @@ class _AppWebviewState extends State<AppWebview> {
|
||||
];
|
||||
|
||||
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<AppWebview> {
|
||||
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<AppWebview> {
|
||||
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,),
|
||||
title: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
actions: actions,
|
||||
),
|
||||
body: body
|
||||
);
|
||||
body: body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,13 +191,12 @@ class DesktopWebview {
|
||||
|
||||
final void Function()? onClose;
|
||||
|
||||
DesktopWebview({
|
||||
required this.initialUrl,
|
||||
DesktopWebview(
|
||||
{required this.initialUrl,
|
||||
this.onTitleChange,
|
||||
this.onNavigation,
|
||||
this.onStarted,
|
||||
this.onClose
|
||||
});
|
||||
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<Map<String, String>> getCookies(String url) async{
|
||||
Future<Map<String, String>> getCookies(String url) async {
|
||||
var allCookies = await _webview!.getAllCookies();
|
||||
var res = <String, String>{};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
30
lib/utils/app_links.dart
Normal file
30
lib/utils/app_links.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <desktop_webview_window/desktop_webview_window_plugin.h>
|
||||
#include <flutter_qjs/flutter_qjs_plugin.h>
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
@@ -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);
|
||||
|
@@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_webview_window
|
||||
flutter_qjs
|
||||
gtk
|
||||
screen_retriever
|
||||
sqlite3_flutter_libs
|
||||
url_launcher_linux
|
||||
|
@@ -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"))
|
||||
|
40
pubspec.lock
40
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:
|
||||
|
@@ -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:
|
||||
|
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <desktop_webview_window/desktop_webview_window_plugin.h>
|
||||
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
||||
#include <flutter_qjs/flutter_qjs_plugin.h>
|
||||
@@ -16,6 +17,8 @@
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AppLinksPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||
DesktopWebviewWindowPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin"));
|
||||
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
|
||||
|
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
desktop_webview_window
|
||||
flutter_inappwebview_windows
|
||||
flutter_qjs
|
||||
|
Reference in New Issue
Block a user