diff --git a/client/src/api.ts b/client/src/api.ts index 3ab437b..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 { diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx index 034c1cf..970cef6 100644 --- a/client/src/components/MainWindow.tsx +++ b/client/src/components/MainWindow.tsx @@ -2,10 +2,13 @@ import React, { useReducer, useState, Reducer } from 'react'; 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, WindowState, WindowType } from './windows/Windows'; +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({ @@ -98,13 +101,29 @@ export default function MainWindow(props: any) { dispatch={tabDispatch} mainDispatch={dispatch} /> + case WindowType.Album: + return + case WindowType.Tag: + return + case WindowType.Song: + return default: throw new Error("Unimplemented window type"); } }); - console.log("State:", state) - return string, + 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. } @@ -46,7 +50,11 @@ export function SongTable(props: IProps) { const artist = stringifyList(artistNames); const mainArtistId = props.songGetters.getArtistIds(song)[0]; const mainArtistName = artistNames[0]; - const album = stringifyList(props.songGetters.getAlbumNames(song)); + 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 tags = props.songGetters.getTagNames(song).map((tag: string[]) => { return { @@ -56,7 +64,6 @@ export function SongTable(props: IProps) { }); const onClickArtist = () => { - console.log("onClick!") props.mainDispatch({ type: MainWindowStateActions.AddTab, tabState: { @@ -64,12 +71,37 @@ export function SongTable(props: IProps) { artistId: mainArtistId, metadata: null, }, - tabLabel: "Artist " + mainArtistId, 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 TextCell = (props: any) => { const classes = makeStyles({ button: { @@ -94,9 +126,9 @@ export function SongTable(props: IProps) { } return - {title} + {title} {artist} - {album} + {album} {tags} diff --git a/client/src/components/windows/AlbumWindow.tsx b/client/src/components/windows/AlbumWindow.tsx new file mode 100644 index 0000000..adf703a --- /dev/null +++ b/client/src/components/windows/AlbumWindow.tsx @@ -0,0 +1,101 @@ +import React, { useEffect } from 'react'; +import { Box, Typography } from '@material-ui/core'; +import AlbumIcon from '@material-ui/icons/Album'; +import * as serverApi from '../../api'; +import { WindowState } from './Windows'; + +export interface AlbumMetadata { + name: string, +} + +export interface AlbumWindowState extends WindowState { + albumId: number, + metadata: AlbumMetadata | null, +} + +export enum AlbumWindowStateActions { + SetMetadata = "SetMetadata", +} + +export function AlbumWindowReducer(state: AlbumWindowState, action: any) { + switch (action.type) { + case AlbumWindowStateActions.SetMetadata: + return { ...state, metadata: action.value } + default: + throw new Error("Unimplemented AlbumWindow state update.") + } +} + +export interface IProps { + state: AlbumWindowState, + dispatch: (action: any) => void, + mainDispatch: (action: any) => void, +} + +export async function getAlbumMetadata(id: number) { + const query = { + prop: serverApi.QueryElemProperty.albumId, + propOperand: id, + propOperator: serverApi.QueryFilterOp.Eq, + }; + + var q: serverApi.QueryRequest = { + query: query, + offsetsLimits: { + albumOffset: 0, + albumLimit: 1, + }, + ordering: { + orderBy: { + type: serverApi.OrderByType.Name, + }, + ascending: true, + }, + }; + + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(q), + }; + + return (async () => { + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) + let json: any = await response.json(); + let album = json.albums[0]; + return { + name: album.name + } + })(); +} + +export default function AlbumWindow(props: IProps) { + let metadata = props.state.metadata; + + useEffect(() => { + getAlbumMetadata(props.state.albumId) + .then((m: AlbumMetadata) => { + console.log("metadata", m); + props.dispatch({ + type: AlbumWindowStateActions.SetMetadata, + value: m + }); + }) + }, []); + + return + + + + + {metadata && {metadata.name}} + + +} \ No newline at end of file diff --git a/client/src/components/windows/ArtistWindow.tsx b/client/src/components/windows/ArtistWindow.tsx index 7f7de88..e6d7268 100644 --- a/client/src/components/windows/ArtistWindow.tsx +++ b/client/src/components/windows/ArtistWindow.tsx @@ -22,7 +22,7 @@ export function ArtistWindowReducer(state: ArtistWindowState, action: any) { case ArtistWindowStateActions.SetMetadata: return { ...state, metadata: action.value } default: - throw new Error("Unimplemented QueryWindow state update.") + throw new Error("Unimplemented ArtistWindow state update.") } } diff --git a/client/src/components/windows/QueryWindow.tsx b/client/src/components/windows/QueryWindow.tsx index 489415d..3367743 100644 --- a/client/src/components/windows/QueryWindow.tsx +++ b/client/src/components/windows/QueryWindow.tsx @@ -74,9 +74,11 @@ export default function QueryWindow(props: IProps) { const songGetters = { getTitle: (song: any) => song.title, + getId: (song: any) => song.songId, getArtistNames: (song: any) => song.artists.map((a: any) => a.name), getArtistIds: (song: any) => song.artists.map((a: any) => a.artistId), getAlbumNames: (song: any) => song.albums.map((a: any) => a.name), + getAlbumIds: (song: any) => song.albums.map((a: any) => a.albumId), getTagNames: (song: any) => { // Recursively resolve the name. const resolveTag = (tag: any) => { diff --git a/client/src/components/windows/SongWindow.tsx b/client/src/components/windows/SongWindow.tsx new file mode 100644 index 0000000..c7b97ef --- /dev/null +++ b/client/src/components/windows/SongWindow.tsx @@ -0,0 +1,101 @@ +import React, { useEffect } from 'react'; +import { Box, Typography } from '@material-ui/core'; +import AudiotrackIcon from '@material-ui/icons/Audiotrack'; +import * as serverApi from '../../api'; +import { WindowState } from './Windows'; + +export interface SongMetadata { + title: string, +} + +export interface SongWindowState extends WindowState { + songId: number, + metadata: SongMetadata | null, +} + +export enum SongWindowStateActions { + SetMetadata = "SetMetadata", +} + +export function SongWindowReducer(state: SongWindowState, action: any) { + switch (action.type) { + case SongWindowStateActions.SetMetadata: + return { ...state, metadata: action.value } + default: + throw new Error("Unimplemented SongWindow state update.") + } +} + +export interface IProps { + state: SongWindowState, + dispatch: (action: any) => void, + mainDispatch: (action: any) => void, +} + +export async function getSongMetadata(id: number) { + const query = { + prop: serverApi.QueryElemProperty.songId, + propOperand: id, + propOperator: serverApi.QueryFilterOp.Eq, + }; + + var q: serverApi.QueryRequest = { + query: query, + offsetsLimits: { + songOffset: 0, + songLimit: 1, + }, + ordering: { + orderBy: { + type: serverApi.OrderByType.Name, + }, + ascending: true, + }, + }; + + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(q), + }; + + return (async () => { + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) + let json: any = await response.json(); + let song = json.songs[0]; + return { + title: song.title + } + })(); +} + +export default function SongWindow(props: IProps) { + let metadata = props.state.metadata; + + useEffect(() => { + getSongMetadata(props.state.songId) + .then((m: SongMetadata) => { + console.log("metadata", m); + props.dispatch({ + type: SongWindowStateActions.SetMetadata, + value: m + }); + }) + }, []); + + return + + + + + {metadata && {metadata.title}} + + +} \ No newline at end of file diff --git a/client/src/components/windows/TagWindow.tsx b/client/src/components/windows/TagWindow.tsx new file mode 100644 index 0000000..3d1f44d --- /dev/null +++ b/client/src/components/windows/TagWindow.tsx @@ -0,0 +1,101 @@ +import React, { useEffect } from 'react'; +import { Box, Typography } from '@material-ui/core'; +import LocalOfferIcon from '@material-ui/icons/LocalOffer'; +import * as serverApi from '../../api'; +import { WindowState } from './Windows'; + +export interface TagMetadata { + name: string, +} + +export interface TagWindowState extends WindowState { + tagId: number, + metadata: TagMetadata | null, +} + +export enum TagWindowStateActions { + SetMetadata = "SetMetadata", +} + +export function TagWindowReducer(state: TagWindowState, action: any) { + switch (action.type) { + case TagWindowStateActions.SetMetadata: + return { ...state, metadata: action.value } + default: + throw new Error("Unimplemented TagWindow state update.") + } +} + +export interface IProps { + state: TagWindowState, + dispatch: (action: any) => void, + mainDispatch: (action: any) => void, +} + +export async function getTagMetadata(id: number) { + const query = { + prop: serverApi.QueryElemProperty.tagId, + propOperand: id, + propOperator: serverApi.QueryFilterOp.Eq, + }; + + var q: serverApi.QueryRequest = { + query: query, + offsetsLimits: { + tagOffset: 0, + tagLimit: 1, + }, + ordering: { + orderBy: { + type: serverApi.OrderByType.Name, + }, + ascending: true, + }, + }; + + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(q), + }; + + return (async () => { + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) + let json: any = await response.json(); + let tag = json.tags[0]; + return { + name: tag.name + } + })(); +} + +export default function TagWindow(props: IProps) { + let metadata = props.state.metadata; + + useEffect(() => { + getTagMetadata(props.state.tagId) + .then((m: TagMetadata) => { + console.log("metadata", m); + props.dispatch({ + type: TagWindowStateActions.SetMetadata, + value: m + }); + }) + }, []); + + return + + + + + {metadata && {metadata.name}} + + +} \ No newline at end of file diff --git a/client/src/components/windows/Windows.tsx b/client/src/components/windows/Windows.tsx index 17edca6..77b79f3 100644 --- a/client/src/components/windows/Windows.tsx +++ b/client/src/components/windows/Windows.tsx @@ -1,12 +1,21 @@ import React from 'react'; -import { QueryWindowReducer, QueryWindowState } from "./QueryWindow"; -import { ArtistWindowReducer, ArtistWindowState } from "./ArtistWindow"; +import { QueryWindowReducer } from "./QueryWindow"; +import { ArtistWindowReducer } from "./ArtistWindow"; import SearchIcon from '@material-ui/icons/Search'; import PersonIcon from '@material-ui/icons/Person'; +import AlbumIcon from '@material-ui/icons/Album'; +import LocalOfferIcon from '@material-ui/icons/LocalOffer'; +import AudiotrackIcon from '@material-ui/icons/Audiotrack'; +import { SongWindowReducer } from './SongWindow'; +import { AlbumWindowReducer } from './AlbumWindow'; +import { TagWindowReducer } from './TagWindow'; export enum WindowType { Query = "Query", Artist = "Artist", + Album = "Album", + Tag = "Tag", + Song = "Song", } export interface WindowState { @@ -16,6 +25,9 @@ export interface WindowState { export const newWindowReducer = { [WindowType.Query]: QueryWindowReducer, [WindowType.Artist]: ArtistWindowReducer, + [WindowType.Album]: AlbumWindowReducer, + [WindowType.Song]: SongWindowReducer, + [WindowType.Tag]: TagWindowReducer, } export const newWindowState = { @@ -33,5 +45,26 @@ export const newWindowState = { artistId: 1, metadata: null, } - } + }, + [WindowType.Album]: () => { + return { + tabLabel: <>Album, + albumId: 1, + metadata: null, + } + }, + [WindowType.Song]: () => { + return { + tabLabel: <>Song, + songId: 1, + metadata: null, + } + }, + [WindowType.Tag]: () => { + return { + tabLabel: <>Tag, + tagId: 1, + metadata: null, + } + }, } \ No newline at end of file diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/QueryEndpointHandler.ts index e0ca7ec..00ad1d5 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/QueryEndpointHandler.ts @@ -14,6 +14,7 @@ enum ObjectType { // To keep track of which database objects are needed to filter on // certain properties. const propertyObjects: Record = { + [api.QueryElemProperty.albumId]: ObjectType.Album, [api.QueryElemProperty.albumName]: ObjectType.Album, [api.QueryElemProperty.artistId]: ObjectType.Artist, [api.QueryElemProperty.artistName]: ObjectType.Artist, @@ -101,6 +102,7 @@ function addLeafWhere(knexQuery: any, queryElem: api.QueryElem, type: WhereType) [api.QueryElemProperty.artistName]: 'artists.name', [api.QueryElemProperty.artistId]: 'artists.id', [api.QueryElemProperty.albumName]: 'albums.name', + [api.QueryElemProperty.albumId]: 'albums.id', [api.QueryElemProperty.tagId]: 'tags.id', }