Started adding results tables for items other than tracks. Buggy still.

master
Sander Vocke 4 years ago
parent 9df02ccb48
commit ba566126d5
  1. 309
      client/src/components/tables/ResultsTable.tsx
  2. 6
      client/src/components/windows/album/AlbumWindow.tsx
  3. 6
      client/src/components/windows/artist/ArtistWindow.tsx
  4. 48
      client/src/components/windows/query/QueryWindow.tsx
  5. 6
      client/src/components/windows/tag/TagWindow.tsx

@ -2,34 +2,69 @@ import React from 'react';
import { TableContainer, Table, TableHead, TableRow, TableCell, Paper, makeStyles, TableBody, Chip, Box, Button } from '@material-ui/core'; import { TableContainer, Table, TableHead, TableRow, TableCell, Paper, makeStyles, TableBody, Chip, Box, Button } from '@material-ui/core';
import stringifyList from '../../lib/stringifyList'; import stringifyList from '../../lib/stringifyList';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { Artist, QueryResponseTrackDetails, Tag, Name } from '../../api/api'; import { Artist, QueryResponseTrackDetails, Tag, Name, Id, TagDetails, QueryResponseArtistDetails, QueryResponseAlbumDetails } from '../../api/api';
import { isTemplateHead } from 'typescript';
function getTagNames (track: QueryResponseTrackDetails) : string[][] { function getFullTagNames(item: any,
getTagName: (tag: any) => string,
getTagParent: (tag: any) => any,
getItemTags: (item: any) => any[]): string[][] {
// Recursively resolve the name. // Recursively resolve the name.
const resolveTag = (tag: any) => { const resolveTag = (tag: any) => {
var r = [tag.name]; var r = [getTagName(tag)];
if (tag.parent) { r.unshift(resolveTag(tag.parent)); } const parent = getTagParent(tag);
if (parent) { r = resolveTag(parent).concat(r); }
return r; return r;
} }
return track.tags.map((tag: Tag) => resolveTag(tag)); return (getItemTags(item) || []).map((tag: Tag) => resolveTag(tag));
} }
function getTagIds (track: QueryResponseTrackDetails) : number[][] { function getFullTagIds(item: any,
// Recursively resolve the id. getTagId: (tag: any) => number,
getTagParent: (tag: any) => any,
getItemTags: (item: any) => any[]): number[][] {
// Recursively resolve the name.
const resolveTag = (tag: any) => { const resolveTag = (tag: any) => {
var r = [tag.tagId]; var r = [getTagId(tag)];
if (tag.parent) { r.unshift(resolveTag(tag.parent)); } const parent = getTagParent(tag);
if (parent) { r = resolveTag(parent).concat(r); }
return r; return r;
} }
return track.tags.map((tag: any) => resolveTag(tag)); return (getItemTags(item) || []).map((tag: Tag) => resolveTag(tag));
} }
export default function TrackTable(props: { export enum ColumnType {
tracks: QueryResponseTrackDetails[] Text = 0,
Tags,
}
export interface TextColumnData {
}
export interface TagsColumnData {
}
export interface ColumnDescription {
type: ColumnType,
title: string,
getText?: (item: any) => string,
getMaybeOnClick?: (item: any) => () => void,
getTags?: (item: any) => any[],
getTagName?: (tag: any) => string,
getTagId?: (tag: any) => number,
getTagParent?: (tag: any) => any,
getTagOnClick?: (tag: any) => () => void,
}
export function RenderItem(props: {
columnDescription: ColumnDescription,
item: any
}) { }) {
const history = useHistory(); let { columnDescription: cd, item } = props;
const classes = makeStyles({ const classes = makeStyles({
button: { button: {
@ -38,6 +73,59 @@ export default function TrackTable(props: {
paddingLeft: '0', paddingLeft: '0',
textAlign: 'left', textAlign: 'left',
}, },
})();
const TextCell = (props: any) => {
return <TableCell padding="none" {...props}>
<Button className={classes.button} fullWidth={true} onClick={props._onClick}>
<Box
width="100%"
display="flex"
alignItems="center"
paddingLeft="16px"
>
{props.children}
</Box>
</Button>
</TableCell>;
}
switch (props.columnDescription.type) {
case ColumnType.Text:
const text = cd.getText && cd.getText(item) || "Unknown";
const onClick = cd.getMaybeOnClick && cd.getMaybeOnClick(item) || null;
return <TextCell align="left" _onClick={onClick}>{text}</TextCell>
break;
case ColumnType.Tags:
const tags: any[] = cd.getTags && cd.getTags(item) || [];
const fullTagNames: string[][] = getFullTagNames(
item,
cd.getTagName || (() => "Unknown"),
cd.getTagParent || (() => null),
cd.getTags || (() => []),
);
return <>{fullTagNames.map((tag: string[], i: number) => {
const fullTag = stringifyList(tag, undefined, (idx: number, e: string) => {
return (idx === 0) ? e : " / " + e;
})
return <Box ml={0.5} mr={0.5}>
<Chip size="small"
label={fullTag}
onClick={cd.getTagOnClick && cd.getTagOnClick(tags[tags.length - 1])}
/>
</Box>;
})}</>;
break;
default:
throw 'Unknown column type';
}
}
export function ItemsTable(props: {
items: any[],
columns: ColumnDescription[],
}) {
const classes = makeStyles({
table: { table: {
minWidth: 650, minWidth: 650,
}, },
@ -48,83 +136,136 @@ export default function TrackTable(props: {
<Table className={classes.table} aria-label="a dense table"> <Table className={classes.table} aria-label="a dense table">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell align="left">Title</TableCell> {props.columns.map((c: ColumnDescription) =>
<TableCell align="left">Artist</TableCell> <TableCell align="left">{c.title}</TableCell>)}
<TableCell align="left">Album</TableCell>
<TableCell align="left">Tags</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{props.tracks.map((track: QueryResponseTrackDetails) => { {props.items.map((item: any, idx: number) => {
const name = track.name; return <TableRow key={idx}>
// TODO: display artists and albums separately! {props.columns.map((c: ColumnDescription) =>
const artistNames = track.artists <RenderItem
.filter( (a: Artist) => a.name ) columnDescription={c}
.map( (a: (Artist & Name)) => a.name ); item={item}
const artist = stringifyList(artistNames); />)}
const mainArtistId = </TableRow>;
(track.artists.length > 0 && track.artists[0].id) || undefined;
const album = track.album?.name || undefined;
const albumId = track.album?.id || undefined;
const trackId = track.id;
const tagIds = getTagIds(track);
const onClickArtist = () => {
history.push('/artist/' + mainArtistId);
}
const onClickAlbum = () => {
history.push('/album/' + albumId || '');
}
const onClickTrack = () => {
history.push('/track/' + trackId);
}
const onClickTag = (id: number, name: string) => {
history.push('/tag/' + id);
}
const tags = getTagNames(track).map((tag: string[], i: number) => {
const fullTag = stringifyList(tag, undefined, (idx: number, e: string) => {
return (idx === 0) ? e : " / " + e;
})
return <Box ml={0.5} mr={0.5}>
<Chip size="small"
label={fullTag}
onClick={() => onClickTag(tagIds[i][tagIds[i].length - 1], fullTag)}
/>
</Box>
});
const TextCell = (props: any) => {
return <TableCell padding="none" {...props}>
<Button className={classes.button} fullWidth={true} onClick={props._onClick}>
<Box
width="100%"
display="flex"
alignItems="center"
paddingLeft="16px"
>
{props.children}
</Box>
</Button>
</TableCell>;
}
return <TableRow key={name}>
<TextCell align="left" _onClick={onClickTrack}>{name}</TextCell>
<TextCell align="left" _onClick={onClickArtist}>{artist}</TextCell>
{album ? <TextCell align="left" _onClick={onClickAlbum}>{album}</TextCell> : <TextCell/>}
<TableCell padding="none" align="left" width="25%">
<Box display="flex" alignItems="center">
{tags}
</Box>
</TableCell>
</TableRow>
})} })}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
); );
}
export function TracksTable(props: {
tracks: QueryResponseTrackDetails[]
}) {
const history = useHistory();
return <ItemsTable
items={props.tracks}
columns={[
{
title: 'Title', type: ColumnType.Text, getText: (i: QueryResponseTrackDetails) => i.name,
getMaybeOnClick: (i: QueryResponseTrackDetails) => () => {
history.push('/track/' + i.id);
},
},
{
title: 'Artist', type: ColumnType.Text,
getText: (i: QueryResponseTrackDetails) => {
const artistNames = i.artists
.filter((a: Artist) => a.name)
.map((a: (Artist & Name)) => a.name);
return stringifyList(artistNames);
},
getMaybeOnClick: (i: QueryResponseTrackDetails) => () => {
// TODO
const mainArtistId =
(i.artists.length > 0 && i.artists[0].id) || undefined;
history.push('/artist/' + mainArtistId || 'undefined');
},
},
{
title: 'Album', type: ColumnType.Text, getText: (i: QueryResponseTrackDetails) => i.album?.name || "Unknown",
getMaybeOnClick: (i: QueryResponseTrackDetails) => () => {
history.push('/album/' + i.album?.id || 'undefined');
},
},
{
title: 'Tags', type: ColumnType.Tags,
getTags: (i: QueryResponseTrackDetails) => i.tags,
getTagId: (t: Tag & Id) => t.id,
getTagName: (t: Tag & Name) => t.name,
getTagParent: (t: Tag & TagDetails) => t.parent,
getTagOnClick: (t: Tag & Id) => () => { history.push('/tag/' + t.id) }
}
]}
/>
}
export function ArtistsTable(props: {
artists: QueryResponseArtistDetails[]
}) {
const history = useHistory();
return <ItemsTable
items={props.artists}
columns={[
{
title: 'Name', type: ColumnType.Text, getText: (i: QueryResponseArtistDetails) => i.name,
getMaybeOnClick: (i: QueryResponseArtistDetails) => () => {
history.push('/artist/' + i.id);
},
},
{
title: 'Tags', type: ColumnType.Tags,
getTags: (i: QueryResponseArtistDetails) => (i.tags || []),
getTagId: (t: Tag & Id) => t.id,
getTagName: (t: Tag & Name) => t.name,
getTagParent: (t: Tag & TagDetails) => t.parent,
getTagOnClick: (t: Tag & Id) => () => { history.push('/tag/' + t.id) }
}
]}
/>
}
export function AlbumsTable(props: {
albums: QueryResponseAlbumDetails[]
}) {
const history = useHistory();
return <ItemsTable
items={props.albums}
columns={[
{
title: 'Name', type: ColumnType.Text, getText: (i: QueryResponseAlbumDetails) => i.name,
getMaybeOnClick: (i: QueryResponseAlbumDetails) => () => {
history.push('/album/' + i.id);
},
},
{
title: 'Artist', type: ColumnType.Text,
getText: (i: QueryResponseAlbumDetails) => {
const artistNames = (i.artists || [])
.filter((a: Artist) => a.name)
.map((a: Artist) => a.name || "Unknown");
return stringifyList(artistNames);
},
getMaybeOnClick: (i: QueryResponseAlbumDetails) => () => {
// TODO
const mainArtistId =
((i.artists || []).length > 0 && (i.artists || [])[0].id) || undefined;
history.push('/artist/' + mainArtistId || 'undefined');
},
},
{
title: 'Tags', type: ColumnType.Tags,
getTags: (i: QueryResponseTrackDetails) => i.tags,
getTagId: (t: Tag & Id) => t.id,
getTagName: (t: Tag & Name) => t.name,
getTagParent: (t: Tag & TagDetails) => t.parent,
getTagOnClick: (t: Tag & Id) => () => { history.push('/tag/' + t.id) }
}
]}
/>
} }

@ -4,7 +4,7 @@ import AlbumIcon from '@material-ui/icons/Album';
import * as serverApi from '../../../api/api'; import * as serverApi from '../../../api/api';
import { WindowState } from '../Windows'; import { WindowState } from '../Windows';
import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import TrackTable from '../../tables/ResultsTable'; import { ColumnType, ItemsTable, TracksTable } from '../../tables/ResultsTable';
import { modifyAlbum, modifyTrack } from '../../../lib/saveChanges'; import { modifyAlbum, modifyTrack } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryAlbums, queryTracks } from '../../../lib/backend/queries'; import { queryAlbums, queryTracks } from '../../../lib/backend/queries';
@ -163,9 +163,7 @@ export function AlbumWindowControlled(props: {
<Box display="flex" flexDirection="column" alignItems="left"> <Box display="flex" flexDirection="column" alignItems="left">
<Typography>Tracks in this album in your library:</Typography> <Typography>Tracks in this album in your library:</Typography>
</Box> </Box>
{props.state.tracksOnAlbum && <TrackTable {props.state.tracksOnAlbum && <TracksTable tracks={props.state.tracksOnAlbum}/>}
tracks={props.state.tracksOnAlbum}
/>}
{!props.state.tracksOnAlbum && <CircularProgress />} {!props.state.tracksOnAlbum && <CircularProgress />}
</Box> </Box>
{metadata && <EditItemDialog {metadata && <EditItemDialog

@ -4,7 +4,7 @@ import PersonIcon from '@material-ui/icons/Person';
import * as serverApi from '../../../api/api'; import * as serverApi from '../../../api/api';
import { WindowState } from '../Windows'; import { WindowState } from '../Windows';
import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import TrackTable from '../../tables/ResultsTable'; import { ColumnType, ItemsTable, TracksTable } from '../../tables/ResultsTable';
import { modifyAlbum, modifyArtist } from '../../../lib/saveChanges'; import { modifyAlbum, modifyArtist } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryArtists, queryTracks } from '../../../lib/backend/queries'; import { queryArtists, queryTracks } from '../../../lib/backend/queries';
@ -168,9 +168,7 @@ export function ArtistWindowControlled(props: {
<Box display="flex" flexDirection="column" alignItems="left"> <Box display="flex" flexDirection="column" alignItems="left">
<Typography>Tracks by this artist in your library:</Typography> <Typography>Tracks by this artist in your library:</Typography>
</Box> </Box>
{props.state.tracksByArtist && <TrackTable {props.state.tracksByArtist && <TracksTable tracks={props.state.tracksByArtist}/>}
tracks={props.state.tracksByArtist}
/>}
{!props.state.tracksByArtist && <CircularProgress />} {!props.state.tracksByArtist && <CircularProgress />}
</Box> </Box>
{metadata && <EditItemDialog {metadata && <EditItemDialog

@ -1,14 +1,15 @@
import React, { useEffect, useReducer, useCallback } from 'react'; import React, { useEffect, useReducer, useCallback } from 'react';
import { Box, LinearProgress } from '@material-ui/core'; import { Box, LinearProgress, Typography } from '@material-ui/core';
import { QueryElem, QueryLeafBy, QueryLeafElem, QueryLeafOp } from '../../../lib/query/Query'; import { QueryElem, QueryLeafBy, QueryLeafElem, QueryLeafOp } from '../../../lib/query/Query';
import QueryBuilder from '../../querybuilder/QueryBuilder'; import QueryBuilder from '../../querybuilder/QueryBuilder';
import TrackTable from '../../tables/ResultsTable'; import { AlbumsTable, ArtistsTable, ColumnType, ItemsTable, TracksTable } from '../../tables/ResultsTable';
import { queryArtists, queryTracks, queryAlbums, queryTags } from '../../../lib/backend/queries'; import { queryArtists, queryTracks, queryAlbums, queryTags } from '../../../lib/backend/queries';
import { WindowState } from '../Windows'; import { WindowState } from '../Windows';
import { QueryResponseType, QueryResponseAlbumDetails, QueryResponseTagDetails, QueryResponseArtistDetails, QueryResponseTrackDetails } from '../../../api/api'; import { QueryResponseType, QueryResponseAlbumDetails, QueryResponseTagDetails, QueryResponseArtistDetails, QueryResponseTrackDetails, Artist, Name } from '../../../api/api';
import { ServerStreamResponseOptions } from 'http2'; import { ServerStreamResponseOptions } from 'http2';
import { TrackChangesSharp } from '@material-ui/icons'; import { TrackChangesSharp } from '@material-ui/icons';
import { v4 as genUuid } from 'uuid'; import { v4 as genUuid } from 'uuid';
import stringifyList from '../../../lib/stringifyList';
var _ = require('lodash'); var _ = require('lodash');
export enum QueryItemType { export enum QueryItemType {
@ -242,15 +243,38 @@ export function QueryWindowControlled(props: {
m={1} m={1}
width="80%" width="80%"
> >
{Object.values(resultsForQueries).map((r: ResultsForQuery | null) => <> {(() => {
{r !== null && r.kind == QueryItemType.Tracks && <TrackTable var rr = Object.values(resultsForQueries);
tracks={r.results as QueryResponseTrackDetails[]} rr = rr.sort((r: ResultsForQuery | null) => {
/>} if (r === null) { return 99; }
{r !== null && r.kind == QueryItemType.Albums && <>Found {r.results.length} albums.</>} return {
{r !== null && r.kind == QueryItemType.Artists && <>Found {r.results.length} artists.</>} [QueryItemType.Tracks]: 0,
{r !== null && r.kind == QueryItemType.Tags && <>Found {r.results.length} tags.</>} [QueryItemType.Albums]: 1,
{r === null && <LinearProgress />} [QueryItemType.Artists]: 2,
</>)} [QueryItemType.Tags]: 3
}[r.kind];
});
// TODO: the sorting is not working
return rr.map((r: ResultsForQuery | null) => <>
{r !== null && r.kind == QueryItemType.Tracks && <>
<Typography variant="h5">Tracks</Typography>
<TracksTable tracks={r.results as QueryResponseTrackDetails[]}/>
</>}
{r !== null && r.kind == QueryItemType.Albums && <>
<Typography variant="h5">Albums</Typography>
<AlbumsTable albums={r.results as QueryResponseAlbumDetails[]}/>
</>}
{r !== null && r.kind == QueryItemType.Artists && <>
<Typography variant="h5">Artists</Typography>
<ArtistsTable artists={r.results as QueryResponseArtistDetails[]}/>
</>}
{r !== null && r.kind == QueryItemType.Tags && <>
<Typography variant="h5">Tags</Typography>
<Typography>Found {r.results.length} tags.</Typography>
</>}
{r === null && <LinearProgress />}
</>);
})()}
</Box> </Box>
</Box> </Box>
} }

@ -4,7 +4,7 @@ import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import * as serverApi from '../../../api/api'; import * as serverApi from '../../../api/api';
import { WindowState } from '../Windows'; import { WindowState } from '../Windows';
import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import TrackTable from '../../tables/ResultsTable'; import { ItemsTable, ColumnType, TracksTable } from '../../tables/ResultsTable';
import { modifyTag } from '../../../lib/backend/tags'; import { modifyTag } from '../../../lib/backend/tags';
import { queryTags, queryTracks } from '../../../lib/backend/queries'; import { queryTags, queryTracks } from '../../../lib/backend/queries';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
@ -172,9 +172,7 @@ export function TagWindowControlled(props: {
<Box display="flex" flexDirection="column" alignItems="left"> <Box display="flex" flexDirection="column" alignItems="left">
<Typography>Tracks with this tag in your library:</Typography> <Typography>Tracks with this tag in your library:</Typography>
</Box> </Box>
{props.state.tracksWithTag && <TrackTable {props.state.tracksWithTag && <TracksTable tracks={props.state.tracksWithTag}/>}
tracks={props.state.tracksWithTag}
/>}
{!props.state.tracksWithTag && <CircularProgress />} {!props.state.tracksWithTag && <CircularProgress />}
</Box> </Box>
{metadata && <EditItemDialog {metadata && <EditItemDialog

Loading…
Cancel
Save