|
|
|
@ -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) } |
|
|
|
|
} |
|
|
|
|
]} |
|
|
|
|
/> |
|
|
|
|
} |