Files
plezy/lib/widgets/video_controls/sheets/queue_sheet.dart
T

138 lines
4.8 KiB
Dart

import 'package:flutter/material.dart';
import '../../../media/ids.dart';
import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import '../../../i18n/strings.g.dart';
import '../../../media/media_item.dart';
import '../../../providers/playback_state_provider.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/media_selector_thumbnail.dart';
import 'base_video_control_sheet.dart';
import '../../optimized_media_image.dart';
const _kThumbWidth = 60.0;
const _kThumbHeight = 34.0;
/// Bottom sheet for viewing and navigating the play queue
class QueueSheet extends StatefulWidget {
final Function(MediaItem) onItemSelected;
const QueueSheet({super.key, required this.onItemSelected});
@override
State<QueueSheet> createState() => _QueueSheetState();
}
class _QueueSheetState extends State<QueueSheet> {
final _initialScroll = InitialItemScrollController();
@override
void dispose() {
_initialScroll.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return 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);
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,
),
maxLines: 1,
overflow: .ellipsis,
),
subtitle: Text(
_buildSubtitle(item),
style: TextStyle(
color: isCurrent ? primaryColor.withValues(alpha: 0.7) : tokens(context).textMuted,
fontSize: 12,
),
maxLines: 1,
overflow: .ellipsis,
),
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);
},
);
}
Widget? _buildThumbnail(BuildContext context, MediaItem item, bool isCurrent) {
if (item.thumbPath == null) return null;
// Try to get client for thumbnails, may fail in offline mode
final client = context.tryGetMediaClientForServer(serverIdOrNull(item.serverId));
return MediaSelectorThumbnail(
width: _kThumbWidth,
height: _kThumbHeight,
thumbnail: OptimizedMediaImage.thumb(
client: client,
imagePath: item.thumbPath,
width: _kThumbWidth,
height: _kThumbHeight,
fit: BoxFit.cover,
errorWidget: (context, url, error) =>
AppIcon(Symbols.image_rounded, fill: 1, color: Colors.white54, size: _kThumbHeight),
),
isCurrent: isCurrent,
borderColor: Theme.of(context).colorScheme.primary,
);
}
String _buildSubtitle(MediaItem item) {
if (item.grandparentTitle != null && item.parentIndex != null && item.index != null) {
return '${item.grandparentTitle} \u00b7 S${item.parentIndex}E${item.index}';
}
if (item.grandparentTitle != null) {
return item.grandparentTitle!;
}
if (item.year != null) {
final edition = item.editionTitle;
return edition != null ? '${item.year} · $edition' : '${item.year}';
}
return item.kind.name;
}
}