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',
}