mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 02:15:01 +00:00
Webamp optionally fully contained into a DOM element (#1338)
* Webamp optionally fully contained into a DOM element * Fix spelling * Reenable updating search index for uploads * Replace contained flag on renderWhenReady with a new method. Add docs --------- Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>
This commit is contained in:
parent
f600fb0344
commit
162025f8a0
19 changed files with 223 additions and 30 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
|||
node_modules
|
||||
.vscode
|
||||
.idea
|
||||
dist
|
||||
|
||||
# Turborepo cache
|
||||
|
|
|
|||
24
examples/contained/.gitignore
vendored
Normal file
24
examples/contained/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
examples/contained/README.md
Normal file
3
examples/contained/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# `webamp/contained` Example
|
||||
|
||||
Example for Webamp fully contained into a DOM element. Uses [Vite](https://vitejs.dev/) for development and bundling.
|
||||
13
examples/contained/index.html
Normal file
13
examples/contained/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Webamp (contained)</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
18
examples/contained/package.json
Normal file
18
examples/contained/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "contained",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"webamp": "^2.2.0"
|
||||
}
|
||||
}
|
||||
36
examples/contained/src/main.ts
Normal file
36
examples/contained/src/main.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import Webamp from "../../../packages/webamp/js/webamp";
|
||||
|
||||
const webamp = new Webamp({
|
||||
initialTracks: [
|
||||
{
|
||||
metaData: {
|
||||
artist: "DJ Mike Llama",
|
||||
title: "Llama Whippin' Intro",
|
||||
},
|
||||
// NOTE: Your audio file must be served from the same domain as your HTML
|
||||
// file, or served with permissive CORS HTTP headers:
|
||||
// https://docs.webamp.org/docs/guides/cors
|
||||
url: "https://cdn.jsdelivr.net/gh/captbaritone/webamp@43434d82cfe0e37286dbbe0666072dc3190a83bc/mp3/llama-2.91.mp3",
|
||||
duration: 5.322286,
|
||||
},
|
||||
],
|
||||
windowLayout: {
|
||||
main: { position: { left: 0, top: 0 } },
|
||||
equalizer: { position: { left: 0, top: 116 } },
|
||||
playlist: {
|
||||
position: { left: 0, top: 232 },
|
||||
size: { extraHeight: 4, extraWidth: 0 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Container smaller than body
|
||||
const container = document.getElementById("app");
|
||||
container!.style.position = "absolute";
|
||||
container!.style.left = "20px";
|
||||
container!.style.top = "20px";
|
||||
container!.style.right = "120px";
|
||||
container!.style.bottom = "120px";
|
||||
container!.style.backgroundColor = "lightyellow";
|
||||
|
||||
webamp.renderWhenReady(container!, true);
|
||||
1
examples/contained/src/vite-env.d.ts
vendored
Normal file
1
examples/contained/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
25
examples/contained/tsconfig.json
Normal file
25
examples/contained/tsconfig.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -118,7 +118,7 @@ async function addClassicSkinFromBuffer(
|
|||
await setHashesForSkin(skin);
|
||||
|
||||
// Disable while we figure out our quota
|
||||
// await Skins.updateSearchIndex(ctx, md5);
|
||||
await Skins.updateSearchIndex(ctx, md5);
|
||||
|
||||
return { md5, status: "ADDED", skinType: "CLASSIC" };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -458,7 +458,7 @@ program
|
|||
);
|
||||
const md5s = rows.map((row) => row.md5);
|
||||
console.log(md5s.length);
|
||||
console.log(await Skins.updateSearchIndexs(ctx, md5s));
|
||||
console.log(await Skins.updateSearchIndexes(ctx, md5s));
|
||||
}
|
||||
if (refreshContentHash) {
|
||||
const ctx = new UserContext();
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ async function getSearchIndexes(
|
|||
);
|
||||
}
|
||||
|
||||
export async function updateSearchIndexs(
|
||||
export async function updateSearchIndexes(
|
||||
ctx: UserContext,
|
||||
md5s: string[]
|
||||
): Promise<any> {
|
||||
|
|
@ -276,7 +276,7 @@ export async function updateSearchIndex(
|
|||
ctx: UserContext,
|
||||
md5: string
|
||||
): Promise<any | null> {
|
||||
return updateSearchIndexs(ctx, [md5]);
|
||||
return updateSearchIndexes(ctx, [md5]);
|
||||
}
|
||||
|
||||
export async function hideSkin(md5: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ Create a DOM element somewhere in your HTML document. This will be used by Webam
|
|||
|
||||
:::tip
|
||||
**Webamp will not actually insert itself as a child of this element.** It will will insert itself as a child of the body element, and will attempt to center itself within this element. This is needed to allow the various Webamp windows to dragged around the page unencumbered.
|
||||
|
||||
If you want Webamp to be a child of a specific element, use the [`renderInto(domNode)`](./06_API/03_instance-methods.md#renderintodomnode-htmlelement-promisevoid) method instead.
|
||||
:::
|
||||
|
||||
## Initialize Webamp instance
|
||||
|
|
|
|||
|
|
@ -229,6 +229,19 @@ Toggle repeat mode between enabled and disabled.
|
|||
webamp.toggleRepeat();
|
||||
```
|
||||
|
||||
### `renderInto(domNode: HTMLElement): Promise<void>`
|
||||
|
||||
Webamp will wait until it has fetched the skin and fully parsed it, and then render itself as a child of the provided `domNode` and position itself in the center of that DOM node.
|
||||
|
||||
A promise is returned which will resolve after the render is complete.
|
||||
|
||||
```ts
|
||||
const container = document.getElementById("webamp-container");
|
||||
webamp.renderWhenReady(container).then(() => {
|
||||
console.log("rendered webamp!");
|
||||
});
|
||||
```
|
||||
|
||||
### `renderWhenReady(domNode: HTMLElement): Promise<void>`
|
||||
|
||||
Webamp will wait until it has fetched the skin and fully parsed it, and then render itself into a new DOM node at the end of the `<body>` tag.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ If you want access to the changes in this section before they are officially rel
|
|||
|
||||
### Improvements
|
||||
|
||||
- Added a new Webamp instance method: [`webamp.renderInto(domNode)`](./06_API/03_instance-methods.md#renderintodomnode-htmlelement-promisevoid). Which renders Webamp as a _child_ of the provided DOM node, rather than within its own top-level container.
|
||||
- Added new [`requireButterchurnPresets`](./06_API/02_webamp-constructor.md#requirebutterchurnpresets---promisepreset) option when constructing a Webamp instance. This allows you to specify which Butterchurn presets to use for the Milkdrop visualizer. If you don't specify this option, Webamp will use the default Butterchurn presets.
|
||||
|
||||
### Bug Fixes
|
||||
|
|
|
|||
|
|
@ -104,20 +104,27 @@ export function updateWindowPositions(
|
|||
return { type: "UPDATE_WINDOW_POSITIONS", positions, absolute };
|
||||
}
|
||||
|
||||
export function centerWindowsInContainer(container: HTMLElement): Thunk {
|
||||
export function centerWindowsInContainer(
|
||||
container: HTMLElement,
|
||||
contained: boolean
|
||||
): Thunk {
|
||||
return (dispatch, getState) => {
|
||||
if (!Selectors.getPositionsAreRelative(getState())) {
|
||||
return;
|
||||
}
|
||||
const { left, top } = container.getBoundingClientRect();
|
||||
const { left, top } = contained
|
||||
? { left: 0, top: 0 }
|
||||
: container.getBoundingClientRect();
|
||||
const { scrollWidth: width, scrollHeight: height } = container;
|
||||
dispatch(centerWindows({ left, top, width, height }));
|
||||
};
|
||||
}
|
||||
|
||||
export function centerWindowsInView(): Thunk {
|
||||
const height = window.innerHeight;
|
||||
const width = window.innerWidth;
|
||||
export function centerWindowsInView(parentDomNode?: HTMLElement): Thunk {
|
||||
const { width, height } =
|
||||
parentDomNode === document.body || !parentDomNode
|
||||
? { width: window.innerWidth, height: window.innerHeight }
|
||||
: Utils.getElementSize(parentDomNode);
|
||||
return centerWindows({ left: 0, top: 0, width, height });
|
||||
}
|
||||
|
||||
|
|
@ -168,13 +175,16 @@ export function centerWindows({ left, top, width, height }: Box): Thunk {
|
|||
};
|
||||
}
|
||||
|
||||
export function browserWindowSizeChanged(size: {
|
||||
height: number;
|
||||
width: number;
|
||||
}): Thunk {
|
||||
export function browserWindowSizeChanged(
|
||||
size: {
|
||||
height: number;
|
||||
width: number;
|
||||
},
|
||||
parentDomNode?: HTMLElement
|
||||
): Thunk {
|
||||
return (dispatch: Dispatch) => {
|
||||
dispatch({ type: "BROWSER_WINDOW_SIZE_CHANGED", ...size });
|
||||
dispatch(ensureWindowsAreOnScreen());
|
||||
dispatch(ensureWindowsAreOnScreen(parentDomNode));
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -234,13 +244,16 @@ export function setWindowLayout(layout?: WindowLayout): Thunk {
|
|||
};
|
||||
}
|
||||
|
||||
export function ensureWindowsAreOnScreen(): Thunk {
|
||||
export function ensureWindowsAreOnScreen(parentDomNode?: HTMLElement): Thunk {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const windowsInfo = Selectors.getWindowsInfo(state);
|
||||
const getOpen = Selectors.getWindowOpen(state);
|
||||
const { height, width } = Utils.getWindowSize();
|
||||
const { height, width } =
|
||||
parentDomNode === document.body || !parentDomNode
|
||||
? Utils.getWindowSize()
|
||||
: Utils.getElementSize(parentDomNode);
|
||||
const bounding = Utils.calculateBoundingBox(
|
||||
windowsInfo.filter((w) => getOpen(w.key))
|
||||
);
|
||||
|
|
@ -294,6 +307,6 @@ export function ensureWindowsAreOnScreen(): Thunk {
|
|||
// I give up. Just reset everything.
|
||||
dispatch(resetWindowSizes());
|
||||
dispatch(stackWindows());
|
||||
dispatch(centerWindowsInView());
|
||||
dispatch(centerWindowsInView(parentDomNode));
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export default function App({
|
|||
webampNode.style.right = "0";
|
||||
webampNode.style.bottom = "0";
|
||||
webampNode.style.overflow = "hidden";
|
||||
browserWindowSizeChanged(Utils.getWindowSize());
|
||||
browserWindowSizeChanged(Utils.getWindowSize(), parentDomNode);
|
||||
webampNode.style.right = "auto";
|
||||
webampNode.style.bottom = "auto";
|
||||
webampNode.style.overflow = "visible";
|
||||
|
|
@ -100,7 +100,7 @@ export default function App({
|
|||
return () => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
};
|
||||
}, [browserWindowSizeChanged, webampNode]);
|
||||
}, [parentDomNode, browserWindowSizeChanged, webampNode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onMount != null) {
|
||||
|
|
@ -151,7 +151,10 @@ export default function App({
|
|||
<ContextMenuWrapper
|
||||
renderContents={() => <MainContextMenu filePickers={filePickers} />}
|
||||
>
|
||||
<WindowManager windows={renderWindows()} />
|
||||
<WindowManager
|
||||
windows={renderWindows()}
|
||||
parentDomNode={parentDomNode}
|
||||
/>
|
||||
</ContextMenuWrapper>
|
||||
</div>
|
||||
</StrictMode>,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const abuts = (a: Box, b: Box) => {
|
|||
|
||||
interface Props {
|
||||
windows: { [windowId: string]: ReactNode };
|
||||
parentDomNode: HTMLElement;
|
||||
}
|
||||
|
||||
type DraggingState = {
|
||||
|
|
@ -25,9 +26,12 @@ type DraggingState = {
|
|||
mouseStart: Point;
|
||||
};
|
||||
|
||||
function useHandleMouseDown(propsWindows: {
|
||||
[windowId: string]: ReactNode;
|
||||
}): (
|
||||
function useHandleMouseDown(
|
||||
propsWindows: {
|
||||
[windowId: string]: ReactNode;
|
||||
},
|
||||
parentDomNode: HTMLElement
|
||||
): (
|
||||
key: WindowId,
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
) => void {
|
||||
|
|
@ -69,7 +73,9 @@ function useHandleMouseDown(propsWindows: {
|
|||
|
||||
const withinDiff = SnapUtils.snapWithinDiff(
|
||||
proposedBox,
|
||||
browserWindowSize
|
||||
parentDomNode === document.body || !parentDomNode
|
||||
? browserWindowSize
|
||||
: Utils.getElementSize(parentDomNode)
|
||||
);
|
||||
|
||||
const finalDiff = SnapUtils.applyMultipleDiffs(
|
||||
|
|
@ -102,7 +108,7 @@ function useHandleMouseDown(propsWindows: {
|
|||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
window.removeEventListener("touchend", handleMouseUp);
|
||||
};
|
||||
}, [browserWindowSize, draggingState, updateWindowPositions]);
|
||||
}, [parentDomNode, browserWindowSize, draggingState, updateWindowPositions]);
|
||||
|
||||
// Mouse down handler
|
||||
return useCallback(
|
||||
|
|
@ -149,10 +155,13 @@ function useHandleMouseDown(propsWindows: {
|
|||
);
|
||||
}
|
||||
|
||||
export default function WindowManager({ windows: propsWindows }: Props) {
|
||||
export default function WindowManager({
|
||||
windows: propsWindows,
|
||||
parentDomNode,
|
||||
}: Props) {
|
||||
const windowsInfo = useTypedSelector(Selectors.getWindowsInfo);
|
||||
const setFocusedWindow = useActionCreator(Actions.setFocusedWindow);
|
||||
const handleMouseDown = useHandleMouseDown(propsWindows);
|
||||
const handleMouseDown = useHandleMouseDown(propsWindows, parentDomNode);
|
||||
|
||||
const windows = windowsInfo.filter((w) => propsWindows[w.key]);
|
||||
|
||||
|
|
|
|||
|
|
@ -370,6 +370,16 @@ export function findLastIndex<T>(arr: T[], cb: (val: T) => boolean) {
|
|||
return -1;
|
||||
}
|
||||
|
||||
export function getElementSize(domNode: HTMLElement): {
|
||||
width: number;
|
||||
height: number;
|
||||
} {
|
||||
return {
|
||||
width: Math.max(domNode.scrollWidth, domNode.offsetWidth),
|
||||
height: Math.max(domNode.scrollHeight, domNode.offsetHeight),
|
||||
};
|
||||
}
|
||||
|
||||
export function getWindowSize(): { width: number; height: number } {
|
||||
// Apparently this is crazy across browsers.
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -479,12 +479,33 @@ class Webamp {
|
|||
*
|
||||
* Webamp is rendered into a new DOM node at the end of the <body> tag with the id `#webamp`.
|
||||
*
|
||||
* If a domNode is passed, Webamp will place itself in the center of that DOM node.
|
||||
* Webamp will position itself on top of the center of the given DOM node.
|
||||
*
|
||||
* @returns A promise is returned which will resolve after the render is complete.
|
||||
*/
|
||||
async renderWhenReady(node: HTMLElement): Promise<void> {
|
||||
this.store.dispatch(Actions.centerWindowsInContainer(node));
|
||||
return this._render(node, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Webamp will wait until it has fetched the skin and fully parsed it and then render itself.
|
||||
*
|
||||
* Webamp will render itself as a child of the given DOM node and position
|
||||
* itself in the center of that node.
|
||||
*
|
||||
* @returns A promise is returned which will resolve after the render is complete.
|
||||
*/
|
||||
async renderInto(node: HTMLElement): Promise<void> {
|
||||
if (getComputedStyle(node)?.position === "static") {
|
||||
throw new Error(
|
||||
"Webamp Error: The DOM node passed to renderInto must have a non-static position."
|
||||
);
|
||||
}
|
||||
return this._render(node, true);
|
||||
}
|
||||
|
||||
async _render(node: HTMLElement, contained: boolean): Promise<void> {
|
||||
this.store.dispatch(Actions.centerWindowsInContainer(node, contained));
|
||||
await this.skinIsLoaded();
|
||||
if (this._disposable.disposed) {
|
||||
return;
|
||||
|
|
@ -511,7 +532,7 @@ class Webamp {
|
|||
media={this.media}
|
||||
filePickers={this.options.filePickers || []}
|
||||
onMount={onMount}
|
||||
parentDomNode={document.body}
|
||||
parentDomNode={contained ? node : document.body}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue