Files
AudiOhm/frontend/lib/presentation/widgets/player/queue_track_tile.dart
T
root a89c7894cf Initial commit: AudiOhm - Alternative Spotify avec streaming YouTube
Backend:
- FastAPI avec PostgreSQL et Redis
- Authentification JWT complète
- API REST pour musique, playlists, recherche
- Streaming audio via yt-dlp
- SQLAlchemy 2.0 async

Frontend:
- Flutter avec thème néon cyberpunk
- State management Riverpod
- Layout adaptatif desktop/mobile
- Lecteur audio avec mini-player

Infrastructure:
- Docker Compose (PostgreSQL + Redis)
- Scripts d'installation automatisés
- Scripts de build pour exécutables

Fichiers ajoutés:
- BUILD_CLIENT_*.bat/sh: Scripts de compilation
- BUILD_CLIENT_README.md: Documentation compilation
- CHECK_FLUTTER.sh: Vérificateur d'environnement
- requirements.txt mis à jour pour Python 3.13
- Modèles SQLAlchemy corrigés (metadata -> extra_metadata)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-18 20:08:36 +00:00

288 lines
7.2 KiB
Dart

import 'package:flutter/material.dart';
import '../../../../core/theme/colors.dart';
import '../../../../domain/entities/track.dart';
/// Queue Track Tile Widget
///
/// Displays a track in the queue with:
/// - Track info (art, title, artist, duration)
/// - Remove button
/// - Drag handle
/// - Visual indication for currently playing track
class QueueTrackTile extends StatelessWidget {
final Track track;
final bool isPlaying;
final int index;
final VoidCallback? onTap;
final VoidCallback? onRemove;
final bool isDragging;
const QueueTrackTile({
super.key,
required this.track,
this.isPlaying = false,
required this.index,
this.onTap,
this.onRemove,
this.isDragging = false,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
color: isPlaying
? AppColors.cyan.withOpacity(0.1)
: AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isPlaying
? AppColors.cyan.withOpacity(0.3)
: AppColors.surfaceVariant,
width: 1,
),
boxShadow: isPlaying
? [
BoxShadow(
color: AppColors.cyan.withOpacity(0.1),
blurRadius: 10,
spreadRadius: 1,
),
]
: null,
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
// Drag handle
_buildDragHandle(),
const SizedBox(width: 8),
// Track number or playing indicator
_buildTrackIndicator(),
const SizedBox(width: 12),
// Album art
_buildAlbumArt(),
const SizedBox(width: 12),
// Track info
Expanded(
child: _buildTrackInfo(),
),
const SizedBox(width: 12),
// Duration
_buildDuration(),
const SizedBox(width: 8),
// Remove button
if (onRemove != null) _buildRemoveButton(),
],
),
),
),
),
);
}
Widget _buildDragHandle() {
return MouseRegion(
cursor: SystemMouseCursors.grab,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Icon(
Icons.drag_handle,
color: AppColors.muted,
size: 20,
),
),
);
}
Widget _buildTrackIndicator() {
if (isPlaying) {
return SizedBox(
width: 20,
child: _PlayingAnimation(),
);
}
return SizedBox(
width: 20,
child: Text(
'${index + 1}',
style: const TextStyle(
color: AppColors.muted,
fontSize: 14,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
);
}
Widget _buildAlbumArt() {
return Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: AppColors.accentGradient,
borderRadius: BorderRadius.circular(8),
boxShadow: isPlaying ? AppColors.violetGlow : null,
),
child: track.imageUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
track.imageUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(
Icons.music_note,
color: AppColors.onBackground,
size: 24,
);
},
),
)
: const Icon(
Icons.music_note,
color: AppColors.onBackground,
size: 24,
),
);
}
Widget _buildTrackInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
track.title,
style: TextStyle(
color: isPlaying ? AppColors.cyan : AppColors.onSurface,
fontSize: 14,
fontWeight: isPlaying ? FontWeight.w600 : FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
track.artist?.name ?? 'Unknown Artist',
style: const TextStyle(
color: AppColors.muted,
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
);
}
Widget _buildDuration() {
return Text(
track.formattedDuration,
style: const TextStyle(
color: AppColors.muted,
fontSize: 12,
fontWeight: FontWeight.w500,
),
);
}
Widget _buildRemoveButton() {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onRemove,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.rouge.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.close,
color: AppColors.rouge,
size: 18,
),
),
),
);
}
}
/// Playing Animation Widget
class _PlayingAnimation extends StatefulWidget {
@override
State<_PlayingAnimation> createState() => _PlayingAnimationState();
}
class _PlayingAnimationState extends State<_PlayingAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (index) {
final delay = index * 0.2;
final animation = _controller
.drive(CurveTween(curve: Curves.easeInOut))
.drive(Tween<double>(begin: 0.3, end: 1.0));
return Transform.scale(
scale: (animation.value - delay + 1) % 1 * 0.7 + 0.3,
child: Container(
width: 3,
height: 12,
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration(
color: AppColors.cyan,
borderRadius: BorderRadius.circular(2),
),
),
);
}),
);
},
);
}
}