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";
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 {

View File

@ -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>
&nbsp;by Mseq (c) copyright 2016 Licensed under a 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>
) : 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>
&nbsp;by Mseq (c) copyright 2016 Licensed under a
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>
);

View File

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

View File

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

View File

@ -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));

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/solid";
#playerControls {
background-color: #11151c;
.is-overflow-hidden {
overflow: hidden !important;
}

View File

@ -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>