Rework music player with bulma

pull/6/head
Tristan Daniël Maat 2022-07-30 22:17:32 +01:00
parent aa721767fa
commit 9ffc145834
Signed by: tlater
GPG Key ID: 49670FD774E43268
7 changed files with 154 additions and 121 deletions

View File

@ -6,81 +6,85 @@ import Visualizer from "./components/visualizer";
import { State } from "./store"; import { State } from "./store";
type AudioState = { type AudioState = {
audioContext: AudioContext; audioContext: AudioContext;
audioSource: HTMLAudioElement; audioSource: HTMLAudioElement;
audioSourceNode: MediaElementAudioSourceNode; audioSourceNode: MediaElementAudioSourceNode;
audioVolume: GainNode; audioVolume: GainNode;
}; };
type MusicPlayerProps = { type MusicPlayerProps = {
playing: boolean; playing: boolean;
muted: boolean; muted: boolean;
source?: string; source?: string;
}; };
class MusicPlayer extends React.Component<MusicPlayerProps, State> { class MusicPlayer extends React.Component<MusicPlayerProps, State> {
private audioState: AudioState; private audioState: AudioState;
constructor(props: MusicPlayerProps) { constructor(props: MusicPlayerProps) {
super(props); super(props);
const context = new AudioContext(); const context = new AudioContext();
const source = new Audio(); const source = new Audio();
const sourceNode = context.createMediaElementSource(source); const sourceNode = context.createMediaElementSource(source);
const volume = context.createGain(); const volume = context.createGain();
sourceNode.connect(volume); sourceNode.connect(volume);
volume.connect(context.destination); volume.connect(context.destination);
this.audioState = { this.audioState = {
audioContext: context, audioContext: context,
audioSourceNode: sourceNode, audioSourceNode: sourceNode,
audioSource: source, audioSource: source,
audioVolume: volume, audioVolume: volume,
}; };
} }
render() { render() {
return ( return (
<div id="player" style={{ height: "100%", width: "100%" }}> <div className="is-flex-grow-1 is-flex is-flex-direction-column">
<Visualizer <div className="is-flex-grow-1 is-overflow-hidden">
audioContext={this.audioState.audioContext} <Visualizer
audioSource={this.audioState.audioSourceNode} audioContext={this.audioState.audioContext}
/> audioSource={this.audioState.audioSourceNode}
<Controls /> />
</div> </div>
); <div className="is-flex-grow-0">
} <Controls />
</div>
</div>
);
}
componentDidUpdate() { componentDidUpdate() {
const context = this.audioState.audioContext; const context = this.audioState.audioContext;
const source = this.audioState.audioSource; const source = this.audioState.audioSource;
const volume = this.audioState.audioVolume; const volume = this.audioState.audioVolume;
// First, set the audio source (if it changed) // First, set the audio source (if it changed)
if (this.props.source && source.src != this.props.source) { if (this.props.source && source.src != this.props.source) {
source.src = this.props.source; source.src = this.props.source;
} }
if (this.props.playing) { if (this.props.playing) {
source source
.play() .play()
.then(() => { .then(() => {
console.info("Started playing audio"); console.info("Started playing audio");
}) })
.catch((error) => { .catch((error) => {
console.error(`Could not play audio: ${error}`); console.error(`Could not play audio: ${error}`);
}); });
} else { } else {
source.pause(); source.pause();
} }
if (!this.props.muted) { if (!this.props.muted) {
volume.gain.setValueAtTime(1, context.currentTime); volume.gain.setValueAtTime(1, context.currentTime);
} else { } else {
volume.gain.setValueAtTime(0, context.currentTime); volume.gain.setValueAtTime(0, context.currentTime);
} }
} }
} }
function mapStateToProps(state: State): MusicPlayerProps { function mapStateToProps(state: State): MusicPlayerProps {

View File

@ -11,29 +11,45 @@ type ControlProps = {
class Controls extends React.Component<ControlProps, State> { class Controls extends React.Component<ControlProps, State> {
render() { render() {
return ( let title = (
<div id="playerControls" className="container-fluid fixed-bottom"> <div className="notification is-primary">
<div className="align-items-center row p-2"> <div className="level-item">{this.props.title.name}</div>
<Indicator></Indicator> </div>
<div );
id="playerText"
className="text-justify text-truncate col-6 playerControlsContent"
>
{this.props.title.name} - {this.props.title.album}
</div>
{this.props.title.name === "Journey" && if (
this.props.title.artist === "Mseq" ? ( this.props.title.name == "Journey" &&
<div id="copyrightNotice" className="col text-center"> this.props.title.artist == "Mseq"
<a href="http://dig.ccmixter.org/files/Mseq/54702">Journey</a> ) {
&nbsp;by Mseq (c) copyright 2016 Licensed under a Creative title = (
Commons&nbsp; <div className="notification is-primary">
<a href="http://creativecommons.org/licenses/by-nc/3.0/"> <div className="level-item">
Attribution Noncommercial (3.0) <a href="http://dig.ccmixter.org/files/Mseq/54702">
</a> Journey
&nbsp; license. Ft: Admiral Bob,Texas Radio Fish </a>
</div> &nbsp;by Mseq (c) copyright 2016 Licensed under a
) : null} Creative Commons&nbsp;
<a href="http://creativecommons.org/licenses/by-nc/3.0/">
Attribution Noncommercial (3.0)
</a>
&nbsp; license. Ft: Admiral Bob,Texas Radio Fish
</div>
</div>
);
}
return (
<div className="notification is-primary">
<div className="level">
<div className="level-left">
<Indicator></Indicator>
{title}
</div>
<div className="level-right">
<div className="level-item">
{this.props.title.artist}
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@ -22,22 +22,30 @@ class Indicator extends React.Component<Props, State> {
} }
render() { render() {
const classes = classNames({ const button_classes = classNames({
btn: true, button: true,
"col-auto": true, "is-primary": true,
"level-item": true,
// TODO(tlater): Add loading logic here
});
const icon_classes = classNames({
fas: true, fas: true,
"fa-2x": true,
"fa-muted": this.props.muted, "fa-muted": this.props.muted,
"fa-play": this.props.playing, "fa-play": !this.props.playing,
"fa-pause": !this.props.playing, "fa-pause": this.props.playing,
}); });
return ( return (
<button <button
type="button" type="button"
id="playerIndicator"
onClick={this.click.bind(this)} onClick={this.click.bind(this)}
className={classes} className={button_classes}>
></button> <span className="icon is-medium">
<i className={icon_classes}></i>
</span>
</button>
); );
} }
} }

View File

@ -80,7 +80,7 @@ class CanvasDrawer {
}); });
this.renderer.setClearColor(new three.Color(0x0f0f0f)); this.renderer.setClearColor(new three.Color(0x0f0f0f));
this.renderer.setSize(canvas.width, canvas.height); this.renderer.setSize(canvas.width, canvas.height, false);
// Set up canvas resizing // Set up canvas resizing
window.addEventListener("resize", this.resize.bind(this)); window.addEventListener("resize", this.resize.bind(this));
@ -147,28 +147,31 @@ class CanvasDrawer {
resize() { resize() {
const canvas = this.canvas; const canvas = this.canvas;
if (canvas.parentElement === null) { if (canvas.parentElement === null) {
throw Error("Could not access canvas parent for size calculation"); throw Error("Could not access canvas parent for size calculation");
} }
// Compute the height of all our siblings // This is stupid, but by setting the canvas proportions to 0
let combinedHeight = 0; // for a split second the browser can actually figure out the
for (let i = 0; i < canvas.parentElement.children.length; i++) { // height of the parentElement.
const child = canvas.parentElement.children[i]; //
// If the canvas is allowed to keep its height, it will
// prevent the parentElement from being able to shrink because
// its contents are filling it completely.
//
// I've seen others use `overflow: hidden` to achieve this
// (and in fact seen it render in my browser on jsfiddle), but
// that doesn't work here somehow.
canvas.height = 0;
canvas.width = 0;
if (child != canvas) { let height = canvas.parentElement.clientHeight;
combinedHeight += child.clientHeight; let width = canvas.parentElement.clientWidth;
}
}
// The remaining space we want to fill this.camera.aspect = width / height;
const remainingHeight = canvas.parentElement.clientHeight - combinedHeight;
canvas.height = remainingHeight;
canvas.width = canvas.parentElement.clientWidth;
this.camera.aspect = canvas.width / remainingHeight;
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix();
this.renderer.setSize(canvas.width, remainingHeight); this.renderer.setSize(width, height, false);
} }
stop() { stop() {
@ -179,9 +182,9 @@ class CanvasDrawer {
} }
class Visualizer extends React.Component<VisualizerProps, State> { class Visualizer extends React.Component<VisualizerProps, State> {
private analyser: AnalyserNode; private analyser?: AnalyserNode;
private canvas: React.RefObject<HTMLCanvasElement>; private canvas: React.RefObject<HTMLCanvasElement>;
private drawer: CanvasDrawer; private drawer?: CanvasDrawer;
constructor(props: VisualizerProps) { constructor(props: VisualizerProps) {
super(props); super(props);
@ -191,10 +194,10 @@ class Visualizer extends React.Component<VisualizerProps, State> {
render(): React.ReactNode { render(): React.ReactNode {
return ( return (
<canvas <canvas
id="visualizer"
ref={this.canvas} ref={this.canvas}
style={{ width: "100%", height: "100%" }} style={{
></canvas> display: "block",
}}></canvas>
); );
} }
@ -207,11 +210,14 @@ class Visualizer extends React.Component<VisualizerProps, State> {
this.analyser.fftSize = 2048; this.analyser.fftSize = 2048;
this.analyser.smoothingTimeConstant = 0.8; this.analyser.smoothingTimeConstant = 0.8;
this.props.audioSource.connect(this.analyser); this.props.audioSource.connect(this.analyser);
this.drawer = new CanvasDrawer(this.analyser, this.canvas.current); this.drawer = new CanvasDrawer(this.analyser, this.canvas.current);
} }
componentWillUnmount(): void { componentWillUnmount(): void {
if (!this.drawer || !this.analyser) {
return;
}
this.drawer.stop(); this.drawer.stop();
this.props.audioSource.disconnect(this.analyser); this.props.audioSource.disconnect(this.analyser);
} }

View File

@ -1,5 +1,4 @@
import React from "react"; import { createRoot } from "react-dom/client";
import ReactDOM from "react-dom";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { store } from "./store"; import { store } from "./store";
@ -10,11 +9,11 @@ import mseq from "./Mseq_-_Journey.mp3";
const rootElement = document.getElementById("playerUI"); const rootElement = document.getElementById("playerUI");
ReactDOM.render( const root = createRoot(rootElement!);
root.render(
<Provider store={store}> <Provider store={store}>
<MusicPlayer /> <MusicPlayer />
</Provider>, </Provider>
rootElement
); );
store.dispatch(setSource(mseq)); store.dispatch(setSource(mseq));

View File

@ -3,6 +3,6 @@ $fa-font-path: "npm:@fortawesome/fontawesome-free/webfonts";
@import "~/node_modules/@fortawesome/fontawesome-free/scss/fontawesome"; @import "~/node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
@import "~/node_modules/@fortawesome/fontawesome-free/scss/solid"; @import "~/node_modules/@fortawesome/fontawesome-free/scss/solid";
#playerControls { .is-overflow-hidden {
background-color: #11151c; overflow: hidden !important;
} }

View File

@ -3,8 +3,8 @@
<link rel="stylesheet" , href="music/music.scss" /> <link rel="stylesheet" , href="music/music.scss" />
</block> </block>
<block name="footer"> <block name="content">
<div id="playerUI" class="container-fluid flex-grow-1"></div> <div id="playerUI" class="is-flex-grow-1 is-flex"></div>
<script type="module" src="./music/index.tsx"></script> <script type="module" src="./music/index.tsx"></script>
</block> </block>
</extends> </extends>