Rework music player with bulma
This commit is contained in:
parent
aa721767fa
commit
9ffc145834
|
@ -6,81 +6,85 @@ import Visualizer from "./components/visualizer";
|
|||
import { State } from "./store";
|
||||
|
||||
type AudioState = {
|
||||
audioContext: AudioContext;
|
||||
audioSource: HTMLAudioElement;
|
||||
audioSourceNode: MediaElementAudioSourceNode;
|
||||
audioVolume: GainNode;
|
||||
audioContext: AudioContext;
|
||||
audioSource: HTMLAudioElement;
|
||||
audioSourceNode: MediaElementAudioSourceNode;
|
||||
audioVolume: GainNode;
|
||||
};
|
||||
|
||||
type MusicPlayerProps = {
|
||||
playing: boolean;
|
||||
muted: boolean;
|
||||
source?: string;
|
||||
playing: boolean;
|
||||
muted: boolean;
|
||||
source?: string;
|
||||
};
|
||||
|
||||
class MusicPlayer extends React.Component<MusicPlayerProps, State> {
|
||||
private audioState: AudioState;
|
||||
private audioState: AudioState;
|
||||
|
||||
constructor(props: MusicPlayerProps) {
|
||||
super(props);
|
||||
constructor(props: MusicPlayerProps) {
|
||||
super(props);
|
||||
|
||||
const context = new AudioContext();
|
||||
const source = new Audio();
|
||||
const sourceNode = context.createMediaElementSource(source);
|
||||
const volume = context.createGain();
|
||||
const context = new AudioContext();
|
||||
const source = new Audio();
|
||||
const sourceNode = context.createMediaElementSource(source);
|
||||
const volume = context.createGain();
|
||||
|
||||
sourceNode.connect(volume);
|
||||
volume.connect(context.destination);
|
||||
sourceNode.connect(volume);
|
||||
volume.connect(context.destination);
|
||||
|
||||
this.audioState = {
|
||||
audioContext: context,
|
||||
audioSourceNode: sourceNode,
|
||||
audioSource: source,
|
||||
audioVolume: volume,
|
||||
};
|
||||
}
|
||||
this.audioState = {
|
||||
audioContext: context,
|
||||
audioSourceNode: sourceNode,
|
||||
audioSource: source,
|
||||
audioVolume: volume,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="player" style={{ height: "100%", width: "100%" }}>
|
||||
<Visualizer
|
||||
audioContext={this.audioState.audioContext}
|
||||
audioSource={this.audioState.audioSourceNode}
|
||||
/>
|
||||
<Controls />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="is-flex-grow-1 is-flex is-flex-direction-column">
|
||||
<div className="is-flex-grow-1 is-overflow-hidden">
|
||||
<Visualizer
|
||||
audioContext={this.audioState.audioContext}
|
||||
audioSource={this.audioState.audioSourceNode}
|
||||
/>
|
||||
</div>
|
||||
<div className="is-flex-grow-0">
|
||||
<Controls />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const context = this.audioState.audioContext;
|
||||
const source = this.audioState.audioSource;
|
||||
const volume = this.audioState.audioVolume;
|
||||
componentDidUpdate() {
|
||||
const context = this.audioState.audioContext;
|
||||
const source = this.audioState.audioSource;
|
||||
const volume = this.audioState.audioVolume;
|
||||
|
||||
// First, set the audio source (if it changed)
|
||||
if (this.props.source && source.src != this.props.source) {
|
||||
source.src = this.props.source;
|
||||
}
|
||||
// First, set the audio source (if it changed)
|
||||
if (this.props.source && source.src != this.props.source) {
|
||||
source.src = this.props.source;
|
||||
}
|
||||
|
||||
if (this.props.playing) {
|
||||
source
|
||||
.play()
|
||||
.then(() => {
|
||||
console.info("Started playing audio");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Could not play audio: ${error}`);
|
||||
});
|
||||
} else {
|
||||
source.pause();
|
||||
}
|
||||
if (this.props.playing) {
|
||||
source
|
||||
.play()
|
||||
.then(() => {
|
||||
console.info("Started playing audio");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Could not play audio: ${error}`);
|
||||
});
|
||||
} else {
|
||||
source.pause();
|
||||
}
|
||||
|
||||
if (!this.props.muted) {
|
||||
volume.gain.setValueAtTime(1, context.currentTime);
|
||||
} else {
|
||||
volume.gain.setValueAtTime(0, context.currentTime);
|
||||
}
|
||||
}
|
||||
if (!this.props.muted) {
|
||||
volume.gain.setValueAtTime(1, context.currentTime);
|
||||
} else {
|
||||
volume.gain.setValueAtTime(0, context.currentTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: State): MusicPlayerProps {
|
||||
|
|
|
@ -11,29 +11,45 @@ type ControlProps = {
|
|||
|
||||
class Controls extends React.Component<ControlProps, State> {
|
||||
render() {
|
||||
return (
|
||||
<div id="playerControls" className="container-fluid fixed-bottom">
|
||||
<div className="align-items-center row p-2">
|
||||
<Indicator></Indicator>
|
||||
<div
|
||||
id="playerText"
|
||||
className="text-justify text-truncate col-6 playerControlsContent"
|
||||
>
|
||||
{this.props.title.name} - {this.props.title.album}
|
||||
</div>
|
||||
let title = (
|
||||
<div className="notification is-primary">
|
||||
<div className="level-item">{this.props.title.name}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
{this.props.title.name === "Journey" &&
|
||||
this.props.title.artist === "Mseq" ? (
|
||||
<div id="copyrightNotice" className="col text-center">
|
||||
<a href="http://dig.ccmixter.org/files/Mseq/54702">Journey</a>
|
||||
by Mseq (c) copyright 2016 Licensed under a Creative
|
||||
Commons
|
||||
<a href="http://creativecommons.org/licenses/by-nc/3.0/">
|
||||
Attribution Noncommercial (3.0)
|
||||
</a>
|
||||
license. Ft: Admiral Bob,Texas Radio Fish
|
||||
</div>
|
||||
) : null}
|
||||
if (
|
||||
this.props.title.name == "Journey" &&
|
||||
this.props.title.artist == "Mseq"
|
||||
) {
|
||||
title = (
|
||||
<div className="notification is-primary">
|
||||
<div className="level-item">
|
||||
<a href="http://dig.ccmixter.org/files/Mseq/54702">
|
||||
Journey
|
||||
</a>
|
||||
by Mseq (c) copyright 2016 Licensed under a
|
||||
Creative Commons
|
||||
<a href="http://creativecommons.org/licenses/by-nc/3.0/">
|
||||
Attribution Noncommercial (3.0)
|
||||
</a>
|
||||
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>
|
||||
);
|
||||
|
|
|
@ -22,22 +22,30 @@ class Indicator extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const classes = classNames({
|
||||
btn: true,
|
||||
"col-auto": true,
|
||||
const button_classes = classNames({
|
||||
button: true,
|
||||
"is-primary": true,
|
||||
"level-item": true,
|
||||
// TODO(tlater): Add loading logic here
|
||||
});
|
||||
|
||||
const icon_classes = classNames({
|
||||
fas: true,
|
||||
"fa-2x": true,
|
||||
"fa-muted": this.props.muted,
|
||||
"fa-play": this.props.playing,
|
||||
"fa-pause": !this.props.playing,
|
||||
"fa-play": !this.props.playing,
|
||||
"fa-pause": this.props.playing,
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
id="playerIndicator"
|
||||
onClick={this.click.bind(this)}
|
||||
className={classes}
|
||||
></button>
|
||||
className={button_classes}>
|
||||
<span className="icon is-medium">
|
||||
<i className={icon_classes}></i>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ class CanvasDrawer {
|
|||
});
|
||||
|
||||
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
|
||||
window.addEventListener("resize", this.resize.bind(this));
|
||||
|
@ -147,28 +147,31 @@ class CanvasDrawer {
|
|||
|
||||
resize() {
|
||||
const canvas = this.canvas;
|
||||
|
||||
if (canvas.parentElement === null) {
|
||||
throw Error("Could not access canvas parent for size calculation");
|
||||
}
|
||||
|
||||
// Compute the height of all our siblings
|
||||
let combinedHeight = 0;
|
||||
for (let i = 0; i < canvas.parentElement.children.length; i++) {
|
||||
const child = canvas.parentElement.children[i];
|
||||
// This is stupid, but by setting the canvas proportions to 0
|
||||
// for a split second the browser can actually figure out the
|
||||
// height of the parentElement.
|
||||
//
|
||||
// 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) {
|
||||
combinedHeight += child.clientHeight;
|
||||
}
|
||||
}
|
||||
let height = canvas.parentElement.clientHeight;
|
||||
let width = canvas.parentElement.clientWidth;
|
||||
|
||||
// The remaining space we want to fill
|
||||
const remainingHeight = canvas.parentElement.clientHeight - combinedHeight;
|
||||
canvas.height = remainingHeight;
|
||||
canvas.width = canvas.parentElement.clientWidth;
|
||||
|
||||
this.camera.aspect = canvas.width / remainingHeight;
|
||||
this.camera.aspect = width / height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(canvas.width, remainingHeight);
|
||||
this.renderer.setSize(width, height, false);
|
||||
}
|
||||
|
||||
stop() {
|
||||
|
@ -179,9 +182,9 @@ class CanvasDrawer {
|
|||
}
|
||||
|
||||
class Visualizer extends React.Component<VisualizerProps, State> {
|
||||
private analyser: AnalyserNode;
|
||||
private analyser?: AnalyserNode;
|
||||
private canvas: React.RefObject<HTMLCanvasElement>;
|
||||
private drawer: CanvasDrawer;
|
||||
private drawer?: CanvasDrawer;
|
||||
|
||||
constructor(props: VisualizerProps) {
|
||||
super(props);
|
||||
|
@ -191,10 +194,10 @@ class Visualizer extends React.Component<VisualizerProps, State> {
|
|||
render(): React.ReactNode {
|
||||
return (
|
||||
<canvas
|
||||
id="visualizer"
|
||||
ref={this.canvas}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
></canvas>
|
||||
style={{
|
||||
display: "block",
|
||||
}}></canvas>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -207,11 +210,14 @@ class Visualizer extends React.Component<VisualizerProps, State> {
|
|||
this.analyser.fftSize = 2048;
|
||||
this.analyser.smoothingTimeConstant = 0.8;
|
||||
this.props.audioSource.connect(this.analyser);
|
||||
|
||||
this.drawer = new CanvasDrawer(this.analyser, this.canvas.current);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (!this.drawer || !this.analyser) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.drawer.stop();
|
||||
this.props.audioSource.disconnect(this.analyser);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
import { store } from "./store";
|
||||
|
@ -10,11 +9,11 @@ import mseq from "./Mseq_-_Journey.mp3";
|
|||
|
||||
const rootElement = document.getElementById("playerUI");
|
||||
|
||||
ReactDOM.render(
|
||||
const root = createRoot(rootElement!);
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<MusicPlayer />
|
||||
</Provider>,
|
||||
rootElement
|
||||
</Provider>
|
||||
);
|
||||
|
||||
store.dispatch(setSource(mseq));
|
||||
|
|
|
@ -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/solid";
|
||||
|
||||
#playerControls {
|
||||
background-color: #11151c;
|
||||
.is-overflow-hidden {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<link rel="stylesheet" , href="music/music.scss" />
|
||||
</block>
|
||||
|
||||
<block name="footer">
|
||||
<div id="playerUI" class="container-fluid flex-grow-1"></div>
|
||||
<block name="content">
|
||||
<div id="playerUI" class="is-flex-grow-1 is-flex"></div>
|
||||
<script type="module" src="./music/index.tsx"></script>
|
||||
</block>
|
||||
</extends>
|
||||
|
|
Reference in a new issue