First draft of public API to configure window layout

This commit is contained in:
Jordan Eldredge 2023-02-06 00:46:16 -08:00
parent dcaf517f86
commit 17f0a393f7
8 changed files with 140 additions and 46 deletions

View file

@ -62,7 +62,7 @@ export async function getWebampConfig(
soundCloudPlaylist: SoundCloud.SoundCloudPlaylist | null
): Promise<Options & PrivateOptions> {
let __butterchurnOptions;
let __initialWindowLayout: WindowLayout | undefined;
let windowLayout: WindowLayout | undefined;
if (isButterchurnSupported()) {
const startWithMilkdropHidden = skinUrl != null || screenshot;
@ -72,18 +72,30 @@ export async function getWebampConfig(
startWithMilkdropHidden ||
document.body.clientWidth < MIN_MILKDROP_WIDTH
) {
__initialWindowLayout = {
[WINDOWS.MAIN]: { position: { x: 0, y: 0 } },
[WINDOWS.EQUALIZER]: { position: { x: 0, y: 116 } },
[WINDOWS.PLAYLIST]: { position: { x: 0, y: 232 }, size: [0, 0] },
[WINDOWS.MILKDROP]: { position: { x: 0, y: 348 }, size: [0, 0] },
windowLayout = {
main: { position: { left: 0, top: 0 } },
equalizer: { position: { left: 0, top: 116 } },
playlist: {
position: { left: 0, top: 232 },
size: { extraHeight: 0, extraWidth: 0 },
},
milkdrop: {
position: { left: 0, top: 348 },
size: { extraHeight: 0, extraWidth: 0 },
},
};
} else {
__initialWindowLayout = {
[WINDOWS.MAIN]: { position: { x: 0, y: 0 } },
[WINDOWS.EQUALIZER]: { position: { x: 0, y: 116 } },
[WINDOWS.PLAYLIST]: { position: { x: 0, y: 232 }, size: [0, 4] },
[WINDOWS.MILKDROP]: { position: { x: 275, y: 0 }, size: [7, 12] },
windowLayout = {
main: { position: { left: 0, top: 0 } },
equalizer: { position: { left: 0, top: 116 } },
playlist: {
position: { left: 0, top: 232 },
size: { extraHeight: 4, extraWidth: 0 },
},
milkdrop: {
position: { left: 275, top: 0 },
size: { extraHeight: 12, extraWidth: 7 },
},
};
}
}
@ -99,6 +111,7 @@ export async function getWebampConfig(
? SoundCloud.tracksFromPlaylist(soundCloudPlaylist)
: initialTracks,
availableSkins,
windowLayout,
filePickers: [dropboxFilePicker],
enableHotkeys: true,
handleTrackDropEvent: (e) => {
@ -120,7 +133,6 @@ export async function getWebampConfig(
import(
/* webpackChunkName: "music-metadata-browser" */ "music-metadata-browser/dist/index"
),
__initialWindowLayout,
__initialState: screenshot ? screenshotInitialState : initialState,
__butterchurnOptions,
__customMiddlewares: [sentryMiddleware, loggerMiddleware],

View file

@ -38,6 +38,7 @@ export {
stackWindows,
toggleLlamaMode,
setFocusedWindow,
setWindowLayout,
} from "./windows";
export {
play,

View file

@ -16,13 +16,20 @@ import {
import { getPositionDiff, SizeDiff } from "../resizeUtils";
import { applyDiff } from "../snapUtils";
import { Action, Thunk, WindowId, WindowPositions, Dispatch } from "../types";
import {
Action,
Thunk,
WindowId,
WindowPositions,
Dispatch,
WindowLayout,
} from "../types";
// Dispatch an action and, if needed rearrange the windows to preserve
// the existing edge relationship.
//
// Works by checking the edges before the action is dispatched. Then,
// after disatching, calculating what position change would be required
// after dispatching, calculating what position change would be required
// to restore those relationships.
function withWindowGraphIntegrity(action: Action): Thunk {
return (dispatch, getState) => {
@ -197,6 +204,45 @@ export function stackWindows(): Thunk {
};
}
export function setWindowLayout(layout?: WindowLayout): Thunk {
return (dispatch) => {
if (layout == null) {
dispatch(stackWindows());
return;
}
for (const id of ["playlist", "milkdrop"] as const) {
const w = layout[id];
if (w != null && w.size != null) {
const { extraHeight: plusHeight, extraWidth: plusWidth } = w.size;
dispatch(setWindowSize(id, [plusWidth, plusHeight]));
}
}
for (const id of ["main", "playlist", "equalizer", "milkdrop"] as const) {
const w = layout[id];
if (w == null || w.closed) {
dispatch(closeWindow(id));
}
}
for (const id of ["main", "playlist", "equalizer"] as const) {
if (layout[id]?.shadeMode) {
dispatch({
type: TOGGLE_WINDOW_SHADE_MODE,
windowId: id,
});
}
}
dispatch(
updateWindowPositions(
Utils.objectMap(layout, (w) => ({
x: w.position.left,
y: w.position.top,
})),
false
)
);
};
}
export function ensureWindowsAreOnScreen(): Thunk {
return (dispatch, getState) => {
const state = getState();

View file

@ -4,13 +4,14 @@ import {
MediaStatus,
LoadStyle,
TimeMode,
WindowId,
} from "./types";
import baseSkin from "./baseSkin.json";
export const BANDS: Band[] = [
60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000,
];
export const WINDOWS = {
export const WINDOWS: { [key: string]: WindowId } = {
MAIN: "main",
PLAYLIST: "playlist",
EQUALIZER: "equalizer",

View file

@ -15,10 +15,8 @@ import {
import * as Utils from "../utils";
import { WindowsSerializedStateV1 } from "../serializedStates/v1Types";
export type WindowPosition = Point;
export type WindowPositions = {
[windowId: string]: WindowPosition;
[windowId: string]: Point;
};
export interface WebampWindow {
@ -30,7 +28,7 @@ export interface WebampWindow {
canShade: boolean;
canDouble: boolean;
hotkey?: string;
position: WindowPosition;
position: Point;
}
export interface WindowInfo extends Box {
@ -48,7 +46,7 @@ const defaultWindowsState: WindowsState = {
focused: WINDOWS.MAIN,
positionsAreRelative: true,
genWindows: {
// TODO: Remove static capabilites and derive them from ids/generic
// TODO: Remove static capabilities and derive them from ids/generic
[WINDOWS.MAIN]: {
title: "Main Window",
size: [0, 0],

View file

@ -2,6 +2,8 @@
* NOTE: This file must not import any other types
*/
import { WindowId } from "../types";
export interface WindowsSerializedStateV1 {
positionsAreRelative: boolean;
genWindows: {
@ -13,7 +15,7 @@ export interface WindowsSerializedStateV1 {
position: { x: number; y: number };
};
};
focused: string | null;
focused: WindowId | null;
}
export interface DisplaySerializedStateV1 {

View file

@ -9,7 +9,6 @@ import {
WindowPositions as _WindowPositions,
WebampWindow as _WebampWindow,
WindowInfo as _WindowInfo,
WindowPosition as _WindowPosition,
} from "./reducers/windows";
import { EqualizerState } from "./reducers/equalizer";
import { NetworkState } from "./reducers/network";
@ -22,7 +21,6 @@ import { Store as ReduxStore } from "redux";
// Avoid warnings from Webpack: https://github.com/webpack/webpack/issues/7378
export type WebampWindow = _WebampWindow;
export type WindowInfo = _WindowInfo;
export type WindowPosition = _WindowPosition;
export type WindowPositions = _WindowPositions;
export interface Point {
@ -149,7 +147,7 @@ export interface SkinGenExColors {
listTextSelectedBackground: string;
}
export type WindowId = string;
export type WindowId = "main" | "playlist" | "equalizer" | "milkdrop";
// TODO: Fill these out once we actually use them.
export type SkinData = {
@ -659,6 +657,8 @@ export interface Options {
*/
availableSkins?: { url: string; name: string }[];
windowLayout?: WindowLayout;
/**
* Should global hotkeys be enabled?
*
@ -671,7 +671,7 @@ export interface Options {
*
* These will appear in the "Options" menu under "Play".
*
* In the offical version, this option is used to provide a "Dropbox" file picker.
* For example, this option can be used to provide a "Dropbox" file picker.
*/
filePickers?: [
{
@ -704,11 +704,63 @@ export interface Options {
handleSaveListEvent?: (tracks: Track[]) => null | Promise<null>;
}
/**
* Specifies the initial position and size of a the Winamp windows.
*
* Positions are specified in pixels from the top left corner of an imaginary
* box. On initial render, the collection of visible windows will be centered
* within the HTML element passed to `Webamp.renderWhenReady(element)`. In other
* words, the positions given here will determine the _relative_ position of the
* windows. The absolute position will be determined by the HTML element in
* which Webamp is centered.
*
* Enabling "shade mode" for a window that supports it, will cause it to be
* rendered minimized. Be default windows are not in shade mode.
*
* Windows which support resizing can have their size specified. If omitted,
* they default to their small base size.
*
* Windows that are omitted will start closed. Enabling "closed" for a window
* that supports it, will cause it to start closed.
*/
export type WindowLayout = {
[windowId: string]: {
size?: null | [number, number];
main?: {
position: WindowPosition;
shadeMode?: boolean;
closed?: boolean;
};
equalizer?: {
position: WindowPosition;
shadeMode?: boolean;
closed?: boolean;
};
playlist?: {
position: WindowPosition;
shadeMode?: boolean;
size?: WindowSize | null;
closed?: boolean;
};
milkdrop?: {
position: WindowPosition;
size?: WindowSize | null;
closed?: boolean;
};
};
export type WindowPosition = { top: number; left: number };
/**
* Resizable windows in Winamp have a base size and can be expanded in
* increments based on the size of the skin sprite.
*
* To specify a window being larger than its base size, use `extraHeight` to
* specify how many sprite increments to expand the window's height by, and
* `extraWidth` to specify how many sprite increments to expand the window's
* width by.
*/
export type WindowSize = {
extraHeight: number;
extraWidth: number;
};
/**

View file

@ -10,7 +10,6 @@ import {
Middleware,
ButterchurnOptions,
PartialState,
WindowLayout,
Options,
MediaStatus,
} from "./types";
@ -22,7 +21,6 @@ import * as Selectors from "./selectors";
import * as Actions from "./actionCreators";
import { LOAD_STYLE } from "./constants";
import * as Utils from "./utils";
import * as FileUtils from "./fileUtils";
import {
@ -46,7 +44,6 @@ export interface PrivateOptions {
requireMusicMetadata(): Promise<any>; // TODO: Type musicmetadata
__initialState?: PartialState;
__customMiddlewares?: Middleware[];
__initialWindowLayout?: WindowLayout;
__butterchurnOptions?: ButterchurnOptions;
// This is used by https://winampify.io/ to proxy through to Spotify's API.
__customMediaClass?: typeof Media; // This should have the same interface as Media
@ -189,22 +186,7 @@ class Webamp {
this.store.dispatch({ type: SET_AVAILABLE_SKINS, skins: availableSkins });
}
const layout = options.__initialWindowLayout;
if (layout == null) {
this.store.dispatch(Actions.stackWindows());
} else {
Utils.objectForEach(layout, (w, windowId) => {
if (w.size != null) {
this.store.dispatch(Actions.setWindowSize(windowId, w.size));
}
});
this.store.dispatch(
Actions.updateWindowPositions(
Utils.objectMap(layout, (w) => w.position),
false
)
);
}
this.store.dispatch(Actions.setWindowLayout(options.windowLayout));
if (enableHotkeys) {
this._disposable.add(bindHotkeys(this.store.dispatch));