feat(flutter): drift-first History tab
Slice 4 of the smooth-loading pass. Adds CachedHistorySnapshot (schema 4) and rewires _historyProvider through cacheFirst, mirroring the homeProvider pattern: yield the last cached page immediately on subscribe so the tab paints from disk on cold open, then SWR-refresh in the background to surface fresh plays. Also enables basic offline scrollback — the most recent History page survives both app restart and connectivity loss. JSON blob storage (vs columnar) because the page is small, always read whole, and HistoryPage.fromJson already accepts the wire shape, so server-side field additions don't force a migration. History delta sync via library_changes is out of scope here; the next visit's SWR pull is the source of freshness for now.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -132,10 +134,68 @@ final _libraryAlbumsProvider = AsyncNotifierProvider<
|
||||
_LibraryAlbumsNotifier,
|
||||
wire.Paged<AlbumRef>>(_LibraryAlbumsNotifier.new);
|
||||
|
||||
final _historyProvider = FutureProvider<HistoryPage>((ref) async {
|
||||
return (await ref.watch(_meApiProvider.future)).history();
|
||||
// Drift-first History tab. Mirrors homeProvider's pattern: store the
|
||||
// last /api/me/history page as JSON in a single-row drift table, yield
|
||||
// it immediately on subscribe (so the tab paints from disk on cold
|
||||
// open), then SWR-refresh in the background. Also gives basic offline
|
||||
// scrollback — the last fetched page survives connectivity loss.
|
||||
//
|
||||
// JSON blob (vs columnar) because the page is small, always read whole,
|
||||
// and the HistoryPage.fromJson constructor already accepts the wire
|
||||
// shape — no schema-evolution pain when server-side fields change.
|
||||
final _historyProvider = StreamProvider<HistoryPage>((ref) {
|
||||
final db = ref.watch(appDbProvider);
|
||||
return cacheFirst<CachedHistorySnapshotData, HistoryPage>(
|
||||
driftStream: db.select(db.cachedHistorySnapshot).watch(),
|
||||
fetchAndPopulate: () async {
|
||||
final api = await ref.read(_meApiProvider.future);
|
||||
final fresh = await api.history();
|
||||
await db.into(db.cachedHistorySnapshot).insertOnConflictUpdate(
|
||||
CachedHistorySnapshotCompanion.insert(
|
||||
json: _encodeHistoryPage(fresh),
|
||||
updatedAt: drift.Value(DateTime.now()),
|
||||
),
|
||||
);
|
||||
},
|
||||
toResult: (rows) => rows.isEmpty
|
||||
? const HistoryPage(events: [], hasMore: false)
|
||||
: HistoryPage.fromJson(
|
||||
jsonDecode(rows.first.json) as Map<String, dynamic>),
|
||||
isOnline: () async => (await ref
|
||||
.read(connectivityProvider.future)
|
||||
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
|
||||
// SWR: yield cache instantly, then refresh in the background so the
|
||||
// tab reflects the freshest plays. Matches homeProvider behavior.
|
||||
alwaysRefresh: true,
|
||||
tag: 'history',
|
||||
);
|
||||
});
|
||||
|
||||
/// Encodes HistoryPage back to the wire-format JSON shape
|
||||
/// /api/me/history emits, so HistoryPage.fromJson can round-trip
|
||||
/// through the drift cache.
|
||||
String _encodeHistoryPage(HistoryPage h) => jsonEncode({
|
||||
'events': h.events
|
||||
.map((e) => {
|
||||
'id': e.id,
|
||||
'played_at': e.playedAt,
|
||||
'track': {
|
||||
'id': e.track.id,
|
||||
'title': e.track.title,
|
||||
'album_id': e.track.albumId,
|
||||
'album_title': e.track.albumTitle,
|
||||
'artist_id': e.track.artistId,
|
||||
'artist_name': e.track.artistName,
|
||||
'track_number': e.track.trackNumber,
|
||||
'disc_number': e.track.discNumber,
|
||||
'duration_sec': e.track.durationSec,
|
||||
'stream_url': e.track.streamUrl,
|
||||
},
|
||||
})
|
||||
.toList(),
|
||||
'has_more': h.hasMore,
|
||||
});
|
||||
|
||||
// Drift-first Liked tabs (#357 plan C). Read from cached_likes joined
|
||||
// against cached_tracks / cached_albums / cached_artists. SyncController
|
||||
// keeps both sides fresh; LikesController mutates cached_likes
|
||||
|
||||
Reference in New Issue
Block a user