Implémentation du menu déroulant pour les actions d'éléments dans le ServerDrawer
Ajout d'un menu déroulant avec les actions 'Ajouter à la queue' et 'Ajouter après' pour les éléments dans le ServerDrawer. - Remplacement des fonctions handlePlayItem et handleQueueItem par des fonctions plus spécifiques (handlePlayItem, handleAddToQueue, handleAddAfterCurrent) - Ajout d'une gestion du menu déroulant avec ouverture/fermeture - Intégration du composant useRenderers pour les actions de lecture - Mise à jour des styles pour le menu déroulant avec animations - Correction de l'alignement du backdrop sur mobile - Amélioration des transitions et animations du drawer - Ajout de la fonction addAfterCurrent dans l'API et le control point - Support de l'ajout d'éléments après le morceau actuel dans les queues
This commit is contained in:
@@ -588,6 +588,19 @@ impl ControlPoint {
|
||||
&self,
|
||||
renderer_id: &DeviceId,
|
||||
items: Vec<PlaybackItem>,
|
||||
) -> Result<(), ControlPointError> {
|
||||
self.enqueue_items_with_mode(renderer_id, items, EnqueueMode::AppendToEnd)
|
||||
}
|
||||
|
||||
/// Enqueue items to a renderer's queue with a specific enqueue mode.
|
||||
///
|
||||
/// This is the low-level version that allows specifying the enqueue mode.
|
||||
/// User-driven operations should detach any playlist binding.
|
||||
pub fn enqueue_items_with_mode(
|
||||
&self,
|
||||
renderer_id: &DeviceId,
|
||||
items: Vec<PlaybackItem>,
|
||||
mode: EnqueueMode,
|
||||
) -> Result<(), ControlPointError> {
|
||||
// User-driven mutation: detach any playlist binding
|
||||
self.detach_playlist_binding(renderer_id, "enqueue_items");
|
||||
@@ -600,7 +613,7 @@ impl ControlPoint {
|
||||
|
||||
let new_len = {
|
||||
let mut queue = renderer.get_queue_mut();
|
||||
queue.enqueue_items(items, EnqueueMode::AppendToEnd)?;
|
||||
queue.enqueue_items(items, mode)?;
|
||||
queue.upcoming_len()?
|
||||
};
|
||||
|
||||
@@ -608,6 +621,7 @@ impl ControlPoint {
|
||||
renderer = renderer_id.0.as_str(),
|
||||
added = item_count,
|
||||
queue_len = new_len,
|
||||
mode = ?mode,
|
||||
"Enqueued playback items"
|
||||
);
|
||||
|
||||
@@ -855,7 +869,7 @@ impl ControlPoint {
|
||||
})?;
|
||||
|
||||
// Use generic queue access (works for all backends)
|
||||
let Some((item, remaining)) = renderer.get_queue().peek_current()? else {
|
||||
let Some((item, remaining)) = renderer.get_queue_mut().peek_current()? else {
|
||||
debug!(
|
||||
renderer = renderer_id.0.as_str(),
|
||||
"play_current_from_queue: queue is empty or no current item"
|
||||
@@ -976,7 +990,7 @@ impl ControlPoint {
|
||||
}
|
||||
|
||||
// Get the next item from the queue using peek_current
|
||||
let Ok(Some((_, remaining))) = renderer.get_queue().peek_current() else {
|
||||
let Ok(Some((_, remaining))) = renderer.get_queue_mut().peek_current() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1022,7 +1036,7 @@ impl ControlPoint {
|
||||
// Set queue index
|
||||
renderer.get_queue_mut().set_index(Some(index))?;
|
||||
|
||||
let Some((item, remaining)) = renderer.get_queue().peek_current()? else {
|
||||
let Some((item, remaining)) = renderer.get_queue_mut().peek_current()? else {
|
||||
debug!(
|
||||
renderer = renderer_id.0.as_str(),
|
||||
index, "play_queue_index: no item at index"
|
||||
|
||||
@@ -231,7 +231,7 @@ impl MusicRenderer {
|
||||
|
||||
/// Play the current item from the queue.
|
||||
pub fn play_current_from_queue(&self) -> Result<(), ControlPointError> {
|
||||
let queue = self.queue.lock().expect("Queue mutex poisoned");
|
||||
let mut queue = self.queue.lock().expect("Queue mutex poisoned");
|
||||
let backend = self.backend.lock().expect("Backend mutex poisoned");
|
||||
|
||||
// Get the current item from the queue
|
||||
|
||||
@@ -1392,6 +1392,118 @@ async fn add_to_queue(
|
||||
}))
|
||||
}
|
||||
|
||||
/// POST /control/renderers/{renderer_id}/queue/add-after - Ajouter du contenu après le morceau actuel
|
||||
#[cfg(feature = "pmoserver")]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/renderers/{renderer_id}/queue/add-after",
|
||||
params(
|
||||
("renderer_id" = String, Path, description = "ID unique du renderer")
|
||||
),
|
||||
request_body = PlayContentRequest,
|
||||
responses(
|
||||
(status = 200, description = "Contenu ajouté après le morceau actuel", body = SuccessResponse),
|
||||
(status = 404, description = "Renderer ou serveur non trouvé", body = ErrorResponse),
|
||||
(status = 504, description = "Timeout de la commande", body = ErrorResponse),
|
||||
(status = 500, description = "Erreur lors de l'exécution", body = ErrorResponse)
|
||||
),
|
||||
tag = "control"
|
||||
)]
|
||||
async fn add_after_current(
|
||||
State(state): State<ControlPointState>,
|
||||
Path(renderer_id): Path<String>,
|
||||
Json(req): Json<PlayContentRequest>,
|
||||
) -> Result<Json<SuccessResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let rid = DeviceId(renderer_id.clone());
|
||||
let sid = DeviceId(req.server_id.clone());
|
||||
let object_id = req.object_id.clone();
|
||||
let object_id_for_log = object_id.clone();
|
||||
|
||||
// Verify renderer exists
|
||||
state
|
||||
.control_point
|
||||
.music_renderer_by_id(&rid)
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(ErrorResponse {
|
||||
error: format!("Renderer {} not found", renderer_id),
|
||||
}),
|
||||
)
|
||||
})?;
|
||||
|
||||
let control_point = Arc::clone(&state.control_point);
|
||||
|
||||
// Spawn blocking task for content loading
|
||||
let add_task = tokio::task::spawn_blocking(move || {
|
||||
// Fetch playback items from server
|
||||
let items = fetch_playback_items(&control_point, &sid, &object_id)?;
|
||||
|
||||
if items.is_empty() {
|
||||
return Err(anyhow::anyhow!("No playable content found"));
|
||||
}
|
||||
|
||||
// Insert items after current using the new method
|
||||
control_point.enqueue_items_with_mode(
|
||||
&rid,
|
||||
items,
|
||||
crate::queue::EnqueueMode::InsertAfterCurrent,
|
||||
)?;
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
|
||||
time::timeout(QUEUE_COMMAND_TIMEOUT, add_task)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
warn!(
|
||||
"Add after current command for renderer {} exceeded {:?}",
|
||||
renderer_id, QUEUE_COMMAND_TIMEOUT
|
||||
);
|
||||
(
|
||||
StatusCode::GATEWAY_TIMEOUT,
|
||||
Json(ErrorResponse {
|
||||
error: format!(
|
||||
"Add after current timed out after {}s",
|
||||
QUEUE_COMMAND_TIMEOUT.as_secs()
|
||||
),
|
||||
}),
|
||||
)
|
||||
})?
|
||||
.map_err(|e| {
|
||||
warn!("Task join error during add after current: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: format!("Internal task error: {}", e),
|
||||
}),
|
||||
)
|
||||
})?
|
||||
.map_err(|e| {
|
||||
warn!(
|
||||
"Failed to add content after current for renderer {}: {}",
|
||||
renderer_id, e
|
||||
);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: format!("Failed to add after current: {}", e),
|
||||
}),
|
||||
)
|
||||
})?;
|
||||
|
||||
debug!(
|
||||
renderer = renderer_id.as_str(),
|
||||
server = req.server_id.as_str(),
|
||||
object = object_id_for_log.as_str(),
|
||||
"Content added after current via HTTP API"
|
||||
);
|
||||
|
||||
Ok(Json(SuccessResponse {
|
||||
message: "Content added after current track".to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HANDLERS - MEDIA SERVERS
|
||||
// ============================================================================
|
||||
@@ -1703,6 +1815,10 @@ pub fn create_api_router(state: ControlPointState, control_point: Arc<ControlPoi
|
||||
// Queue content
|
||||
.route("/renderers/{renderer_id}/queue/play", post(play_content))
|
||||
.route("/renderers/{renderer_id}/queue/add", post(add_to_queue))
|
||||
.route(
|
||||
"/renderers/{renderer_id}/queue/add-after",
|
||||
post(add_after_current),
|
||||
)
|
||||
// Servers
|
||||
.route("/servers", get(list_servers))
|
||||
.route(
|
||||
|
||||
@@ -38,7 +38,7 @@ use crate::{PlaybackItem, QueueSnapshot, errors::ControlPointError};
|
||||
pub enum EnqueueMode {
|
||||
/// Append new items at the end of the queue.
|
||||
AppendToEnd,
|
||||
/// Insert new items immediately after the current index
|
||||
/// Insert new items immediately after the current playing index
|
||||
/// (or at the beginning if there is no current index).
|
||||
InsertAfterCurrent,
|
||||
/// Replace the whole queue with the new items.
|
||||
@@ -161,7 +161,7 @@ pub trait QueueBackend {
|
||||
|
||||
/// Returns the current item (or the first pending item if no index is set)
|
||||
/// along with the count of remaining items.
|
||||
fn peek_current(&self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
|
||||
fn peek_current(&mut self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
|
||||
let snapshot = self.queue_snapshot()?;
|
||||
let QueueSnapshot {
|
||||
items,
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
//! - maintains a `current_index`,
|
||||
//! - never starts playback (transport control is handled elsewhere).
|
||||
|
||||
|
||||
use crate::{
|
||||
DeviceId, DeviceIdentity, RendererInfo,
|
||||
errors::ControlPointError,
|
||||
@@ -72,27 +71,16 @@ impl QueueBackend for InternalQueue {
|
||||
}
|
||||
|
||||
fn position_to_id(&self, id: usize) -> Result<u32, ControlPointError> {
|
||||
u32::try_from(id).map_err(|_| {
|
||||
ControlPointError::QueueError(format!(
|
||||
"Position {} exceeds u32::MAX",
|
||||
id
|
||||
))
|
||||
})
|
||||
u32::try_from(id)
|
||||
.map_err(|_| ControlPointError::QueueError(format!("Position {} exceeds u32::MAX", id)))
|
||||
}
|
||||
|
||||
fn current_track(&self) -> Result<Option<u32>, ControlPointError> {
|
||||
match self.current_index {
|
||||
None => Ok(None),
|
||||
Some(i) => {
|
||||
u32::try_from(i)
|
||||
.map(Some)
|
||||
.map_err(|_| {
|
||||
ControlPointError::QueueError(format!(
|
||||
"Current index {} exceeds u32::MAX",
|
||||
i
|
||||
))
|
||||
})
|
||||
}
|
||||
Some(i) => u32::try_from(i).map(Some).map_err(|_| {
|
||||
ControlPointError::QueueError(format!("Current index {} exceeds u32::MAX", i))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,16 +130,14 @@ impl QueueBackend for InternalQueue {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_queue(
|
||||
&mut self,
|
||||
items: Vec<PlaybackItem>
|
||||
) -> Result<(), ControlPointError> {
|
||||
fn sync_queue(&mut self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
|
||||
if items.is_empty() {
|
||||
return self.replace_queue(Vec::new(), None);
|
||||
}
|
||||
|
||||
// Récupérer l'item actuel
|
||||
let current = self.current_index
|
||||
let current = self
|
||||
.current_index
|
||||
.and_then(|idx| self.items.get(idx).map(|item| (idx, item.uri.clone())));
|
||||
|
||||
if let Some((_current_idx, current_uri)) = current {
|
||||
@@ -187,7 +173,8 @@ impl QueueBackend for InternalQueue {
|
||||
self.items.extend(items);
|
||||
}
|
||||
EnqueueMode::InsertAfterCurrent => {
|
||||
let insert_pos = self.current_index
|
||||
let insert_pos = self
|
||||
.current_index
|
||||
.map(|i| (i + 1).min(self.items.len()))
|
||||
.unwrap_or(0);
|
||||
|
||||
@@ -242,7 +229,7 @@ impl QueueBackend for InternalQueue {
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn peek_current(&self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
|
||||
fn peek_current(&mut self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
|
||||
if self.items.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -250,7 +237,11 @@ impl QueueBackend for InternalQueue {
|
||||
let len = self.items.len();
|
||||
let (item, resolved_index) = match self.current_index {
|
||||
Some(idx) if idx < len => (self.items.get(idx).cloned(), Some(idx)),
|
||||
_ => (self.items.first().cloned(), None),
|
||||
_ => {
|
||||
// Si current_index est None ou invalide, initialiser à 0
|
||||
self.current_index = Some(0);
|
||||
(self.items.first().cloned(), Some(0))
|
||||
}
|
||||
};
|
||||
|
||||
let item = match item {
|
||||
|
||||
@@ -156,7 +156,7 @@ impl QueueBackend for MusicQueue {
|
||||
}
|
||||
}
|
||||
|
||||
fn peek_current(&self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
|
||||
fn peek_current(&mut self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
|
||||
match self {
|
||||
MusicQueue::Internal(q) => q.peek_current(),
|
||||
MusicQueue::OpenHome(q) => q.peek_current(),
|
||||
|
||||
@@ -778,7 +778,7 @@ impl QueueBackend for OpenHomeQueue {
|
||||
}
|
||||
|
||||
/// Optimized peek_current: use primitives instead of full snapshot.
|
||||
fn peek_current(&self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
|
||||
fn peek_current(&mut self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
|
||||
let len = self.len()?;
|
||||
if len == 0 {
|
||||
return Ok(None);
|
||||
|
||||
Reference in New Issue
Block a user