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:
@@ -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,
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -62,6 +62,7 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
|
||||
return Listener(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onPointerDown: (event) {
|
||||
_futurePosition = null;
|
||||
if (_isMouseScroll) {
|
||||
setState(() {
|
||||
_isMouseScroll = false;
|
||||
|
@@ -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();
|
||||
}
|
Reference in New Issue
Block a user