diff --git a/packages/webamp/demo/js/webampConfig.ts b/packages/webamp/demo/js/webampConfig.ts index 12249a73..dc2f2c2b 100644 --- a/packages/webamp/demo/js/webampConfig.ts +++ b/packages/webamp/demo/js/webampConfig.ts @@ -62,7 +62,7 @@ export async function getWebampConfig( soundCloudPlaylist: SoundCloud.SoundCloudPlaylist | null ): Promise { 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], diff --git a/packages/webamp/js/actionCreators/index.ts b/packages/webamp/js/actionCreators/index.ts index 81d79e2a..65631fa5 100644 --- a/packages/webamp/js/actionCreators/index.ts +++ b/packages/webamp/js/actionCreators/index.ts @@ -38,6 +38,7 @@ export { stackWindows, toggleLlamaMode, setFocusedWindow, + setWindowLayout, } from "./windows"; export { play, diff --git a/packages/webamp/js/actionCreators/windows.ts b/packages/webamp/js/actionCreators/windows.ts index a45e9bdc..dd7e6ac9 100644 --- a/packages/webamp/js/actionCreators/windows.ts +++ b/packages/webamp/js/actionCreators/windows.ts @@ -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(); diff --git a/packages/webamp/js/constants.ts b/packages/webamp/js/constants.ts index d5a7ff77..190ced99 100644 --- a/packages/webamp/js/constants.ts +++ b/packages/webamp/js/constants.ts @@ -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", diff --git a/packages/webamp/js/reducers/windows.ts b/packages/webamp/js/reducers/windows.ts index 5ff8f447..274b9d73 100644 --- a/packages/webamp/js/reducers/windows.ts +++ b/packages/webamp/js/reducers/windows.ts @@ -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], diff --git a/packages/webamp/js/serializedStates/v1Types.ts b/packages/webamp/js/serializedStates/v1Types.ts index b876d990..60d301eb 100644 --- a/packages/webamp/js/serializedStates/v1Types.ts +++ b/packages/webamp/js/serializedStates/v1Types.ts @@ -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 { diff --git a/packages/webamp/js/types.ts b/packages/webamp/js/types.ts index 5a56eb60..2fd95960 100644 --- a/packages/webamp/js/types.ts +++ b/packages/webamp/js/types.ts @@ -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; } +/** + * 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; }; /** diff --git a/packages/webamp/js/webampLazy.tsx b/packages/webamp/js/webampLazy.tsx index 92cb866d..b5a6c80c 100644 --- a/packages/webamp/js/webampLazy.tsx +++ b/packages/webamp/js/webampLazy.tsx @@ -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; // 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));