Files
bvandeusen 41dd2892e3 feat(player): resume last session on launch (#54)
The #52 teardown clears the session when idle/dismissed, so the
headset / lock-screen play button had nothing to resume and the user
lost their place.

- track.dart: toJson() (round-trips fromJson)
- db.dart: CachedResumeState single-row snapshot, schema 9->10 + migration
- audio_handler.dart: queuedTracks getter
- player_provider.dart: restoreQueue() — configure + setQueueFromTracks
  + seek, no play (restores PAUSED)
- resume_controller.dart: restores last snapshot paused on launch;
  persists {source,index,position_ms,tracks} debounced on track
  change / pause and immediately on app teardown; never clobbers the
  saved snapshot when the queue is empty so a teardown stays
  recoverable; restore gated on auth + no active session
- app.dart: wired into postFrame

db.g.dart regenerated by CI per project convention. Deferred fast-follow:
media-button-when-fully-stopped re-init (needs a configure()-injected
callback; tracked on #54).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:57:58 -04:00

59 lines
1.9 KiB
Dart

// Mirrors internal/api/types.go TrackRef and web/src/lib/api/types.ts.
//
// Server uses `duration_sec` (NOT duration_ms). `track_number` and
// `disc_number` are omitempty server-side so they're nullable here.
// `stream_url` points at /api/tracks/{id}/stream.
class TrackRef {
const TrackRef({
required this.id,
required this.title,
required this.albumId,
required this.artistId,
this.albumTitle = '',
this.artistName = '',
this.trackNumber,
this.discNumber,
this.durationSec = 0,
this.streamUrl = '',
});
final String id;
final String title;
final String albumId;
final String albumTitle;
final String artistId;
final String artistName;
final int? trackNumber;
final int? discNumber;
final int durationSec;
final String streamUrl;
factory TrackRef.fromJson(Map<String, dynamic> j) => TrackRef(
id: j['id'] as String,
title: j['title'] as String,
albumId: j['album_id'] as String,
albumTitle: j['album_title'] as String? ?? '',
artistId: j['artist_id'] as String,
artistName: j['artist_name'] as String? ?? '',
trackNumber: (j['track_number'] as num?)?.toInt(),
discNumber: (j['disc_number'] as num?)?.toInt(),
durationSec: (j['duration_sec'] as num?)?.toInt() ?? 0,
streamUrl: j['stream_url'] as String? ?? '',
);
/// Round-trips through [TrackRef.fromJson] (same server snake_case
/// keys). Used to persist the playback queue for resume-on-launch.
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'album_id': albumId,
'album_title': albumTitle,
'artist_id': artistId,
'artist_name': artistName,
if (trackNumber != null) 'track_number': trackNumber,
if (discNumber != null) 'disc_number': discNumber,
'duration_sec': durationSec,
'stream_url': streamUrl,
};
}