offline updates and playhead fix

This commit is contained in:
Forrest 2026-08-16 12:46:30 -04:00
parent 7a199fe4df
commit 6663330260
14 changed files with 1673 additions and 545 deletions

View file

@ -7,6 +7,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import '../state/providers.dart';
import '../theme/tokens.dart';
import 'art_image.dart';
/// Animated cassette for the Now Playing screen. Composites, in the shell's
/// `469×298` coordinate space, from back to front:
@ -44,13 +45,18 @@ class _CassetteViewState extends ConsumerState<CassetteView>
}
void _onTick(Duration elapsed) {
final dt = (elapsed - _last).inMicroseconds / Duration.microsecondsPerSecond;
final dt =
(elapsed - _last).inMicroseconds / Duration.microsecondsPerSecond;
_last = elapsed;
if (dt <= 0) return;
// Read (not watch) inside the ticker: the model drives repaints itself, and
// watching here would rebuild the whole widget every position tick.
final s = ref.read(activePlaybackProvider);
_model.update(dt: dt, playing: s.playing && s.supported, progress: s.progress);
_model.update(
dt: dt,
playing: s.playing && s.supported,
progress: s.progress,
);
}
@override
@ -118,8 +124,7 @@ class _CassetteViewState extends ConsumerState<CassetteView>
child: AnimatedBuilder(
animation: _model,
child: cog,
builder: (_, child) =>
Transform.rotate(angle: angle(), child: child),
builder: (_, child) => Transform.rotate(angle: angle(), child: child),
),
);
}
@ -133,15 +138,12 @@ class _LabelArt extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (artUri == null) {
return ColoredBox(color: TimbreColors.surface);
}
return Image.network(
artUri!,
key: ValueKey(artUri),
fit: BoxFit.cover, // square art → wide label: crop the sides/top
gaplessPlayback: true,
errorBuilder: (_, _, _) => ColoredBox(color: TimbreColors.surface),
// Square art → wide label: crop the sides/top. A plain surface fill backs
// the null/error cases (no album glyph here — the shell frames the label).
return ArtImage(
artUri,
fit: BoxFit.cover,
placeholder: ColoredBox(color: TimbreColors.surface),
);
}
}
@ -193,9 +195,15 @@ class _TapePainter extends CustomPainter {
canvas.drawRect(cfg.windowRect, Paint()..color = cfg.padColor);
final tape = Paint()..color = cfg.tapeColor;
canvas.drawCircle(
cfg.leftReel, cfg.radius(model.progress, supply: true), tape);
cfg.leftReel,
cfg.radius(model.progress, supply: true),
tape,
);
canvas.drawCircle(
cfg.rightReel, cfg.radius(model.progress, supply: false), tape);
cfg.rightReel,
cfg.radius(model.progress, supply: false),
tape,
);
canvas.restore();
}
@ -245,7 +253,8 @@ class _CassetteConfig {
/// reel is full at p=0 and empty at p=1; the take-up reel is the reverse.
double radius(double progress, {required bool supply}) {
final frac = (supply ? 1 - progress : progress).clamp(0.0, 1.0);
final r2 = hubRadius * hubRadius +
final r2 =
hubRadius * hubRadius +
(fullRadius * fullRadius - hubRadius * hubRadius) * frac;
return math.sqrt(r2);
}