47572b2e95
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>
52 lines
1.7 KiB
Dart
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]}.';
|
|
}
|
|
}
|