feat(flutter): Discover suggestions feed — web parity
The Flutter Discover screen was Lidarr-search-only; its empty state
showed a "Type to search" placeholder while web's /discover renders
the LB-derived out-of-library artist SuggestionFeed as its default
surface. Parity gap that slipped #356.
- ArtistSuggestion/SeedContribution model mirroring web types, with
attributionText() matching web's "Because you liked/played X[, Y, and
Z]." (Oxford comma, max 3).
- DiscoverApi.listSuggestions() → GET /api/discover/suggestions
(image_url already resolved server-side from Lidarr, a7bea43).
- discover_screen: empty search box → suggestions feed (artist art +
name + attribution + Request, reusing the existing createRequest +
mutation-queue-replay flow with optimistic hide); typing → Lidarr
search replaces; clearing → suggestions return. Mirrors web exactly.
Flutter-only; server endpoint unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/// Mirrors web/src/lib/api/types.ts ArtistSuggestion / SeedContribution —
|
||||
/// one out-of-library artist from GET /api/discover/suggestions. image_url
|
||||
/// is resolved on-demand from Lidarr server-side (may be empty).
|
||||
class SeedContribution {
|
||||
const SeedContribution({required this.name, required this.isLiked});
|
||||
|
||||
final String name;
|
||||
final bool isLiked;
|
||||
|
||||
factory SeedContribution.fromJson(Map<String, dynamic> j) => SeedContribution(
|
||||
name: j['name'] as String? ?? '',
|
||||
isLiked: j['is_liked'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
class ArtistSuggestion {
|
||||
const ArtistSuggestion({
|
||||
required this.mbid,
|
||||
required this.name,
|
||||
required this.imageUrl,
|
||||
required this.attribution,
|
||||
});
|
||||
|
||||
final String mbid;
|
||||
final String name;
|
||||
final String imageUrl;
|
||||
final List<SeedContribution> attribution;
|
||||
|
||||
factory ArtistSuggestion.fromJson(Map<String, dynamic> j) => ArtistSuggestion(
|
||||
mbid: j['mbid'] as String? ?? '',
|
||||
name: j['name'] as String? ?? '',
|
||||
imageUrl: j['image_url'] as String? ?? '',
|
||||
attribution: ((j['attribution'] as List?) ?? const [])
|
||||
.map((e) =>
|
||||
SeedContribution.fromJson((e as Map).cast<String, dynamic>()))
|
||||
.toList(growable: false),
|
||||
);
|
||||
|
||||
/// Mirrors web SuggestionFeed.attributionText (Oxford comma, max 3).
|
||||
String get attributionText {
|
||||
if (attribution.isEmpty) return '';
|
||||
final phrases = attribution
|
||||
.map((s) => '${s.isLiked ? 'liked' : 'played'} ${s.name}')
|
||||
.toList(growable: false);
|
||||
if (phrases.length == 1) return 'Because you ${phrases[0]}.';
|
||||
if (phrases.length == 2) {
|
||||
return 'Because you ${phrases[0]} and ${phrases[1]}.';
|
||||
}
|
||||
return 'Because you ${phrases[0]}, ${phrases[1]}, and ${phrases[2]}.';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user