Merge branch '1-add-queue' into 'main'
Add functioning queue See merge request libretunes/libretunes!11
This commit is contained in:
commit
59b11dae76
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -1311,8 +1311,10 @@ checksum = "f41f2deec9249d16ef6b1a8442fbe16013f67053797052aa0b7d2f5ebd0f0098"
|
||||
dependencies = [
|
||||
"icondata_ai",
|
||||
"icondata_bs",
|
||||
"icondata_cg",
|
||||
"icondata_core",
|
||||
"icondata_io",
|
||||
"icondata_ri",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1333,6 +1335,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "icondata_core"
|
||||
version = "0.0.2"
|
||||
@ -1348,6 +1359,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
|
@ -23,6 +23,8 @@ leptos_icons = { version = "0.1.0", default_features = false, features = [
|
||||
"BsPauseFill",
|
||||
"BsSkipStartFill",
|
||||
"BsSkipEndFill",
|
||||
"RiPlayListMediaFill",
|
||||
"CgTrash",
|
||||
"IoReturnUpBackSharp",
|
||||
"AiEyeFilled",
|
||||
"AiEyeInvisibleFilled"
|
||||
|
11
src/app.rs
11
src/app.rs
@ -1,3 +1,6 @@
|
||||
use crate::playbar::PlayBar;
|
||||
use crate::playstatus::PlayStatus;
|
||||
use crate::queue::Queue;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
@ -34,7 +37,13 @@ pub fn App() -> impl IntoView {
|
||||
/// Renders the home page of your application.
|
||||
#[component]
|
||||
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
|
||||
|
@ -4,6 +4,8 @@ pub mod songdata;
|
||||
pub mod playstatus;
|
||||
pub mod playbar;
|
||||
pub mod database;
|
||||
pub mod queue;
|
||||
pub mod song;
|
||||
pub mod models;
|
||||
pub mod pages;
|
||||
pub mod users;
|
||||
|
@ -6,6 +6,7 @@ use leptos::html::{Audio, Div};
|
||||
use leptos::leptos_dom::*;
|
||||
use leptos::*;
|
||||
use leptos_icons::BsIcon::*;
|
||||
use leptos_icons::RiIcon::*;
|
||||
use leptos_icons::*;
|
||||
|
||||
/// 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
|
||||
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
|
||||
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
|
||||
/// * `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| {
|
||||
if let Some(audio) = status.get_audio() {
|
||||
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
|
||||
/// * `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() {
|
||||
error!("Unable to skip to non-finite time: {}", time);
|
||||
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
|
||||
/// * `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| {
|
||||
if let Some(audio) = status.get_audio() {
|
||||
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
|
||||
///
|
||||
/// 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
|
||||
#[component]
|
||||
pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
@ -457,6 +493,7 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||
<MediaInfo status=status />
|
||||
<PlayControls status=status />
|
||||
<PlayDuration elapsed_secs=elapsed_secs.into() total_secs=total_secs.into() />
|
||||
<QueueToggle status=status />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ use crate::songdata::SongData;
|
||||
pub struct PlayStatus {
|
||||
/// Whether or not the audio player is currently playing
|
||||
pub playing: bool,
|
||||
/// Whether or not the queue is open
|
||||
pub queue_open: bool,
|
||||
/// A reference to the HTML audio element
|
||||
pub audio_player: Option<NodeRef<Audio>>,
|
||||
/// A queue of songs that have been played, ordered from oldest to newest
|
||||
@ -53,6 +55,7 @@ impl Default for PlayStatus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
playing: false,
|
||||
queue_open: false,
|
||||
audio_player: None,
|
||||
history: VecDeque::new(),
|
||||
queue: VecDeque::new(),
|
||||
|
121
src/queue.rs
Normal file
121
src/queue.rs
Normal 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
14
src/song.rs
Normal 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>
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/// Holds information about a song
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SongData {
|
||||
/// Song name
|
||||
pub name: String,
|
||||
|
@ -1,5 +1,6 @@
|
||||
@import 'playbar.scss';
|
||||
@import 'theme.scss';
|
||||
@import 'queue.scss';
|
||||
@import 'login.scss';
|
||||
@import 'signup.scss';
|
||||
|
||||
|
@ -88,4 +88,28 @@
|
||||
right: 10px;
|
||||
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
76
style/queue.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ $controls-click-color: #909090;
|
||||
$play-bar-background-color: #212121;
|
||||
$play-grad-start: #0a0533;
|
||||
$play-grad-end: $accent-color;
|
||||
$queue-background-color: $play-bar-background-color;
|
||||
|
||||
$auth-inputs: #796dd4;
|
||||
$auth-containers: white;
|
||||
|
Loading…
x
Reference in New Issue
Block a user