Merge branch '1-add-queue' into 'main'

Add functioning queue

See merge request libretunes/libretunes!11
This commit is contained in:
Connor Wittman 2024-03-01 16:46:45 -05:00
commit 59b11dae76
13 changed files with 315 additions and 5 deletions

20
Cargo.lock generated
View File

@ -1311,8 +1311,10 @@ checksum = "f41f2deec9249d16ef6b1a8442fbe16013f67053797052aa0b7d2f5ebd0f0098"
dependencies = [ dependencies = [
"icondata_ai", "icondata_ai",
"icondata_bs", "icondata_bs",
"icondata_cg",
"icondata_core", "icondata_core",
"icondata_io", "icondata_io",
"icondata_ri",
] ]
[[package]] [[package]]
@ -1333,6 +1335,15 @@ dependencies = [
"icondata_core", "icondata_core",
] ]
[[package]]
name = "icondata_cg"
version = "0.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b90cda35aa524761219a8dbd006513734e08a4cf92ee05820b01749e76435462"
dependencies = [
"icondata_core",
]
[[package]] [[package]]
name = "icondata_core" name = "icondata_core"
version = "0.0.2" version = "0.0.2"
@ -1348,6 +1359,15 @@ dependencies = [
"icondata_core", "icondata_core",
] ]
[[package]]
name = "icondata_ri"
version = "0.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d3adc5b64b22d10ab23a7b1a005f4cb52f3d08909f578fbaa09af9f9c0b7b"
dependencies = [
"icondata_core",
]
[[package]] [[package]]
name = "ident_case" name = "ident_case"
version = "1.0.1" version = "1.0.1"

View File

@ -23,6 +23,8 @@ leptos_icons = { version = "0.1.0", default_features = false, features = [
"BsPauseFill", "BsPauseFill",
"BsSkipStartFill", "BsSkipStartFill",
"BsSkipEndFill", "BsSkipEndFill",
"RiPlayListMediaFill",
"CgTrash",
"IoReturnUpBackSharp", "IoReturnUpBackSharp",
"AiEyeFilled", "AiEyeFilled",
"AiEyeInvisibleFilled" "AiEyeInvisibleFilled"

View File

@ -1,3 +1,6 @@
use crate::playbar::PlayBar;
use crate::playstatus::PlayStatus;
use crate::queue::Queue;
use leptos::*; use leptos::*;
use leptos_meta::*; use leptos_meta::*;
use leptos_router::*; use leptos_router::*;
@ -34,7 +37,13 @@ pub fn App() -> impl IntoView {
/// Renders the home page of your application. /// Renders the home page of your application.
#[component] #[component]
fn HomePage() -> impl IntoView { fn HomePage() -> impl IntoView {
view! {} let mut play_status = PlayStatus::default();
let play_status = create_rw_signal(play_status);
view! {
<PlayBar status=play_status/>
<Queue status=play_status/>
}
} }
/// 404 - Not Found /// 404 - Not Found

View File

@ -4,6 +4,8 @@ pub mod songdata;
pub mod playstatus; pub mod playstatus;
pub mod playbar; pub mod playbar;
pub mod database; pub mod database;
pub mod queue;
pub mod song;
pub mod models; pub mod models;
pub mod pages; pub mod pages;
pub mod users; pub mod users;

View File

@ -6,6 +6,7 @@ use leptos::html::{Audio, Div};
use leptos::leptos_dom::*; use leptos::leptos_dom::*;
use leptos::*; use leptos::*;
use leptos_icons::BsIcon::*; use leptos_icons::BsIcon::*;
use leptos_icons::RiIcon::*;
use leptos_icons::*; use leptos_icons::*;
/// Width and height of the forward/backward skip buttons /// Width and height of the forward/backward skip buttons
@ -13,6 +14,9 @@ const SKIP_BTN_SIZE: &str = "3.5em";
/// Width and height of the play/pause button /// Width and height of the play/pause button
const PLAY_BTN_SIZE: &str = "5em"; const PLAY_BTN_SIZE: &str = "5em";
// Width and height of the queue button
const QUEUE_BTN_SIZE: &str = "3.5em";
/// Threshold in seconds for skipping to the previous song instead of skipping to the start of the current song /// Threshold in seconds for skipping to the previous song instead of skipping to the start of the current song
const MIN_SKIP_BACK_TIME: f64 = 5.0; const MIN_SKIP_BACK_TIME: f64 = 5.0;
@ -32,7 +36,7 @@ const ARROW_KEY_SKIP_TIME: f64 = 5.0;
/// * `None` if the audio element is not available /// * `None` if the audio element is not available
/// * `Some((current_time, duration))` if the audio element is available /// * `Some((current_time, duration))` if the audio element is available
/// ///
fn get_song_time_duration(status: impl SignalWithUntracked<Value = PlayStatus>) -> Option<(f64, f64)> { pub fn get_song_time_duration(status: impl SignalWithUntracked<Value = PlayStatus>) -> Option<(f64, f64)> {
status.with_untracked(|status| { status.with_untracked(|status| {
if let Some(audio) = status.get_audio() { if let Some(audio) = status.get_audio() {
Some((audio.current_time(), audio.duration())) Some((audio.current_time(), audio.duration()))
@ -53,7 +57,7 @@ fn get_song_time_duration(status: impl SignalWithUntracked<Value = PlayStatus>)
/// * `status` - The `PlayStatus` to get the audio element from, as a signal /// * `status` - The `PlayStatus` to get the audio element from, as a signal
/// * `time` - The time to skip to, in seconds /// * `time` - The time to skip to, in seconds
/// ///
fn skip_to(status: impl SignalUpdate<Value = PlayStatus>, time: f64) { pub fn skip_to(status: impl SignalUpdate<Value = PlayStatus>, time: f64) {
if time.is_infinite() || time.is_nan() { if time.is_infinite() || time.is_nan() {
error!("Unable to skip to non-finite time: {}", time); error!("Unable to skip to non-finite time: {}", time);
return return
@ -77,7 +81,7 @@ fn skip_to(status: impl SignalUpdate<Value = PlayStatus>, time: f64) {
/// * `status` - The `PlayStatus` to get the audio element from, as a signal /// * `status` - The `PlayStatus` to get the audio element from, as a signal
/// * `play` - `true` to play the song, `false` to pause it /// * `play` - `true` to play the song, `false` to pause it
/// ///
fn set_playing(status: impl SignalUpdate<Value = PlayStatus>, play: bool) { pub fn set_playing(status: impl SignalUpdate<Value = PlayStatus>, play: bool) {
status.update(|status| { status.update(|status| {
if let Some(audio) = status.get_audio() { if let Some(audio) = status.get_audio() {
if play { if play {
@ -101,6 +105,14 @@ fn set_playing(status: impl SignalUpdate<Value = PlayStatus>, play: bool) {
}); });
} }
fn toggle_queue(status: impl SignalUpdate<Value = PlayStatus>) {
status.update(|status| {
status.queue_open = !status.queue_open;
});
}
/// Set the source of the audio player /// Set the source of the audio player
/// ///
/// Logs an error if the audio element is not available /// Logs an error if the audio element is not available
@ -309,6 +321,30 @@ fn ProgressBar(percentage: MaybeSignal<f64>, status: RwSignal<PlayStatus>) -> im
} }
} }
#[component]
fn QueueToggle(status: RwSignal<PlayStatus>) -> impl IntoView {
let update_queue = move |_| {
toggle_queue(status);
log!("queue button pressed, queue status: {:?}", status.with_untracked(|status| status.queue_open));
};
// We use this to prevent the buttons from being focused when clicked
// If buttons were focused on clicks, then pressing space bar to play/pause would "click" the button
// and trigger unwanted behavior
let prevent_focus = move |e: MouseEvent| {
e.prevent_default();
};
view! {
<div class="queue-toggle">
<button on:click=update_queue on:mousedown=prevent_focus>
<Icon class="controlbtn" width=QUEUE_BTN_SIZE height=QUEUE_BTN_SIZE icon=Icon::from(RiPlayListMediaFill) />
</button>
</div>
}
}
/// The main play bar component, containing the progress bar, media info, play controls, and play duration /// The main play bar component, containing the progress bar, media info, play controls, and play duration
#[component] #[component]
pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView { pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
@ -457,6 +493,7 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
<MediaInfo status=status /> <MediaInfo status=status />
<PlayControls status=status /> <PlayControls status=status />
<PlayDuration elapsed_secs=elapsed_secs.into() total_secs=total_secs.into() /> <PlayDuration elapsed_secs=elapsed_secs.into() total_secs=total_secs.into() />
<QueueToggle status=status />
</div> </div>
} }
} }

View File

@ -9,6 +9,8 @@ use crate::songdata::SongData;
pub struct PlayStatus { pub struct PlayStatus {
/// Whether or not the audio player is currently playing /// Whether or not the audio player is currently playing
pub playing: bool, pub playing: bool,
/// Whether or not the queue is open
pub queue_open: bool,
/// A reference to the HTML audio element /// A reference to the HTML audio element
pub audio_player: Option<NodeRef<Audio>>, pub audio_player: Option<NodeRef<Audio>>,
/// A queue of songs that have been played, ordered from oldest to newest /// A queue of songs that have been played, ordered from oldest to newest
@ -53,6 +55,7 @@ impl Default for PlayStatus {
fn default() -> Self { fn default() -> Self {
Self { Self {
playing: false, playing: false,
queue_open: false,
audio_player: None, audio_player: None,
history: VecDeque::new(), history: VecDeque::new(),
queue: VecDeque::new(), queue: VecDeque::new(),

121
src/queue.rs Normal file
View File

@ -0,0 +1,121 @@
use crate::playstatus::PlayStatus;
use crate::song::Song;
use leptos::ev::MouseEvent;
use leptos::leptos_dom::*;
use leptos::*;
use leptos_icons::*;
use leptos_icons::CgIcon::*;
use leptos::ev::DragEvent;
const RM_BTN_SIZE: &str = "2.5rem";
fn remove_song_fn(index: usize, status: RwSignal<PlayStatus>) {
if index == 0 {
log!("Error: Trying to remove currently playing song (index 0) from queue");
} else {
log!("Remove Song from Queue: Song is not currently playing, deleting song from queue and not adding to history");
status.update(|status| {
status.queue.remove(index);
});
}
}
#[component]
pub fn Queue(status: RwSignal<PlayStatus>) -> impl IntoView {
let remove_song = move |index: usize| {
remove_song_fn(index, status);
log!("Removed song {}", index + 1);
};
let prevent_focus = move |e: MouseEvent| {
e.prevent_default();
};
let index_being_dragged = create_rw_signal(-1);
let index_being_hovered = create_rw_signal(-1);
let on_drag_start = move |_e: DragEvent, index: usize| {
// set the index of the item being dragged
index_being_dragged.set(index as i32);
};
let on_drop = move |e: DragEvent| {
e.prevent_default();
// if the index of the item being dragged is not the same as the index of the item being hovered over
if index_being_dragged.get() != index_being_hovered.get() && index_being_dragged.get() > 0 && index_being_hovered.get() > 0 {
// get the index of the item being dragged
let dragged_index = index_being_dragged.get_untracked() as usize;
// get the index of the item being hovered over
let hovered_index = index_being_hovered.get_untracked() as usize;
// update the queue
status.update(|status| {
// remove the dragged item from the list
let dragged_item = status.queue.remove(dragged_index);
// insert the dragged item at the index of the item being hovered over
status.queue.insert(hovered_index, dragged_item.unwrap());
});
// reset the index of the item being dragged
index_being_dragged.set(-1);
// reset the index of the item being hovered over
index_being_hovered.set(-1);
log!("drag end. Moved item from index {} to index {}", dragged_index, hovered_index);
}
else {
// reset the index of the item being dragged
index_being_dragged.set(-1);
// reset the index of the item being hovered over
index_being_hovered.set(-1);
}
};
let on_drag_enter = move |_e: DragEvent, index: usize| {
// set the index of the item being hovered over
index_being_hovered.set(index as i32);
};
let on_drag_over = move |e: DragEvent| {
e.prevent_default();
};
view!{
<Show
when=move || status.with(|status| status.queue_open)
fallback=|| view!{""}>
<div class="queue">
<div class="queue-header">
<h2>Queue</h2>
</div>
<ul>
{
move || status.with(|status| status.queue.iter()
.enumerate()
.map(|(index, song)| view! {
<div class="queue-item"
draggable="true"
on:dragstart=move |e: DragEvent| on_drag_start(e, index)
on:drop=on_drop
on:dragenter=move |e: DragEvent| on_drag_enter(e, index)
on:dragover=on_drag_over
>
<Song song_image_path=song.image_path.clone() song_title=song.name.clone() song_artist=song.artist.clone() />
<Show
when=move || index != 0
fallback=|| view!{
<p>Playing</p>
}>
<button on:click=move |_| remove_song(index) on:mousedown=prevent_focus>
<Icon class="remove-song" width=RM_BTN_SIZE height=RM_BTN_SIZE icon=Icon::from(CgTrash) />
</button>
</Show>
</div>
})
.collect::<Vec<_>>())
}
</ul>
</div>
</Show>
}
}

14
src/song.rs Normal file
View File

@ -0,0 +1,14 @@
use leptos::*;
#[component]
pub fn Song(song_image_path: String, song_title: String, song_artist: String) -> impl IntoView {
view!{
<div class="queue-song">
<img src={song_image_path} alt={song_title.clone()} />
<div class="queue-song-info">
<h3>{song_title}</h3>
<p>{song_artist}</p>
</div>
</div>
}
}

View File

@ -1,5 +1,5 @@
/// Holds information about a song /// Holds information about a song
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct SongData { pub struct SongData {
/// Song name /// Song name
pub name: String, pub name: String,

View File

@ -1,5 +1,6 @@
@import 'playbar.scss'; @import 'playbar.scss';
@import 'theme.scss'; @import 'theme.scss';
@import 'queue.scss';
@import 'login.scss'; @import 'login.scss';
@import 'signup.scss'; @import 'signup.scss';

View File

@ -88,4 +88,28 @@
right: 10px; right: 10px;
top: 13px; top: 13px;
} }
.queue-toggle {
position: absolute;
bottom: 13px;
top: 13px;
right: 90px;
button {
.controlbtn {
color: $text-controls-color;
}
.controlbtn:hover {
color: $controls-hover-color;
}
.controlbtn:active {
color: $controls-click-color;
}
background-color: transparent;
border: transparent;
}
}
} }

76
style/queue.scss Normal file
View File

@ -0,0 +1,76 @@
@import 'theme.scss';
.queue {
position: fixed;
top: 0;
right: 0;
width: 400px;
height: calc(100% - 78.9px); /* Adjust height to fit the queue */
background-color: #424242; /* Queue background color */
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
overflow-y: auto; /* Add scroll bar when queue is too long */
.queue-header {
background-color: #333; /* Header background color */
color: #fff; /* Header text color */
padding: 10px;
text-align: center;
}
ul {
list-style: none;
padding: 0;
margin: 0;
.queue-item {
display: flex;
align-items: center;
padding-left: 10px;
padding-right: 10px;
padding-top: 5px;
padding-bottom: 5px;
.queue-song {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #ccc; /* Separator line color */
img {
max-width: 50px; /* Adjust maximum width for images */
margin-right: 10px; /* Add spacing between image and text */
border-radius: 5px; /* Add border radius to image */
}
.queue-song-info {
h3 {
margin: 0; /* Remove default margin for heading */
color: #fff; /* Adjust text color for song */
}
p {
margin: 0; /* Remove default margin for paragraph */
color: #aaa; /* Adjust text color for artist */
}
}
}
button {
background: none;
border: none;
color: #fff;
cursor: pointer;
margin-left: auto;
}
p {
color: #fff;
font-weight: bold;
margin-left: auto;
background: none;
border: none;
}
}
}
}

View File

@ -6,6 +6,7 @@ $controls-click-color: #909090;
$play-bar-background-color: #212121; $play-bar-background-color: #212121;
$play-grad-start: #0a0533; $play-grad-start: #0a0533;
$play-grad-end: $accent-color; $play-grad-end: $accent-color;
$queue-background-color: $play-bar-background-color;
$auth-inputs: #796dd4; $auth-inputs: #796dd4;
$auth-containers: white; $auth-containers: white;