mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
add star rating, network cache, advanced search option, loginWithCookies, loadNext; fix some minor issues
This commit is contained in:
@@ -308,14 +308,17 @@ function setInterval(callback, delay) {
|
|||||||
return timer;
|
return timer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Cookie(name, value, domain = null) {
|
/**
|
||||||
let obj = {};
|
* Create a cookie object.
|
||||||
obj.name = name;
|
* @param name {string}
|
||||||
obj.value = value;
|
* @param value {string}
|
||||||
if (domain) {
|
* @param domain {string}
|
||||||
obj.domain = domain;
|
* @constructor
|
||||||
}
|
*/
|
||||||
return obj;
|
function Cookie({name, value, domain}) {
|
||||||
|
this.name = name;
|
||||||
|
this.value = value;
|
||||||
|
this.domain = domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -491,7 +494,7 @@ class HtmlDocument {
|
|||||||
/**
|
/**
|
||||||
* Query a single element from the HTML document.
|
* Query a single element from the HTML document.
|
||||||
* @param {string} query - The query string.
|
* @param {string} query - The query string.
|
||||||
* @returns {HtmlElement} The first matching element.
|
* @returns {HtmlElement | null} The first matching element.
|
||||||
*/
|
*/
|
||||||
querySelector(query) {
|
querySelector(query) {
|
||||||
let k = sendMessage({
|
let k = sendMessage({
|
||||||
@@ -530,6 +533,22 @@ class HtmlDocument {
|
|||||||
key: this.key
|
key: this.key
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the element by its id.
|
||||||
|
* @param id {string}
|
||||||
|
* @returns {HtmlElement|null}
|
||||||
|
*/
|
||||||
|
getElementById(id) {
|
||||||
|
let k = sendMessage({
|
||||||
|
method: "html",
|
||||||
|
function: "getElementById",
|
||||||
|
key: this.key,
|
||||||
|
id: id
|
||||||
|
})
|
||||||
|
if(k == null) return null;
|
||||||
|
return new HtmlElement(k, this.key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -703,6 +722,36 @@ class HtmlElement {
|
|||||||
doc: this.doc,
|
doc: this.doc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the previous sibling element of the element. If the element has no previous sibling, return null.
|
||||||
|
* @returns {HtmlElement|null}
|
||||||
|
*/
|
||||||
|
get previousElementSibling() {
|
||||||
|
let k = sendMessage({
|
||||||
|
method: "html",
|
||||||
|
function: "getPreviousSibling",
|
||||||
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
|
})
|
||||||
|
if(k == null) return null;
|
||||||
|
return new HtmlElement(k, this.doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next sibling element of the element. If the element has no next sibling, return null.
|
||||||
|
* @returns {HtmlElement|null}
|
||||||
|
*/
|
||||||
|
get nextElementSibling() {
|
||||||
|
let k = sendMessage({
|
||||||
|
method: "html",
|
||||||
|
function: "getNextSibling",
|
||||||
|
key: this.key,
|
||||||
|
doc: this.doc,
|
||||||
|
})
|
||||||
|
if (k == null) return null;
|
||||||
|
return new HtmlElement(k, this.doc);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HtmlNode {
|
class HtmlNode {
|
||||||
@@ -789,9 +838,10 @@ let console = {
|
|||||||
* @param maxPage {number?}
|
* @param maxPage {number?}
|
||||||
* @param language {string?}
|
* @param language {string?}
|
||||||
* @param favoriteId {string?} - Only set this field if the comic is from favorites page
|
* @param favoriteId {string?} - Only set this field if the comic is from favorites page
|
||||||
|
* @param stars {number?} - 0-5, double
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function Comic({id, title, subtitle, cover, tags, description, maxPage, language, favoriteId}) {
|
function Comic({id, title, subtitle, cover, tags, description, maxPage, language, favoriteId, stars}) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.subtitle = subtitle;
|
this.subtitle = subtitle;
|
||||||
@@ -801,6 +851,7 @@ function Comic({id, title, subtitle, cover, tags, description, maxPage, language
|
|||||||
this.maxPage = maxPage;
|
this.maxPage = maxPage;
|
||||||
this.language = language;
|
this.language = language;
|
||||||
this.favoriteId = favoriteId;
|
this.favoriteId = favoriteId;
|
||||||
|
this.stars = stars;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -821,9 +872,11 @@ function Comic({id, title, subtitle, cover, tags, description, maxPage, language
|
|||||||
* @param updateTime {string?}
|
* @param updateTime {string?}
|
||||||
* @param uploadTime {string?}
|
* @param uploadTime {string?}
|
||||||
* @param url {string?}
|
* @param url {string?}
|
||||||
|
* @param stars {number?} - 0-5, double
|
||||||
|
* @param maxPage {number?}
|
||||||
* @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, stars, maxPage}) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.cover = cover;
|
this.cover = cover;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
@@ -840,6 +893,8 @@ function ComicDetails({title, cover, description, tags, chapters, isFavorite, su
|
|||||||
this.updateTime = updateTime;
|
this.updateTime = updateTime;
|
||||||
this.uploadTime = uploadTime;
|
this.uploadTime = uploadTime;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
this.stars = stars;
|
||||||
|
this.maxPage = maxPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -567,6 +567,7 @@ class SliverSearchBar extends StatefulWidget {
|
|||||||
required this.controller,
|
required this.controller,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.action,
|
this.action,
|
||||||
|
this.focusNode,
|
||||||
});
|
});
|
||||||
|
|
||||||
final SearchBarController controller;
|
final SearchBarController controller;
|
||||||
@@ -575,6 +576,8 @@ class SliverSearchBar extends StatefulWidget {
|
|||||||
|
|
||||||
final Widget? action;
|
final Widget? action;
|
||||||
|
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SliverSearchBar> createState() => _SliverSearchBarState();
|
State<SliverSearchBar> createState() => _SliverSearchBarState();
|
||||||
}
|
}
|
||||||
@@ -613,6 +616,7 @@ class _SliverSearchBarState extends State<SliverSearchBar>
|
|||||||
topPadding: MediaQuery.of(context).padding.top,
|
topPadding: MediaQuery.of(context).padding.top,
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
action: widget.action,
|
action: widget.action,
|
||||||
|
focusNode: widget.focusNode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -629,12 +633,15 @@ class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
|
|
||||||
final Widget? action;
|
final Widget? action;
|
||||||
|
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
const _SliverSearchBarDelegate({
|
const _SliverSearchBarDelegate({
|
||||||
required this.editingController,
|
required this.editingController,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.topPadding,
|
required this.topPadding,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.action,
|
this.action,
|
||||||
|
this.focusNode,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const _kAppBarHeight = 52.0;
|
static const _kAppBarHeight = 52.0;
|
||||||
@@ -662,6 +669,7 @@ class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
focusNode: focusNode,
|
||||||
controller: editingController,
|
controller: editingController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "Search".tl,
|
hintText: "Search".tl,
|
||||||
|
@@ -195,6 +195,7 @@ class ComicTile extends StatelessWidget {
|
|||||||
enableTranslate: ComicSource.find(comic.sourceKey)
|
enableTranslate: ComicSource.find(comic.sourceKey)
|
||||||
?.enableTagsTranslate ??
|
?.enableTagsTranslate ??
|
||||||
false,
|
false,
|
||||||
|
rating: comic.stars,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -285,6 +286,7 @@ class _ComicDescription extends StatelessWidget {
|
|||||||
this.badge,
|
this.badge,
|
||||||
this.maxLines = 2,
|
this.maxLines = 2,
|
||||||
this.tags,
|
this.tags,
|
||||||
|
this.rating,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
@@ -294,6 +296,7 @@ class _ComicDescription extends StatelessWidget {
|
|||||||
final List<String>? tags;
|
final List<String>? tags;
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
final bool enableTranslate;
|
final bool enableTranslate;
|
||||||
|
final double? rating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -358,6 +361,7 @@ class _ComicDescription extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
if (rating != null) StarRating(value: rating!, size: 18),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
@@ -623,9 +627,9 @@ class ComicListState extends State<ComicList> {
|
|||||||
String? _nextUrl;
|
String? _nextUrl;
|
||||||
|
|
||||||
void remove(Comic c) {
|
void remove(Comic c) {
|
||||||
if(_data[_page] == null || !_data[_page]!.remove(c)) {
|
if (_data[_page] == null || !_data[_page]!.remove(c)) {
|
||||||
for(var page in _data.values) {
|
for (var page in _data.values) {
|
||||||
if(page.remove(c)) {
|
if (page.remove(c)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -685,7 +689,7 @@ class ComicListState extends State<ComicList> {
|
|||||||
(_maxPage == null || page <= _maxPage!)) {
|
(_maxPage == null || page <= _maxPage!)) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_error = null;
|
_error = null;
|
||||||
this._page = page;
|
_page = page;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
context.showMessage(
|
context.showMessage(
|
||||||
@@ -777,10 +781,10 @@ class ComicListState extends State<ComicList> {
|
|||||||
Future<void> _fetchNext() async {
|
Future<void> _fetchNext() async {
|
||||||
var res = await widget.loadNext!(_nextUrl);
|
var res = await widget.loadNext!(_nextUrl);
|
||||||
_data[_data.length + 1] = res.data;
|
_data[_data.length + 1] = res.data;
|
||||||
if (res.subData['next'] == null) {
|
if (res.subData == null) {
|
||||||
_maxPage = _data.length;
|
_maxPage = _data.length;
|
||||||
} else {
|
} else {
|
||||||
_nextUrl = res.subData['next'];
|
_nextUrl = res.subData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,3 +832,286 @@ class ComicListState extends State<ComicList> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StarRating extends StatelessWidget {
|
||||||
|
const StarRating({
|
||||||
|
super.key,
|
||||||
|
required this.value,
|
||||||
|
this.onTap,
|
||||||
|
this.size = 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double value; // 0-5
|
||||||
|
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var interval = size * 0.1;
|
||||||
|
var value = this.value;
|
||||||
|
if (value.isNaN) {
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
var child = SizedBox(
|
||||||
|
height: size,
|
||||||
|
width: size * 5 + interval * 4,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < 5; i++)
|
||||||
|
_Star(
|
||||||
|
value: (value - i).clamp(0.0, 1.0),
|
||||||
|
size: size,
|
||||||
|
).paddingRight(i == 4 ? 0 : interval),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return onTap == null
|
||||||
|
? child
|
||||||
|
: GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Star extends StatelessWidget {
|
||||||
|
const _Star({required this.value, required this.size});
|
||||||
|
|
||||||
|
final double value; // 0-1
|
||||||
|
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.star_outline,
|
||||||
|
size: size,
|
||||||
|
color: context.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
ClipRect(
|
||||||
|
clipper: _StarClipper(value),
|
||||||
|
child: Icon(
|
||||||
|
Icons.star,
|
||||||
|
size: size,
|
||||||
|
color: context.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StarClipper extends CustomClipper<Rect> {
|
||||||
|
final double value;
|
||||||
|
|
||||||
|
_StarClipper(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Rect getClip(Size size) {
|
||||||
|
return Rect.fromLTWH(0, 0, size.width * value, size.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldReclip(covariant CustomClipper<Rect> oldClipper) {
|
||||||
|
return oldClipper is! _StarClipper || oldClipper.value != value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RatingWidget extends StatefulWidget {
|
||||||
|
/// star number
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
/// Max score
|
||||||
|
final double maxRating;
|
||||||
|
|
||||||
|
/// Current score value
|
||||||
|
final double value;
|
||||||
|
|
||||||
|
/// Star size
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
/// Space between the stars
|
||||||
|
final double padding;
|
||||||
|
|
||||||
|
/// Whether the score can be modified by sliding
|
||||||
|
final bool selectable;
|
||||||
|
|
||||||
|
/// Callbacks when ratings change
|
||||||
|
final ValueChanged<double> onRatingUpdate;
|
||||||
|
|
||||||
|
const RatingWidget(
|
||||||
|
{super.key,
|
||||||
|
this.maxRating = 10.0,
|
||||||
|
this.count = 5,
|
||||||
|
this.value = 10.0,
|
||||||
|
this.size = 20,
|
||||||
|
required this.padding,
|
||||||
|
this.selectable = false,
|
||||||
|
required this.onRatingUpdate});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RatingWidget> createState() => _RatingWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RatingWidgetState extends State<RatingWidget> {
|
||||||
|
double value = 10;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Listener(
|
||||||
|
onPointerDown: (PointerDownEvent event) {
|
||||||
|
double x = event.localPosition.dx;
|
||||||
|
if (x < 0) x = 0;
|
||||||
|
pointValue(x);
|
||||||
|
},
|
||||||
|
onPointerMove: (PointerMoveEvent event) {
|
||||||
|
double x = event.localPosition.dx;
|
||||||
|
if (x < 0) x = 0;
|
||||||
|
pointValue(x);
|
||||||
|
},
|
||||||
|
onPointerUp: (_) {},
|
||||||
|
behavior: HitTestBehavior.deferToChild,
|
||||||
|
child: buildRowRating(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pointValue(double dx) {
|
||||||
|
if (!widget.selectable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dx >=
|
||||||
|
widget.size * widget.count + widget.padding * (widget.count - 1)) {
|
||||||
|
value = widget.maxRating;
|
||||||
|
} else {
|
||||||
|
for (double i = 1; i < widget.count + 1; i++) {
|
||||||
|
if (dx > widget.size * i + widget.padding * (i - 1) &&
|
||||||
|
dx < widget.size * i + widget.padding * i) {
|
||||||
|
value = i * (widget.maxRating / widget.count);
|
||||||
|
break;
|
||||||
|
} else if (dx > widget.size * (i - 1) + widget.padding * (i - 1) &&
|
||||||
|
dx < widget.size * i + widget.padding * i) {
|
||||||
|
value = (dx - widget.padding * (i - 1)) /
|
||||||
|
(widget.size * widget.count) *
|
||||||
|
widget.maxRating;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value % 1 >= 0.5) {
|
||||||
|
value = value ~/ 1 + 1;
|
||||||
|
} else {
|
||||||
|
value = (value ~/ 1).toDouble();
|
||||||
|
}
|
||||||
|
if (value < 0) {
|
||||||
|
value = 0;
|
||||||
|
} else if (value > 10) {
|
||||||
|
value = 10;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
widget.onRatingUpdate(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int fullStars() {
|
||||||
|
return (value / (widget.maxRating / widget.count)).floor();
|
||||||
|
}
|
||||||
|
|
||||||
|
double star() {
|
||||||
|
if (widget.count / fullStars() == widget.maxRating / value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (value % (widget.maxRating / widget.count)) /
|
||||||
|
(widget.maxRating / widget.count);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> buildRow() {
|
||||||
|
int full = fullStars();
|
||||||
|
List<Widget> children = [];
|
||||||
|
for (int i = 0; i < full; i++) {
|
||||||
|
children.add(Icon(
|
||||||
|
Icons.star,
|
||||||
|
size: widget.size,
|
||||||
|
color: context.colorScheme.secondary,
|
||||||
|
));
|
||||||
|
if (i < widget.count - 1) {
|
||||||
|
children.add(
|
||||||
|
SizedBox(
|
||||||
|
width: widget.padding,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (full < widget.count) {
|
||||||
|
children.add(ClipRect(
|
||||||
|
clipper: SMClipper(rating: star() * widget.size),
|
||||||
|
child: Icon(
|
||||||
|
Icons.star,
|
||||||
|
size: widget.size,
|
||||||
|
color: context.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> buildNormalRow() {
|
||||||
|
List<Widget> children = [];
|
||||||
|
for (int i = 0; i < widget.count; i++) {
|
||||||
|
children.add(Icon(
|
||||||
|
Icons.star_border,
|
||||||
|
size: widget.size,
|
||||||
|
color: context.colorScheme.secondary,
|
||||||
|
));
|
||||||
|
if (i < widget.count - 1) {
|
||||||
|
children.add(SizedBox(
|
||||||
|
width: widget.padding,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildRowRating() {
|
||||||
|
return Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
children: buildNormalRow(),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: buildRow(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
value = widget.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SMClipper extends CustomClipper<Rect> {
|
||||||
|
final double rating;
|
||||||
|
|
||||||
|
SMClipper({required this.rating});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Rect getClip(Size size) {
|
||||||
|
return Rect.fromLTRB(0.0, 0.0, rating, size.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldReclip(SMClipper oldClipper) {
|
||||||
|
return rating != oldClipper.rating;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -3,7 +3,7 @@ library components;
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui';
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@@ -19,7 +19,7 @@ class BlurEffect extends StatelessWidget {
|
|||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: borderRadius ?? BorderRadius.zero,
|
borderRadius: borderRadius ?? BorderRadius.zero,
|
||||||
child: BackdropFilter(
|
child: BackdropFilter(
|
||||||
filter: ImageFilter.blur(
|
filter: ui.ImageFilter.blur(
|
||||||
sigmaX: blur,
|
sigmaX: blur,
|
||||||
sigmaY: blur,
|
sigmaY: blur,
|
||||||
tileMode: TileMode.mirror,
|
tileMode: TileMode.mirror,
|
||||||
|
@@ -21,11 +21,11 @@ class AnimatedImage extends StatefulWidget {
|
|||||||
this.gaplessPlayback = false,
|
this.gaplessPlayback = false,
|
||||||
this.filterQuality = FilterQuality.medium,
|
this.filterQuality = FilterQuality.medium,
|
||||||
this.isAntiAlias = false,
|
this.isAntiAlias = false,
|
||||||
|
this.part,
|
||||||
Map<String, String>? headers,
|
Map<String, String>? headers,
|
||||||
int? cacheWidth,
|
int? cacheWidth,
|
||||||
int? cacheHeight,
|
int? cacheHeight,
|
||||||
}
|
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image),
|
||||||
): image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image),
|
|
||||||
assert(cacheWidth == null || cacheWidth > 0),
|
assert(cacheWidth == null || cacheWidth > 0),
|
||||||
assert(cacheHeight == null || cacheHeight > 0);
|
assert(cacheHeight == null || cacheHeight > 0);
|
||||||
|
|
||||||
@@ -61,13 +61,16 @@ class AnimatedImage extends StatefulWidget {
|
|||||||
|
|
||||||
final bool isAntiAlias;
|
final bool isAntiAlias;
|
||||||
|
|
||||||
|
final ImagePart? part;
|
||||||
|
|
||||||
static void clear() => _AnimatedImageState.clear();
|
static void clear() => _AnimatedImageState.clear();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AnimatedImage> createState() => _AnimatedImageState();
|
State<AnimatedImage> createState() => _AnimatedImageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserver {
|
class _AnimatedImageState extends State<AnimatedImage>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
ImageStream? _imageStream;
|
ImageStream? _imageStream;
|
||||||
ImageInfo? _imageInfo;
|
ImageInfo? _imageInfo;
|
||||||
ImageChunkEvent? _loadingProgress;
|
ImageChunkEvent? _loadingProgress;
|
||||||
@@ -138,8 +141,8 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateInvertColors() {
|
void _updateInvertColors() {
|
||||||
_invertColors = MediaQuery.maybeInvertColorsOf(context)
|
_invertColors = MediaQuery.maybeInvertColorsOf(context) ??
|
||||||
?? SemanticsBinding.instance.accessibilityFeatures.invertColors;
|
SemanticsBinding.instance.accessibilityFeatures.invertColors;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resolveImage() {
|
void _resolveImage() {
|
||||||
@@ -150,14 +153,17 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
|
|||||||
final ImageStream newStream =
|
final ImageStream newStream =
|
||||||
provider.resolve(createLocalImageConfiguration(
|
provider.resolve(createLocalImageConfiguration(
|
||||||
context,
|
context,
|
||||||
size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
|
size: widget.width != null && widget.height != null
|
||||||
|
? Size(widget.width!, widget.height!)
|
||||||
|
: null,
|
||||||
));
|
));
|
||||||
_updateSourceStream(newStream);
|
_updateSourceStream(newStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageStreamListener? _imageStreamListener;
|
ImageStreamListener? _imageStreamListener;
|
||||||
|
|
||||||
ImageStreamListener _getListener({bool recreateListener = false}) {
|
ImageStreamListener _getListener({bool recreateListener = false}) {
|
||||||
if(_imageStreamListener == null || recreateListener) {
|
if (_imageStreamListener == null || recreateListener) {
|
||||||
_lastException = null;
|
_lastException = null;
|
||||||
_imageStreamListener = ImageStreamListener(
|
_imageStreamListener = ImageStreamListener(
|
||||||
_handleImageFrame,
|
_handleImageFrame,
|
||||||
@@ -191,7 +197,8 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
|
|||||||
|
|
||||||
void _replaceImage({required ImageInfo? info}) {
|
void _replaceImage({required ImageInfo? info}) {
|
||||||
final ImageInfo? oldImageInfo = _imageInfo;
|
final ImageInfo? oldImageInfo = _imageInfo;
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) => oldImageInfo?.dispose());
|
SchedulerBinding.instance
|
||||||
|
.addPostFrameCallback((_) => oldImageInfo?.dispose());
|
||||||
_imageInfo = info;
|
_imageInfo = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +215,9 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!widget.gaplessPlayback) {
|
if (!widget.gaplessPlayback) {
|
||||||
setState(() { _replaceImage(info: null); });
|
setState(() {
|
||||||
|
_replaceImage(info: null);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -247,7 +256,9 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keepStreamAlive && _completerHandle == null && _imageStream?.completer != null) {
|
if (keepStreamAlive &&
|
||||||
|
_completerHandle == null &&
|
||||||
|
_imageStream?.completer != null) {
|
||||||
_completerHandle = _imageStream!.completer!.keepAlive();
|
_completerHandle = _imageStream!.completer!.keepAlive();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +270,19 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget result;
|
Widget result;
|
||||||
|
|
||||||
if(_imageInfo != null){
|
if (_imageInfo != null) {
|
||||||
|
if(widget.part != null) {
|
||||||
|
return CustomPaint(
|
||||||
|
painter: ImagePainter(
|
||||||
|
image: _imageInfo!.image,
|
||||||
|
part: widget.part!,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: widget.width,
|
||||||
|
height: widget.height,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
result = RawImage(
|
result = RawImage(
|
||||||
image: _imageInfo?.image,
|
image: _imageInfo?.image,
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
@@ -291,7 +314,7 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
|
|||||||
child: result,
|
child: result,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else{
|
} else {
|
||||||
result = const Center();
|
result = const Center();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,8 +330,59 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
|
|||||||
super.debugFillProperties(description);
|
super.debugFillProperties(description);
|
||||||
description.add(DiagnosticsProperty<ImageStream>('stream', _imageStream));
|
description.add(DiagnosticsProperty<ImageStream>('stream', _imageStream));
|
||||||
description.add(DiagnosticsProperty<ImageInfo>('pixels', _imageInfo));
|
description.add(DiagnosticsProperty<ImageInfo>('pixels', _imageInfo));
|
||||||
description.add(DiagnosticsProperty<ImageChunkEvent>('loadingProgress', _loadingProgress));
|
description.add(DiagnosticsProperty<ImageChunkEvent>(
|
||||||
|
'loadingProgress', _loadingProgress));
|
||||||
description.add(DiagnosticsProperty<int>('frameNumber', _frameNumber));
|
description.add(DiagnosticsProperty<int>('frameNumber', _frameNumber));
|
||||||
description.add(DiagnosticsProperty<bool>('wasSynchronouslyLoaded', _wasSynchronouslyLoaded));
|
description.add(DiagnosticsProperty<bool>(
|
||||||
|
'wasSynchronouslyLoaded', _wasSynchronouslyLoaded));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImagePart {
|
||||||
|
final double? x1;
|
||||||
|
final double? y1;
|
||||||
|
final double? x2;
|
||||||
|
final double? y2;
|
||||||
|
|
||||||
|
const ImagePart({
|
||||||
|
this.x1,
|
||||||
|
this.y1,
|
||||||
|
this.x2,
|
||||||
|
this.y2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImagePainter extends CustomPainter {
|
||||||
|
final ui.Image image;
|
||||||
|
|
||||||
|
final ImagePart part;
|
||||||
|
|
||||||
|
/// Render a part of the image.
|
||||||
|
const ImagePainter({
|
||||||
|
required this.image,
|
||||||
|
this.part = const ImagePart(),
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final Rect src = Rect.fromPoints(
|
||||||
|
Offset(part.x1 ?? 0, part.y1 ?? 0),
|
||||||
|
Offset(
|
||||||
|
part.x2 ?? image.width.toDouble(),
|
||||||
|
part.y2 ?? image.height.toDouble(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final Rect dst = Offset.zero & size;
|
||||||
|
canvas.drawImageRect(image, src, dst, Paint());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||||
|
return oldDelegate is! ImagePainter ||
|
||||||
|
oldDelegate.image != image ||
|
||||||
|
oldDelegate.part.x1 != part.x1 ||
|
||||||
|
oldDelegate.part.y1 != part.y1 ||
|
||||||
|
oldDelegate.part.x2 != part.x2 ||
|
||||||
|
oldDelegate.part.y2 != part.y2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -62,6 +62,7 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
|
|||||||
return Listener(
|
return Listener(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onPointerDown: (event) {
|
onPointerDown: (event) {
|
||||||
|
_futurePosition = null;
|
||||||
if (_isMouseScroll) {
|
if (_isMouseScroll) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isMouseScroll = false;
|
_isMouseScroll = false;
|
||||||
|
@@ -2,8 +2,10 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/state_controller.dart';
|
import 'package:venera/foundation/state_controller.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
@@ -97,8 +99,12 @@ class WindowFrame extends StatelessWidget {
|
|||||||
).toAlign(Alignment.centerLeft).paddingLeft(4),
|
).toAlign(Alignment.centerLeft).paddingLeft(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!App.isMacOS)
|
if (kDebugMode)
|
||||||
const WindowButtons()
|
const TextButton(
|
||||||
|
onPressed: debug,
|
||||||
|
child: Text('Debug'),
|
||||||
|
),
|
||||||
|
if (!App.isMacOS) const WindowButtons()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -622,3 +628,7 @@ TransitionBuilder VirtualWindowFrameInit() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void debug() {
|
||||||
|
ComicSource.reload();
|
||||||
|
}
|
@@ -27,6 +27,10 @@ part 'models.dart';
|
|||||||
/// build comic list, [Res.subData] should be maxPage or null if there is no limit.
|
/// build comic list, [Res.subData] should be maxPage or null if there is no limit.
|
||||||
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
|
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
|
||||||
|
|
||||||
|
/// build comic list with next param, [Res.subData] should be next page param or null if there is no next page.
|
||||||
|
typedef ComicListBuilderWithNext = Future<Res<List<Comic>>> Function(
|
||||||
|
String? next);
|
||||||
|
|
||||||
typedef LoginFunction = Future<Res<bool>> Function(String, String);
|
typedef LoginFunction = Future<Res<bool>> Function(String, String);
|
||||||
|
|
||||||
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
|
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
|
||||||
@@ -40,7 +44,7 @@ typedef CommentsLoader = Future<Res<List<Comment>>> Function(
|
|||||||
typedef SendCommentFunc = Future<Res<bool>> Function(
|
typedef SendCommentFunc = Future<Res<bool>> Function(
|
||||||
String id, String? subId, String content, String? replyTo);
|
String id, String? subId, String content, String? replyTo);
|
||||||
|
|
||||||
typedef GetImageLoadingConfigFunc = Map<String, dynamic> Function(
|
typedef GetImageLoadingConfigFunc = Future<Map<String, dynamic>> Function(
|
||||||
String imageKey, String comicId, String epId)?;
|
String imageKey, String comicId, String epId)?;
|
||||||
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
|
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
|
||||||
String imageKey)?;
|
String imageKey)?;
|
||||||
@@ -64,6 +68,9 @@ typedef VoteCommentFunc = Future<Res<int?>> Function(
|
|||||||
typedef HandleClickTagEvent = Map<String, String> Function(
|
typedef HandleClickTagEvent = Map<String, String> Function(
|
||||||
String namespace, String tag);
|
String namespace, String tag);
|
||||||
|
|
||||||
|
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
|
||||||
|
typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating);
|
||||||
|
|
||||||
class ComicSource {
|
class ComicSource {
|
||||||
static final List<ComicSource> _sources = [];
|
static final List<ComicSource> _sources = [];
|
||||||
|
|
||||||
@@ -163,8 +170,7 @@ class ComicSource {
|
|||||||
/// Load comic pages.
|
/// Load comic pages.
|
||||||
final LoadComicPagesFunc? loadComicPages;
|
final LoadComicPagesFunc? loadComicPages;
|
||||||
|
|
||||||
final Map<String, dynamic> Function(
|
final GetImageLoadingConfigFunc? getImageLoadingConfig;
|
||||||
String imageKey, String comicId, String epId)? getImageLoadingConfig;
|
|
||||||
|
|
||||||
final Map<String, dynamic> Function(String imageKey)?
|
final Map<String, dynamic> Function(String imageKey)?
|
||||||
getThumbnailLoadingConfig;
|
getThumbnailLoadingConfig;
|
||||||
@@ -203,6 +209,8 @@ class ComicSource {
|
|||||||
|
|
||||||
final bool enableTagsTranslate;
|
final bool enableTagsTranslate;
|
||||||
|
|
||||||
|
final StarRatingFunc? starRatingFunc;
|
||||||
|
|
||||||
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()) {
|
||||||
@@ -270,6 +278,7 @@ class ComicSource {
|
|||||||
this.linkHandler,
|
this.linkHandler,
|
||||||
this.enableTagsSuggestions,
|
this.enableTagsSuggestions,
|
||||||
this.enableTagsTranslate,
|
this.enableTagsTranslate,
|
||||||
|
this.starRatingFunc,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,12 +297,21 @@ class AccountConfig {
|
|||||||
|
|
||||||
final bool Function(String url, String title)? checkLoginStatus;
|
final bool Function(String url, String title)? checkLoginStatus;
|
||||||
|
|
||||||
|
final void Function()? onLoginWithWebviewSuccess;
|
||||||
|
|
||||||
|
final List<String>? cookieFields;
|
||||||
|
|
||||||
|
final Future<bool> Function(List<String>)? validateCookies;
|
||||||
|
|
||||||
const AccountConfig(
|
const AccountConfig(
|
||||||
this.login,
|
this.login,
|
||||||
this.loginWebsite,
|
this.loginWebsite,
|
||||||
this.registerWebsite,
|
this.registerWebsite,
|
||||||
this.logout,
|
this.logout,
|
||||||
this.checkLoginStatus,
|
this.checkLoginStatus,
|
||||||
|
this.onLoginWithWebviewSuccess,
|
||||||
|
this.cookieFields,
|
||||||
|
this.validateCookies,
|
||||||
) : allowReLogin = true,
|
) : allowReLogin = true,
|
||||||
infoItems = const [];
|
infoItems = const [];
|
||||||
}
|
}
|
||||||
@@ -322,6 +340,8 @@ class ExplorePageData {
|
|||||||
|
|
||||||
final ComicListBuilder? loadPage;
|
final ComicListBuilder? loadPage;
|
||||||
|
|
||||||
|
final ComicListBuilderWithNext? loadNext;
|
||||||
|
|
||||||
final Future<Res<List<ExplorePagePart>>> Function()? loadMultiPart;
|
final Future<Res<List<ExplorePagePart>>> Function()? loadMultiPart;
|
||||||
|
|
||||||
/// return a `List` contains `List<Comic>` or `ExplorePagePart`
|
/// return a `List` contains `List<Comic>` or `ExplorePagePart`
|
||||||
@@ -331,6 +351,7 @@ class ExplorePageData {
|
|||||||
this.title,
|
this.title,
|
||||||
this.type,
|
this.type,
|
||||||
this.loadPage,
|
this.loadPage,
|
||||||
|
this.loadNext,
|
||||||
this.loadMultiPart,
|
this.loadMultiPart,
|
||||||
this.loadMixed,
|
this.loadMixed,
|
||||||
);
|
);
|
||||||
@@ -388,9 +409,13 @@ class SearchOptions {
|
|||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
const SearchOptions(this.options, this.label);
|
final String type;
|
||||||
|
|
||||||
String get defaultValue => options.keys.first;
|
final String? defaultVal;
|
||||||
|
|
||||||
|
const SearchOptions(this.options, this.label, this.type, this.defaultVal);
|
||||||
|
|
||||||
|
String get defaultValue => defaultVal ?? options.keys.first;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
||||||
@@ -401,7 +426,7 @@ class CategoryComicsData {
|
|||||||
final List<CategoryComicsOptions> options;
|
final List<CategoryComicsOptions> options;
|
||||||
|
|
||||||
/// [category] is the one clicked by the user on the category page.
|
/// [category] is the one clicked by the user on the category page.
|
||||||
|
///
|
||||||
/// if [BaseCategoryPart.categoryParams] is not null, [param] will be not null.
|
/// if [BaseCategoryPart.categoryParams] is not null, [param] will be not null.
|
||||||
///
|
///
|
||||||
/// [Res.subData] should be maxPage or null if there is no limit.
|
/// [Res.subData] should be maxPage or null if there is no limit.
|
||||||
@@ -415,9 +440,12 @@ class CategoryComicsData {
|
|||||||
class RankingData {
|
class RankingData {
|
||||||
final Map<String, String> options;
|
final Map<String, String> options;
|
||||||
|
|
||||||
final Future<Res<List<Comic>>> Function(String option, int page) load;
|
final Future<Res<List<Comic>>> Function(String option, int page)? load;
|
||||||
|
|
||||||
const RankingData(this.options, this.load);
|
final Future<Res<List<Comic>>> Function(String option, String? next)?
|
||||||
|
loadWithNext;
|
||||||
|
|
||||||
|
const RankingData(this.options, this.load, this.loadWithNext);
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoryComicsOptions {
|
class CategoryComicsOptions {
|
||||||
@@ -434,7 +462,6 @@ class CategoryComicsOptions {
|
|||||||
const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen);
|
const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class LinkHandler {
|
class LinkHandler {
|
||||||
final List<String> domains;
|
final List<String> domains;
|
||||||
|
|
||||||
|
@@ -1,20 +1,26 @@
|
|||||||
part of 'comic_source.dart';
|
part of 'comic_source.dart';
|
||||||
|
|
||||||
typedef AddOrDelFavFunc = Future<Res<bool>> Function(String comicId, String folderId, bool isAdding, String? favId);
|
typedef AddOrDelFavFunc = Future<Res<bool>> Function(
|
||||||
|
String comicId, String folderId, bool isAdding, String? favId);
|
||||||
|
|
||||||
class FavoriteData{
|
class FavoriteData {
|
||||||
final String key;
|
final String key;
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
final bool multiFolder;
|
final bool multiFolder;
|
||||||
|
|
||||||
final Future<Res<List<Comic>>> Function(int page, [String? folder]) loadComic;
|
final Future<Res<List<Comic>>> Function(int page, [String? folder])?
|
||||||
|
loadComic;
|
||||||
|
|
||||||
|
final Future<Res<List<Comic>>> Function(String? next, [String? folder])?
|
||||||
|
loadNext;
|
||||||
|
|
||||||
/// key-id, value-name
|
/// key-id, value-name
|
||||||
///
|
///
|
||||||
/// if comicId is not null, Res.subData is the folders that the comic is in
|
/// if comicId is not null, Res.subData is the folders that the comic is in
|
||||||
final Future<Res<Map<String, String>>> Function([String? comicId])? loadFolders;
|
final Future<Res<Map<String, String>>> Function([String? comicId])?
|
||||||
|
loadFolders;
|
||||||
|
|
||||||
/// A value of null disables this feature
|
/// A value of null disables this feature
|
||||||
final Future<Res<bool>> Function(String key)? deleteFolder;
|
final Future<Res<bool>> Function(String key)? deleteFolder;
|
||||||
@@ -32,19 +38,21 @@ class FavoriteData{
|
|||||||
required this.title,
|
required this.title,
|
||||||
required this.multiFolder,
|
required this.multiFolder,
|
||||||
required this.loadComic,
|
required this.loadComic,
|
||||||
|
required this.loadNext,
|
||||||
this.loadFolders,
|
this.loadFolders,
|
||||||
this.deleteFolder,
|
this.deleteFolder,
|
||||||
this.addFolder,
|
this.addFolder,
|
||||||
this.allFavoritesId,
|
this.allFavoritesId,
|
||||||
this.addOrDelFavorite});
|
this.addOrDelFavorite,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
FavoriteData getFavoriteData(String key){
|
FavoriteData getFavoriteData(String key) {
|
||||||
var source = ComicSource.find(key) ?? (throw "Unknown source key: $key");
|
var source = ComicSource.find(key) ?? (throw "Unknown source key: $key");
|
||||||
return source.favoriteData!;
|
return source.favoriteData!;
|
||||||
}
|
}
|
||||||
|
|
||||||
FavoriteData? getFavoriteDataOrNull(String key){
|
FavoriteData? getFavoriteDataOrNull(String key) {
|
||||||
var source = ComicSource.find(key);
|
var source = ComicSource.find(key);
|
||||||
return source?.favoriteData;
|
return source?.favoriteData;
|
||||||
}
|
}
|
@@ -7,9 +7,9 @@ class Comment {
|
|||||||
final String? time;
|
final String? time;
|
||||||
final int? replyCount;
|
final int? replyCount;
|
||||||
final String? id;
|
final String? id;
|
||||||
final int? score;
|
int? score;
|
||||||
final bool? isLiked;
|
final bool? isLiked;
|
||||||
final int? voteStatus; // 1: upvote, -1: downvote, 0: none
|
int? voteStatus; // 1: upvote, -1: downvote, 0: none
|
||||||
|
|
||||||
static String? parseTime(dynamic value) {
|
static String? parseTime(dynamic value) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
@@ -60,6 +60,9 @@ class Comic {
|
|||||||
|
|
||||||
final String? favoriteId;
|
final String? favoriteId;
|
||||||
|
|
||||||
|
/// 0-5
|
||||||
|
final double? stars;
|
||||||
|
|
||||||
const Comic(
|
const Comic(
|
||||||
this.title,
|
this.title,
|
||||||
this.cover,
|
this.cover,
|
||||||
@@ -70,7 +73,7 @@ class Comic {
|
|||||||
this.sourceKey,
|
this.sourceKey,
|
||||||
this.maxPage,
|
this.maxPage,
|
||||||
this.language,
|
this.language,
|
||||||
): favoriteId = null;
|
): favoriteId = null, stars = null;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
@@ -96,7 +99,8 @@ class Comic {
|
|||||||
description = json["description"] ?? "",
|
description = json["description"] ?? "",
|
||||||
maxPage = json["maxPage"],
|
maxPage = json["maxPage"],
|
||||||
language = json["language"],
|
language = json["language"],
|
||||||
favoriteId = json["favoriteId"];
|
favoriteId = json["favoriteId"],
|
||||||
|
stars = (json["stars"] as num?)?.toDouble();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
@@ -151,6 +155,11 @@ class ComicDetails with HistoryMixin {
|
|||||||
|
|
||||||
final String? url;
|
final String? url;
|
||||||
|
|
||||||
|
final double? stars;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final int? maxPage;
|
||||||
|
|
||||||
static Map<String, List<String>> _generateMap(Map<dynamic, 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) {
|
||||||
@@ -182,7 +191,9 @@ class ComicDetails with HistoryMixin {
|
|||||||
uploader = json["uploader"],
|
uploader = json["uploader"],
|
||||||
uploadTime = json["uploadTime"],
|
uploadTime = json["uploadTime"],
|
||||||
updateTime = json["updateTime"],
|
updateTime = json["updateTime"],
|
||||||
url = json["url"];
|
url = json["url"],
|
||||||
|
stars = (json["stars"] as num?)?.toDouble(),
|
||||||
|
maxPage = json["maxPage"];
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
|
@@ -150,6 +150,7 @@ class ComicSourceParser {
|
|||||||
_parseLinkHandler(),
|
_parseLinkHandler(),
|
||||||
_getValue("search.enableTagsSuggestions") ?? false,
|
_getValue("search.enableTagsSuggestions") ?? false,
|
||||||
_getValue("comic.enableTagsTranslate") ?? false,
|
_getValue("comic.enableTagsTranslate") ?? false,
|
||||||
|
_parseStarRatingFunc(),
|
||||||
);
|
);
|
||||||
|
|
||||||
await source.loadData();
|
await source.loadData();
|
||||||
@@ -182,7 +183,10 @@ class ComicSourceParser {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Res<bool>> login(account, pwd) async {
|
Future<Res<bool>> Function(String account, String pwd)? login;
|
||||||
|
|
||||||
|
if(_checkExists("account.login")) {
|
||||||
|
login = (account, pwd) async {
|
||||||
try {
|
try {
|
||||||
await JsEngine().runCode("""
|
await JsEngine().runCode("""
|
||||||
ComicSource.sources.$_key.account.login(${jsonEncode(account)},
|
ComicSource.sources.$_key.account.login(${jsonEncode(account)},
|
||||||
@@ -196,36 +200,62 @@ class ComicSourceParser {
|
|||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void logout() {
|
void logout() {
|
||||||
JsEngine().runCode("ComicSource.sources.$_key.account.logout()");
|
JsEngine().runCode("ComicSource.sources.$_key.account.logout()");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_checkExists('account.loginWithWebview')) {
|
bool Function(String url, String title)? checkLoginStatus;
|
||||||
return AccountConfig(
|
|
||||||
login,
|
void Function()? onLoginSuccess;
|
||||||
null,
|
|
||||||
_getValue("account.registerWebsite"),
|
if (_checkExists('account.loginWithWebview')) {
|
||||||
logout,
|
checkLoginStatus = (url, title) {
|
||||||
null,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return AccountConfig(
|
|
||||||
null,
|
|
||||||
_getValue("account.loginWithWebview.url"),
|
|
||||||
_getValue("account.registerWebsite"),
|
|
||||||
logout,
|
|
||||||
(url, title) {
|
|
||||||
return JsEngine().runCode("""
|
return JsEngine().runCode("""
|
||||||
ComicSource.sources.$_key.account.loginWithWebview.checkStatus(
|
ComicSource.sources.$_key.account.loginWithWebview.checkStatus(
|
||||||
${jsonEncode(url)}, ${jsonEncode(title)})
|
${jsonEncode(url)}, ${jsonEncode(title)})
|
||||||
""");
|
""");
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
if (_checkExists('account.loginWithWebview.onLoginSuccess')) {
|
||||||
|
onLoginSuccess = () {
|
||||||
|
JsEngine().runCode("""
|
||||||
|
ComicSource.sources.$_key.account.loginWithWebview.onLoginSuccess()
|
||||||
|
""");
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> Function(List<String>)? validateCookies;
|
||||||
|
|
||||||
|
if (_checkExists('account.loginWithCookies?.validate')) {
|
||||||
|
validateCookies = (cookies) async {
|
||||||
|
try {
|
||||||
|
var res = await JsEngine().runCode("""
|
||||||
|
ComicSource.sources.$_key.account.loginWithCookies.validate(${jsonEncode(cookies)})
|
||||||
|
""");
|
||||||
|
return res;
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return AccountConfig(
|
||||||
|
login,
|
||||||
|
_getValue("account.loginWithWebview?.url"),
|
||||||
|
_getValue("account.registerWebsite"),
|
||||||
|
logout,
|
||||||
|
checkLoginStatus,
|
||||||
|
onLoginSuccess,
|
||||||
|
ListOrNull.from(_getValue("account.loginWithCookies?.fields")),
|
||||||
|
validateCookies,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
List<ExplorePageData> _loadExploreData() {
|
List<ExplorePageData> _loadExploreData() {
|
||||||
if (!_checkExists("explore")) {
|
if (!_checkExists("explore")) {
|
||||||
return const [];
|
return const [];
|
||||||
@@ -237,6 +267,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<Comic>>> Function(String? next)? loadNext;
|
||||||
Future<Res<List<Object>>> Function(int index)? loadMixed;
|
Future<Res<List<Object>>> Function(int index)? loadMixed;
|
||||||
if (type == "singlePageWithMultiPart") {
|
if (type == "singlePageWithMultiPart") {
|
||||||
loadMultiPart = () async {
|
loadMultiPart = () async {
|
||||||
@@ -257,6 +288,7 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else if (type == "multiPageComicList") {
|
} else if (type == "multiPageComicList") {
|
||||||
|
if (_checkExists("explore[$i].load")) {
|
||||||
loadPage = (int page) async {
|
loadPage = (int page) async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine().runCode(
|
var res = await JsEngine().runCode(
|
||||||
@@ -270,6 +302,22 @@ class ComicSourceParser {
|
|||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
loadNext = (next) async {
|
||||||
|
try {
|
||||||
|
var res = await JsEngine().runCode(
|
||||||
|
"ComicSource.sources.$_key.explore[$i].loadNext(${jsonEncode(next)})");
|
||||||
|
return Res(
|
||||||
|
List.generate(res["comics"].length,
|
||||||
|
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
||||||
|
subData: res["next"],
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
} else if (type == "multiPartPage") {
|
} else if (type == "multiPartPage") {
|
||||||
loadMultiPart = () async {
|
loadMultiPart = () async {
|
||||||
try {
|
try {
|
||||||
@@ -330,6 +378,7 @@ class ComicSourceParser {
|
|||||||
throw ComicSourceParseException("Unknown explore page type $type")
|
throw ComicSourceParseException("Unknown explore page type $type")
|
||||||
},
|
},
|
||||||
loadPage,
|
loadPage,
|
||||||
|
loadNext,
|
||||||
loadMultiPart,
|
loadMultiPart,
|
||||||
loadMixed,
|
loadMixed,
|
||||||
));
|
));
|
||||||
@@ -406,7 +455,11 @@ class ComicSourceParser {
|
|||||||
var value = split.join("-");
|
var value = split.join("-");
|
||||||
options[key] = value;
|
options[key] = value;
|
||||||
}
|
}
|
||||||
rankingData = RankingData(options, (option, page) async {
|
Future<Res<List<Comic>>> Function(String option, int page)? load;
|
||||||
|
Future<Res<List<Comic>>> Function(String option, String? next)?
|
||||||
|
loadWithNext;
|
||||||
|
if (_checkExists("categoryComics.ranking.load")) {
|
||||||
|
load = (option, page) async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine().runCode("""
|
var res = await JsEngine().runCode("""
|
||||||
ComicSource.sources.$_key.categoryComics.ranking.load(
|
ComicSource.sources.$_key.categoryComics.ranking.load(
|
||||||
@@ -420,7 +473,26 @@ class ComicSourceParser {
|
|||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
} else {
|
||||||
|
loadWithNext = (option, next) async {
|
||||||
|
try {
|
||||||
|
var res = await JsEngine().runCode("""
|
||||||
|
ComicSource.sources.$_key.categoryComics.ranking.loadWithNext(
|
||||||
|
${jsonEncode(option)}, ${jsonEncode(next)})
|
||||||
|
""");
|
||||||
|
return Res(
|
||||||
|
List.generate(res["comics"].length,
|
||||||
|
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
||||||
|
subData: res["next"],
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rankingData = RankingData(options, load, loadWithNext);
|
||||||
}
|
}
|
||||||
return CategoryComicsData(options, (category, param, options, page) async {
|
return CategoryComicsData(options, (category, param, options, page) async {
|
||||||
try {
|
try {
|
||||||
@@ -457,7 +529,12 @@ class ComicSourceParser {
|
|||||||
var value = split.join("-");
|
var value = split.join("-");
|
||||||
map[key] = value;
|
map[key] = value;
|
||||||
}
|
}
|
||||||
options.add(SearchOptions(map, element["label"]));
|
options.add(SearchOptions(
|
||||||
|
map,
|
||||||
|
element["label"],
|
||||||
|
element['type'] ?? 'select',
|
||||||
|
element['default'] == null ? null : jsonEncode(element['default']),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
return SearchPageData(options, (keyword, page, searchOption) async {
|
return SearchPageData(options, (keyword, page, searchOption) async {
|
||||||
try {
|
try {
|
||||||
@@ -550,7 +627,12 @@ class ComicSourceParser {
|
|||||||
return retryZone(func);
|
return retryZone(func);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Res<List<Comic>>> loadComic(int page, [String? folder]) async {
|
Future<Res<List<Comic>>> Function(int page, [String? folder])? loadComic;
|
||||||
|
|
||||||
|
Future<Res<List<Comic>>> Function(String? next, [String? folder])? loadNext;
|
||||||
|
|
||||||
|
if (_checkExists("favorites.loadComic")) {
|
||||||
|
loadComic = (int page, [String? folder]) async {
|
||||||
Future<Res<List<Comic>>> func() async {
|
Future<Res<List<Comic>>> func() async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine().runCode("""
|
var res = await JsEngine().runCode("""
|
||||||
@@ -568,6 +650,30 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return retryZone(func);
|
return retryZone(func);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_checkExists("favorites.loadNext")) {
|
||||||
|
loadNext = (String? next, [String? folder]) async {
|
||||||
|
Future<Res<List<Comic>>> func() async {
|
||||||
|
try {
|
||||||
|
var res = await JsEngine().runCode("""
|
||||||
|
ComicSource.sources.$_key.favorites.loadNext(
|
||||||
|
${jsonEncode(next)}, ${jsonEncode(folder)})
|
||||||
|
""");
|
||||||
|
return Res(
|
||||||
|
List.generate(res["comics"].length,
|
||||||
|
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
||||||
|
subData: res["next"],
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return retryZone(func);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Res<Map<String, String>>> Function([String? comicId])? loadFolders;
|
Future<Res<Map<String, String>>> Function([String? comicId])? loadFolders;
|
||||||
@@ -625,6 +731,7 @@ class ComicSourceParser {
|
|||||||
title: _name!,
|
title: _name!,
|
||||||
multiFolder: multiFolder,
|
multiFolder: multiFolder,
|
||||||
loadComic: loadComic,
|
loadComic: loadComic,
|
||||||
|
loadNext: loadNext,
|
||||||
loadFolders: loadFolders,
|
loadFolders: loadFolders,
|
||||||
addFolder: addFolder,
|
addFolder: addFolder,
|
||||||
deleteFolder: deleteFolder,
|
deleteFolder: deleteFolder,
|
||||||
@@ -683,11 +790,15 @@ class ComicSourceParser {
|
|||||||
if (!_checkExists("comic.onImageLoad")) {
|
if (!_checkExists("comic.onImageLoad")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (imageKey, comicId, ep) {
|
return (imageKey, comicId, ep) async {
|
||||||
return JsEngine().runCode("""
|
var res = JsEngine().runCode("""
|
||||||
ComicSource.sources.$_key.comic.onImageLoad(
|
ComicSource.sources.$_key.comic.onImageLoad(
|
||||||
${jsonEncode(imageKey)}, ${jsonEncode(comicId)}, ${jsonEncode(ep)})
|
${jsonEncode(imageKey)}, ${jsonEncode(comicId)}, ${jsonEncode(ep)})
|
||||||
""") as Map<String, dynamic>;
|
""");
|
||||||
|
if (res is Future) {
|
||||||
|
return await res;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,4 +937,21 @@ class ComicSourceParser {
|
|||||||
|
|
||||||
return LinkHandler(domains, linkToId);
|
return LinkHandler(domains, linkToId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StarRatingFunc? _parseStarRatingFunc() {
|
||||||
|
if (!_checkExists("comic.starRating")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (id, rating) async {
|
||||||
|
try {
|
||||||
|
await JsEngine().runCode("""
|
||||||
|
ComicSource.sources.$_key.comic.starRating(${jsonEncode(id)}, ${jsonEncode(rating)})
|
||||||
|
""");
|
||||||
|
return const Res(true);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -55,6 +55,12 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
|||||||
_cacheSize += data.length;
|
_cacheSize += data.length;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if(e.toString().contains("Invalid Status Code: 404")) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
if(e.toString().contains("Invalid Status Code: 403")) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
if (e.toString().contains("handshake")) {
|
if (e.toString().contains("handshake")) {
|
||||||
if (retryTime < 5) {
|
if (retryTime < 5) {
|
||||||
retryTime = 5;
|
retryTime = 5;
|
||||||
|
@@ -21,7 +21,6 @@ import 'package:pointycastle/block/modes/ecb.dart';
|
|||||||
import 'package:pointycastle/block/modes/ofb.dart';
|
import 'package:pointycastle/block/modes/ofb.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:venera/network/app_dio.dart';
|
import 'package:venera/network/app_dio.dart';
|
||||||
import 'package:venera/network/cloudflare.dart';
|
|
||||||
import 'package:venera/network/cookie_jar.dart';
|
import 'package:venera/network/cookie_jar.dart';
|
||||||
|
|
||||||
import 'comic_source/comic_source.dart';
|
import 'comic_source/comic_source.dart';
|
||||||
@@ -66,8 +65,6 @@ class JsEngine with _JSEngineApi {
|
|||||||
_dio ??= AppDio(BaseOptions(
|
_dio ??= AppDio(BaseOptions(
|
||||||
responseType: ResponseType.plain, validateStatus: (status) => true));
|
responseType: ResponseType.plain, validateStatus: (status) => true));
|
||||||
_cookieJar ??= SingleInstanceCookieJar.instance!;
|
_cookieJar ??= SingleInstanceCookieJar.instance!;
|
||||||
_dio!.interceptors.add(CookieManagerSql(_cookieJar!));
|
|
||||||
_dio!.interceptors.add(CloudflareInterceptor());
|
|
||||||
_closed = false;
|
_closed = false;
|
||||||
_engine = FlutterQjs();
|
_engine = FlutterQjs();
|
||||||
_engine!.dispatch();
|
_engine!.dispatch();
|
||||||
@@ -160,7 +157,7 @@ class JsEngine with _JSEngineApi {
|
|||||||
String key = message["key"];
|
String key = message["key"];
|
||||||
String settingKey = message["setting_key"];
|
String settingKey = message["setting_key"];
|
||||||
var source = ComicSource.find(key)!;
|
var source = ComicSource.find(key)!;
|
||||||
return source.data["setting"]?[settingKey] ??
|
return source.data["settings"]?[settingKey] ??
|
||||||
source.settings?[settingKey]['default'] ??
|
source.settings?[settingKey]['default'] ??
|
||||||
(throw "Setting not found: $settingKey");
|
(throw "Setting not found: $settingKey");
|
||||||
}
|
}
|
||||||
@@ -236,8 +233,14 @@ mixin class _JSEngineApi {
|
|||||||
Object? handleHtmlCallback(Map<String, dynamic> data) {
|
Object? handleHtmlCallback(Map<String, dynamic> data) {
|
||||||
switch (data["function"]) {
|
switch (data["function"]) {
|
||||||
case "parse":
|
case "parse":
|
||||||
if (_documents.length > 2) {
|
if (_documents.length > 8) {
|
||||||
_documents.remove(_documents.keys.first);
|
var shouldDelete = _documents.keys.first;
|
||||||
|
Log.warning(
|
||||||
|
"JS Engine",
|
||||||
|
"Too many documents, deleting the oldest: $shouldDelete\n"
|
||||||
|
"Current documents: ${_documents.keys}",
|
||||||
|
);
|
||||||
|
_documents.remove(shouldDelete);
|
||||||
}
|
}
|
||||||
_documents[data["key"]] = DocumentWrapper.parse(data["data"]);
|
_documents[data["key"]] = DocumentWrapper.parse(data["data"]);
|
||||||
return null;
|
return null;
|
||||||
@@ -286,6 +289,12 @@ mixin class _JSEngineApi {
|
|||||||
return _documents[data["doc"]]!.getId(data["key"]);
|
return _documents[data["doc"]]!.getId(data["key"]);
|
||||||
case "getLocalName":
|
case "getLocalName":
|
||||||
return _documents[data["doc"]]!.getLocalName(data["key"]);
|
return _documents[data["doc"]]!.getLocalName(data["key"]);
|
||||||
|
case "getElementById":
|
||||||
|
return _documents[data["key"]]!.getElementById(data["id"]);
|
||||||
|
case "getPreviousSibling":
|
||||||
|
return _documents[data["doc"]]!.getPreviousSibling(data["key"]);
|
||||||
|
case "getNextSibling":
|
||||||
|
return _documents[data["doc"]]!.getNextSibling(data["key"]);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -593,4 +602,25 @@ class DocumentWrapper {
|
|||||||
String? getLocalName(int key) {
|
String? getLocalName(int key) {
|
||||||
return (elements[key]).localName;
|
return (elements[key]).localName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int? getElementById(String id) {
|
||||||
|
var element = doc.getElementById(id);
|
||||||
|
if (element == null) return null;
|
||||||
|
elements.add(element);
|
||||||
|
return elements.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? getPreviousSibling(int key) {
|
||||||
|
var res = elements[key].previousElementSibling;
|
||||||
|
if (res == null) return null;
|
||||||
|
elements.add(res);
|
||||||
|
return elements.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? getNextSibling(int key) {
|
||||||
|
var res = elements[key].nextElementSibling;
|
||||||
|
if (res == null) return null;
|
||||||
|
elements.add(res);
|
||||||
|
return elements.length - 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -127,6 +127,9 @@ class LocalComic with HistoryMixin implements Comic {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String? get favoriteId => null;
|
String? get favoriteId => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? get stars => null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalManager with ChangeNotifier {
|
class LocalManager with ChangeNotifier {
|
||||||
|
@@ -6,9 +6,12 @@ import 'package:dio/io.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
|
import 'package:venera/network/cache.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
|
|
||||||
import '../foundation/app.dart';
|
import '../foundation/app.dart';
|
||||||
|
import 'cloudflare.dart';
|
||||||
|
import 'cookie_jar.dart';
|
||||||
|
|
||||||
export 'package:dio/dio.dart';
|
export 'package:dio/dio.dart';
|
||||||
|
|
||||||
@@ -107,6 +110,9 @@ class AppDio with DioMixin {
|
|||||||
this.options = options ?? BaseOptions();
|
this.options = options ?? BaseOptions();
|
||||||
interceptors.add(MyLogInterceptor());
|
interceptors.add(MyLogInterceptor());
|
||||||
httpClientAdapter = IOHttpClientAdapter(createHttpClient: createHttpClient);
|
httpClientAdapter = IOHttpClientAdapter(createHttpClient: createHttpClient);
|
||||||
|
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
||||||
|
interceptors.add(NetworkCacheManager());
|
||||||
|
interceptors.add(CloudflareInterceptor());
|
||||||
}
|
}
|
||||||
|
|
||||||
static HttpClient createHttpClient() {
|
static HttpClient createHttpClient() {
|
||||||
|
197
lib/network/cache.dart
Normal file
197
lib/network/cache.dart
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
class NetworkCache {
|
||||||
|
final Uri uri;
|
||||||
|
|
||||||
|
final Map<String, dynamic> requestHeaders;
|
||||||
|
|
||||||
|
final Map<String, List<String>> responseHeaders;
|
||||||
|
|
||||||
|
final Object? data;
|
||||||
|
|
||||||
|
final DateTime time;
|
||||||
|
|
||||||
|
final int size;
|
||||||
|
|
||||||
|
const NetworkCache({
|
||||||
|
required this.uri,
|
||||||
|
required this.requestHeaders,
|
||||||
|
required this.responseHeaders,
|
||||||
|
required this.data,
|
||||||
|
required this.time,
|
||||||
|
required this.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class NetworkCacheManager implements Interceptor {
|
||||||
|
NetworkCacheManager._();
|
||||||
|
|
||||||
|
static final NetworkCacheManager instance = NetworkCacheManager._();
|
||||||
|
|
||||||
|
factory NetworkCacheManager() => instance;
|
||||||
|
|
||||||
|
final Map<Uri, NetworkCache> _cache = {};
|
||||||
|
|
||||||
|
int size = 0;
|
||||||
|
|
||||||
|
NetworkCache? getCache(Uri uri) {
|
||||||
|
return _cache[uri];
|
||||||
|
}
|
||||||
|
|
||||||
|
static const _maxCacheSize = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
void setCache(NetworkCache cache) {
|
||||||
|
while(size > _maxCacheSize){
|
||||||
|
size -= _cache.values.first.size;
|
||||||
|
_cache.remove(_cache.keys.first);
|
||||||
|
}
|
||||||
|
_cache[cache.uri] = cache;
|
||||||
|
size += cache.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeCache(Uri uri) {
|
||||||
|
var cache = _cache[uri];
|
||||||
|
if(cache != null){
|
||||||
|
size -= cache.size;
|
||||||
|
}
|
||||||
|
_cache.remove(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
_cache.clear();
|
||||||
|
size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var preventParallel = <Uri, Completer>{};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||||
|
if(err.requestOptions.method != "GET"){
|
||||||
|
return handler.next(err);
|
||||||
|
}
|
||||||
|
if(preventParallel[err.requestOptions.uri] != null){
|
||||||
|
preventParallel[err.requestOptions.uri]!.complete();
|
||||||
|
preventParallel.remove(err.requestOptions.uri);
|
||||||
|
}
|
||||||
|
return handler.next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onRequest(
|
||||||
|
RequestOptions options, RequestInterceptorHandler handler) async {
|
||||||
|
if(options.method != "GET"){
|
||||||
|
return handler.next(options);
|
||||||
|
}
|
||||||
|
if(preventParallel[options.uri] != null){
|
||||||
|
await preventParallel[options.uri]!.future;
|
||||||
|
}
|
||||||
|
var cache = getCache(options.uri);
|
||||||
|
if (cache == null || !compareHeaders(options.headers, cache.requestHeaders)) {
|
||||||
|
if(options.headers['cache-time'] != null){
|
||||||
|
options.headers.remove('cache-time');
|
||||||
|
}
|
||||||
|
if(options.headers['prevent-parallel'] != null){
|
||||||
|
options.headers.remove('prevent-parallel');
|
||||||
|
preventParallel[options.uri] = Completer();
|
||||||
|
}
|
||||||
|
return handler.next(options);
|
||||||
|
} else {
|
||||||
|
if(options.headers['cache-time'] == 'no'){
|
||||||
|
options.headers.remove('cache-time');
|
||||||
|
removeCache(options.uri);
|
||||||
|
return handler.next(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var time = DateTime.now();
|
||||||
|
var diff = time.difference(cache.time);
|
||||||
|
if (options.headers['cache-time'] == 'long'
|
||||||
|
&& diff < const Duration(hours: 2)) {
|
||||||
|
return handler.resolve(Response(
|
||||||
|
requestOptions: options,
|
||||||
|
data: cache.data,
|
||||||
|
headers: Headers.fromMap(cache.responseHeaders),
|
||||||
|
statusCode: 200,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else if (diff < const Duration(seconds: 5)) {
|
||||||
|
return handler.resolve(Response(
|
||||||
|
requestOptions: options,
|
||||||
|
data: cache.data,
|
||||||
|
headers: Headers.fromMap(cache.responseHeaders),
|
||||||
|
statusCode: 200,
|
||||||
|
));
|
||||||
|
} else if (diff < const Duration(hours: 1)) {
|
||||||
|
var o = options.copyWith(
|
||||||
|
method: "HEAD",
|
||||||
|
);
|
||||||
|
var dio = Dio();
|
||||||
|
var response = await dio.fetch(o);
|
||||||
|
if (response.statusCode == 200 &&
|
||||||
|
compareHeaders(cache.responseHeaders, response.headers.map)) {
|
||||||
|
return handler.resolve(Response(
|
||||||
|
requestOptions: options,
|
||||||
|
data: cache.data,
|
||||||
|
headers: Headers.fromMap(cache.responseHeaders),
|
||||||
|
statusCode: 200,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeCache(options.uri);
|
||||||
|
handler.next(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool compareHeaders(Map<String, dynamic> a, Map<String, dynamic> b) {
|
||||||
|
if (a.length != b.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (var key in a.keys) {
|
||||||
|
if (a[key] != b[key]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onResponse(
|
||||||
|
Response<dynamic> response, ResponseInterceptorHandler handler) {
|
||||||
|
if (response.requestOptions.method != "GET") {
|
||||||
|
return handler.next(response);
|
||||||
|
}
|
||||||
|
var size = _calculateSize(response.data);
|
||||||
|
if(size != null && size < 1024 * 1024) {
|
||||||
|
var cache = NetworkCache(
|
||||||
|
uri: response.requestOptions.uri,
|
||||||
|
requestHeaders: response.requestOptions.headers,
|
||||||
|
responseHeaders: response.headers.map,
|
||||||
|
data: response.data,
|
||||||
|
time: DateTime.now(),
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
setCache(cache);
|
||||||
|
}
|
||||||
|
if(preventParallel[response.requestOptions.uri] != null){
|
||||||
|
preventParallel[response.requestOptions.uri]!.complete();
|
||||||
|
preventParallel.remove(response.requestOptions.uri);
|
||||||
|
}
|
||||||
|
handler.next(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int? _calculateSize(Object? data){
|
||||||
|
if(data == null){
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if(data is List<int>) {
|
||||||
|
return data.length;
|
||||||
|
}
|
||||||
|
if(data is String) {
|
||||||
|
return data.length * 4;
|
||||||
|
}
|
||||||
|
if(data is Map) {
|
||||||
|
return data.toString().length * 4;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@@ -202,6 +202,9 @@ class CookieManagerSql extends Interceptor {
|
|||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
var cookies = cookieJar.loadForRequestCookieHeader(options.uri);
|
var cookies = cookieJar.loadForRequestCookieHeader(options.uri);
|
||||||
if (cookies.isNotEmpty) {
|
if (cookies.isNotEmpty) {
|
||||||
|
if(options.headers["cookie"] != null) {
|
||||||
|
cookies = "${options.headers["cookie"]}; $cookies";
|
||||||
|
}
|
||||||
options.headers["cookie"] = cookies;
|
options.headers["cookie"] = cookies;
|
||||||
}
|
}
|
||||||
handler.next(options);
|
handler.next(options);
|
||||||
|
@@ -7,7 +7,8 @@ import 'package:venera/foundation/consts.dart';
|
|||||||
import 'app_dio.dart';
|
import 'app_dio.dart';
|
||||||
|
|
||||||
class ImageDownloader {
|
class ImageDownloader {
|
||||||
static Stream<ImageDownloadProgress> loadThumbnail(String url, String? sourceKey) async* {
|
static Stream<ImageDownloadProgress> loadThumbnail(
|
||||||
|
String url, String? sourceKey) async* {
|
||||||
final cacheKey = "$url@$sourceKey";
|
final cacheKey = "$url@$sourceKey";
|
||||||
final cache = await CacheManager().findCache(cacheKey);
|
final cache = await CacheManager().findCache(cacheKey);
|
||||||
|
|
||||||
@@ -25,12 +26,14 @@ class ImageDownloader {
|
|||||||
var comicSource = ComicSource.find(sourceKey);
|
var comicSource = ComicSource.find(sourceKey);
|
||||||
configs = comicSource!.getThumbnailLoadingConfig?.call(url) ?? {};
|
configs = comicSource!.getThumbnailLoadingConfig?.call(url) ?? {};
|
||||||
}
|
}
|
||||||
configs['headers'] ??= {
|
configs['headers'] ??= {};
|
||||||
'user-agent': webUA,
|
if(configs['headers']['user-agent'] == null
|
||||||
};
|
&& configs['headers']['User-Agent'] == null) {
|
||||||
|
configs['headers']['user-agent'] = webUA;
|
||||||
|
}
|
||||||
|
|
||||||
var dio = AppDio(BaseOptions(
|
var dio = AppDio(BaseOptions(
|
||||||
headers: configs['headers'],
|
headers: Map<String, dynamic>.from(configs['headers']),
|
||||||
method: configs['method'] ?? 'GET',
|
method: configs['method'] ?? 'GET',
|
||||||
responseType: ResponseType.stream,
|
responseType: ResponseType.stream,
|
||||||
));
|
));
|
||||||
@@ -53,7 +56,7 @@ class ImageDownloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(configs['onResponse'] != null) {
|
if (configs['onResponse'] != null) {
|
||||||
buffer = configs['onResponse'](buffer);
|
buffer = configs['onResponse'](buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +68,8 @@ class ImageDownloader {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Stream<ImageDownloadProgress> loadComicImage(String imageKey, String? sourceKey, String cid, String eid) async* {
|
static Stream<ImageDownloadProgress> loadComicImage(
|
||||||
|
String imageKey, String? sourceKey, String cid, String eid) async* {
|
||||||
final cacheKey = "$imageKey@$sourceKey@$cid@$eid";
|
final cacheKey = "$imageKey@$sourceKey@$cid@$eid";
|
||||||
final cache = await CacheManager().findCache(cacheKey);
|
final cache = await CacheManager().findCache(cacheKey);
|
||||||
|
|
||||||
@@ -81,7 +85,8 @@ class ImageDownloader {
|
|||||||
var configs = <String, dynamic>{};
|
var configs = <String, dynamic>{};
|
||||||
if (sourceKey != null) {
|
if (sourceKey != null) {
|
||||||
var comicSource = ComicSource.find(sourceKey);
|
var comicSource = ComicSource.find(sourceKey);
|
||||||
configs = comicSource!.getImageLoadingConfig?.call(imageKey, cid, eid) ?? {};
|
configs = (await comicSource!.getImageLoadingConfig
|
||||||
|
?.call(imageKey, cid, eid)) ?? {};
|
||||||
}
|
}
|
||||||
configs['headers'] ??= {
|
configs['headers'] ??= {
|
||||||
'user-agent': webUA,
|
'user-agent': webUA,
|
||||||
@@ -111,7 +116,7 @@ class ImageDownloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(configs['onResponse'] != null) {
|
if (configs['onResponse'] != null) {
|
||||||
buffer = configs['onResponse'](buffer);
|
buffer = configs['onResponse'](buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
@@ -158,6 +159,8 @@ class _LoginPageState extends State<_LoginPage> {
|
|||||||
String password = "";
|
String password = "";
|
||||||
bool loading = false;
|
bool loading = false;
|
||||||
|
|
||||||
|
final Map<String, String> _cookies = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -173,6 +176,7 @@ class _LoginPageState extends State<_LoginPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text("Login".tl, style: const TextStyle(fontSize: 24)),
|
Text("Login".tl, style: const TextStyle(fontSize: 24)),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
if (widget.config.cookieFields == null)
|
||||||
TextField(
|
TextField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Username".tl,
|
labelText: "Username".tl,
|
||||||
@@ -182,8 +186,8 @@ class _LoginPageState extends State<_LoginPage> {
|
|||||||
onChanged: (s) {
|
onChanged: (s) {
|
||||||
username = s;
|
username = s;
|
||||||
},
|
},
|
||||||
),
|
).paddingBottom(16),
|
||||||
const SizedBox(height: 16),
|
if (widget.config.cookieFields == null)
|
||||||
TextField(
|
TextField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Password".tl,
|
labelText: "Password".tl,
|
||||||
@@ -195,9 +199,21 @@ class _LoginPageState extends State<_LoginPage> {
|
|||||||
password = s;
|
password = s;
|
||||||
},
|
},
|
||||||
onSubmitted: (s) => login(),
|
onSubmitted: (s) => login(),
|
||||||
|
).paddingBottom(16),
|
||||||
|
for (var field in widget.config.cookieFields ?? <String>[])
|
||||||
|
TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: field,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
obscureText: true,
|
||||||
if (widget.config.login == null)
|
enabled: widget.config.validateCookies != null,
|
||||||
|
onChanged: (s) {
|
||||||
|
_cookies[field] = s;
|
||||||
|
},
|
||||||
|
).paddingBottom(16),
|
||||||
|
if (widget.config.login == null &&
|
||||||
|
widget.config.cookieFields == null)
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -214,7 +230,7 @@ class _LoginPageState extends State<_LoginPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
if (widget.config.loginWebsite != null)
|
if (widget.config.loginWebsite != null)
|
||||||
FilledButton(
|
TextButton(
|
||||||
onPressed: loginWithWebview,
|
onPressed: loginWithWebview,
|
||||||
child: Text("Login with webview".tl),
|
child: Text("Login with webview".tl),
|
||||||
),
|
),
|
||||||
@@ -240,6 +256,7 @@ class _LoginPageState extends State<_LoginPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void login() {
|
void login() {
|
||||||
|
if (widget.config.login != null) {
|
||||||
if (username.isEmpty || password.isEmpty) {
|
if (username.isEmpty || password.isEmpty) {
|
||||||
showToast(
|
showToast(
|
||||||
message: "Cannot be empty".tl,
|
message: "Cannot be empty".tl,
|
||||||
@@ -263,48 +280,57 @@ class _LoginPageState extends State<_LoginPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if (widget.config.validateCookies != null) {
|
||||||
|
setState(() {
|
||||||
|
loading = true;
|
||||||
|
});
|
||||||
|
var cookies =
|
||||||
|
widget.config.cookieFields!.map((e) => _cookies[e] ?? '').toList();
|
||||||
|
widget.config.validateCookies!(cookies).then((value) {
|
||||||
|
if (value) {
|
||||||
|
widget.source.data['account'] = 'ok';
|
||||||
|
widget.source.saveData();
|
||||||
|
context.pop();
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: "Invalid cookies".tl);
|
||||||
|
setState(() {
|
||||||
|
loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void loginWithWebview() async {
|
void loginWithWebview() async {
|
||||||
var url = widget.config.loginWebsite!;
|
var url = widget.config.loginWebsite!;
|
||||||
var title = '';
|
var title = '';
|
||||||
bool success = false;
|
bool success = false;
|
||||||
|
|
||||||
|
void validate(InAppWebViewController c) async {
|
||||||
|
if (widget.config.checkLoginStatus != null
|
||||||
|
&& widget.config.checkLoginStatus!(url, title)) {
|
||||||
|
var cookies = (await c.getCookies(url)) ?? [];
|
||||||
|
SingleInstanceCookieJar.instance?.saveFromResponse(
|
||||||
|
Uri.parse(url),
|
||||||
|
cookies,
|
||||||
|
);
|
||||||
|
success = true;
|
||||||
|
widget.config.onLoginWithWebviewSuccess?.call();
|
||||||
|
App.mainNavigatorKey?.currentContext?.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await context.to(
|
await context.to(
|
||||||
() => AppWebview(
|
() => AppWebview(
|
||||||
initialUrl: widget.config.loginWebsite!,
|
initialUrl: widget.config.loginWebsite!,
|
||||||
onNavigation: (u, c) {
|
onNavigation: (u, c) {
|
||||||
url = u;
|
url = u;
|
||||||
print(url);
|
validate(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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
onTitleChange: (t, c) {
|
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;
|
title = t;
|
||||||
|
validate(c);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -352,6 +352,18 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text("Information".tl),
|
title: Text("Information".tl),
|
||||||
),
|
),
|
||||||
|
if (comic.stars != null)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
StarRating(
|
||||||
|
value: comic.stars!,
|
||||||
|
size: 24,
|
||||||
|
onTap: starRating,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(comic.stars!.toStringAsFixed(2)),
|
||||||
|
],
|
||||||
|
).paddingLeft(16).paddingVertical(8),
|
||||||
for (var e in comic.tags.entries)
|
for (var e in comic.tags.entries)
|
||||||
buildWrap(
|
buildWrap(
|
||||||
children: [
|
children: [
|
||||||
@@ -641,6 +653,72 @@ abstract mixin class _ComicPageActions {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void starRating() {
|
||||||
|
if (!comicSource.isLogged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var rating = 0.0;
|
||||||
|
var isLoading = false;
|
||||||
|
showDialog(
|
||||||
|
context: App.rootContext,
|
||||||
|
builder: (dialogContext) => StatefulBuilder(
|
||||||
|
builder: (context, setState) => SimpleDialog(
|
||||||
|
title: const Text("Rating"),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 100,
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 210,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
RatingWidget(
|
||||||
|
padding: 2,
|
||||||
|
onRatingUpdate: (value) => rating = value,
|
||||||
|
value: 1,
|
||||||
|
selectable: true,
|
||||||
|
size: 40,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Button.filled(
|
||||||
|
isLoading: isLoading,
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
isLoading = true;
|
||||||
|
});
|
||||||
|
comicSource.starRatingFunc!
|
||||||
|
(comic.id, rating.round())
|
||||||
|
.then((value) {
|
||||||
|
if (value.success) {
|
||||||
|
App.rootContext
|
||||||
|
.showMessage(message: "Success".tl);
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
} else {
|
||||||
|
App.rootContext
|
||||||
|
.showMessage(message: value.errorMessage!);
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text("Submit".tl),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ActionButton extends StatelessWidget {
|
class _ActionButton extends StatelessWidget {
|
||||||
@@ -880,11 +958,34 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverGrid(
|
SliverGrid(
|
||||||
delegate: SliverChildBuilderDelegate(childCount: thumbnails.length,
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
childCount: thumbnails.length,
|
||||||
(context, index) {
|
(context, index) {
|
||||||
if (index == thumbnails.length - 1 && error == null) {
|
if (index == thumbnails.length - 1 && error == null) {
|
||||||
loadNext();
|
loadNext();
|
||||||
}
|
}
|
||||||
|
var url = thumbnails[index];
|
||||||
|
ImagePart? part;
|
||||||
|
if (url.contains('@')) {
|
||||||
|
var params = url.split('@')[1].split('&');
|
||||||
|
url = url.split('@')[0];
|
||||||
|
double? x1, y1, x2, y2;
|
||||||
|
try {
|
||||||
|
for (var p in params) {
|
||||||
|
if (p.startsWith('x')) {
|
||||||
|
var r = p.split('=')[1];
|
||||||
|
x1 = double.parse(r.split('-')[0]);
|
||||||
|
x2 = double.parse(r.split('-')[1]);
|
||||||
|
}
|
||||||
|
if (p.startsWith('y')) {
|
||||||
|
var r = p.split('=')[1];
|
||||||
|
y1 = double.parse(r.split('-')[0]);
|
||||||
|
y2 = double.parse(r.split('-')[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {}
|
||||||
|
part = ImagePart(x1: x1, y1: y1, x2: x2, y2: y2);
|
||||||
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: context.width < changePoint
|
padding: context.width < changePoint
|
||||||
? const EdgeInsets.all(4)
|
? const EdgeInsets.all(4)
|
||||||
@@ -895,7 +996,8 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => state.read(null, index + 1),
|
onTap: () => state.read(null, index + 1),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
borderRadius:
|
||||||
|
const BorderRadius.all(Radius.circular(16)),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius:
|
borderRadius:
|
||||||
@@ -911,12 +1013,13 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
|
|||||||
const BorderRadius.all(Radius.circular(16)),
|
const BorderRadius.all(Radius.circular(16)),
|
||||||
child: AnimatedImage(
|
child: AnimatedImage(
|
||||||
image: CachedImageProvider(
|
image: CachedImageProvider(
|
||||||
thumbnails[index],
|
url,
|
||||||
sourceKey: state.widget.sourceKey,
|
sourceKey: state.widget.sourceKey,
|
||||||
),
|
),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
|
part: part,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -929,13 +1032,14 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: 200,
|
maxCrossAxisExtent: 200,
|
||||||
childAspectRatio: 0.65,
|
childAspectRatio: 0.65,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if(error != null)
|
if (error != null)
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -1288,7 +1392,8 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
});
|
});
|
||||||
var res = await widget.comicSource.favoriteData!.addOrDelFavorite!(
|
var res =
|
||||||
|
await widget.comicSource.favoriteData!.addOrDelFavorite!(
|
||||||
widget.cid,
|
widget.cid,
|
||||||
selected!,
|
selected!,
|
||||||
!addedFolders.contains(selected!),
|
!addedFolders.contains(selected!),
|
||||||
|
@@ -267,7 +267,7 @@ class _CommentTileState extends State<_CommentTile> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(widget.comment.userName),
|
Text(widget.comment.userName, style: ts.bold,),
|
||||||
if (widget.comment.time != null)
|
if (widget.comment.time != null)
|
||||||
Text(widget.comment.time!, style: ts.s12),
|
Text(widget.comment.time!, style: ts.s12),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
@@ -403,38 +403,45 @@ class _CommentTileState extends State<_CommentTile> {
|
|||||||
|
|
||||||
int? voteStatus;
|
int? voteStatus;
|
||||||
|
|
||||||
bool isVoteUp = false;
|
bool isVotingUp = false;
|
||||||
|
|
||||||
bool isVoteDown = false;
|
bool isVotingDown = false;
|
||||||
|
|
||||||
void vote(bool isUp) async {
|
void vote(bool isUp) async {
|
||||||
if (isVoteUp || isVoteDown) return;
|
if (isVotingUp || isVotingDown) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
if (isUp) {
|
if (isUp) {
|
||||||
isVoteUp = true;
|
isVotingUp = true;
|
||||||
} else {
|
} else {
|
||||||
isVoteDown = true;
|
isVotingDown = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
var isCancel = (isUp && voteStatus == 1) || (!isUp && voteStatus == -1);
|
||||||
var res = await widget.source.voteCommentFunc!(
|
var res = await widget.source.voteCommentFunc!(
|
||||||
widget.comic.comicId,
|
widget.comic.comicId,
|
||||||
widget.comic.subId,
|
widget.comic.subId,
|
||||||
widget.comment.id!,
|
widget.comment.id!,
|
||||||
isUp,
|
isUp,
|
||||||
(isUp && voteStatus == 1) || (!isUp && voteStatus == -1),
|
isCancel,
|
||||||
);
|
);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
|
if(isCancel) {
|
||||||
|
voteStatus = 0;
|
||||||
|
} else {
|
||||||
if (isUp) {
|
if (isUp) {
|
||||||
voteStatus = 1;
|
voteStatus = 1;
|
||||||
} else {
|
} else {
|
||||||
voteStatus = -1;
|
voteStatus = -1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
widget.comment.voteStatus = voteStatus;
|
||||||
|
widget.comment.score = res.data ?? widget.comment.score;
|
||||||
} else {
|
} else {
|
||||||
context.showMessage(message: res.errorMessage ?? "Error");
|
context.showMessage(message: res.errorMessage ?? "Error");
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
isVoteUp = false;
|
isVotingUp = false;
|
||||||
isVoteDown = false;
|
isVotingDown = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,7 +468,7 @@ class _CommentTileState extends State<_CommentTile> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Button.icon(
|
Button.icon(
|
||||||
isLoading: isVoteUp,
|
isLoading: isVotingUp,
|
||||||
icon: const Icon(Icons.arrow_upward),
|
icon: const Icon(Icons.arrow_upward),
|
||||||
size: 18,
|
size: 18,
|
||||||
color: upColor,
|
color: upColor,
|
||||||
@@ -471,7 +478,7 @@ class _CommentTileState extends State<_CommentTile> {
|
|||||||
Text(widget.comment.score.toString()),
|
Text(widget.comment.score.toString()),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Button.icon(
|
Button.icon(
|
||||||
isLoading: isVoteDown,
|
isLoading: isVotingDown,
|
||||||
icon: const Icon(Icons.arrow_downward),
|
icon: const Icon(Icons.arrow_downward),
|
||||||
size: 18,
|
size: 18,
|
||||||
color: downColor,
|
color: downColor,
|
||||||
|
@@ -179,7 +179,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (data.loadMultiPart != null) {
|
if (data.loadMultiPart != null) {
|
||||||
return buildMultiPart();
|
return buildMultiPart();
|
||||||
} else if (data.loadPage != null) {
|
} else if (data.loadPage != null || data.loadNext != null) {
|
||||||
return buildComicList();
|
return buildComicList();
|
||||||
} else if (data.loadMixed != null) {
|
} else if (data.loadMixed != null) {
|
||||||
return _MixedExplorePage(
|
return _MixedExplorePage(
|
||||||
@@ -196,7 +196,8 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> {
|
|||||||
|
|
||||||
Widget buildComicList() {
|
Widget buildComicList() {
|
||||||
return ComicList(
|
return ComicList(
|
||||||
loadPage: data.loadPage!,
|
loadPage: data.loadPage,
|
||||||
|
loadNext: data.loadNext,
|
||||||
key: ValueKey(key),
|
key: ValueKey(key),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -112,7 +112,8 @@ class _NormalFavoritePage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
title: Text(data.title),
|
title: Text(data.title),
|
||||||
),
|
),
|
||||||
loadPage: (i) => data.loadComic(i),
|
loadPage: data.loadComic == null ? null : (i) => data.loadComic!(i),
|
||||||
|
loadNext: data.loadNext == null ? null : (next) => data.loadNext!(next),
|
||||||
menuBuilder: (comic) {
|
menuBuilder: (comic) {
|
||||||
return [
|
return [
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
@@ -393,7 +394,8 @@ class _FolderTile extends StatelessWidget {
|
|||||||
return StatefulBuilder(builder: (context, setState) {
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Delete".tl,
|
title: "Delete".tl,
|
||||||
content: Text("Are you sure you want to delete this folder?".tl).paddingHorizontal(16),
|
content: Text("Are you sure you want to delete this folder?".tl)
|
||||||
|
.paddingHorizontal(16),
|
||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
isLoading: loading,
|
isLoading: loading,
|
||||||
@@ -516,7 +518,11 @@ class _FavoriteFolder extends StatelessWidget {
|
|||||||
errorLeading: Appbar(
|
errorLeading: Appbar(
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
),
|
),
|
||||||
loadPage: (i) => data.loadComic(i, folderID),
|
loadPage:
|
||||||
|
data.loadComic == null ? null : (i) => data.loadComic!(i, folderID),
|
||||||
|
loadNext: data.loadNext == null
|
||||||
|
? null
|
||||||
|
: (next) => data.loadNext!(next, folderID),
|
||||||
menuBuilder: (comic) {
|
menuBuilder: (comic) {
|
||||||
return [
|
return [
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
|
@@ -46,7 +46,12 @@ class _RankingPageState extends State<RankingPage> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ComicList(
|
child: ComicList(
|
||||||
loadPage: (i) => data.rankingData!.load(optionValue, i),
|
loadPage: data.rankingData!.load == null
|
||||||
|
? null
|
||||||
|
: (i) => data.rankingData!.load!(optionValue, i),
|
||||||
|
loadNext: data.rankingData!.loadWithNext == null
|
||||||
|
? null
|
||||||
|
: (i) => data.rankingData!.loadWithNext!(optionValue, i),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
@@ -24,6 +26,8 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
|
|
||||||
String searchTarget = "";
|
String searchTarget = "";
|
||||||
|
|
||||||
|
var focusNode = FocusNode();
|
||||||
|
|
||||||
var options = <String>[];
|
var options = <String>[];
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
@@ -137,6 +141,12 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -152,6 +162,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
onChanged: (s) {
|
onChanged: (s) {
|
||||||
findSuggestions();
|
findSuggestions();
|
||||||
},
|
},
|
||||||
|
focusNode: focusNode,
|
||||||
);
|
);
|
||||||
if (suggestions.isNotEmpty) {
|
if (suggestions.isNotEmpty) {
|
||||||
yield buildSuggestions(context);
|
yield buildSuggestions(context);
|
||||||
@@ -186,6 +197,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
searchTarget = e.key;
|
searchTarget = e.key;
|
||||||
|
useDefaultOptions();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -197,6 +209,13 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void useDefaultOptions() {
|
||||||
|
final searchOptions =
|
||||||
|
ComicSource.find(searchTarget)!.searchPageData!.searchOptions ??
|
||||||
|
<SearchOptions>[];
|
||||||
|
options = searchOptions.map((e) => e.defaultValue).toList();
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildSearchOptions() {
|
Widget buildSearchOptions() {
|
||||||
var children = <Widget>[];
|
var children = <Widget>[];
|
||||||
|
|
||||||
@@ -204,30 +223,21 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
ComicSource.find(searchTarget)!.searchPageData!.searchOptions ??
|
ComicSource.find(searchTarget)!.searchPageData!.searchOptions ??
|
||||||
<SearchOptions>[];
|
<SearchOptions>[];
|
||||||
if (searchOptions.length != options.length) {
|
if (searchOptions.length != options.length) {
|
||||||
options = searchOptions.map((e) => e.defaultValue).toList();
|
useDefaultOptions();
|
||||||
}
|
}
|
||||||
if (searchOptions.isEmpty) {
|
if (searchOptions.isEmpty) {
|
||||||
return const SliverToBoxAdapter(child: SizedBox());
|
return const SliverToBoxAdapter(child: SizedBox());
|
||||||
}
|
}
|
||||||
for (int i = 0; i < searchOptions.length; i++) {
|
for (int i = 0; i < searchOptions.length; i++) {
|
||||||
final option = searchOptions[i];
|
final option = searchOptions[i];
|
||||||
children.add(ListTile(
|
children.add(SearchOptionWidget(
|
||||||
contentPadding: EdgeInsets.zero,
|
option: option,
|
||||||
title: Text(option.label.tl),
|
value: options[i],
|
||||||
));
|
onChanged: (value) {
|
||||||
children.add(Wrap(
|
options[i] = value;
|
||||||
runSpacing: 8,
|
|
||||||
spacing: 8,
|
|
||||||
children: option.options.entries.map((e) {
|
|
||||||
return OptionChip(
|
|
||||||
text: e.value.ts(searchTarget),
|
|
||||||
isSelected: options[i] == e.key,
|
|
||||||
onTap: () {
|
|
||||||
options[i] = e.key;
|
|
||||||
update();
|
update();
|
||||||
},
|
},
|
||||||
);
|
sourceKey: searchTarget,
|
||||||
}).toList(),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,7 +346,9 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
} else {
|
} else {
|
||||||
controller.text += "$text ";
|
controller.text += "$text ";
|
||||||
}
|
}
|
||||||
|
suggestions.clear();
|
||||||
update();
|
update();
|
||||||
|
focusNode.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool showMethod = MediaQuery.of(context).size.width < 600;
|
bool showMethod = MediaQuery.of(context).size.width < 600;
|
||||||
@@ -444,3 +456,77 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SearchOptionWidget extends StatelessWidget {
|
||||||
|
const SearchOptionWidget({
|
||||||
|
super.key,
|
||||||
|
required this.option,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
required this.sourceKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SearchOptions option;
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
final void Function(String) onChanged;
|
||||||
|
|
||||||
|
final String sourceKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(option.label.ts(sourceKey)),
|
||||||
|
),
|
||||||
|
if(option.type == 'select')
|
||||||
|
Wrap(
|
||||||
|
runSpacing: 8,
|
||||||
|
spacing: 8,
|
||||||
|
children: option.options.entries.map((e) {
|
||||||
|
return OptionChip(
|
||||||
|
text: e.value.ts(sourceKey),
|
||||||
|
isSelected: value == e.key,
|
||||||
|
onTap: () {
|
||||||
|
onChanged(e.key);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
if(option.type == 'multi-select')
|
||||||
|
Wrap(
|
||||||
|
runSpacing: 8,
|
||||||
|
spacing: 8,
|
||||||
|
children: option.options.entries.map((e) {
|
||||||
|
return OptionChip(
|
||||||
|
text: e.value.ts(sourceKey),
|
||||||
|
isSelected: (jsonDecode(value) as List).contains(e.key),
|
||||||
|
onTap: () {
|
||||||
|
var list = jsonDecode(value) as List;
|
||||||
|
if(list.contains(e.key)) {
|
||||||
|
list.remove(e.key);
|
||||||
|
} else {
|
||||||
|
list.add(e.key);
|
||||||
|
}
|
||||||
|
onChanged(jsonEncode(list));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
if(option.type == 'dropdown')
|
||||||
|
Select(
|
||||||
|
current: option.options[value],
|
||||||
|
values: option.options.values.toList(),
|
||||||
|
onTap: (index) {
|
||||||
|
onChanged(option.options.keys.elementAt(index));
|
||||||
|
},
|
||||||
|
minWidth: 96,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import 'package:venera/foundation/app.dart';
|
|||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.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/pages/search_page.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/tags_translation.dart';
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
@@ -87,12 +88,27 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
|||||||
);
|
);
|
||||||
sourceKey = widget.sourceKey;
|
sourceKey = widget.sourceKey;
|
||||||
options = widget.options;
|
options = widget.options;
|
||||||
|
validateOptions();
|
||||||
text = widget.text;
|
text = widget.text;
|
||||||
appdata.addSearchHistory(text);
|
appdata.addSearchHistory(text);
|
||||||
suggestionsController = _SuggestionsController(controller);
|
suggestionsController = _SuggestionsController(controller);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void validateOptions() {
|
||||||
|
var source = ComicSource.find(sourceKey);
|
||||||
|
if (source == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var searchOptions = source.searchPageData!.searchOptions;
|
||||||
|
if (searchOptions == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options.length != searchOptions.length) {
|
||||||
|
options = searchOptions.map((e) => e.defaultValue).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ComicList(
|
return ComicList(
|
||||||
@@ -422,25 +438,15 @@ class _SearchSettingsDialogState extends State<_SearchSettingsDialog> {
|
|||||||
}
|
}
|
||||||
for (int i = 0; i < searchOptions.length; i++) {
|
for (int i = 0; i < searchOptions.length; i++) {
|
||||||
final option = searchOptions[i];
|
final option = searchOptions[i];
|
||||||
children.add(ListTile(
|
children.add(SearchOptionWidget(
|
||||||
contentPadding: EdgeInsets.zero,
|
option: option,
|
||||||
title: Text(option.label.tl),
|
value: options[i],
|
||||||
));
|
onChanged: (value) {
|
||||||
children.add(Wrap(
|
|
||||||
runSpacing: 8,
|
|
||||||
spacing: 8,
|
|
||||||
children: option.options.entries.map((e) {
|
|
||||||
return OptionChip(
|
|
||||||
text: e.value.ts(searchTarget),
|
|
||||||
isSelected: options[i] == e.key,
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
options[i] = e.key;
|
options[i] = value;
|
||||||
});
|
});
|
||||||
onChanged();
|
|
||||||
},
|
},
|
||||||
);
|
sourceKey: searchTarget,
|
||||||
}).toList(),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user