mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-29 07:18:49 +02:00
feat(ui): smooth download progress updates
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Smooths visual progress changes without increasing backend polling.
|
||||
///
|
||||
/// The first value is shown immediately so restored downloads never animate
|
||||
/// from zero. Later monotonic increases are interpolated locally, while
|
||||
/// resets, completion, and disabled animations snap to the authoritative
|
||||
/// backend value.
|
||||
class SmoothedProgressScope extends StatefulWidget {
|
||||
final double value;
|
||||
final bool animate;
|
||||
final Duration duration;
|
||||
final Curve curve;
|
||||
final Widget child;
|
||||
|
||||
const SmoothedProgressScope({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.child,
|
||||
this.animate = true,
|
||||
this.duration = const Duration(milliseconds: 900),
|
||||
this.curve = Curves.linear,
|
||||
});
|
||||
|
||||
static ValueListenable<double> of(BuildContext context) {
|
||||
final scope = context
|
||||
.dependOnInheritedWidgetOfExactType<_SmoothedProgressScopeData>();
|
||||
assert(scope != null, 'No SmoothedProgressScope found in context');
|
||||
return scope!.progress;
|
||||
}
|
||||
|
||||
@override
|
||||
State<SmoothedProgressScope> createState() => _SmoothedProgressScopeState();
|
||||
}
|
||||
|
||||
class _SmoothedProgressScopeState extends State<SmoothedProgressScope>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final ValueNotifier<double> _displayedProgress;
|
||||
Animation<double>? _animation;
|
||||
|
||||
double _normalized(double value) => value.clamp(0.0, 1.0).toDouble();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final initialValue = _normalized(widget.value);
|
||||
_displayedProgress = ValueNotifier<double>(initialValue);
|
||||
_controller = AnimationController(vsync: this, duration: widget.duration)
|
||||
..addListener(_updateDisplayedProgress);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SmoothedProgressScope oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_controller.duration = widget.duration;
|
||||
|
||||
final target = _normalized(widget.value);
|
||||
final current = _displayedProgress.value;
|
||||
if ((target - current).abs() < 0.0001) {
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldSnap =
|
||||
!widget.animate ||
|
||||
target < _normalized(oldWidget.value) ||
|
||||
target <= current ||
|
||||
target <= 0 ||
|
||||
target >= 1 ||
|
||||
widget.duration == Duration.zero;
|
||||
if (shouldSnap) {
|
||||
_controller.stop();
|
||||
_animation = null;
|
||||
_displayedProgress.value = target;
|
||||
return;
|
||||
}
|
||||
|
||||
_animation = Tween<double>(
|
||||
begin: current,
|
||||
end: target,
|
||||
).animate(CurvedAnimation(parent: _controller, curve: widget.curve));
|
||||
_controller.forward(from: 0);
|
||||
}
|
||||
|
||||
void _updateDisplayedProgress() {
|
||||
final animation = _animation;
|
||||
if (animation != null) {
|
||||
_displayedProgress.value = animation.value;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller
|
||||
..removeListener(_updateDisplayedProgress)
|
||||
..dispose();
|
||||
_displayedProgress.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _SmoothedProgressScopeData(
|
||||
progress: _displayedProgress,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SmoothedProgressScopeData extends InheritedWidget {
|
||||
final ValueListenable<double> progress;
|
||||
|
||||
const _SmoothedProgressScopeData({
|
||||
required this.progress,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_SmoothedProgressScopeData oldWidget) =>
|
||||
progress != oldWidget.progress;
|
||||
}
|
||||
|
||||
/// Rebuilds only the visual fragment that consumes smoothed progress.
|
||||
class SmoothedProgressBuilder extends StatelessWidget {
|
||||
final Widget? child;
|
||||
final Widget Function(BuildContext context, double progress, Widget? child)
|
||||
builder;
|
||||
|
||||
const SmoothedProgressBuilder({super.key, required this.builder, this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<double>(
|
||||
valueListenable: SmoothedProgressScope.of(context),
|
||||
builder: builder,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user