Merge pull request 'User interface overhaul' (#16) from ui_overhaul into master

Reviewed-on: #16
pull/20/head
Sander Vocke 5 years ago
commit fb4439dec9
  1. 5
      client/package-lock.json
  2. 1
      client/package.json
  3. 108
      client/public/logo.svg
  4. 124
      client/src/App.tsx
  5. 9
      client/src/api.ts
  6. 115
      client/src/components/AppBar.tsx
  7. 68
      client/src/components/BrowseWindow.tsx
  8. 19
      client/src/components/DraggableItemListItem.tsx
  9. 46
      client/src/components/EditArtistDialog.tsx
  10. 85
      client/src/components/EditSongDialog.tsx
  11. 189
      client/src/components/FilterControl.tsx
  12. 24
      client/src/components/ItemList.tsx
  13. 18
      client/src/components/ItemListArtistItem.tsx
  14. 19
      client/src/components/ItemListItem.tsx
  15. 35
      client/src/components/ItemListLoadedArtistItem.tsx
  16. 41
      client/src/components/ItemListLoadedSongItem.tsx
  17. 22
      client/src/components/ItemListLoadingArtistItem.tsx
  18. 22
      client/src/components/ItemListLoadingSongItem.tsx
  19. 18
      client/src/components/ItemListSongItem.tsx
  20. 198
      client/src/components/QueryBrowseWindow.tsx
  21. 178
      client/src/components/Window.tsx
  22. 63
      client/src/components/querybuilder/QBAddElemMenu.tsx
  23. 16
      client/src/components/querybuilder/QBAndBlock.tsx
  24. 15
      client/src/components/querybuilder/QBEditButton.tsx
  25. 122
      client/src/components/querybuilder/QBLeafElem.tsx
  26. 47
      client/src/components/querybuilder/QBNodeElem.tsx
  27. 33
      client/src/components/querybuilder/QBOrBlock.tsx
  28. 41
      client/src/components/querybuilder/QBPlaceholder.tsx
  29. 34
      client/src/components/querybuilder/QBQueryElem.tsx
  30. 121
      client/src/components/querybuilder/QBSelectWithRequest.tsx
  31. 50
      client/src/components/querybuilder/QueryBuilder.tsx
  32. 49
      client/src/components/tables/ResultsTable.tsx
  33. 186
      client/src/lib/query/Query.tsx
  34. 13
      client/src/lib/stringifyList.tsx
  35. 44
      client/src/types/DisplayItem.tsx
  36. 3
      client/src/types/DragTypes.tsx
  37. 129
      client/src/types/Query.tsx
  38. 2
      client/tsconfig.json
  39. 108
      resources/logo.svg
  40. 129
      resources/logo_src.svg
  41. 111
      server/endpoints/QueryEndpointHandler.ts
  42. 2
      server/knexfile.ts

@ -8698,6 +8698,11 @@
"react-double-scrollbar": "0.0.15" "react-double-scrollbar": "0.0.15"
} }
}, },
"material-ui-nested-menu-item": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/material-ui-nested-menu-item/-/material-ui-nested-menu-item-1.0.2.tgz",
"integrity": "sha512-LZb8xI0FrAI/A3P2vT3CB9bmSoOFWOK0dikTc1t9VvEpp1a8hZkbVUz7VhETnoLUYu3NXCkgulmXcl3zitqI9A=="
},
"md5.js": { "md5.js": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",

@ -18,6 +18,7 @@
"jsurl": "^0.1.5", "jsurl": "^0.1.5",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"material-table": "^1.69.0", "material-table": "^1.69.0",
"material-ui-nested-menu-item": "^1.0.2",
"react": "^16.13.1", "react": "^16.13.1",
"react-dnd": "^11.1.3", "react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3", "react-dnd-html5-backend": "^11.1.3",

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="logo.svg"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
id="svg18727"
version="1.1"
viewBox="0 0 23.655632 6.0053663"
height="6.0053663mm"
width="23.655632mm">
<defs
id="defs18721" />
<sodipodi:namedview
fit-margin-bottom="0"
fit-margin-right="0"
fit-margin-left="0"
fit-margin-top="0"
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="0"
inkscape:window-height="1003"
inkscape:window-width="1920"
showgrid="false"
inkscape:document-rotation="0"
inkscape:current-layer="text19329-1"
inkscape:document-units="mm"
inkscape:cy="62.911439"
inkscape:cx="102.24884"
inkscape:zoom="3.2732474"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base" />
<metadata
id="metadata18724">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(-3.3979867,-3.1434163)"
id="layer1"
inkscape:groupmode="layer"
inkscape:label="Layer 1">
<g
id="g19333-7"
transform="matrix(0.08007155,0,0,0.08007155,-1.5581106,-3.4995302)">
<circle
r="37.5"
cy="120.46263"
cx="99.395859"
id="path19290-0"
style="opacity:1;fill:#000000;fill-opacity:1;stroke-width:0.250376" />
<path
sodipodi:nodetypes="ccccccccccccccccccc"
d="m 92.982378,89.657996 c -3.214522,2.093337 -4.24212,7.031009 -1.108874,9.671786 5.24273,4.451138 13.265596,4.436238 19.302696,1.673558 1.73254,-0.17246 4.05826,-4.268097 4.65801,-1.184616 l 9.1469,19.305016 c -11.78326,11.26664 -8.10053,7.66991 -19.42383,19.3183 -2.27661,-2.56474 -3.40773,-6.38658 -5.13269,-9.49333 -5.473243,-11.38823 -3.832091,-7.85153 -9.287489,-19.24832 -5.536267,-5.31929 -14.559415,-4.8235 -20.943976,-1.35995 -4.000132,1.68472 -6.517664,7.40602 -2.846358,10.67593 4.390452,4.12727 11.194164,4.2922 16.706571,2.76198 2.403413,0.009 5.557982,-3.86321 6.945922,-3.08654 7.573174,15.89575 7.904958,16.92939 15.67079,32.73148 14.19609,-12.10634 14.03749,-12.17528 26.75668,-24.28851 -5.8538,-13.45436 -5.43451,-11.62083 -11.81526,-24.83794 -2.08946,-4.119182 -3.9803,-8.395024 -6.19289,-12.417086 -4.06756,-4.102075 -10.44295,-4.610675 -15.796809,-3.242783 -2.304682,0.751118 -4.652063,1.603012 -6.639433,3.021035 z"
style="fill:#ffffff;fill-opacity:1;stroke-width:0.31796"
id="path19306-8" />
</g>
<g
style="font-style:normal;font-weight:normal;font-size:50.8px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
id="text19329-1"
aria-label="muDBase">
<path
id="path19362"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 11.235629,4.5504098 h 0.11043 q 0.35887,0 0.61039,0.263788 0.22698,-0.263788 0.60425,-0.263788 h 0.11043 q 0.47236,0 0.76989,0.503036 0.092,0.177904 0.092,0.343537 v 2.512113 l -0.0644,0.06441 h -0.40488 l -0.0644,-0.06441 v -2.463037 q 0,-0.288325 -0.29753,-0.380344 l -0.0982,-0.01227 h -0.006 q -0.3282,0 -0.37728,0.432488 v 2.423165 l -0.0644,0.06441 h -0.40488 l -0.0644,-0.06441 v -2.423162 q -0.0491,-0.432488 -0.37727,-0.432488 h -0.0184 q -0.38341,0.05214 -0.38341,0.392614 v 2.463036 l -0.0644,0.06441 h -0.40488 l -0.0644,-0.06441 v -2.512112 q 0,-0.453961 0.49997,-0.748421 0.18711,-0.09815 0.36194,-0.09815 z" />
<path
id="path19364"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 13.750809,4.5381408 h 0.40489 l 0.0644,0.06441 v 2.472238 q 0.0613,0.395681 0.37728,0.395681 h 0.0184 q 0.38341,-0.06135 0.38341,-0.392614 v -2.475305 l 0.0644,-0.06441 h 0.40489 l 0.0644,0.06441 v 2.536651 q 0,0.432488 -0.49997,0.739217 -0.19631,0.09509 -0.34967,0.09509 h -0.14723 q -0.36501,0 -0.67787,-0.374211 -0.17177,-0.24845 -0.17177,-0.472362 v -2.524382 z" />
<path
id="path19366"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 16.928529,3.7038368 h 0.12269 q 0.71161,0 1.13796,0.763756 0.10429,0.260721 0.10429,0.463162 v 2.024412 q 0,0.460094 -0.48156,0.831237 -0.2914,0.187105 -0.55212,0.187105 h -0.34353 q -0.59199,0 -0.96927,-0.680939 -0.0767,-0.236182 -0.0767,-0.509171 v -1.322003 l 0.0644,-0.06441 h 0.41715 l 0.0644,0.06441 v 1.444694 q 0,0.395682 0.43556,0.545979 0.0368,0.0184 0.10122,0.0184 h 0.24538 q 0.41408,0 0.54904,-0.475431 l 0.0123,-0.104288 v -1.935462 q 0,-0.466228 -0.50917,-0.705476 -0.13189,-0.04294 -0.22391,-0.04294 h -0.0736 q -0.46623,0 -0.69934,0.539843 -0.0521,0.30673 -0.13496,0.30673 h -0.36807 l -0.0644,-0.06441 v -0.05828 q 0,-0.693208 0.76376,-1.119562 0.26685,-0.107356 0.4785,-0.107356 z" />
<path
id="path19368"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 19.480519,3.7038368 h 0.34353 q 0.51224,0 0.87725,0.524507 0.1687,0.272989 0.1687,0.579719 0,0.484632 -0.37421,0.809765 0.13803,0.06748 0.35274,0.365007 0.20551,0.322067 0.20551,0.641064 v 0.122692 q 0,0.693209 -0.76376,1.119562 -0.26685,0.107356 -0.4785,0.107356 h -0.12269 q -0.71161,0 -1.13796,-0.763756 -0.10429,-0.26072 -0.10429,-0.463162 v -2.024412 q 0,-0.460095 0.48156,-0.831236 0.29753,-0.187106 0.55212,-0.187106 z m -0.49997,1.082755 v 1.935461 q 0,0.466229 0.50917,0.705477 0.13496,0.04294 0.22391,0.04294 h 0.0736 q 0.44783,0 0.68708,-0.509171 0.046,-0.141096 0.046,-0.239249 v -0.07361 q 0,-0.466228 -0.50304,-0.702409 l -0.35581,-0.07055 v -0.392614 q 0,-0.08282 0.2822,-0.119624 0.38034,-0.187105 0.38034,-0.518373 v -0.07361 q 0,-0.39568 -0.43556,-0.545978 -0.0368,-0.0184 -0.10122,-0.0184 h -0.24538 q -0.40488,0 -0.54904,0.463162 z" />
<path
id="path19370"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 22.069309,4.5504098 h 0.13496 q 0.4785,0 0.7975,0.570517 0.0521,0.190172 0.0521,0.288325 v 1.705414 q 0,0.558248 -0.6656,0.831236 l -0.1779,0.02761 h -0.1411 q -0.5153,0 -0.80976,-0.582785 -0.0522,-0.150298 -0.0522,-0.263788 v -0.110423 q 0,-0.472363 0.57972,-0.822034 0.73308,-0.518372 0.73308,-0.772957 0,-0.257654 -0.319,-0.368076 h -0.0951 q -0.26379,0 -0.36501,0.325133 0,0.141096 -0.10122,0.141096 h -0.36808 l -0.0644,-0.06441 v -0.05828 q 0,-0.509171 0.59199,-0.788295 0.12882,-0.05828 0.26992,-0.05828 z m -0.3282,2.527449 q 0,0.294461 0.31593,0.392614 h 0.0859 q 0.28832,0 0.37727,-0.349672 v -0.926322 h 0.0982 q -0.23311,0.214711 -0.79442,0.616526 -0.0828,0.116557 -0.0828,0.266854 z" />
<path
id="path19372"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 24.050779,4.5504098 h 0.15336 q 0.49384,0 0.7975,0.582786 0.0521,0.162566 0.0521,0.276056 v 0.04601 l -0.0644,0.06441 h -0.36807 q -0.0859,0 -0.13496,-0.239249 -0.13803,-0.22698 -0.30673,-0.22698 h -0.0982 q -0.23004,0 -0.34047,0.337402 v 0.05521 q 0,0.220845 0.82511,0.82817 0.48769,0.463161 0.48769,0.766823 v 0.07361 q 0,0.558248 -0.6656,0.831236 l -0.1779,0.02761 h -0.15336 q -0.49384,0 -0.7975,-0.582785 -0.0521,-0.165635 -0.0521,-0.276058 v -0.04601 l 0.0644,-0.06441 h 0.36807 q 0.0859,0 0.13496,0.239249 0.13803,0.22698 0.30673,0.22698 h 0.0982 q 0.23004,0 0.34047,-0.337403 v -0.05521 q 0,-0.220845 -0.82511,-0.828169 -0.4877,-0.466228 -0.4877,-0.766823 v -0.07362 q 0,-0.558248 0.66561,-0.831236 z" />
<path
id="path19374"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 26.050659,4.5504098 h 0.14109 q 0.51531,0 0.80977,0.582786 0.0521,0.150298 0.0521,0.263787 v 0.110423 q 0,0.472363 -0.57972,0.822033 -0.69627,0.490767 -0.73308,0.74842 0.0644,0.392614 0.37728,0.392614 h 0.0245 q 0.3006,0 0.37728,-0.37421 0,-0.09202 0.10122,-0.09202 h 0.36808 l 0.0644,0.06441 v 0.05828 q 0,0.50917 -0.59199,0.788294 -0.12576,0.05828 -0.26992,0.05828 h -0.13496 q -0.49383,0 -0.7975,-0.582785 -0.0521,-0.165635 -0.0521,-0.276058 v -1.705414 q 0,-0.561314 0.6656,-0.831236 z m -0.3098,1.779029 h -0.0981 q 0.23311,-0.214709 0.79442,-0.616525 0.0828,-0.116558 0.0828,-0.266855 0,-0.29446 -0.31593,-0.392613 h -0.0859 q -0.28833,0 -0.37728,0.349672 z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

@ -1,131 +1,25 @@
import React, { useEffect } from 'react'; import React from 'react';
import AppBar, { ActiveTab as AppBarActiveTab } from './components/AppBar';
import { Query, isQuery, QueryKeys, QueryOrdering, OrderKey, TypesIncluded, isTypesIncluded, isQueryOrdering } from './types/Query';
import QueryBrowseWindow from './components/QueryBrowseWindow';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import { import {
HashRouter as Router, HashRouter as Router,
Switch, Switch,
Route, Route
useHistory,
useLocation,
Redirect
} from "react-router-dom"; } from "react-router-dom";
import Window from './components/Window';
const JSURL = require('jsurl');
function fixQuery(q: any): Query {
if (!isQuery(q)) {
return {
[QueryKeys.TitleLike]: ''
};
}
return q;
}
function fixOrder(q: any): QueryOrdering {
if (!isQueryOrdering(q)) {
return {
[QueryKeys.OrderBy]: {
[QueryKeys.OrderKey]: OrderKey.Name,
},
[QueryKeys.Ascending]: true,
};
}
return q;
}
function fixTypes(q: any): TypesIncluded {
if (!isTypesIncluded(q)) {
return {
[QueryKeys.Songs]: true,
[QueryKeys.Artists]: false,
[QueryKeys.Tags]: false,
};
}
return q;
}
function AppBody() {
const history = useHistory();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const itemQuery: Query | undefined = JSURL.tryParse(queryParams.get('query'), undefined);
const itemOrder: QueryOrdering | undefined = JSURL.tryParse(queryParams.get('order'), undefined);
const itemTypes: TypesIncluded | undefined = JSURL.tryParse(queryParams.get('types'), undefined);
const pushQuery = (
q: Query,
o: QueryOrdering,
t: TypesIncluded
) => {
const newParams = new URLSearchParams(location.search);
newParams.set('query', JSURL.stringify(q));
newParams.set('order', JSURL.stringify(o));
newParams.set('types', JSURL.stringify(t));
history.push({
search: "?" + newParams.toString()
})
}
useEffect(() => {
const fq = fixQuery(itemQuery);
const fo = fixOrder(itemOrder);
const ft = fixTypes(itemTypes);
if (fq !== itemQuery || fo !== itemOrder || ft !== itemTypes) {
pushQuery(fq, fo, ft);
return;
}
}, [ itemOrder, itemQuery, itemTypes ]);
const onAppBarTabChange = (value: AppBarActiveTab) => {
switch (value) {
case AppBarActiveTab.Query: {
history.push('/query');
break;
}
}
}
const onQueryChange = (q: Query) => {
pushQuery(q, fixOrder(itemOrder), fixTypes(itemTypes));
}
const onOrderChange = (o: QueryOrdering) => {
pushQuery(fixQuery(itemQuery), o, fixTypes(itemTypes));
}
const onTypesChange = (t: TypesIncluded) => {
pushQuery(fixQuery(itemQuery), fixOrder(itemOrder), t);
}
return (
<div style={{ maxWidth: '100%' }}>
<Switch>
<Redirect exact from='/' to="/query" />
<Route path='/query'>
<AppBar activeTab={AppBarActiveTab.Query} onActiveTabChange={onAppBarTabChange} />
<QueryBrowseWindow
query={itemQuery}
typesIncluded={itemTypes}
resultOrder={itemOrder}
onQueryChange={onQueryChange}
onTypesChange={onTypesChange}
onOrderChange={onOrderChange}
/>
</Route>
</Switch>
</div>
);
}
function App() { function App() {
return ( return (
<Router> <Router>
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<AppBody /> <Switch>
<Route path="/">
<Window/>
</Route>
</Switch>
</DndProvider> </DndProvider>
</Router> </Router>
); );

@ -22,6 +22,14 @@ export interface ArtistDetails {
export function isArtistDetails(q: any): q is ArtistDetails { export function isArtistDetails(q: any): q is ArtistDetails {
return 'artistId' in q; return 'artistId' in q;
} }
export interface AlbumDetails {
albumId: number,
name: string,
storeLinks?: string[],
}
export function isAlbumDetails(q: any): q is ArtistDetails {
return 'albumId' in q;
}
export interface TagDetails { export interface TagDetails {
tagId: number, tagId: number,
name: string, name: string,
@ -45,6 +53,7 @@ export interface SongDetails {
songId: number, songId: number,
title: string, title: string,
artists?: ArtistDetails[], artists?: ArtistDetails[],
albums?: AlbumDetails[],
tags?: TagDetails[], tags?: TagDetails[],
storeLinks?: string[], storeLinks?: string[],
rankings?: RankingDetails[], rankings?: RankingDetails[],

@ -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,178 @@
import React, { useState, useEffect } from 'react';
import { ThemeProvider, CssBaseline, createMuiTheme, AppBar, Box } from '@material-ui/core';
import { QueryElem, toApiQuery } from '../lib/query/Query';
import QueryBuilder from './querybuilder/QueryBuilder';
import * as serverApi from '../api';
import { SongTable } from './tables/ResultsTable';
import stringifyList from '../lib/stringifyList';
var _ = require('lodash');
const darkTheme = createMuiTheme({
palette: {
type: 'dark'
},
});
export async function getArtists(filter: string) {
const query = filter.length > 0 ? {
prop: serverApi.QueryElemProperty.artistName,
propOperand: filter,
propOperator: serverApi.QueryFilterOp.Like,
} : {};
var q: serverApi.QueryRequest = {
query: query,
offsetsLimits: {
artistOffset: 0,
artistLimit: 100,
},
ordering: {
orderBy: {
type: serverApi.OrderByType.Name,
},
ascending: true,
},
};
const requestOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(q),
};
return (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json();
const names: string[] = json.artists.map((elem: any) => { return elem.name; });
return [...new Set(names)];
})();
}
export async function getSongTitles(filter: string) {
const query = filter.length > 0 ? {
prop: serverApi.QueryElemProperty.songTitle,
propOperand: filter,
propOperator: serverApi.QueryFilterOp.Like,
} : {};
var q: serverApi.QueryRequest = {
query: query,
offsetsLimits: {
songOffset: 0,
songLimit: 100,
},
ordering: {
orderBy: {
type: serverApi.OrderByType.Name,
},
ascending: true,
},
};
const requestOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(q),
};
return (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json();
const titles: string[] = json.songs.map((elem: any) => { return elem.title; });
return [...new Set(titles)];
})();
}
export default function Window(props: any) {
interface ResultsFor {
for: QueryElem,
results: any[],
};
const [query, setQuery] = useState<QueryElem | null>(null);
const [resultsFor, setResultsFor] = useState<ResultsFor | null>(null);
const loading = query && (!resultsFor || !_.isEqual(resultsFor.for, query));
const showResults = (query && resultsFor && query == resultsFor.for) ? resultsFor.results : [];
const songGetters = {
getTitle: (song: any) => song.title,
getArtist: (song: any) => stringifyList(song.artists, (a: any) => a.name),
getAlbum: (song: any) => stringifyList(song.albums, (a: any) => a.name),
}
const doQuery = async (_query: QueryElem) => {
var q: serverApi.QueryRequest = {
query: toApiQuery(_query),
offsetsLimits: {
songOffset: 0,
songLimit: 100,
},
ordering: {
orderBy: {
type: serverApi.OrderByType.Name,
},
ascending: true,
},
};
const requestOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(q),
};
return (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json();
if(_.isEqual(query, _query)) {
setResultsFor({
for: _query,
results: json.songs,
})
}
})();
}
useEffect(() => {
if (query) {
doQuery(query);
} else {
setResultsFor(null);
}
}, [query]);
return <ThemeProvider theme={darkTheme}>
<CssBaseline />
<AppBar position="static" style={{ background: 'grey' }}>
<Box m={0.5} display="flex" alignItems="center">
<img height="30px" src={process.env.PUBLIC_URL + "/logo.svg"} alt="error"></img>
</Box>
</AppBar>
<Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
width="80%"
>
<QueryBuilder
query={query}
onChangeQuery={setQuery}
requestFunctions={{
getArtists: getArtists,
getSongTitles: getSongTitles,
}}
/>
</Box>
<Box
m={1}
width="80%"
>
<SongTable
songs={showResults}
songGetters={songGetters}
/>
</Box>
</Box>
</ThemeProvider>
}

@ -0,0 +1,63 @@
import React from 'react';
import { Menu, MenuItem } from '@material-ui/core';
import NestedMenuItem from "material-ui-nested-menu-item";
import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../lib/query/Query';
import QBSelectWithRequest from './QBSelectWithRequest';
import { Requests } from './QueryBuilder';
export interface MenuProps {
anchorEl: null | HTMLElement,
onClose: () => void,
onCreateQuery: (q: QueryElem) => void,
requestFunctions: Requests,
}
export function QBAddElemMenu(props: MenuProps) {
let anchorEl = props.anchorEl;
let onClose = props.onClose;
return <Menu
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={onClose}
>
<MenuItem disabled={true}>New query element</MenuItem>
<NestedMenuItem
label="Song"
parentMenuOpen={Boolean(anchorEl)}
>
<QBSelectWithRequest
label="Title"
getNewOptions={props.requestFunctions.getSongTitles}
onSubmit={(s: string, exact: boolean) => {
onClose();
props.onCreateQuery({
a: QueryLeafBy.SongTitle,
leafOp: exact ? QueryLeafOp.Equals : QueryLeafOp.Like,
b: s
});
}}
style={{ width: 300 }}
/>
</NestedMenuItem>
<NestedMenuItem
label="Artist"
parentMenuOpen={Boolean(anchorEl)}
>
<QBSelectWithRequest
label="Name"
getNewOptions={props.requestFunctions.getArtists}
onSubmit={(s: string, exact: boolean) => {
onClose();
props.onCreateQuery({
a: QueryLeafBy.ArtistName,
leafOp: exact ? QueryLeafOp.Equals : QueryLeafOp.Like,
b: s
});
}}
style={{ width: 300 }}
/>
</NestedMenuItem>
</Menu >
}

@ -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, idx: number) => {
return <Box m={0.5} key={idx}>
{child}
</Box>
})}
<Box m={0.5} />
</Box>
</Paper>
}

