mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
add star rating, network cache, advanced search option, loginWithCookies, loadNext; fix some minor issues
This commit is contained in:
@@ -195,6 +195,7 @@ class ComicTile extends StatelessWidget {
|
||||
enableTranslate: ComicSource.find(comic.sourceKey)
|
||||
?.enableTagsTranslate ??
|
||||
false,
|
||||
rating: comic.stars,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -285,6 +286,7 @@ class _ComicDescription extends StatelessWidget {
|
||||
this.badge,
|
||||
this.maxLines = 2,
|
||||
this.tags,
|
||||
this.rating,
|
||||
});
|
||||
|
||||
final String title;
|
||||
@@ -294,6 +296,7 @@ class _ComicDescription extends StatelessWidget {
|
||||
final List<String>? tags;
|
||||
final int maxLines;
|
||||
final bool enableTranslate;
|
||||
final double? rating;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -358,6 +361,7 @@ class _ComicDescription extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const Spacer(),
|
||||
if (rating != null) StarRating(value: rating!, size: 18),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
@@ -623,9 +627,9 @@ class ComicListState extends State<ComicList> {
|
||||
String? _nextUrl;
|
||||
|
||||
void remove(Comic c) {
|
||||
if(_data[_page] == null || !_data[_page]!.remove(c)) {
|
||||
for(var page in _data.values) {
|
||||
if(page.remove(c)) {
|
||||
if (_data[_page] == null || !_data[_page]!.remove(c)) {
|
||||
for (var page in _data.values) {
|
||||
if (page.remove(c)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -685,7 +689,7 @@ class ComicListState extends State<ComicList> {
|
||||
(_maxPage == null || page <= _maxPage!)) {
|
||||
setState(() {
|
||||
_error = null;
|
||||
this._page = page;
|
||||
_page = page;
|
||||
});
|
||||
} else {
|
||||
context.showMessage(
|
||||
@@ -777,10 +781,10 @@ class ComicListState extends State<ComicList> {
|
||||
Future<void> _fetchNext() async {
|
||||
var res = await widget.loadNext!(_nextUrl);
|
||||
_data[_data.length + 1] = res.data;
|
||||
if (res.subData['next'] == null) {
|
||||
if (res.subData == null) {
|
||||
_maxPage = _data.length;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user