add star rating, network cache, advanced search option, loginWithCookies, loadNext; fix some minor issues

This commit is contained in:
nyne
2024-10-25 22:51:23 +08:00
parent b682d7d87b
commit 897f92f4c9
27 changed files with 1420 additions and 319 deletions

View File

@@ -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;
}
}