@ -0,0 +1,15 @@
import React from 'react';
import { IconButton } from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
import CheckIcon from '@material-ui/icons/Check';
export interface IProps {
editing: boolean
}
export default function QBEditButton(props: any) {
return <IconButton {...props}>
{(!props.editing) && <SearchIcon style={{ fontSize: 80 }} />}
{(props.editing) && <CheckIcon style={{ fontSize: 80 }} />}
</IconButton>
}

@ -0,0 +1,122 @@
import React from 'react';
import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryElem } from '../../lib/query/Query';
import { Chip, Typography, IconButton, Box } from '@material-ui/core';
import { QBPlaceholder } from './QBPlaceholder';
import DeleteIcon from '@material-ui/icons/Delete';
import { Requests } from './QueryBuilder';
export interface ElemChipProps {
label: any,
extraElements?: any,
}
export function LabeledElemChip(props: ElemChipProps) {
const label = <Box display="flex" alignItems="center">
<Typography>{props.label}</Typography>
{props.extraElements}
</Box>
return <Chip label={label} />
}
export interface LeafProps {
elem: QueryLeafElem,
onReplace: (q: QueryElem) => void,
extraElements?: any,
}
export function QBQueryElemArtistEquals(props: LeafProps) {
return <LabeledElemChip
label={"By " + props.elem.b}
extraElements={props.extraElements}
/>
}
export function QBQueryElemArtistLike(props: LeafProps) {
return <LabeledElemChip label={"Artist like \"" + props.elem.b + "\""}
extraElements={props.extraElements}
/>
}
export function QBQueryElemTitleEquals(props: LeafProps) {
return <LabeledElemChip
label={"\"" + props.elem.b + "\""}
extraElements={props.extraElements}
/>
}
export function QBQueryElemTitleLike(props: LeafProps) {
return <LabeledElemChip
label={"Title like \"" + props.elem.b + "\""}
extraElements={props.extraElements}
/>
}
export interface DeleteButtonProps {
onClick?: (e: any) => void,
}
export function QBQueryElemDeleteButton(props: DeleteButtonProps) {
return <IconButton
onClick={props.onClick}
disableRipple={true}
size="small"
>
<DeleteIcon />
</IconButton>
}
export interface IProps {
elem: QueryLeafElem,
onReplace: (q: QueryElem | null) => void,
editingQuery: boolean,
requestFunctions: Requests,
}
export function QBLeafElem(props: IProps) {
let e = props.elem;
const extraElements = props.editingQuery ?
<Box m={0.5}>
<QBQueryElemDeleteButton
onClick={() => props.onReplace(null)}
/>
</Box>
: undefined;
if (e.a == QueryLeafBy.ArtistName &&
e.leafOp == QueryLeafOp.Equals &&
typeof e.b == "string") {
return <QBQueryElemArtistEquals
{...props}
extraElements={extraElements}
/>
} else if (e.a == QueryLeafBy.ArtistName &&
e.leafOp == QueryLeafOp.Like &&
typeof e.b == "string") {
return <QBQueryElemArtistLike
{...props}
extraElements={extraElements}
/>
} if (e.a == QueryLeafBy.SongTitle &&
e.leafOp == QueryLeafOp.Equals &&
typeof e.b == "string") {
return <QBQueryElemTitleEquals
{...props}
extraElements={extraElements}
/>
} else if (e.a == QueryLeafBy.SongTitle &&
e.leafOp == QueryLeafOp.Like &&
typeof e.b == "string") {
return <QBQueryElemTitleLike
{...props}
extraElements={extraElements}
/>
} else if (e.leafOp == QueryLeafOp.Placeholder) {
return <QBPlaceholder
onReplace={props.onReplace}
requestFunctions={props.requestFunctions}
/>
}
throw "Unsupported leaf element";
}

