fix more whitespace issues in rsx autofmt nested rsx blocks (#5257)
* trim * works a bit better * almost... * it work? * yay! * codex tried a thing * move * reduce to 1 * remove search * simplify * adjust test case * clippy
This commit is contained in:
1 parent
ceb82612e0
commit
b6793dc2b4
4 files changed
+667
-58
No files matched your search
+166
-58
@@ -5,7 +5,7 @@ use quote::ToTokens;
|
|||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::{HashMap, VecDeque},
|
collections::{HashMap, HashSet, VecDeque},
|
||||||
fmt::{Result, Write},
|
fmt::{Result, Write},
|
||||||
};
|
};
|
||||||
use syn::{spanned::Spanned, token::Brace, Expr};
|
use syn::{spanned::Spanned, token::Brace, Expr};
|
||||||
@@ -848,92 +848,200 @@ impl<'a> Writer<'a> {
|
|||||||
static COMMENT_REGEX: Regex = Regex::new("\"[^\"]*\"|(//.*)").unwrap();
|
static COMMENT_REGEX: Regex = Regex::new("\"[^\"]*\"|(//.*)").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let pretty_expr = self.retrieve_formatted_expr(&expr).to_string();
|
let pretty = self.retrieve_formatted_expr(&expr).to_string();
|
||||||
|
let source = src_span.source_text().unwrap_or_default();
|
||||||
|
let mut src_lines = source.lines().peekable();
|
||||||
|
|
||||||
// Adding comments back to the formatted expression
|
// Comments already in pretty output (from nested rsx!) - skip these from source
|
||||||
let source_text = src_span.source_text().unwrap_or_default();
|
let pretty_comments: HashSet<_> = pretty
|
||||||
let mut source_lines = source_text.lines().peekable();
|
.lines()
|
||||||
let mut output = String::from("");
|
.filter(|l| l.trim().starts_with("//"))
|
||||||
let mut printed_empty_line = false;
|
.map(|l| l.trim())
|
||||||
|
.collect();
|
||||||
|
|
||||||
if source_lines.peek().is_none() {
|
let mut out = String::new();
|
||||||
output = pretty_expr;
|
|
||||||
|
if src_lines.peek().is_none() {
|
||||||
|
out = pretty;
|
||||||
} else {
|
} else {
|
||||||
for line in pretty_expr.lines() {
|
for line in pretty.lines() {
|
||||||
let compacted_pretty_line = line.replace(" ", "").replace(",", "");
|
let trimmed = line.trim();
|
||||||
let trimmed_pretty_line = line.trim();
|
let compacted = line.replace(" ", "").replace(",", "");
|
||||||
|
|
||||||
// Nested expressions might have comments already. We handle writing all of those
|
// Pretty comments: consume matching source lines, preserve preceding empty lines
|
||||||
// at the outer level, so we skip them here
|
if trimmed.starts_with("//") {
|
||||||
if trimmed_pretty_line.starts_with("//") {
|
if !out.is_empty() {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
let mut had_empty = false;
|
||||||
|
while let Some(s) = src_lines.peek() {
|
||||||
|
let t = s.trim();
|
||||||
|
if t.is_empty() {
|
||||||
|
had_empty = true;
|
||||||
|
src_lines.next();
|
||||||
|
} else if t == trimmed {
|
||||||
|
src_lines.next();
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if had_empty {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
out.push_str(line);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !output.is_empty() {
|
// Pretty empty lines: preserve and sync with source
|
||||||
output.push('\n');
|
if trimmed.is_empty() {
|
||||||
|
if !out.is_empty() {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
while src_lines
|
||||||
|
.peek()
|
||||||
|
.map(|s| s.trim().is_empty())
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
src_lines.next();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// pull down any source lines with whitespace until we hit a line that matches our current line.
|
if !out.is_empty() {
|
||||||
while let Some(src) = source_lines.peek() {
|
out.push('\n');
|
||||||
let trimmed_src = src.trim();
|
}
|
||||||
|
|
||||||
// Write comments and empty lines as they are
|
// Scan source for comments/empty lines before the matching line
|
||||||
if trimmed_src.starts_with("//") || trimmed_src.is_empty() {
|
let mut pending_comments = Vec::new();
|
||||||
if !trimmed_src.is_empty() {
|
let mut had_empty = false;
|
||||||
// Match the whitespace of the incoming source line
|
let mut multiline: Option<Vec<&str>> = None;
|
||||||
for s in line.chars().take_while(|c| c.is_whitespace()) {
|
|
||||||
output.push(s);
|
while let Some(src) = src_lines.peek() {
|
||||||
|
let src_trimmed = src.trim();
|
||||||
|
|
||||||
|
if src_trimmed.is_empty() || src_trimmed.starts_with("//") {
|
||||||
|
if src_trimmed.is_empty() {
|
||||||
|
if pending_comments.is_empty() {
|
||||||
|
had_empty = true;
|
||||||
}
|
}
|
||||||
|
} else if !pretty_comments.contains(src_trimmed) {
|
||||||
// Bump out the indent level if the line starts with a closing brace (ie we're at the end of a block)
|
pending_comments.push(src_trimmed);
|
||||||
if matches!(trimmed_pretty_line.chars().next(), Some(')' | '}' | ']')) {
|
|
||||||
output.push_str(self.out.indent.indent_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
printed_empty_line = false;
|
|
||||||
output.push_str(trimmed_src);
|
|
||||||
output.push('\n');
|
|
||||||
} else if !printed_empty_line {
|
|
||||||
output.push('\n');
|
|
||||||
printed_empty_line = true;
|
|
||||||
}
|
}
|
||||||
|
src_lines.next();
|
||||||
_ = source_lines.next();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let compacted_src_line = src.replace(" ", "").replace(",", "");
|
let src_compacted = src.replace(" ", "").replace(",", "");
|
||||||
|
|
||||||
// If this source line matches our pretty line, we stop pulling down
|
// Exact match
|
||||||
if compacted_src_line.contains(&compacted_pretty_line) {
|
if src_compacted.contains(&compacted) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, consume this source line and keep going
|
// Multi-line method chain (e.g., foo\n .bar()\n .baz())
|
||||||
_ = source_lines.next();
|
if !src_compacted.is_empty() && compacted.starts_with(&src_compacted) {
|
||||||
|
let is_call = src_trimmed.ends_with('(')
|
||||||
|
|| src_trimmed.ends_with(',')
|
||||||
|
|| src_trimmed.ends_with('{');
|
||||||
|
if !is_call {
|
||||||
|
multiline = Some(vec![*src]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-matching line - clear pending and skip
|
||||||
|
pending_comments.clear();
|
||||||
|
had_empty = false;
|
||||||
|
src_lines.next();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once all whitespace is written, write the pretty line
|
// Output empty line if needed
|
||||||
output.push_str(line);
|
if had_empty {
|
||||||
printed_empty_line = false;
|
out.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
// And then pull the corresponding source line
|
// Output pending comments
|
||||||
let source_line = source_lines.next();
|
for comment in &pending_comments {
|
||||||
|
for c in line.chars().take_while(|c| c.is_whitespace()) {
|
||||||
|
out.push(c);
|
||||||
|
}
|
||||||
|
if matches!(trimmed.chars().next(), Some(')' | '}' | ']')) {
|
||||||
|
out.push_str(self.out.indent.indent_str());
|
||||||
|
}
|
||||||
|
out.push_str(comment);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
// And then write any inline comments
|
// Handle multi-line method chains
|
||||||
if let Some(source_line) = source_line {
|
if let Some(mut ml) = multiline {
|
||||||
if let Some(captures) = COMMENT_REGEX.with(|f| f.captures(source_line)) {
|
src_lines.next();
|
||||||
if let Some(comment) = captures.get(1) {
|
let mut acc = ml[0].replace(" ", "").replace(",", "");
|
||||||
output.push_str(" // ");
|
|
||||||
output.push_str(comment.as_str().replace("//", "").trim());
|
while let Some(src) = src_lines.peek() {
|
||||||
|
let t = src.trim();
|
||||||
|
if t.starts_with("//") {
|
||||||
|
ml.push(src);
|
||||||
|
src_lines.next();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if t.is_empty() {
|
||||||
|
src_lines.next();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.push_str(&src.replace(" ", "").replace(",", ""));
|
||||||
|
ml.push(src);
|
||||||
|
|
||||||
|
if acc.contains(&compacted) {
|
||||||
|
src_lines.next();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cont = t.starts_with('.')
|
||||||
|
|| t.starts_with("&&")
|
||||||
|
|| t.starts_with("||")
|
||||||
|
|| matches!(t.chars().next(), Some('+' | '-' | '*' | '/' | '?'));
|
||||||
|
|
||||||
|
if cont || compacted.starts_with(&acc) {
|
||||||
|
src_lines.next();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write multi-line with adjusted indentation
|
||||||
|
let base_indent = ml[0].chars().take_while(|c| c.is_whitespace()).count();
|
||||||
|
let target: String = line.chars().take_while(|c| c.is_whitespace()).collect();
|
||||||
|
|
||||||
|
for (i, src_line) in ml.iter().enumerate() {
|
||||||
|
let indent = src_line.chars().take_while(|c| c.is_whitespace()).count();
|
||||||
|
out.push_str(&target);
|
||||||
|
for _ in 0..indent.saturating_sub(base_indent) {
|
||||||
|
out.push(' ');
|
||||||
|
}
|
||||||
|
out.push_str(src_line.trim());
|
||||||
|
if i < ml.len() - 1 {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single line - output pretty line and capture inline comments
|
||||||
|
out.push_str(line);
|
||||||
|
if let Some(src_line) = src_lines.next() {
|
||||||
|
if let Some(cap) = COMMENT_REGEX.with(|r| r.captures(src_line)) {
|
||||||
|
if let Some(c) = cap.get(1) {
|
||||||
|
out.push_str(" // ");
|
||||||
|
out.push_str(c.as_str().replace("//", "").trim());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.write_mulitiline_tokens(output)?;
|
self.write_mulitiline_tokens(out)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,3 +41,4 @@ twoway!("syntax_error" => syntax_error (IndentOptions::new(IndentType::Spaces, 4
|
|||||||
twoway!("skipfail" => skipfail (IndentOptions::new(IndentType::Spaces, 4, false)));
|
twoway!("skipfail" => skipfail (IndentOptions::new(IndentType::Spaces, 4, false)));
|
||||||
twoway!("comments-inline-4sp" => comments_inline_4sp (IndentOptions::new(IndentType::Spaces, 4, false)));
|
twoway!("comments-inline-4sp" => comments_inline_4sp (IndentOptions::new(IndentType::Spaces, 4, false)));
|
||||||
twoway!("comments-attributes-4sp" => comments_attributes_4sp (IndentOptions::new(IndentType::Spaces, 4, false)));
|
twoway!("comments-attributes-4sp" => comments_attributes_4sp (IndentOptions::new(IndentType::Spaces, 4, false)));
|
||||||
|
twoway!("comments-big" => comments_big (IndentOptions::new(IndentType::Spaces, 4, false)));
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
pub fn MediaPlayerControls() -> Element {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
// Control Panel Row
|
||||||
|
nav { class: "h-12 border-b border-gray-200 dark:border-[#222] flex items-center justify-between px-6 shrink-0 bg-gray-50 dark:bg-[#000000]",
|
||||||
|
// Right: Playback controls + Settings
|
||||||
|
div { class: "flex items-center gap-2",
|
||||||
|
// Playback buttons - shown only when track is loaded
|
||||||
|
if let Some(track_id_str) = ¤t_track_id {
|
||||||
|
// Get current track from the tracks hashmap
|
||||||
|
{
|
||||||
|
let current_track = tracks().get(track_id_str).cloned();
|
||||||
|
let track_id_owned = track_id_str.clone();
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
// Play and Download buttons
|
||||||
|
if current_track.is_some() {
|
||||||
|
{
|
||||||
|
rsx! {
|
||||||
|
// Play button - streams audio to device
|
||||||
|
button {
|
||||||
|
class: "px-2.5 py-1 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded transition-colors flex items-center gap-1.5 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 cursor-pointer disabled:opacity-50",
|
||||||
|
title: "Play track (streams audio)",
|
||||||
|
disabled: is_buffering(),
|
||||||
|
onclick: {
|
||||||
|
let tid = track_id_owned.clone();
|
||||||
|
// Capture output device for streaming
|
||||||
|
let output_device = current_track
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|t| t.preferred_output.clone());
|
||||||
|
move |_| {
|
||||||
|
let tid = tid.clone();
|
||||||
|
let output_device = output_device.clone();
|
||||||
|
async move {
|
||||||
|
playback_result.set(None);
|
||||||
|
playback_error.set(None);
|
||||||
|
is_buffering.set(true);
|
||||||
|
|
||||||
|
let track_id: uuid::Uuid = tid.parse().unwrap_or_default();
|
||||||
|
let request = StreamAudioRequest {};
|
||||||
|
|
||||||
|
match start_audio_stream(track_id, request).await {
|
||||||
|
Ok(stream) => {
|
||||||
|
// Use output device if available, otherwise fall back to default
|
||||||
|
let device = output_device
|
||||||
|
.map(|d| format!("{}/", d))
|
||||||
|
.unwrap_or_else(|| "/dev/audio/default".to_string());
|
||||||
|
|
||||||
|
// Connect to the audio stream endpoint
|
||||||
|
let stream_url = format!(
|
||||||
|
"audio://stream/{}@{}{}",
|
||||||
|
stream.session_token,
|
||||||
|
stream.server_host,
|
||||||
|
device,
|
||||||
|
);
|
||||||
|
// Open the audio stream
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
let _ = window.location().set_href(&stream_url);
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "web"))]
|
||||||
|
let _ = stream_url;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
playback_error
|
||||||
|
.set(Some(format!("Failed to start stream: {}", e)));
|
||||||
|
show_error_modal.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is_buffering.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Icon {
|
||||||
|
name: "play_arrow",
|
||||||
|
size: 16,
|
||||||
|
color: IconColor::Custom("#2563eb".to_string()),
|
||||||
|
}
|
||||||
|
if is_buffering() {
|
||||||
|
"Buffering..."
|
||||||
|
} else if SHOW_CONTROL_TEXT {
|
||||||
|
"Play"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download button - downloads track locally
|
||||||
|
button {
|
||||||
|
class: "px-2.5 py-1 hover:bg-gray-100 dark:hover:bg-[#252525] rounded transition-colors flex items-center gap-1.5 text-sm text-gray-600 dark:text-[#888] hover:text-gray-900 dark:hover:text-white cursor-pointer disabled:opacity-50",
|
||||||
|
title: "Download track for offline playback",
|
||||||
|
disabled: is_buffering(),
|
||||||
|
onclick: {
|
||||||
|
let tid = track_id_owned.clone();
|
||||||
|
move |_| {
|
||||||
|
let tid = tid.clone();
|
||||||
|
async move {
|
||||||
|
playback_result.set(None);
|
||||||
|
playback_error.set(None);
|
||||||
|
is_buffering.set(true);
|
||||||
|
show_download_modal.set(true);
|
||||||
|
|
||||||
|
let track_id: uuid::Uuid = tid.parse().unwrap_or_default();
|
||||||
|
let request = DownloadTrackRequest {};
|
||||||
|
|
||||||
|
match download_track(track_id, request).await {
|
||||||
|
Ok(download) => {
|
||||||
|
playback_result.set(Some(download));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
playback_error
|
||||||
|
.set(Some(format!("Failed to download: {}", e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is_buffering.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Icon {
|
||||||
|
name: "download",
|
||||||
|
size: 16,
|
||||||
|
color: IconColor::Custom("#666".to_string()),
|
||||||
|
}
|
||||||
|
if is_buffering() {
|
||||||
|
"Downloading..."
|
||||||
|
} else if SHOW_CONTROL_TEXT {
|
||||||
|
"Download"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fullscreen mode toggle
|
||||||
|
button {
|
||||||
|
class: "px-2.5 py-1 hover:bg-gray-100 dark:hover:bg-[#252525] rounded transition-colors flex items-center gap-1.5 text-sm text-gray-600 dark:text-[#888] hover:text-gray-900 dark:hover:text-white mr-2 pr-3 border-r border-gray-200 dark:border-[#333] cursor-pointer",
|
||||||
|
title: if (ctx.fullscreen)() { "Exit fullscreen" } else { "Enter fullscreen" },
|
||||||
|
onclick: move |_| ctx.fullscreen.set(!(ctx.fullscreen)()),
|
||||||
|
Icon {
|
||||||
|
name: if (ctx.fullscreen)() { "fullscreen_exit" } else { "fullscreen" },
|
||||||
|
size: 16,
|
||||||
|
color: IconColor::Custom("#666".to_string()),
|
||||||
|
}
|
||||||
|
if SHOW_CONTROL_TEXT {
|
||||||
|
if (ctx.fullscreen)() {
|
||||||
|
"Exit Fullscreen"
|
||||||
|
} else {
|
||||||
|
"Fullscreen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div { class: "flex items-center gap-1 mr-2 pr-3 border-r border-gray-200 dark:border-[#333]",
|
||||||
|
button {
|
||||||
|
class: "px-2 py-1 hover:bg-gray-100 dark:hover:bg-[#252525] rounded transition-colors flex items-center gap-1.5 text-sm text-gray-600 dark:text-[#888] hover:text-gray-900 dark:hover:text-white cursor-pointer",
|
||||||
|
title: "Add to playlist",
|
||||||
|
onclick: move |_| show_playlist_modal.set(true),
|
||||||
|
Icon {
|
||||||
|
name: "playlist_add",
|
||||||
|
size: 16,
|
||||||
|
color: IconColor::Custom("#666".to_string()),
|
||||||
|
}
|
||||||
|
if SHOW_CONTROL_TEXT {
|
||||||
|
"Add to Playlist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref track) = current_track {
|
||||||
|
{
|
||||||
|
let track_id_for_action = track_id_owned.clone();
|
||||||
|
match track.status {
|
||||||
|
TrackStatus::Playing { .. } => rsx! {
|
||||||
|
button {
|
||||||
|
class: "px-2 py-1 hover:bg-gray-100 dark:hover:bg-[#252525] rounded transition-colors flex items-center gap-1.5 text-sm text-gray-600 dark:text-[#888] hover:text-gray-900 dark:hover:text-white cursor-pointer",
|
||||||
|
title: "Pause playback",
|
||||||
|
onclick: move |_| {
|
||||||
|
let tid = track_id_for_action.clone();
|
||||||
|
async move {
|
||||||
|
let track_id: uuid::Uuid = tid.parse().unwrap_or_default();
|
||||||
|
if let Err(e) = pause_track(track_id).await {
|
||||||
|
error!("Failed to pause: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Icon {
|
||||||
|
name: "pause",
|
||||||
|
size: 16,
|
||||||
|
color: IconColor::Custom("#666".to_string()),
|
||||||
|
}
|
||||||
|
if SHOW_CONTROL_TEXT {
|
||||||
|
"Pause"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TrackStatus::Paused { .. } => rsx! {
|
||||||
|
button {
|
||||||
|
class: "px-2 py-1 bg-green-600 hover:bg-green-500 text-white rounded transition-colors flex items-center gap-1.5 text-sm cursor-pointer",
|
||||||
|
title: "Resume playback",
|
||||||
|
onclick: move |_| {
|
||||||
|
let tid = track_id_for_action.clone();
|
||||||
|
async move {
|
||||||
|
let track_id: uuid::Uuid = tid.parse().unwrap_or_default();
|
||||||
|
if let Err(e) = resume_track(track_id).await {
|
||||||
|
error!("Failed to resume: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Icon { name: "play_arrow", size: 16, color: IconColor::Light }
|
||||||
|
if SHOW_CONTROL_TEXT {
|
||||||
|
"Resume"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => rsx! {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let track_id_for_stop = track_id_owned.clone();
|
||||||
|
rsx! {
|
||||||
|
button {
|
||||||
|
class: "px-2 py-1 hover:bg-red-100 dark:hover:bg-red-900/30 rounded transition-colors flex items-center gap-1.5 text-sm text-red-600 dark:text-red-400 cursor-pointer",
|
||||||
|
title: "Stop playback",
|
||||||
|
onclick: move |_| {
|
||||||
|
let tid = track_id_for_stop.clone();
|
||||||
|
async move {
|
||||||
|
if let Err(e) = stop_track(tid).await {
|
||||||
|
error!("Failed to stop: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Icon {
|
||||||
|
name: "stop",
|
||||||
|
size: 16,
|
||||||
|
color: IconColor::Custom("#dc2626".to_string()),
|
||||||
|
}
|
||||||
|
if SHOW_CONTROL_TEXT {
|
||||||
|
"Stop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
pub fn MediaPlayerControls() -> Element {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
// Control Panel Row
|
||||||
|
nav { class: "h-12 border-b border-gray-200 dark:border-[#222] flex items-center justify-between px-6 shrink-0 bg-gray-50 dark:bg-[#000000]",
|
||||||
|
// Right: Playback controls + Settings
|
||||||
|
div { class: "flex items-center gap-2",
|
||||||
|
// Playback buttons - shown only when track is loaded
|
||||||
|
if let Some(track_id_str) = ¤t_track_id {
|
||||||
|
// Get current track from the tracks hashmap
|
||||||
|
{
|
||||||
|
let current_track = tracks().get(track_id_str).cloned();
|
||||||
|
let track_id_owned = track_id_str.clone();
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
// Play and Download buttons
|
||||||
|
if current_track.is_some() {
|
||||||
|
{
|
||||||
|
rsx! {
|
||||||
|
// Play button - streams audio to device
|
||||||
|
button {
|
||||||
|
class: "px-2.5 py-1 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded transition-colors flex items-center gap-1.5 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 cursor-pointer disabled:opacity-50",
|
||||||
|
title: "Play track (streams audio)",
|
||||||
|
disabled: is_buffering(),
|
||||||
|
onclick: {
|
||||||
|
let tid = track_id_owned.clone();
|
||||||
|
// Capture output device for streaming
|
||||||
|
let output_device = current_track
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|t| t.preferred_output.clone());
|
||||||
|
move |_| {
|
||||||
|
let tid = tid.clone();
|
||||||
|
let output_device = output_device.clone();
|
||||||
|
async move {
|
||||||
|
playback_result.set(None);
|
||||||
|
playback_error.set(None);
|
||||||
|
is_buffering.set(true);
|
||||||
|
|
||||||
|
let track_id: uuid::Uuid = tid.parse().unwrap_or_default();
|
||||||
|
let request = StreamAudioRequest {};
|
||||||
|
|
||||||
|
match start_audio_stream(track_id, request).await {
|
||||||
|
Ok(stream) => {
|
||||||
|
// Use output device if available, otherwise fall back to default
|
||||||
|
let device = output_device
|
||||||
|
.map(|d| format!("{}/", d))
|
||||||
|
.unwrap_or_else(|| "/dev/audio/default".to_string());
|
||||||
|
|
||||||
|
// Connect to the audio stream endpoint
|
||||||
|
let stream_url = format!(
|
||||||
|
"audio://stream/{}@{}{}",
|
||||||
|
stream.session_token,
|
||||||
|
stream.server_host,
|
||||||
|
device,
|
||||||
|
);
|
||||||
|
// Open the audio stream
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
let _ = window.location().set_href(&stream_url);
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "web"))]
|
||||||
|
let _ = stream_url;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
playback_error
|
||||||
|
.set(Some(format!("Failed to start stream: {}", e)));
|
||||||
|
show_error_modal.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is_buffering.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Icon {
|
||||||
|
name: "play_arrow",
|
||||||
|
size: 16,
|
||||||
|
color: IconColor::Custom("#2563eb".to_string()),
|
||||||
|
}
|
||||||
|
if is_buffering() {
|
||||||
|
"Buffering..."
|
||||||
|
} else if SHOW_CONTROL_TEXT {
|
||||||
|
"Play"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download button - downloads track locally
|
||||||
|
button {
|
||||||
|
class: "px-2.5 py-1 hover:bg-gray-100 dark:hover:bg-[#252525] rounded transition-colors flex items-center gap-1.5 text-sm text-gray-600 dark:text-[#888] hover:text-gray-900 dark:hover:text-white cursor-pointer disabled:opacity-50",
|
||||||
|
title: "Download track for offline playback",
|
||||||
|
disabled: is_buffering(),
|
||||||
|
onclick: {
|
||||||
|
let tid = track_id_owned.clone();
|
||||||
|
move |_| {
|
||||||
|
let tid = tid.clone();
|
||||||
|
async move {
|
||||||
|
playback_result.set(None);
|
||||||
|
playback_error.set(None);
|
||||||
|
is_buffering.set(true);
|
||||||
|
show_download_modal.set(true);
|
||||||
|
|
||||||
|
let track_id: uuid::Uuid = tid.parse().unwrap_or_default();
|
||||||
|
let request = DownloadTrackRequest {};
|
||||||
|
|
||||||
|
match download_track(track_id, request).await {
|
||||||
|
Ok(download) => {
|
||||||
|
playback_result.set(Some(download));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
playback_error
|
||||||
|
.set(Some(format!("Failed to download: {}", e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is_buffering.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Icon {
|
||||||
|
name: "download",
|
||||||
|
size: 16,
|
||||||
|
color: IconColor::Custom("#666".to_string()),
|
||||||
|
}
|
||||||
|
if is_buffering() {
|
||||||
|
"Downloading..."
|
||||||
|
} else if SHOW_CONTROL_TEXT {
|
||||||
|
"Download"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fullscreen mode toggle
|
||||||
|
button {
|
||||||
|
class: "px-2.5 py-1 hover:bg-gray-100 dark:hover:bg-[#252525] rounded transition-colors flex items-center gap-1.5 text-sm text-gray-600 dark:text-[#888] hover:text-gray-900 dark:hover:text-white mr-2 pr-3 border-r border-gray-200 dark:border-[#333] cursor-pointer",
|
||||||
|
title: if (ctx.fullscreen)() { "Exit fullscreen" } else { "Enter fullscreen" },
|
||||||
|
onclick: move |_| ctx.fullscreen.set(!(ctx.fullscreen)()),
|
||||||
|
Icon {
|
||||||
|
name: if (ctx.fullscreen)() { "fullscreen_exit" } else { "fullscreen" },
|
||||||
|
size: 16,
|
||||||
|
color: IconColor::Custom("#666".to_string()),
|
||||||
|
}
|
||||||
|
if SHOW_CONTROL_TEXT {
|
||||||
|
if (ctx.fullscreen)() {
|
||||||
|
"Exit Fullscreen"
|
||||||
|
} else {
|
||||||
|
"Fullscreen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div { class: "flex items-center gap-1 mr-2 pr-3 border-r border-gray-200 dark:border-[#333]",
|
||||||
|
button {
|
||||||
|
class: "px-2 py-1 hover:bg-gray-100 dark:hover:bg-[#252525] rounded transition-colors flex items-center gap-1.5 text-sm text-gray-600 dark:text-[#888] hover:text-gray-900 dark:hover:text-white cursor-pointer",
|
||||||
|
title: "Add to playlist",
|
||||||
|
onclick: move |_| show_playlist_modal.set(true),
|
||||||
|
Icon {
|
||||||
|
name: "playlist_add",
|
||||||
|
size: 16,
|
||||||
|
color: IconColor::Custom("#666".to_string()),
|
||||||
|
}
|
||||||
|
if SHOW_CONTROL_TEXT {
|
||||||
|
"Add to Playlist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref track) = current_track {
|
||||||
|
{
|
||||||
|
let track_id_for_action = track_id_owned.clone();
|
||||||
|
match track.status {
|
||||||
|
TrackStatus::Playing { .. } => rsx! {
|
||||||
|
button {
|
||||||
|
class: "px-2 py-1 hover:bg-gray-100 dark:hover:bg-[#252525] rounded transition-colors flex items-center gap-1.5 text-sm text-gray-600 dark:text-[#888] hover:text-gray-900 dark:hover:text-white cursor-pointer",
|
||||||
|
title: "Pause playback",
|
||||||
|
onclick: move |_| {
|
||||||
|
let tid = track_id_for_action.clone();
|
||||||
|
async move {
|
||||||
|
let track_id: uuid::Uuid = tid.parse().unwrap_or_default();
|
||||||
|
if let Err(e) = pause_track(track_id).await {
|
||||||
|
error!("Failed to pause: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Icon {
|
||||||
|
name: "pause",
|
||||||
|
size: 16,
|
||||||
|
color: IconColor::Custom("#666".to_string()),
|
||||||
|
}
|
||||||
|
if SHOW_CONTROL_TEXT {
|
||||||
|
"Pause"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TrackStatus::Paused { .. } => rsx! {
|
||||||
|
button {
|
||||||
|
class: "px-2 py-1 bg-green-600 hover:bg-green-500 text-white rounded transition-colors flex items-center gap-1.5 text-sm cursor-pointer",
|
||||||
|
title: "Resume playback",
|
||||||
|
onclick: move |_| {
|
||||||
|
let tid = track_id_for_action.clone();
|
||||||
|
async move {
|
||||||
|
let track_id: uuid::Uuid = tid.parse().unwrap_or_default();
|
||||||
|
if let Err(e) = resume_track(track_id).await {
|
||||||
|
error!("Failed to resume: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Icon { name: "play_arrow", size: 16, color: IconColor::Light }
|
||||||
|
if SHOW_CONTROL_TEXT {
|
||||||
|
"Resume"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => rsx! {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let track_id_for_stop = track_id_owned.clone();
|
||||||
|
rsx! {
|
||||||
|
button {
|
||||||
|
class: "px-2 py-1 hover:bg-red-100 dark:hover:bg-red-900/30 rounded transition-colors flex items-center gap-1.5 text-sm text-red-600 dark:text-red-400 cursor-pointer",
|
||||||
|
title: "Stop playback",
|
||||||
|
onclick: move |_| {
|
||||||
|
let tid = track_id_for_stop.clone();
|
||||||
|
async move {
|
||||||
|
if let Err(e) = stop_track(tid).await {
|
||||||
|
error!("Failed to stop: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Icon {
|
||||||
|
name: "stop",
|
||||||
|
size: 16,
|
||||||
|
color: IconColor::Custom("#dc2626".to_string()),
|
||||||
|
}
|
||||||
|
if SHOW_CONTROL_TEXT {
|
||||||
|
"Stop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user