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"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</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>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
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
|
Venera JavaScript Library
|
||||||
|
|
||||||
|
This library provides a set of APIs for interacting with the Venera app.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/// encode, decode, hash, decrypt
|
/// encode, decode, hash, decrypt
|
||||||
@@ -305,7 +307,7 @@ let Network = {
|
|||||||
* @param {string} url - The URL to send the request to.
|
* @param {string} url - The URL to send the request to.
|
||||||
* @param {Object} headers - The headers to include in the request.
|
* @param {Object} headers - The headers to include in the request.
|
||||||
* @param data - The data to send with 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) {
|
async fetchBytes(method, url, headers, data) {
|
||||||
let result = await sendMessage({
|
let result = await sendMessage({
|
||||||
@@ -330,7 +332,7 @@ let Network = {
|
|||||||
* @param {string} url - The URL to send the request to.
|
* @param {string} url - The URL to send the request to.
|
||||||
* @param {Object} headers - The headers to include in the request.
|
* @param {Object} headers - The headers to include in the request.
|
||||||
* @param data - The data to send with 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) {
|
async sendRequest(method, url, headers, data) {
|
||||||
let result = await sendMessage({
|
let result = await sendMessage({
|
||||||
@@ -352,7 +354,7 @@ let Network = {
|
|||||||
* Sends an HTTP GET request.
|
* Sends an HTTP GET request.
|
||||||
* @param {string} url - The URL to send the request to.
|
* @param {string} url - The URL to send the request to.
|
||||||
* @param {Object} headers - The headers to include in the request.
|
* @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) {
|
async get(url, headers) {
|
||||||
return this.sendRequest('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 {string} url - The URL to send the request to.
|
||||||
* @param {Object} headers - The headers to include in the request.
|
* @param {Object} headers - The headers to include in the request.
|
||||||
* @param data - The data to send with 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) {
|
async post(url, headers, data) {
|
||||||
return this.sendRequest('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 {string} url - The URL to send the request to.
|
||||||
* @param {Object} headers - The headers to include in the request.
|
* @param {Object} headers - The headers to include in the request.
|
||||||
* @param data - The data to send with 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) {
|
async put(url, headers, data) {
|
||||||
return this.sendRequest('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 {string} url - The URL to send the request to.
|
||||||
* @param {Object} headers - The headers to include in the request.
|
* @param {Object} headers - The headers to include in the request.
|
||||||
* @param data - The data to send with 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) {
|
async patch(url, headers, data) {
|
||||||
return this.sendRequest('PATCH', url, headers, data);
|
return this.sendRequest('PATCH', url, headers, data);
|
||||||
@@ -395,7 +397,7 @@ let Network = {
|
|||||||
* Sends an HTTP DELETE request.
|
* Sends an HTTP DELETE request.
|
||||||
* @param {string} url - The URL to send the request to.
|
* @param {string} url - The URL to send the request to.
|
||||||
* @param {Object} headers - The headers to include in the request.
|
* @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) {
|
async delete(url, headers) {
|
||||||
return this.sendRequest('DELETE', url, headers);
|
return this.sendRequest('DELETE', url, headers);
|
||||||
@@ -577,6 +579,91 @@ class HtmlElement {
|
|||||||
})
|
})
|
||||||
return ks.map(k => new HtmlElement(k));
|
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) {
|
function log(level, title, content) {
|
||||||
@@ -608,10 +695,11 @@ let console = {
|
|||||||
* @param cover {string}
|
* @param cover {string}
|
||||||
* @param tags {string[]}
|
* @param tags {string[]}
|
||||||
* @param description {string}
|
* @param description {string}
|
||||||
* @param maxPage {number | null}
|
* @param maxPage {number?}
|
||||||
|
* @param language {string?}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function Comic({id, title, subtitle, cover, tags, description, maxPage}) {
|
function Comic({id, title, subtitle, cover, tags, description, maxPage, language}) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.subtitle = subtitle;
|
this.subtitle = subtitle;
|
||||||
@@ -619,26 +707,27 @@ function Comic({id, title, subtitle, cover, tags, description, maxPage}) {
|
|||||||
this.tags = tags;
|
this.tags = tags;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.maxPage = maxPage;
|
this.maxPage = maxPage;
|
||||||
|
this.language = language;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a comic details object
|
* Create a comic details object
|
||||||
* @param title {string}
|
* @param title {string}
|
||||||
* @param cover {string}
|
* @param cover {string}
|
||||||
* @param description {string | null}
|
* @param description {string?}
|
||||||
* @param tags {Map<string, string[]> | {} | null}
|
* @param tags {Map<string, string[]> | {} | null | undefined}
|
||||||
* @param chapters {Map<string, string> | {} | null} - key: chapter id, value: chapter title
|
* @param chapters {Map<string, string> | {} | null | undefined}} - 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 isFavorite {boolean | null | undefined}} - 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 subId {string?} - 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 thumbnails {string[]? - for multiple page thumbnails, set this to null, and use `loadThumbnails` api to load thumbnails
|
||||||
* @param recommend {Comic[] | null} - related comics
|
* @param recommend {Comic[]?} - related comics
|
||||||
* @param commentCount {number | null}
|
* @param commentCount {number?}
|
||||||
* @param likesCount {number | null}
|
* @param likesCount {number?}
|
||||||
* @param isLiked {boolean | null}
|
* @param isLiked {boolean?}
|
||||||
* @param uploader {string | null}
|
* @param uploader {string?}
|
||||||
* @param updateTime {string | null}
|
* @param updateTime {string?}
|
||||||
* @param uploadTime {string | null}
|
* @param uploadTime {string?}
|
||||||
* @param url {string | null}
|
* @param url {string?}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function ComicDetails({title, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url}) {
|
function ComicDetails({title, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url}) {
|
||||||
|
@@ -3,9 +3,9 @@ part of 'components.dart';
|
|||||||
class SliverGridViewWithFixedItemHeight extends StatelessWidget {
|
class SliverGridViewWithFixedItemHeight extends StatelessWidget {
|
||||||
const SliverGridViewWithFixedItemHeight(
|
const SliverGridViewWithFixedItemHeight(
|
||||||
{required this.delegate,
|
{required this.delegate,
|
||||||
required this.maxCrossAxisExtent,
|
required this.maxCrossAxisExtent,
|
||||||
required this.itemHeight,
|
required this.itemHeight,
|
||||||
super.key});
|
super.key});
|
||||||
|
|
||||||
final SliverChildDelegate delegate;
|
final SliverChildDelegate delegate;
|
||||||
|
|
||||||
@@ -16,13 +16,14 @@ class SliverGridViewWithFixedItemHeight extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SliverLayoutBuilder(
|
return SliverLayoutBuilder(
|
||||||
builder: ((context, constraints) => SliverGrid(
|
builder: (context, constraints) => SliverGrid(
|
||||||
delegate: delegate,
|
delegate: delegate,
|
||||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: maxCrossAxisExtent,
|
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||||
childAspectRatio:
|
childAspectRatio: calcChildAspectRatio(constraints.crossAxisExtent),
|
||||||
calcChildAspectRatio(constraints.crossAxisExtent)),
|
),
|
||||||
)));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double calcChildAspectRatio(double width) {
|
double calcChildAspectRatio(double width) {
|
||||||
@@ -35,7 +36,7 @@ class SliverGridViewWithFixedItemHeight extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SliverGridDelegateWithFixedHeight extends SliverGridDelegate{
|
class SliverGridDelegateWithFixedHeight extends SliverGridDelegate {
|
||||||
const SliverGridDelegateWithFixedHeight({
|
const SliverGridDelegateWithFixedHeight({
|
||||||
required this.maxCrossAxisExtent,
|
required this.maxCrossAxisExtent,
|
||||||
required this.itemHeight,
|
required this.itemHeight,
|
||||||
@@ -58,23 +59,21 @@ class SliverGridDelegateWithFixedHeight extends SliverGridDelegate{
|
|||||||
crossAxisStride: width / crossItems,
|
crossAxisStride: width / crossItems,
|
||||||
childMainAxisExtent: itemHeight,
|
childMainAxisExtent: itemHeight,
|
||||||
childCrossAxisExtent: width / crossItems,
|
childCrossAxisExtent: width / crossItems,
|
||||||
reverseCrossAxis: false
|
reverseCrossAxis: false);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {
|
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {
|
||||||
if(oldDelegate is! SliverGridDelegateWithFixedHeight) return true;
|
if (oldDelegate is! SliverGridDelegateWithFixedHeight) return true;
|
||||||
if(oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent
|
if (oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent ||
|
||||||
|| oldDelegate.itemHeight != itemHeight){
|
oldDelegate.itemHeight != itemHeight) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SliverGridDelegateWithComics extends SliverGridDelegate{
|
class SliverGridDelegateWithComics extends SliverGridDelegate {
|
||||||
SliverGridDelegateWithComics([this.useBriefMode = false, this.scale]);
|
SliverGridDelegateWithComics([this.useBriefMode = false, this.scale]);
|
||||||
|
|
||||||
final bool useBriefMode;
|
final bool useBriefMode;
|
||||||
@@ -83,14 +82,17 @@ class SliverGridDelegateWithComics extends SliverGridDelegate{
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
SliverGridLayout getLayout(SliverConstraints constraints) {
|
SliverGridLayout getLayout(SliverConstraints constraints) {
|
||||||
if(appdata.settings['comicDisplayMode'] == 'brief' || useBriefMode){
|
if (appdata.settings['comicDisplayMode'] == 'brief' || useBriefMode) {
|
||||||
return getBriefModeLayout(constraints, scale ?? appdata.settings['comicTileScale']);
|
return getBriefModeLayout(
|
||||||
|
constraints, scale ?? appdata.settings['comicTileScale']);
|
||||||
} else {
|
} 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;
|
const minCrossAxisExtent = 360;
|
||||||
final itemHeight = 152 * scale;
|
final itemHeight = 152 * scale;
|
||||||
final width = constraints.crossAxisExtent;
|
final width = constraints.crossAxisExtent;
|
||||||
@@ -102,15 +104,17 @@ class SliverGridDelegateWithComics extends SliverGridDelegate{
|
|||||||
crossAxisStride: width / crossItems,
|
crossAxisStride: width / crossItems,
|
||||||
childMainAxisExtent: itemHeight,
|
childMainAxisExtent: itemHeight,
|
||||||
childCrossAxisExtent: width / crossItems,
|
childCrossAxisExtent: width / crossItems,
|
||||||
reverseCrossAxis: false
|
reverseCrossAxis: false);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SliverGridLayout getBriefModeLayout(SliverConstraints constraints, double scale){
|
SliverGridLayout getBriefModeLayout(
|
||||||
|
SliverConstraints constraints, double scale) {
|
||||||
final maxCrossAxisExtent = 192.0 * scale;
|
final maxCrossAxisExtent = 192.0 * scale;
|
||||||
const childAspectRatio = 0.72;
|
const childAspectRatio = 0.72;
|
||||||
const crossAxisSpacing = 0.0;
|
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
|
// Ensure a minimum count of 1, can be zero and result in an infinite extent
|
||||||
// below when the window size is 0.
|
// below when the window size is 0.
|
||||||
crossAxisCount = math.max(1, crossAxisCount);
|
crossAxisCount = math.max(1, crossAxisCount);
|
||||||
@@ -134,4 +138,4 @@ class SliverGridDelegateWithComics extends SliverGridDelegate{
|
|||||||
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {
|
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -199,7 +199,7 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
|
|
||||||
int _page = 1;
|
int _page = 1;
|
||||||
|
|
||||||
int _maxPage = 1;
|
int? _maxPage;
|
||||||
|
|
||||||
Future<Res<List<S>>> loadData(int page);
|
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 isFirstLoading => _isFirstLoading;
|
||||||
|
|
||||||
bool get haveNextPage => _page <= _maxPage;
|
bool get haveNextPage => _maxPage == null || _page <= _maxPage!;
|
||||||
|
|
||||||
void nextPage() {
|
void nextPage() {
|
||||||
if (_page > _maxPage) return;
|
if (_maxPage != null && _page > _maxPage!) return;
|
||||||
if (_isLoading) return;
|
if (_isLoading) return;
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
loadData(_page).then((value) {
|
loadData(_page).then((value) {
|
||||||
|
@@ -88,7 +88,8 @@ class RandomCategoryPart extends BaseCategoryPart {
|
|||||||
if (randomNumber >= tags.length) {
|
if (randomNumber >= tags.length) {
|
||||||
return tags;
|
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
|
@override
|
||||||
|
@@ -197,6 +197,8 @@ class ComicSource {
|
|||||||
|
|
||||||
final HandleClickTagEvent? handleClickTagEvent;
|
final HandleClickTagEvent? handleClickTagEvent;
|
||||||
|
|
||||||
|
final LinkHandler? linkHandler;
|
||||||
|
|
||||||
Future<void> loadData() async {
|
Future<void> loadData() async {
|
||||||
var file = File("${App.dataPath}/comic_source/$key.data");
|
var file = File("${App.dataPath}/comic_source/$key.data");
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
@@ -261,14 +263,13 @@ class ComicSource {
|
|||||||
this.idMatcher,
|
this.idMatcher,
|
||||||
this.translations,
|
this.translations,
|
||||||
this.handleClickTagEvent,
|
this.handleClickTagEvent,
|
||||||
|
this.linkHandler,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountConfig {
|
class AccountConfig {
|
||||||
final LoginFunction? login;
|
final LoginFunction? login;
|
||||||
|
|
||||||
final FutureOr<void> Function(BuildContext)? onLogin;
|
|
||||||
|
|
||||||
final String? loginWebsite;
|
final String? loginWebsite;
|
||||||
|
|
||||||
final String? registerWebsite;
|
final String? registerWebsite;
|
||||||
@@ -279,10 +280,15 @@ class AccountConfig {
|
|||||||
|
|
||||||
final List<AccountInfoItem> infoItems;
|
final List<AccountInfoItem> infoItems;
|
||||||
|
|
||||||
|
final bool Function(String url, String title)? checkLoginStatus;
|
||||||
|
|
||||||
const AccountConfig(
|
const AccountConfig(
|
||||||
this.login, this.loginWebsite, this.registerWebsite, this.logout,
|
this.login,
|
||||||
{this.onLogin})
|
this.loginWebsite,
|
||||||
: allowReLogin = true,
|
this.registerWebsite,
|
||||||
|
this.logout,
|
||||||
|
this.checkLoginStatus,
|
||||||
|
) : allowReLogin = true,
|
||||||
infoItems = const [];
|
infoItems = const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,11 +321,13 @@ class ExplorePageData {
|
|||||||
/// return a `List` contains `List<Comic>` or `ExplorePagePart`
|
/// return a `List` contains `List<Comic>` or `ExplorePagePart`
|
||||||
final Future<Res<List<Object>>> Function(int index)? loadMixed;
|
final Future<Res<List<Object>>> Function(int index)? loadMixed;
|
||||||
|
|
||||||
final WidgetBuilder? overridePageBuilder;
|
ExplorePageData(
|
||||||
|
this.title,
|
||||||
ExplorePageData(this.title, this.type, this.loadPage, this.loadMultiPart)
|
this.type,
|
||||||
: loadMixed = null,
|
this.loadPage,
|
||||||
overridePageBuilder = null;
|
this.loadMultiPart,
|
||||||
|
this.loadMixed,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExplorePagePart {
|
class ExplorePagePart {
|
||||||
@@ -422,3 +430,12 @@ class CategoryComicsOptions {
|
|||||||
|
|
||||||
const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen);
|
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 bool? isLiked;
|
||||||
final int? voteStatus; // 1: upvote, -1: downvote, 0: none
|
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)
|
Comment.fromJson(Map<String, dynamic> json)
|
||||||
: userName = json["userName"],
|
: userName = json["userName"],
|
||||||
avatar = json["avatar"],
|
avatar = json["avatar"],
|
||||||
content = json["content"],
|
content = json["content"],
|
||||||
time = json["time"],
|
time = parseTime(json["time"]),
|
||||||
replyCount = json["replyCount"],
|
replyCount = json["replyCount"],
|
||||||
id = json["id"].toString(),
|
id = json["id"].toString(),
|
||||||
score = json["score"],
|
score = json["score"],
|
||||||
@@ -40,8 +52,19 @@ class Comic {
|
|||||||
|
|
||||||
final int? maxPage;
|
final int? maxPage;
|
||||||
|
|
||||||
const Comic(this.title, this.cover, this.id, this.subtitle, this.tags,
|
final String? language;
|
||||||
this.description, this.sourceKey, this.maxPage);
|
|
||||||
|
const Comic(
|
||||||
|
this.title,
|
||||||
|
this.cover,
|
||||||
|
this.id,
|
||||||
|
this.subtitle,
|
||||||
|
this.tags,
|
||||||
|
this.description,
|
||||||
|
this.sourceKey,
|
||||||
|
this.maxPage,
|
||||||
|
this.language,
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
@@ -53,6 +76,7 @@ class Comic {
|
|||||||
"description": description,
|
"description": description,
|
||||||
"sourceKey": sourceKey,
|
"sourceKey": sourceKey,
|
||||||
"maxPage": maxPage,
|
"maxPage": maxPage,
|
||||||
|
"language": language,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +87,8 @@ class Comic {
|
|||||||
id = json["id"],
|
id = json["id"],
|
||||||
tags = List<String>.from(json["tags"] ?? []),
|
tags = List<String>.from(json["tags"] ?? []),
|
||||||
description = json["description"] ?? "",
|
description = json["description"] ?? "",
|
||||||
maxPage = json["maxPage"];
|
maxPage = json["maxPage"],
|
||||||
|
language = json["language"];
|
||||||
}
|
}
|
||||||
|
|
||||||
class ComicDetails with HistoryMixin {
|
class ComicDetails with HistoryMixin {
|
||||||
@@ -109,7 +134,7 @@ class ComicDetails with HistoryMixin {
|
|||||||
|
|
||||||
final String? url;
|
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>>{};
|
var res = <String, List<String>>{};
|
||||||
map.forEach((key, value) {
|
map.forEach((key, value) {
|
||||||
res[key] = List<String>.from(value);
|
res[key] = List<String>.from(value);
|
||||||
|
@@ -147,6 +147,7 @@ class ComicSourceParser {
|
|||||||
_parseIdMatch(),
|
_parseIdMatch(),
|
||||||
_parseTranslation(),
|
_parseTranslation(),
|
||||||
_parseClickTagEvent(),
|
_parseClickTagEvent(),
|
||||||
|
_parseLinkHandler(),
|
||||||
);
|
);
|
||||||
|
|
||||||
await source.loadData();
|
await source.loadData();
|
||||||
@@ -199,8 +200,28 @@ class ComicSourceParser {
|
|||||||
JsEngine().runCode("ComicSource.sources.$_key.account.logout()");
|
JsEngine().runCode("ComicSource.sources.$_key.account.logout()");
|
||||||
}
|
}
|
||||||
|
|
||||||
return AccountConfig(login, _getValue("account.login.website"),
|
if(!_checkExists('account.loginWithWebview')) {
|
||||||
_getValue("account.registerWebsite"), logout);
|
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() {
|
List<ExplorePageData> _loadExploreData() {
|
||||||
@@ -214,6 +235,7 @@ class ComicSourceParser {
|
|||||||
final String type = _getValue("explore[$i].type");
|
final String type = _getValue("explore[$i].type");
|
||||||
Future<Res<List<ExplorePagePart>>> Function()? loadMultiPart;
|
Future<Res<List<ExplorePagePart>>> Function()? loadMultiPart;
|
||||||
Future<Res<List<Comic>>> Function(int page)? loadPage;
|
Future<Res<List<Comic>>> Function(int page)? loadPage;
|
||||||
|
Future<Res<List<Object>>> Function(int index)? loadMixed;
|
||||||
if (type == "singlePageWithMultiPart") {
|
if (type == "singlePageWithMultiPart") {
|
||||||
loadMultiPart = () async {
|
loadMultiPart = () async {
|
||||||
try {
|
try {
|
||||||
@@ -246,18 +268,69 @@ class ComicSourceParser {
|
|||||||
return Res.error(e.toString());
|
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(
|
pages.add(ExplorePageData(
|
||||||
title,
|
title,
|
||||||
switch (type) {
|
switch (type) {
|
||||||
"singlePageWithMultiPart" =>
|
"singlePageWithMultiPart" => ExplorePageType.singlePageWithMultiPart,
|
||||||
ExplorePageType.singlePageWithMultiPart,
|
"multiPartPage" => ExplorePageType.singlePageWithMultiPart,
|
||||||
"multiPageComicList" => ExplorePageType.multiPageComicList,
|
"multiPageComicList" => ExplorePageType.multiPageComicList,
|
||||||
_ =>
|
"mixed" => ExplorePageType.mixed,
|
||||||
throw ComicSourceParseException("Unknown explore page type $type")
|
_ =>
|
||||||
},
|
throw ComicSourceParseException("Unknown explore page type $type")
|
||||||
loadPage,
|
},
|
||||||
loadMultiPart));
|
loadPage,
|
||||||
|
loadMultiPart,
|
||||||
|
loadMixed,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
return pages;
|
return pages;
|
||||||
}
|
}
|
||||||
@@ -279,8 +352,11 @@ class ComicSourceParser {
|
|||||||
final String type = c["type"];
|
final String type = c["type"];
|
||||||
final List<String> tags = List.from(c["categories"]);
|
final List<String> tags = List.from(c["categories"]);
|
||||||
final String itemType = c["itemType"];
|
final String itemType = c["itemType"];
|
||||||
final List<String>? categoryParams =
|
List<String>? categoryParams = ListOrNull.from(c["categoryParams"]);
|
||||||
c["categoryParams"] == null ? null : List.from(c["categoryParams"]);
|
final String? groupParam = c["groupParam"];
|
||||||
|
if (groupParam != null) {
|
||||||
|
categoryParams = List.filled(tags.length, groupParam);
|
||||||
|
}
|
||||||
if (type == "fixed") {
|
if (type == "fixed") {
|
||||||
categoryParts
|
categoryParts
|
||||||
.add(FixedCategoryPart(name, tags, itemType, categoryParams));
|
.add(FixedCategoryPart(name, tags, itemType, categoryParams));
|
||||||
@@ -407,6 +483,7 @@ class ComicSourceParser {
|
|||||||
if (res is! Map<String, dynamic>) throw "Invalid data";
|
if (res is! Map<String, dynamic>) throw "Invalid data";
|
||||||
res['comicId'] = id;
|
res['comicId'] = id;
|
||||||
res['sourceKey'] = _key;
|
res['sourceKey'] = _key;
|
||||||
|
JsEngine().clearHtml();
|
||||||
return Res(ComicDetails.fromJson(res));
|
return Res(ComicDetails.fromJson(res));
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
@@ -728,4 +805,18 @@ class ComicSourceParser {
|
|||||||
return Map.from(r);
|
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{
|
mixin class _JSEngineApi{
|
||||||
final Map<int, dom.Document> _documents = {};
|
final Map<int, dom.Document> _documents = {};
|
||||||
final Map<int, dom.Element> _elements = {};
|
final Map<int, dom.Element> _elements = {};
|
||||||
|
final Map<int, dom.Node> _nodes = {};
|
||||||
CookieJarSql? _cookieJar;
|
CookieJarSql? _cookieJar;
|
||||||
|
|
||||||
dynamic handleHtmlCallback(Map<String, dynamic> data) {
|
dynamic handleHtmlCallback(Map<String, dynamic> data) {
|
||||||
@@ -270,6 +271,38 @@ mixin class _JSEngineApi{
|
|||||||
keys.add(_elements.length - 1);
|
keys.add(_elements.length - 1);
|
||||||
}
|
}
|
||||||
return keys;
|
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();
|
_documents.clear();
|
||||||
_elements.clear();
|
_elements.clear();
|
||||||
|
_nodes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearCookies(List<String> domains) async{
|
void clearCookies(List<String> domains) async{
|
||||||
|
@@ -120,6 +120,9 @@ class LocalComic with HistoryMixin implements Comic {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String? get subTitle => subtitle;
|
String? get subTitle => subtitle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get language => null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalManager with ChangeNotifier {
|
class LocalManager with ChangeNotifier {
|
||||||
|
@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/pages/main_page.dart';
|
import 'package:venera/pages/main_page.dart';
|
||||||
|
import 'package:venera/utils/app_links.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
import 'components/components.dart';
|
import 'components/components.dart';
|
||||||
import 'components/window_frame.dart';
|
import 'components/window_frame.dart';
|
||||||
@@ -19,6 +20,9 @@ void main(List<String> args) {
|
|||||||
runZonedGuarded(() async {
|
runZonedGuarded(() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await init();
|
await init();
|
||||||
|
if(App.isAndroid) {
|
||||||
|
handleLinks();
|
||||||
|
}
|
||||||
FlutterError.onError = (details) {
|
FlutterError.onError = (details) {
|
||||||
Log.error(
|
Log.error(
|
||||||
"Unhandled Exception", "${details.exception}\n${details.stack}");
|
"Unhandled Exception", "${details.exception}\n${details.stack}");
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
import 'dart:io' as io;
|
import 'dart:io' as io;
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/consts.dart';
|
import 'package:venera/foundation/consts.dart';
|
||||||
import 'package:venera/pages/webview.dart';
|
import 'package:venera/pages/webview.dart';
|
||||||
|
import 'package:venera/utils/ext.dart';
|
||||||
|
|
||||||
import 'cookie_jar.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(
|
var webview = DesktopWebview(
|
||||||
initialUrl: url,
|
initialUrl: url,
|
||||||
onTitleChange: (title, controller) async {
|
onTitleChange: (title, controller) async {
|
||||||
@@ -136,12 +140,12 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
webview.open();
|
webview.open();
|
||||||
} else if (App.isMobile) {
|
} else {
|
||||||
await App.rootContext.to(
|
await App.rootContext.to(
|
||||||
() => AppWebview(
|
() => AppWebview(
|
||||||
initialUrl: url,
|
initialUrl: url,
|
||||||
singlePage: true,
|
singlePage: true,
|
||||||
onTitleChange: (title, controller) async {
|
onLoadStop: (controller) async {
|
||||||
var res = await controller.platform.evaluateJavascript(
|
var res = await controller.platform.evaluateJavascript(
|
||||||
source:
|
source:
|
||||||
"document.head.innerHTML.includes('#challenge-success-text')");
|
"document.head.innerHTML.includes('#challenge-success-text')");
|
||||||
@@ -151,11 +155,11 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
|
|||||||
appdata.implicitData['ua'] = ua;
|
appdata.implicitData['ua'] = ua;
|
||||||
appdata.writeImplicitData();
|
appdata.writeImplicitData();
|
||||||
}
|
}
|
||||||
var cookiesMap = await controller.getCookies(url) ?? {};
|
var cookies = await controller.getCookies(url) ?? [];
|
||||||
if(cookiesMap['cf_clearance'] == null) {
|
if(cookies.firstWhereOrNull((element) => element.name == 'cf_clearance') == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
saveCookies(cookiesMap);
|
SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies);
|
||||||
App.rootPop();
|
App.rootPop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -165,13 +169,11 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
|
|||||||
appdata.implicitData['ua'] = ua;
|
appdata.implicitData['ua'] = ua;
|
||||||
appdata.writeImplicitData();
|
appdata.writeImplicitData();
|
||||||
}
|
}
|
||||||
var cookiesMap = await controller.getCookies(url) ?? {};
|
var cookies = await controller.getCookies(url) ?? [];
|
||||||
saveCookies(cookiesMap);
|
SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
onFinished();
|
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/app.dart';
|
||||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
import 'package:venera/foundation/state_controller.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';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
class AccountsPageLogic extends StateController {
|
class AccountsPageLogic extends StateController {
|
||||||
@@ -60,18 +62,13 @@ class AccountsPage extends StatelessWidget {
|
|||||||
title: Text("Log in".tl),
|
title: Text("Log in".tl),
|
||||||
trailing: const Icon(Icons.arrow_right),
|
trailing: const Icon(Icons.arrow_right),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (element.account!.onLogin != null) {
|
await context.to(
|
||||||
await element.account!.onLogin!(context);
|
() => _LoginPage(
|
||||||
}
|
config: element.account!,
|
||||||
if (element.account!.login != null && context.mounted) {
|
source: element,
|
||||||
await context.to(
|
),
|
||||||
() => _LoginPage(
|
);
|
||||||
login: element.account!.login!,
|
element.saveData();
|
||||||
registerWebsite: element.account!.registerWebsite,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
element.saveData();
|
|
||||||
}
|
|
||||||
logic.update();
|
logic.update();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -121,7 +118,7 @@ class AccountsPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
yield ListTile(
|
yield ListTile(
|
||||||
title: Text("Exit".tl),
|
title: Text("Log out".tl),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
element.data["account"] = null;
|
element.data["account"] = null;
|
||||||
element.account?.logout();
|
element.account?.logout();
|
||||||
@@ -146,11 +143,11 @@ class AccountsPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LoginPage extends StatefulWidget {
|
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
|
@override
|
||||||
State<_LoginPage> createState() => _LoginPageState();
|
State<_LoginPage> createState() => _LoginPageState();
|
||||||
@@ -181,6 +178,7 @@ class _LoginPageState extends State<_LoginPage> {
|
|||||||
labelText: "Username".tl,
|
labelText: "Username".tl,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
|
enabled: widget.config.login != null,
|
||||||
onChanged: (s) {
|
onChanged: (s) {
|
||||||
username = s;
|
username = s;
|
||||||
},
|
},
|
||||||
@@ -192,21 +190,39 @@ class _LoginPageState extends State<_LoginPage> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
|
enabled: widget.config.login != null,
|
||||||
onChanged: (s) {
|
onChanged: (s) {
|
||||||
password = s;
|
password = s;
|
||||||
},
|
},
|
||||||
onSubmitted: (s) => login(),
|
onSubmitted: (s) => login(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Button.filled(
|
if (widget.config.login == null)
|
||||||
isLoading: loading,
|
Row(
|
||||||
onPressed: login,
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Text("Continue".tl),
|
children: [
|
||||||
),
|
const Icon(Icons.error_outline),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(width: 8),
|
||||||
if (widget.registerWebsite != null)
|
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(
|
TextButton(
|
||||||
onPressed: () => launchUrlString(widget.registerWebsite!),
|
onPressed: () =>
|
||||||
|
launchUrlString(widget.config.registerWebsite!),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -235,7 +251,7 @@ class _LoginPageState extends State<_LoginPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
loading = true;
|
loading = true;
|
||||||
});
|
});
|
||||||
widget.login(username, password).then((value) {
|
widget.config.login!(username, password).then((value) {
|
||||||
if (value.error) {
|
if (value.error) {
|
||||||
context.showMessage(message: value.errorMessage!);
|
context.showMessage(message: value.errorMessage!);
|
||||||
setState(() {
|
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),
|
const SizedBox(width: 16),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.primaryContainer,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
height: 144,
|
height: 144,
|
||||||
@@ -369,11 +370,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
buildTag(text: comic.uploadTime!),
|
buildTag(text: comic.uploadTime!),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (comic.uploadTime != null)
|
if (comic.updateTime != null)
|
||||||
buildWrap(
|
buildWrap(
|
||||||
children: [
|
children: [
|
||||||
buildTag(text: 'Update Time'.tl, isTitle: true),
|
buildTag(text: 'Update Time'.tl, isTitle: true),
|
||||||
buildTag(text: comicSource.name),
|
buildTag(text: comic.updateTime!),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -1120,13 +1121,18 @@ class _FavoritePanelState extends State<_FavoritePanel> {
|
|||||||
cid: widget.cid,
|
cid: widget.cid,
|
||||||
comicSource: comicSource,
|
comicSource: comicSource,
|
||||||
isFavorite: widget.isFavorite,
|
isFavorite: widget.isFavorite,
|
||||||
|
onFavorite: widget.onFavorite,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NetworkFavorites extends StatefulWidget {
|
class _NetworkFavorites extends StatefulWidget {
|
||||||
const _NetworkFavorites(
|
const _NetworkFavorites({
|
||||||
{required this.cid, required this.comicSource, required this.isFavorite});
|
required this.cid,
|
||||||
|
required this.comicSource,
|
||||||
|
required this.isFavorite,
|
||||||
|
required this.onFavorite,
|
||||||
|
});
|
||||||
|
|
||||||
final String cid;
|
final String cid;
|
||||||
|
|
||||||
@@ -1134,6 +1140,8 @@ class _NetworkFavorites extends StatefulWidget {
|
|||||||
|
|
||||||
final bool? isFavorite;
|
final bool? isFavorite;
|
||||||
|
|
||||||
|
final void Function(bool) onFavorite;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_NetworkFavorites> createState() => _NetworkFavoritesState();
|
State<_NetworkFavorites> createState() => _NetworkFavoritesState();
|
||||||
}
|
}
|
||||||
@@ -1167,7 +1175,10 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> {
|
|||||||
var res = await widget.comicSource.favoriteData!
|
var res = await widget.comicSource.favoriteData!
|
||||||
.addOrDelFavorite!(widget.cid, '', !isFavorite);
|
.addOrDelFavorite!(widget.cid, '', !isFavorite);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
|
widget.onFavorite(!isFavorite);
|
||||||
context.pop();
|
context.pop();
|
||||||
|
App.rootContext.showMessage(
|
||||||
|
message: isFavorite ? "Removed".tl : "Added".tl);
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
@@ -187,13 +187,6 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> {
|
|||||||
comicSourceKey,
|
comicSourceKey,
|
||||||
key: ValueKey(key),
|
key: ValueKey(key),
|
||||||
);
|
);
|
||||||
} else if (data.overridePageBuilder != null) {
|
|
||||||
return Builder(
|
|
||||||
builder: (context) {
|
|
||||||
return data.overridePageBuilder!(context);
|
|
||||||
},
|
|
||||||
key: ValueKey(key),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: Text("Empty Page"),
|
child: Text("Empty Page"),
|
||||||
|
@@ -121,6 +121,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
"${e.time} | ${comicSource?.name ?? "Unknown"}",
|
"${e.time} | ${comicSource?.name ?? "Unknown"}",
|
||||||
comicSource?.key ?? "Unknown",
|
comicSource?.key ?? "Unknown",
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
menuBuilder: (c) {
|
menuBuilder: (c) {
|
||||||
@@ -202,6 +203,7 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
|
|||||||
"${e.time} | ${comicSource?.name ?? "Unknown"}",
|
"${e.time} | ${comicSource?.name ?? "Unknown"}",
|
||||||
comicSource?.key ?? "Unknown",
|
comicSource?.key ?? "Unknown",
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@@ -87,6 +87,7 @@ class _HistoryPageState extends State<HistoryPage> {
|
|||||||
getDescription(e),
|
getDescription(e),
|
||||||
e.type.comicSource?.key ?? "Invalid",
|
e.type.comicSource?.key ?? "Invalid",
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).toList(),
|
).toList(),
|
||||||
|
@@ -94,7 +94,12 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
const BackButton(),
|
const BackButton(),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
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),
|
const SizedBox(width: 8),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
@@ -356,7 +361,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
context,
|
context,
|
||||||
ReaderSettings(
|
ReaderSettings(
|
||||||
onChanged: (key) {
|
onChanged: (key) {
|
||||||
if(key == "readerMode") {
|
if (key == "readerMode") {
|
||||||
context.reader.mode = ReaderMode.fromKey(appdata.settings[key]);
|
context.reader.mode = ReaderMode.fromKey(appdata.settings[key]);
|
||||||
App.rootContext.pop();
|
App.rootContext.pop();
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,15 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
|||||||
|
|
||||||
late List<String> options;
|
late List<String> options;
|
||||||
|
|
||||||
void search([String? text]) {}
|
late String text;
|
||||||
|
|
||||||
|
void search([String? text]) {
|
||||||
|
if (text != null) {
|
||||||
|
setState(() {
|
||||||
|
this.text = text;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -37,12 +45,14 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
|||||||
);
|
);
|
||||||
sourceKey = widget.sourceKey;
|
sourceKey = widget.sourceKey;
|
||||||
options = widget.options;
|
options = widget.options;
|
||||||
|
text = widget.text;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ComicList(
|
return ComicList(
|
||||||
|
key: Key(text + options.toString()),
|
||||||
errorLeading: AppSearchBar(
|
errorLeading: AppSearchBar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
),
|
),
|
||||||
@@ -52,7 +62,7 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
|||||||
loadPage: (i) {
|
loadPage: (i) {
|
||||||
var source = ComicSource.find(sourceKey);
|
var source = ComicSource.find(sourceKey);
|
||||||
return source!.searchPageData!.loadPage!(
|
return source!.searchPageData!.loadPage!(
|
||||||
controller.initialText,
|
text,
|
||||||
i,
|
i,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
@@ -11,11 +11,13 @@ import 'package:venera/foundation/app.dart';
|
|||||||
import 'package:venera/network/app_dio.dart';
|
import 'package:venera/network/app_dio.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/translations.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{
|
extension WebviewExtension on InAppWebViewController {
|
||||||
Future<Map<String, String>?> getCookies(String url) async{
|
Future<List<io.Cookie>?> getCookies(String url) async {
|
||||||
if(url.contains("https://")){
|
if(url.contains("https://")){
|
||||||
url.replaceAll("https://", "");
|
url.replaceAll("https://", "");
|
||||||
}
|
}
|
||||||
@@ -24,18 +26,20 @@ extension WebviewExtension on InAppWebViewController{
|
|||||||
}
|
}
|
||||||
CookieManager cookieManager = CookieManager.instance();
|
CookieManager cookieManager = CookieManager.instance();
|
||||||
final cookies = await cookieManager.getCookies(url: WebUri(url));
|
final cookies = await cookieManager.getCookies(url: WebUri(url));
|
||||||
Map<String, String> res = {};
|
var res = <io.Cookie>[];
|
||||||
for(var cookie in cookies){
|
for (var cookie in cookies) {
|
||||||
res[cookie.name] = cookie.value;
|
var c = io.Cookie(cookie.name, cookie.value);
|
||||||
|
c.domain = cookie.domain;
|
||||||
|
res.add(c);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getUA() async{
|
Future<String?> getUA() async {
|
||||||
var res = await evaluateJavascript(source: "navigator.userAgent");
|
var res = await evaluateJavascript(source: "navigator.userAgent");
|
||||||
if(res is String){
|
if (res is String) {
|
||||||
if(res[0] == "'" || res[0] == "\"") {
|
if (res[0] == "'" || res[0] == "\"") {
|
||||||
res = res.substring(1, res.length-1);
|
res = res.substring(1, res.length - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res is String ? res : null;
|
return res is String ? res : null;
|
||||||
@@ -43,17 +47,27 @@ extension WebviewExtension on InAppWebViewController{
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AppWebview extends StatefulWidget {
|
class AppWebview extends StatefulWidget {
|
||||||
const AppWebview({required this.initialUrl, this.onTitleChange,
|
const AppWebview(
|
||||||
this.onNavigation, this.singlePage = false, this.onStarted, super.key});
|
{required this.initialUrl,
|
||||||
|
this.onTitleChange,
|
||||||
|
this.onNavigation,
|
||||||
|
this.singlePage = false,
|
||||||
|
this.onStarted,
|
||||||
|
this.onLoadStop,
|
||||||
|
super.key});
|
||||||
|
|
||||||
final String initialUrl;
|
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)? onStarted;
|
||||||
|
|
||||||
|
final void Function(InAppWebViewController controller)? onLoadStop;
|
||||||
|
|
||||||
final bool singlePage;
|
final bool singlePage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -74,35 +88,42 @@ class _AppWebviewState extends State<AppWebview> {
|
|||||||
message: "More",
|
message: "More",
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.more_horiz),
|
icon: const Icon(Icons.more_horiz),
|
||||||
onPressed: (){
|
onPressed: () {
|
||||||
showMenu(context: context, position: RelativeRect.fromLTRB(
|
showMenu(
|
||||||
MediaQuery.of(context).size.width,
|
context: context,
|
||||||
0,
|
position: RelativeRect.fromLTRB(
|
||||||
MediaQuery.of(context).size.width,
|
MediaQuery.of(context).size.width,
|
||||||
0
|
0,
|
||||||
), items: [
|
MediaQuery.of(context).size.width,
|
||||||
PopupMenuItem(
|
0),
|
||||||
child: Text("Open in browser".tl),
|
items: [
|
||||||
onTap: () async => launchUrlString((await controller?.getUrl())!.path),
|
PopupMenuItem(
|
||||||
),
|
child: Text("Open in browser".tl),
|
||||||
PopupMenuItem(
|
onTap: () async =>
|
||||||
child: Text("Copy link".tl),
|
launchUrlString((await controller?.getUrl())!.path),
|
||||||
onTap: () async => Clipboard.setData(ClipboardData(text: (await controller?.getUrl())!.path)),
|
),
|
||||||
),
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
child: Text("Copy link".tl),
|
||||||
child: Text("Reload".tl),
|
onTap: () async => Clipboard.setData(ClipboardData(
|
||||||
onTap: () => controller?.reload(),
|
text: (await controller?.getUrl())!.path)),
|
||||||
),
|
),
|
||||||
]);
|
PopupMenuItem(
|
||||||
|
child: Text("Reload".tl),
|
||||||
|
onTap: () => controller?.reload(),
|
||||||
|
),
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget body = InAppWebView(
|
Widget body = InAppWebView(
|
||||||
|
initialSettings: InAppWebViewSettings(
|
||||||
|
isInspectable: true,
|
||||||
|
),
|
||||||
initialUrlRequest: URLRequest(url: WebUri(widget.initialUrl)),
|
initialUrlRequest: URLRequest(url: WebUri(widget.initialUrl)),
|
||||||
onTitleChanged: (c, t){
|
onTitleChanged: (c, t) {
|
||||||
if(mounted){
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
title = t ?? "Webview";
|
title = t ?? "Webview";
|
||||||
});
|
});
|
||||||
@@ -110,19 +131,24 @@ class _AppWebviewState extends State<AppWebview> {
|
|||||||
widget.onTitleChange?.call(title, controller!);
|
widget.onTitleChange?.call(title, controller!);
|
||||||
},
|
},
|
||||||
shouldOverrideUrlLoading: (c, r) async {
|
shouldOverrideUrlLoading: (c, r) async {
|
||||||
var res = widget.onNavigation?.call(r.request.url?.toString() ?? "") ?? false;
|
var res =
|
||||||
if(res) {
|
widget.onNavigation?.call(r.request.url?.toString() ?? "", c) ??
|
||||||
|
false;
|
||||||
|
if (res) {
|
||||||
return NavigationActionPolicy.CANCEL;
|
return NavigationActionPolicy.CANCEL;
|
||||||
} else {
|
} else {
|
||||||
return NavigationActionPolicy.ALLOW;
|
return NavigationActionPolicy.ALLOW;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onWebViewCreated: (c){
|
onWebViewCreated: (c) {
|
||||||
controller = c;
|
controller = c;
|
||||||
widget.onStarted?.call(c);
|
widget.onStarted?.call(c);
|
||||||
},
|
},
|
||||||
onProgressChanged: (c, p){
|
onLoadStop: (c, r) {
|
||||||
if(mounted){
|
widget.onLoadStop?.call(c);
|
||||||
|
},
|
||||||
|
onProgressChanged: (c, p) {
|
||||||
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_progress = p / 100;
|
_progress = p / 100;
|
||||||
});
|
});
|
||||||
@@ -133,19 +159,22 @@ class _AppWebviewState extends State<AppWebview> {
|
|||||||
body = Stack(
|
body = Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(child: body),
|
Positioned.fill(child: body),
|
||||||
if(_progress < 1.0)
|
if (_progress < 1.0)
|
||||||
const Positioned.fill(child: Center(
|
const Positioned.fill(
|
||||||
child: CircularProgressIndicator()))
|
child: Center(child: CircularProgressIndicator()))
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: Appbar(
|
appBar: Appbar(
|
||||||
title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis,),
|
title: Text(
|
||||||
actions: actions,
|
title,
|
||||||
),
|
maxLines: 1,
|
||||||
body: body
|
overflow: TextOverflow.ellipsis,
|
||||||
);
|
),
|
||||||
|
actions: actions,
|
||||||
|
),
|
||||||
|
body: body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,13 +191,12 @@ class DesktopWebview {
|
|||||||
|
|
||||||
final void Function()? onClose;
|
final void Function()? onClose;
|
||||||
|
|
||||||
DesktopWebview({
|
DesktopWebview(
|
||||||
required this.initialUrl,
|
{required this.initialUrl,
|
||||||
this.onTitleChange,
|
this.onTitleChange,
|
||||||
this.onNavigation,
|
this.onNavigation,
|
||||||
this.onStarted,
|
this.onStarted,
|
||||||
this.onClose
|
this.onClose});
|
||||||
});
|
|
||||||
|
|
||||||
Webview? _webview;
|
Webview? _webview;
|
||||||
|
|
||||||
@@ -178,8 +206,8 @@ class DesktopWebview {
|
|||||||
|
|
||||||
void onMessage(String message) {
|
void onMessage(String message) {
|
||||||
var json = jsonDecode(message);
|
var json = jsonDecode(message);
|
||||||
if(json is Map){
|
if (json is Map) {
|
||||||
if(json["id"] == "document_created"){
|
if (json["id"] == "document_created") {
|
||||||
title = json["data"]["title"];
|
title = json["data"]["title"];
|
||||||
_ua = json["data"]["ua"];
|
_ua = json["data"]["ua"];
|
||||||
onTitleChange?.call(title!, this);
|
onTitleChange?.call(title!, this);
|
||||||
@@ -210,14 +238,15 @@ class DesktopWebview {
|
|||||||
}
|
}
|
||||||
collect();
|
collect();
|
||||||
''';
|
''';
|
||||||
if(_webview != null) {
|
if (_webview != null) {
|
||||||
onMessage(await evaluateJavascript(js) ?? '');
|
onMessage(await evaluateJavascript(js) ?? '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void open() async {
|
void open() async {
|
||||||
_webview = await WebviewWindow.create(configuration: CreateConfiguration(
|
_webview = await WebviewWindow.create(
|
||||||
|
configuration: CreateConfiguration(
|
||||||
useWindowPositionAndSize: true,
|
useWindowPositionAndSize: true,
|
||||||
userDataFolderWindows: "${App.dataPath}\\webview",
|
userDataFolderWindows: "${App.dataPath}\\webview",
|
||||||
title: "webview",
|
title: "webview",
|
||||||
@@ -242,11 +271,11 @@ class DesktopWebview {
|
|||||||
return _webview!.evaluateJavaScript(source);
|
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 allCookies = await _webview!.getAllCookies();
|
||||||
var res = <String, String>{};
|
var res = <String, String>{};
|
||||||
for(var c in allCookies) {
|
for (var c in allCookies) {
|
||||||
if(_cookieMatch(url, c.domain)){
|
if (_cookieMatch(url, c.domain)) {
|
||||||
res[_removeCode0(c.name)] = _removeCode0(c.value);
|
res[_removeCode0(c.name)] = _removeCode0(c.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,4 +308,4 @@ class DesktopWebview {
|
|||||||
_webview?.close();
|
_webview?.close();
|
||||||
_webview = null;
|
_webview = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 <desktop_webview_window/desktop_webview_window_plugin.h>
|
||||||
#include <flutter_qjs/flutter_qjs_plugin.h>
|
#include <flutter_qjs/flutter_qjs_plugin.h>
|
||||||
|
#include <gtk/gtk_plugin.h>
|
||||||
#include <screen_retriever/screen_retriever_plugin.h>
|
#include <screen_retriever/screen_retriever_plugin.h>
|
||||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_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 =
|
g_autoptr(FlPluginRegistrar) flutter_qjs_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterQjsPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterQjsPlugin");
|
||||||
flutter_qjs_plugin_register_with_registrar(flutter_qjs_registrar);
|
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 =
|
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
|
||||||
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
|
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
desktop_webview_window
|
desktop_webview_window
|
||||||
flutter_qjs
|
flutter_qjs
|
||||||
|
gtk
|
||||||
screen_retriever
|
screen_retriever
|
||||||
sqlite3_flutter_libs
|
sqlite3_flutter_libs
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import app_links
|
||||||
import desktop_webview_window
|
import desktop_webview_window
|
||||||
import flutter_inappwebview_macos
|
import flutter_inappwebview_macos
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
@@ -15,6 +16,7 @@ import url_launcher_macos
|
|||||||
import window_manager
|
import window_manager
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||||
DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin"))
|
DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin"))
|
||||||
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
40
pubspec.lock
40
pubspec.lock
@@ -1,6 +1,38 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
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:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -255,6 +287,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
gtk:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: gtk
|
||||||
|
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
html:
|
html:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@@ -48,6 +48,7 @@ dependencies:
|
|||||||
url: https://github.com/wgh136/flutter_desktop_webview
|
url: https://github.com/wgh136/flutter_desktop_webview
|
||||||
path: packages/desktop_webview_window
|
path: packages/desktop_webview_window
|
||||||
flutter_inappwebview: ^6.1.5
|
flutter_inappwebview: ^6.1.5
|
||||||
|
app_links: ^6.3.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
#include <desktop_webview_window/desktop_webview_window_plugin.h>
|
#include <desktop_webview_window/desktop_webview_window_plugin.h>
|
||||||
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
||||||
#include <flutter_qjs/flutter_qjs_plugin.h>
|
#include <flutter_qjs/flutter_qjs_plugin.h>
|
||||||
@@ -16,6 +17,8 @@
|
|||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
AppLinksPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||||
DesktopWebviewWindowPluginRegisterWithRegistrar(
|
DesktopWebviewWindowPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin"));
|
registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin"));
|
||||||
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
|
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
app_links
|
||||||
desktop_webview_window
|
desktop_webview_window
|
||||||
flutter_inappwebview_windows
|
flutter_inappwebview_windows
|
||||||
flutter_qjs
|
flutter_qjs
|
||||||
|
Reference in New Issue
Block a user