parent
fc25e1ba8d
commit
4f3cd30e77
26 changed files with 243 additions and 1215 deletions
@ -1,115 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import MuiAppBar from '@material-ui/core/AppBar'; |
|
||||||
import Toolbar from '@material-ui/core/Toolbar'; |
|
||||||
import IconButton from '@material-ui/core/IconButton'; |
|
||||||
import Typography from '@material-ui/core/Typography'; |
|
||||||
import InputBase from '@material-ui/core/InputBase'; |
|
||||||
import { createStyles, fade, Theme, makeStyles } from '@material-ui/core/styles'; |
|
||||||
import MenuIcon from '@material-ui/icons/Menu'; |
|
||||||
import SearchIcon from '@material-ui/icons/Search'; |
|
||||||
import Tabs from '@material-ui/core/Tabs'; |
|
||||||
import Tab from '@material-ui/core/Tab'; |
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => |
|
||||||
createStyles({ |
|
||||||
root: { |
|
||||||
flexGrow: 1, |
|
||||||
}, |
|
||||||
menuButton: { |
|
||||||
marginRight: theme.spacing(2), |
|
||||||
}, |
|
||||||
title: { |
|
||||||
flexGrow: 1, |
|
||||||
display: 'none', |
|
||||||
[theme.breakpoints.up('sm')]: { |
|
||||||
display: 'block', |
|
||||||
}, |
|
||||||
}, |
|
||||||
search: { |
|
||||||
position: 'relative', |
|
||||||
borderRadius: theme.shape.borderRadius, |
|
||||||
backgroundColor: fade(theme.palette.common.white, 0.15), |
|
||||||
'&:hover': { |
|
||||||
backgroundColor: fade(theme.palette.common.white, 0.25), |
|
||||||
}, |
|
||||||
marginLeft: 0, |
|
||||||
width: '100%', |
|
||||||
[theme.breakpoints.up('sm')]: { |
|
||||||
marginLeft: theme.spacing(1), |
|
||||||
width: 'auto', |
|
||||||
}, |
|
||||||
}, |
|
||||||
searchIcon: { |
|
||||||
padding: theme.spacing(0, 2), |
|
||||||
height: '100%', |
|
||||||
position: 'absolute', |
|
||||||
pointerEvents: 'none', |
|
||||||
display: 'flex', |
|
||||||
alignItems: 'center', |
|
||||||
justifyContent: 'center', |
|
||||||
}, |
|
||||||
inputRoot: { |
|
||||||
color: 'inherit', |
|
||||||
}, |
|
||||||
inputInput: { |
|
||||||
padding: theme.spacing(1, 1, 1, 0), |
|
||||||
// vertical padding + font size from searchIcon
|
|
||||||
paddingLeft: `calc(1em + ${theme.spacing(4)}px)`, |
|
||||||
transition: theme.transitions.create('width'), |
|
||||||
width: '100%', |
|
||||||
[theme.breakpoints.up('sm')]: { |
|
||||||
width: '12ch', |
|
||||||
'&:focus': { |
|
||||||
width: '20ch', |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}), |
|
||||||
); |
|
||||||
|
|
||||||
export enum ActiveTab { |
|
||||||
Query = 0, |
|
||||||
} |
|
||||||
|
|
||||||
export interface IProps { |
|
||||||
activeTab: ActiveTab, |
|
||||||
onActiveTabChange: (tab:ActiveTab) => void |
|
||||||
} |
|
||||||
|
|
||||||
export default function AppBar(props: IProps) { |
|
||||||
const classes = useStyles(); |
|
||||||
|
|
||||||
return ( |
|
||||||
<div className={classes.root}> |
|
||||||
<MuiAppBar position="static"> |
|
||||||
<Toolbar> |
|
||||||
<IconButton |
|
||||||
edge="start" |
|
||||||
className={classes.menuButton} |
|
||||||
color="inherit" |
|
||||||
aria-label="open drawer" |
|
||||||
> |
|
||||||
<MenuIcon /> |
|
||||||
</IconButton> |
|
||||||
<Typography className={classes.title} variant="h6" noWrap>MuDBase</Typography> |
|
||||||
<div className={classes.search}> |
|
||||||
<div className={classes.searchIcon}> |
|
||||||
<SearchIcon /> |
|
||||||
</div> |
|
||||||
<InputBase |
|
||||||
placeholder="Search…" |
|
||||||
classes={{ |
|
||||||
root: classes.inputRoot, |
|
||||||
input: classes.inputInput, |
|
||||||
}} |
|
||||||
inputProps={{ 'aria-label': 'search' }} |
|
||||||
/> |
|
||||||
</div> |
|
||||||
</Toolbar> |
|
||||||
<Tabs value={props.activeTab} onChange={(evt:any, idx:any) => { props.onActiveTabChange(idx); }}> |
|
||||||
<Tab label="Query"/> |
|
||||||
</Tabs> |
|
||||||
</MuiAppBar> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
@ -1,68 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
|
|
||||||
import { Paper } from '@material-ui/core'; |
|
||||||
import { DisplayItem } from '../types/DisplayItem'; |
|
||||||
import DraggableItemListItem from './DraggableItemListItem'; |
|
||||||
import ItemList from './ItemList'; |
|
||||||
import * as serverApi from '../api'; |
|
||||||
import StoreIcon from '@material-ui/icons/Store'; |
|
||||||
import { ReactComponent as GooglePlayIcon } from '../assets/googleplaymusic_icon.svg'; |
|
||||||
|
|
||||||
type SongItem = serverApi.SongDetails; |
|
||||||
type ArtistItem = serverApi.ArtistDetails; |
|
||||||
export type Item = SongItem | ArtistItem; |
|
||||||
|
|
||||||
const getStoreIcon = (url: String) => { |
|
||||||
if (url.includes('play.google.com')) { |
|
||||||
return <GooglePlayIcon height='30px' width='30px' />; |
|
||||||
} |
|
||||||
return <StoreIcon />; |
|
||||||
} |
|
||||||
|
|
||||||
function toDisplayItem(item: Item): DisplayItem | undefined { |
|
||||||
if (serverApi.isSongDetails(item)) { |
|
||||||
return { |
|
||||||
title: item.title, |
|
||||||
artistNames: (item.artists && item.artists.map((artist: serverApi.ArtistDetails) => { |
|
||||||
return artist.name; |
|
||||||
})) || ['Unknown'], |
|
||||||
tagNames: (item.tags && item.tags.map((tag: serverApi.TagDetails) => { |
|
||||||
return tag.name; |
|
||||||
})) || [], |
|
||||||
storeLinks: (item.storeLinks && item.storeLinks.map((url: String) => { |
|
||||||
return { |
|
||||||
icon: getStoreIcon(url), |
|
||||||
url: url |
|
||||||
} |
|
||||||
})) || [], |
|
||||||
} |
|
||||||
} else if (serverApi.isArtistDetails(item)) { |
|
||||||
return { |
|
||||||
name: item.name ? item.name : "Unknown", |
|
||||||
tagNames: [], // TODO
|
|
||||||
storeLinks: (item.storeLinks && item.storeLinks.map((url: String) => { |
|
||||||
return { |
|
||||||
icon: getStoreIcon(url), |
|
||||||
url: url |
|
||||||
} |
|
||||||
})) || [], |
|
||||||
}; |
|
||||||
|
|
||||||
} |
|
||||||
return undefined; |
|
||||||
} |
|
||||||
|
|
||||||
interface IProps { |
|
||||||
items: Item[] |
|
||||||
} |
|
||||||
|
|
||||||
export default function BrowseWindow(props: IProps) { |
|
||||||
return <Paper> |
|
||||||
<ItemList> |
|
||||||
{props.items.map((item: Item) => { |
|
||||||
const di = toDisplayItem(item); |
|
||||||
return di && <DraggableItemListItem item={di} />; |
|
||||||
})} |
|
||||||
</ItemList> |
|
||||||
</Paper>; |
|
||||||
} |
|
@ -1,19 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import ItemListItem from './ItemListItem'; |
|
||||||
import { useDrag } from 'react-dnd'; |
|
||||||
import { dragTypes } from '../types/DragTypes'; |
|
||||||
|
|
||||||
export default function DraggableItemListItem(props: any) { |
|
||||||
const [ /*{ isDragging: boolean }*/ , drag] = useDrag({ |
|
||||||
item: { type: dragTypes.ListItem }, |
|
||||||
collect: (monitor: any) => ({ |
|
||||||
isDragging: !!monitor.isDragging(), |
|
||||||
}), |
|
||||||
}); |
|
||||||
|
|
||||||
return <div |
|
||||||
ref={drag} |
|
||||||
> |
|
||||||
<ItemListItem {...props} /> |
|
||||||
</div>; |
|
||||||
} |
|
@ -1,46 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { Dialog, Grid, Typography, TextField, Button } from '@material-ui/core'; |
|
||||||
|
|
||||||
var cloneDeep = require('lodash/cloneDeep'); |
|
||||||
|
|
||||||
export interface ArtistProperties { |
|
||||||
name: String, |
|
||||||
} |
|
||||||
|
|
||||||
export interface IProps { |
|
||||||
dialogOpen: boolean, |
|
||||||
onClose?: () => void, |
|
||||||
onChangeArtistProperties?: (props: ArtistProperties) => void, |
|
||||||
artistProperties: ArtistProperties, |
|
||||||
onSubmit?: () => void, |
|
||||||
} |
|
||||||
|
|
||||||
export default function EditArtistDialog(props: IProps) { |
|
||||||
const onNameChange = (name: String) => { |
|
||||||
if (props.onChangeArtistProperties) { |
|
||||||
const p = cloneDeep(props.artistProperties); |
|
||||||
p.name = name; |
|
||||||
props.onChangeArtistProperties(p); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
return <Dialog |
|
||||||
open={props.dialogOpen} |
|
||||||
onClose={props.onClose} |
|
||||||
> |
|
||||||
<Typography variant='h6' gutterBottom> |
|
||||||
Artist Details |
|
||||||
</Typography> |
|
||||||
<Grid container spacing={3}> |
|
||||||
<Grid item xs={12}> |
|
||||||
<TextField |
|
||||||
label="Name" |
|
||||||
value={props.artistProperties.name} |
|
||||||
onChange={(i: any) => onNameChange(i.target.value)} |
|
||||||
fullWidth |
|
||||||
/> |
|
||||||
</Grid> |
|
||||||
</Grid> |
|
||||||
<Button variant="contained" onClick={props.onSubmit}>Submit</Button> |
|
||||||
</Dialog> |
|
||||||
} |
|
@ -1,85 +0,0 @@ |
|||||||
import React, { useState, useEffect } from 'react'; |
|
||||||
import { Dialog, Grid, Typography, TextField, Button } from '@material-ui/core'; |
|
||||||
import { Autocomplete } from '@material-ui/lab'; |
|
||||||
|
|
||||||
var cloneDeep = require('lodash/cloneDeep'); |
|
||||||
|
|
||||||
export interface SongProperties { |
|
||||||
title: String, |
|
||||||
artistId: Number | undefined, |
|
||||||
} |
|
||||||
|
|
||||||
export interface ArtistProperties { |
|
||||||
name: String, |
|
||||||
id: Number, |
|
||||||
} |
|
||||||
|
|
||||||
export interface IProps { |
|
||||||
dialogOpen: boolean, |
|
||||||
onClose?: () => void, |
|
||||||
onChangeSongProperties?: (props: SongProperties) => void, |
|
||||||
songProperties: SongProperties, |
|
||||||
onSubmit?: () => void, |
|
||||||
artists: ArtistProperties[], |
|
||||||
} |
|
||||||
|
|
||||||
export default function EditSongDialog(props: IProps) { |
|
||||||
const onTitleChange = (title: String) => { |
|
||||||
if (props.onChangeSongProperties) { |
|
||||||
const p = cloneDeep(props.songProperties); |
|
||||||
p.title = title; |
|
||||||
props.onChangeSongProperties(p); |
|
||||||
} |
|
||||||
}; |
|
||||||
const onArtistChange = (artist: Number | undefined) => { |
|
||||||
if (props.onChangeSongProperties) { |
|
||||||
const p = cloneDeep(props.songProperties); |
|
||||||
p.artistId = artist; |
|
||||||
props.onChangeSongProperties(p); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
return <Dialog |
|
||||||
open={props.dialogOpen} |
|
||||||
onClose={props.onClose} |
|
||||||
> |
|
||||||
<Typography variant='h6' gutterBottom> |
|
||||||
Song Details |
|
||||||
</Typography> |
|
||||||
<Grid container spacing={3}> |
|
||||||
<Grid item xs={12}> |
|
||||||
<TextField |
|
||||||
label="Song Title" |
|
||||||
value={props.songProperties.title} |
|
||||||
onChange={(i: any) => onTitleChange(i.target.value)} |
|
||||||
fullWidth |
|
||||||
/> |
|
||||||
</Grid> |
|
||||||
<Grid item xs={12}> |
|
||||||
{ // TODO: this autocomplete is not controlled but does send updates to the parent
|
|
||||||
// right away. In other words, there is no way to affect its value from outside
|
|
||||||
// the dialog.
|
|
||||||
} |
|
||||||
<Autocomplete |
|
||||||
options={props.artists} |
|
||||||
getOptionLabel={(option) => option.name as string} |
|
||||||
onChange={(event, newValue) => { |
|
||||||
if(newValue) { |
|
||||||
onArtistChange(newValue.id); |
|
||||||
} else { |
|
||||||
onArtistChange(undefined); |
|
||||||
} |
|
||||||
}} |
|
||||||
renderInput={ |
|
||||||
(params) => |
|
||||||
<TextField {...params} |
|
||||||
label="Artist" |
|
||||||
fullWidth |
|
||||||
/> |
|
||||||
} |
|
||||||
/> |
|
||||||
</Grid> |
|
||||||
</Grid> |
|
||||||
<Button variant="contained" onClick={props.onSubmit}>Submit</Button> |
|
||||||
</Dialog> |
|
||||||
} |
|
@ -1,189 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
|
|
||||||
import { |
|
||||||
TextField, |
|
||||||
Paper, |
|
||||||
Select, |
|
||||||
MenuItem, |
|
||||||
Typography |
|
||||||
} from '@material-ui/core'; |
|
||||||
|
|
||||||
import { |
|
||||||
TitleQuery, |
|
||||||
ArtistQuery, |
|
||||||
isTitleQuery, |
|
||||||
isArtistQuery, |
|
||||||
Query, |
|
||||||
isAndQuery, |
|
||||||
isOrQuery, |
|
||||||
QueryKeys, |
|
||||||
} from '../types/Query'; |
|
||||||
|
|
||||||
interface TitleFilterControlProps { |
|
||||||
query: TitleQuery, |
|
||||||
onChangeQuery: (q: Query) => void, |
|
||||||
} |
|
||||||
function TitleFilterControl(props: TitleFilterControlProps) { |
|
||||||
return <TextField |
|
||||||
label="Title" |
|
||||||
value={props.query[QueryKeys.TitleLike]} |
|
||||||
onChange={(i: any) => props.onChangeQuery({ |
|
||||||
[QueryKeys.TitleLike]: i.target.value |
|
||||||
})} |
|
||||||
/> |
|
||||||
} |
|
||||||
|
|
||||||
interface ArtistFilterControlProps { |
|
||||||
query: ArtistQuery, |
|
||||||
onChangeQuery: (q: Query) => void, |
|
||||||
} |
|
||||||
function ArtistFilterControl(props: ArtistFilterControlProps) { |
|
||||||
return <TextField |
|
||||||
label="Name" |
|
||||||
value={props.query[QueryKeys.ArtistLike]} |
|
||||||
onChange={(i: any) => props.onChangeQuery({ |
|
||||||
[QueryKeys.ArtistLike]: i.target.value |
|
||||||
})} |
|
||||||
/> |
|
||||||
} |
|
||||||
|
|
||||||
interface AndNodeControlProps { |
|
||||||
query: any, |
|
||||||
onChangeQuery: (q: Query) => void, |
|
||||||
} |
|
||||||
function AndNodeControl(props: AndNodeControlProps) { |
|
||||||
const onChangeSubQuery = (a: Query, b: Query) => { |
|
||||||
props.onChangeQuery({ |
|
||||||
[QueryKeys.AndQuerySignature]: true, |
|
||||||
[QueryKeys.OperandA]: a, |
|
||||||
[QueryKeys.OperandB]: b |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
return <Paper> |
|
||||||
{props.query && isAndQuery(props.query) && <> |
|
||||||
<Typography>And</Typography> |
|
||||||
<FilterControl query={props.query.a} onChangeQuery={(q: Query) => { onChangeSubQuery(q, props.query.b); }} /> |
|
||||||
<FilterControl query={props.query.b} onChangeQuery={(q: Query) => { onChangeSubQuery(props.query.a, q); }} /> |
|
||||||
</>} |
|
||||||
</Paper>; |
|
||||||
} |
|
||||||
|
|
||||||
interface OrNodeControlProps { |
|
||||||
query: any, |
|
||||||
onChangeQuery: (q: Query) => void, |
|
||||||
} |
|
||||||
function OrNodeControl(props: OrNodeControlProps) { |
|
||||||
const onChangeSubQuery = (a: Query, b: Query) => { |
|
||||||
props.onChangeQuery({ |
|
||||||
[QueryKeys.OrQuerySignature]: true, |
|
||||||
[QueryKeys.OperandA]: a, |
|
||||||
[QueryKeys.OperandB]: b |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
return <Paper> |
|
||||||
{props.query && isOrQuery(props.query) && <> |
|
||||||
<Typography>Or</Typography> |
|
||||||
<FilterControl query={props.query.a} onChangeQuery={(q: Query) => { onChangeSubQuery(q, props.query.b); }} /> |
|
||||||
<FilterControl query={props.query.b} onChangeQuery={(q: Query) => { onChangeSubQuery(props.query.a, q); }} /> |
|
||||||
</>} |
|
||||||
</Paper>; |
|
||||||
} |
|
||||||
|
|
||||||
export interface IProps { |
|
||||||
query: Query | undefined, |
|
||||||
onChangeQuery: (query: Query) => void, |
|
||||||
} |
|
||||||
|
|
||||||
export function FilterControlLeaf(props: IProps) { |
|
||||||
const selectTypeOptions: string[] = ['Title', 'Artist']; |
|
||||||
const selectTypeOption: string = (props.query && isTitleQuery(props.query) && 'Title') || |
|
||||||
(props.query && isArtistQuery(props.query) && 'Artist') || |
|
||||||
"Unknown"; |
|
||||||
|
|
||||||
const selectInsertOptions: string[] = ['And', 'Or']; |
|
||||||
|
|
||||||
const handleQueryOnChange = (event: any) => { |
|
||||||
switch (event.target.value) { |
|
||||||
case 'Title': { |
|
||||||
props.onChangeQuery({ |
|
||||||
[QueryKeys.TitleLike]: '' |
|
||||||
}) |
|
||||||
break; |
|
||||||
} |
|
||||||
case 'Artist': { |
|
||||||
props.onChangeQuery({ |
|
||||||
[QueryKeys.ArtistLike]: '' |
|
||||||
}) |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const handleInsertElem = (event: any) => { |
|
||||||
switch (event.target.value) { |
|
||||||
case 'And': { |
|
||||||
props.onChangeQuery({ |
|
||||||
[QueryKeys.AndQuerySignature]: true, |
|
||||||
[QueryKeys.OperandA]: props.query || { [QueryKeys.TitleLike]: '' }, |
|
||||||
[QueryKeys.OperandB]: { |
|
||||||
[QueryKeys.TitleLike]: '' |
|
||||||
} |
|
||||||
}) |
|
||||||
break; |
|
||||||
} |
|
||||||
case 'Or': { |
|
||||||
props.onChangeQuery({ |
|
||||||
[QueryKeys.OrQuerySignature]: true, |
|
||||||
[QueryKeys.OperandA]: props.query || { [QueryKeys.TitleLike]: '' }, |
|
||||||
[QueryKeys.OperandB]: { |
|
||||||
[QueryKeys.TitleLike]: '' |
|
||||||
} |
|
||||||
}) |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return <Paper> |
|
||||||
{/* The selector for inserting another element here. */} |
|
||||||
<Select |
|
||||||
onChange={handleInsertElem} |
|
||||||
> |
|
||||||
{selectInsertOptions.map((option: string) => { |
|
||||||
return <MenuItem value={option}>{option}</MenuItem> |
|
||||||
})} |
|
||||||
</Select> |
|
||||||
{/* The selector for the type of filter element. */} |
|
||||||
<Select |
|
||||||
value={selectTypeOption} |
|
||||||
onChange={handleQueryOnChange} |
|
||||||
> |
|
||||||
{selectTypeOptions.map((option: string) => { |
|
||||||
return <MenuItem value={option}>{option}</MenuItem> |
|
||||||
})} |
|
||||||
</Select> |
|
||||||
{props.query && isTitleQuery(props.query) && <TitleFilterControl query={props.query} onChangeQuery={props.onChangeQuery} />} |
|
||||||
{props.query && isArtistQuery(props.query) && <ArtistFilterControl query={props.query} onChangeQuery={props.onChangeQuery} />} |
|
||||||
</Paper>; |
|
||||||
} |
|
||||||
|
|
||||||
export function FilterControlNode(props: IProps) { |
|
||||||
return <> |
|
||||||
{props.query && isAndQuery(props.query) && <AndNodeControl {...props} />} |
|
||||||
{props.query && isOrQuery(props.query) && <OrNodeControl {...props} />} |
|
||||||
</>; |
|
||||||
} |
|
||||||
|
|
||||||
export default function FilterControl(props: IProps) { |
|
||||||
const isLeaf = (query: Query | undefined) => { |
|
||||||
return query && (isTitleQuery(query) || isArtistQuery(query)); |
|
||||||
} |
|
||||||
const isNode = (query: Query | undefined) => !isLeaf(query); |
|
||||||
|
|
||||||
return <> |
|
||||||
{isLeaf(props.query) && <FilterControlLeaf {...props} />} |
|
||||||
{isNode(props.query) && <FilterControlNode {...props} />} |
|
||||||
</> |
|
||||||
} |
|
@ -1,24 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; |
|
||||||
import List from '@material-ui/core/List'; |
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => |
|
||||||
createStyles({ |
|
||||||
root: { |
|
||||||
flexGrow: 1, |
|
||||||
maxWidth: 752, |
|
||||||
}, |
|
||||||
}), |
|
||||||
); |
|
||||||
|
|
||||||
export default function ItemList(props:any) { |
|
||||||
const classes = useStyles(); |
|
||||||
|
|
||||||
return ( |
|
||||||
<div className={classes.root}> |
|
||||||
<List dense={true}> |
|
||||||
{props.children} |
|
||||||
</List> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
@ -1,18 +0,0 @@ |
|||||||
import React, { useEffect } from 'react'; |
|
||||||
import ItemListItem from './ItemListItem'; |
|
||||||
import { ArtistDisplayItem, LoadingArtistDisplayItem } from '../types/DisplayItem'; |
|
||||||
|
|
||||||
export interface IProps { |
|
||||||
getDetails: () => Promise<ArtistDisplayItem> |
|
||||||
} |
|
||||||
|
|
||||||
export default function ItemListArtistItem(props: IProps) { |
|
||||||
const [ artist, setArtist ] = React.useState<ArtistDisplayItem | LoadingArtistDisplayItem>({ loadingArtist: true }); |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
props.getDetails() |
|
||||||
.then((details:ArtistDisplayItem) => { setArtist(details); }); |
|
||||||
}); |
|
||||||
|
|
||||||
return <ItemListItem item={artist}/> |
|
||||||
} |
|
@ -1,19 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { DisplayItem, isSong, isLoadingSong, isArtist, isLoadingArtist } from '../types/DisplayItem'; |
|
||||||
import ItemListLoadedSongItem from './ItemListLoadedSongItem'; |
|
||||||
import ItemListLoadingSongItem from './ItemListLoadingSongItem'; |
|
||||||
import ItemListLoadedArtistItem from './ItemListLoadedArtistItem'; |
|
||||||
import ItemListLoadingArtistItem from './ItemListLoadingArtistItem'; |
|
||||||
|
|
||||||
export interface IProps { |
|
||||||
item: DisplayItem |
|
||||||
} |
|
||||||
|
|
||||||
export default function ItemListItem(props: IProps) { |
|
||||||
return <> |
|
||||||
{isSong(props.item) && <ItemListLoadedSongItem item={props.item}/>} |
|
||||||
{isLoadingSong(props.item) && <ItemListLoadingSongItem item={props.item}/>} |
|
||||||
{isArtist(props.item) && <ItemListLoadedArtistItem item={props.item}/>} |
|
||||||
{isLoadingArtist(props.item) && <ItemListLoadingArtistItem item={props.item}/>} |
|
||||||
</> |
|
||||||
} |
|
@ -1,35 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import ListItem from '@material-ui/core/ListItem'; |
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon'; |
|
||||||
import ListItemText from '@material-ui/core/ListItemText'; |
|
||||||
import GroupIcon from '@material-ui/icons/Group'; |
|
||||||
import Chip from '@material-ui/core/Chip'; |
|
||||||
|
|
||||||
import { ArtistDisplayItem } from '../types/DisplayItem'; |
|
||||||
|
|
||||||
export interface IProps { |
|
||||||
item: ArtistDisplayItem |
|
||||||
} |
|
||||||
|
|
||||||
export default function ItemListLoadedArtistItem(props: IProps) { |
|
||||||
return ( |
|
||||||
<ListItem> |
|
||||||
<ListItemIcon> |
|
||||||
<GroupIcon /> |
|
||||||
</ListItemIcon> |
|
||||||
<ListItemText |
|
||||||
primary={props.item.name} |
|
||||||
/> |
|
||||||
{props.item.tagNames.map((tag: any) => { |
|
||||||
return <Chip label={tag}/> |
|
||||||
})} |
|
||||||
{props.item.storeLinks.map((link: any) => { |
|
||||||
return <a href={link.url} target="_blank" rel="noopener noreferrer"> |
|
||||||
<ListItemIcon> |
|
||||||
{link.icon} |
|
||||||
</ListItemIcon> |
|
||||||
</a>; |
|
||||||
})} |
|
||||||
</ListItem> |
|
||||||
); |
|
||||||
} |
|
@ -1,41 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import ListItem from '@material-ui/core/ListItem'; |
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon'; |
|
||||||
import ListItemText from '@material-ui/core/ListItemText'; |
|
||||||
import MusicNoteIcon from '@material-ui/icons/MusicNote'; |
|
||||||
import Chip from '@material-ui/core/Chip'; |
|
||||||
|
|
||||||
import { SongDisplayItem } from '../types/DisplayItem'; |
|
||||||
|
|
||||||
export interface IProps { |
|
||||||
item: SongDisplayItem |
|
||||||
} |
|
||||||
|
|
||||||
export default function ItemListLoadedSongItem(props: IProps) { |
|
||||||
var artists = props.item.artistNames.length ? props.item.artistNames[0] : "Unknown"; |
|
||||||
for (var i: number = 1; i < props.item.artistNames.length; i++) { |
|
||||||
artists = artists.concat(", " + props.item.artistNames[i]); |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<ListItem> |
|
||||||
<ListItemIcon> |
|
||||||
<MusicNoteIcon /> |
|
||||||
</ListItemIcon> |
|
||||||
<ListItemText |
|
||||||
primary={props.item.title} |
|
||||||
secondary={artists} |
|
||||||
/> |
|
||||||
{props.item.tagNames.map((tag: any) => { |
|
||||||
return <Chip label={tag}/> |
|
||||||
})} |
|
||||||
{props.item.storeLinks.map((link: any) => { |
|
||||||
return <a href={link.url} target="_blank" rel="noopener noreferrer"> |
|
||||||
<ListItemIcon> |
|
||||||
{link.icon} |
|
||||||
</ListItemIcon> |
|
||||||
</a>; |
|
||||||
})} |
|
||||||
</ListItem> |
|
||||||
); |
|
||||||
} |
|
@ -1,22 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import ListItem from '@material-ui/core/ListItem'; |
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon'; |
|
||||||
import GroupIcon from '@material-ui/icons/Group'; |
|
||||||
import CircularProgress from '@material-ui/core/CircularProgress'; |
|
||||||
|
|
||||||
import { LoadingArtistDisplayItem } from '../types/DisplayItem'; |
|
||||||
|
|
||||||
export interface IProps { |
|
||||||
item: LoadingArtistDisplayItem |
|
||||||
} |
|
||||||
|
|
||||||
export default function ItemListLoadingArtistItem(props: IProps) { |
|
||||||
return ( |
|
||||||
<ListItem> |
|
||||||
<ListItemIcon> |
|
||||||
<GroupIcon /> |
|
||||||
</ListItemIcon> |
|
||||||
<CircularProgress size={24}/> |
|
||||||
</ListItem> |
|
||||||
); |
|
||||||
} |
|
@ -1,22 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import ListItem from '@material-ui/core/ListItem'; |
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon'; |
|
||||||
import MusicNoteIcon from '@material-ui/icons/MusicNote'; |
|
||||||
import CircularProgress from '@material-ui/core/CircularProgress'; |
|
||||||
|
|
||||||
import { LoadingSongDisplayItem } from '../types/DisplayItem'; |
|
||||||
|
|
||||||
export interface IProps { |
|
||||||
item: LoadingSongDisplayItem |
|
||||||
} |
|
||||||
|
|
||||||
export default function ItemListLoadingSongItem(props: IProps) { |
|
||||||
return ( |
|
||||||
<ListItem> |
|
||||||
<ListItemIcon> |
|
||||||
<MusicNoteIcon /> |
|
||||||
</ListItemIcon> |
|
||||||
<CircularProgress size={24}/> |
|
||||||
</ListItem> |
|
||||||
); |
|
||||||
} |
|
@ -1,18 +0,0 @@ |
|||||||
import React, { useEffect } from 'react'; |
|
||||||
import ItemListItem from './ItemListItem'; |
|
||||||
import { SongDisplayItem, LoadingSongDisplayItem } from '../types/DisplayItem'; |
|
||||||
|
|
||||||
export interface IProps { |
|
||||||
getDetails: () => Promise<SongDisplayItem> |
|
||||||
} |
|
||||||
|
|
||||||
export default function ItemListSongItem(props: IProps) { |
|
||||||
const [ song, setSong ] = React.useState<SongDisplayItem | LoadingSongDisplayItem>({ loadingSong: true }); |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
props.getDetails() |
|
||||||
.then((details:SongDisplayItem) => { setSong(details); }); |
|
||||||
}); |
|
||||||
|
|
||||||
return <ItemListItem item={song}/> |
|
||||||
} |
|
@ -1,198 +0,0 @@ |
|||||||
import React, { useState, useEffect } from 'react'; |
|
||||||
|
|
||||||
import { Query, toApiQuery, QueryOrdering, TypesIncluded, QueryKeys, OrderKey } from '../types/Query'; |
|
||||||
import FilterControl from './FilterControl'; |
|
||||||
import * as serverApi from '../api'; |
|
||||||
import BrowseWindow, { Item } from './BrowseWindow'; |
|
||||||
import { FormControl, FormLabel, FormGroup, FormControlLabel, Checkbox, Select, MenuItem } from '@material-ui/core'; |
|
||||||
|
|
||||||
const _ = require('lodash'); |
|
||||||
|
|
||||||
interface ItemTypeCheckboxesProps { |
|
||||||
types: TypesIncluded, |
|
||||||
onChange: (types: TypesIncluded) => void; |
|
||||||
} |
|
||||||
|
|
||||||
function ItemTypeCheckboxes(props: ItemTypeCheckboxesProps) { |
|
||||||
const songChange = (v: any) => { |
|
||||||
props.onChange({ |
|
||||||
[QueryKeys.Songs]: v.target.checked, |
|
||||||
[QueryKeys.Artists]: props.types[QueryKeys.Artists], |
|
||||||
[QueryKeys.Tags]: props.types[QueryKeys.Tags] |
|
||||||
}); |
|
||||||
} |
|
||||||
const artistChange = (v: any) => { |
|
||||||
props.onChange({ |
|
||||||
[QueryKeys.Songs]: props.types[QueryKeys.Songs], |
|
||||||
[QueryKeys.Artists]: v.target.checked, |
|
||||||
[QueryKeys.Tags]: props.types[QueryKeys.Tags] |
|
||||||
}); |
|
||||||
} |
|
||||||
const tagChange = (v: any) => { |
|
||||||
props.onChange({ |
|
||||||
[QueryKeys.Songs]: props.types[QueryKeys.Songs], |
|
||||||
[QueryKeys.Artists]: props.types[QueryKeys.Artists], |
|
||||||
[QueryKeys.Tags]: v.target.checked |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
return <FormControl component='fieldset'> |
|
||||||
<FormLabel component='legend'>Result types</FormLabel> |
|
||||||
<FormGroup> |
|
||||||
<FormControlLabel |
|
||||||
control={<Checkbox checked={props.types[QueryKeys.Songs]} onChange={songChange} name='Songs' />} |
|
||||||
label="Songs" |
|
||||||
/> |
|
||||||
<FormControlLabel |
|
||||||
control={<Checkbox checked={props.types[QueryKeys.Artists]} onChange={artistChange} name='Artists' />} |
|
||||||
label="Artists" |
|
||||||
/> |
|
||||||
<FormControlLabel |
|
||||||
control={<Checkbox checked={props.types[QueryKeys.Tags]} onChange={tagChange} name='Tags' />} |
|
||||||
label="Tags" |
|
||||||
/> |
|
||||||
</FormGroup> |
|
||||||
</FormControl>; |
|
||||||
} |
|
||||||
|
|
||||||
interface OrderingWidgetProps { |
|
||||||
ordering: QueryOrdering, |
|
||||||
onChange: (o: QueryOrdering) => void; |
|
||||||
} |
|
||||||
|
|
||||||
function OrderingWidget(props: OrderingWidgetProps) { |
|
||||||
const onTypeChange = (e: any) => { |
|
||||||
props.onChange({ |
|
||||||
[QueryKeys.OrderBy]: { |
|
||||||
[QueryKeys.OrderKey]: e.target.value, |
|
||||||
}, |
|
||||||
[QueryKeys.Ascending]: props.ordering[QueryKeys.Ascending], |
|
||||||
}); |
|
||||||
} |
|
||||||
const onAscendingChange = (e: any) => { |
|
||||||
props.onChange({ |
|
||||||
[QueryKeys.OrderBy]: props.ordering[QueryKeys.OrderBy], |
|
||||||
[QueryKeys.Ascending]: (e.target.value === 'asc'), |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
return <FormControl component='fieldset'> |
|
||||||
<FormLabel component='legend'>Ordering</FormLabel> |
|
||||||
<FormGroup> |
|
||||||
<Select |
|
||||||
onChange={onTypeChange} |
|
||||||
value={props.ordering[QueryKeys.OrderBy][QueryKeys.OrderKey]} |
|
||||||
> |
|
||||||
<MenuItem value={OrderKey.Name}>Name</MenuItem> |
|
||||||
</Select> |
|
||||||
<Select |
|
||||||
onChange={onAscendingChange} |
|
||||||
value={props.ordering[QueryKeys.Ascending] ? 'asc' : 'desc'} |
|
||||||
> |
|
||||||
<MenuItem value={'asc'}>Ascending</MenuItem> |
|
||||||
<MenuItem value={'desc'}>Descending</MenuItem> |
|
||||||
</Select> |
|
||||||
</FormGroup> |
|
||||||
</FormControl>; |
|
||||||
} |
|
||||||
|
|
||||||
function toServerOrdering(o: QueryOrdering | undefined): serverApi.Ordering { |
|
||||||
if (!o) { |
|
||||||
return { |
|
||||||
orderBy: { |
|
||||||
type: serverApi.OrderByType.Name |
|
||||||
}, |
|
||||||
ascending: true |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
const keys = { |
|
||||||
[OrderKey.Name]: serverApi.OrderByType.Name, |
|
||||||
}; |
|
||||||
|
|
||||||
return { |
|
||||||
orderBy: { |
|
||||||
type: keys[o[QueryKeys.OrderBy][QueryKeys.OrderKey]] |
|
||||||
}, |
|
||||||
ascending: o[QueryKeys.Ascending], |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export interface IProps { |
|
||||||
query: Query | undefined, |
|
||||||
typesIncluded: TypesIncluded | undefined, |
|
||||||
resultOrder: QueryOrdering | undefined, |
|
||||||
onQueryChange: (q: Query) => void, |
|
||||||
onTypesChange: (t: TypesIncluded) => void, |
|
||||||
onOrderChange: (o: QueryOrdering) => void, |
|
||||||
} |
|
||||||
|
|
||||||
export default function QueryBrowseWindow(props: IProps) { |
|
||||||
const [songs, setSongs] = useState<serverApi.SongDetails[]>([]); |
|
||||||
const [artists, setArtists] = useState<serverApi.ArtistDetails[]>([]); |
|
||||||
//const [tags, setTags] = useState<serverApi.TagDetails[]>([]);
|
|
||||||
|
|
||||||
var items: Item[] = []; |
|
||||||
props.typesIncluded && props.typesIncluded[QueryKeys.Songs] && items.push(...songs); |
|
||||||
props.typesIncluded && props.typesIncluded[QueryKeys.Artists] && items.push(...artists); |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
if (!props.query) { return; } |
|
||||||
const q = _.cloneDeep(props.query); |
|
||||||
const r = _.cloneDeep(props.resultOrder); |
|
||||||
const t = _.cloneDeep(props.typesIncluded); |
|
||||||
|
|
||||||
const request: serverApi.QueryRequest = { |
|
||||||
query: toApiQuery(props.query), |
|
||||||
offsetsLimits: { |
|
||||||
songOffset: 0, |
|
||||||
songLimit: 5, // TODO
|
|
||||||
artistOffset: 0, |
|
||||||
artistLimit: 5, |
|
||||||
tagOffset: 0, |
|
||||||
tagLimit: 5, |
|
||||||
}, |
|
||||||
ordering: toServerOrdering(props.resultOrder), |
|
||||||
} |
|
||||||
const requestOpts = { |
|
||||||
method: 'POST', |
|
||||||
headers: { 'Content-Type': 'application/json' }, |
|
||||||
body: JSON.stringify(request) |
|
||||||
}; |
|
||||||
fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) |
|
||||||
.then((response: any) => response.json()) |
|
||||||
.then((json: any) => { |
|
||||||
const match = _.isEqual(q, props.query) && _.isEqual(r, props.resultOrder) && _.isEqual(t, props.typesIncluded); |
|
||||||
'songs' in json && match && setSongs(json.songs); |
|
||||||
'artists' in json && match && setArtists(json.artists); |
|
||||||
}); |
|
||||||
}, [ props.query, props.resultOrder, props.typesIncluded ]); |
|
||||||
|
|
||||||
return <> |
|
||||||
<FormControl component='fieldset'> |
|
||||||
<FormLabel component='legend'>Query</FormLabel> |
|
||||||
<FilterControl |
|
||||||
query={props.query} |
|
||||||
onChangeQuery={props.onQueryChange} |
|
||||||
/> |
|
||||||
</FormControl> |
|
||||||
<ItemTypeCheckboxes |
|
||||||
types={props.typesIncluded || { |
|
||||||
[QueryKeys.Songs]: true, |
|
||||||
[QueryKeys.Artists]: true, |
|
||||||
[QueryKeys.Tags]: true, |
|
||||||
}} |
|
||||||
onChange={props.onTypesChange} |
|
||||||
/> |
|
||||||
<OrderingWidget |
|
||||||
ordering={props.resultOrder || { |
|
||||||
[QueryKeys.OrderBy]: { |
|
||||||
[QueryKeys.OrderKey]: OrderKey.Name |
|
||||||
}, |
|
||||||
[QueryKeys.Ascending]: true |
|
||||||
}} |
|
||||||
onChange={props.onOrderChange} |
|
||||||
/> |
|
||||||
<BrowseWindow items={items} /> |
|
||||||
</> |
|
||||||
} |
|
@ -0,0 +1,37 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; |
||||||
|
import { QBQueryElem } from './querybuilder/QBQueryElem'; |
||||||
|
import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryElem, QueryNodeElem, queryOr, queryAnd } from '../lib/Query'; |
||||||
|
|
||||||
|
const darkTheme = createMuiTheme({ |
||||||
|
palette: { |
||||||
|
type: 'dark' |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
export default function Window(props: any) { |
||||||
|
let queens = new QueryLeafElem( |
||||||
|
QueryLeafBy.ArtistName, |
||||||
|
QueryLeafOp.Equals, |
||||||
|
"Queens of the Stone Age" |
||||||
|
); |
||||||
|
let muse = new QueryLeafElem( |
||||||
|
QueryLeafBy.ArtistName, |
||||||
|
QueryLeafOp.Equals, |
||||||
|
"Muse" |
||||||
|
); |
||||||
|
let dawnbros = new QueryLeafElem( |
||||||
|
QueryLeafBy.ArtistName, |
||||||
|
QueryLeafOp.Equals, |
||||||
|
"Dawn Brothers" |
||||||
|
); |
||||||
|
let query = queryOr( |
||||||
|
queryAnd(queens, muse), |
||||||
|
dawnbros |
||||||
|
); |
||||||
|
|
||||||
|
return <ThemeProvider theme={darkTheme}> |
||||||
|
<CssBaseline /> |
||||||
|
<QBQueryElem elem={query} /> |
||||||
|
</ThemeProvider> |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { Box, Paper } from '@material-ui/core'; |
||||||
|
|
||||||
|
export default function QBAndBlock(props: any) { |
||||||
|
return <Paper elevation={3}> |
||||||
|
<Box display="flex" flexDirection="column" alignItems="center"> |
||||||
|
<Box m={0.5} /> |
||||||
|
{props.children.map((child: any) => { |
||||||
|
return <Box m={0.5}> |
||||||
|
{child} |
||||||
|
</Box> |
||||||
|
})} |
||||||
|
<Box m={0.5} /> |
||||||
|
</Box> |
||||||
|
</Paper> |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { Box, Typography } from '@material-ui/core'; |
||||||
|
|
||||||
|
export default function QBOrBlock(props: any) { |
||||||
|
const firstChild = Array.isArray(props.children) && props.children.length >= 1 ? |
||||||
|
props.children[0] : undefined; |
||||||
|
|
||||||
|
const otherChildren = Array.isArray(props.children) && props.children.length > 1 ? |
||||||
|
props.children.slice(1) : []; |
||||||
|
|
||||||
|
return <Box display="flex" alignItems="center"> |
||||||
|
<Box m={1}> |
||||||
|
{firstChild} |
||||||
|
</Box> |
||||||
|
{otherChildren.map((child: any) => { |
||||||
|
return <> |
||||||
|
<Box m={1}> |
||||||
|
<Typography variant="button">Or</Typography> |
||||||
|
</Box> |
||||||
|
<Box m={1}> |
||||||
|
{child} |
||||||
|
</Box> |
||||||
|
</>; |
||||||
|
})} |
||||||
|
</Box> |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { QueryLeafElem, QueryNodeElem } from '../../lib/Query'; |
||||||
|
import { QBQueryLeafElem } from './QBQueryLeafElem'; |
||||||
|
import { QBQueryNodeElem } from './QBQueryNodeElem'; |
||||||
|
|
||||||
|
export interface IProps { |
||||||
|
elem: QueryLeafElem | QueryNodeElem, |
||||||
|
} |
||||||
|
|
||||||
|
export function QBQueryElem(props: IProps) { |
||||||
|
let e = props.elem; |
||||||
|
|
||||||
|
let renderLeaf = (l: any) => { |
||||||
|
return <QBQueryLeafElem elem={l} /> |
||||||
|
} |
||||||
|
|
||||||
|
if (e instanceof QueryLeafElem) { |
||||||
|
return renderLeaf(e); |
||||||
|
} else if (e instanceof QueryNodeElem) { |
||||||
|
return <QBQueryNodeElem elem={e} |
||||||
|
renderLeaf={renderLeaf} /> |
||||||
|
} |
||||||
|
|
||||||
|
throw "Unsupported query element"; |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryNodeElem, QueryNodeOp } from '../../lib/Query'; |
||||||
|
import { Chip, Typography } from '@material-ui/core'; |
||||||
|
|
||||||
|
export interface LeafProps { |
||||||
|
elem: QueryLeafElem |
||||||
|
} |
||||||
|
|
||||||
|
export function QBQueryElemArtistEquals(props: LeafProps) { |
||||||
|
let e = props.elem; |
||||||
|
|
||||||
|
const label = <Typography> |
||||||
|
By {e.b} |
||||||
|
</Typography>; |
||||||
|
|
||||||
|
return <Chip label={label} /> |
||||||
|
} |
||||||
|
|
||||||
|
export function QBQueryLeafElem(props: LeafProps) { |
||||||
|
let e = props.elem; |
||||||
|
|
||||||
|
if (e.a == QueryLeafBy.ArtistName && |
||||||
|
e.op == QueryLeafOp.Equals && |
||||||
|
typeof e.b == "string") { |
||||||
|
return <QBQueryElemArtistEquals {...props} /> |
||||||
|
} |
||||||
|
|
||||||
|
throw "Unsupported leaf element"; |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import QBOrBlock from './QBOrBlock'; |
||||||
|
import QBAndBlock from './QBAndBlock'; |
||||||
|
import { QueryNodeElem, QueryNodeOp } from '../../lib/Query'; |
||||||
|
|
||||||
|
export interface NodeProps { |
||||||
|
elem: QueryNodeElem, |
||||||
|
renderLeaf: (leaf: any) => any, |
||||||
|
} |
||||||
|
|
||||||
|
export function QBQueryNodeElem(props: NodeProps) { |
||||||
|
let e = props.elem; |
||||||
|
|
||||||
|
if (e.op == QueryNodeOp.And) { |
||||||
|
return <QBAndBlock> |
||||||
|
{e.operands.map((o: any) => { |
||||||
|
if(o instanceof QueryNodeElem) { |
||||||
|
return <QBQueryNodeElem elem={o} renderLeaf={props.renderLeaf}/> |
||||||
|
} |
||||||
|
return props.renderLeaf(o); |
||||||
|
})} |
||||||
|
</QBAndBlock> |
||||||
|
} else if (e.op == QueryNodeOp.Or) { |
||||||
|
return <QBOrBlock> |
||||||
|
{e.operands.map((o: any) => { |
||||||
|
if(o instanceof QueryNodeElem) { |
||||||
|
return <QBQueryNodeElem elem={o} renderLeaf={props.renderLeaf}/> |
||||||
|
} |
||||||
|
return props.renderLeaf(o); |
||||||
|
})} |
||||||
|
</QBOrBlock> |
||||||
|
} |
||||||
|
|
||||||
|
throw "Unsupported node element"; |
||||||
|
} |
@ -0,0 +1,63 @@ |
|||||||
|
export enum QueryLeafBy { |
||||||
|
ArtistName = 0, |
||||||
|
AlbumName, |
||||||
|
TagName, |
||||||
|
SongTitle |
||||||
|
} |
||||||
|
|
||||||
|
export enum QueryLeafOp { |
||||||
|
Equals = 0, |
||||||
|
Like, |
||||||
|
} |
||||||
|
|
||||||
|
export type QueryLeafOperand = string | number; |
||||||
|
|
||||||
|
export class QueryLeafElem { |
||||||
|
a: QueryLeafBy; |
||||||
|
op: QueryLeafOp; |
||||||
|
b: QueryLeafOperand; |
||||||
|
|
||||||
|
constructor( |
||||||
|
a: QueryLeafBy, |
||||||
|
op: QueryLeafOp, |
||||||
|
b: QueryLeafOperand |
||||||
|
) { |
||||||
|
this.a = a; |
||||||
|
this.op = op; |
||||||
|
this.b = b; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
export enum QueryNodeOp { |
||||||
|
And = 0, |
||||||
|
Or, |
||||||
|
} |
||||||
|
|
||||||
|
export class QueryNodeElem { |
||||||
|
operands: QueryElem[]; |
||||||
|
op: QueryNodeOp; |
||||||
|
|
||||||
|
constructor( |
||||||
|
operands: QueryElem[], |
||||||
|
op: QueryNodeOp |
||||||
|
) { |
||||||
|
this.operands = operands; |
||||||
|
this.op = op; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function queryOr(...args: QueryElem[]) { |
||||||
|
return new QueryNodeElem( |
||||||
|
args, |
||||||
|
QueryNodeOp.Or |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function queryAnd(...args: QueryElem[]) { |
||||||
|
return new QueryNodeElem( |
||||||
|
args, |
||||||
|
QueryNodeOp.And |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export type QueryElem = QueryLeafElem | QueryNodeElem; |
@ -1,44 +0,0 @@ |
|||||||
export interface SongDisplayItem { |
|
||||||
title:String, |
|
||||||
artistNames:String[], |
|
||||||
tagNames:String[], |
|
||||||
storeLinks: { |
|
||||||
icon: JSX.Element, |
|
||||||
url: String, |
|
||||||
}[] |
|
||||||
} |
|
||||||
|
|
||||||
export interface LoadingSongDisplayItem { |
|
||||||
loadingSong: boolean, |
|
||||||
} |
|
||||||
|
|
||||||
export interface ArtistDisplayItem { |
|
||||||
name:String, |
|
||||||
tagNames:String[], |
|
||||||
storeLinks: { |
|
||||||
icon: JSX.Element, |
|
||||||
url: String, |
|
||||||
}[] |
|
||||||
} |
|
||||||
|
|
||||||
export interface LoadingArtistDisplayItem { |
|
||||||
loadingArtist: boolean, |
|
||||||
} |
|
||||||
|
|
||||||
export type DisplayItem = SongDisplayItem | LoadingSongDisplayItem | ArtistDisplayItem | LoadingArtistDisplayItem; |
|
||||||
|
|
||||||
export function isSong(item: DisplayItem): item is SongDisplayItem { |
|
||||||
return "title" in item; |
|
||||||
} |
|
||||||
|
|
||||||
export function isLoadingSong(item: DisplayItem): item is LoadingSongDisplayItem { |
|
||||||
return "loadingSong" in item; |
|
||||||
} |
|
||||||
|
|
||||||
export function isArtist(item: DisplayItem): item is ArtistDisplayItem { |
|
||||||
return "name" in item; |
|
||||||
} |
|
||||||
|
|
||||||
export function isLoadingArtist(item: DisplayItem): item is LoadingArtistDisplayItem { |
|
||||||
return "loadingArtist" in item; |
|
||||||
} |
|
@ -1,3 +0,0 @@ |
|||||||
export const dragTypes = { |
|
||||||
ListItem: 'list item' |
|
||||||
} |
|
@ -1,129 +0,0 @@ |
|||||||
import { QueryElemProperty, QueryFilterOp, QueryElemOp } from '../api'; |
|
||||||
|
|
||||||
export enum QueryKeys { |
|
||||||
TitleLike = 'tl', |
|
||||||
ArtistLike = 'al', |
|
||||||
AndQuerySignature = 'and', |
|
||||||
OrQuerySignature = 'or', |
|
||||||
OperandA = 'a', |
|
||||||
OperandB = 'b', |
|
||||||
Name = 'n', |
|
||||||
ArtistRanking = 'an', |
|
||||||
TagRanking = 'tn', |
|
||||||
Songs = 's', |
|
||||||
Artists = 'at', |
|
||||||
Tags = 't', |
|
||||||
OrderBy = 'ob', |
|
||||||
OrderKey = 'ok', |
|
||||||
Ascending = 'asc' |
|
||||||
} |
|
||||||
|
|
||||||
export interface TitleQuery { |
|
||||||
[QueryKeys.TitleLike]: String |
|
||||||
}; |
|
||||||
export function isTitleQuery(q: Query): q is TitleQuery { |
|
||||||
return QueryKeys.TitleLike in q; |
|
||||||
} |
|
||||||
export function TitleToApiQuery(q: TitleQuery) { |
|
||||||
return { |
|
||||||
'prop': QueryElemProperty.songTitle, |
|
||||||
'propOperand': '%' + q[QueryKeys.TitleLike] + '%', |
|
||||||
'propOperator': QueryFilterOp.Like, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export interface ArtistQuery { |
|
||||||
[QueryKeys.ArtistLike]: String |
|
||||||
}; |
|
||||||
export function isArtistQuery(q: Query): q is ArtistQuery { |
|
||||||
return QueryKeys.ArtistLike in q; |
|
||||||
} |
|
||||||
export function ArtistToApiQuery(q: ArtistQuery) { |
|
||||||
return { |
|
||||||
'prop': QueryElemProperty.artistName, |
|
||||||
'propOperand': '%' + q[QueryKeys.ArtistLike] + '%', |
|
||||||
'propOperator': QueryFilterOp.Like, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export interface AndQuery<T> { |
|
||||||
[QueryKeys.AndQuerySignature]: any, |
|
||||||
[QueryKeys.OperandA]: T, |
|
||||||
[QueryKeys.OperandB]: T, |
|
||||||
} |
|
||||||
export function isAndQuery(q: Query): q is AndQuery<Query> { |
|
||||||
return QueryKeys.AndQuerySignature in q; |
|
||||||
} |
|
||||||
export function AndToApiQuery(q: AndQuery<Query>) { |
|
||||||
return { |
|
||||||
'childrenOperator': QueryElemOp.And, |
|
||||||
'children': [ |
|
||||||
toApiQuery(q.a), |
|
||||||
toApiQuery(q.b), |
|
||||||
] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export interface OrQuery<T> { |
|
||||||
[QueryKeys.OrQuerySignature]: any, |
|
||||||
[QueryKeys.OperandA]: T, |
|
||||||
[QueryKeys.OperandB]: T, |
|
||||||
} |
|
||||||
export function isOrQuery(q: Query): q is OrQuery<Query> { |
|
||||||
return QueryKeys.OrQuerySignature in q; |
|
||||||
} |
|
||||||
export function OrToApiQuery(q: OrQuery<Query>) { |
|
||||||
return { |
|
||||||
'childrenOperator': QueryElemOp.Or, |
|
||||||
'children': [ |
|
||||||
toApiQuery(q.a), |
|
||||||
toApiQuery(q.b), |
|
||||||
] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export type Query = TitleQuery | ArtistQuery | AndQuery<Query> | OrQuery<Query>; |
|
||||||
|
|
||||||
export enum OrderKey { |
|
||||||
Name = 'n', |
|
||||||
} |
|
||||||
|
|
||||||
export interface QueryOrdering { |
|
||||||
[QueryKeys.OrderBy]: { |
|
||||||
[QueryKeys.OrderKey]: OrderKey, |
|
||||||
} |
|
||||||
[QueryKeys.Ascending]: boolean, |
|
||||||
} |
|
||||||
|
|
||||||
export interface TypesIncluded { |
|
||||||
[QueryKeys.Songs]: boolean, |
|
||||||
[QueryKeys.Artists]: boolean, |
|
||||||
[QueryKeys.Tags]: boolean, |
|
||||||
} |
|
||||||
|
|
||||||
export function isQuery(q: any): q is Query { |
|
||||||
return q != null && |
|
||||||
(isTitleQuery(q) || isArtistQuery(q) || isAndQuery(q) || isOrQuery(q)); |
|
||||||
} |
|
||||||
|
|
||||||
export function isQueryOrdering(q: any): q is QueryOrdering { |
|
||||||
return q != null && |
|
||||||
QueryKeys.OrderBy in q && |
|
||||||
QueryKeys.OrderKey in q[QueryKeys.OrderBy] && |
|
||||||
QueryKeys.Ascending in q; |
|
||||||
} |
|
||||||
|
|
||||||
export function isTypesIncluded(q: any): q is TypesIncluded { |
|
||||||
return q != null && |
|
||||||
QueryKeys.Songs in q && |
|
||||||
QueryKeys.Artists in q && |
|
||||||
QueryKeys.Tags in q; |
|
||||||
} |
|
||||||
|
|
||||||
export function toApiQuery(q: Query): any { |
|
||||||
return (isTitleQuery(q) && TitleToApiQuery(q)) || |
|
||||||
(isArtistQuery(q) && ArtistToApiQuery(q)) || |
|
||||||
(isAndQuery(q) && AndToApiQuery(q)) || |
|
||||||
(isOrQuery(q) && OrToApiQuery(q)) || |
|
||||||
{}; |
|
||||||
} |
|
Loading…
Reference in new issue