From 147d6e280e782e1629ac00f2fa7a1bf024d9e0fd Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 8 May 2026 14:30:01 -0400 Subject: [PATCH] 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 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 --- flutter_client/lib/api/endpoints/search.dart | 29 +++ flutter_client/lib/library/home_screen.dart | 12 + flutter_client/lib/models/page.dart | 42 +++ .../lib/models/search_response.dart | 38 +++ flutter_client/lib/player/audio_handler.dart | 4 +- .../lib/player/now_playing_screen.dart | 4 +- flutter_client/lib/player/player_bar.dart | 4 +- .../lib/search/search_provider.dart | 37 +++ flutter_client/lib/search/search_screen.dart | 198 +++++++++++++++ flutter_client/lib/shared/routing.dart | 2 + flutter_client/pubspec.lock | 240 +++++++++++++++--- flutter_client/pubspec.yaml | 16 +- 12 files changed, 581 insertions(+), 45 deletions(-) create mode 100644 flutter_client/lib/api/endpoints/search.dart create mode 100644 flutter_client/lib/models/page.dart create mode 100644 flutter_client/lib/models/search_response.dart create mode 100644 flutter_client/lib/search/search_provider.dart create mode 100644 flutter_client/lib/search/search_screen.dart diff --git a/flutter_client/lib/api/endpoints/search.dart b/flutter_client/lib/api/endpoints/search.dart new file mode 100644 index 00000000..2ba84df1 --- /dev/null +++ b/flutter_client/lib/api/endpoints/search.dart @@ -0,0 +1,29 @@ +import 'package:dio/dio.dart'; + +import '../../models/search_response.dart'; + +/// SearchApi wraps GET /api/search. The server runs three facets (artists, +/// albums, tracks) sharing one limit/offset pair. Each facet returns its +/// own total reflecting the full match count. +class SearchApi { + SearchApi(this._dio); + final Dio _dio; + + /// Empty / whitespace-only query is the caller's responsibility to + /// guard against — the server returns 400 bad_request for it. + Future search( + String query, { + int limit = 20, + int offset = 0, + }) async { + final r = await _dio.get>( + '/api/search', + queryParameters: { + 'q': query, + 'limit': limit, + 'offset': offset, + }, + ); + return SearchResponse.fromJson(r.data ?? const {}); + } +} diff --git a/flutter_client/lib/library/home_screen.dart b/flutter_client/lib/library/home_screen.dart index b01fd579..fc4bfab1 100644 --- a/flutter_client/lib/library/home_screen.dart +++ b/flutter_client/lib/library/home_screen.dart @@ -24,6 +24,18 @@ class HomeScreen extends ConsumerWidget { final fs = Theme.of(context).extension()!; return Scaffold( backgroundColor: fs.obsidian, + appBar: AppBar( + backgroundColor: fs.obsidian, + elevation: 0, + title: Text('Minstrel', style: TextStyle(color: fs.parchment)), + actions: [ + IconButton( + icon: Icon(Icons.search, color: fs.parchment), + tooltip: 'Search', + onPressed: () => context.push('/search'), + ), + ], + ), body: SafeArea( child: ref.watch(homeProvider).when( error: (e, _) { diff --git a/flutter_client/lib/models/page.dart b/flutter_client/lib/models/page.dart new file mode 100644 index 00000000..eb38a464 --- /dev/null +++ b/flutter_client/lib/models/page.dart @@ -0,0 +1,42 @@ +// Mirrors the server's Page 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 { + const Page({ + required this.items, + required this.total, + required this.limit, + required this.offset, + }); + + final List items; + final int total; + final int limit; + final int offset; + + factory Page.fromJson( + Map j, + T Function(Map) itemFromJson, + ) { + final raw = (j['items'] as List?) ?? const []; + return Page( + items: raw + .map((e) => itemFromJson((e as Map).cast())) + .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; +} diff --git a/flutter_client/lib/models/search_response.dart b/flutter_client/lib/models/search_response.dart new file mode 100644 index 00000000..cf61e5a0 --- /dev/null +++ b/flutter_client/lib/models/search_response.dart @@ -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 artists; + final Page albums; + final Page tracks; + + factory SearchResponse.fromJson(Map j) => SearchResponse( + artists: Page.fromJson( + (j['artists'] as Map?)?.cast() ?? const {}, + ArtistRef.fromJson, + ), + albums: Page.fromJson( + (j['albums'] as Map?)?.cast() ?? const {}, + AlbumRef.fromJson, + ), + tracks: Page.fromJson( + (j['tracks'] as Map?)?.cast() ?? const {}, + TrackRef.fromJson, + ), + ); + + bool get isEmpty => + artists.items.isEmpty && albums.items.isEmpty && tracks.items.isEmpty; +} diff --git a/flutter_client/lib/player/audio_handler.dart b/flutter_client/lib/player/audio_handler.dart index 81048a2d..3e0a86ef 100644 --- a/flutter_client/lib/player/audio_handler.dart +++ b/flutter_client/lib/player/audio_handler.dart @@ -34,8 +34,8 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl return AudioSource.uri(Uri.parse(url), headers: headers); }).toList(); - await _player.setAudioSource( - ConcatenatingAudioSource(children: sources), + await _player.setAudioSources( + sources, initialIndex: initialIndex, ); } diff --git a/flutter_client/lib/player/now_playing_screen.dart b/flutter_client/lib/player/now_playing_screen.dart index 7c192a5b..138974f7 100644 --- a/flutter_client/lib/player/now_playing_screen.dart +++ b/flutter_client/lib/player/now_playing_screen.dart @@ -10,8 +10,8 @@ class NowPlayingScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final fs = Theme.of(context).extension()!; - final media = ref.watch(mediaItemProvider).valueOrNull; - final playback = ref.watch(playbackStateProvider).valueOrNull; + final media = ref.watch(mediaItemProvider).value; + final playback = ref.watch(playbackStateProvider).value; if (media == null) { return const Scaffold(body: Center(child: Text('Nothing playing.'))); diff --git a/flutter_client/lib/player/player_bar.dart b/flutter_client/lib/player/player_bar.dart index fbda3315..810b3e7e 100644 --- a/flutter_client/lib/player/player_bar.dart +++ b/flutter_client/lib/player/player_bar.dart @@ -11,8 +11,8 @@ class PlayerBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final fs = Theme.of(context).extension()!; - final mediaItem = ref.watch(mediaItemProvider).valueOrNull; - final playback = ref.watch(playbackStateProvider).valueOrNull; + final mediaItem = ref.watch(mediaItemProvider).value; + final playback = ref.watch(playbackStateProvider).value; if (mediaItem == null) return const SizedBox.shrink(); return Material( diff --git a/flutter_client/lib/search/search_provider.dart b/flutter_client/lib/search/search_provider.dart new file mode 100644 index 00000000..b02021d1 --- /dev/null +++ b/flutter_client/lib/search/search_provider.dart @@ -0,0 +1,37 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../api/endpoints/search.dart'; +import '../library/library_providers.dart' show dioProvider; +import '../models/search_response.dart'; + +final searchApiProvider = FutureProvider((ref) async { + return SearchApi(await ref.watch(dioProvider.future)); +}); + +/// Current search query text. The screen's TextField writes here on +/// every keystroke; the resultsProvider below debounces. +class SearchQueryNotifier extends Notifier { + @override + String build() => ''; + void set(String q) => state = q; +} + +final searchQueryProvider = + NotifierProvider(SearchQueryNotifier.new); + +/// Debounced search results. Returns null for empty queries so the +/// screen can show a neutral empty state instead of "0 results." +/// +/// Debounce: 250ms. After the wait, re-reads the live query — if the +/// user has typed more in that window, this attempt is silently +/// abandoned (returns null) and a newer attempt fires on the next +/// rebuild. No request hits the server while the user is mid-typing. +final searchResultsProvider = FutureProvider((ref) async { + final q = ref.watch(searchQueryProvider).trim(); + if (q.isEmpty) return null; + await Future.delayed(const Duration(milliseconds: 250)); + // Re-read the (possibly newer) query; if the user typed more, bail. + if (ref.read(searchQueryProvider).trim() != q) return null; + final api = await ref.watch(searchApiProvider.future); + return api.search(q); +}); diff --git a/flutter_client/lib/search/search_screen.dart b/flutter_client/lib/search/search_screen.dart new file mode 100644 index 00000000..e3473d27 --- /dev/null +++ b/flutter_client/lib/search/search_screen.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../library/widgets/album_card.dart'; +import '../library/widgets/artist_card.dart'; +import '../library/widgets/track_row.dart'; +import '../models/search_response.dart'; +import '../player/player_provider.dart'; +import '../theme/theme_extension.dart'; +import 'search_provider.dart'; + +class SearchScreen extends ConsumerStatefulWidget { + const SearchScreen({super.key}); + + @override + ConsumerState createState() => _SearchScreenState(); +} + +class _SearchScreenState extends ConsumerState { + final _controller = TextEditingController(); + final _focus = FocusNode(); + + @override + void initState() { + super.initState(); + // Autofocus the field on screen entry; keyboard pops up so the + // user can start typing immediately. + WidgetsBinding.instance.addPostFrameCallback((_) => _focus.requestFocus()); + } + + @override + void dispose() { + _controller.dispose(); + _focus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final fs = Theme.of(context).extension()!; + final results = ref.watch(searchResultsProvider); + return Scaffold( + backgroundColor: fs.obsidian, + appBar: AppBar( + backgroundColor: fs.obsidian, + elevation: 0, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: fs.parchment), + onPressed: () => context.pop(), + ), + title: TextField( + controller: _controller, + focusNode: _focus, + autofocus: true, + style: TextStyle(color: fs.parchment), + cursorColor: fs.accent, + decoration: InputDecoration( + hintText: 'Search artists, albums, tracks', + hintStyle: TextStyle(color: fs.ash), + border: InputBorder.none, + ), + onChanged: (v) => ref.read(searchQueryProvider.notifier).set(v), + textInputAction: TextInputAction.search, + ), + actions: [ + if (_controller.text.isNotEmpty) + IconButton( + icon: Icon(Icons.clear, color: fs.ash), + onPressed: () { + _controller.clear(); + ref.read(searchQueryProvider.notifier).set(''); + _focus.requestFocus(); + }, + ), + ], + ), + body: results.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text('$e', style: TextStyle(color: fs.error)), + ), + ), + data: (r) => r == null + ? const _Hint(message: 'Type to search your library.') + : r.isEmpty + ? const _Hint(message: 'No matches.') + : _Results(results: r), + ), + ); + } +} + +class _Hint extends StatelessWidget { + const _Hint({required this.message}); + final String message; + + @override + Widget build(BuildContext context) { + final fs = Theme.of(context).extension()!; + return Center( + child: Text(message, style: TextStyle(color: fs.ash)), + ); + } +} + +class _Results extends ConsumerWidget { + const _Results({required this.results}); + final SearchResponse results; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListView( + children: [ + if (results.artists.items.isNotEmpty) ...[ + _Header(label: 'Artists', count: results.artists.total), + SizedBox( + height: 168, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8), + itemCount: results.artists.items.length, + itemBuilder: (ctx, i) { + final a = results.artists.items[i]; + return ArtistCard( + artist: a, + onTap: () => ctx.push('/artists/${a.id}'), + ); + }, + ), + ), + ], + if (results.albums.items.isNotEmpty) ...[ + _Header(label: 'Albums', count: results.albums.total), + SizedBox( + height: 200, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8), + itemCount: results.albums.items.length, + itemBuilder: (ctx, i) { + final a = results.albums.items[i]; + return AlbumCard( + album: a, + onTap: () => ctx.push('/albums/${a.id}'), + ); + }, + ), + ), + ], + if (results.tracks.items.isNotEmpty) ...[ + _Header(label: 'Tracks', count: results.tracks.total), + ...results.tracks.items.asMap().entries.map((e) { + final i = e.key; + final t = e.value; + return TrackRow( + track: t, + onTap: () => ref + .read(playerActionsProvider) + .playTracks(results.tracks.items, initialIndex: i), + ); + }), + ], + const SizedBox(height: 96), + ], + ); + } +} + +class _Header extends StatelessWidget { + const _Header({required this.label, required this.count}); + final String label; + final int count; + + @override + Widget build(BuildContext context) { + final fs = Theme.of(context).extension()!; + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Row( + children: [ + Text( + label, + style: TextStyle( + color: fs.parchment, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + Text('$count', style: TextStyle(color: fs.ash, fontSize: 13)), + ], + ), + ); + } +} diff --git a/flutter_client/lib/shared/routing.dart b/flutter_client/lib/shared/routing.dart index a23a980d..23baa13e 100644 --- a/flutter_client/lib/shared/routing.dart +++ b/flutter_client/lib/shared/routing.dart @@ -10,6 +10,7 @@ import '../library/artist_detail_screen.dart'; import '../library/home_screen.dart'; import '../player/now_playing_screen.dart'; import '../player/player_bar.dart'; +import '../search/search_screen.dart'; import 'widgets/version_gate.dart'; /// Exposed as a Provider so its single argument is a real `Ref` (the @@ -51,6 +52,7 @@ GoRouter buildRouter(Ref ref) { builder: (_, s) => AlbumDetailScreen(id: s.pathParameters['id']!), ), GoRoute(path: '/now-playing', builder: (_, __) => const NowPlayingScreen()), + GoRoute(path: '/search', builder: (_, __) => const SearchScreen()), ], ), ], diff --git a/flutter_client/pubspec.lock b/flutter_client/pubspec.lock index b83fd40c..22e4a936 100644 --- a/flutter_client/pubspec.lock +++ b/flutter_client/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" + url: "https://pub.dev" + source: hosted + version: "93.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b + url: "https://pub.dev" + source: hosted + version: "10.0.1" args: dependency: transitive description: @@ -65,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -89,6 +113,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" crypto: dependency: transitive description: @@ -170,66 +210,66 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "6.0.0" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + sha256: "4e166be88e1dbbaa34a280bdb744aeae73b7ef25fdf8db7a3bb776760a3648e2" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "3.3.1" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + sha256: "8b302d17096ba88f911b7eb317c71d5e691da60a259549f42b38c658d1776d87" url: "https://pub.dev" source: hosted - version: "9.2.4" + version: "10.1.0" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "3af15a3cb2bf5b8b776832bd01776f8018766aece55623176e28b406481fb320" + url: "https://pub.dev" + source: hosted + version: "0.3.0" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" url: "https://pub.dev" source: hosted - version: "1.2.3" - flutter_secure_storage_macos: - dependency: transitive - description: - name: flutter_secure_storage_macos - sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" - url: "https://pub.dev" - source: hosted - version: "3.1.3" + version: "3.0.0" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.0.1" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "2.1.1" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "4.1.0" flutter_svg: dependency: "direct main" description: @@ -248,6 +288,14 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" glob: dependency: transitive description: @@ -260,18 +308,18 @@ packages: dependency: "direct main" description: name: go_router - sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + sha256: "92d8cee7c57dff0a6c409c05597b460002434eccf7424a712283225b3962d03f" url: "https://pub.dev" source: hosted - version: "14.8.1" + version: "17.2.3" google_fonts: dependency: "direct main" description: name: google_fonts - sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + sha256: "4e9391085e524954a51e3625b7c9c7e9851dc3f376603208bb45c24b9a66255d" url: "https://pub.dev" source: hosted - version: "6.3.3" + version: "8.1.0" hooks: dependency: transitive description: @@ -288,6 +336,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" http_parser: dependency: transitive description: @@ -296,6 +352,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" jni: dependency: transitive description: @@ -324,10 +388,10 @@ packages: dependency: "direct main" description: name: just_audio - sha256: f978d5b4ccea08f267dae0232ec5405c1b05d3f3cd63f82097ea46c015d5c09e + sha256: "9694e4734f515f2a052493d1d7e0d6de219ee0427c7c29492e246ff32a219908" url: "https://pub.dev" source: hosted - version: "0.9.46" + version: "0.10.5" just_audio_platform_interface: dependency: transitive description: @@ -372,10 +436,10 @@ packages: dependency: transitive description: name: lints - sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "6.1.0" logging: dependency: transitive description: @@ -432,6 +496,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.17.6" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" objective_c: dependency: transitive description: @@ -552,6 +624,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" pub_semver: dependency: "direct main" description: @@ -572,10 +652,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + sha256: "8c22216be8ad3ef2b44af3a329693558c98eca7b8bd4ef495c92db0bba279f83" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "3.2.1" rxdart: dependency: transitive description: @@ -584,11 +664,59 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: @@ -685,6 +813,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + test: + dependency: transitive + description: + name: test + sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + url: "https://pub.dev" + source: hosted + version: "1.30.0" test_api: dependency: transitive description: @@ -693,6 +829,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + url: "https://pub.dev" + source: hosted + version: "0.6.16" typed_data: dependency: transitive description: @@ -749,6 +893,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" web: dependency: transitive description: @@ -757,6 +909,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" win32: dependency: transitive description: diff --git a/flutter_client/pubspec.yaml b/flutter_client/pubspec.yaml index 30e6378d..713f2755 100644 --- a/flutter_client/pubspec.yaml +++ b/flutter_client/pubspec.yaml @@ -10,22 +10,24 @@ environment: dependencies: flutter: sdk: flutter - flutter_riverpod: ^2.5.1 + flutter_riverpod: ^3.3.1 dio: ^5.7.0 - just_audio: ^0.9.40 + just_audio: ^0.10.5 audio_service: ^0.18.15 - flutter_secure_storage: ^9.2.2 - go_router: ^14.6.1 + flutter_secure_storage: ^10.1.0 + go_router: ^17.2.3 flutter_svg: ^2.0.16 - google_fonts: ^6.2.1 - package_info_plus: ^8.0.0 + google_fonts: ^8.1.0 + # 10.x conflicts with flutter_secure_storage 10.x on win32. Hold at 8.3.1 + # until either lib bumps win32 to 6.x. + package_info_plus: ^8.3.1 pub_semver: ^2.1.4 cupertino_icons: ^1.0.8 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^4.0.0 + flutter_lints: ^6.0.0 mocktail: ^1.0.4 flutter: