Skip to content

Commit

Permalink
add audio element
Browse files Browse the repository at this point in the history
  • Loading branch information
mikebarkmin committed Jun 19, 2023
1 parent 9eadbb1 commit a9d0900
Show file tree
Hide file tree
Showing 22 changed files with 437 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .changeset/wild-cycles-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@hyperbook/element-audio": minor
"hyperbook-studio": minor
"@platforms/web": minor
---

Add audio element for playing audio files and showing information about the audio file.
9 changes: 9 additions & 0 deletions packages/element-audio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# @hyperbook/element-audio

## Installation

```sh
yarn add @hyperbook/element-audio
# or
npm i @hyperbook/element-audio
```
59 changes: 59 additions & 0 deletions packages/element-audio/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"name": "@hyperbook/element-audio",
"version": "0.0.0",
"author": "Mike Barkmin",
"homepage": "https://github.com/openpatch/hyperbook#readme",
"license": "MIT",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.mjs",
"types": "dist/index.d.ts",
"typings": "dist/index.d.ts",
"sideEffects": false,
"exports": {
".": {
"require": "./dist/index.cjs.js",
"default": "./dist/index.esm.mjs"
},
"./index.css": "./dist/index.css"
},
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/openpatch/hyperbook.git",
"directory": "packages/element-audio"
},
"bugs": {
"url": "https://github.com/openpatch/hyperbook/issues"
},
"scripts": {
"prebuild": "rimraf dist",
"version": "pnpm build",
"lint": "tsc --noEmit",
"build": "pnpm build:pkg && pnpm build:types",
"build:pkg": "node ../../scripts/build.mjs",
"build:types": "tsc --project tsconfig.build.json --declaration --emitDeclarationOnly"
},
"dependencies": {
"@hyperbook/provider": "workspace:*",
"@hyperbook/store": "workspace:*",
"@types/wavesurfer.js": "^6.0.6",
"wavesurfer.js": "^6.6.4"
},
"peerDependencies": {
"react": "18.x",
"react-dom": "18.x",
"react-redux": "8.x"
},
"devDependencies": {
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"react": "18.2.0",
"react-dom": "18.2.0",
"vitest": "^0.26.3"
}
}
53 changes: 53 additions & 0 deletions packages/element-audio/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
.element-audio {
margin-bottom: 16px;
}

.element-audio .information {
margin-top: 4px;
text-align: center;
font-style: italic;
}

.element-audio .player .thumbnail {
width: 64px;
height: 64px;
background-size: cover;
background-position: center;
border-right: 1px solid;
border-right-color: var(--color-nav-border);
}

.element-audio .player .thumbnail.right {
border-right: none;
border-left: 1px solid;
border-left-color: var(--color-nav-border);
}

.element-audio .player {
display: flex;
overflow: hidden;
align-items: center;
justify-content: center;
background-color: var(--color-nav);
border-radius: 8px;
border-style: solid;
border-width: 1px;
border-color: var(--color-nav-border);
}

.element-audio .player .wave {
flex: 1;
}

.element-audio .player .play {
width: 64px;
height: 64px;
display: flex;
border: none;
color: var(--color-text);
background: none;
justify-content: center;
align-items: center;
font-size: 32px;
padding: 0;
}
152 changes: 152 additions & 0 deletions packages/element-audio/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { FC, useEffect, useRef, useState } from "react";
import { createSlice } from "@hyperbook/store";
import { useMakeUrl } from "@hyperbook/provider";
import type Wavesurfer from "wavesurfer.js";
import PlayIcon from "./play";
import PauseIcon from "./pause";
import "./index.css";

type DirectiveAudioProps = {
src: string;
title?: string;
author?: string;
thumbnail?: string;
position?: "left" | "right";
};

function secondsToTimestamp(seconds: number) {
seconds = Math.floor(seconds);
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds - h * 3600) / 60);
var s = seconds - h * 3600 - m * 60;

let th = h < 10 ? "0" + h : h;
let tm = m < 10 ? "0" + m : m;
let ts = s < 10 ? "0" + s : s;
if (h > 0) {
return th + ":" + tm + ":" + ts;
}

return tm + ":" + ts;
}

const DirectiveAudio: FC<DirectiveAudioProps> = ({
src,
title,
thumbnail,
author,
position = "left",
}) => {
const makeUrl = useMakeUrl();
src = makeUrl(src);
thumbnail = makeUrl(thumbnail);

const containerRef = useRef<HTMLDivElement>(null);
const wavesurferRef = useRef<Wavesurfer>();
const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);

useEffect(() => {
import("wavesurfer.js").then((wavesurfer) => {
if (containerRef.current) {
const ws = wavesurfer.default.create({
container: containerRef.current,
cursorWidth: 4,
barWidth: 4,
barGap: 5,
barRadius: 2,
height: 64,
responsive: true,
});
ws.load(src);
ws.on("ready", () => {
wavesurferRef.current = ws;
setDuration(ws.getDuration());
});

ws.on("audioprocess", () => {
setCurrentTime(Math.floor(ws.getCurrentTime()));
});

ws.on("pause", () => {
setIsPlaying(false);
});

ws.on("finish", () => {
setIsPlaying(false);
});

ws.on("play", () => {
setIsPlaying(true);
});

ws.on;
}
});

return () => {
wavesurferRef.current?.destroy();
};
}, [src]);

const togglePlayPause = () => {
if (wavesurferRef.current) {
wavesurferRef.current.playPause();
setIsPlaying(wavesurferRef.current.isPlaying());
}
};

return (
<div className="element-audio">
<div className="player">
{position === "left" && thumbnail && (
<div
className="thumbnail"
style={{ backgroundImage: `url(${thumbnail})` }}
></div>
)}
{position === "left" && (
<button className="play" onClick={togglePlayPause}>
{isPlaying ? <PauseIcon /> : <PlayIcon />}
</button>
)}
<div className="wave" ref={containerRef} />
{position === "right" && (
<button className="play right" onClick={togglePlayPause}>
{isPlaying ? <PauseIcon /> : <PlayIcon />}
</button>
)}
{position === "right" && thumbnail && (
<div
className="thumbnail right"
style={{ backgroundImage: `url(${thumbnail})` }}
></div>
)}
</div>
<div className="information">
{title && <span className="title">{title}</span>}{" "}
{title && author && <span className="spacer">-</span>}{" "}
{author && <span className="author">{author}</span>}{" "}
<span className="duration">
{secondsToTimestamp(currentTime)}/{secondsToTimestamp(duration)}
</span>
</div>
</div>
);
};

type ElementAudioState = {};

const initialState: ElementAudioState = {};

const sliceAudio = createSlice({
name: "element.audio",
initialState,
reducers: {},
});

export default {
directives: { audio: DirectiveAudio },
slice: sliceAudio,
};
15 changes: 15 additions & 0 deletions packages/element-audio/src/pause.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SVGProps } from "react";

const SvgComponent = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 -960 960 960"
fill="currentColor"
{...props}
>
<path d="M555-200v-560h175v560H555Zm-325 0v-560h175v560H230Z" />
</svg>
);
export default SvgComponent;
15 changes: 15 additions & 0 deletions packages/element-audio/src/play.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SVGProps } from "react";

const SvgComponent = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 -960 960 960"
fill="currentColor"
{...props}
>
<path d="M320-203v-560l440 280-440 280Z" />
</svg>
);
export default SvgComponent;
8 changes: 8 additions & 0 deletions packages/element-audio/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": ["src", "../../types"],
"compilerOptions": {
"rootDir": "src",
"declarationDir": "dist"
}
}
4 changes: 4 additions & 0 deletions packages/element-audio/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src", "../../types", "tests"]
}
3 changes: 3 additions & 0 deletions packages/element-audio/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineConfig } from "vitest/config";

export default defineConfig({});
2 changes: 2 additions & 0 deletions platforms/vscode/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react";
import { Provider, ProviderProps } from "@hyperbook/provider";
import elementTab from "@hyperbook/element-tabs";
import elementAudio from "@hyperbook/element-audio";
import elementAlert from "@hyperbook/element-alert";
import elementBitflow from "@hyperbook/element-bitflow";
import elementTerm from "@hyperbook/element-term";
Expand Down Expand Up @@ -133,6 +134,7 @@ export const App = () => {
}}
elements={[
elementTab,
elementAudio,
elementAlert,
elementBitflow,
elementTerm,
Expand Down
1 change: 1 addition & 0 deletions platforms/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@
"webpack-cli": "4.10.0"
},
"dependencies": {
"@hyperbook/element-audio": "workspace:*",
"@hyperbook/element-alert": "workspace:*",
"@hyperbook/element-bitflow": "workspace:*",
"@hyperbook/element-bookmarks": "workspace:*",
Expand Down
1 change: 1 addition & 0 deletions platforms/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"next:build": "next build && next export"
},
"dependencies": {
"@hyperbook/element-audio": "workspace:*",
"@hyperbook/element-alert": "workspace:*",
"@hyperbook/element-bitflow": "workspace:*",
"@hyperbook/element-bookmarks": "workspace:*",
Expand Down
3 changes: 3 additions & 0 deletions platforms/web/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import "@hyperbook/markdown/index.css";
import { Provider, ProviderProps } from "@hyperbook/provider";
import elementTab from "@hyperbook/element-tabs";
import "@hyperbook/element-tabs/index.css";
import elementAudio from "@hyperbook/element-audio";
import "@hyperbook/element-audio/index.css";
import elementAlert from "@hyperbook/element-alert";
import "@hyperbook/element-alert/index.css";
import elementTerm from "@hyperbook/element-term";
Expand Down Expand Up @@ -89,6 +91,7 @@ export default function MyApp({ Component, pageProps }) {
env={process.env.NODE_ENV === "production" ? "production" : "development"}
elements={[
elementTab,
elementAudio,
elementAlert,
elementTerm,
elementYoutube,
Expand Down
2 changes: 1 addition & 1 deletion plop-templates/element/src/index.tsx.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FC, Fragment, ReactNode } from "react";
import { useDispatch, useSelector } from "react-redux";
import { createSlice } from "@openpatch/store";
import { createSlice } from "@hyperbook/store";
import { useActivePageId } from "@hyperbook/provider";
import "./index.css"

Expand Down
Loading

1 comment on commit a9d0900

@vercel
Copy link

@vercel vercel bot commented on a9d0900 Jun 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.