@ -0,0 +1,47 @@
import React from 'react';
import QBOrBlock from './QBOrBlock';
import QBAndBlock from './QBAndBlock';
import { QueryNodeElem, QueryNodeOp, QueryElem, isNodeElem, simplify } from '../../lib/query/Query';
import { QBLeafElem } from './QBLeafElem';
import { QBQueryElem } from './QBQueryElem';
import { O_APPEND } from 'constants';
import { Requests } from './QueryBuilder';
export interface NodeProps {
elem: QueryNodeElem,
onReplace: (q: QueryElem | null) => void,
editingQuery: boolean,
requestFunctions: Requests,
}
export function QBNodeElem(props: NodeProps) {
let e = props.elem;
const onReplace = (idx: number, q: QueryElem | null) => {
var ops = e.operands;
if (q) {
ops[idx] = q;
} else {
ops.splice(idx, 1);
}
let newNode = simplify({ operands: ops, nodeOp: e.nodeOp });
props.onReplace(newNode);
}
const children = e.operands.map((o: any, idx: number) => {
return <QBQueryElem
elem={o}
onReplace={(q: QueryElem | null) => onReplace(idx, q)}
editingQuery={props.editingQuery}
requestFunctions={props.requestFunctions}
/>
});
if (e.nodeOp == QueryNodeOp.And) {
return <QBAndBlock>{children}</QBAndBlock>
} else if (e.nodeOp == QueryNodeOp.Or) {
return <QBOrBlock>{children}</QBOrBlock>
}
throw "Unsupported node element";
}

@ -0,0 +1,33 @@
import React from 'react';
import { Box, Typography } from '@material-ui/core';
export interface IProps {
children: any,
}
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, idx: number) => {
return <Box display="flex" alignItems="center" key={idx}>
<Box m={1}>
<Typography variant="button">Or</Typography>
</Box>
<Box m={1}>
{child}
</Box>
</Box>;
})}
</Box>
}

@ -0,0 +1,41 @@
import React from 'react';
import { Chip } from '@material-ui/core';
import { QBAddElemMenu } from './QBAddElemMenu';
import { QueryElem } from '../../lib/query/Query';
import { Requests } from './QueryBuilder';
export interface IProps {
onReplace: (q: QueryElem) => void,
requestFunctions: Requests,
}
export function QBPlaceholder(props: IProps & any) {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const onOpen = (event: any) => {
setAnchorEl(event.currentTarget);
};
const onClose = () => {
setAnchorEl(null);
};
const onCreate = (q: QueryElem) => {
props.onReplace(q);
};
return <>
<Chip
variant="outlined"
label=""
style={{ width: "50px" }}
clickable={true}
onClick={onOpen}
component="div"
/>
<QBAddElemMenu
anchorEl={anchorEl}
onClose={onClose}
onCreateQuery={onCreate}
requestFunctions={props.requestFunctions}
/>
</>
}

@ -0,0 +1,34 @@
import React from 'react';
import { QueryLeafElem, QueryNodeElem, QueryElem, isLeafElem, isNodeElem } from '../../lib/query/Query';
import { QBLeafElem } from './QBLeafElem';
import { QBNodeElem } from './QBNodeElem';
import { Requests } from './QueryBuilder';
export interface IProps {
elem: QueryLeafElem | QueryNodeElem,
onReplace: (q: QueryElem | null) => void,
editingQuery: boolean,
requestFunctions: Requests,
}
export function QBQueryElem(props: IProps) {
let e = props.elem;
if (isLeafElem(e)) {
return <QBLeafElem
elem={e}
onReplace={props.onReplace}
editingQuery={props.editingQuery}
requestFunctions={props.requestFunctions}
/>
} else if (isNodeElem(e)) {
return <QBNodeElem
elem={e}
onReplace={props.onReplace}
editingQuery={props.editingQuery}
requestFunctions={props.requestFunctions}
/>
}
throw new Error("Unsupported query element");
}

@ -0,0 +1,121 @@
import React, { useState, useEffect } from 'react';
import TextField from '@material-ui/core/TextField';
import Autocomplete from '@material-ui/lab/Autocomplete';
import CircularProgress from '@material-ui/core/CircularProgress';
interface IProps {
getNewOptions: (textInput: string) => Promise<string[]>,
label: string,
onSubmit: (s: string, exactMatch: boolean) => void,
}
// Autocompleted combo box which can make asynchronous requests
// to get new options.
// Based on Material UI example: https://material-ui.com/components/autocomplete/
export default function QBSelectWithRequest(props: IProps & any) {
interface OptionsFor {
forInput: string,
options: string[],
};
const [open, setOpen] = useState(false);
const [options, setOptions] = useState<OptionsFor | null>(null);
const [input, setInput] = useState<string>("");
const { getNewOptions, label, onSubmit, ...restProps } = props;
const loading: boolean = !options || options.forInput !== input;
const updateOptions = (forInput: string, options: any[]) => {
if (forInput === input) {
console.log("setting options.");
setOptions({
forInput: forInput,
options: options,
});
}
}
const startRequest = (_input: string) => {
console.log('starting req', _input);
setInput(_input);
(async () => {
const newOptions = await getNewOptions(_input);
console.log('new options', newOptions);
updateOptions(_input, newOptions);
})();
};
// // Ensure a new request is made whenever the loading option is enabled.
// useEffect(() => {
// startRequest(input);
// }, []);
// Ensure options are cleared whenever the element is closed.
// useEffect(() => {
// if (!open) {
// setOptions(null);
// }
// }, [open]);
useEffect(() => {
startRequest(input);
}, [input]);
const onInputChange = (e: any, val: any, reason: any) => {
if (reason === 'reset') {
// User selected a preset option.
props.onSubmit(val, true);
} else {
// User changed text, start a new request.
setInput(val);
}
}
console.log("Render props:", props);
return (
<Autocomplete
{...restProps}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionSelected={(option, value) => option === value}
getOptionLabel={(option) => option}
options={options ? options.options : null}
loading={loading}
freeSolo={true}
value={input}
onInputChange={onInputChange}
onKeyDown={(e: any) => {
// Prevent the event from propagating, because
// that would trigger keyboard navigation of the menu.
e.stopPropagation();
if (e.key === 'Enter') {
// User submitted free-form value.
props.onSubmit(input, options && options.options.includes(input));
}
}}
renderInput={(params) => (
<TextField
{...params}
label={label}
variant="outlined"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
);
}

@ -0,0 +1,50 @@
import React, { useState } from 'react';
import { Box } from '@material-ui/core';
import QBQueryButton from './QBEditButton';
import { QBQueryElem } from './QBQueryElem';
import { QueryElem, addPlaceholders, removePlaceholders, simplify } from '../../lib/query/Query';
export interface Requests {
getArtists: (filter: string) => Promise<string[]>,
getSongTitles: (filter: string) => Promise<string[]>,
}
export interface IProps {
query: QueryElem | null,
onChangeQuery: (q: QueryElem | null) => void,
requestFunctions: Requests,
}
export default function QueryBuilder(props: IProps) {
const [editing, setEditing] = useState<boolean>(false);
const simpleQuery = simplify(props.query);
const showQuery = editing ?
addPlaceholders(simpleQuery, null) : simpleQuery;
const onReplace = (q: any) => {
const newQ = removePlaceholders(q);
setEditing(false);
props.onChangeQuery(newQ);
}
return <>
<Box display="flex" alignItems="center">
<Box m={2}>
<QBQueryButton
onClick={() => setEditing(!editing)}
editing={editing}
/>
</Box>
<Box m={2}>
{showQuery && <QBQueryElem
elem={showQuery}
onReplace={onReplace}
editingQuery={editing}
requestFunctions={props.requestFunctions}
/>}
</Box>
</Box>
</>
}

@ -0,0 +1,49 @@
import React from 'react';
import { TableContainer, Table, TableHead, TableRow, TableCell, Paper, makeStyles, TableBody } from '@material-ui/core';
export interface SongGetters {
getTitle: (song: any) => string,
getArtist: (song: any) => string,
getAlbum: (song: any) => string,
}
export interface IProps {
songs: any[],
songGetters: SongGetters,
}
export function SongTable(props: IProps) {
const useTableStyles = makeStyles({
table: {
minWidth: 650,
},
});
const classes = useTableStyles();
return (
<TableContainer component={Paper}>
<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>
</TableRow>
</TableHead>
<TableBody>
{props.songs.map((song:any) => {
const title = props.songGetters.getTitle(song);
const artist = props.songGetters.getArtist(song);
const album = props.songGetters.getAlbum(song);
return <TableRow key={title}>
<TableCell align="left">{title}</TableCell>
<TableCell align="left">{artist}</TableCell>
<TableCell align="left">{album}</TableCell>
</TableRow>
})}
</TableBody>
</Table>
</TableContainer>
);
}

@ -0,0 +1,186 @@
import * as serverApi from '../../api';
export enum QueryLeafBy {
ArtistName = 0,
AlbumName,
TagName,
SongTitle
}
export enum QueryLeafOp {
Equals = 0,
Like,
Placeholder, // Special op which indicates that this leaf is not filled in yet.
}
export type QueryLeafOperand = string | number;
export interface QueryLeafElem {
a: QueryLeafBy;
leafOp: QueryLeafOp;
b: QueryLeafOperand;
};
export function isLeafElem(q: QueryElem): q is QueryLeafElem {
return 'leafOp' in q;
}
export enum QueryNodeOp {
And = 0,
Or,
}
export interface QueryNodeElem {
operands: QueryElem[];
nodeOp: QueryNodeOp;
}
export function isNodeElem(q: QueryElem): q is QueryNodeElem {
return 'nodeOp' in q;
}
export function queryOr(...args: QueryElem[]) {
return {
operands: args,
nodeOp: QueryNodeOp.Or
}
}
export function queryAnd(...args: QueryElem[]) {
return {
operands: args,
nodeOp: QueryNodeOp.And
};
}
export type QueryElem = QueryLeafElem | QueryNodeElem;
// Take a query and add placeholders. The placeholders are empty
// leaves. They should be placed so that all possible node combinations
// from the existing nodes could have an added combinational leaf.
// In other words: for AND/OR, this should result in a query that has
// placeholders for all AND/OR combinations with existing nodes.
export function addPlaceholders(
q: QueryElem | null,
inNode: null | QueryNodeOp.And | QueryNodeOp.Or,
): QueryElem {
const makePlaceholder = () => {
return {
a: 0,
leafOp: QueryLeafOp.Placeholder,
b: ""
}
};
const otherOp: Record<QueryNodeOp, QueryNodeOp> = {
[QueryNodeOp.And]: QueryNodeOp.Or,
[QueryNodeOp.Or]: QueryNodeOp.And,
}
if (q == null) {
return makePlaceholder();
} else if (isNodeElem(q)) {
var operands = q.operands.map((op: any, idx: number) => {
return addPlaceholders(op, q.nodeOp);
});
operands.push(makePlaceholder());
const newBlock = { operands: operands, nodeOp: q.nodeOp };
if (inNode == null) {
return { operands: [newBlock, makePlaceholder()], nodeOp: otherOp[q.nodeOp] };
} else {
return newBlock;
}
} else if (isLeafElem(q) &&
q.leafOp != QueryLeafOp.Placeholder &&
inNode !== null) {
return { operands: [q, makePlaceholder()], nodeOp: otherOp[inNode] };
} else if (isLeafElem(q) &&
q.leafOp != QueryLeafOp.Placeholder &&
inNode === null) {
return {
operands: [
{ operands: [q, makePlaceholder()], nodeOp: QueryNodeOp.And },
makePlaceholder(),
], nodeOp: QueryNodeOp.Or
}
}
return q;
}
// See addPlaceholders.
export function removePlaceholders(q: QueryElem | null): QueryElem | null {
if (q && isNodeElem(q)) {
var newOperands: QueryElem[] = [];
q.operands.forEach((op: any) => {
if (isLeafElem(op) && op.leafOp == QueryLeafOp.Placeholder) {
return;
}
const newOp = removePlaceholders(op);
if (newOp) {
newOperands.push(newOp);
}
})
if (newOperands.length == 0) {
return null;
}
if (newOperands.length == 1) {
return newOperands[0];
}
return { operands: newOperands, nodeOp: q.nodeOp };
} else if (q && isLeafElem(q) && q.leafOp == QueryLeafOp.Placeholder) {
return null;
}
return q;
}
export function simplify(q: QueryElem | null): QueryElem | null {
if (q && isNodeElem(q)) {
var newOperands: QueryElem[] = [];
q.operands.forEach((o: QueryElem) => {
const s = simplify(o);
if (s !== null) { newOperands.push(s); }
})
if (newOperands.length === 0) { return null; }
if (newOperands.length === 1) { return newOperands[0]; }
return { operands: newOperands, nodeOp: q.nodeOp };
}
return q;
}
export function toApiQuery(q: QueryElem) : serverApi.Query {
const propsMapping: any = {
[QueryLeafBy.SongTitle]: serverApi.QueryElemProperty.songTitle,
[QueryLeafBy.ArtistName]: serverApi.QueryElemProperty.artistName,
}
const leafOpsMapping: any = {
[QueryLeafOp.Equals]: serverApi.QueryFilterOp.Eq,
[QueryLeafOp.Like]: serverApi.QueryFilterOp.Like,
}
const nodeOpsMapping: any = {
[QueryNodeOp.And]: serverApi.QueryElemOp.And,
[QueryNodeOp.Or]: serverApi.QueryElemOp.Or,
}
if(isLeafElem(q)) {
const r: serverApi.QueryElem = {
prop: propsMapping[q.a],
propOperator: leafOpsMapping[q.leafOp],
propOperand: q.b,
}
return r;
} else if(isNodeElem(q)) {
const r = {
children: q.operands.map((op: any) => toApiQuery(op)),
childrenOperator: nodeOpsMapping[q.nodeOp]
}
return r;
}
return {};
}

@ -0,0 +1,13 @@
export default function stringifyList(
s: any[],
stringifyElem?: (e: any) => string,
) {
const stringify = stringifyElem || ((e: any) => e);
var r = "";
if (s.length > 0) { r += stringify(s[0]) }
for (let i = 1; i < s.length; i++) {
r += ", " + stringify(s[i]);
}
return r;
}

@ -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)) ||
{};
}

