diff --git a/package-lock.json b/package-lock.json index 11dfdd5..8a61914 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3125,6 +3125,11 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", diff --git a/package.json b/package.json index 5ce1291..2c0f846 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,18 @@ }, "dependencies": { "@fortawesome/fontawesome-free": "^5.12.1", + "@types/react": "^16.9.23", + "@types/react-redux": "^7.1.7", "bootstrap": "^4.0.0", + "classnames": "^2.2.6", + "immutability-helper": "^3.0.1", "jquery": "^3.3.1", "popper.js": "^1.16.1", + "react": "^16.13.0", + "react-dom": "^16.13.1", + "react-redux": "^7.2.0", + "redux": "^4.0.5", + "redux-act": "^1.7.7", "three": "^0.101.1" }, "scripts": { diff --git a/src/music/MusicPlayer.tsx b/src/music/MusicPlayer.tsx new file mode 100644 index 0000000..22cb10d --- /dev/null +++ b/src/music/MusicPlayer.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { connect } from "react-redux"; + +import Controls from "./components/controls"; +import Visualizer from "./components/visualizer"; +import { State } from "./store"; + +type AudioState = { + audioContext: AudioContext; + audioSource: HTMLAudioElement; + audioSourceNode: MediaElementAudioSourceNode; + audioVolume: GainNode; +}; + +type MusicPlayerProps = { + playing: boolean; + muted: boolean; + source?: string; +}; + +class MusicPlayer extends React.Component { + private audioState: AudioState; + + constructor(props: MusicPlayerProps) { + super(props); + + let context = new AudioContext(); + let source = new Audio(); + let sourceNode = context.createMediaElementSource(source); + let volume = context.createGain(); + + sourceNode.connect(volume); + volume.connect(context.destination); + + this.audioState = { + audioContext: context, + audioSourceNode: sourceNode, + audioSource: source, + audioVolume: volume, + }; + } + + render() { + return ( +
+ + +
+ ); + } + + componentDidUpdate() { + let context = this.audioState.audioContext; + let source = this.audioState.audioSource; + let 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; + } + + 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); + } + } +} + +function mapStateToProps(state: State): MusicPlayerProps { + return { + playing: state.musicState.playing, + muted: state.musicState.muted, + source: state.musicState.source, + }; +} + +export default connect(mapStateToProps)(MusicPlayer); diff --git a/src/music/components/controls.tsx b/src/music/components/controls.tsx new file mode 100644 index 0000000..1ff9ac2 --- /dev/null +++ b/src/music/components/controls.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import Redux from "redux"; +import { connect } from "react-redux"; + +import { State } from "../store"; +import { Title, togglePlay } from "../store/music/types"; +import Indicator from "./indicator"; + +type ControlProps = { + title: Title; +}; + +class Controls extends React.Component { + render() { + return ( +
+
+ +
+ {this.props.title.name} - {this.props.title.album} +
+ + {this.props.title.name === "Journey" && + this.props.title.artist === "Mseq" ? ( +
+ Journey +  by Mseq (c) copyright 2016 Licensed under a Creative + Commons  + + Attribution Noncommercial (3.0) + +   license. Ft: Admiral Bob,Texas Radio Fish +
+ ) : null} +
+
+ ); + } +} + +function mapStateToProps(state: State): ControlProps { + return { + title: state.musicState.title, + }; +} + +export default connect(mapStateToProps)(Controls); diff --git a/src/music/components/indicator.tsx b/src/music/components/indicator.tsx new file mode 100644 index 0000000..8324bbc --- /dev/null +++ b/src/music/components/indicator.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import { connect } from "react-redux"; +import classNames from "classnames"; + +import { State } from "../store"; +import { MusicState, togglePlay } from "../store/music/types"; + +type IndicatorProps = { + muted: boolean; + playing: boolean; +}; + +type IndicatorDispatch = { + play: () => void; +}; + +type Props = IndicatorProps & IndicatorDispatch; + +class Indicator extends React.Component { + click() { + this.props.play(); + } + + render() { + let classes = classNames({ + "col-auto": true, + "text-center": true, + fas: true, + "fa-muted": this.props.muted, + "fa-play": this.props.playing, + "fa-pause": !this.props.playing, + }); + + return ( +
+ ); + } +} + +function mapStateToProps(state: State): IndicatorProps { + return { + muted: state.musicState.muted, + playing: state.musicState.playing, + }; +} + +function mapDispatchToProps(dispatch: Redux.Dispatch): IndicatorDispatch { + return { + play: () => { + dispatch(togglePlay()); + }, + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Indicator); diff --git a/src/music/components/visualizer.tsx b/src/music/components/visualizer.tsx new file mode 100644 index 0000000..ebb7bfe --- /dev/null +++ b/src/music/components/visualizer.tsx @@ -0,0 +1,218 @@ +import React from "react"; +import * as three from "three"; + +type VisualizerProps = { + audioContext: AudioContext; + audioSource: AudioNode; +}; + +class CanvasDrawer { + private analyser: AnalyserNode; + private canvas: HTMLCanvasElement; + + private analyserData: Float32Array; + + private boxes: Array; + private camera: three.Camera; + private renderer: three.Renderer; + private scene: three.Scene; + + private angle: number; + + private animationFrame: number; + private lastTime: number; + + constructor(analyser: AnalyserNode, canvas: HTMLCanvasElement) { + this.analyser = analyser; + this.canvas = canvas; + + // Set up analyser data storage + this.analyserData = new Float32Array(analyser.frequencyBinCount); + + // Initialize the scene + this.scene = new three.Scene(); + + // Make a bunch of boxes to represent the bars + this.boxes = Array(analyser.frequencyBinCount); + let width = 2 / analyser.frequencyBinCount; + for (let freq = 0; freq < analyser.frequencyBinCount; freq++) { + let geometry = new three.BoxGeometry(1, 1, 1); + let material = new three.MeshLambertMaterial({ + color: new three.Color(0x99d1ce), + }); + let cube = new three.Mesh(geometry, material); + + cube.scale.set(width, 1e-6, width); + cube.position.set(-1 + freq * width, 0, 0); + + this.scene.add(cube); + this.boxes[freq] = cube; + } + + // Add lights for shadowing + let ambientLight = new three.AmbientLight(0xffffff, 0.4); + this.scene.add(ambientLight); + + let directionalLight = new three.DirectionalLight(0xffffff, 1); + directionalLight.position.set(-1, 0.3, -1); + directionalLight.castShadow = true; + this.scene.add(directionalLight); + + // Add a camera + this.angle = 3; + this.camera = new three.PerspectiveCamera( + 70, + canvas.width / canvas.height, + 0.01, + 10 + ); + this.camera.lookAt(0, 0, 0); + this.scene.add(this.camera); + this.rotateCamera(1); + + // Add a renderer + this.renderer = new three.WebGLRenderer({ + antialias: true, + canvas: canvas, + powerPreference: "low-power", + }); + + this.renderer.setClearColor(new three.Color(0x0f0f0f)); + this.renderer.setSize(canvas.width, canvas.height); + + // Set up canvas resizing + window.addEventListener("resize", this.resize.bind(this)); + + // Run the first, set the first animation frame time and start requesting + // animation frames + this.resize(); + this.lastTime = 0; + this.animationFrame = requestAnimationFrame(this.render.bind(this)); + } + + render(time: number) { + // Set our animation frame to 0, so that if we stop, we don't try to cancel a past animation frame + this.animationFrame = 0; + // Update elapsed time + let elapsed = time - this.lastTime; + this.lastTime = time; + + let camera = this.camera; + let renderer = this.renderer; + let scene = this.scene; + + this.scaleBoxes(); + this.rotateCamera(elapsed); + + renderer.render(scene, camera); + this.animationFrame = requestAnimationFrame(this.render.bind(this)); + } + + scaleBoxes() { + let analyser = this.analyser; + + analyser.getFloatFrequencyData(this.analyserData); + + for (let freq = 0; freq < analyser.frequencyBinCount; freq++) { + let height = analyser.maxDecibels / this.analyserData[freq]; + + if (height > 0.3) { + height -= 0.3; + } else { + height = 1e-6; + } + + this.boxes[freq].scale.y = height; + } + } + + rotateCamera(elapsed: number) { + if (this.angle >= Math.PI * 2) { + this.angle = 0; + } else { + this.angle += 0.1 * (elapsed / 1000); + } + + let camera = this.camera; + let angle = this.angle; + + camera.position.x = 1.01 * Math.sin(angle); + camera.position.z = 1.01 * Math.cos(angle); + + /* camera.position.y = (1 - Math.abs(angle - 0.5) / 0.5); */ + camera.lookAt(0, 0, 0); + } + + resize() { + let 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]; + + if (child != canvas) { + combinedHeight += child.clientHeight; + } + } + + // The remaining space we want to fill + let remainingHeight = canvas.parentElement.clientHeight - combinedHeight; + canvas.height = remainingHeight; + canvas.width = canvas.parentElement.clientWidth; + + this.camera.aspect = canvas.width / remainingHeight; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(canvas.width, remainingHeight); + } + + stop() { + if (this.animationFrame != 0) { + cancelAnimationFrame(this.animationFrame); + } + } +} + +class Visualizer extends React.Component { + private analyser: AnalyserNode; + private canvas: React.RefObject; + private drawer: CanvasDrawer; + + constructor(props: VisualizerProps) { + super(props); + this.canvas = React.createRef(); + } + + render() { + return ( + + ); + } + + componentDidMount() { + if (this.canvas.current === null) { + throw Error("Failed to create canvas; aborting"); + } + + this.analyser = this.props.audioContext.createAnalyser(); + 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() { + this.drawer.stop(); + this.props.audioSource.disconnect(this.analyser); + } +} + +export default Visualizer; diff --git a/src/music/index.tsx b/src/music/index.tsx new file mode 100644 index 0000000..a5c2917 --- /dev/null +++ b/src/music/index.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import { Provider } from "react-redux"; + +import { store } from "./store"; +import MusicPlayer from "./MusicPlayer"; +import { setSource, setTitle } from "./store/music/types"; +import mseq from "./Mseq_-_Journey.mp3"; + +const rootElement = document.getElementById("playerUI"); + +ReactDOM.render( + + + , + rootElement +); + +store.dispatch(setSource(mseq)); +store.dispatch( + setTitle({ + name: "Journey", + artist: "Mseq", + album: "Unknown album", + length: 192052244, + }) +); diff --git a/src/music/music.js b/src/music/music.js deleted file mode 100644 index fb6e764..0000000 --- a/src/music/music.js +++ /dev/null @@ -1,4 +0,0 @@ -import music from "./Mseq_-_Journey.mp3"; -import Player from "./player/player.js"; - -window.player = new Player(music); diff --git a/src/music/music.scss b/src/music/music.scss index adf4336..6f79835 100644 --- a/src/music/music.scss +++ b/src/music/music.scss @@ -2,16 +2,19 @@ // @import "@fortawesome/fontawesome-free/scss/fontawesome.scss"; // @import "@fortawesome/fontawesome-free/scss/solid.scss"; -@import "~/src/lib/scss/main"; +/* @import "~/src/lib/scss/main"; */ +@import url("~/node_modules/@fortawesome/fontawesome-free/scss/fontawesome"); +@import url("~/node_modules/@fortawesome/fontawesome-free/scss/solid"); #playerControls { - background-color: $dark; + background-color: #11151c; max-width: 100%; padding: 0.5rem 1rem; } -.playerControlsContent { +.playerControlsContent, #playerIndicator { padding: 0.5rem 1rem; + line-height: 1.5; } #playerButton { diff --git a/src/music/player/audio_manager.js b/src/music/player/audio_manager.js deleted file mode 100644 index ca20973..0000000 --- a/src/music/player/audio_manager.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * AudioManager() - * - * A class to manage the audio stream. - * - * @param {string} src - The URL of the audio to play. - */ -class AudioManager { - constructor(src) { - this._muted = false; - - // Create audio graph - this._context = new AudioContext(); - this.audio = new Audio(src); - this._volume = this._context.createGain(); - this._source = this._context.createMediaElementSource(this.audio); - - let context = this._context; - let volume = this._volume; - let source = this._source; - - source.connect(volume); - volume.connect(context.destination); - } - - get context() { - return this._context; - } - - get source() { - return this._source; - } - - get muted() { - return this._muted; - } - - addEventListener(...args) { - this.audio.addEventListener(...args); - } - - mute() { - let context = this._context; - let volume = this._volume; - - if (this._muted) { - volume.gain.setValueAtTime(1, context.currentTime); - } else { - volume.gain.setValueAtTime(0, context.currentTime); - } - - this._muted = !this._muted; - } -} - -export default AudioManager; diff --git a/src/music/player/background.js b/src/music/player/background.js deleted file mode 100644 index 031dfd2..0000000 --- a/src/music/player/background.js +++ /dev/null @@ -1,12 +0,0 @@ -class Background { - constructor(display, audioManager) { - if (this.constructor === Background) - throw new Error("Cannot instantiate abstract class!"); - } - - start() {} - - stop() {} -} - -export default Background; diff --git a/src/music/player/backgrounds/Spectrum.js b/src/music/player/backgrounds/Spectrum.js deleted file mode 100644 index eec5efd..0000000 --- a/src/music/player/backgrounds/Spectrum.js +++ /dev/null @@ -1,135 +0,0 @@ -import $ from "jquery"; -import * as three from "three"; - -import Background from "../background"; - -class Spectrum extends Background { - constructor(display, audioManager) { - super(audioManager, display); - - this._audioManager = audioManager; - this._display = display; - - this._init_analyser(); - this._init_scene(); - this._init_objects(); - this._resize(); - } - - _get_canvas_height() { - return ( - this._display.parent().height() - - this._display - .siblings() - .toArray() - .reduce((a, b) => { - return a + b.clientHeight; - }, 0) - ); - } - - _resize() { - this._camera.aspect = this._display.width() / this._get_canvas_height(); - this._camera.updateProjectionMatrix(); - this._renderer.setSize( - this._display.width(), - this._get_canvas_height() - ); - } - - _init_analyser() { - let audioManager = this._audioManager; - - let analyser = audioManager.context.createAnalyser(); - analyser.fftSize = 2048; - analyser.smoothingTimeConstant = 0.8; - - let analyser_data = new Float32Array(analyser.frequencyBinCount); - - audioManager.source.connect(analyser); - analyser.getFloatFrequencyData(analyser_data); - - this._analyser = analyser; - this._analyser_data = analyser_data; - } - - _init_scene() { - let scene = new three.Scene(); - - let camera = new three.PerspectiveCamera( - 70, - this._display.width() / this._get_canvas_height(), - 0.01, - 10 - ); - camera.position.z = 1; - scene.add(camera); - - let renderer = new three.WebGLRenderer({ - antialias: true, - powerPreference: "low-power" - }); - - renderer.setSize(this._display.width(), this._display.height()); - - this._display.append(renderer.domElement); - - this._scene = scene; - this._camera = camera; - this._renderer = renderer; - - // Set the resize handler - $(window).resize(() => this._resize()); - } - - _init_objects() { - let analyser = this._analyser; - let scene = this._scene; - - let boxes = Array(analyser.frequencyBinCount); - let width = 2 / analyser.frequencyBinCount; - - for (let freq = 0; freq < analyser.frequencyBinCount; freq++) { - let geometry = new three.BoxGeometry(1, 1, 1); - let material = new three.MeshBasicMaterial({ color: 0x99d1ce }); - let cube = new three.Mesh(geometry, material); - - cube.scale.set(width, 1e-6, width); - cube.position.set(-1 + freq * width, 0, 0); - - scene.add(cube); - boxes[freq] = cube; - } - - this._boxes = boxes; - } - - start() { - requestAnimationFrame(this._render.bind(this)); - } - - _render() { - let analyser = this._analyser; - let camera = this._camera; - let renderer = this._renderer; - let scene = this._scene; - - for (let freq = 0; freq < analyser.frequencyBinCount; freq++) { - let height = analyser.maxDecibels / this._analyser_data[freq]; - - if (height > 0.3) { - height -= 0.3; - } else { - height = 1e-6; - } - - this._boxes[freq].scale.y = height; - } - - renderer.render(scene, camera); - analyser.getFloatFrequencyData(this._analyser_data); - requestAnimationFrame(this._render.bind(this)); - } -} - -export default Spectrum; diff --git a/src/music/player/controls.js b/src/music/player/controls.js deleted file mode 100644 index 4603df2..0000000 --- a/src/music/player/controls.js +++ /dev/null @@ -1,195 +0,0 @@ -import $ from "jquery"; - -/** - * _Text() - * - * A helper class to control the text area for audio controls. - * - * @private - * @param {HTMLElement} div - The div of the text area. - */ -class _Text { - constructor(div) { - this._element = div; - - this._text = ""; - this._flashing = false; - - this._flash_timeout = null; - } - - flashText(text) { - let element = this._element; - let flash_timeout = this._flash_timeout; - - element.html(text); - this._flashing = true; - - clearTimeout(flash_timeout); - this._flash_timeout = setTimeout(() => { - this._flashing = false; - }, 1000); - } - - setText(text) { - let element = this._element; - let flashing = this._flashing; - - if (!flashing) { - element.html(text); - } - - this._text = text; - } -} - -/** - * _Button() - * - * A helper class to control the indicator button for audio controls. - * - * @private - * @param {HTMLElement} button - The div of the button. - */ -class _Button { - constructor(button) { - this._element = button; - - this._classes = ""; - this._flash_classes = ""; - this._flashing = false; - - this._flash_timeout = null; - } - - click(callback) { - this._element.click(callback); - } - - flashIcon(icon) { - let classes = this._classes; - let element = this._element; - let flash_classes = this._flash_classes; - let flash_timeout = this._flash_timeout; - - // We remove any potentially current classes, and add the - // requested icon class. - element.removeClass(flash_classes); - element.removeClass(classes); - element.addClass(icon); - - // We keep track of the classes we are flashing and the fact - // that we are flashing, so that we can remove the correct - // classes later. - this._flash_classes = icon; - this._flashing = true; - - // If we previously started a timeout, we'll want to cancel it - // now, since otherwise *it* will clear out our classes. - clearTimeout(flash_timeout); - - // We start a timeout to reset to the original classes. - this._flash_timeout = setTimeout(() => { - element.removeClass(this._flash_classes); - element.addClass(this._classes); - - this._flash_classes = ""; - this._flashing = false; - }, 1000); - } - - setIcon(icon) { - let classes = this._classes; - let element = this._element; - let flashing = this._flashing; - - // If we are currently flashing an icon, we'll hold off on - // changing the icon. - if (!flashing) { - element.removeClass(classes); - element.addClass(icon); - } - - this._classes = icon; - } -} - -/** - * Controls() - * - * A class to manage music player controls. - * - * @param {AudioManager} audioManager - The audio manager to interact with. - */ -class Controls { - constructor(audioManager) { - this._element = $("#playerControls"); - this._button = new _Button($("#playerControls #playerButton")); - this._text = new _Text($("#playerControls #playerText")); - this._audioManager = audioManager; - - this._setAudioEvents({ - canplay: () => this._audioManager.audio.play(), - error: () => this.setError(this._audioManager.audio.error.message), - loadstart: () => this.setLoading(), - playing: () => this.setPlaying(), - stalled: () => this.setLoading("Stalling") - }); - - this._button.click(() => this.mute()); - } - - setPlaying() { - let button = this._button; - let text = this._text; - - button.setIcon("fa-play"); - text.setText(""); - } - - setLoading(reason = "Loading") { - let button = this._button; - let text = this._text; - - button.setIcon("fa-spin fa-spinner"); - text.setText(reason); - } - - setError(reason = "") { - let button = this._button; - let text = this._text; - - if (reason == "") { - reason = "Error"; - } else { - reason = `Error: ${reason}`; - } - - button.setIcon("fa-stop-circle"); - text.setText(reason); - } - - mute() { - let audioManager = this._audioManager; - let button = this._button; - let text = this._text; - - audioManager.mute(); - - if (audioManager.muted) { - button.flashIcon("fa-volume-off"); - text.flashText("Muted"); - } else { - button.flashIcon("fa-volume-up"); - text.flashText("Unmuted"); - } - } - - _setAudioEvents(events) { - for (let [event, handler] of Object.entries(events)) { - this._audioManager.addEventListener(event, handler); - } - } -} - -export default Controls; diff --git a/src/music/player/display.js b/src/music/player/display.js deleted file mode 100644 index 2828909..0000000 --- a/src/music/player/display.js +++ /dev/null @@ -1,23 +0,0 @@ -import $ from "jquery"; -import Spectrum from "./backgrounds/Spectrum.js"; - -let backgrounds = { - Spectrum: Spectrum -}; - -class Display { - constructor(audioManager) { - this._audioManager = audioManager; - this._element = $("#playerContent"); - - this._background = new backgrounds["Spectrum"]( - this._element, - audioManager - ); - this._background.start(); - } - - set background(name) {} -} - -export default Display; diff --git a/src/music/player/player.js b/src/music/player/player.js deleted file mode 100644 index 749c8e1..0000000 --- a/src/music/player/player.js +++ /dev/null @@ -1,15 +0,0 @@ -import $ from "jquery"; -import AudioManager from "./audio_manager"; -import Controls from "./controls"; -import Display from "./display"; - -class Player { - constructor(src = "https://tlater.net/assets/Mseq_-_Journey.mp3") { - this._ui = $("#playerUI"); - this._audioManager = new AudioManager(src); - this._controls = new Controls(this._audioManager); - this._display = new Display(this._audioManager); - } -} - -export default Player; diff --git a/src/music/store/index.ts b/src/music/store/index.ts new file mode 100644 index 0000000..f523735 --- /dev/null +++ b/src/music/store/index.ts @@ -0,0 +1,17 @@ +import { createStore, combineReducers } from "redux"; + +import { MusicState } from "./music/types"; +import { musicStateReducer } from "./music/reducers"; + +export interface State { + musicState: MusicState; +} + +const rootReducer = combineReducers({ + musicState: musicStateReducer, +}); + +export const store = createStore( + rootReducer, + window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() +); diff --git a/src/music/store/music/reducers.ts b/src/music/store/music/reducers.ts new file mode 100644 index 0000000..83883d1 --- /dev/null +++ b/src/music/store/music/reducers.ts @@ -0,0 +1,45 @@ +import { createReducer } from "redux-act"; +import update from "immutability-helper"; + +import { + Title, + MusicState, + setTitle, + toggleMute, + togglePlay, + setSource, +} from "./types"; + +const defaultTitle: Title = { + name: "Untitled", + artist: "Unknown Artist", + album: "Unknown Album", + length: 0, +}; + +const initialState: MusicState = { + muted: false, + playing: false, + title: defaultTitle, + playTime: 0, +}; + +export const musicStateReducer = createReducer( + { + [setTitle]: (state: MusicState, title: Title): MusicState => { + return update(state, { + title: { $set: title }, + }); + }, + [togglePlay]: (state: MusicState): MusicState => { + return update(state, { $toggle: ["playing"] }); + }, + [toggleMute]: (state: MusicState): MusicState => { + return update(state, { $toggle: ["muted"] }); + }, + [setSource]: (state: MusicState, source: string): MusicState => { + return update(state, { source: { $set: source } }); + }, + }, + initialState +); diff --git a/src/music/store/music/types.ts b/src/music/store/music/types.ts new file mode 100644 index 0000000..39555fc --- /dev/null +++ b/src/music/store/music/types.ts @@ -0,0 +1,35 @@ +import { Action, createAction } from "redux-act"; + +export interface Title { + name: string; + artist: string; + album: string; + /** + * The length of the title in nanoseconds. + */ + length: number; +} + +export interface MusicState { + muted: boolean; + playing: boolean; + title: Title; + playTime: number; + source?: string; +} + +export const setTitle: (title: Title) => Action = createAction( + "set currently playing title" +); + +export const setPlayTime: (time: number) => Action = createAction( + "set the play time" +); + +export const toggleMute: () => Action = createAction("toggle mute"); + +export const togglePlay: () => Action = createAction("toggle play"); + +export const setSource: (source: string) => Action = createAction( + "set the title" +); diff --git a/src/music/store/ui/types.ts b/src/music/store/ui/types.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/music_sample.pug b/src/music_sample.pug index 66235ce..78dcdb2 100644 --- a/src/music_sample.pug +++ b/src/music_sample.pug @@ -5,10 +5,4 @@ block stylesheets block footer #playerUI.container-fluid.flex-grow-1 - .row.d-flex.flex-column.h-100 - #playerContent.container-fluid.flex-grow-1 - #playerControls.container-fluid.fixed-bottom - .row - span#playerButton.col-1.playerControlsContent.fa.fa-fw.fa-spin.fa-spinner - #playerText.col-11.playerControlsContent Some text for now - script(type="text/javascript" src="./music/music.js") + script(type="text/javascript" src="./music/") diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9efa87c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "strictNullChecks": true, + "esModuleInterop": true, + "jsx": "react" + } +} +