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

@@ -567,6 +567,7 @@ class SliverSearchBar extends StatefulWidget {
required this.controller,
this.onChanged,
this.action,
this.focusNode,
});
final SearchBarController controller;
@@ -575,6 +576,8 @@ class SliverSearchBar extends StatefulWidget {
final Widget? action;
final FocusNode? focusNode;
@override
State<SliverSearchBar> createState() => _SliverSearchBarState();
}
@@ -613,6 +616,7 @@ class _SliverSearchBarState extends State<SliverSearchBar>
topPadding: MediaQuery.of(context).padding.top,
onChanged: widget.onChanged,
action: widget.action,
focusNode: widget.focusNode,
),
);
}
@@ -629,12 +633,15 @@ class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate {
final Widget? action;
final FocusNode? focusNode;
const _SliverSearchBarDelegate({
required this.editingController,
required this.controller,
required this.topPadding,
this.onChanged,
this.action,
this.focusNode,
});
static const _kAppBarHeight = 52.0;
@@ -662,6 +669,7 @@ class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: TextField(
focusNode: focusNode,
controller: editingController,
decoration: InputDecoration(
hintText: "Search".tl,

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

View File

@@ -3,7 +3,7 @@ library components;
import 'dart:async';
import 'dart:collection';
import 'dart:math' as math;
import 'dart:ui';
import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

View File

@@ -19,7 +19,7 @@ class BlurEffect extends StatelessWidget {
return ClipRRect(
borderRadius: borderRadius ?? BorderRadius.zero,
child: BackdropFilter(
filter: ImageFilter.blur(
filter: ui.ImageFilter.blur(
sigmaX: blur,
sigmaY: blur,
tileMode: TileMode.mirror,

View File

@@ -21,11 +21,11 @@ class AnimatedImage extends StatefulWidget {
this.gaplessPlayback = false,
this.filterQuality = FilterQuality.medium,
this.isAntiAlias = false,
this.part,
Map<String, String>? headers,
int? cacheWidth,
int? cacheHeight,
}
): image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image),
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image),
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0);
@@ -61,13 +61,16 @@ class AnimatedImage extends StatefulWidget {
final bool isAntiAlias;
final ImagePart? part;
static void clear() => _AnimatedImageState.clear();
@override
State<AnimatedImage> createState() => _AnimatedImageState();
}
class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserver {
class _AnimatedImageState extends State<AnimatedImage>
with WidgetsBindingObserver {
ImageStream? _imageStream;
ImageInfo? _imageInfo;
ImageChunkEvent? _loadingProgress;
@@ -138,8 +141,8 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
}
void _updateInvertColors() {
_invertColors = MediaQuery.maybeInvertColorsOf(context)
?? SemanticsBinding.instance.accessibilityFeatures.invertColors;
_invertColors = MediaQuery.maybeInvertColorsOf(context) ??
SemanticsBinding.instance.accessibilityFeatures.invertColors;
}
void _resolveImage() {
@@ -148,16 +151,19 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
imageProvider: widget.image,
);
final ImageStream newStream =
provider.resolve(createLocalImageConfiguration(
provider.resolve(createLocalImageConfiguration(
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);
}
ImageStreamListener? _imageStreamListener;
ImageStreamListener _getListener({bool recreateListener = false}) {
if(_imageStreamListener == null || recreateListener) {
if (_imageStreamListener == null || recreateListener) {
_lastException = null;
_imageStreamListener = ImageStreamListener(
_handleImageFrame,
@@ -191,7 +197,8 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
void _replaceImage({required ImageInfo? info}) {
final ImageInfo? oldImageInfo = _imageInfo;
SchedulerBinding.instance.addPostFrameCallback((_) => oldImageInfo?.dispose());
SchedulerBinding.instance
.addPostFrameCallback((_) => oldImageInfo?.dispose());
_imageInfo = info;
}
@@ -208,7 +215,9 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
}
if (!widget.gaplessPlayback) {
setState(() { _replaceImage(info: null); });
setState(() {
_replaceImage(info: null);
});
}
setState(() {
@@ -247,7 +256,9 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
return;
}
if (keepStreamAlive && _completerHandle == null && _imageStream?.completer != null) {
if (keepStreamAlive &&
_completerHandle == null &&
_imageStream?.completer != null) {
_completerHandle = _imageStream!.completer!.keepAlive();
}
@@ -259,7 +270,19 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
Widget build(BuildContext context) {
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(
image: _imageInfo?.image,
width: widget.width,
@@ -291,7 +314,7 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
child: result,
);
}
} else{
} else {
result = const Center();
}
@@ -307,8 +330,59 @@ class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserv
super.debugFillProperties(description);
description.add(DiagnosticsProperty<ImageStream>('stream', _imageStream));
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<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;
}
}

View File

@@ -62,6 +62,7 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
return Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (event) {
_futurePosition = null;
if (_isMouseScroll) {
setState(() {
_isMouseScroll = false;

View File

@@ -2,8 +2,10 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.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:window_manager/window_manager.dart';
@@ -97,8 +99,12 @@ class WindowFrame extends StatelessWidget {
).toAlign(Alignment.centerLeft).paddingLeft(4),
),
),
if (!App.isMacOS)
const WindowButtons()
if (kDebugMode)
const TextButton(
onPressed: debug,
child: Text('Debug'),
),
if (!App.isMacOS) const WindowButtons()
],
),
);
@@ -622,3 +628,7 @@ TransitionBuilder VirtualWindowFrameInit() {
);
};
}
void debug() {
ComicSource.reload();
}