From f60ea726591cf193a77fd193589e3f7ebd3364b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Nov 2025 16:17:09 +0000 Subject: [PATCH] fix: resolve Series deserialization issues and add author sorting - Fix genres error by parsing JSON manually to avoid Series deserialization - Fix series blank screen by using SimpleSeries class instead of full Series - Add alphabetical sorting to authors page - Work around shelfsdk Series variant detection issues The Audiobookshelf API returns Series objects that don't match any of the variant detection patterns in the shelfsdk SeriesConverter. Since we can't modify the external shelfsdk, we parse the API responses manually to extract only the data we need for display. --- lib/api/library_browser_provider.dart | 72 +++++++++++++++---- .../view/library_authors_page.dart | 8 ++- .../view/library_series_page.dart | 31 +++----- 3 files changed, 72 insertions(+), 39 deletions(-) diff --git a/lib/api/library_browser_provider.dart b/lib/api/library_browser_provider.dart index 38aec82..607cb56 100644 --- a/lib/api/library_browser_provider.dart +++ b/lib/api/library_browser_provider.dart @@ -1,13 +1,33 @@ import 'package:hooks_riverpod/hooks_riverpod.dart' show Ref; import 'package:logging/logging.dart' show Logger; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart' - show Author, GetLibrarysSeriesResponse, LibraryFilterData; +import 'package:shelfsdk/audiobookshelf_api.dart' show Author; import 'package:vaani/api/api_provider.dart' show authenticatedApiProvider; import 'package:vaani/api/library_provider.dart' show currentLibraryProvider; part 'library_browser_provider.g.dart'; +/// Simple series data for display purposes +class SimpleSeries { + final String id; + final String name; + final int? numBooks; + + SimpleSeries({ + required this.id, + required this.name, + this.numBooks, + }); + + factory SimpleSeries.fromJson(Map json) { + return SimpleSeries( + id: json['id'] as String, + name: json['name'] as String, + numBooks: json['numBooks'] as int?, + ); + } +} + final _logger = Logger('LibraryBrowserProvider'); /// Provider for fetching all authors in the current library @@ -43,37 +63,59 @@ Future> libraryGenres(Ref ref) async { return []; } - final filterData = await api.libraries.getFilterData(libraryId: currentLibrary.id); + // Use raw API call to avoid Series deserialization issues in LibraryFilterData + final genres = await api.getJson>( + path: '/api/libraries/${currentLibrary.id}/filterdata', + requiresAuth: true, + fromJson: (json) { + if (json is Map && json.containsKey('genres')) { + final genresList = json['genres'] as List; + return genresList.map((e) => e.toString()).toList(); + } + return []; + }, + ); - if (filterData == null) { - _logger.warning('Failed to fetch filter data for library ${currentLibrary.id}'); + if (genres == null) { + _logger.warning('Failed to fetch genres for library ${currentLibrary.id}'); return []; } - _logger.fine('Fetched ${filterData.genres.length} genres'); - return filterData.genres; + _logger.fine('Fetched ${genres.length} genres'); + return genres; } /// Provider for fetching all series in the current library @riverpod -Future librarySeries(Ref ref) async { +Future> librarySeries(Ref ref) async { final api = ref.watch(authenticatedApiProvider); final currentLibrary = await ref.watch(currentLibraryProvider.future); if (currentLibrary == null) { _logger.warning('No current library found'); - return null; + return []; } - final series = await api.libraries.getSeries( - libraryId: currentLibrary.id, + // Use raw API call to avoid Series deserialization issues + final seriesList = await api.getJson>( + path: '/api/libraries/${currentLibrary.id}/series', + requiresAuth: true, + fromJson: (json) { + if (json is Map && json.containsKey('results')) { + final results = json['results'] as List; + return results + .map((e) => SimpleSeries.fromJson(e as Map)) + .toList(); + } + return []; + }, ); - if (series == null) { + if (seriesList == null) { _logger.warning('Failed to fetch series for library ${currentLibrary.id}'); - return null; + return []; } - _logger.fine('Fetched ${series.results.length} series (${series.total} total)'); - return series; + _logger.fine('Fetched ${seriesList.length} series'); + return seriesList; } diff --git a/lib/features/library_browser/view/library_authors_page.dart b/lib/features/library_browser/view/library_authors_page.dart index c01cc8d..7c42ac8 100644 --- a/lib/features/library_browser/view/library_authors_page.dart +++ b/lib/features/library_browser/view/library_authors_page.dart @@ -25,6 +25,10 @@ class LibraryAuthorsPage extends HookConsumerWidget { ); } + // Sort authors alphabetically by name + final sortedAuthors = List.from(authors) + ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + return GridView.builder( padding: const EdgeInsets.all(16), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( @@ -33,9 +37,9 @@ class LibraryAuthorsPage extends HookConsumerWidget { crossAxisSpacing: 16, mainAxisSpacing: 16, ), - itemCount: authors.length, + itemCount: sortedAuthors.length, itemBuilder: (context, index) { - final author = authors[index]; + final author = sortedAuthors[index]; return AuthorCard(author: author); }, ); diff --git a/lib/features/library_browser/view/library_series_page.dart b/lib/features/library_browser/view/library_series_page.dart index 50fbab1..33248c5 100644 --- a/lib/features/library_browser/view/library_series_page.dart +++ b/lib/features/library_browser/view/library_series_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/library_browser_provider.dart'; class LibrarySeriesPage extends HookConsumerWidget { @@ -15,8 +14,8 @@ class LibrarySeriesPage extends HookConsumerWidget { title: const Text('Series'), ), body: seriesAsync.when( - data: (seriesResponse) { - if (seriesResponse == null || seriesResponse.results.isEmpty) { + data: (seriesList) { + if (seriesList.isEmpty) { return const Center( child: Text('No series found'), ); @@ -24,9 +23,9 @@ class LibrarySeriesPage extends HookConsumerWidget { return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: seriesResponse.results.length, + itemCount: seriesList.length, itemBuilder: (context, index) { - final series = seriesResponse.results[index]; + final series = seriesList[index]; return SeriesListTile(series: series); }, ); @@ -55,22 +54,10 @@ class SeriesListTile extends StatelessWidget { required this.series, }); - final Series series; + final SimpleSeries series; @override Widget build(BuildContext context) { - // Extract series data based on variant - final String seriesName = series.name; - final int? numBooks = series.maybeMap( - (s) => null, // base variant - numBooks: (s) => s.numBooks, - books: (s) => s.books.length, - sequence: (s) => null, - shelf: (s) => s.books.length, - author: (s) => s.items?.length, - orElse: () => null, - ); - return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ListTile( @@ -87,18 +74,18 @@ class SeriesListTile extends StatelessWidget { ), ), title: Text( - seriesName, + series.name, style: Theme.of(context).textTheme.titleMedium, ), - subtitle: numBooks != null - ? Text('$numBooks ${numBooks == 1 ? 'book' : 'books'}') + subtitle: series.numBooks != null + ? Text('${series.numBooks} ${series.numBooks == 1 ? 'book' : 'books'}') : null, trailing: const Icon(Icons.chevron_right), onTap: () { // TODO: Navigate to series detail page ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Tapped on $seriesName'), + content: Text('Tapped on ${series.name}'), duration: const Duration(seconds: 1), ), );