Revert "Rm media library (#957)"

This reverts commit d24b86e189.
This commit is contained in:
Jordan Eldredge 2019-12-06 08:09:34 -08:00
parent abc1495d9b
commit b056b252d9
24 changed files with 878 additions and 5 deletions

View file

@ -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);
}

View 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;
}

View file

@ -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,

View file

@ -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";

View file

@ -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:

View 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;

View 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]}
/>
);
}
}

View 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;

View 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);

View 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>
);
}

View 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>
);
}
}

View 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);

View 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 />}
/>
);
}
}

View file

@ -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);

View file

@ -23,6 +23,7 @@ export const WINDOWS = {
MAIN: "main",
PLAYLIST: "playlist",
EQUALIZER: "equalizer",
MEDIA_LIBRARY: "mediaLibrary",
MILKDROP: "milkdrop",
};

View file

@ -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,

View file

@ -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) {

View file

@ -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 => {

View file

@ -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;

View file

@ -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();
}
);

View file

@ -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 }
| {

View file

@ -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]);

View file

@ -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;
};
}

View file

@ -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 });