mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-23 10:07:35 +00:00
parent
abc1495d9b
commit
b056b252d9
24 changed files with 878 additions and 5 deletions
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 />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue