Files
bvandeusen 47572b2e95 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>
2026-05-17 20:48:54 -04:00

52 lines
1.7 KiB
Dart

/// 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]}.';
}
}