@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es6",
"lib": [ "lib": [
"dom", "dom",
"dom.iterable", "dom.iterable",

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="logo.svg"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
id="svg18727"
version="1.1"
viewBox="0 0 23.655632 6.0053663"
height="6.0053663mm"
width="23.655632mm">
<defs
id="defs18721" />
<sodipodi:namedview
fit-margin-bottom="0"
fit-margin-right="0"
fit-margin-left="0"
fit-margin-top="0"
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="0"
inkscape:window-height="1003"
inkscape:window-width="1920"
showgrid="false"
inkscape:document-rotation="0"
inkscape:current-layer="text19329-1"
inkscape:document-units="mm"
inkscape:cy="62.911439"
inkscape:cx="102.24884"
inkscape:zoom="3.2732474"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base" />
<metadata
id="metadata18724">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(-3.3979867,-3.1434163)"
id="layer1"
inkscape:groupmode="layer"
inkscape:label="Layer 1">
<g
id="g19333-7"
transform="matrix(0.08007155,0,0,0.08007155,-1.5581106,-3.4995302)">
<circle
r="37.5"
cy="120.46263"
cx="99.395859"
id="path19290-0"
style="opacity:1;fill:#000000;fill-opacity:1;stroke-width:0.250376" />
<path
sodipodi:nodetypes="ccccccccccccccccccc"
d="m 92.982378,89.657996 c -3.214522,2.093337 -4.24212,7.031009 -1.108874,9.671786 5.24273,4.451138 13.265596,4.436238 19.302696,1.673558 1.73254,-0.17246 4.05826,-4.268097 4.65801,-1.184616 l 9.1469,19.305016 c -11.78326,11.26664 -8.10053,7.66991 -19.42383,19.3183 -2.27661,-2.56474 -3.40773,-6.38658 -5.13269,-9.49333 -5.473243,-11.38823 -3.832091,-7.85153 -9.287489,-19.24832 -5.536267,-5.31929 -14.559415,-4.8235 -20.943976,-1.35995 -4.000132,1.68472 -6.517664,7.40602 -2.846358,10.67593 4.390452,4.12727 11.194164,4.2922 16.706571,2.76198 2.403413,0.009 5.557982,-3.86321 6.945922,-3.08654 7.573174,15.89575 7.904958,16.92939 15.67079,32.73148 14.19609,-12.10634 14.03749,-12.17528 26.75668,-24.28851 -5.8538,-13.45436 -5.43451,-11.62083 -11.81526,-24.83794 -2.08946,-4.119182 -3.9803,-8.395024 -6.19289,-12.417086 -4.06756,-4.102075 -10.44295,-4.610675 -15.796809,-3.242783 -2.304682,0.751118 -4.652063,1.603012 -6.639433,3.021035 z"
style="fill:#ffffff;fill-opacity:1;stroke-width:0.31796"
id="path19306-8" />
</g>
<g
style="font-style:normal;font-weight:normal;font-size:50.8px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
id="text19329-1"
aria-label="muDBase">
<path
id="path19362"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 11.235629,4.5504098 h 0.11043 q 0.35887,0 0.61039,0.263788 0.22698,-0.263788 0.60425,-0.263788 h 0.11043 q 0.47236,0 0.76989,0.503036 0.092,0.177904 0.092,0.343537 v 2.512113 l -0.0644,0.06441 h -0.40488 l -0.0644,-0.06441 v -2.463037 q 0,-0.288325 -0.29753,-0.380344 l -0.0982,-0.01227 h -0.006 q -0.3282,0 -0.37728,0.432488 v 2.423165 l -0.0644,0.06441 h -0.40488 l -0.0644,-0.06441 v -2.423162 q -0.0491,-0.432488 -0.37727,-0.432488 h -0.0184 q -0.38341,0.05214 -0.38341,0.392614 v 2.463036 l -0.0644,0.06441 h -0.40488 l -0.0644,-0.06441 v -2.512112 q 0,-0.453961 0.49997,-0.748421 0.18711,-0.09815 0.36194,-0.09815 z" />
<path
id="path19364"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 13.750809,4.5381408 h 0.40489 l 0.0644,0.06441 v 2.472238 q 0.0613,0.395681 0.37728,0.395681 h 0.0184 q 0.38341,-0.06135 0.38341,-0.392614 v -2.475305 l 0.0644,-0.06441 h 0.40489 l 0.0644,0.06441 v 2.536651 q 0,0.432488 -0.49997,0.739217 -0.19631,0.09509 -0.34967,0.09509 h -0.14723 q -0.36501,0 -0.67787,-0.374211 -0.17177,-0.24845 -0.17177,-0.472362 v -2.524382 z" />
<path
id="path19366"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 16.928529,3.7038368 h 0.12269 q 0.71161,0 1.13796,0.763756 0.10429,0.260721 0.10429,0.463162 v 2.024412 q 0,0.460094 -0.48156,0.831237 -0.2914,0.187105 -0.55212,0.187105 h -0.34353 q -0.59199,0 -0.96927,-0.680939 -0.0767,-0.236182 -0.0767,-0.509171 v -1.322003 l 0.0644,-0.06441 h 0.41715 l 0.0644,0.06441 v 1.444694 q 0,0.395682 0.43556,0.545979 0.0368,0.0184 0.10122,0.0184 h 0.24538 q 0.41408,0 0.54904,-0.475431 l 0.0123,-0.104288 v -1.935462 q 0,-0.466228 -0.50917,-0.705476 -0.13189,-0.04294 -0.22391,-0.04294 h -0.0736 q -0.46623,0 -0.69934,0.539843 -0.0521,0.30673 -0.13496,0.30673 h -0.36807 l -0.0644,-0.06441 v -0.05828 q 0,-0.693208 0.76376,-1.119562 0.26685,-0.107356 0.4785,-0.107356 z" />
<path
id="path19368"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 19.480519,3.7038368 h 0.34353 q 0.51224,0 0.87725,0.524507 0.1687,0.272989 0.1687,0.579719 0,0.484632 -0.37421,0.809765 0.13803,0.06748 0.35274,0.365007 0.20551,0.322067 0.20551,0.641064 v 0.122692 q 0,0.693209 -0.76376,1.119562 -0.26685,0.107356 -0.4785,0.107356 h -0.12269 q -0.71161,0 -1.13796,-0.763756 -0.10429,-0.26072 -0.10429,-0.463162 v -2.024412 q 0,-0.460095 0.48156,-0.831236 0.29753,-0.187106 0.55212,-0.187106 z m -0.49997,1.082755 v 1.935461 q 0,0.466229 0.50917,0.705477 0.13496,0.04294 0.22391,0.04294 h 0.0736 q 0.44783,0 0.68708,-0.509171 0.046,-0.141096 0.046,-0.239249 v -0.07361 q 0,-0.466228 -0.50304,-0.702409 l -0.35581,-0.07055 v -0.392614 q 0,-0.08282 0.2822,-0.119624 0.38034,-0.187105 0.38034,-0.518373 v -0.07361 q 0,-0.39568 -0.43556,-0.545978 -0.0368,-0.0184 -0.10122,-0.0184 h -0.24538 q -0.40488,0 -0.54904,0.463162 z" />
<path
id="path19370"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 22.069309,4.5504098 h 0.13496 q 0.4785,0 0.7975,0.570517 0.0521,0.190172 0.0521,0.288325 v 1.705414 q 0,0.558248 -0.6656,0.831236 l -0.1779,0.02761 h -0.1411 q -0.5153,0 -0.80976,-0.582785 -0.0522,-0.150298 -0.0522,-0.263788 v -0.110423 q 0,-0.472363 0.57972,-0.822034 0.73308,-0.518372 0.73308,-0.772957 0,-0.257654 -0.319,-0.368076 h -0.0951 q -0.26379,0 -0.36501,0.325133 0,0.141096 -0.10122,0.141096 h -0.36808 l -0.0644,-0.06441 v -0.05828 q 0,-0.509171 0.59199,-0.788295 0.12882,-0.05828 0.26992,-0.05828 z m -0.3282,2.527449 q 0,0.294461 0.31593,0.392614 h 0.0859 q 0.28832,0 0.37727,-0.349672 v -0.926322 h 0.0982 q -0.23311,0.214711 -0.79442,0.616526 -0.0828,0.116557 -0.0828,0.266854 z" />
<path
id="path19372"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 24.050779,4.5504098 h 0.15336 q 0.49384,0 0.7975,0.582786 0.0521,0.162566 0.0521,0.276056 v 0.04601 l -0.0644,0.06441 h -0.36807 q -0.0859,0 -0.13496,-0.239249 -0.13803,-0.22698 -0.30673,-0.22698 h -0.0982 q -0.23004,0 -0.34047,0.337402 v 0.05521 q 0,0.220845 0.82511,0.82817 0.48769,0.463161 0.48769,0.766823 v 0.07361 q 0,0.558248 -0.6656,0.831236 l -0.1779,0.02761 h -0.15336 q -0.49384,0 -0.7975,-0.582785 -0.0521,-0.165635 -0.0521,-0.276058 v -0.04601 l 0.0644,-0.06441 h 0.36807 q 0.0859,0 0.13496,0.239249 0.13803,0.22698 0.30673,0.22698 h 0.0982 q 0.23004,0 0.34047,-0.337403 v -0.05521 q 0,-0.220845 -0.82511,-0.828169 -0.4877,-0.466228 -0.4877,-0.766823 v -0.07362 q 0,-0.558248 0.66561,-0.831236 z" />
<path
id="path19374"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.0327177"
d="m 26.050659,4.5504098 h 0.14109 q 0.51531,0 0.80977,0.582786 0.0521,0.150298 0.0521,0.263787 v 0.110423 q 0,0.472363 -0.57972,0.822033 -0.69627,0.490767 -0.73308,0.74842 0.0644,0.392614 0.37728,0.392614 h 0.0245 q 0.3006,0 0.37728,-0.37421 0,-0.09202 0.10122,-0.09202 h 0.36808 l 0.0644,0.06441 v 0.05828 q 0,0.50917 -0.59199,0.788294 -0.12576,0.05828 -0.26992,0.05828 h -0.13496 q -0.49383,0 -0.7975,-0.582785 -0.0521,-0.165635 -0.0521,-0.276058 v -1.705414 q 0,-0.561314 0.6656,-0.831236 z m -0.3098,1.779029 h -0.0981 q 0.23311,-0.214709 0.79442,-0.616525 0.0828,-0.116558 0.0828,-0.266855 0,-0.29446 -0.31593,-0.392613 h -0.0859 q -0.28833,0 -0.37728,0.349672 z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="logo_src.svg"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
id="svg18727"
version="1.1"
viewBox="0 0 210 297"
height="297mm"
width="210mm">
<defs
id="defs18721" />
<sodipodi:namedview
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="0"
inkscape:window-height="1003"
inkscape:window-width="1920"
showgrid="false"
inkscape:document-rotation="0"
inkscape:current-layer="text19329-1"
inkscape:document-units="mm"
inkscape:cy="471.86865"
inkscape:cx="134.26586"
inkscape:zoom="0.72322562"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base" />
<metadata
id="metadata18724">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:groupmode="layer"
inkscape:label="Layer 1">
<g
transform="matrix(0.64752541,0,0,0.64752541,-28.155456,62.68152)"
id="g19333">
<circle
style="opacity:1;fill:#000000;fill-opacity:1;stroke-width:0.250376"
id="path19290"
cx="99.395859"
cy="120.46263"
r="37.5" />
<path
id="path19306"
style="fill:#ffffff;fill-opacity:1;stroke-width:0.31796"
d="m 92.982378,89.657996 c -3.214522,2.093337 -4.24212,7.031009 -1.108874,9.671786 5.24273,4.451138 13.265596,4.436238 19.302696,1.673558 1.73254,-0.17246 4.05826,-4.268097 4.65801,-1.184616 l 9.1469,19.305016 c -11.78326,11.26664 -8.10053,7.66991 -19.42383,19.3183 -2.27661,-2.56474 -3.40773,-6.38658 -5.13269,-9.49333 -5.473243,-11.38823 -3.832091,-7.85153 -9.287489,-19.24832 -5.536267,-5.31929 -14.559415,-4.8235 -20.943976,-1.35995 -4.000132,1.68472 -6.517664,7.40602 -2.846358,10.67593 4.390452,4.12727 11.194164,4.2922 16.706571,2.76198 2.403413,0.009 5.557982,-3.86321 6.945922,-3.08654 7.573174,15.89575 7.904958,16.92939 15.67079,32.73148 14.19609,-12.10634 14.03749,-12.17528 26.75668,-24.28851 -5.8538,-13.45436 -5.43451,-11.62083 -11.81526,-24.83794 -2.08946,-4.119182 -3.9803,-8.395024 -6.19289,-12.417086 -4.06756,-4.102075 -10.44295,-4.610675 -15.796809,-3.242783 -2.304682,0.751118 -4.652063,1.603012 -6.639433,3.021035 z"
sodipodi:nodetypes="ccccccccccccccccccc" />
</g>
<text
id="text19329"
y="154.96599"
x="67.715202"
style="font-style:normal;font-weight:normal;font-size:50.8px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.264583"
y="154.96599"
x="67.715202"
id="tspan19327"
sodipodi:role="line">muDBase</tspan></text>
<g
id="g19333-7"
transform="matrix(0.6475254,0,0,0.6475254,-28.328934,143.91229)">
<circle
r="37.5"
cy="120.46263"
cx="99.395859"
id="path19290-0"
style="opacity:1;fill:#000000;fill-opacity:1;stroke-width:0.250376" />
<path
sodipodi:nodetypes="ccccccccccccccccccc"
d="m 92.982378,89.657996 c -3.214522,2.093337 -4.24212,7.031009 -1.108874,9.671786 5.24273,4.451138 13.265596,4.436238 19.302696,1.673558 1.73254,-0.17246 4.05826,-4.268097 4.65801,-1.184616 l 9.1469,19.305016 c -11.78326,11.26664 -8.10053,7.66991 -19.42383,19.3183 -2.27661,-2.56474 -3.40773,-6.38658 -5.13269,-9.49333 -5.473243,-11.38823 -3.832091,-7.85153 -9.287489,-19.24832 -5.536267,-5.31929 -14.559415,-4.8235 -20.943976,-1.35995 -4.000132,1.68472 -6.517664,7.40602 -2.846358,10.67593 4.390452,4.12727 11.194164,4.2922 16.706571,2.76198 2.403413,0.009 5.557982,-3.86321 6.945922,-3.08654 7.573174,15.89575 7.904958,16.92939 15.67079,32.73148 14.19609,-12.10634 14.03749,-12.17528 26.75668,-24.28851 -5.8538,-13.45436 -5.43451,-11.62083 -11.81526,-24.83794 -2.08946,-4.119182 -3.9803,-8.395024 -6.19289,-12.417086 -4.06756,-4.102075 -10.44295,-4.610675 -15.796809,-3.242783 -2.304682,0.751118 -4.652063,1.603012 -6.639433,3.021035 z"
style="fill:#ffffff;fill-opacity:1;stroke-width:0.31796"
id="path19306-8" />
</g>
<g
style="font-style:normal;font-weight:normal;font-size:50.8px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
id="text19329-1"
aria-label="muDBase">
<path
id="path19362"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.264583"
d="m 75.131959,209.01082 h 0.892969 q 2.902149,0 4.936133,2.13321 1.835547,-2.13321 4.886523,-2.13321 h 0.892969 q 3.819922,0 6.225976,4.06797 0.744141,1.43868 0.744141,2.77813 v 20.31504 l -0.520898,0.5209 h -3.274219 l -0.520898,-0.5209 v -19.91817 q 0,-2.33164 -2.406055,-3.07578 l -0.79375,-0.0992 h -0.04961 q -2.654102,0 -3.050977,3.49746 v 19.59571 l -0.520898,0.5209 h -3.274219 l -0.520899,-0.5209 v -19.59571 q -0.396875,-3.49746 -3.050976,-3.49746 h -0.148828 q -3.100586,0.42168 -3.100586,3.175 v 19.91817 l -0.520899,0.5209 h -3.274218 l -0.520899,-0.5209 v -20.31504 q 0,-3.6711 4.043164,-6.05235 1.513086,-0.79375 2.926953,-0.79375 z" />
<path
id="path19364"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.264583"
d="m 95.471803,208.91161 h 3.274219 l 0.520898,0.52089 v 19.99258 q 0.496094,3.19981 3.05098,3.19981 h 0.14883 q 3.10058,-0.4961 3.10058,-3.175 V 209.4325 l 0.5209,-0.52089 h 3.27422 l 0.5209,0.52089 v 20.51348 q 0,3.49746 -4.04317,5.97793 -1.5875,0.76895 -2.82773,0.76895 h -1.19063 q -2.951755,0 -5.481833,-3.02618 -1.389062,-2.00918 -1.389062,-3.81992 V 209.4325 Z" />
<path
id="path19366"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.264583"
d="m 121.16946,202.16473 h 0.99218 q 5.75469,0 9.20254,6.17637 0.84336,2.1084 0.84336,3.74551 v 16.37109 q 0,3.7207 -3.89433,6.72207 -2.35645,1.51309 -4.46485,1.51309 h -2.77812 q -4.78731,0 -7.83828,-5.50664 -0.62012,-1.90997 -0.62012,-4.11758 v -10.69082 l 0.5209,-0.5209 h 3.37344 l 0.52089,0.5209 v 11.683 q 0,3.19981 3.52227,4.41524 0.29766,0.14883 0.81855,0.14883 h 1.98438 q 3.34863,0 4.44004,-3.84473 l 0.0992,-0.84336 v -15.65176 q 0,-3.77031 -4.11758,-5.70507 -1.0666,-0.34727 -1.81074,-0.34727 h -0.59532 q -3.77031,0 -5.65546,4.36562 -0.42168,2.48047 -1.09141,2.48047 h -2.97656 l -0.5209,-0.52089 v -0.47129 q 0,-5.60586 6.17637,-9.05371 2.158,-0.86817 3.86953,-0.86817 z" />
<path
id="path19368"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.264583"
d="m 141.80696,202.16473 h 2.77812 q 4.14238,0 7.09414,4.2416 1.36426,2.20762 1.36426,4.68809 0,3.91914 -3.02617,6.54844 1.11621,0.5457 2.85254,2.95175 1.66191,2.6045 1.66191,5.18418 v 0.99219 q 0,5.60586 -6.17637,9.05371 -2.158,0.86817 -3.86953,0.86817 h -0.99218 q -5.75469,0 -9.20254,-6.17637 -0.84336,-2.1084 -0.84336,-3.74551 v -16.37109 q 0,-3.72071 3.89433,-6.72207 2.40606,-1.51309 4.46485,-1.51309 z m -4.04317,8.75606 v 15.65175 q 0,3.77032 4.11758,5.70508 1.09141,0.34727 1.81074,0.34727 h 0.59532 q 3.62148,0 5.55624,-4.11758 0.37208,-1.14102 0.37208,-1.93477 v -0.59531 q 0,-3.77031 -4.06797,-5.68027 l -2.87735,-0.57051 v -3.175 q 0,-0.66973 2.28203,-0.96738 3.07579,-1.51309 3.07579,-4.192 v -0.59531 q 0,-3.1998 -3.52227,-4.41523 -0.29766,-0.14883 -0.81855,-0.14883 h -1.98438 q -3.27422,0 -4.44004,3.74551 z" />
<path
id="path19370"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.264583"
d="m 162.74211,209.01082 h 1.09141 q 3.86953,0 6.44922,4.61368 0.42168,1.53789 0.42168,2.33164 v 13.7914 q 0,4.51446 -5.38262,6.72207 l -1.43867,0.22325 h -1.14102 q -4.16718,0 -6.54843,-4.71289 -0.42168,-1.21543 -0.42168,-2.13321 v -0.89297 q 0,-3.81992 4.68808,-6.64765 5.92832,-4.19199 5.92832,-6.25078 0,-2.0836 -2.57969,-2.97657 h -0.76894 q -2.1332,0 -2.95176,2.6293 0,1.14102 -0.81855,1.14102 h -2.97657 l -0.52089,-0.5209 v -0.47129 q 0,-4.11758 4.7873,-6.37481 1.0418,-0.47129 2.18281,-0.47129 z m -2.6541,20.43907 q 0,2.38125 2.55488,3.175 h 0.69454 q 2.33164,0 3.05097,-2.82774 v -7.49101 h 0.79375 q -1.88515,1.73633 -6.42441,4.98574 -0.66973,0.94258 -0.66973,2.15801 z" />
<path
id="path19372"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.264583"
d="m 178.76594,209.01082 h 1.24023 q 3.99356,0 6.44922,4.7129 0.42168,1.31464 0.42168,2.23242 v 0.37207 l -0.5209,0.5209 h -2.97656 q -0.69453,0 -1.09141,-1.93477 -1.11621,-1.83555 -2.48047,-1.83555 h -0.79375 q -1.86035,0 -2.75332,2.72852 v 0.44648 q 0,1.78594 6.67247,6.69727 3.94394,3.74551 3.94394,6.20117 v 0.59531 q 0,4.51446 -5.38262,6.72207 l -1.43867,0.22325 h -1.24023 q -3.99356,0 -6.44922,-4.71289 -0.42168,-1.33946 -0.42168,-2.23243 v -0.37207 l 0.5209,-0.5209 h 2.97656 q 0.69453,0 1.09141,1.93477 1.11621,1.83555 2.48046,1.83555 h 0.79375 q 1.86036,0 2.75332,-2.72852 v -0.44648 q 0,-1.78594 -6.67246,-6.69727 -3.94394,-3.77031 -3.94394,-6.20117 v -0.59531 q 0,-4.51446 5.38262,-6.72207 z" />
<path
id="path19374"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:'Baby Superhero';-inkscape-font-specification:'Baby Superhero';stroke-width:0.264583"
d="m 194.9386,209.01082 h 1.14101 q 4.16719,0 6.54844,4.7129 0.42168,1.21543 0.42168,2.1332 v 0.89297 q 0,3.81992 -4.68808,6.64765 -5.63067,3.96875 -5.92832,6.05235 0.52089,3.175 3.05097,3.175 h 0.19844 q 2.43086,0 3.05098,-3.02617 0,-0.74415 0.81855,-0.74415 h 2.97656 l 0.5209,0.5209 v 0.47129 q 0,4.11758 -4.7873,6.37481 -1.017,0.47129 -2.18282,0.47129 h -1.0914 q -3.99356,0 -6.44922,-4.71289 -0.42168,-1.33946 -0.42168,-2.23243 v -13.7914 q 0,-4.53926 5.38262,-6.72207 z m -2.50527,14.38672 h -0.79375 q 1.88515,-1.73632 6.42441,-4.98574 0.66973,-0.94258 0.66973,-2.15801 0,-2.38125 -2.55489,-3.175 h -0.69453 q -2.33164,0 -3.05097,2.82774 z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@ -104,7 +104,9 @@ function addLeafWhere(knexQuery: any, queryElem: api.QueryElem, type: WhereType)
if (!queryElem.propOperator) throw "Cannot create where clause without an operator."; if (!queryElem.propOperator) throw "Cannot create where clause without an operator.";
const operator = queryElem.propOperator || api.QueryFilterOp.Eq; const operator = queryElem.propOperator || api.QueryFilterOp.Eq;
const a = queryElem.prop && propertyKeys[queryElem.prop]; const a = queryElem.prop && propertyKeys[queryElem.prop];
const b = queryElem.propOperand || ""; const b = operator === api.QueryFilterOp.Like ?
'%' + (queryElem.propOperand || "") + '%'
: (queryElem.propOperand || "");
if (Object.keys(simpleLeafOps).includes(operator)) { if (Object.keys(simpleLeafOps).includes(operator)) {
if (type == WhereType.And) { if (type == WhereType.And) {
@ -295,13 +297,19 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Tag, await songIdsPromise); return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Tag, await songIdsPromise);
})() : })() :
(async () => { return {}; })(); (async () => { return {}; })();
const songsAlbumsPromise: Promise<Record<number, any[]>> = (songLimit && songLimit > 0) ?
(async () => {
return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Album, await songIdsPromise);
})() :
(async () => { return {}; })();
const [ const [
songs, songs,
artists, artists,
tags, tags,
songsArtists, songsArtists,
songsTags songsTags,
songsAlbums,
] = ] =
await Promise.all([ await Promise.all([
songsPromise, songsPromise,
@ -309,6 +317,7 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
tagsPromise, tagsPromise,
songsArtistsPromise, songsArtistsPromise,
songsTagsPromise, songsTagsPromise,
songsAlbumsPromise,
]); ]);
const response: api.QueryResponse = { const response: api.QueryResponse = {
@ -330,7 +339,13 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
name: tag['tags.name'], name: tag['tags.name'],
}; };
}), }),
albums: [], //FIXME albums: songsAlbums[song['songs.id']].map((album: any) => {
return <api.AlbumDetails>{
albumId: album['albums.id'],
name: album['albums.name'],
storeLinks: asJson(album['albums.storeLinks']),
};
}),
} }
}), }),
artists: artists.map((artist: any) => { artists: artists.map((artist: any) => {
@ -353,94 +368,4 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any,
} catch (e) { } catch (e) {
catchUnhandledErrors(e); catchUnhandledErrors(e);
} }
// try {
// const songLimit = reqObject.offsetsLimits.songLimit;
// const songOffset = reqObject.offsetsLimits.songOffset;
// const tagLimit = reqObject.offsetsLimits.tagLimit;
// const tagOffset = reqObject.offsetsLimits.tagOffset;
// const artistLimit = reqObject.offsetsLimits.artistLimit;
// const artistOffset = reqObject.offsetsLimits.artistOffset;
// const songs = (songLimit && songLimit > 0) && await models.Song.findAll({
// // NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938.
// // Custom pagination is implemented before responding.
// where: getSequelizeWhere(reqObject.query, QueryType.Song),
// order: getSequelizeOrder(reqObject.ordering, QueryType.Song),
// include: [ models.Artist, models.Album, models.Tag, models.Ranking ],
// //limit: reqObject.limit,
// //offset: reqObject.offset,
// })
// const artists = (artistLimit && artistLimit > 0) && await models.Artist.findAll({
// // NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938.
// // Custom pagination is implemented before responding.
// where: getSequelizeWhere(reqObject.query, QueryType.Artist),
// order: getSequelizeOrder(reqObject.ordering, QueryType.Artist),
// include: [models.Song, models.Album, models.Tag],
// //limit: reqObject.limit,
// //offset: reqObject.offset,
// })
// const tags = (tagLimit && tagLimit > 0) && await models.Tag.findAll({
// // NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938.
// // Custom pagination is implemented before responding.
// where: getSequelizeWhere(reqObject.query, QueryType.Tag),
// order: getSequelizeOrder(reqObject.ordering, QueryType.Tag),
// include: [models.Song, models.Album, models.Artist],
// //limit: reqObject.limit,
// //offset: reqObject.offset,
// })
// const response: api.QueryResponse = {
// songs: ((songLimit || -1) <= 0) ? [] : await Promise.all(songs.map(async (song: any) => {
// const artists = song.getArtists();
// const tags = song.getTags();
// const rankings = song.getRankings();
// return <api.SongDetails>{
// songId: song.id,
// title: song.title,
// storeLinks: song.storeLinks,
// artists: (await artists).map((artist: any) => {
// return <api.ArtistDetails>{
// artistId: artist.id,
// name: artist.name,
// }
// }),
// tags: (await tags).map((tag: any) => {
// return <api.TagDetails>{
// tagId: tag.id,
// name: tag.name,
// }
// }),
// rankings: await (await rankings).map(async (ranking: any) => {
// const maybeTagContext: api.TagDetails | undefined = await ranking.getTagContext();
// const maybeArtistContext: api.ArtistDetails | undefined = await ranking.getArtistContext();
// const maybeContext = maybeTagContext || maybeArtistContext;
// return <api.RankingDetails>{
// rankingId: ranking.id,
// type: api.ItemType.Song,
// rankedId: song.id,
// context: maybeContext,
// value: ranking.value,
// }
// })
// };
// }).slice(songOffset || 0, (songOffset || 0) + (songLimit || 10))),
// // TODO: custom pagination due to bug mentioned above
// artists: ((artistLimit || -1) <= 0) ? [] : await Promise.all(artists.map(async (artist: any) => {
// return <api.ArtistDetails>{
// artistId: artist.id,
// name: artist.name,
// };
// }).slice(artistOffset || 0, (artistOffset || 0) + (artistLimit || 10))),
// tags: ((tagLimit || -1) <= 0) ? [] : await Promise.all(tags.map(async (tag: any) => {
// return <api.TagDetails>{
// tagId: tag.id,
// name: tag.name,
// };
// }).slice(tagOffset || 0, (tagOffset || 0) + (tagLimit || 10))),
// };
// res.send(response);
// } catch (e) {
// catchUnhandledErrors(e);
// }
} }

@ -12,6 +12,6 @@ export default <Record<string,any>> {
// In production, we base the config on an environment // In production, we base the config on an environment
// variable setting. // variable setting.
production: JSON.parse(process.env.MUDBASE_DB_CONFIG || "") production: JSON.parse(process.env.MUDBASE_DB_CONFIG || "{}")
}; };

Loading…
Cancel
Save