mirror of
https://github.com/Dangoware/dango-music-player.git
synced 2025-04-19 10:02:53 -05:00
Added basic visual config editor
This commit is contained in:
parent
04da9c88f3
commit
6f90aa09b9
11 changed files with 151 additions and 54 deletions
|
@ -364,7 +364,6 @@ fn last_fm_scrobble(scrobbler: Scrobbler, now_playing_tx: Receiver<Song>, scrobb
|
||||||
// TODO: Add support for scrobble storage for later
|
// TODO: Add support for scrobble storage for later
|
||||||
|
|
||||||
let mut song: Option<Song> = None;
|
let mut song: Option<Song> = None;
|
||||||
let mut last_song: Option<Song> = None;
|
|
||||||
LAST_FM_ACTIVE.store(true, Ordering::Relaxed);
|
LAST_FM_ACTIVE.store(true, Ordering::Relaxed);
|
||||||
println!("last.fm connected");
|
println!("last.fm connected");
|
||||||
|
|
||||||
|
|
|
@ -175,6 +175,7 @@ pub struct ControllerHandle {
|
||||||
pub(super) player_mail_rx: async_channel::Sender<PlayerCommandInput>,
|
pub(super) player_mail_rx: async_channel::Sender<PlayerCommandInput>,
|
||||||
pub(super) queue_mail_rx: async_channel::Sender<QueueCommandInput>,
|
pub(super) queue_mail_rx: async_channel::Sender<QueueCommandInput>,
|
||||||
pub(super) connections_rx: crossbeam_channel::Sender<ConnectionsNotification>,
|
pub(super) connections_rx: crossbeam_channel::Sender<ConnectionsNotification>,
|
||||||
|
pub config: Arc<RwLock<Config>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ControllerHandle {
|
impl ControllerHandle {
|
||||||
|
@ -199,6 +200,7 @@ impl ControllerHandle {
|
||||||
player_mail_rx: player_mail_rx.clone(),
|
player_mail_rx: player_mail_rx.clone(),
|
||||||
queue_mail_rx: queue_mail_rx.clone(),
|
queue_mail_rx: queue_mail_rx.clone(),
|
||||||
connections_rx: connections_mail_rx.clone(),
|
connections_rx: connections_mail_rx.clone(),
|
||||||
|
config: config.clone(),
|
||||||
},
|
},
|
||||||
ControllerInput {
|
ControllerInput {
|
||||||
player_mail: (player_mail_rx, player_mail_tx),
|
player_mail: (player_mail_rx, player_mail_tx),
|
||||||
|
|
|
@ -182,7 +182,7 @@ impl ControllerHandle {
|
||||||
self.connections_rx.send(super::connections::ConnectionsNotification::TryEnableConnection(super::connections::TryConnectionType::Discord(client_id))).unwrap();
|
self.connections_rx.send(super::connections::ConnectionsNotification::TryEnableConnection(super::connections::TryConnectionType::Discord(client_id))).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn listenbrainz_scrobble(&self, token: String) {
|
pub fn listenbrainz_scrobble_auth(&self, token: String) {
|
||||||
self.connections_rx.send(super::connections::ConnectionsNotification::TryEnableConnection(super::connections::TryConnectionType::ListenBrainz(token))).unwrap();
|
self.connections_rx.send(super::connections::ConnectionsNotification::TryEnableConnection(super::connections::TryConnectionType::ListenBrainz(token))).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,8 @@
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jprochazk/cbor": "github:jprochazk/cbor",
|
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-shell": "^2",
|
"@tauri-apps/plugin-shell": "^2",
|
||||||
"cbor": "github:jprochazk/cbor",
|
|
||||||
"cbor-x": "^1.6.0",
|
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
|
|
38
src-tauri/src/config.rs
Normal file
38
src-tauri/src/config.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use dmp_core::{config::Config, music_controller::controller::ControllerHandle};
|
||||||
|
use tauri::{State, WebviewWindowBuilder, Window, Wry};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_config_window(app: tauri::AppHandle<Wry>) -> Result<(), String> {
|
||||||
|
WebviewWindowBuilder::new(&app, "editdmpconfig", tauri::WebviewUrl::App(PathBuf::from("src/config/index.html")))
|
||||||
|
.title("Edit Dango Music Player")
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_config(ctrl_handle: State<'_, ControllerHandle>) -> Result<Config, String> {
|
||||||
|
Ok(ctrl_handle.config.read().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn save_config(ctrl_handle: State<'_, ControllerHandle>, config: Config) -> Result<(), String> {
|
||||||
|
let config_original = ctrl_handle.config.read().clone();
|
||||||
|
|
||||||
|
if config.connections.listenbrainz_token.as_ref().is_some_and(|t| Some(t) != config_original.connections.listenbrainz_token.as_ref()) {
|
||||||
|
let token = config.connections.listenbrainz_token.clone().unwrap();
|
||||||
|
ctrl_handle.listenbrainz_scrobble_auth(dbg!(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
*ctrl_handle.config.write() = config;
|
||||||
|
ctrl_handle.config.read().write_file().unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn close_window(window: Window<Wry>) -> Result<(), String> {
|
||||||
|
window.close().unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -8,16 +8,13 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use config::{close_window, get_config, open_config_window, save_config};
|
||||||
use crossbeam::channel::{bounded, unbounded, Receiver, Sender};
|
use crossbeam::channel::{bounded, unbounded, Receiver, Sender};
|
||||||
use dmp_core::{
|
use dmp_core::{
|
||||||
config::{Config, ConfigLibrary},
|
config::{Config, ConfigLibrary},
|
||||||
music_controller::{
|
music_controller::{connections::LastFMAuth, controller::{Controller, ControllerHandle, PlaybackInfo}},
|
||||||
connections::ConnectionsInput,
|
|
||||||
controller::{Controller, ControllerHandle, PlaybackInfo},
|
|
||||||
},
|
|
||||||
music_storage::library::{MusicLibrary, Song},
|
music_storage::library::{MusicLibrary, Song},
|
||||||
};
|
};
|
||||||
use futures::channel::oneshot;
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use tauri::{http::Response, Emitter, Manager, State, Wry};
|
use tauri::{http::Response, Emitter, Manager, State, Wry};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -27,23 +24,28 @@ use crate::wrappers::{
|
||||||
get_library, get_playlist, get_playlists, get_queue, get_song, import_playlist, next, pause,
|
get_library, get_playlist, get_playlists, get_queue, get_song, import_playlist, next, pause,
|
||||||
play, prev, remove_from_queue, seek, set_volume,
|
play, prev, remove_from_queue, seek, set_volume,
|
||||||
};
|
};
|
||||||
use commands::{add_song_to_queue, display_album_art, play_now};
|
use commands::{add_song_to_queue, display_album_art, last_fm_init_auth, play_now};
|
||||||
|
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod wrappers;
|
pub mod wrappers;
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
const DEFAULT_IMAGE: &[u8] = include_bytes!("../icons/icon.png");
|
const DEFAULT_IMAGE: &[u8] = include_bytes!("../icons/icon.png");
|
||||||
|
|
||||||
|
const DISCORD_CLIENT_ID: u64 = 1198868728243290152;
|
||||||
|
const LAST_FM_API_KEY: &str = env!("LAST_FM_API_KEY", "None");
|
||||||
|
const LAST_FM_API_SECRET: &str = env!("LAST_FM_API_SECRET", "None");
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let (rx, tx) = unbounded::<Config>();
|
let (config_rx, config_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 (playback_info_rx, playback_info_tx) = bounded(1);
|
||||||
let (next_rx, next_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 = { 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);
|
||||||
|
@ -97,25 +99,35 @@ pub fn run() {
|
||||||
|
|
||||||
library.save(save_path).unwrap();
|
library.save(save_path).unwrap();
|
||||||
|
|
||||||
|
let last_fm_session = config.connections.last_fm_session.clone();
|
||||||
|
let listenbrainz_token = config.connections.listenbrainz_token.clone();
|
||||||
|
|
||||||
let (handle, input, playback_info, next_song_notification) = ControllerHandle::new(
|
let (handle, input, playback_info, next_song_notification) = ControllerHandle::new(
|
||||||
library,
|
library,
|
||||||
std::sync::Arc::new(RwLock::new(config)),
|
std::sync::Arc::new(RwLock::new(config))
|
||||||
Some(ConnectionsInput {
|
|
||||||
discord_rpc_client_id: std::option_env!("DISCORD_CLIENT_ID")
|
|
||||||
.map(|id| id.parse::<u64>().unwrap()),
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
handle.discord_rpc(DISCORD_CLIENT_ID);
|
||||||
|
if let Some(token) = listenbrainz_token {
|
||||||
|
handle.listenbrainz_scrobble_auth(token);
|
||||||
|
} else {
|
||||||
|
println!("No ListenBrainz token found");
|
||||||
|
}
|
||||||
|
if let Some(session) = last_fm_session {
|
||||||
|
handle.last_fm_scrobble_auth(LAST_FM_API_KEY.to_string(), LAST_FM_API_SECRET.to_string(), LastFMAuth::Session(Some(session)));
|
||||||
|
}
|
||||||
|
|
||||||
handle_rx.send(handle).unwrap();
|
handle_rx.send(handle).unwrap();
|
||||||
playback_info_rx.send(playback_info).unwrap();
|
playback_info_rx.send(playback_info).unwrap();
|
||||||
next_rx.send(next_song_notification).unwrap();
|
next_rx.send(next_song_notification).unwrap();
|
||||||
|
|
||||||
let _controller = futures::executor::block_on(Controller::start(input)).unwrap();
|
let _controller = futures::executor::block_on(Controller::start(input)).unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
let app = tauri::Builder::default()
|
let app = tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
get_config,
|
init_get_config,
|
||||||
create_new_library,
|
create_new_library,
|
||||||
get_library,
|
get_library,
|
||||||
play,
|
play,
|
||||||
|
@ -135,8 +147,13 @@ pub fn run() {
|
||||||
remove_from_queue,
|
remove_from_queue,
|
||||||
display_album_art,
|
display_album_art,
|
||||||
seek,
|
seek,
|
||||||
|
last_fm_init_auth,
|
||||||
|
open_config_window,
|
||||||
|
get_config,
|
||||||
|
save_config,
|
||||||
|
close_window,
|
||||||
])
|
])
|
||||||
.manage(ConfigRx(rx))
|
.manage(ConfigRx(config_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())
|
||||||
|
@ -238,7 +255,7 @@ struct LibRx(Sender<Option<PathBuf>>);
|
||||||
struct HandleTx(Receiver<ControllerHandle>);
|
struct HandleTx(Receiver<ControllerHandle>);
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_config(state: State<'_, ConfigRx>) -> Result<Config, String> {
|
async fn init_get_config(state: State<'_, ConfigRx>) -> Result<Config, String> {
|
||||||
if let Some(dir) = directories::ProjectDirs::from("", "Dangoware", "dmp") {
|
if let Some(dir) = directories::ProjectDirs::from("", "Dangoware", "dmp") {
|
||||||
let path = dir.config_dir();
|
let path = dir.config_dir();
|
||||||
fs::create_dir_all(path)
|
fs::create_dir_all(path)
|
||||||
|
@ -268,6 +285,7 @@ async fn get_config(state: State<'_, ConfigRx>) -> Result<Config, String> {
|
||||||
|
|
||||||
state.inner().0.send(config.clone()).unwrap();
|
state.inner().0.send(config.clone()).unwrap();
|
||||||
|
|
||||||
|
println!("got config");
|
||||||
Ok(config)
|
Ok(config)
|
||||||
} else {
|
} else {
|
||||||
panic!("No config dir for DMP")
|
panic!("No config dir for DMP")
|
||||||
|
|
10
src/App.tsx
10
src/App.tsx
|
@ -200,6 +200,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: Play
|
||||||
} }>Library</button>
|
} }>Library</button>
|
||||||
{ playlists }
|
{ playlists }
|
||||||
<button onClick={ handle_import }>Import .m3u Playlist</button>
|
<button onClick={ handle_import }>Import .m3u Playlist</button>
|
||||||
|
<button onClick={() => { invoke('open_config_window').then(() => {}) }} style={{marginLeft: "auto", float: "right"}}>Edit DMP</button>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -312,12 +313,6 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
||||||
invoke('seek', { time: Math.round(val * 1000) }).then()
|
invoke('seek', { time: Math.round(val * 1000) }).then()
|
||||||
};
|
};
|
||||||
|
|
||||||
const lastFmLogin = () => {
|
|
||||||
invoke('last_fm_init_auth').then(() => {
|
|
||||||
setLastFmLoggedIn(true);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="playBar" className="playBar unselectable">
|
<section id="playBar" className="playBar unselectable">
|
||||||
<div className="seekBar" ref={ seekBarRef } onClick={ seek } onDrag={ seek }>
|
<div className="seekBar" ref={ seekBarRef } onClick={ seek } onDrag={ seek }>
|
||||||
|
@ -334,7 +329,6 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
|
||||||
<button onClick={ () => invoke('next').then(() => {}) }>⏭</button>
|
<button onClick={ () => invoke('next').then(() => {}) }>⏭</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="bottomRight">
|
<div className="bottomRight">
|
||||||
<button id="lastFmLogin" onClick={ lastFmLogin } style={{visibility: lastFmLoggedIn ? 'hidden' : 'visible' }} >last.fm</button>
|
|
||||||
<button>🔀</button>
|
<button>🔀</button>
|
||||||
<button>🔁</button>
|
<button>🔁</button>
|
||||||
<input type="range" name="volume" id="volumeSlider" onChange={ (volume) => {
|
<input type="range" name="volume" id="volumeSlider" onChange={ (volume) => {
|
||||||
|
@ -414,7 +408,7 @@ function QueueSong({ song, location, index }: QueueSongProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfig(): any {
|
function getConfig(): any {
|
||||||
invoke('get_config').then( (_config) => {
|
invoke('init_get_config').then( (_config) => {
|
||||||
let config = _config as Config;
|
let config = _config as Config;
|
||||||
if (config.libraries.libraries.length == 0) {
|
if (config.libraries.libraries.length == 0) {
|
||||||
invoke('create_new_library').then(() => {})
|
invoke('create_new_library').then(() => {})
|
||||||
|
|
71
src/config/code.tsx
Normal file
71
src/config/code.tsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { Config, ConfigConnections } from "../types";
|
||||||
|
import { TauriEvent } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<>
|
||||||
|
<App />
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
let [config, setConfig] = useState<Config>();
|
||||||
|
useEffect(() => {
|
||||||
|
invoke('get_config').then((_config) => {
|
||||||
|
let config = _config as Config;
|
||||||
|
console.log(config);
|
||||||
|
|
||||||
|
setConfig(config);
|
||||||
|
});
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const last_fm_login = () => {
|
||||||
|
invoke('last_fm_init_auth').then(() => {})
|
||||||
|
}
|
||||||
|
const save_config = () => {
|
||||||
|
invoke('save_config', { config: config }).then(() => {
|
||||||
|
// invoke('close_window').then(() => {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Config</h1>
|
||||||
|
<label>last.fm:</label>
|
||||||
|
{ config?.connections.last_fm_session ? (" already signed in") : (<button onClick={last_fm_login}>sign into last.fm</button>) }
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<ListenBrainz config={ config } setConfig={ setConfig } />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<button onClick={ save_config }>save</button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListenBrainzProps {
|
||||||
|
config: Config | undefined,
|
||||||
|
setConfig: React.Dispatch<React.SetStateAction<Config | undefined>>,
|
||||||
|
}
|
||||||
|
function ListenBrainz({ config, setConfig }: ListenBrainzProps) {
|
||||||
|
const [token, setToken] = useState("");
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
console.log("Token: " + token);
|
||||||
|
|
||||||
|
config? setConfig((prev) => ({...prev!, connections: {...config.connections, listenbrainz_token: token}})) : {}
|
||||||
|
}, [token])
|
||||||
|
|
||||||
|
const updateToken = (e: ChangeEvent<HTMLInputElement>)=> {
|
||||||
|
setToken(e.currentTarget.value);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label>{ "Listenbrainz Token" }</label>
|
||||||
|
<input type="text" value={ config?.connections.listenbrainz_token } onChange={updateToken} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Library Thing</title>
|
<title>Edit Config</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
|
@ -1,24 +0,0 @@
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { useRef } from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
||||||
<App />,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
let x = useRef('')
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h2>Insert your music folder path here</h2>
|
|
||||||
<form>
|
|
||||||
<input type="text" name="libinput" id="libinput" onChange={ (event) => x.current = event.target.value as string } />
|
|
||||||
<input type="submit" value="sumbit" onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
invoke('create_library', { path: x.current }).then(() => {})
|
|
||||||
}} />
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -20,6 +20,8 @@ export interface Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigConnections {
|
export interface ConfigConnections {
|
||||||
|
discord_rpc_client_id?: number,
|
||||||
|
last_fm_session?: string,
|
||||||
listenbrainz_token?: string
|
listenbrainz_token?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue