@@ -6,12 +6,15 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../i18n/strings.g.dart';
|
||||
import '../../../media/media_item.dart';
|
||||
import '../../../media/media_item_types.dart';
|
||||
import '../../../providers/playback_state_provider.dart';
|
||||
import '../../../services/settings_service.dart';
|
||||
import '../../../theme/mono_tokens.dart';
|
||||
import '../../../utils/provider_extensions.dart';
|
||||
import '../../../utils/scroll_utils.dart';
|
||||
import '../../../widgets/focusable_list_tile.dart';
|
||||
import '../../../widgets/overlay_sheet.dart';
|
||||
import '../../../widgets/settings_builder.dart';
|
||||
import '../widgets/media_selector_thumbnail.dart';
|
||||
import 'base_video_control_sheet.dart';
|
||||
import '../../optimized_media_image.dart';
|
||||
@@ -40,65 +43,68 @@ class _QueueSheetState extends State<QueueSheet> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<PlaybackStateProvider>(
|
||||
builder: (context, playbackState, _) {
|
||||
final items = playbackState.loadedItems;
|
||||
final currentItemID = playbackState.currentPlayQueueItemID;
|
||||
return SettingValueBuilder<bool>(
|
||||
pref: SettingsService.hideSpoilers,
|
||||
builder: (context, hideSpoilers, _) => Consumer<PlaybackStateProvider>(
|
||||
builder: (context, playbackState, _) {
|
||||
final items = playbackState.loadedItems;
|
||||
final currentItemID = playbackState.currentPlayQueueItemID;
|
||||
|
||||
Widget content;
|
||||
if (items.isEmpty) {
|
||||
content = Center(
|
||||
child: Text(t.videoControls.noQueueItems, style: TextStyle(color: tokens(context).textMuted)),
|
||||
);
|
||||
} else {
|
||||
final currentIndex = items.indexWhere((item) => playbackState.playQueueItemIdFor(item) == currentItemID);
|
||||
_initialScroll.maybeScrollTo(currentIndex);
|
||||
Widget content;
|
||||
if (items.isEmpty) {
|
||||
content = Center(
|
||||
child: Text(t.videoControls.noQueueItems, style: TextStyle(color: tokens(context).textMuted)),
|
||||
);
|
||||
} else {
|
||||
final currentIndex = items.indexWhere((item) => playbackState.playQueueItemIdFor(item) == currentItemID);
|
||||
_initialScroll.maybeScrollTo(currentIndex);
|
||||
|
||||
content = ListView.builder(
|
||||
controller: _initialScroll.controller,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final isCurrent = playbackState.playQueueItemIdFor(item) == currentItemID;
|
||||
content = ListView.builder(
|
||||
controller: _initialScroll.controller,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final isCurrent = playbackState.playQueueItemIdFor(item) == currentItemID;
|
||||
|
||||
final primaryColor = Theme.of(context).colorScheme.primary;
|
||||
return FocusableListTile(
|
||||
key: index == 0 ? _initialScroll.firstItemKey : null,
|
||||
leading: _buildThumbnail(context, item, isCurrent),
|
||||
title: Text(
|
||||
item.title ?? '',
|
||||
style: TextStyle(
|
||||
color: isCurrent ? primaryColor : null,
|
||||
fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal,
|
||||
final primaryColor = Theme.of(context).colorScheme.primary;
|
||||
return FocusableListTile(
|
||||
key: index == 0 ? _initialScroll.firstItemKey : null,
|
||||
leading: _buildThumbnail(context, item, isCurrent, hideSpoilers: hideSpoilers),
|
||||
title: Text(
|
||||
item.title ?? '',
|
||||
style: TextStyle(
|
||||
color: isCurrent ? primaryColor : null,
|
||||
fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
_buildSubtitle(item),
|
||||
style: TextStyle(
|
||||
color: isCurrent ? primaryColor.withValues(alpha: 0.7) : tokens(context).textMuted,
|
||||
fontSize: 12,
|
||||
subtitle: Text(
|
||||
_buildSubtitle(item),
|
||||
style: TextStyle(
|
||||
color: isCurrent ? primaryColor.withValues(alpha: 0.7) : tokens(context).textMuted,
|
||||
fontSize: 12,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
),
|
||||
trailing: isCurrent ? AppIcon(Symbols.play_circle_rounded, fill: 1, color: primaryColor) : null,
|
||||
onTap: () {
|
||||
widget.onItemSelected(item);
|
||||
OverlaySheetController.of(context).close();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
trailing: isCurrent ? AppIcon(Symbols.play_circle_rounded, fill: 1, color: primaryColor) : null,
|
||||
onTap: () {
|
||||
widget.onItemSelected(item);
|
||||
OverlaySheetController.of(context).close();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return BaseVideoControlSheet(title: t.videoControls.queue, icon: Symbols.queue_rounded, child: content);
|
||||
},
|
||||
return BaseVideoControlSheet(title: t.videoControls.queue, icon: Symbols.queue_rounded, child: content);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildThumbnail(BuildContext context, MediaItem item, bool isCurrent) {
|
||||
Widget? _buildThumbnail(BuildContext context, MediaItem item, bool isCurrent, {required bool hideSpoilers}) {
|
||||
if (item.thumbPath == null) return null;
|
||||
|
||||
// Try to get client for thumbnails, may fail in offline mode
|
||||
@@ -118,6 +124,7 @@ class _QueueSheetState extends State<QueueSheet> {
|
||||
),
|
||||
isCurrent: isCurrent,
|
||||
borderColor: Theme.of(context).colorScheme.primary,
|
||||
blurThumbnail: hideSpoilers && item.shouldHideSpoiler,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,12 @@ import '../../../focus/dpad_navigator.dart';
|
||||
import '../../../focus/focusable_wrapper.dart';
|
||||
import '../../../i18n/strings.g.dart';
|
||||
import '../../../media/media_item.dart';
|
||||
import '../../../media/media_item_types.dart';
|
||||
import '../../../media/media_server_client.dart';
|
||||
import '../../../mpv/mpv.dart';
|
||||
import '../../../media/media_source_info.dart';
|
||||
import '../../../providers/playback_state_provider.dart';
|
||||
import '../../../services/settings_service.dart';
|
||||
import '../../../services/download_storage_service.dart';
|
||||
import '../../../utils/formatters.dart';
|
||||
import '../../../utils/player_utils.dart';
|
||||
@@ -22,6 +24,7 @@ import '../../../utils/scroll_utils.dart';
|
||||
import '../../app_icon.dart';
|
||||
import '../../clickable_cursor.dart';
|
||||
import '../../optimized_media_image.dart';
|
||||
import '../../settings_builder.dart';
|
||||
import 'media_selector_thumbnail.dart';
|
||||
|
||||
/// Horizontal scrollable strip of chapter/queue items shown on swipe-up.
|
||||
@@ -475,93 +478,97 @@ class ContentStripState extends State<ContentStrip> {
|
||||
final thumbWidth = isTablet ? 200.0 : 120.0;
|
||||
final thumbHeight = isTablet ? 112.0 : 68.0;
|
||||
|
||||
return Consumer<PlaybackStateProvider>(
|
||||
builder: (context, playbackState, _) {
|
||||
final items = playbackState.loadedItems;
|
||||
final currentItemID = playbackState.currentPlayQueueItemID;
|
||||
final currentIndex = currentItemID == null
|
||||
? -1
|
||||
: items.indexWhere((item) => playbackState.playQueueItemIdFor(item) == currentItemID);
|
||||
return SettingValueBuilder<bool>(
|
||||
pref: SettingsService.hideSpoilers,
|
||||
builder: (context, hideSpoilers, _) => Consumer<PlaybackStateProvider>(
|
||||
builder: (context, playbackState, _) {
|
||||
final items = playbackState.loadedItems;
|
||||
final currentItemID = playbackState.currentPlayQueueItemID;
|
||||
final currentIndex = currentItemID == null
|
||||
? -1
|
||||
: items.indexWhere((item) => playbackState.playQueueItemIdFor(item) == currentItemID);
|
||||
|
||||
_trimItemKeys(_queueItemKeys, items.length);
|
||||
_trimItemKeys(_queueItemKeys, items.length);
|
||||
|
||||
if (currentIndex >= 0 &&
|
||||
(_lastAutoScrolledQueueItemID != currentItemID || _lastAutoScrolledQueueIndex != currentIndex)) {
|
||||
_lastAutoScrolledQueueItemID = currentItemID;
|
||||
_lastAutoScrolledQueueIndex = currentIndex;
|
||||
_autoScrollTo(
|
||||
_queueScrollController,
|
||||
_queueItemKeys,
|
||||
currentIndex,
|
||||
isTablet: isTablet,
|
||||
isCurrent: () =>
|
||||
_lastAutoScrolledQueueItemID == currentItemID && _lastAutoScrolledQueueIndex == currentIndex,
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.useFocusNavigation) {
|
||||
_ensureFocusNodes(_queueFocusNodes, items.length, 'QueueFocus');
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
controller: _queueScrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
clipBehavior: widget.useFocusNavigation ? Clip.none : Clip.hardEdge,
|
||||
itemCount: items.length,
|
||||
padding: .symmetric(horizontal: widget.useFocusNavigation ? 12 : 4),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final isCurrent = playbackState.playQueueItemIdFor(item) == currentItemID;
|
||||
|
||||
final client = item.serverId != null
|
||||
? context.tryGetMediaClientForServer(serverIdOrNull(item.serverId))
|
||||
: null;
|
||||
|
||||
void onTap() => widget.onQueueItemSelected?.call(item);
|
||||
|
||||
final itemKey = _itemKeyFor(_queueItemKeys, index);
|
||||
final stripItem = _buildStripItem(
|
||||
key: itemKey,
|
||||
isCurrent: isCurrent,
|
||||
if (currentIndex >= 0 &&
|
||||
(_lastAutoScrolledQueueItemID != currentItemID || _lastAutoScrolledQueueIndex != currentIndex)) {
|
||||
_lastAutoScrolledQueueItemID = currentItemID;
|
||||
_lastAutoScrolledQueueIndex = currentIndex;
|
||||
_autoScrollTo(
|
||||
_queueScrollController,
|
||||
_queueItemKeys,
|
||||
currentIndex,
|
||||
isTablet: isTablet,
|
||||
thumbnail: item.thumbPath != null
|
||||
? OptimizedMediaImage.thumb(
|
||||
client: client,
|
||||
imagePath: item.thumbPath,
|
||||
width: thumbWidth,
|
||||
height: thumbHeight,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, _, _) =>
|
||||
const AppIcon(Symbols.image_rounded, fill: 1, color: Colors.white54, size: 34),
|
||||
)
|
||||
: null,
|
||||
title: item.title ?? '',
|
||||
subtitle: _buildQueueSubtitle(item),
|
||||
onTap: onTap,
|
||||
isCurrent: () =>
|
||||
_lastAutoScrolledQueueItemID == currentItemID && _lastAutoScrolledQueueIndex == currentIndex,
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.useFocusNavigation) {
|
||||
return Align(
|
||||
alignment: .topCenter,
|
||||
child: FocusableWrapper(
|
||||
focusNode: _queueFocusNodes[index],
|
||||
onSelect: onTap,
|
||||
onKeyEvent: (_, event) => _handleFocusItemKeyEvent(event, index, items.length, _StripTab.queue),
|
||||
onFocusChange: (hasFocus) {
|
||||
if (hasFocus) widget.onFocusActivity?.call();
|
||||
},
|
||||
borderRadius: 6,
|
||||
autoScroll: false,
|
||||
useBackgroundFocus: true,
|
||||
child: stripItem,
|
||||
),
|
||||
if (widget.useFocusNavigation) {
|
||||
_ensureFocusNodes(_queueFocusNodes, items.length, 'QueueFocus');
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
controller: _queueScrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
clipBehavior: widget.useFocusNavigation ? Clip.none : Clip.hardEdge,
|
||||
itemCount: items.length,
|
||||
padding: .symmetric(horizontal: widget.useFocusNavigation ? 12 : 4),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final isCurrent = playbackState.playQueueItemIdFor(item) == currentItemID;
|
||||
|
||||
final client = item.serverId != null
|
||||
? context.tryGetMediaClientForServer(serverIdOrNull(item.serverId))
|
||||
: null;
|
||||
|
||||
void onTap() => widget.onQueueItemSelected?.call(item);
|
||||
|
||||
final itemKey = _itemKeyFor(_queueItemKeys, index);
|
||||
final stripItem = _buildStripItem(
|
||||
key: itemKey,
|
||||
isCurrent: isCurrent,
|
||||
isTablet: isTablet,
|
||||
thumbnail: item.thumbPath != null
|
||||
? OptimizedMediaImage.thumb(
|
||||
client: client,
|
||||
imagePath: item.thumbPath,
|
||||
width: thumbWidth,
|
||||
height: thumbHeight,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, _, _) =>
|
||||
const AppIcon(Symbols.image_rounded, fill: 1, color: Colors.white54, size: 34),
|
||||
)
|
||||
: null,
|
||||
blurThumbnail: hideSpoilers && item.shouldHideSpoiler,
|
||||
title: item.title ?? '',
|
||||
subtitle: _buildQueueSubtitle(item),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
return stripItem;
|
||||
},
|
||||
);
|
||||
},
|
||||
if (widget.useFocusNavigation) {
|
||||
return Align(
|
||||
alignment: .topCenter,
|
||||
child: FocusableWrapper(
|
||||
focusNode: _queueFocusNodes[index],
|
||||
onSelect: onTap,
|
||||
onKeyEvent: (_, event) => _handleFocusItemKeyEvent(event, index, items.length, _StripTab.queue),
|
||||
onFocusChange: (hasFocus) {
|
||||
if (hasFocus) widget.onFocusActivity?.call();
|
||||
},
|
||||
borderRadius: 6,
|
||||
autoScroll: false,
|
||||
useBackgroundFocus: true,
|
||||
child: stripItem,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return stripItem;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -584,6 +591,7 @@ class ContentStripState extends State<ContentStrip> {
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required VoidCallback onTap,
|
||||
bool blurThumbnail = false,
|
||||
bool isTablet = false,
|
||||
}) {
|
||||
final itemWidth = isTablet ? 200.0 : 120.0;
|
||||
@@ -610,6 +618,7 @@ class ContentStripState extends State<ContentStrip> {
|
||||
isCurrent: isCurrent,
|
||||
borderColor: Colors.white,
|
||||
radius: 6,
|
||||
blurThumbnail: blurThumbnail,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
@@ -14,6 +16,7 @@ class MediaSelectorThumbnail extends StatelessWidget {
|
||||
final Color fallbackIconColor;
|
||||
final double fallbackIconSize;
|
||||
final IconData fallbackIcon;
|
||||
final bool blurThumbnail;
|
||||
|
||||
const MediaSelectorThumbnail({
|
||||
super.key,
|
||||
@@ -27,27 +30,27 @@ class MediaSelectorThumbnail extends StatelessWidget {
|
||||
this.fallbackIconColor = Colors.white38,
|
||||
this.fallbackIconSize = 28,
|
||||
this.fallbackIcon = Symbols.movie_rounded,
|
||||
this.blurThumbnail = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderRadius = BorderRadius.all(Radius.circular(radius));
|
||||
final child = thumbnail != null
|
||||
? _maybeBlurThumbnail(thumbnail!)
|
||||
: Container(
|
||||
color: fallbackBackgroundColor,
|
||||
child: Center(
|
||||
child: AppIcon(fallbackIcon, fill: 1, color: fallbackIconColor, size: fallbackIconSize),
|
||||
),
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child:
|
||||
thumbnail ??
|
||||
Container(
|
||||
color: fallbackBackgroundColor,
|
||||
child: Center(
|
||||
child: AppIcon(fallbackIcon, fill: 1, color: fallbackIconColor, size: fallbackIconSize),
|
||||
),
|
||||
),
|
||||
),
|
||||
ClipRRect(borderRadius: borderRadius, child: child),
|
||||
if (isCurrent)
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
@@ -61,4 +64,11 @@ class MediaSelectorThumbnail extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _maybeBlurThumbnail(Widget child) {
|
||||
if (!blurThumbnail) return child;
|
||||
return ClipRect(
|
||||
child: ImageFiltered(imageFilter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), child: child),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/i18n/strings.g.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/play_queue.dart';
|
||||
import 'package:plezy/mpv/mpv.dart';
|
||||
import 'package:plezy/providers/playback_state_provider.dart';
|
||||
import 'package:plezy/services/settings_service.dart';
|
||||
import 'package:plezy/theme/mono_tokens.dart';
|
||||
import 'package:plezy/widgets/video_controls/sheets/queue_sheet.dart';
|
||||
import 'package:plezy/widgets/video_controls/widgets/content_strip.dart';
|
||||
import 'package:plezy/widgets/video_controls/widgets/media_selector_thumbnail.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
const _testTokens = MonoTokens(
|
||||
radiusSm: 8,
|
||||
radiusMd: 12,
|
||||
space: 8,
|
||||
fast: Duration(milliseconds: 1),
|
||||
normal: Duration(milliseconds: 1),
|
||||
slow: Duration(milliseconds: 1),
|
||||
bg: Colors.black,
|
||||
surface: Colors.black,
|
||||
outline: Colors.white24,
|
||||
text: Colors.white,
|
||||
textMuted: Colors.white70,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
);
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() async {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
resetSharedPreferencesForTest();
|
||||
SettingsService.resetForTesting();
|
||||
await SettingsService.getInstance();
|
||||
});
|
||||
|
||||
testWidgets('MediaSelectorThumbnail applies blur only to real thumbnails', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: MediaSelectorThumbnail(
|
||||
width: 60,
|
||||
height: 34,
|
||||
thumbnail: ColoredBox(color: Colors.red),
|
||||
isCurrent: true,
|
||||
borderColor: Colors.white,
|
||||
blurThumbnail: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ImageFiltered), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: MediaSelectorThumbnail(
|
||||
width: 60,
|
||||
height: 34,
|
||||
thumbnail: null,
|
||||
isCurrent: true,
|
||||
borderColor: Colors.white,
|
||||
blurThumbnail: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ImageFiltered), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('content strip queue blurs spoiler episode thumbnails', (tester) async {
|
||||
await SettingsService.instance.write(SettingsService.hideSpoilers, true);
|
||||
final playback = _playbackWithQueue();
|
||||
addTearDown(playback.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_queueHarness(
|
||||
playback: playback,
|
||||
child: ContentStrip(
|
||||
player: _FakePlayer(),
|
||||
chapters: const [],
|
||||
chaptersLoaded: true,
|
||||
showQueueTab: true,
|
||||
onQueueItemSelected: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final thumbnails = tester.widgetList<MediaSelectorThumbnail>(find.byType(MediaSelectorThumbnail)).toList();
|
||||
|
||||
expect(thumbnails.map((thumbnail) => thumbnail.blurThumbnail), [true, false, false]);
|
||||
});
|
||||
|
||||
testWidgets('queue sheet blurs spoiler episode thumbnails', (tester) async {
|
||||
await SettingsService.instance.write(SettingsService.hideSpoilers, true);
|
||||
final playback = _playbackWithQueue();
|
||||
addTearDown(playback.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_queueHarness(
|
||||
playback: playback,
|
||||
child: QueueSheet(onItemSelected: (_) {}),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final thumbnails = tester.widgetList<MediaSelectorThumbnail>(find.byType(MediaSelectorThumbnail)).toList();
|
||||
|
||||
expect(thumbnails.map((thumbnail) => thumbnail.blurThumbnail), [true, false, false]);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _queueHarness({required PlaybackStateProvider playback, required Widget child}) {
|
||||
return ChangeNotifierProvider<PlaybackStateProvider>.value(
|
||||
value: playback,
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(extensions: const [_testTokens]),
|
||||
home: Scaffold(body: SizedBox(width: 600, height: 400, child: child)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PlaybackStateProvider _playbackWithQueue() {
|
||||
final playback = PlaybackStateProvider();
|
||||
playback.setPlaybackFromLocalQueue(
|
||||
LocalPlayQueue(
|
||||
id: 'test-queue',
|
||||
backendId: MediaBackend.plex.id,
|
||||
currentIndex: 0,
|
||||
items: [
|
||||
_episode('spoiler-episode', title: 'Spoiler Episode'),
|
||||
_episode('watched-episode', title: 'Watched Episode', viewCount: 1),
|
||||
MediaItem(
|
||||
id: 'movie',
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.movie,
|
||||
title: 'Movie',
|
||||
thumbPath: 'https://example.invalid/movie.jpg',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return playback;
|
||||
}
|
||||
|
||||
MediaItem _episode(String id, {required String title, int? viewCount}) {
|
||||
return MediaItem(
|
||||
id: id,
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.episode,
|
||||
title: title,
|
||||
grandparentTitle: 'Show',
|
||||
parentIndex: 1,
|
||||
index: 1,
|
||||
viewCount: viewCount,
|
||||
thumbPath: 'https://example.invalid/$id.jpg',
|
||||
);
|
||||
}
|
||||
|
||||
class _FakePlayer implements Player {
|
||||
@override
|
||||
PlayerState get state => PlayerState();
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
Reference in New Issue
Block a user