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 stringifyList from '../../lib/stringifyList';
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.
const resolveTag = (tag: any) => {
var r = [tag.name];
if (tag.parent) { r.unshift(resolveTag(tag.parent)); }
var r = [getTagName(tag)];
const parent = getTagParent(tag);
if (parent) { r = resolveTag(parent).concat(r); }
return r;
}
return track.tags.map((tag: Tag) => resolveTag(tag));
return (getItemTags(item) || []).map((tag: Tag) => resolveTag(tag));
}
function getTagIds (track: QueryResponseTrackDetails) : number[][] {
// Recursively resolve the id.
function getFullTagIds(item: any,
getTagId: (tag: any) => number,
getTagParent: (tag: any) => any,
getItemTags: (item: any) => any[]): number[][] {
// Recursively resolve the name.
const resolveTag = (tag: any) => {
var r = [tag.tagId];
if (tag.parent) { r.unshift(resolveTag(tag.parent)); }
var r = [getTagId(tag)];
const parent = getTagParent(tag);
if (parent) { r = resolveTag(parent).concat(r); }
return r;
}
return track.tags.map((tag: any) => resolveTag(tag));
return (getItemTags(item) || []).map((tag: Tag) => resolveTag(tag));
}
export default function TrackTable(props: {
tracks: QueryResponseTrackDetails[]
export enum ColumnType {
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({
button: {
@ -38,6 +73,59 @@ export default function TrackTable(props: {
paddingLeft: '0',
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: {
minWidth: 650,
},
@ -48,83 +136,136 @@ export default function TrackTable(props: {
<Table className={classes.table} aria-label="a dense table">
<TableHead>
<TableRow>
<TableCell align="left">Title</TableCell>
<TableCell align="left">Artist</TableCell>
<TableCell align="left">Album</TableCell>
<TableCell align="left">Tags</TableCell>
{props.columns.map((c: ColumnDescription) =>
<TableCell align="left">{c.title}</TableCell>)}
</TableRow>
</TableHead>
<TableBody>
{props.tracks.map((track: QueryResponseTrackDetails) => {
const name = track.name;
// TODO: display artists and albums separately!
const artistNames = track.artists
.filter( (a: Artist) => a.name )
.map( (a: (Artist & Name)) => a.name );
const artist = stringifyList(artistNames);
const mainArtistId =
(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>
{props.items.map((item: any, idx: number) => {
return <TableRow key={idx}>
{props.columns.map((c: ColumnDescription) =>
<RenderItem
columnDescription={c}
item={item}
/>)}
</TableRow>;
})}
</TableBody>
</Table>
</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 { WindowState } from '../Windows';
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 { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryAlbums, queryTracks } from '../../../lib/backend/queries';
@ -163,9 +163,7 @@ export function AlbumWindowControlled(props: {
<Box display="flex" flexDirection="column" alignItems="left">
<Typography>Tracks in this album in your library:</Typography>
</Box>
{props.state.tracksOnAlbum && <TrackTable
tracks={props.state.tracksOnAlbum}
/>}
{props.state.tracksOnAlbum && <TracksTable tracks={props.state.tracksOnAlbum}/>}
{!props.state.tracksOnAlbum && <CircularProgress />}
</Box>
{metadata && <EditItemDialog

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

@ -1,14 +1,15 @@
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 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 { 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 { TrackChangesSharp } from '@material-ui/icons';
import { v4 as genUuid } from 'uuid';
import stringifyList from '../../../lib/stringifyList';
var _ = require('lodash');
export enum QueryItemType {
@ -242,15 +243,38 @@ export function QueryWindowControlled(props: {
m={1}
width="80%"
>
{Object.values(resultsForQueries).map((r: ResultsForQuery | null) => <>
{r !== null && r.kind == QueryItemType.Tracks && <TrackTable
tracks={r.results as QueryResponseTrackDetails[]}
/>}
{r !== null && r.kind == QueryItemType.Albums && <>Found {r.results.length} albums.</>}
{r !== null && r.kind == QueryItemType.Artists && <>Found {r.results.length} artists.</>}
{r !== null && r.kind == QueryItemType.Tags && <>Found {r.results.length} tags.</>}
{r === null && <LinearProgress />}
</>)}
{(() => {
var rr = Object.values(resultsForQueries);
rr = rr.sort((r: ResultsForQuery | null) => {
if (r === null) { return 99; }
return {
[QueryItemType.Tracks]: 0,
[QueryItemType.Albums]: 1,
[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>
}

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

Loading…
Cancel
Save