diff --git a/lib/common/widgets/custom_toast.dart b/lib/common/widgets/custom_toast.dart index 30d0e84b5..edc416fb8 100644 --- a/lib/common/widgets/custom_toast.dart +++ b/lib/common/widgets/custom_toast.dart @@ -10,23 +10,21 @@ class CustomToast extends StatelessWidget { @override Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); + final colorScheme = ColorScheme.of(context); return Container( margin: EdgeInsets.only( bottom: MediaQuery.viewPaddingOf(context).bottom + 30, ), padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10), decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer.withValues( - alpha: toastOpacity, - ), + color: colorScheme.primaryContainer.withValues(alpha: toastOpacity), borderRadius: const BorderRadius.all(Radius.circular(20)), ), child: Text( msg, style: TextStyle( fontSize: 13, - color: theme.colorScheme.onPrimaryContainer, + color: colorScheme.onPrimaryContainer, ), ), ); @@ -41,7 +39,7 @@ class LoadingWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); + final theme = Theme.of(context); final onSurfaceVariant = theme.colorScheme.onSurfaceVariant; return Container( padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), @@ -58,7 +56,6 @@ class LoadingWidget extends StatelessWidget { strokeWidth: 3, valueColor: AlwaysStoppedAnimation(onSurfaceVariant), ), - //msg Text(msg, style: TextStyle(color: onSurfaceVariant)), ], diff --git a/lib/common/widgets/flutter/refresh_indicator.dart b/lib/common/widgets/flutter/refresh_indicator.dart index 68b226ad4..d1a5fe944 100644 --- a/lib/common/widgets/flutter/refresh_indicator.dart +++ b/lib/common/widgets/flutter/refresh_indicator.dart @@ -217,8 +217,8 @@ class RefreshIndicatorState extends State RefreshIndicatorStatus? _status; late Future _pendingRefreshFuture; double? _dragOffset; - late Color _effectiveValueColor = - widget.color ?? Theme.of(context).colorScheme.primary; + late Color _effectiveValueColor; + // late Color _backgroundColor; static final Animatable _threeQuarterTween = Tween( begin: 0.0, @@ -274,9 +274,10 @@ class RefreshIndicatorState extends State } void _setupColorTween() { + final colorScheme = ColorScheme.of(context); + // _backgroundColor = colorScheme.surfaceContainerHighest; // Reset the current value color. - _effectiveValueColor = - widget.color ?? Theme.of(context).colorScheme.primary; + _effectiveValueColor = widget.color ?? colorScheme.primary; final Color color = _effectiveValueColor; if (color.a == 0) { // Set an always stopped animation instead of a driven tween. @@ -558,14 +559,52 @@ class RefreshIndicatorState extends State } bool _onDrag(double offset, double viewportDimension) { - if (_positionController.value > 0.0 && - _status == RefreshIndicatorStatus.drag) { + if (_positionController.value > 0.0 && _status == .drag) { _dragOffset = _dragOffset! + offset; _checkDragOffset(viewportDimension); return true; } return false; } + + // late final _refreshKey = GlobalKey(); + // Widget _m3eRefreshProgressIndicator(bool showIndeterminateIndicator) { + // const indicatorMargin = EdgeInsets.all(4); + // const indicatorPadding = EdgeInsets.all(6); + // const indicatorSize = 41.0; + + // final progress = _value.value; + // return Padding( + // padding: indicatorMargin, + // child: SizedBox( + // width: indicatorSize, + // height: indicatorSize, + // child: Material( + // type: MaterialType.circle, + // color: _backgroundColor, + // elevation: widget.elevation, + // child: Padding( + // padding: indicatorPadding, + // child: showIndeterminateIndicator + // ? M3ELoadingIndicator( + // childKey: _refreshKey, + // color: _effectiveValueColor, + // morphs: Morphs.refreshMorphs, + // size: null, + // ) + // : RawM3ELoadingIndicator( + // key: _refreshKey, + // morph: Morphs.manualMorph, + // progress: progress, + // angle: -progress * math.pi, + // color: _valueColor.value!, + // size: null, + // ), + // ), + // ), + // ), + // ); + // } } // ignore: camel_case_types diff --git a/lib/common/widgets/image_viewer/loading_indicator.dart b/lib/common/widgets/image_viewer/loading_indicator.dart index 6cedb908e..54efaba40 100644 --- a/lib/common/widgets/image_viewer/loading_indicator.dart +++ b/lib/common/widgets/image_viewer/loading_indicator.dart @@ -18,6 +18,7 @@ import 'dart:math' show pi; import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart' show SemanticsConfiguration; /// /// created by dom on 2026/02/14 @@ -73,6 +74,7 @@ class RenderLoadingIndicator extends RenderBox { if (_progress == value) return; _progress = value; markNeedsPaint(); + markNeedsSemanticsUpdate(); } @override @@ -119,6 +121,16 @@ class RenderLoadingIndicator extends RenderBox { ); } + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config + ..role = .progressBar + ..minValue = '0' + ..maxValue = '100' + ..value = (_progress * 100).round().toString(); + } + @override bool get isRepaintBoundary => true; } diff --git a/lib/common/widgets/loading_widget/loading_widget.dart b/lib/common/widgets/loading_widget/loading_widget.dart index 026758898..d1df73e60 100644 --- a/lib/common/widgets/loading_widget/loading_widget.dart +++ b/lib/common/widgets/loading_widget/loading_widget.dart @@ -4,8 +4,6 @@ import 'package:flutter/material.dart'; const Widget m3eLoading = Center(child: M3ELoadingIndicator()); -const Widget circularLoading = Center(child: CircularProgressIndicator()); - const Widget linearLoading = SliverToBoxAdapter( child: LinearProgressIndicator(), ); diff --git a/lib/common/widgets/loading_widget/m3e_loading_indicator.dart b/lib/common/widgets/loading_widget/m3e_loading_indicator.dart index 03f89da89..52ebffa61 100644 --- a/lib/common/widgets/loading_widget/m3e_loading_indicator.dart +++ b/lib/common/widgets/loading_widget/m3e_loading_indicator.dart @@ -17,14 +17,27 @@ import 'dart:math' as math; +import 'package:PiliPlus/common/widgets/loading_widget/morphs.dart'; import 'package:flutter/material.dart'; import 'package:flutter/physics.dart' show SpringSimulation; +import 'package:flutter/semantics.dart'; import 'package:material_new_shapes/material_new_shapes.dart'; /// reimplement of https://github.com/EmilyMoonstone/material_3_expressive/tree/main/packages/loading_indicator_m3e class M3ELoadingIndicator extends StatefulWidget { - const M3ELoadingIndicator({super.key}); + const M3ELoadingIndicator({ + super.key, + // this.childKey, + this.morphs, + this.color, + this.size = const Size.square(40), + }); + final List? morphs; + + final Color? color; + final Size size; + // final Key? childKey; @override State createState() => _M3ELoadingIndicatorState(); @@ -32,42 +45,25 @@ class M3ELoadingIndicator extends StatefulWidget { class _M3ELoadingIndicatorState extends State with SingleTickerProviderStateMixin { - static final List _morphs = () { - final List shapes = [ - MaterialShapes.softBurst, - MaterialShapes.cookie9Sided, - MaterialShapes.pentagon, - MaterialShapes.pill, - MaterialShapes.sunny, - MaterialShapes.cookie4Sided, - MaterialShapes.oval, - ]; - return [ - for (var i = 0; i < shapes.length; i++) - Morph( - shapes[i], - shapes[(i + 1) % shapes.length], - ), - ]; - }(); - static const int _morphIntervalMs = 650; - static const double _fullRotation = 360.0; + static const double _fullRotation = 2 * math.pi; static const int _globalRotationDurationMs = 4666; static const double _quarterRotation = _fullRotation / 4; + late final List _morphs; late final AnimationController _controller; int _morphIndex = 1; - double _morphRotationTargetAngle = _quarterRotation; + double _morphRotationTarget = _quarterRotation; - final _morphAnimationSpec = SpringSimulation( + static final _morphAnimationSpec = SpringSimulation( SpringDescription.withDampingRatio(ratio: 0.6, stiffness: 200.0, mass: 1.0), 0.0, 1.0, 5.0, snapToEnd: true, + // tolerance: const Tolerance(velocity: 0.1, distance: 0.1), ); void _statusListener(AnimationStatus status) { @@ -78,23 +74,21 @@ class _M3ELoadingIndicatorState extends State void _startAnimation() { _morphIndex++; - _morphRotationTargetAngle = - (_morphRotationTargetAngle + _quarterRotation) % _fullRotation; - _controller - ..value = 0.0 - ..animateWith(_morphAnimationSpec); + _morphRotationTarget = + (_morphRotationTarget + _quarterRotation) % _fullRotation; + _controller.animateWith(_morphAnimationSpec); } @override void initState() { super.initState(); + _morphs = widget.morphs ?? Morphs.loadingMorphs; _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: _morphIntervalMs), ) ..addStatusListener(_statusListener) - ..value = 0.0 ..animateWith(_morphAnimationSpec); } @@ -110,82 +104,86 @@ class _M3ELoadingIndicatorState extends State final elapsedInMs = _morphIntervalMs * (_morphIndex - 1) + (_controller.lastElapsedDuration?.inMilliseconds ?? 0); - final globalRotationControllerValue = - (elapsedInMs % _globalRotationDurationMs) / _globalRotationDurationMs; - final globalRotationDegrees = globalRotationControllerValue * _fullRotation; - final totalRotationDegrees = - progress * _quarterRotation + - _morphRotationTargetAngle + - globalRotationDegrees; - return totalRotationDegrees * (math.pi / 180.0); + final globalRotation = + (elapsedInMs % _globalRotationDurationMs) / + _globalRotationDurationMs * + _fullRotation; + + return progress * _quarterRotation + _morphRotationTarget + globalRotation; } @override Widget build(BuildContext context) { - final color = Theme.of(context).colorScheme.secondaryFixedDim; + final color = widget.color ?? ColorScheme.of(context).secondaryFixedDim; return AnimatedBuilder( animation: _controller, builder: (context, child) { final progress = _controller.value; - return _M3ELoadingIndicator( + return RawM3ELoadingIndicator( + // key: widget.childKey, morph: _morphs[_morphIndex % _morphs.length], progress: progress, angle: _calcAngle(progress), color: color, + size: widget.size, ); }, ); } } -class _M3ELoadingIndicator extends LeafRenderObjectWidget { - const _M3ELoadingIndicator({ +class RawM3ELoadingIndicator extends LeafRenderObjectWidget { + const RawM3ELoadingIndicator({ + super.key, required this.morph, required this.progress, required this.angle, required this.color, + required this.size, }); final Morph morph; - final double progress; - final double angle; - final Color color; + final Size size; @override RenderObject createRenderObject(BuildContext context) { - return _RenderM3ELoadingIndicator( + return RenderM3ELoadingIndicator( morph: morph, progress: progress, angle: angle, color: color, + size: size, ); } @override void updateRenderObject( BuildContext context, - _RenderM3ELoadingIndicator renderObject, + RenderM3ELoadingIndicator renderObject, ) { renderObject ..morph = morph ..progress = progress ..angle = angle - ..color = color; + ..color = color + ..preferredSize = size; } } -class _RenderM3ELoadingIndicator extends RenderBox { - _RenderM3ELoadingIndicator({ +class RenderM3ELoadingIndicator extends RenderBox { + RenderM3ELoadingIndicator({ required Morph morph, required double progress, required double angle, required Color color, + required Size size, }) : _morph = morph, _progress = progress, _angle = angle, + _preferredSize = size, _color = color, _paint = Paint() ..style = PaintingStyle.fill @@ -223,20 +221,38 @@ class _RenderM3ELoadingIndicator extends RenderBox { markNeedsPaint(); } + Size _preferredSize; + set preferredSize(Size value) { + if (_preferredSize == value) return; + _preferredSize = size; + markNeedsLayout(); + } + + @override + Size computeDryLayout(covariant BoxConstraints constraints) { + return constraints.constrain(_preferredSize); + } + @override void performLayout() { - size = constraints.constrainDimensions(40, 40); + size = computeDryLayout(constraints); + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.role = .loadingSpinner; } @override void paint(PaintingContext context, Offset offset) { final width = size.width; final value = size.width / 2; - final matrix = Matrix4.identity() - ..translateByDouble(offset.dx + value, offset.dy + value, 0.0, 1.0) - ..rotateZ(angle) - ..translateByDouble(-value, -value, 0.0, 1.0) - ..scaleByDouble(width, width, width, 1.0); + final matrix = + Matrix4.translationValues(offset.dx + value, offset.dy + value, 0.0) + ..rotateZ(angle) + ..translateByDouble(-value, -value, 0.0, 1.0) + ..scaleByDouble(width, width, width, 1.0); final path = morph.toPath(progress: progress).transform(matrix.storage); context.canvas.drawPath(path, _paint); diff --git a/lib/common/widgets/loading_widget/morphs.dart b/lib/common/widgets/loading_widget/morphs.dart new file mode 100644 index 000000000..f153bc80c --- /dev/null +++ b/lib/common/widgets/loading_widget/morphs.dart @@ -0,0 +1,41 @@ +import 'package:material_new_shapes/material_new_shapes.dart'; + +abstract final class Morphs { + static List buildMorph( + List shapes, { + bool loop = true, + }) { + assert(shapes.length >= 2); + return [ + for (var i = 0; i < shapes.length - 1; i++) + Morph(shapes[i], shapes[i + 1]), + if (loop) Morph(shapes[shapes.length - 1], shapes[0]), + ]; + } + + static final loadingMorphs = buildMorph([ + MaterialShapes.softBurst, + MaterialShapes.cookie9Sided, + MaterialShapes.pentagon, + MaterialShapes.pill, + MaterialShapes.sunny, + MaterialShapes.cookie4Sided, + MaterialShapes.oval, + ]); + + // static final refreshMorphs = buildMorph([ + // MaterialShapes.softBurst, + // MaterialShapes.cookie9Sided, + // MaterialShapes.gem, + // MaterialShapes.flower, + // MaterialShapes.sunny, + // MaterialShapes.cookie4Sided, + // MaterialShapes.oval, + // MaterialShapes.cookie12Sided, + // ]); + + // static final manualMorph = Morph( + // MaterialShapes.circle, + // MaterialShapes.softBurst, + // ); +}