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