Implement basic volume scrolling interaction

This commit is contained in:
finsofblueimnotyou 2025-02-12 19:58:15 -05:00 committed by GitHub
parent a2637f4329
commit 23e411e78c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,417 +1,427 @@
import React, { createRef, useEffect, useRef, 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, playbackInfo } 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";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
const appWindow = getCurrentWebviewWindow(); const appWindow = getCurrentWebviewWindow();
function App() { function App() {
const library = useState<JSX.Element[]>([]); const library = useState<JSX.Element[]>([]);
const [queue, setQueue] = useState<JSX.Element[]>([]); const [queue, setQueue] = useState<JSX.Element[]>([]);
const [playing, setPlaying] = useState(false); const [playing, setPlaying] = useState(false);
const [playlists, setPlaylists] = useState<JSX.Element[]>([]); const [playlists, setPlaylists] = useState<JSX.Element[]>([]);
const [viewName, setViewName] = useState("Library"); const [viewName, setViewName] = useState("Library");
const [nowPlaying, setNowPlaying] = useState<JSX.Element>( const [nowPlaying, setNowPlaying] = useState<JSX.Element>(
<NowPlaying <NowPlaying
title="Title" title="Title"
album="Album" album="Album"
artist="Artist" artist="Artist"
artwork={<img src={convertFileSrc("abc") + "?" + "default" } id="nowPlayingArtwork" alt="Now Playing Artwork" key={'default_image'} />} artwork={<img src={convertFileSrc("abc") + "?" + "default" } id="nowPlayingArtwork" alt="Now Playing Artwork" key={'default_image'} />}
/> />
); );
useEffect(() => { useEffect(() => {
const unlisten = appWindow.listen<any>("now_playing_change", ({ payload, }) => { const unlisten = appWindow.listen<any>("now_playing_change", ({ payload, }) => {
const displayArtwork = () => { const displayArtwork = () => {
invoke('display_album_art', { uuid: payload.uuid }).then(() => {}) invoke('display_album_art', { uuid: payload.uuid }).then(() => {})
} }
// console.log(event); // console.log(event);
setNowPlaying( setNowPlaying(
<NowPlaying <NowPlaying
title={ payload.tags.TrackTitle } title={ payload.tags.TrackTitle }
album={ payload.tags.AlbumTitle } album={ payload.tags.AlbumTitle }
artist={ payload.tags.TrackArtist } artist={ payload.tags.TrackArtist }
artwork={ <img src={convertFileSrc("abc") + "?" + payload.uuid } id="nowPlayingArtwork" alt="Now Playing Artwork" key={payload.uuid} onDoubleClick={ displayArtwork } /> } artwork={ <img src={convertFileSrc("abc") + "?" + payload.uuid } id="nowPlayingArtwork" alt="Now Playing Artwork" key={payload.uuid} onDoubleClick={ displayArtwork } /> }
/> />
) )
}) })
return () => { unlisten.then((f) => f()) } return () => { unlisten.then((f) => f()) }
}, []); }, []);
useEffect(() => { useEffect(() => {
const unlisten = appWindow.listen<any>("queue_updated", (_) => { const unlisten = appWindow.listen<any>("queue_updated", (_) => {
// console.log(event); // console.log(event);
invoke('get_queue').then((_songs) => { invoke('get_queue').then((_songs) => {
let songs = _songs as any[] let songs = _songs as any[]
setQueue( setQueue(
songs.filter((_, i) => i != 0).map((song, i) => songs.filter((_, i) => i != 0).map((song, i) =>
<QueueSong <QueueSong
song={ song[0] } song={ song[0] }
location={ song[1] as "Library" | {"Playlist" : string}} location={ song[1] as "Library" | {"Playlist" : string}}
index={i+1} index={i+1}
key={ song.uuid + '_' + Math.floor((Math.random() * 100_000) + 1) + '_' + Date.now() } key={ song.uuid + '_' + Math.floor((Math.random() * 100_000) + 1) + '_' + Date.now() }
/> />
) )
) )
}) })
}) })
return () => { unlisten.then((f) => f()) } return () => { unlisten.then((f) => f()) }
}, []); }, []);
useEffect(() => { useEffect(() => {
const unlisten = appWindow.listen<any>("playing", (_) => { const unlisten = appWindow.listen<any>("playing", (_) => {
setPlaying(true) setPlaying(true)
}) })
return () => { unlisten.then((f) => f()) } return () => { unlisten.then((f) => f()) }
}, []); }, []);
useEffect(() => { useEffect(() => {
const unlisten = appWindow.listen<any>("paused", (_) => { const unlisten = appWindow.listen<any>("paused", (_) => {
setPlaying(false) setPlaying(false)
}) })
return () => { unlisten.then((f) => f()) } return () => { unlisten.then((f) => f()) }
}, []); }, []);
useEffect(() => { useEffect(() => {
getConfig(); getConfig();
}, []) }, [])
return ( return (
<main> <main>
<div className="container"> <div className="container">
<div className="leftSide"> <div className="leftSide">
<PlaylistHead playlists={ playlists } setPlaylists={ setPlaylists } setViewName={ setViewName } setLibrary={ library[1] } /> <PlaylistHead playlists={ playlists } setPlaylists={ setPlaylists } setViewName={ setViewName } setLibrary={ library[1] } />
<MainView lib_ref={ library } viewName={ viewName } /> <MainView lib_ref={ library } viewName={ viewName } />
</div> </div>
<div className="rightSide"> <div className="rightSide">
{ nowPlaying } { nowPlaying }
<Queue songs={ queue } /> <Queue songs={ queue } />
</div> </div>
</div> </div>
<div className="bottom"> <div className="bottom">
<PlayBar playing={ playing } setPlaying={ setPlaying } /> <PlayBar playing={ playing } setPlaying={ setPlaying } />
</div> </div>
</main> </main>
); );
} }
export default App; export default App;
interface PlaylistHeadProps { interface PlaylistHeadProps {
playlists: JSX.Element[] playlists: JSX.Element[]
setPlaylists: React.Dispatch<React.SetStateAction<JSX.Element[]>>, setPlaylists: React.Dispatch<React.SetStateAction<JSX.Element[]>>,
setViewName: React.Dispatch<React.SetStateAction<string>>, setViewName: React.Dispatch<React.SetStateAction<string>>,
setLibrary: React.Dispatch<React.SetStateAction<JSX.Element[]>>, setLibrary: React.Dispatch<React.SetStateAction<JSX.Element[]>>,
} }
function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: PlaylistHeadProps) { function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: PlaylistHeadProps) {
useEffect(() => { useEffect(() => {
const unlisten = appWindow.listen<any[]>("playlists_gotten", (_res) => { const unlisten = appWindow.listen<any[]>("playlists_gotten", (_res) => {
// console.log(event); // console.log(event);
let res = _res.payload; let res = _res.payload;
setPlaylists([ setPlaylists([
...res.map( (item) => { ...res.map( (item) => {
return ( return (
<button onClick={ () => { <button onClick={ () => {
invoke('get_playlist', { uuid: item.uuid }).then((list) => { invoke('get_playlist', { uuid: item.uuid }).then((list) => {
setLibrary([...(list as any[]).map((song) => { setLibrary([...(list as any[]).map((song) => {
// console.log(song); // console.log(song);
return ( return (
<Song <Song
key={ song.uuid } key={ song.uuid }
location={ song.location } location={ song.location }
playerLocation={ {"Playlist" : item.uuid } } playerLocation={ {"Playlist" : item.uuid } }
uuid={ song.uuid } uuid={ song.uuid }
plays={ song.plays } plays={ song.plays }
duration={ song.duration } duration={ song.duration }
tags={ song.tags } tags={ song.tags }
/> />
) )
})]) })])
}) })
setViewName( item.name ) setViewName( item.name )
} } key={ 'playlist_' + item.uuid }>{ item.name }</button> } } key={ 'playlist_' + item.uuid }>{ item.name }</button>
) )
}) })
]) ])
}) })
return () => { unlisten.then((f) => f()) } return () => { unlisten.then((f) => f()) }
}, []); }, []);
let handle_import = () => { let handle_import = () => {
invoke('import_playlist').then((_res) => { invoke('import_playlist').then((_res) => {
let res = _res as any; let res = _res as any;
setPlaylists([ setPlaylists([
...playlists, ...playlists,
<button onClick={ () => { <button onClick={ () => {
invoke('get_playlist', { uuid: res.uuid }).then((list) => { invoke('get_playlist', { uuid: res.uuid }).then((list) => {
console.log((list as any[]).length); console.log((list as any[]).length);
setLibrary([...(list as any[]).map((song) => { setLibrary([...(list as any[]).map((song) => {
// console.log(song); // console.log(song);
return ( return (
<Song <Song
key={ song.uuid } key={ song.uuid }
location={ song.location } location={ song.location }
playerLocation={ {"Playlist" : res.uuid } } playerLocation={ {"Playlist" : res.uuid } }
uuid={ song.uuid } uuid={ song.uuid }
plays={ song.plays } plays={ song.plays }
duration={ song.duration } duration={ song.duration }
tags={ song.tags } tags={ song.tags }
/> />
) )
})]) })])
}) })
setViewName( res.name ) setViewName( res.name )
} } key={ 'playlist_' + res.uuid }>{ res.name }</button> } } key={ 'playlist_' + res.uuid }>{ res.name }</button>
]) ])
console.log(res.name); console.log(res.name);
}) })
} }
return ( return (
<section className="playlistHead"> <section className="playlistHead">
<button onClick={() => { <button onClick={() => {
setViewName("Library"); setViewName("Library");
invoke('get_library').then((lib) => { invoke('get_library').then((lib) => {
setLibrary([...(lib as any[]).map((song) => { setLibrary([...(lib as any[]).map((song) => {
console.log(song); console.log(song);
return ( return (
<Song <Song
key={ song.uuid } key={ song.uuid }
location={ song.location } location={ song.location }
playerLocation="Library" playerLocation="Library"
uuid={ song.uuid } uuid={ song.uuid }
plays={ song.plays } plays={ song.plays }
duration={ song.duration } duration={ song.duration }
tags={ song.tags } tags={ song.tags }
/> />
) )
})]) })])
}) })
} }>Library</button> } }>Library</button>
{ playlists } { playlists }
<button onClick={ handle_import }>Import .m3u Playlist</button> <button onClick={ handle_import }>Import .m3u Playlist</button>
</section> </section>
) )
} }
interface MainViewProps { interface MainViewProps {
lib_ref: [JSX.Element[], React.Dispatch<React.SetStateAction<JSX.Element[]>>], lib_ref: [JSX.Element[], React.Dispatch<React.SetStateAction<JSX.Element[]>>],
viewName: string viewName: string
} }
function MainView({ lib_ref, viewName }: MainViewProps) { function MainView({ lib_ref, viewName }: MainViewProps) {
const [library, setLibrary] = lib_ref; const [library, setLibrary] = lib_ref;
useEffect(() => { useEffect(() => {
const unlisten = appWindow.listen<any>("library_loaded", (_) => { const unlisten = appWindow.listen<any>("library_loaded", (_) => {
console.log("library_loaded"); console.log("library_loaded");
invoke('get_library').then((lib) => { invoke('get_library').then((lib) => {
setLibrary([...(lib as any[]).map((song) => { setLibrary([...(lib as any[]).map((song) => {
return ( return (
<Song <Song
key={ song.uuid } key={ song.uuid }
location={ song.location } location={ song.location }
playerLocation="Library" playerLocation="Library"
uuid={ song.uuid } uuid={ song.uuid }
plays={ song.plays } plays={ song.plays }
duration={ song.duration } duration={ song.duration }
tags={ song.tags } tags={ song.tags }
/> />
) )
})]) })])
}) })
invoke('get_playlists').then(() => {}) invoke('get_playlists').then(() => {})
}) })
return () => { unlisten.then((f) => f()) } return () => { unlisten.then((f) => f()) }
}, []); }, []);
return ( return (
<div className="mainView"> <div className="mainView">
<h1>{ viewName }</h1> <h1>{ viewName }</h1>
<div>{ library }</div> <div>{ library }</div>
</div> </div>
) )
} }
interface SongProps { interface SongProps {
location: any, location: any,
playerLocation: string | {"Playlist" : any}, playerLocation: string | {"Playlist" : any},
uuid: string, uuid: string,
plays: number, plays: number,
format?: string, format?: string,
duration: string, duration: string,
last_played?: string, last_played?: string,
date_added?: string, date_added?: string,
date_modified?: string, date_modified?: string,
tags: any tags: any
} }
function Song(props: SongProps) { function Song(props: SongProps) {
// console.log(props.tags); // console.log(props.tags);
return( return(
<div onDoubleClick={() => { <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 unselectable">{ props.tags.TrackArtist }</p> <p className="artist unselectable">{ props.tags.TrackArtist }</p>
<p className="title unselectable">{ props.tags.TrackTitle }</p> <p className="title unselectable">{ props.tags.TrackTitle }</p>
<p className="album unselectable">{ props.tags.AlbumTitle }</p> <p className="album unselectable">{ props.tags.AlbumTitle }</p>
<p className="duration unselectable"> <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>
</div> </div>
) )
} }
interface PlayBarProps { interface PlayBarProps {
playing: boolean, playing: boolean,
setPlaying: React.Dispatch<React.SetStateAction<boolean>> setPlaying: React.Dispatch<React.SetStateAction<boolean>>
} }
function PlayBar({ playing, setPlaying }: PlayBarProps) { function PlayBar({ playing, setPlaying }: PlayBarProps) {
const [position, setPosition] = useState(0); const [position, setPosition] = useState(0);
const [duration, setDuration] = useState(0); const [duration, setDuration] = useState(0);
const [seekBarSize, setSeekBarSize] = useState(0); const [seekBarSize, setSeekBarSize] = useState(0);
const seekBarRef = React.createRef<HTMLDivElement>(); const seekBarRef = React.createRef<HTMLDivElement>();
useEffect(() => { useEffect(() => {
const unlisten = appWindow.listen<any>("playback_info", ({ payload, }) => { const unlisten = appWindow.listen<any>("playback_info", ({ payload, }) => {
const info = payload as playbackInfo; const info = payload as playbackInfo;
const pos_ = Array.isArray(info.position) ? info.position![0] : 0; const pos_ = Array.isArray(info.position) ? info.position![0] : 0;
const dur_ = Array.isArray(info.duration) ? info.duration![0] : 0; const dur_ = Array.isArray(info.duration) ? info.duration![0] : 0;
setPosition(pos_); setPosition(pos_);
setDuration(dur_); setDuration(dur_);
let progress = ((dur_/pos_) * 100); let progress = ((dur_/pos_) * 100);
setSeekBarSize(progress) setSeekBarSize(progress)
}) })
return () => { unlisten.then((f) => f()) } return () => { unlisten.then((f) => f()) }
}, []); }, []);
const seek = (event: React.MouseEvent<HTMLDivElement>) => { const seek = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation(); event.stopPropagation();
let rect = seekBarRef.current!.getBoundingClientRect(); let rect = seekBarRef.current!.getBoundingClientRect();
let val = ((event.clientX-rect.left) / (rect.width))*position; let val = ((event.clientX-rect.left) / (rect.width))*position;
invoke('seek', { time: Math.round(val * 1000) }).then() invoke('seek', { time: Math.round(val * 1000) }).then()
}; };
return ( const wheelVolume = (event: React.WheelEvent<HTMLDivElement>) => {
<section id="playBar" className="playBar unselectable"> let x = volumeSlider.value;
<div className="seekBar" ref={ seekBarRef } onClick={ seek } onDrag={ seek }> if (event.deltaY < 0) {
<div className="seekOverlay" id="seekOverlay" style={{ width: seekBarSize + '%' } }></div> volumeSlider.value++;
</div> } else {
<div className="bottomSpaced"> volumeSlider.value--;
<div className="bottomLeft"> }
<button onClick={ (_) => { invoke('set_volume', { volume: volumeSlider.value }).then(() => {})
setPlaying( playing ? false : true ); };
invoke( playing ? 'pause' : 'play' ).then(() => {})
}}>{ playing ? '⏸' : '⏵' }</button> return (
<button onClick={ () => invoke('stop').then(() => {}) }></button> <section id="playBar" className="playBar unselectable">
<button onClick={ () => invoke('prev').then(() => {}) }></button> <div className="seekBar" ref={ seekBarRef } onClick={ seek } onDrag={ seek }>
<button onClick={ () => invoke('next').then(() => {}) }></button> <div className="seekOverlay" id="seekOverlay" style={{ width: seekBarSize + '%' } }></div>
</div> </div>
<div className="bottomRight"> <div className="bottomSpaced">
<button>🔀</button> <div className="bottomLeft">
<button>🔁</button> <button onClick={ (_) => {
<input type="range" name="volume" id="volumeSlider" onChange={ (volume) => { setPlaying( playing ? false : true );
invoke('set_volume', { volume: volume.target.value }).then(() => {}) invoke( playing ? 'pause' : 'play' ).then(() => {})
}} /> }}>{ playing ? '⏸' : '⏵' }</button>
<p id="timeDisplay"> <button onClick={ () => invoke('stop').then(() => {}) }></button>
{ Math.floor(+duration / 60).toString().padStart(2, "0") }: <button onClick={ () => invoke('prev').then(() => {}) }></button>
{ (+duration % 60).toString().padStart(2, "0") }/ <button onClick={ () => invoke('next').then(() => {}) }></button>
{ Math.floor(+position / 60).toString().padStart(2, "0") }: </div>
{ (+position % 60).toString().padStart(2, "0") } <div className="bottomRight">
<button>🔀</button>
</p> <button>🔁</button>
</div> <input onWheel={wheelVolume} type="range" name="volume" id="volumeSlider" onChange={ (volume) => {
</div> invoke('set_volume', { volume: volume.target.value }).then(() => {})
</section> }} />
) <p id="timeDisplay">
} { Math.floor(+duration / 60).toString().padStart(2, "0") }:
{ (+duration % 60).toString().padStart(2, "0") }/
interface NowPlayingProps { { Math.floor(+position / 60).toString().padStart(2, "0") }:
title: string, { (+position % 60).toString().padStart(2, "0") }
artist: string,
album: string, </p>
artwork: JSX.Element </div>
} </div>
</section>
function NowPlaying({ title, artist, album, artwork }: NowPlayingProps) { )
return ( }
<section className="nowPlaying">
<div className="artworkWrapper unselectable"> interface NowPlayingProps {
{ artwork } title: string,
</div> artist: string,
<h3>{ title }</h3> album: string,
<p>{ artist }</p> artwork: JSX.Element
<p>{ album }</p> }
</section>
) function NowPlaying({ title, artist, album, artwork }: NowPlayingProps) {
} return (
<section className="nowPlaying">
interface QueueProps { <div className="artworkWrapper unselectable">
songs: JSX.Element[], { artwork }
} </div>
<h3>{ title }</h3>
function Queue({ songs }: QueueProps) { <p>{ artist }</p>
return ( <p>{ album }</p>
<section className="Queue"> </section>
{ songs } )
</section> }
)
} interface QueueProps {
songs: JSX.Element[],
interface QueueSongProps { }
song: any,
location: "Library" | {"Playlist": string}, function Queue({ songs }: QueueProps) {
index: number, return (
} <section className="Queue">
{ songs }
function QueueSong({ song, location, index }: QueueSongProps) { </section>
// console.log(song.tags); )
}
let removeFromQueue = () => {
invoke('remove_from_queue', { index: index }).then(() => {}) interface QueueSongProps {
} song: any,
location: "Library" | {"Playlist": string},
let playNow = () => { index: number,
invoke('play_now', { uuid: song.uuid, location: location }).then(() => {}) }
}
function QueueSong({ song, location, index }: QueueSongProps) {
return ( // console.log(song.tags);
<div className="queueSong unselectable" onAuxClickCapture={ removeFromQueue } onDoubleClickCapture={ playNow }>
<img className="queueSongCoverArt" src={ convertFileSrc('abc') + '?' + song.uuid } key={ 'coverArt_' + song.uuid }/> let removeFromQueue = () => {
<div className="queueSongTags"> invoke('remove_from_queue', { index: index }).then(() => {})
<p className="queueSongTitle">{ song.tags.TrackTitle }</p> }
<p className="queueSongArtist">{ song.tags.TrackArtist }</p>
</div> let playNow = () => {
</div> invoke('play_now', { uuid: song.uuid, location: location }).then(() => {})
) }
}
return (
function getConfig(): any { <div className="queueSong unselectable" onAuxClickCapture={ removeFromQueue } onDoubleClickCapture={ playNow }>
invoke('get_config').then( (_config) => { <img className="queueSongCoverArt" src={ convertFileSrc('abc') + '?' + song.uuid } key={ 'coverArt_' + song.uuid }/>
let config = _config as Config; <div className="queueSongTags">
if (config.libraries.libraries.length == 0) { <p className="queueSongTitle">{ song.tags.TrackTitle }</p>
invoke('create_new_library').then(() => {}) <p className="queueSongArtist">{ song.tags.TrackArtist }</p>
} else { </div>
// console.log("else"); </div>
invoke('lib_already_created').then(() => {}) )
} }
})
} function getConfig(): any {
invoke('get_config').then( (_config) => {
let config = _config as Config;
if (config.libraries.libraries.length == 0) {
invoke('create_new_library').then(() => {})
} else {
// console.log("else");
invoke('lib_already_created').then(() => {})
}
})
}