mirror of
https://github.com/Dangoware/dango-music-player.git
synced 2025-04-19 10:02:53 -05:00
Added Seeking and started work on RPC integration
This commit is contained in:
parent
fd12d3db02
commit
077eae00c5
9 changed files with 215 additions and 36 deletions
|
@ -37,4 +37,5 @@ futures = "0.3.30"
|
||||||
async-channel = "2.3.1"
|
async-channel = "2.3.1"
|
||||||
ciborium = "0.2.2"
|
ciborium = "0.2.2"
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
prismriver = { git = "https://github.com/Dangoware/prismriver.git" }
|
prismriver = { git = "https://github.com/Dangoware/prismriver.git"}
|
||||||
|
parking_lot = "0.12.3"
|
||||||
|
|
|
@ -3,8 +3,10 @@
|
||||||
//! other functions
|
//! other functions
|
||||||
#![allow(while_true)]
|
#![allow(while_true)]
|
||||||
|
|
||||||
|
use async_channel::unbounded;
|
||||||
use chrono::TimeDelta;
|
use chrono::TimeDelta;
|
||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
use kushi::{Queue, QueueItemType};
|
use kushi::{Queue, QueueItemType};
|
||||||
use kushi::{QueueError, QueueItem};
|
use kushi::{QueueError, QueueItem};
|
||||||
use prismriver::{Prismriver, Volume, Error as PrismError};
|
use prismriver::{Prismriver, Volume, Error as PrismError};
|
||||||
|
@ -82,6 +84,7 @@ pub enum PlayerCommand {
|
||||||
PrevSong,
|
PrevSong,
|
||||||
Pause,
|
Pause,
|
||||||
Play,
|
Play,
|
||||||
|
Seek(i64),
|
||||||
Enqueue(usize),
|
Enqueue(usize),
|
||||||
SetVolume(f32),
|
SetVolume(f32),
|
||||||
PlayNow(Uuid, PlayerLocation),
|
PlayNow(Uuid, PlayerLocation),
|
||||||
|
@ -161,6 +164,7 @@ pub struct ControllerInput {
|
||||||
library: MusicLibrary,
|
library: MusicLibrary,
|
||||||
config: Arc<RwLock<Config>>,
|
config: Arc<RwLock<Config>>,
|
||||||
playback_info: Arc<AtomicCell<PlaybackInfo>>,
|
playback_info: Arc<AtomicCell<PlaybackInfo>>,
|
||||||
|
notify_next_song: Sender<Song>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ControllerHandle {
|
pub struct ControllerHandle {
|
||||||
|
@ -170,11 +174,12 @@ pub struct ControllerHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ControllerHandle {
|
impl ControllerHandle {
|
||||||
pub fn new(library: MusicLibrary, config: Arc<RwLock<Config>>) -> (Self, ControllerInput, Arc<AtomicCell<PlaybackInfo>>) {
|
pub fn new(library: MusicLibrary, config: Arc<RwLock<Config>>) -> (Self, ControllerInput, Arc<AtomicCell<PlaybackInfo>>, Receiver<Song>) {
|
||||||
let lib_mail = MailMan::double();
|
let lib_mail = MailMan::double();
|
||||||
let player_mail = MailMan::double();
|
let player_mail = MailMan::double();
|
||||||
let queue_mail = MailMan::double();
|
let queue_mail = MailMan::double();
|
||||||
let playback_info = Arc::new(AtomicCell::new(PlaybackInfo::default()));
|
let playback_info = Arc::new(AtomicCell::new(PlaybackInfo::default()));
|
||||||
|
let notify_next_song = crossbeam::channel::unbounded::<Song>();
|
||||||
(
|
(
|
||||||
ControllerHandle {
|
ControllerHandle {
|
||||||
lib_mail: lib_mail.0.clone(),
|
lib_mail: lib_mail.0.clone(),
|
||||||
|
@ -188,8 +193,10 @@ impl ControllerHandle {
|
||||||
library,
|
library,
|
||||||
config,
|
config,
|
||||||
playback_info: Arc::clone(&playback_info),
|
playback_info: Arc::clone(&playback_info),
|
||||||
|
notify_next_song: notify_next_song.0,
|
||||||
},
|
},
|
||||||
playback_info,
|
playback_info,
|
||||||
|
notify_next_song.1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -237,6 +244,7 @@ impl Controller {
|
||||||
mut library,
|
mut library,
|
||||||
config,
|
config,
|
||||||
playback_info,
|
playback_info,
|
||||||
|
notify_next_song,
|
||||||
}: ControllerInput
|
}: ControllerInput
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let queue: Queue<QueueSong, QueueAlbum> = Queue {
|
let queue: Queue<QueueSong, QueueAlbum> = Queue {
|
||||||
|
@ -309,6 +317,7 @@ impl Controller {
|
||||||
player_mail.0,
|
player_mail.0,
|
||||||
queue_mail.0,
|
queue_mail.0,
|
||||||
playback_info,
|
playback_info,
|
||||||
|
notify_next_song,
|
||||||
).unwrap();
|
).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -328,7 +337,6 @@ impl Controller {
|
||||||
mut state: ControllerState,
|
mut state: ControllerState,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
player.write().unwrap().set_volume(Volume::new(state.volume));
|
player.write().unwrap().set_volume(Volume::new(state.volume));
|
||||||
println!("volume set to {}", state.volume);
|
|
||||||
'outer: while true {
|
'outer: while true {
|
||||||
let _mail = player_mail.recv().await;
|
let _mail = player_mail.recv().await;
|
||||||
if let Ok(mail) = _mail {
|
if let Ok(mail) = _mail {
|
||||||
|
@ -343,9 +351,13 @@ impl Controller {
|
||||||
player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap();
|
player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PlayerCommand::Seek(time) => {
|
||||||
|
let res = player.write().unwrap().seek_to(TimeDelta::milliseconds(time));
|
||||||
|
player_mail.send(PlayerResponse::Empty(res.map_err(|e| e.into()))).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
PlayerCommand::SetVolume(volume) => {
|
PlayerCommand::SetVolume(volume) => {
|
||||||
player.write().unwrap().set_volume(Volume::new(volume));
|
player.write().unwrap().set_volume(Volume::new(volume));
|
||||||
println!("volume set to {volume}");
|
|
||||||
player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap();
|
player_mail.send(PlayerResponse::Empty(Ok(()))).await.unwrap();
|
||||||
|
|
||||||
state.volume = volume;
|
state.volume = volume;
|
||||||
|
@ -403,6 +415,9 @@ impl Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
player_mail.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone()))).await.unwrap();
|
player_mail.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone()))).await.unwrap();
|
||||||
|
|
||||||
|
state.now_playing = np_song.song.uuid;
|
||||||
|
_ = state.write_file();
|
||||||
} QueueResponse::Item(Err(e)) => {
|
} QueueResponse::Item(Err(e)) => {
|
||||||
player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap();
|
player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap();
|
||||||
}
|
}
|
||||||
|
@ -425,6 +440,9 @@ impl Controller {
|
||||||
|
|
||||||
let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")};
|
let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")};
|
||||||
player_mail.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone()))).await.unwrap();
|
player_mail.send(PlayerResponse::NowPlaying(Ok(np_song.song.clone()))).await.unwrap();
|
||||||
|
|
||||||
|
state.now_playing = np_song.song.uuid;
|
||||||
|
_ = state.write_file();
|
||||||
}
|
}
|
||||||
QueueResponse::Item(Err(e)) => {
|
QueueResponse::Item(Err(e)) => {
|
||||||
player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap();
|
player_mail.send(PlayerResponse::NowPlaying(Err(e.into()))).await.unwrap();
|
||||||
|
@ -543,6 +561,7 @@ impl Controller {
|
||||||
player_mail: MailMan<PlayerCommand, PlayerResponse>,
|
player_mail: MailMan<PlayerCommand, PlayerResponse>,
|
||||||
queue_mail: MailMan<QueueCommand, QueueResponse>,
|
queue_mail: MailMan<QueueCommand, QueueResponse>,
|
||||||
player_info: Arc<AtomicCell<PlaybackInfo>>,
|
player_info: Arc<AtomicCell<PlaybackInfo>>,
|
||||||
|
notify_next_song: Sender<Song>,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
|
|
||||||
let finished_recv = player.read().unwrap().get_finished_recv();
|
let finished_recv = player.read().unwrap().get_finished_recv();
|
||||||
|
@ -550,7 +569,7 @@ impl Controller {
|
||||||
std::thread::scope(|s| {
|
std::thread::scope(|s| {
|
||||||
// Thread for timing and metadata
|
// Thread for timing and metadata
|
||||||
s.spawn({
|
s.spawn({
|
||||||
let player = Arc::clone(&player);
|
// let player = Arc::clone(&player);
|
||||||
move || {
|
move || {
|
||||||
while true {
|
while true {
|
||||||
let player = player.read().unwrap();
|
let player = player.read().unwrap();
|
||||||
|
@ -567,13 +586,21 @@ impl Controller {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Thread for End of Track
|
// Thread for End of Track
|
||||||
s.spawn(move || {
|
s.spawn(move || { futures::executor::block_on(async {
|
||||||
while true {
|
while true {
|
||||||
let _ = finished_recv.recv();
|
let _ = finished_recv.recv();
|
||||||
|
println!("End of song");
|
||||||
|
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
player_mail.send(PlayerCommand::NextSong).await.unwrap();
|
||||||
|
let PlayerResponse::NowPlaying(res) = player_mail.recv().await.unwrap() else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
if let Ok(song) = res {
|
||||||
|
notify_next_song.send(song).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
});});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check for duration and spit it out
|
// Check for duration and spit it out
|
||||||
|
@ -681,7 +708,7 @@ impl Controller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default, Serialize, Clone)]
|
||||||
pub struct PlaybackInfo {
|
pub struct PlaybackInfo {
|
||||||
pub duration: Option<TimeDelta>,
|
pub duration: Option<TimeDelta>,
|
||||||
pub position: Option<TimeDelta>,
|
pub position: Option<TimeDelta>,
|
||||||
|
|
|
@ -106,6 +106,14 @@ impl ControllerHandle {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn seek(&self, time: i64) -> Result<(), PlayerError> {
|
||||||
|
self.player_mail.send(PlayerCommand::Seek(time)).await.unwrap();
|
||||||
|
let PlayerResponse::Empty(res) = self.player_mail.recv().await.unwrap() else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn set_volume(&self, volume: f32) -> () {
|
pub async fn set_volume(&self, volume: f32) -> () {
|
||||||
self.player_mail.send(PlayerCommand::SetVolume(volume)).await.unwrap();
|
self.player_mail.send(PlayerCommand::SetVolume(volume)).await.unwrap();
|
||||||
let PlayerResponse::Empty(Ok(())) = self.player_mail.recv().await.unwrap() else {
|
let PlayerResponse::Empty(Ok(())) = self.player_mail.recv().await.unwrap() else {
|
||||||
|
|
|
@ -36,6 +36,8 @@ rfd = "0.15.1"
|
||||||
colog = "1.3.0"
|
colog = "1.3.0"
|
||||||
tempfile = "3.14.0"
|
tempfile = "3.14.0"
|
||||||
opener = "0.7.2"
|
opener = "0.7.2"
|
||||||
|
discord-presence = { version = "1.4.1", features = ["activity_type"] }
|
||||||
|
parking_lot = "0.12.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [ "custom-protocol" ]
|
default = [ "custom-protocol" ]
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
use std::{fs, path::PathBuf, str::FromStr, thread::{scope, spawn}};
|
#![allow(while_true)]
|
||||||
|
|
||||||
use crossbeam::channel::{unbounded, Receiver, Sender};
|
use std::{borrow::BorrowMut, fs, ops::Deref, path::PathBuf, sync::{atomic::Ordering, Arc}, thread::{scope, spawn}, time::Duration};
|
||||||
use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse, PlaybackInfo}, music_storage::library::MusicLibrary};
|
|
||||||
|
use crossbeam::channel::{bounded, unbounded, Receiver, Sender};
|
||||||
|
use discord_presence::{models::{Activity, ActivityButton, ActivityTimestamps, ActivityType}, Event};
|
||||||
|
use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse, PlaybackInfo}, music_storage::library::{MusicLibrary, Song}};
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use parking_lot::lock_api::{RawRwLock, RwLock};
|
||||||
use rfd::FileHandle;
|
use rfd::FileHandle;
|
||||||
use tauri::{http::Response, Emitter, Manager, State, WebviewWindowBuilder, Wry};
|
use tauri::{http::Response, Emitter, Manager, State, WebviewWindowBuilder, Wry};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use wrappers::_Song;
|
||||||
|
|
||||||
use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue, import_playlist, get_playlist, get_playlists, remove_from_queue};
|
use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue, import_playlist, get_playlist, get_playlists, remove_from_queue, seek};
|
||||||
use commands::{add_song_to_queue, play_now, display_album_art};
|
use commands::{add_song_to_queue, play_now, display_album_art};
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,15 +26,25 @@ pub fn run() {
|
||||||
let (rx, tx) = unbounded::<Config>();
|
let (rx, tx) = unbounded::<Config>();
|
||||||
let (lib_rx, lib_tx) = unbounded::<Option<PathBuf>>();
|
let (lib_rx, lib_tx) = unbounded::<Option<PathBuf>>();
|
||||||
let (handle_rx, handle_tx) = unbounded::<ControllerHandle>();
|
let (handle_rx, handle_tx) = unbounded::<ControllerHandle>();
|
||||||
|
let (playback_info_rx, playback_info_tx) = bounded(1);
|
||||||
|
let (next_rx, next_tx) = bounded(1);
|
||||||
|
|
||||||
let controller_thread = spawn(move || {
|
let _controller_thread = spawn(move || {
|
||||||
let mut config = { tx.recv().unwrap() } ;
|
let mut config = { tx.recv().unwrap() } ;
|
||||||
let scan_path = { lib_tx.recv().unwrap() };
|
let scan_path = { lib_tx.recv().unwrap() };
|
||||||
let _temp_config = ConfigLibrary::default();
|
let _temp_config = ConfigLibrary::default();
|
||||||
let _lib = config.libraries.get_default().unwrap_or(&_temp_config);
|
let _lib = config.libraries.get_default().unwrap_or(&_temp_config);
|
||||||
|
|
||||||
let save_path = if _lib.path == PathBuf::default() {
|
let save_path = if _lib.path == PathBuf::default() {
|
||||||
scan_path.as_ref().unwrap().clone().canonicalize().unwrap().join("library.dlib")
|
let p = scan_path.as_ref().unwrap().clone().canonicalize().unwrap();
|
||||||
|
|
||||||
|
if cfg!(windows) {
|
||||||
|
p.join("library_windows.dlib")
|
||||||
|
} else if cfg!(unix) {
|
||||||
|
p.join("library_unix.dlib")
|
||||||
|
} else {
|
||||||
|
p.join("library.dlib")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_lib.path.clone()
|
_lib.path.clone()
|
||||||
};
|
};
|
||||||
|
@ -54,21 +70,22 @@ pub fn run() {
|
||||||
|
|
||||||
library.save(save_path).unwrap();
|
library.save(save_path).unwrap();
|
||||||
|
|
||||||
let (handle, input, playback_info) = ControllerHandle::new(
|
let (
|
||||||
|
handle,
|
||||||
|
input,
|
||||||
|
playback_info,
|
||||||
|
next_song_notification,
|
||||||
|
) = ControllerHandle::new(
|
||||||
library,
|
library,
|
||||||
std::sync::Arc::new(std::sync::RwLock::new(config))
|
std::sync::Arc::new(std::sync::RwLock::new(config))
|
||||||
);
|
);
|
||||||
|
|
||||||
handle_rx.send(handle).unwrap();
|
handle_rx.send(handle).unwrap();
|
||||||
|
playback_info_rx.send(playback_info).unwrap();
|
||||||
|
next_rx.send(next_song_notification).unwrap();
|
||||||
|
|
||||||
scope(|s| {
|
let _controller = futures::executor::block_on(Controller::start(input)).unwrap();
|
||||||
s.spawn(|| {
|
|
||||||
let _controller = futures::executor::block_on(Controller::start(input)).unwrap();
|
|
||||||
});
|
|
||||||
s.spawn(|| {
|
|
||||||
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -93,10 +110,85 @@ pub fn run() {
|
||||||
get_playlists,
|
get_playlists,
|
||||||
remove_from_queue,
|
remove_from_queue,
|
||||||
display_album_art,
|
display_album_art,
|
||||||
|
seek,
|
||||||
]).manage(ConfigRx(rx))
|
]).manage(ConfigRx(rx))
|
||||||
.manage(LibRx(lib_rx))
|
.manage(LibRx(lib_rx))
|
||||||
.manage(HandleTx(handle_tx))
|
.manage(HandleTx(handle_tx))
|
||||||
.manage(tempfile::TempDir::new().unwrap())
|
.manage(tempfile::TempDir::new().unwrap())
|
||||||
|
.setup(|app| {
|
||||||
|
let _app = app.handle().clone();
|
||||||
|
let app = _app.clone();
|
||||||
|
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("PlaybackInfo handler".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
let mut _info = Arc::new(RwLock::new(PlaybackInfo::default()));
|
||||||
|
let mut _now_playing: Arc<RwLock<_, Option<Song>>> = Arc::new(RwLock::new(None));
|
||||||
|
|
||||||
|
scope(|s| {
|
||||||
|
let info = _info.clone();
|
||||||
|
s.spawn(|| {
|
||||||
|
let info = info;
|
||||||
|
let playback_info = playback_info_tx.recv().unwrap();
|
||||||
|
while true {
|
||||||
|
let i = playback_info.take();
|
||||||
|
app.emit("playback_info", i.clone()).unwrap();
|
||||||
|
*info.write() = i;
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let now_playing = _now_playing.clone();
|
||||||
|
s.spawn(|| {
|
||||||
|
let now_playing = now_playing;
|
||||||
|
let next_song_notification = next_tx.recv().unwrap();
|
||||||
|
while true {
|
||||||
|
let song = next_song_notification.recv().unwrap();
|
||||||
|
app.emit("now_playing_change", _Song::from(&song)).unwrap();
|
||||||
|
app.emit("queue_updated", ()).unwrap();
|
||||||
|
app.emit("playing", ()).unwrap();
|
||||||
|
now_playing.write().insert(song);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let info = _info.clone();
|
||||||
|
let now_playing = _now_playing.clone();
|
||||||
|
s.spawn(|| {
|
||||||
|
let info = info;
|
||||||
|
let now_playing = now_playing;
|
||||||
|
let mut rpc_client = discord_presence::Client::new(std::env!("DISCORD_SECRET").parse::<u64>().unwrap());
|
||||||
|
rpc_client.start();
|
||||||
|
rpc_client.block_until_event(Event::Connected).unwrap();
|
||||||
|
rpc_client.set_activity(|_| {
|
||||||
|
Activity {
|
||||||
|
state: Some("Idle".to_string()),
|
||||||
|
_type: Some(ActivityType::Listening),
|
||||||
|
buttons: vec![ActivityButton {
|
||||||
|
label: Some("Try the Player!(beta)".to_string()),
|
||||||
|
url: Some("https://github.com/Dangoware/dango-music-player".to_string())
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
while true {
|
||||||
|
rpc_client.set_activity(|mut a| {
|
||||||
|
if let Some(song) = now_playing.read().clone() {
|
||||||
|
|
||||||
|
a.timestamps = info.read().duration.map(|dur| ActivityTimestamps::new().end(dur.num_milliseconds() as u64) );
|
||||||
|
a.details = Some(format!("{} 🍡 {}" song.tags. ))
|
||||||
|
}
|
||||||
|
a
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
.register_asynchronous_uri_scheme_protocol("asset", move |ctx, req, res| {
|
.register_asynchronous_uri_scheme_protocol("asset", move |ctx, req, res| {
|
||||||
let query = req
|
let query = req
|
||||||
.clone()
|
.clone()
|
||||||
|
|
|
@ -193,3 +193,8 @@ pub async fn get_song(ctrl_handle: State<'_, ControllerHandle>, uuid: Uuid) -> R
|
||||||
println!("got song {}", &song.tags.get(&Tag::Title).unwrap_or(&String::new()));
|
println!("got song {}", &song.tags.get(&Tag::Title).unwrap_or(&String::new()));
|
||||||
Ok(_Song::from(&song))
|
Ok(_Song::from(&song))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn seek(ctrl_handle: State<'_, ControllerHandle>, time: i64) -> Result<(), String> {
|
||||||
|
ctrl_handle.seek(time).await.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
|
@ -239,3 +239,8 @@ main {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--mediumTextColor);
|
color: var(--mediumTextColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unselectable {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
59
src/App.tsx
59
src/App.tsx
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useState } from "react";
|
import React, { createRef, useEffect, useRef, useState } from "react";
|
||||||
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
|
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import { Config } from "./types";
|
import { Config, playbackInfo } from "./types";
|
||||||
// import { EventEmitter } from "@tauri-apps/plugin-shell";
|
// import { EventEmitter } from "@tauri-apps/plugin-shell";
|
||||||
// import { listen } from "@tauri-apps/api/event";
|
// import { listen } from "@tauri-apps/api/event";
|
||||||
// import { fetch } from "@tauri-apps/plugin-http";
|
// import { fetch } from "@tauri-apps/plugin-http";
|
||||||
|
@ -263,13 +263,13 @@ function Song(props: SongProps) {
|
||||||
// console.log(props.tags);
|
// console.log(props.tags);
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<div onClick={() => {
|
<div onDoubleClick={() => {
|
||||||
invoke("play_now", { uuid: props.uuid, location: props.playerLocation }).then(() => {})
|
invoke("play_now", { uuid: props.uuid, location: props.playerLocation }).then(() => {})
|
||||||
}} className="song">
|
}} className="song">
|
||||||
<p className="artist">{ props.tags.TrackArtist }</p>
|
<p className="artist unselectable">{ props.tags.TrackArtist }</p>
|
||||||
<p className="title">{ props.tags.TrackTitle }</p>
|
<p className="title unselectable">{ props.tags.TrackTitle }</p>
|
||||||
<p className="album">{ props.tags.AlbumTitle }</p>
|
<p className="album unselectable">{ props.tags.AlbumTitle }</p>
|
||||||
<p className="duration">
|
<p className="duration unselectable">
|
||||||
{ Math.round(+props.duration / 60) }:
|
{ Math.round(+props.duration / 60) }:
|
||||||
{ (+props.duration % 60).toString().padStart(2, "0") }
|
{ (+props.duration % 60).toString().padStart(2, "0") }
|
||||||
</p>
|
</p>
|
||||||
|
@ -291,10 +291,38 @@ interface PlayBarProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
||||||
|
const [position, setPosition] = useState(0);
|
||||||
|
const [duration, setDuration] = useState(0);
|
||||||
|
const [seekBarSize, setSeekBarSize] = useState(0);
|
||||||
|
const seekBarRef = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlisten = appWindow.listen<any>("playback_info", ({ payload, }) => {
|
||||||
|
const info = payload as playbackInfo;
|
||||||
|
const _pos = Array.isArray(info.position) ? info.position![0] : 0;
|
||||||
|
const _dur = Array.isArray(info.duration) ? info.duration![0] : 0;
|
||||||
|
|
||||||
|
setPosition(_pos);
|
||||||
|
setDuration(_dur);
|
||||||
|
let progress = (Math.floor((_pos/_dur)*100));
|
||||||
|
console.log(progress + '%');
|
||||||
|
setSeekBarSize(progress)
|
||||||
|
})
|
||||||
|
return () => { unlisten.then((f) => f()) }
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const seek = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
let rect = seekBarRef.current!.getBoundingClientRect();
|
||||||
|
let val = ((event.clientX-rect.left) / (rect.width))*duration;
|
||||||
|
|
||||||
|
invoke('seek', { time: Math.round(val * 1000) }).then()
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="playBar" className="playBar">
|
<section id="playBar" className="playBar unselectable">
|
||||||
<div className="seekBar">
|
<div className="seekBar" ref={ seekBarRef } onClick={ seek } onDrag={ seek }>
|
||||||
<div className="seekOverlay" id="seekOverlay"></div>
|
<div className="seekOverlay" id="seekOverlay" style={{ width: seekBarSize + '%' } }></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bottomSpaced">
|
<div className="bottomSpaced">
|
||||||
<div className="bottomLeft">
|
<div className="bottomLeft">
|
||||||
|
@ -312,7 +340,12 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
||||||
<input type="range" name="volume" id="volumeSlider" onChange={ (volume) => {
|
<input type="range" name="volume" id="volumeSlider" onChange={ (volume) => {
|
||||||
invoke('set_volume', { volume: volume.target.value }).then(() => {})
|
invoke('set_volume', { volume: volume.target.value }).then(() => {})
|
||||||
}} />
|
}} />
|
||||||
<p id="timeDisplay"></p>
|
<p id="timeDisplay">
|
||||||
|
{ Math.round(+position / 60) }:
|
||||||
|
{ (+position % 60).toString().padStart(2, "0") } /
|
||||||
|
{ Math.round(+duration / 60) }:
|
||||||
|
{ (+duration % 60).toString().padStart(2, "0") }
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -329,7 +362,7 @@ interface NowPlayingProps {
|
||||||
function NowPlaying({ title, artist, album, artwork }: NowPlayingProps) {
|
function NowPlaying({ title, artist, album, artwork }: NowPlayingProps) {
|
||||||
return (
|
return (
|
||||||
<section className="nowPlaying">
|
<section className="nowPlaying">
|
||||||
<div className="artworkWrapper">
|
<div className="artworkWrapper unselectable">
|
||||||
{ artwork }
|
{ artwork }
|
||||||
</div>
|
</div>
|
||||||
<h3>{ title }</h3>
|
<h3>{ title }</h3>
|
||||||
|
@ -358,7 +391,7 @@ interface QueueSongProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function QueueSong({ song, location, index }: QueueSongProps) {
|
function QueueSong({ song, location, index }: QueueSongProps) {
|
||||||
console.log(song.tags);
|
// console.log(song.tags);
|
||||||
|
|
||||||
let removeFromQueue = () => {
|
let removeFromQueue = () => {
|
||||||
invoke('remove_from_queue', { index: index }).then(() => {})
|
invoke('remove_from_queue', { index: index }).then(() => {})
|
||||||
|
|
|
@ -61,3 +61,9 @@ export enum URI {
|
||||||
export enum BannedType {
|
export enum BannedType {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface playbackInfo {
|
||||||
|
duration?: number[],
|
||||||
|
position?: [number, number],
|
||||||
|
metadata: Map<string, string>
|
||||||
|
}
|
Loading…
Reference in a new issue