2a87a50b81
Faithful port of Flutter's _OfflinePoolCard + shuffle_source.dart.
I was wrong earlier that Android lacked the source — the
audio_cache_index table already carries lastPlayedAt, exactly what
Flutter reads.
* ShuffleSource @Singleton over the local cache: recentlyPlayed()
= audio_cache_index ordered by lastPlayedAt desc, materialized
from cached_tracks; liked() = same ∩ the liked track-id set.
Both are unions over the cache regardless of storage bucket,
matching Flutter's note that the bucket split is eviction-only.
* OfflinePoolCard widget (sized to match PlaylistCard).
* AudioCacheIndexDao.trackIdsByRecency(); LikesRepository.likedTrackIds().
* HomeViewModel: offline StateFlow from ConnectivityObserver,
playPool(kind) shuffles + plays (snackbar "No cached X tracks yet"
when empty), transientMessages channel.
* buildPlaylistsRow gains an `offline` flag; when offline the two
pool cards (Recently played, Liked) LEAD the Playlists row, before
the system slots — the rest of the row (placeholders + user
playlists) renders regardless, exactly as Flutter does.
* HomeScreen gains a SnackbarHost for the empty-pool message.
Together with edbc8053 (placeholders) this closes audit v3 #28.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>