mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 10:15:31 +00:00
parent
abc1495d9b
commit
b056b252d9
24 changed files with 878 additions and 5 deletions
|
|
@ -225,6 +225,7 @@
|
|||
#webamp .gen-window .gen-middle-right {background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAdCAYAAACXFC2jAAAALElEQVQ4T2PMyqr6f2D/MQYYcHC0YoDxP395y8A4qoBhNBwg6WM0HAZXOAAAZMQtu5vd5AgAAAAASUVORK5CYII=)}
|
||||
#webamp .gen-window .gen-middle-right-bottom {background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAYCAYAAADH2bwQAAAAKUlEQVQ4T2PMyqr6f2D/MQYYcHC0YoDxP395y8A4qoBhNBwg6WMEhQMAnDr5qQGVDEcAAAAASUVORK5CYII=)}
|
||||
#webamp .gen-window .gen-close:active {background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAfElEQVQoU2PcMjH5PwMBwAhStOPMA4bvjD8Yrl9+B1euqSvEwPmfg8HDRIEBrGj92RtgBUY6UmDFIMlzV54xgBQGGmtAFLUtOAZWAJIIt1VkWHn4PpxflWCFUATSpSfAz5A3cSvDpHxvhksfPoJNR1FE0CSi3ESU7wiFEwDes2XpVzKmTwAAAABJRU5ErkJggg==)}
|
||||
#webamp #webamp-media-library button {background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8AAAAPCAYAAAB0i5aaAAAAhUlEQVRIS2Ncu/XYf4YhCK7fv8/ACHK8k5flkHL+vm3HGZauW4tw/Lnz14eEBz48/8AACvVjx44NTcefO3+O4fy166OOp2tyAyWb0ZCna5BDLRsN+YEIdZCdoyE/qEIeVOUOFYBSVIIcDapyv394P1TcD6lhW6Ys/Q/yyY/vP4aMw2EOBQAnuriposTxowAAAABJRU5ErkJggg==)}
|
||||
#webamp .character-48 {background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAGCAYAAAAL+1RLAAAALklEQVQYV2NkYGD4z/CIAQLkIBQjwyOG/zAOWFIOWRBJNUQlTCuGSpAEXBCLRQAyQhABbALQ/gAAAABJRU5ErkJggg==)}
|
||||
#webamp .character-49 {background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAGCAYAAAAL+1RLAAAAJElEQVQYV2NkYGD4zwACjxgYGOTALAZGsCBIAARQBLGqJFsQAB97DAFASJPiAAAAAElFTkSuQmCC)}
|
||||
#webamp .character-50 {background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAGCAYAAAAL+1RLAAAAL0lEQVQYV2NkeMTwnwEG5CAMRgYGJMFHDAwMcngFQSpAAK4dZCaUAzOaEcUiqGoALSMMAep9mTsAAAAASUVORK5CYII=)}
|
||||
|
|
@ -415,3 +416,40 @@
|
|||
#webamp #volume {cursor: url(data:image/x-win-bitmap;base64,AAACAAEAICAAAAAAAADoAgAAFgAAACgAAAAgAAAAQAAAAAEABAAAAAAAgAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAwMDAAICAgAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHcAAAAAAAAAAAAAAAAAAAj3AAAAAAAAAAAAAAAAAACPcAAHAAAAAHAAAAAAAAAAj3AAdwAHcAB3AAAAAAgACPcAAP8AD/AA/wAAAAAIcAj3AAAPAAAAAPAAAAAACPcPcAAAAAAAAAAAAAAAAAj/cAAAAAAAAAAAAAAAAAAI//d3dwAAAAAAAAAAAAAACP/3d3AAAAAAAAAAAAAAAAj/d3cAAAAAAAAAAAAAAAAI/3dwAAAAAAAAAAAAAAAACPd3AAAAAAAAAAAAAAAAAAj3cAAAAAAAAAAAAAAAAAAIdwAAAAAAAAAAAAAAAAAACHAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////////////////////////////////////////////////////////////////////j////w////8M/P/+GMx/3hCEP8wwhD/EOMx/wHz8/8AH///AD///wB///8A////Af///wP///8H////D////x////8/////f////w==), auto}
|
||||
#webamp #volume input {cursor: url(data:image/x-win-bitmap;base64,AAACAAEAICAAAAAAAADoAgAAFgAAACgAAAAgAAAAQAAAAAEABAAAAAAAgAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAwMDAAICAgAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHcAAAAAAAAAAAAAAAAAAAj3AAAAAAAAAAAAAAAAAACPcAAHAAAAAHAAAAAAAAAAj3AAdwAHcAB3AAAAAAgACPcAAP8AD/AA/wAAAAAIcAj3AAAPAAAAAPAAAAAACPcPcAAAAAAAAAAAAAAAAAj/cAAAAAAAAAAAAAAAAAAI//d3dwAAAAAAAAAAAAAACP/3d3AAAAAAAAAAAAAAAAj/d3cAAAAAAAAAAAAAAAAI/3dwAAAAAAAAAAAAAAAACPd3AAAAAAAAAAAAAAAAAAj3cAAAAAAAAAAAAAAAAAAIdwAAAAAAAAAAAAAAAAAACHAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////////////////////////////////////////////////////////////////////j////w////8M/P/+GMx/3hCEP8wwhD/EOMx/wHz8/8AH///AD///wB///8A////Af///wP///8H////D////x////8/////f////w==), auto}
|
||||
#webamp #balance {cursor: url(data:image/x-win-bitmap;base64,AAACAAEAICAAAAAAAADoAgAAFgAAACgAAAAgAAAAQAAAAAEABAAAAAAAgAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAwMDAAICAgAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHcAAAAAAAAAAAAAAAAAAAj3AAAAAAAAAAAAAAAAAACPcAAHAAAAAHAAAAAAAAAAj3AAdwAHcAB3AAAAAAgACPcAAP8AD/AA/wAAAAAIcAj3AAAPAAAAAPAAAAAACPcPcAAAAAAAAAAAAAAAAAj/cAAAAAAAAAAAAAAAAAAI//d3dwAAAAAAAAAAAAAACP/3d3AAAAAAAAAAAAAAAAj/d3cAAAAAAAAAAAAAAAAI/3dwAAAAAAAAAAAAAAAACPd3AAAAAAAAAAAAAAAAAAj3cAAAAAAAAAAAAAAAAAAIdwAAAAAAAAAAAAAAAAAACHAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////////////////////////////////////////////////////////////////////j////w////8M/P/+GMx/3hCEP8wwhD/EOMx/wHz8/8AH///AD///wB///8A////Af///wP///8H////D////x////8/////f////w==), auto}
|
||||
#webamp-media-library {
|
||||
background-color: rgb(56,55,87);
|
||||
color: rgb(255,255,255);
|
||||
}
|
||||
#webamp-media-library input {
|
||||
caret-color: rgb(255,255,255);
|
||||
}
|
||||
#webamp-media-library .webamp-media-library-item {
|
||||
color: rgb(0,255,0);
|
||||
background-color: rgb(0,0,0);
|
||||
border-right: 1px solid rgb(117,116,139);
|
||||
border-bottom: 1px solid rgb(117,116,139);
|
||||
}
|
||||
#webamp-media-library button {
|
||||
color: rgb(57,57,66);
|
||||
}
|
||||
#webamp-media-library .webamp-media-library-vertical-divider {
|
||||
}
|
||||
#webamp-media-library .webamp-media-library-vertical-divider-line,
|
||||
#webamp-media-library .webamp-media-library-horizontal-divider-line
|
||||
{
|
||||
background-color: rgb(117,116,139);
|
||||
}
|
||||
#webamp-media-library .webamp-media-library-table {
|
||||
color: rgb(0,255,0);
|
||||
background-color: rgb(0,0,0);
|
||||
}
|
||||
#webamp-media-library .webamp-media-library-table thead {
|
||||
color: rgb(255,255,255);
|
||||
background-color: rgb(72,72,120);
|
||||
}
|
||||
#webamp-media-library .webamp-media-library-table thead th {
|
||||
border-top: 1px solid rgb(108,108,180);
|
||||
border-left: 1px solid rgb(108,108,180);
|
||||
border-bottom: 1px solid rgb(36,36,60);
|
||||
border-right: 1px solid rgb(36,36,60);
|
||||
}
|
||||
|
|
|
|||
102
css/media-library-window.css
Normal file
102
css/media-library-window.css
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
#webamp-media-library {
|
||||
font-size: 11px;
|
||||
font-family: "MS Sans Serif", "Segoe UI", sans-serif;
|
||||
-webkit-font-smoothing: none;
|
||||
padding-right: 2px;
|
||||
padding-bottom: 3px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-o-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
#webamp-media-library input {
|
||||
height: 15px;
|
||||
border: none;
|
||||
background-color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#webamp-media-library ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#webamp-media-library ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#webamp-media-library ul li ul {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#webamp-media-library .webamp-media-library-vertical-divider {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
#webamp-media-library .webamp-media-library-vertical-divider-line {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#webamp-media-library .webamp-media-library-horizontal-divider {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
#webamp-media-library .webamp-media-library-horizontal-divider-line {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#webamp-media-library .webamp-media-library-item {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#webamp-media-library .webamp-media-library-table .library-table-heading > div {
|
||||
margin-right: 1px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
#webamp-media-library
|
||||
.webamp-media-library-table
|
||||
.library-table-heading
|
||||
> div:active {
|
||||
margin-right: 0px;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 1px;
|
||||
margin-left: 1px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
#webamp-media-library .library-button {
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
height: 15px;
|
||||
cursor: inherit;
|
||||
display: inline-grid;
|
||||
grid-template-columns: 4px auto 4px;
|
||||
margin-right: 4px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
#webamp-media-library .library-button-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#webamp-media-library-track-summary-duration {
|
||||
/* This plus the margin-right on the button add up to 8px; */
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import React from "react";
|
|||
import ReactDOM from "react-dom";
|
||||
import createMiddleware from "raven-for-redux";
|
||||
import isButterchurnSupported from "butterchurn/lib/isSupported.min";
|
||||
import base from "../../skins/base-2.91-png.wsz";
|
||||
import { WINDOWS } from "../../js/constants";
|
||||
import * as Selectors from "../../js/selectors";
|
||||
|
||||
|
|
@ -53,9 +54,17 @@ const MIN_MILKDROP_WIDTH = 725;
|
|||
|
||||
let screenshot = false;
|
||||
let skinUrl = configSkinUrl;
|
||||
let library = false;
|
||||
if ("URLSearchParams" in window) {
|
||||
const params = new URLSearchParams(location.search);
|
||||
screenshot = params.get("screenshot");
|
||||
library = Boolean(params.get("library"));
|
||||
// The default skin CSS baked into the JS library does not have full Media
|
||||
// Library support. If we are going to show the library we have to load a
|
||||
// skin at start time.
|
||||
if (library && skinUrl == null) {
|
||||
skinUrl = base;
|
||||
}
|
||||
skinUrl = params.get("skinUrl") || skinUrl;
|
||||
}
|
||||
|
||||
|
|
@ -114,13 +123,21 @@ Raven.context(async () => {
|
|||
let __initialWindowLayout = null;
|
||||
if (isButterchurnSupported()) {
|
||||
const startWithMilkdropHidden =
|
||||
library ||
|
||||
document.body.clientWidth < MIN_MILKDROP_WIDTH ||
|
||||
skinUrl != null ||
|
||||
screenshot;
|
||||
|
||||
__butterchurnOptions = getButterchurnOptions(startWithMilkdropHidden);
|
||||
|
||||
if (startWithMilkdropHidden) {
|
||||
if (library) {
|
||||
__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.MEDIA_LIBRARY]: { position: { x: 275, y: 0 }, size: [7, 12] },
|
||||
};
|
||||
} else if (startWithMilkdropHidden) {
|
||||
__initialWindowLayout = {
|
||||
[WINDOWS.MAIN]: { position: { x: 0, y: 0 } },
|
||||
[WINDOWS.EQUALIZER]: { position: { x: 0, y: 116 } },
|
||||
|
|
@ -153,6 +170,7 @@ Raven.context(async () => {
|
|||
import(
|
||||
/* webpackChunkName: "music-metadata-browser" */ "music-metadata-browser/dist/index"
|
||||
),
|
||||
__enableMediaLibrary: library,
|
||||
__initialWindowLayout,
|
||||
__initialState: screenshot ? screenshotInitialState : initialState,
|
||||
__butterchurnOptions,
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export const LOAD_SERIALIZED_STATE = "LOAD_SERIALIZED_STATE";
|
|||
export const RESET_WINDOW_SIZES = "RESET_WINDOW_SIZES";
|
||||
export const BROWSER_WINDOW_SIZE_CHANGED = "BROWSER_WINDOW_SIZE_CHANGED";
|
||||
export const LOAD_DEFAULT_SKIN = "LOAD_DEFAULT_SKIN";
|
||||
export const ENABLE_MEDIA_LIBRARY = "ENABLE_MEDIA_LIBRARY";
|
||||
export const ENABLE_MILKDROP = "ENABLE_MILKDROP";
|
||||
export const SET_MILKDROP_DESKTOP = "SET_MILKDROP_DESKTOP";
|
||||
export const SET_VISUALIZER_STYLE = "SET_VISUALIZER_STYLE";
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import WindowManager from "./WindowManager";
|
|||
import MainWindow from "./MainWindow";
|
||||
import PlaylistWindow from "./PlaylistWindow";
|
||||
import EqualizerWindow from "./EqualizerWindow";
|
||||
import MediaLibraryWindow from "./MediaLibraryWindow";
|
||||
import Skin from "./Skin";
|
||||
|
||||
import "../../css/webamp.css";
|
||||
|
|
@ -122,6 +123,8 @@ class App extends React.Component<Props> {
|
|||
return <EqualizerWindow />;
|
||||
case WINDOWS.PLAYLIST:
|
||||
return <PlaylistWindow analyser={media.getAnalyser()} />;
|
||||
case WINDOWS.MEDIA_LIBRARY:
|
||||
return <MediaLibraryWindow />;
|
||||
case WINDOWS.MILKDROP:
|
||||
return <MilkdropWindow analyser={media.getAnalyser()} />;
|
||||
default:
|
||||
|
|
|
|||
17
js/components/MediaLibraryWindow/AlbumsTable.tsx
Normal file
17
js/components/MediaLibraryWindow/AlbumsTable.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import * as React from "react";
|
||||
import LibraryTable from "./LibraryTable";
|
||||
|
||||
const AlbumsTable = React.memo(() => {
|
||||
return (
|
||||
<LibraryTable
|
||||
headings={["Album", "Tracks"]}
|
||||
rows={[
|
||||
["All (1 album)", "1"],
|
||||
["Ben Mason", "1"],
|
||||
]}
|
||||
widths={[50, 200]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default AlbumsTable;
|
||||
19
js/components/MediaLibraryWindow/ArtistsTable.tsx
Normal file
19
js/components/MediaLibraryWindow/ArtistsTable.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import * as React from "react";
|
||||
import LibraryTable from "./LibraryTable";
|
||||
|
||||
interface Props {}
|
||||
|
||||
export default class ArtistsTable extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<LibraryTable
|
||||
headings={["Album", "Tracks", "Other"]}
|
||||
rows={[
|
||||
["All (1 album)", "1", "1"],
|
||||
["Ben Mason", "1", "1"],
|
||||
]}
|
||||
widths={[100, 150, 200]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
16
js/components/MediaLibraryWindow/LibraryButton.tsx
Normal file
16
js/components/MediaLibraryWindow/LibraryButton.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from "react";
|
||||
type Props = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
// TODO: This should be a `<button>` but I couldn't figure out how to style it with css grid
|
||||
const LibraryButton = (props: Props) => {
|
||||
const { children, ...passThroughProps } = props;
|
||||
return (
|
||||
<div className="library-button" {...passThroughProps}>
|
||||
<span className="library-button-left" />
|
||||
<span className="library-button-center">{children}</span>
|
||||
<span className="library-button-right" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryButton;
|
||||
176
js/components/MediaLibraryWindow/LibraryLayout.tsx
Normal file
176
js/components/MediaLibraryWindow/LibraryLayout.tsx
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import React, { ReactNode } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import GenWindow from "../GenWindow";
|
||||
import { WINDOWS } from "../../constants";
|
||||
import { AppState, SkinGenExColors } from "../../types";
|
||||
import * as Selectors from "../../selectors";
|
||||
|
||||
interface StateProps {
|
||||
skinGenExColors: SkinGenExColors;
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
sidebar: ReactNode;
|
||||
artists: ReactNode;
|
||||
albums: ReactNode;
|
||||
tracks: ReactNode;
|
||||
}
|
||||
|
||||
type Props = StateProps & OwnProps;
|
||||
|
||||
interface State {
|
||||
sidebarWidth: number;
|
||||
topPlaylistSectionHeight: number;
|
||||
artistsPanelWidth: number;
|
||||
}
|
||||
|
||||
const DIVIDER_WIDTH = 9;
|
||||
// TODO: Tune these
|
||||
const SIDEBAR_MIN = 25;
|
||||
const SIDEBAR_MAX = 200;
|
||||
|
||||
class LibraryLayout extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
sidebarWidth: 100, // Pixels
|
||||
topPlaylistSectionHeight: 0.25, // Percent
|
||||
artistsPanelWidth: 0.5, // Percent
|
||||
};
|
||||
}
|
||||
|
||||
_onMouseMove(cb: (e: MouseEvent) => void) {
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener("mousemove", cb);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
// TODO: Technically there's a leak here since the component could unmount while we are moving
|
||||
document.addEventListener("mousemove", cb);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
}
|
||||
|
||||
_handleSidebarMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const { pageX: startX } = e;
|
||||
const initialWidth = this.state.sidebarWidth;
|
||||
this._onMouseMove((moveEvent: MouseEvent) => {
|
||||
let sidebarWidth = initialWidth + moveEvent.pageX - startX;
|
||||
if (sidebarWidth < SIDEBAR_MIN) {
|
||||
sidebarWidth = 0;
|
||||
}
|
||||
sidebarWidth = Math.min(sidebarWidth, SIDEBAR_MAX);
|
||||
|
||||
this.setState({ sidebarWidth });
|
||||
});
|
||||
};
|
||||
|
||||
_handlePlaylistResizeMouseDown = (
|
||||
e: React.MouseEvent<HTMLDivElement>,
|
||||
windowHeight: number
|
||||
) => {
|
||||
const { pageY: startY } = e;
|
||||
const avaliableHeight = windowHeight - DIVIDER_WIDTH;
|
||||
const initialHeight = avaliableHeight * this.state.topPlaylistSectionHeight;
|
||||
|
||||
this._onMouseMove((moveEvent: MouseEvent) => {
|
||||
const deltaY = moveEvent.pageY - startY;
|
||||
const topPlaylistSectionPixelHeight = initialHeight + deltaY;
|
||||
this.setState({
|
||||
topPlaylistSectionHeight:
|
||||
topPlaylistSectionPixelHeight / avaliableHeight,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
_handleArtistsResizeMouseDown = (
|
||||
e: React.MouseEvent<HTMLDivElement>,
|
||||
windowWidth: number
|
||||
) => {
|
||||
const { pageX: startX } = e;
|
||||
const avaliableWidth =
|
||||
windowWidth - DIVIDER_WIDTH - this.state.sidebarWidth;
|
||||
const initialWidth = avaliableWidth * this.state.artistsPanelWidth;
|
||||
|
||||
this._onMouseMove((moveEvent: MouseEvent) => {
|
||||
const deltaX = moveEvent.pageX - startX;
|
||||
const artistsPanelPixelWidth = initialWidth + deltaX;
|
||||
this.setState({
|
||||
artistsPanelWidth: artistsPanelPixelWidth / avaliableWidth,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<GenWindow title={"Winamp Library"} windowId={WINDOWS.MEDIA_LIBRARY}>
|
||||
{({ width, height }) => (
|
||||
<div
|
||||
id="webamp-media-library"
|
||||
style={{
|
||||
// TODO: There's probably a better way to fill all avalaible space.
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: "grid",
|
||||
gridTemplateColumns: `${this.state.sidebarWidth}px ${DIVIDER_WIDTH}px auto`,
|
||||
}}
|
||||
>
|
||||
{this.state.sidebarWidth === 0 ? <div /> : this.props.sidebar}
|
||||
<div
|
||||
className="webamp-media-library-vertical-divider"
|
||||
onMouseDown={this._handleSidebarMouseDown}
|
||||
>
|
||||
<div className="webamp-media-library-vertical-divider-line" />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateRows: `${
|
||||
this.state.topPlaylistSectionHeight
|
||||
}fr ${DIVIDER_WIDTH}px ${1 -
|
||||
this.state.topPlaylistSectionHeight}fr`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: `${
|
||||
this.state.artistsPanelWidth
|
||||
}fr ${DIVIDER_WIDTH}px ${1 - this.state.artistsPanelWidth}fr`,
|
||||
}}
|
||||
>
|
||||
{this.props.artists}
|
||||
<div
|
||||
className="webamp-media-library-vertical-divider"
|
||||
onMouseDown={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
this._handleArtistsResizeMouseDown(e, width)
|
||||
}
|
||||
>
|
||||
<div className="webamp-media-library-vertical-divider-line" />
|
||||
</div>
|
||||
{this.props.albums}
|
||||
</div>
|
||||
<div
|
||||
className="webamp-media-library-horizontal-divider"
|
||||
onMouseDown={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
this._handlePlaylistResizeMouseDown(e, height)
|
||||
}
|
||||
>
|
||||
<div className="webamp-media-library-horizontal-divider-line" />
|
||||
</div>
|
||||
<div style={{ overflow: "hidden" }}>{this.props.tracks}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</GenWindow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState): StateProps => {
|
||||
return {
|
||||
skinGenExColors: Selectors.getSkinGenExColors(state),
|
||||
};
|
||||
};
|
||||
export default connect(mapStateToProps)(LibraryLayout);
|
||||
72
js/components/MediaLibraryWindow/LibraryTable.tsx
Normal file
72
js/components/MediaLibraryWindow/LibraryTable.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import React, { useState } from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
interface Props {
|
||||
headings: Array<string>;
|
||||
rows: Array<Array<any>>;
|
||||
widths: Array<number>;
|
||||
}
|
||||
|
||||
export default function LibraryTable(props: Props) {
|
||||
const [selectedRow, setSelectedRow] = useState<number | null>(null);
|
||||
const rowStyle = {
|
||||
display: "grid",
|
||||
gridTemplateColumns: props.widths.map(width => `${width}px`).join(" "),
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="webamp-media-library-item"
|
||||
style={{ height: "100%", position: "relative" }}
|
||||
>
|
||||
<div
|
||||
className="webamp-media-library-table"
|
||||
style={{
|
||||
overflow: "scroll",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div style={rowStyle} className="library-table-heading">
|
||||
{props.headings.map((heading, i) => (
|
||||
<div key={`heading-${i}-${heading}`} style={{ paddingLeft: 5 }}>
|
||||
{heading}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{props.rows.map((row, i) => (
|
||||
<div
|
||||
style={{
|
||||
...rowStyle,
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
className={classnames("library-table-row", {
|
||||
selected: i === selectedRow,
|
||||
})}
|
||||
onClick={() => setSelectedRow(i)}
|
||||
key={`row-${i}`}
|
||||
>
|
||||
{row.map((text, j) => (
|
||||
<div
|
||||
style={{ overflow: "hidden", paddingLeft: 6 }}
|
||||
key={`cell-${j}`}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
zIndex: 99999,
|
||||
color: "white",
|
||||
width: 1,
|
||||
position: "absolute",
|
||||
left: 50,
|
||||
top: 0,
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
js/components/MediaLibraryWindow/Sidebar.tsx
Normal file
45
js/components/MediaLibraryWindow/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import * as React from "react";
|
||||
import LibraryButton from "./LibraryButton";
|
||||
|
||||
export default class Sidebar extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div className="webamp-media-library-item" style={{ flexGrow: 1 }}>
|
||||
<ul style={{ margin: 3 }}>
|
||||
<li>
|
||||
Local Media
|
||||
<ul>
|
||||
<li>Audio</li>
|
||||
<li>Video</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Playlist</li>
|
||||
<li>
|
||||
Devices
|
||||
<ul>
|
||||
<li>CD E:</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Internet Radio</li>
|
||||
<li>Internet TV</li>
|
||||
</ul>
|
||||
</div>
|
||||
<LibraryButton
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: 1,
|
||||
}}
|
||||
>
|
||||
Library
|
||||
</LibraryButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
98
js/components/MediaLibraryWindow/TracksTable.tsx
Normal file
98
js/components/MediaLibraryWindow/TracksTable.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import * as Selectors from "../../selectors";
|
||||
import { AppState, PlaylistTrack } from "../../types";
|
||||
import * as Utils from "../../utils";
|
||||
import * as FileUtils from "../../fileUtils";
|
||||
import LibraryButton from "./LibraryButton";
|
||||
import LibraryTable from "./LibraryTable";
|
||||
|
||||
interface StateProps {
|
||||
tracks: PlaylistTrack[];
|
||||
filterTracks: (query: string) => PlaylistTrack[];
|
||||
}
|
||||
|
||||
interface State {
|
||||
filter: string;
|
||||
}
|
||||
|
||||
class TracksTable extends React.Component<StateProps, State> {
|
||||
constructor(props: StateProps) {
|
||||
super(props);
|
||||
this.state = { filter: "" };
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
height: 23,
|
||||
}}
|
||||
>
|
||||
<span>Search:</span>
|
||||
<input
|
||||
style={{ marginLeft: 12, flexGrow: 1 }}
|
||||
type="text"
|
||||
className="webamp-media-library-item"
|
||||
onChange={e => this.setState({ filter: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<LibraryTable
|
||||
headings={[
|
||||
"Artist",
|
||||
"Title",
|
||||
"Album",
|
||||
"Length",
|
||||
"Track #",
|
||||
"Genere",
|
||||
"Year",
|
||||
"Filename",
|
||||
]}
|
||||
rows={this.props.filterTracks(this.state.filter).map(track => {
|
||||
return [
|
||||
track.artist,
|
||||
track.title,
|
||||
track.album,
|
||||
Utils.getTimeStr(track.duration),
|
||||
1,
|
||||
"Primus",
|
||||
2001,
|
||||
track.url == null
|
||||
? track.defaultName
|
||||
: FileUtils.filenameFromUrl(track.url),
|
||||
];
|
||||
})}
|
||||
widths={[100, 100, 100, 100, 100, 100, 100, 100]}
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: 2 }}>
|
||||
<LibraryButton>Play</LibraryButton>
|
||||
<LibraryButton>Enqueue</LibraryButton>
|
||||
<LibraryButton>Play all</LibraryButton>
|
||||
<LibraryButton>Enqueue all</LibraryButton>
|
||||
<span id="webamp-media-library-track-summary-duration">
|
||||
1 item [3:25]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState): StateProps => {
|
||||
return {
|
||||
tracks: Object.values(Selectors.getTracks(state)),
|
||||
filterTracks: Selectors.getTracksMatchingFilter(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(TracksTable);
|
||||
20
js/components/MediaLibraryWindow/index.tsx
Normal file
20
js/components/MediaLibraryWindow/index.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import * as React from "react";
|
||||
import "../../../css/media-library-window.css";
|
||||
import Sidebar from "./Sidebar";
|
||||
import ArtistsTable from "./ArtistsTable";
|
||||
import AlbumsTable from "./AlbumsTable";
|
||||
import TracksTable from "./TracksTable";
|
||||
import LibraryLayout from "./LibraryLayout";
|
||||
|
||||
export default class MediaLibraryWindow extends React.Component<{}> {
|
||||
render() {
|
||||
return (
|
||||
<LibraryLayout
|
||||
sidebar={<Sidebar />}
|
||||
artists={<ArtistsTable />}
|
||||
albums={<AlbumsTable />}
|
||||
tracks={<TracksTable />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -135,6 +135,71 @@ function cssRulesFromProps(props) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Find a way to make this declarative.
|
||||
cssRules.push(
|
||||
`#webamp-media-library {
|
||||
background-color: ${props.skinGenExColors.windowBackground};
|
||||
color: ${props.skinGenExColors.windowText};
|
||||
}`
|
||||
);
|
||||
cssRules.push(
|
||||
`#webamp-media-library input {
|
||||
caret-color: ${props.skinGenExColors.windowText};
|
||||
}`
|
||||
);
|
||||
cssRules.push(
|
||||
`#webamp-media-library .webamp-media-library-item {
|
||||
color: ${props.skinGenExColors.itemForeground};
|
||||
background-color: ${props.skinGenExColors.itemBackground};
|
||||
border-right: 1px solid ${props.skinGenExColors.divider};
|
||||
border-bottom: 1px solid ${props.skinGenExColors.divider};
|
||||
}`
|
||||
);
|
||||
cssRules.push(
|
||||
`#webamp-media-library .library-button-center {
|
||||
color: ${props.skinGenExColors.buttonText};
|
||||
}`
|
||||
);
|
||||
cssRules.push(
|
||||
`#webamp-media-library .webamp-media-library-vertical-divider {
|
||||
}`
|
||||
);
|
||||
cssRules.push(
|
||||
`#webamp-media-library .webamp-media-library-vertical-divider-line,
|
||||
#webamp-media-library .webamp-media-library-horizontal-divider-line
|
||||
{
|
||||
background-color: ${props.skinGenExColors.divider};
|
||||
}`
|
||||
);
|
||||
cssRules.push(
|
||||
`#webamp-media-library .webamp-media-library-table {
|
||||
color: ${props.skinGenExColors.itemForeground};
|
||||
background-color: ${props.skinGenExColors.itemBackground};
|
||||
}`
|
||||
);
|
||||
|
||||
cssRules.push(
|
||||
`#webamp-media-library .webamp-media-library-table .library-table-heading {
|
||||
background-color: ${props.skinGenExColors.listHeaderFramePressed};
|
||||
}`
|
||||
);
|
||||
|
||||
cssRules.push(
|
||||
`#webamp-media-library .webamp-media-library-table .library-table-heading > div {
|
||||
color: ${props.skinGenExColors.listHeaderText};
|
||||
background-color: ${props.skinGenExColors.listHeaderBackground};
|
||||
border-top: 1px solid ${props.skinGenExColors.listHeaderFrameTopAndLeft};
|
||||
border-left: 1px solid ${props.skinGenExColors.listHeaderFrameTopAndLeft};
|
||||
border-bottom: 1px solid ${props.skinGenExColors.listHeaderFrameBottomAndRight};
|
||||
border-right: 1px solid ${props.skinGenExColors.listHeaderFrameBottomAndRight};
|
||||
}`
|
||||
);
|
||||
cssRules.push(
|
||||
`#webamp-media-library .webamp-media-library-table .library-table-row.selected {
|
||||
background-color: ${props.skinGenExColors.playlistSelection};
|
||||
color: ${props.skinGenExColors.listHeaderText};
|
||||
}`
|
||||
);
|
||||
return cssRules;
|
||||
}
|
||||
|
||||
|
|
@ -168,4 +233,5 @@ export default connect(state => ({
|
|||
skinCursors: state.display.skinCursors,
|
||||
skinRegion: state.display.skinRegion,
|
||||
skinGenLetterWidths: state.display.skinGenLetterWidths,
|
||||
skinGenExColors: state.display.skinGenExColors,
|
||||
}))(Skin);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export const WINDOWS = {
|
|||
MAIN: "main",
|
||||
PLAYLIST: "playlist",
|
||||
EQUALIZER: "equalizer",
|
||||
MEDIA_LIBRARY: "mediaLibrary",
|
||||
MILKDROP: "milkdrop",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
LOAD_SERIALIZED_STATE,
|
||||
BROWSER_WINDOW_SIZE_CHANGED,
|
||||
RESET_WINDOW_SIZES,
|
||||
ENABLE_MEDIA_LIBRARY,
|
||||
ENABLE_MILKDROP,
|
||||
} from "../actionTypes";
|
||||
import * as Utils from "../utils";
|
||||
|
|
@ -93,6 +94,7 @@ const defaultWindowsState: WindowsState = {
|
|||
WINDOWS.PLAYLIST,
|
||||
WINDOWS.EQUALIZER,
|
||||
WINDOWS.MILKDROP,
|
||||
WINDOWS.MEDIA_LIBRARY,
|
||||
WINDOWS.MAIN,
|
||||
],
|
||||
};
|
||||
|
|
@ -102,6 +104,25 @@ const windows = (
|
|||
action: Action
|
||||
): WindowsState => {
|
||||
switch (action.type) {
|
||||
case ENABLE_MEDIA_LIBRARY:
|
||||
return {
|
||||
...state,
|
||||
genWindows: {
|
||||
...state.genWindows,
|
||||
[WINDOWS.MEDIA_LIBRARY]: {
|
||||
title: "Library",
|
||||
size: [0, 0],
|
||||
open: true,
|
||||
hidden: false,
|
||||
shade: false,
|
||||
canResize: true,
|
||||
canShade: false,
|
||||
canDouble: false,
|
||||
hotkey: "Alt+E",
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
},
|
||||
};
|
||||
case ENABLE_MILKDROP:
|
||||
return {
|
||||
...state,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import * as fromDisplay from "./reducers/display";
|
|||
import * as fromEqualizer from "./reducers/equalizer";
|
||||
import * as fromMedia from "./reducers/media";
|
||||
import * as fromWindows from "./reducers/windows";
|
||||
import * as TrackUtils from "./trackUtils";
|
||||
import * as MarqueeUtils from "./marqueeUtils";
|
||||
import { generateGraph } from "./resizeUtils";
|
||||
import { SerializedStateV1 } from "./serializedStates/v1Types";
|
||||
|
|
@ -52,6 +53,17 @@ export const getEqfData = createSelector(getSliders, sliders => {
|
|||
});
|
||||
|
||||
export const getTracks = (state: AppState) => state.tracks;
|
||||
|
||||
export const getTracksMatchingFilter = createSelector(getTracks, tracks => {
|
||||
const tracksArray = Object.values(tracks);
|
||||
const filter = Utils.makeCachingFilterFunction(tracksArray, (track, query) =>
|
||||
TrackUtils.trackFilterContents(track).includes(query)
|
||||
);
|
||||
return (filterString: string): PlaylistTrack[] => {
|
||||
return filter(filterString.toLowerCase());
|
||||
};
|
||||
});
|
||||
|
||||
export const getTrackUrl = (state: AppState) => {
|
||||
return (id: number): string | null => {
|
||||
return state.tracks[id]?.url;
|
||||
|
|
@ -483,6 +495,10 @@ export const getSkinPlaylistStyle = (state: AppState): PlaylistStyle => {
|
|||
return state.display.skinPlaylistStyle || defaultPlaylistStyle;
|
||||
};
|
||||
|
||||
export const getSkinGenExColors = (state: AppState) => {
|
||||
return state.display.skinGenExColors;
|
||||
};
|
||||
|
||||
export const getVisualizerStyle = (state: AppState): string => {
|
||||
const milkdrop = state.windows.genWindows[WINDOWS.MILKDROP];
|
||||
if (milkdrop != null && milkdrop.open) {
|
||||
|
|
|
|||
|
|
@ -318,6 +318,15 @@ export const imageSelectors: Selectors = {
|
|||
GEN_MIDDLE_RIGHT: [".gen-window .gen-middle-right"],
|
||||
GEN_MIDDLE_RIGHT_BOTTOM: [".gen-window .gen-middle-right-bottom"],
|
||||
GEN_CLOSE_SELECTED: [".gen-window .gen-close:active"],
|
||||
GENEX_BUTTON_BACKGROUND_LEFT_UNPRESSED: [
|
||||
"#webamp-media-library .library-button-left",
|
||||
],
|
||||
GENEX_BUTTON_BACKGROUND_CENTER_UNPRESSED: [
|
||||
"#webamp-media-library .library-button-center",
|
||||
],
|
||||
GENEX_BUTTON_BACKGROUND_RIGHT_UNPRESSED: [
|
||||
"#webamp-media-library .library-button-right",
|
||||
],
|
||||
};
|
||||
|
||||
Object.keys(FONT_LOOKUP).forEach(character => {
|
||||
|
|
|
|||
|
|
@ -738,9 +738,6 @@ const sprites: SpriteMap = {
|
|||
{ name: "GEN_MIDDLE_RIGHT_BOTTOM", x: 170, y: 42, width: 8, height: 24 },
|
||||
{ name: "GEN_CLOSE_SELECTED", x: 148, y: 42, width: 9, height: 9 },
|
||||
],
|
||||
/*
|
||||
|
||||
We don't currently support the Media Library, so there are disabled
|
||||
GENEX: [
|
||||
{
|
||||
name: "GENEX_BUTTON_BACKGROUND_LEFT_UNPRESSED",
|
||||
|
|
@ -819,7 +816,6 @@ const sprites: SpriteMap = {
|
|||
width: 28,
|
||||
},
|
||||
],
|
||||
*/
|
||||
};
|
||||
|
||||
export default sprites;
|
||||
|
|
|
|||
|
|
@ -35,3 +35,12 @@ export const trackFilename = Utils.weakMapMemoize(
|
|||
return "???";
|
||||
}
|
||||
);
|
||||
|
||||
export const trackFilterContents = Utils.weakMapMemoize(
|
||||
(track: PlaylistTrack): string => {
|
||||
return [track.artist, track.title, track.defaultName]
|
||||
.filter(Boolean)
|
||||
.join("|")
|
||||
.toLowerCase();
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -496,6 +496,7 @@ export type Action =
|
|||
| { type: "RESET_WINDOW_SIZES" }
|
||||
| { type: "BROWSER_WINDOW_SIZE_CHANGED"; height: number; width: number }
|
||||
| { type: "LOAD_DEFAULT_SKIN" }
|
||||
| { type: "ENABLE_MEDIA_LIBRARY" }
|
||||
| { type: "ENABLE_MILKDROP"; open: boolean }
|
||||
| { type: "SCHEDULE_MILKDROP_MESSAGE"; message: string }
|
||||
| {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
segment,
|
||||
moveSelected,
|
||||
spliceIn,
|
||||
makeCachingFilterFunction,
|
||||
replaceAtIndex,
|
||||
} from "./utils";
|
||||
|
||||
|
|
@ -380,6 +381,92 @@ describe("spliceIn", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("makeCachingFilterFunction", () => {
|
||||
test("caches exact queries", () => {
|
||||
const values = ["abc", "b", "c"];
|
||||
const includes = jest.fn((v, query) => v.includes(query));
|
||||
const filter = makeCachingFilterFunction(values, includes);
|
||||
expect(filter("c")).toEqual(["abc", "c"]);
|
||||
expect(includes.mock.calls.length).toBe(3);
|
||||
expect(filter("c")).toEqual(["abc", "c"]);
|
||||
expect(includes.mock.calls.length).toBe(3);
|
||||
});
|
||||
|
||||
test("caches sub queries", () => {
|
||||
const values = ["a--", "ab-", "abc"];
|
||||
const includes = jest.fn((v, query) => v.includes(query));
|
||||
let comparisons = 0;
|
||||
const newComparisons = () => {
|
||||
const recent = includes.mock.calls.length - comparisons;
|
||||
comparisons += recent;
|
||||
return recent;
|
||||
};
|
||||
const filter = makeCachingFilterFunction(values, includes);
|
||||
// Intial search
|
||||
expect(filter("ab")).toEqual(["ab-", "abc"]);
|
||||
expect(newComparisons()).toBe(3); // Looks at all elements
|
||||
|
||||
// Second search where original search is a prefix
|
||||
expect(filter("abc")).toEqual(["abc"]);
|
||||
expect(newComparisons()).toBe(2); // Only reconsiders the previous matches
|
||||
|
||||
// Unique search
|
||||
expect(filter("b")).toEqual(["ab-", "abc"]); // Looks at all elements
|
||||
expect(newComparisons()).toBe(3); // Reconsiders all elements
|
||||
|
||||
expect(filter("bc")).toEqual(["abc"]); // Only reconsidres the matches that already include `b`
|
||||
expect(newComparisons()).toBe(2);
|
||||
|
||||
// Go back to the initial serach
|
||||
expect(filter("ab")).toEqual(["ab-", "abc"]);
|
||||
expect(newComparisons()).toBe(0); // Result is cached
|
||||
|
||||
// A variation on the second search
|
||||
expect(filter("abcd")).toEqual([]);
|
||||
expect(newComparisons()).toBe(1); // Only recondsiders the results of `abc`
|
||||
});
|
||||
|
||||
test("big data", () => {
|
||||
const values = [...Array(10000)].map((val, i) => String(i));
|
||||
const includes = jest.fn((v, query) => v.includes(query));
|
||||
let comparisons = 0;
|
||||
const newComparisons = () => {
|
||||
const recent = includes.mock.calls.length - comparisons;
|
||||
comparisons += recent;
|
||||
return recent;
|
||||
};
|
||||
const filter = makeCachingFilterFunction(values, includes);
|
||||
// Intial search
|
||||
expect(filter("").length).toEqual(10000);
|
||||
expect(newComparisons()).toBe(0); // Looks at zero
|
||||
|
||||
expect(filter("1").length).toEqual(3439);
|
||||
expect(newComparisons()).toBe(10000); // Looks at all elements
|
||||
|
||||
expect(filter("12").length).toEqual(299);
|
||||
expect(newComparisons()).toBe(3439);
|
||||
|
||||
expect(filter("123").length).toEqual(20);
|
||||
expect(newComparisons()).toBe(299);
|
||||
|
||||
expect(filter("1234").length).toEqual(1);
|
||||
expect(newComparisons()).toBe(20);
|
||||
|
||||
expect(filter("12345").length).toEqual(0);
|
||||
expect(newComparisons()).toBe(1);
|
||||
|
||||
// A variation on the initial non-empty query
|
||||
expect(filter("11").length).toEqual(280);
|
||||
expect(newComparisons()).toBe(3439);
|
||||
|
||||
expect(filter("111").length).toEqual(19);
|
||||
expect(newComparisons()).toBe(280);
|
||||
|
||||
expect(filter("1111").length).toEqual(1);
|
||||
expect(newComparisons()).toBe(19);
|
||||
});
|
||||
});
|
||||
|
||||
describe("replaceAtIndex", () => {
|
||||
test("can replace", () => {
|
||||
expect(replaceAtIndex([1, 2, 3, 4], 2, 0)).toEqual([1, 2, 0, 4]);
|
||||
|
|
|
|||
36
js/utils.ts
36
js/utils.ts
|
|
@ -403,3 +403,39 @@ export function weakMapMemoize<T extends object, R>(
|
|||
return cache.get(value);
|
||||
};
|
||||
}
|
||||
|
||||
interface Cache<V> {
|
||||
results?: V[];
|
||||
subCaches: { [char: string]: Cache<V> };
|
||||
}
|
||||
|
||||
// Is this a premature optimizaiton? Probably. But it's my side-project so I can
|
||||
// do what I like. :P
|
||||
export function makeCachingFilterFunction<V>(
|
||||
values: V[],
|
||||
includes: (v: V, query: string) => boolean
|
||||
) {
|
||||
const cache: Cache<V> = {
|
||||
results: values,
|
||||
subCaches: {},
|
||||
};
|
||||
return (query: string): V[] => {
|
||||
let queryCache: Cache<V> = cache;
|
||||
let lastResults: V[] = values;
|
||||
for (const char of query) {
|
||||
let letterCaches = queryCache.subCaches[char];
|
||||
if (!letterCaches) {
|
||||
letterCaches = queryCache.subCaches[char] = { subCaches: {} };
|
||||
} else if (letterCaches.results) {
|
||||
lastResults = letterCaches.results;
|
||||
}
|
||||
queryCache = letterCaches;
|
||||
}
|
||||
|
||||
if (!queryCache.results) {
|
||||
queryCache.results = lastResults.filter(v => includes(v, query));
|
||||
}
|
||||
|
||||
return queryCache.results;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import {
|
|||
LOADED,
|
||||
SET_Z_INDEX,
|
||||
CLOSE_REQUESTED,
|
||||
ENABLE_MEDIA_LIBRARY,
|
||||
ENABLE_MILKDROP,
|
||||
} from "./actionTypes";
|
||||
import Emitter from "./emitter";
|
||||
|
|
@ -116,6 +117,7 @@ interface PrivateOptions {
|
|||
requireMusicMetadata(): Promise<any>; // TODO: Type musicmetadata
|
||||
__initialState?: AppState;
|
||||
__customMiddlewares?: Middleware[];
|
||||
__enableMediaLibrary?: boolean;
|
||||
__initialWindowLayout: {
|
||||
[windowId: string]: {
|
||||
size: null | [number, number];
|
||||
|
|
@ -239,6 +241,10 @@ class Winamp {
|
|||
);
|
||||
}
|
||||
|
||||
if (options.__enableMediaLibrary) {
|
||||
this.store.dispatch({ type: ENABLE_MEDIA_LIBRARY });
|
||||
}
|
||||
|
||||
const handleOnline = () => this.store.dispatch({ type: NETWORK_CONNECTED });
|
||||
const handleOffline = () =>
|
||||
this.store.dispatch({ type: NETWORK_DISCONNECTED });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue