Added basic visual config editor

This commit is contained in:
MrDulfin 2025-03-22 04:28:01 -04:00
parent 04da9c88f3
commit 6f90aa09b9
11 changed files with 151 additions and 54 deletions

View file

@ -364,7 +364,6 @@ fn last_fm_scrobble(scrobbler: Scrobbler, now_playing_tx: Receiver<Song>, scrobb
// TODO: Add support for scrobble storage for later
let mut song: Option<Song> = None;
let mut last_song: Option<Song> = None;
LAST_FM_ACTIVE.store(true, Ordering::Relaxed);
println!("last.fm connected");

View file

@ -175,6 +175,7 @@ pub struct ControllerHandle {
pub(super) player_mail_rx: async_channel::Sender<PlayerCommandInput>,
pub(super) queue_mail_rx: async_channel::Sender<QueueCommandInput>,
pub(super) connections_rx: crossbeam_channel::Sender<ConnectionsNotification>,
pub config: Arc<RwLock<Config>>,
}
impl ControllerHandle {
@ -199,6 +200,7 @@ impl ControllerHandle {
player_mail_rx: player_mail_rx.clone(),
queue_mail_rx: queue_mail_rx.clone(),
connections_rx: connections_mail_rx.clone(),
config: config.clone(),
},
ControllerInput {
player_mail: (player_mail_rx, player_mail_tx),

View file

@ -182,7 +182,7 @@ impl ControllerHandle {
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();
}

View file

@ -10,11 +10,8 @@
"tauri": "tauri"
},
"dependencies": {
"@jprochazk/cbor": "github:jprochazk/cbor",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-shell": "^2",
"cbor": "github:jprochazk/cbor",
"cbor-x": "^1.6.0",
"node-fetch": "^3.3.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"

38
src-tauri/src/config.rs Normal file
View 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(())
}

View file

@ -8,16 +8,13 @@ use std::{
time::Duration,
};
use config::{close_window, get_config, open_config_window, save_config};
use crossbeam::channel::{bounded, unbounded, Receiver, Sender};
use dmp_core::{
config::{Config, ConfigLibrary},
music_controller::{
connections::ConnectionsInput,
controller::{Controller, ControllerHandle, PlaybackInfo},
},
music_controller::{connections::LastFMAuth, controller::{Controller, ControllerHandle, PlaybackInfo}},
music_storage::library::{MusicLibrary, Song},
};
use futures::channel::oneshot;
use parking_lot::RwLock;
use tauri::{http::Response, Emitter, Manager, State, Wry};
use uuid::Uuid;
@ -27,23 +24,28 @@ use crate::wrappers::{
get_library, get_playlist, get_playlists, get_queue, get_song, import_playlist, next, pause,
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 wrappers;
pub mod config;
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)]
pub fn run() {
let (rx, tx) = unbounded::<Config>();
let (config_rx, config_tx) = unbounded::<Config>();
let (lib_rx, lib_tx) = unbounded::<Option<PathBuf>>();
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 mut config = { tx.recv().unwrap() };
let mut config = { config_tx.recv().unwrap() };
let scan_path = { lib_tx.recv().unwrap() };
let _temp_config = ConfigLibrary::default();
let _lib = config.libraries.get_default().unwrap_or(&_temp_config);
@ -97,25 +99,35 @@ pub fn run() {
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(
library,
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()),
}),
std::sync::Arc::new(RwLock::new(config))
);
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();
playback_info_rx.send(playback_info).unwrap();
next_rx.send(next_song_notification).unwrap();
let _controller = futures::executor::block_on(Controller::start(input)).unwrap();
});
let app = tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![
get_config,
init_get_config,
create_new_library,
get_library,
play,
@ -135,8 +147,13 @@ pub fn run() {
remove_from_queue,
display_album_art,
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(HandleTx(handle_tx))
.manage(tempfile::TempDir::new().unwrap())
@ -238,7 +255,7 @@ struct LibRx(Sender<Option<PathBuf>>);
struct HandleTx(Receiver<ControllerHandle>);
#[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") {
let path = dir.config_dir();
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();
println!("got config");
Ok(config)
} else {
panic!("No config dir for DMP")

View file

@ -200,6 +200,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: Play
} }>Library</button>
{ playlists }
<button onClick={ handle_import }>Import .m3u Playlist</button>
<button onClick={() => { invoke('open_config_window').then(() => {}) }} style={{marginLeft: "auto", float: "right"}}>Edit DMP</button>
</section>
)
}
@ -312,12 +313,6 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
invoke('seek', { time: Math.round(val * 1000) }).then()
};
const lastFmLogin = () => {
invoke('last_fm_init_auth').then(() => {
setLastFmLoggedIn(true);
})
}
return (
<section id="playBar" className="playBar unselectable">
<div className="seekBar" ref={ seekBarRef } onClick={ seek } onDrag={ seek }>
@ -334,7 +329,6 @@ function PlayBar({ playing, setPlaying }: PlayBarProps) {
<button onClick={ () => invoke('next').then(() => {}) }></button>
</div>
<div className="bottomRight">
<button id="lastFmLogin" onClick={ lastFmLogin } style={{visibility: lastFmLoggedIn ? 'hidden' : 'visible' }} >last.fm</button>
<button>🔀</button>
<button>🔁</button>
<input type="range" name="volume" id="volumeSlider" onChange={ (volume) => {
@ -414,7 +408,7 @@ function QueueSong({ song, location, index }: QueueSongProps) {
}
function getConfig(): any {
invoke('get_config').then( (_config) => {
invoke('init_get_config').then( (_config) => {
let config = _config as Config;
if (config.libraries.libraries.length == 0) {
invoke('create_new_library').then(() => {})

71
src/config/code.tsx Normal file
View 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} />
</>
)
}

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Library Thing</title>
<title>Edit Config</title>
</head>
<body>
<div id="root"></div>

View file

@ -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>
</>
)
}

View file

@ -20,6 +20,8 @@ export interface Config {
}
export interface ConfigConnections {
discord_rpc_client_id?: number,
last_fm_session?: string,
listenbrainz_token?: string
}