diff --git a/client/src/api.ts b/client/src/api.ts index b162fff..b671e3e 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -81,6 +81,7 @@ export enum QueryElemProperty { artistName = "artistName", artistId = "artistId", albumName = "albumName", + albumId = "albumId", tagId = "tagId", } export enum OrderByType { @@ -130,7 +131,7 @@ export function checkQueryElem(elem: any): boolean { }); } return (elem.childrenOperator && elem.children) || - (elem.prop && elem.propOperand && elem.propOperator) || + ("prop" in elem && "propOperand" in elem && "propOperator" in elem) || Object.keys(elem).length === 0; } export function checkQueryRequest(req: any): boolean { diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx index 6cbdc67..970cef6 100644 --- a/client/src/components/MainWindow.tsx +++ b/client/src/components/MainWindow.tsx @@ -1,8 +1,14 @@ import React, { useReducer, useState, Reducer } from 'react'; -import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; +import { ThemeProvider, CssBaseline, createMuiTheme, withWidth } from '@material-ui/core'; import { grey } from '@material-ui/core/colors'; import AppBar from './appbar/AppBar'; -import QueryWindow, { QueryWindowReducer, QueryWindowState } from './windows/QueryWindow'; +import QueryWindow from './windows/QueryWindow'; +import { NewTabProps } from './appbar/AddTabMenu'; +import { newWindowState, newWindowReducer, WindowType } from './windows/Windows'; +import ArtistWindow from './windows/ArtistWindow'; +import AlbumWindow from './windows/AlbumWindow'; +import TagWindow from './windows/TagWindow'; +import SongWindow from './windows/SongWindow'; var _ = require('lodash'); const darkTheme = createMuiTheme({ @@ -15,9 +21,9 @@ const darkTheme = createMuiTheme({ }); export interface MainWindowState { - tabLabels: string[], tabStates: any[], tabReducers: Reducer[], + tabTypes: WindowType[], activeTab: number, } @@ -37,16 +43,17 @@ export function MainWindowReducer(state: MainWindowState, action: any) { return { ...state, tabStates: state.tabStates.filter((i: any, idx: number) => idx != action.idx), - tabLabels: state.tabLabels.filter((i: any, idx: number) => idx != action.idx), tabReducers: state.tabReducers.filter((i: any, idx: number) => idx != action.idx), - activeTab: state.activeTab >= (newSize-1) ? (newSize-1) : state.activeTab, + tabTypes: state.tabTypes.filter((i: any, idx: number) => idx != action.idx), + activeTab: state.activeTab >= (newSize - 1) ? (newSize - 1) : state.activeTab, } case MainWindowStateActions.AddTab: + console.log("Add tab: ", action) return { ...state, tabStates: [...state.tabStates, action.tabState], - tabLabels: [...state.tabLabels, action.tabLabel], tabReducers: [...state.tabReducers, action.tabReducer], + tabTypes: [...state.tabTypes, action.tabType], } case MainWindowStateActions.DispatchToTab: return { @@ -64,51 +71,75 @@ export function MainWindowReducer(state: MainWindowState, action: any) { export default function MainWindow(props: any) { const [state, dispatch] = useReducer(MainWindowReducer, { - tabLabels: ["Query"], tabStates: [ - { - editingQuery: false, - query: null, - resultsForQuery: null, - }, + newWindowState[WindowType.Query]() ], - tabReducers: [QueryWindowReducer, QueryWindowReducer], + tabReducers: [newWindowReducer[WindowType.Query]], + tabTypes: [WindowType.Query], activeTab: 0 }) - const queryWindows = state.tabStates.map((state: QueryWindowState, i: number) => { - return { - dispatch({ - type: MainWindowStateActions.DispatchToTab, - tabAction: action, - idx: i - }); - }} - /> + const windows = state.tabStates.map((tabState: any, i: number) => { + const tabDispatch = (action: any) => { + dispatch({ + type: MainWindowStateActions.DispatchToTab, + tabAction: action, + idx: i + }); + } + + switch (state.tabTypes[i]) { + case WindowType.Query: + return + case WindowType.Artist: + return + case WindowType.Album: + return + case WindowType.Tag: + return + case WindowType.Song: + return + default: + throw new Error("Unimplemented window type"); + } }); return s.tabLabel)} selectedTab={state.activeTab} setSelectedTab={(t: number) => dispatch({ type: MainWindowStateActions.SetActiveTab, value: t })} onCloseTab={(t: number) => dispatch({ type: MainWindowStateActions.CloseTab, idx: t })} - onAddTab={() => { + onAddTab={(w: NewTabProps) => { dispatch({ type: MainWindowStateActions.AddTab, - tabState: { - editingQuery: false, - query: null, - resultsForQuery: null, - }, - tabLabel: "Query", - tabReducer: QueryWindowReducer, + tabState: newWindowState[w.windowType](), + tabReducer: newWindowReducer[w.windowType], + tabType: w.windowType, }) }} /> - {queryWindows[state.activeTab]} + {windows[state.activeTab]} } \ No newline at end of file diff --git a/client/src/components/appbar/AddTabMenu.tsx b/client/src/components/appbar/AddTabMenu.tsx new file mode 100644 index 0000000..b001fe1 --- /dev/null +++ b/client/src/components/appbar/AddTabMenu.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { WindowType } from '../windows/Windows'; +import { Menu, MenuItem } from '@material-ui/core'; + +export interface NewTabProps { + windowType: WindowType, +} + +export interface IProps { + anchorEl: null | HTMLElement, + onClose: () => void, + onCreateTab: (q: NewTabProps) => void, +} + +export default function AddTabMenu(props: IProps) { + return + New Tab + { + props.onClose(); + props.onCreateTab({ + windowType: WindowType.Query, + }) + }} + >{WindowType.Query} + +} \ No newline at end of file diff --git a/client/src/components/appbar/AppBar.tsx b/client/src/components/appbar/AppBar.tsx index 0679f92..b7308aa 100644 --- a/client/src/components/appbar/AppBar.tsx +++ b/client/src/components/appbar/AppBar.tsx @@ -2,13 +2,14 @@ import React, { useState } from 'react'; import { AppBar as MuiAppBar, Box, Tab as MuiTab, Tabs, IconButton } from '@material-ui/core'; import CloseIcon from '@material-ui/icons/Close'; import AddIcon from '@material-ui/icons/Add'; +import AddTabMenu, { NewTabProps } from './AddTabMenu'; export interface IProps { tabLabels: string[], selectedTab: number, setSelectedTab: (n: number) => void, onCloseTab: (idx: number) => void, - onAddTab: () => void, + onAddTab: (w: NewTabProps) => void, } export interface TabProps { @@ -17,7 +18,6 @@ export interface TabProps { export function Tab(props: any) { const { onClose, label, ...restProps } = props; - const [hover, setHover] = useState(false); const labelElem = {label} - {hover && + - } + ; return setHover(true)} - onMouseLeave={() => setHover(false)} label={labelElem} {...restProps} /> } export default function AppBar(props: IProps) { - return - - - error + const [addMenuAnchorEl, setAddMenuAnchorEl] = React.useState(null); + + const onOpenAddMenu = (event: any) => { + setAddMenuAnchorEl(event.currentTarget); + }; + const onCloseAddMenu = () => { + setAddMenuAnchorEl(null); + }; + const onAddTab = (w: NewTabProps) => { + props.onAddTab(w); + }; + + return <> + + + + error + + props.setSelectedTab(v)}> + {props.tabLabels.map((l: string, idx: number) => props.onCloseTab(idx)} + />)} + + - props.setSelectedTab(v)}> - {props.tabLabels.map((l: string, idx: number) => props.onCloseTab(idx)} - />)} - - - - + + + } \ No newline at end of file diff --git a/client/src/components/querybuilder/QBAddElemMenu.tsx b/client/src/components/querybuilder/QBAddElemMenu.tsx index 6433435..1aa2b1d 100644 --- a/client/src/components/querybuilder/QBAddElemMenu.tsx +++ b/client/src/components/querybuilder/QBAddElemMenu.tsx @@ -3,7 +3,7 @@ import { Menu, MenuItem } from '@material-ui/core'; import NestedMenuItem from "material-ui-nested-menu-item"; import { QueryElem, QueryLeafBy, QueryLeafOp, TagQueryInfo } from '../../lib/query/Query'; import QBSelectWithRequest from './QBSelectWithRequest'; -import { Requests, TagItem } from './QueryBuilder'; +import { Requests } from './QueryBuilder'; export interface MenuProps { anchorEl: null | HTMLElement, diff --git a/client/src/components/tables/ResultsTable.tsx b/client/src/components/tables/ResultsTable.tsx index 3c6fb5e..154bdaa 100644 --- a/client/src/components/tables/ResultsTable.tsx +++ b/client/src/components/tables/ResultsTable.tsx @@ -1,17 +1,28 @@ import React from 'react'; import { TableContainer, Table, TableHead, TableRow, TableCell, Paper, makeStyles, TableBody, Chip, Box, Button } from '@material-ui/core'; import stringifyList from '../../lib/stringifyList'; +import { MainWindowStateActions } from '../MainWindow'; +import { newWindowReducer, WindowType } from '../windows/Windows'; +import PersonIcon from '@material-ui/icons/Person'; +import AlbumIcon from '@material-ui/icons/Album'; +import AudiotrackIcon from '@material-ui/icons/Audiotrack'; +import LocalOfferIcon from '@material-ui/icons/LocalOffer'; export interface SongGetters { getTitle: (song: any) => string, - getArtist: (song: any) => string, - getAlbum: (song: any) => string, - getTags: (song: any) => string[][], // Each tag is represented as a series of strings. + getId: (song: any) => number, + getArtistNames: (song: any) => string[], + getArtistIds: (song: any) => number[], + getAlbumNames: (song: any) => string[], + getAlbumIds: (song: any) => number[], + getTagNames: (song: any) => string[][], // Each tag is represented as a series of strings. + getTagIds: (song: any) => number[][], // Each tag is represented as a series of ids. } export interface IProps { songs: any[], songGetters: SongGetters, + mainDispatch: (action: any) => void, } export function SongTable(props: IProps) { @@ -36,13 +47,79 @@ export function SongTable(props: IProps) { {props.songs.map((song: any) => { const title = props.songGetters.getTitle(song); - const artist = props.songGetters.getArtist(song); - const album = props.songGetters.getAlbum(song); - const tags = props.songGetters.getTags(song).map((tag: string[]) => { + // TODO / FIXME: display artists and albums separately! + const artistNames = props.songGetters.getArtistNames(song); + const artist = stringifyList(artistNames); + const mainArtistId = props.songGetters.getArtistIds(song)[0]; + const mainArtistName = artistNames[0]; + const albumNames = props.songGetters.getAlbumNames(song); + const album = stringifyList(albumNames); + const mainAlbumName = albumNames[0]; + const mainAlbumId = props.songGetters.getAlbumIds(song)[0]; + const songId = props.songGetters.getId(song); + const tagIds = props.songGetters.getTagIds(song); + + const onClickArtist = () => { + props.mainDispatch({ + type: MainWindowStateActions.AddTab, + tabState: { + tabLabel: <>{mainArtistName}, + artistId: mainArtistId, + metadata: null, + }, + tabReducer: newWindowReducer[WindowType.Artist], + tabType: WindowType.Artist, + }) + } + + const onClickAlbum = () => { + props.mainDispatch({ + type: MainWindowStateActions.AddTab, + tabState: { + tabLabel: <>{mainAlbumName}, + albumId: mainAlbumId, + metadata: null, + }, + tabReducer: newWindowReducer[WindowType.Album], + tabType: WindowType.Album, + }) + } + + const onClickSong = () => { + props.mainDispatch({ + type: MainWindowStateActions.AddTab, + tabState: { + tabLabel: <>{title}, + songId: songId, + metadata: null, + }, + tabReducer: newWindowReducer[WindowType.Song], + tabType: WindowType.Song, + }) + } + + const onClickTag = (id: number, name: string) => { + props.mainDispatch({ + type: MainWindowStateActions.AddTab, + tabState: { + tabLabel: <>{name}, + tagId: id, + metadata: null, + }, + tabReducer: newWindowReducer[WindowType.Tag], + tabType: WindowType.Tag, + }) + } + + const tags = props.songGetters.getTagNames(song).map((tag: string[], i: number) => { + const fullTag = stringifyList(tag, undefined, (idx: number, e: string) => { + return (idx === 0) ? e : " / " + e; + }) return - { - return (idx === 0) ? e : " / " + e; - })} /> + onClickTag(tagIds[i][tagIds[i].length-1], fullTag)} + /> }); @@ -56,7 +133,7 @@ export function SongTable(props: IProps) { } })(); return -