diff --git a/lib/utils/media_quality_labels.dart b/lib/utils/media_quality_labels.dart index 0c368cae..b31984d2 100644 --- a/lib/utils/media_quality_labels.dart +++ b/lib/utils/media_quality_labels.dart @@ -3,6 +3,7 @@ import '../media/media_stream.dart'; import '../media/media_version.dart'; import 'codec_utils.dart'; import 'resolution_label.dart'; +import 'formatters.dart'; List buildMediaQualityLabels(MediaItem item, {int versionIndex = 0}) { final version = _selectedVersion(item.mediaVersions, versionIndex); @@ -26,6 +27,20 @@ List buildMediaQualityLabels(MediaItem item, {int versionIndex = 0}) { return labels; } +String? buildMediaSizeLabel(MediaItem item, {int versionIndex = 0}) { + final version = _selectedVersion(item.mediaVersions, versionIndex); + if (version == null || version.parts.isEmpty) return null; + + var totalBytes = 0; + for (final part in version.parts) { + final sizeBytes = part.sizeBytes; + if (sizeBytes == null || sizeBytes <= 0) return null; + totalBytes += sizeBytes; + } + + return ByteFormatter.formatBytes(totalBytes); +} + String _formatDolbyVision(MediaStream stream) { final profile = stream.dolbyVisionProfile; return profile == null || profile <= 0 ? 'DV' : 'DV P$profile'; @@ -33,7 +48,9 @@ String _formatDolbyVision(MediaStream stream) { MediaVersion? _selectedVersion(List? versions, int versionIndex) { if (versions == null || versions.isEmpty) return null; - if (versionIndex >= 0 && versionIndex < versions.length) return versions[versionIndex]; + if (versionIndex >= 0 && versionIndex < versions.length) { + return versions[versionIndex]; + } return versions.first; } diff --git a/lib/widgets/episode_card.dart b/lib/widgets/episode_card.dart index a760f749..dc2eb1f1 100644 --- a/lib/widgets/episode_card.dart +++ b/lib/widgets/episode_card.dart @@ -125,7 +125,7 @@ class _EpisodeCardState extends State with ContextMenuTapMixin[null, 0, -1]) { + final item = _episodeWithVersion( + MediaVersion( + id: '1', + parts: [ + const MediaPart(id: 'known', sizeBytes: 1024), + MediaPart(id: 'unknown', sizeBytes: invalidSize), + ], + ), + ); + + expect(buildMediaSizeLabel(item), isNull); + } + }); + + test('uses the same requested version index as quality labels', () { + final item = testMediaItem( + id: 'episode-1', + backend: MediaBackend.plex, + kind: MediaKind.episode, + title: 'Episode', + mediaVersions: const [ + MediaVersion( + id: 'small', + parts: [MediaPart(id: 'small-part', sizeBytes: 1024 * 1024 * 1024)], + ), + MediaVersion( + id: 'large', + parts: [MediaPart(id: 'large-part', sizeBytes: 2 * 1024 * 1024 * 1024)], + ), + ], + ); + + expect(buildMediaSizeLabel(item), '1.00 GB'); + expect(buildMediaSizeLabel(item, versionIndex: 1), '2.00 GB'); + }); + }); } MediaItem _episodeWithVersion(MediaVersion? version) { diff --git a/test/widgets/episode_card_test.dart b/test/widgets/episode_card_test.dart index 285067d4..76625456 100644 --- a/test/widgets/episode_card_test.dart +++ b/test/widgets/episode_card_test.dart @@ -6,6 +6,10 @@ import 'package:plezy/database/app_database.dart'; import 'package:plezy/i18n/strings.g.dart'; import 'package:plezy/media/media_backend.dart'; import 'package:plezy/media/media_kind.dart'; +import 'package:plezy/media/media_item.dart'; +import 'package:plezy/media/media_part.dart'; +import 'package:plezy/media/media_stream.dart'; +import 'package:plezy/media/media_version.dart'; import 'package:plezy/providers/download_provider.dart'; import 'package:plezy/services/download_manager_service.dart'; import 'package:plezy/services/download_storage_service.dart'; @@ -50,40 +54,7 @@ void main() { durationMs: 42 * 60 * 1000, ); - final db = AppDatabase.forTesting(NativeDatabase.memory()); - PlexApiCache.initialize(db); - JellyfinApiCache.initialize(db); - final downloadManager = DownloadManagerService( - database: db, - storageService: DownloadStorageService.instance, - clientResolver: (serverId, {clientScopeId}) => null, - ); - downloadManager.recoveryFuture = Future.value(); - final downloadProvider = DownloadProvider.forTesting(downloadManager: downloadManager, database: db); - await downloadProvider.ensureInitialized(); - addTearDown(() async { - downloadProvider.dispose(); - downloadManager.dispose(); - await db.close(); - }); - - await tester.pumpWidget( - TranslationProvider( - child: ChangeNotifierProvider.value( - value: downloadProvider, - child: MaterialApp( - theme: monoTheme(dark: true), - home: Scaffold( - body: SizedBox( - width: 360, - child: EpisodeCard(episode: episode, isOffline: true, onTap: () {}), - ), - ), - ), - ), - ), - ); - await tester.pump(); + await _pumpEpisodeCard(tester, episode); final summaryText = tester.widget( find.descendant(of: find.byType(CollapsibleText), matching: find.byType(Text)).first, @@ -107,4 +78,73 @@ void main() { expect(cardSemantics.getSemanticsData().hasAction(SemanticsAction.tap), isTrue); semantics.dispose(); }); + + testWidgets('shows file size alongside media quality labels', (tester) async { + final episode = testMediaItem( + id: 'sized_episode', + backend: MediaBackend.plex, + kind: MediaKind.episode, + title: 'A Large Episode', + index: 4, + durationMs: 52 * 60 * 1000, + mediaVersions: const [ + MediaVersion( + id: 'source', + videoResolution: '1080', + parts: [ + MediaPart( + id: 'part-1', + sizeBytes: 1536 * 1024 * 1024, + streams: [ + MediaStream(id: 'audio', kind: MediaStreamKind.audio, codec: 'eac3', channels: 6, selected: true), + ], + ), + ], + ), + ], + ); + + await _pumpEpisodeCard(tester, episode); + + final metadataWrap = find.ancestor(of: find.text('1.50 GB'), matching: find.byType(Wrap)); + expect(metadataWrap, findsOneWidget); + expect(find.descendant(of: metadataWrap, matching: find.text('EAC3 5.1')), findsOneWidget); + }); +} + +Future _pumpEpisodeCard(WidgetTester tester, MediaItem episode) async { + final db = AppDatabase.forTesting(NativeDatabase.memory()); + PlexApiCache.initialize(db); + JellyfinApiCache.initialize(db); + final downloadManager = DownloadManagerService( + database: db, + storageService: DownloadStorageService.instance, + clientResolver: (serverId, {clientScopeId}) => null, + ); + downloadManager.recoveryFuture = Future.value(); + final downloadProvider = DownloadProvider.forTesting(downloadManager: downloadManager, database: db); + await downloadProvider.ensureInitialized(); + addTearDown(() async { + downloadProvider.dispose(); + downloadManager.dispose(); + await db.close(); + }); + + await tester.pumpWidget( + TranslationProvider( + child: ChangeNotifierProvider.value( + value: downloadProvider, + child: MaterialApp( + theme: monoTheme(dark: true), + home: Scaffold( + body: SizedBox( + width: 360, + child: EpisodeCard(episode: episode, isOffline: true, onTap: () {}), + ), + ), + ), + ), + ), + ); + await tester.pump(); }