Files
Ensemble/lib/screens/queue_screen.dart
ChrisandClaude Opus 4.5 a4cc7115bf chore: Localization audit and debug cleanup
- Localize hardcoded strings in search_screen.dart (21 strings)
- Localize language names in settings_screen.dart
- Localize queue screen messages and tooltips
- Localize widget strings (queue_panel, debug_console)
- Add 15 new localization keys to app_en.arb
- Remove all debugPrint statements from widget files
  - expandable_player.dart (11 removed)
  - queue_panel.dart (8 removed)
  - player_selector.dart (2 removed)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 08:58:31 +00:00

366 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/music_assistant_provider.dart';
import '../models/player.dart';
import '../services/debug_logger.dart';
import '../widgets/common/empty_state.dart';
import '../widgets/player_picker_sheet.dart';
import '../l10n/app_localizations.dart';
class QueueScreen extends StatefulWidget {
const QueueScreen({super.key});
@override
State<QueueScreen> createState() => _QueueScreenState();
}
class _QueueScreenState extends State<QueueScreen> {
final _logger = DebugLogger();
PlayerQueue? _queue;
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
// Use addPostFrameCallback to safely access context after build
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadQueue();
});
}
Future<void> _loadQueue() async {
if (!mounted) return;
setState(() {
_isLoading = true;
_error = null;
});
try {
final maProvider = context.read<MusicAssistantProvider>();
final player = maProvider.selectedPlayer;
if (player != null && maProvider.api != null) {
final queue = await maProvider.api!.getQueue(player.playerId);
if (mounted) {
setState(() {
_queue = queue;
_isLoading = false;
});
}
} else {
if (mounted) {
setState(() {
_isLoading = false;
_error = S.of(context)!.noPlayerSelected;
});
}
}
} catch (e) {
_logger.log('QueueScreen error loading queue: $e');
if (mounted) {
setState(() {
_isLoading = false;
_error = 'Failed to load queue: $e';
});
}
}
}
Future<void> _handleTransferQueue(MusicAssistantProvider maProvider) async {
final player = maProvider.selectedPlayer;
if (player == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(S.of(context)!.noPlayerSelected)),
);
return;
}
// Get available players, excluding the current source player
final allPlayers = maProvider.availablePlayers;
final targetPlayers = allPlayers
.where((p) => p.playerId != player.playerId && p.available)
.toList();
if (targetPlayers.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(S.of(context)!.noOtherPlayersAvailable),
duration: const Duration(seconds: 2),
),
);
return;
}
// Show player picker sheet
await showPlayerPickerSheet(
context: context,
title: S.of(context)!.transferQueueTo,
players: targetPlayers,
onPlayerSelected: (targetPlayer) async {
try {
await maProvider.api?.transferQueue(
sourceQueueId: player.playerId,
targetQueueId: targetPlayer.playerId,
autoPlay: true,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(S.of(context)!.queueTransferredTo(targetPlayer.name)),
duration: const Duration(seconds: 2),
),
);
}
} catch (e) {
_logger.log('Error transferring queue: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(S.of(context)!.failedToTransferQueue(e.toString())),
backgroundColor: Colors.red.shade700,
duration: const Duration(seconds: 3),
),
);
}
}
},
);
}
@override
Widget build(BuildContext context) {
final maProvider = context.watch<MusicAssistantProvider>();
final player = maProvider.selectedPlayer;
return Scaffold(
backgroundColor: const Color(0xFF1a1a1a),
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text(
player != null ? S.of(context)!.playerQueue(player.name) : S.of(context)!.queue,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w300,
),
),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.swap_horiz_rounded),
tooltip: S.of(context)!.transferQueue,
onPressed: () => _handleTransferQueue(maProvider),
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadQueue,
),
],
),
body: _buildBody(maProvider),
);
}
Widget _buildBody(MusicAssistantProvider maProvider) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.red[400]),
const SizedBox(height: 16),
Text(
_error!,
style: TextStyle(color: Colors.red[300], fontSize: 16),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadQueue,
child: Text(S.of(context)!.retry),
),
],
),
);
}
if (maProvider.selectedPlayer == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.speaker_group, size: 64, color: Colors.grey[700]),
const SizedBox(height: 16),
Text(
S.of(context)!.noPlayerSelected,
style: TextStyle(color: Colors.grey[600], fontSize: 18),
),
],
),
);
}
if (_queue == null || _queue!.items.isEmpty) {
return EmptyState.queue(context: context);
}
// Filter to show only current and upcoming items (not history)
final currentIndex = _queue!.currentIndex ?? 0;
final upcomingItems = _queue!.items.sublist(currentIndex);
if (upcomingItems.isEmpty) {
return EmptyState(
icon: Icons.queue_music,
message: S.of(context)!.noUpcomingTracks,
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Queue info
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(Icons.queue_music, color: Colors.grey[400]),
const SizedBox(width: 8),
Text(
'${upcomingItems.length} track${upcomingItems.length != 1 ? 's' : ''} in queue',
style: TextStyle(
color: Colors.grey[400],
fontSize: 14,
),
),
],
),
),
// Queue items (current + upcoming only)
Expanded(
child: ListView.builder(
itemCount: upcomingItems.length,
itemBuilder: (context, index) {
final item = upcomingItems[index];
final actualIndex = currentIndex + index;
final isCurrentItem = index == 0; // First item in filtered list is current
return _buildQueueItem(item, actualIndex, isCurrentItem, maProvider);
},
),
),
],
);
}
Widget _buildQueueItem(
QueueItem item,
int index,
bool isCurrentItem,
MusicAssistantProvider maProvider,
) {
final imageUrl = maProvider.api?.getImageUrl(item.track, size: 80);
return Dismissible(
key: Key(item.queueItemId),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 16.0),
color: Colors.red[900],
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (direction) {
// TODO: Implement queue item removal via API
// Currently only removes locally from UI state, not from actual Music Assistant queue
// Need to investigate if Music Assistant API supports player_queues/remove_item or similar endpoint
// Requires: API method like removeQueueItem(queueId, queueItemId) that calls player_queues/remove_item
setState(() {
_queue!.items.removeAt(index);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(S.of(context)!.removedItem(item.track.name)),
action: SnackBarAction(
label: S.of(context)!.undo,
onPressed: () {
setState(() {
_queue!.items.insert(index, item);
});
},
),
),
);
},
child: Container(
color: isCurrentItem ? Colors.blue.withOpacity(0.1) : null,
child: ListTile(
leading: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Drag handle (visual only for now - reordering not implemented)
Icon(
Icons.drag_handle,
color: Colors.grey[600],
),
const SizedBox(width: 8),
// Album art
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: imageUrl != null
? Image.network(
imageUrl,
width: 48,
height: 48,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 48,
height: 48,
color: Colors.grey[800],
child: const Icon(Icons.music_note, size: 24),
);
},
)
: Container(
width: 48,
height: 48,
color: Colors.grey[800],
child: const Icon(Icons.music_note, size: 24),
),
),
],
),
title: Text(
item.track.name,
style: TextStyle(
color: isCurrentItem ? Colors.blue : Colors.white,
fontWeight: isCurrentItem ? FontWeight.bold : FontWeight.normal,
),
),
subtitle: item.track.artists != null && item.track.artists!.isNotEmpty
? Text(
item.track.artists!.first.name,
style: TextStyle(
color: isCurrentItem ? Colors.blue[200] : Colors.grey[400],
),
)
: null,
trailing: isCurrentItem
? Icon(Icons.play_arrow, color: Colors.blue[400])
: null,
),
),
);
}
}