feat(flutter): bump deps to latest + Search screen slice
Major version bumps (riverpod 2->3, go_router 14->17, just_audio 0.9->0.10, flutter_secure_storage 9->10, google_fonts 6->8, flutter_lints 4->6). package_info_plus held at 8.x due to win32 conflict with secure_storage 10.x. Riverpod 3 breaks: AsyncValue.valueOrNull -> .value, StateProvider replaced with Notifier subclass. just_audio 0.10: ConcatenatingAudioSource -> setAudioSources. Search slice: - models/page.dart (generic Page<T> envelope) - models/search_response.dart (3-facet wrapper) - api/endpoints/search.dart - search/search_provider.dart (debounced 250ms) - search/search_screen.dart (TextField + 3 horizontal/vertical sections) - Wired /search route + search button on home AppBar
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
// Mirrors the server's Page<T> envelope used by paged endpoints
|
||||
// (/api/search, /api/library/artists, /api/library/albums,
|
||||
// /api/library/history, /api/me/likes/*).
|
||||
//
|
||||
// Wire shape from internal/api/types.go Page[T]:
|
||||
// { items: T[], total: int, limit: int, offset: int }
|
||||
//
|
||||
// Dart generics can't auto-infer T from JSON, so callers pass an
|
||||
// itemFromJson function alongside the raw map. Parse logic stays in
|
||||
// each entity's own fromJson; this class only handles the envelope.
|
||||
class Page<T> {
|
||||
const Page({
|
||||
required this.items,
|
||||
required this.total,
|
||||
required this.limit,
|
||||
required this.offset,
|
||||
});
|
||||
|
||||
final List<T> items;
|
||||
final int total;
|
||||
final int limit;
|
||||
final int offset;
|
||||
|
||||
factory Page.fromJson(
|
||||
Map<String, dynamic> j,
|
||||
T Function(Map<String, dynamic>) itemFromJson,
|
||||
) {
|
||||
final raw = (j['items'] as List?) ?? const [];
|
||||
return Page<T>(
|
||||
items: raw
|
||||
.map((e) => itemFromJson((e as Map).cast<String, dynamic>()))
|
||||
.toList(growable: false),
|
||||
total: (j['total'] as num?)?.toInt() ?? 0,
|
||||
limit: (j['limit'] as num?)?.toInt() ?? 0,
|
||||
offset: (j['offset'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convenience for endpoints that always return the first page or
|
||||
/// when the caller just wants the items.
|
||||
bool get hasMore => offset + items.length < total;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'album.dart';
|
||||
import 'artist.dart';
|
||||
import 'page.dart';
|
||||
import 'track.dart';
|
||||
|
||||
/// Mirrors internal/api/search.go SearchResponse — three pages keyed by
|
||||
/// facet, each independently paged. The mobile UI usually only walks
|
||||
/// the first page of each facet (limit 20-50); infinite scroll within a
|
||||
/// facet is a future enhancement, not v1.
|
||||
class SearchResponse {
|
||||
const SearchResponse({
|
||||
required this.artists,
|
||||
required this.albums,
|
||||
required this.tracks,
|
||||
});
|
||||
|
||||
final Page<ArtistRef> artists;
|
||||
final Page<AlbumRef> albums;
|
||||
final Page<TrackRef> tracks;
|
||||
|
||||
factory SearchResponse.fromJson(Map<String, dynamic> j) => SearchResponse(
|
||||
artists: Page.fromJson(
|
||||
(j['artists'] as Map?)?.cast<String, dynamic>() ?? const {},
|
||||
ArtistRef.fromJson,
|
||||
),
|
||||
albums: Page.fromJson(
|
||||
(j['albums'] as Map?)?.cast<String, dynamic>() ?? const {},
|
||||
AlbumRef.fromJson,
|
||||
),
|
||||
tracks: Page.fromJson(
|
||||
(j['tracks'] as Map?)?.cast<String, dynamic>() ?? const {},
|
||||
TrackRef.fromJson,
|
||||
),
|
||||
);
|
||||
|
||||
bool get isEmpty =>
|
||||
artists.items.isEmpty && albums.items.isEmpty && tracks.items.isEmpty;
|
||||
}
|
||||
Reference in New Issue
Block a user