You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

280 lines
10 KiB

import React, { useEffect, useReducer, useCallback } from 'react';
import { Box, LinearProgress, Typography } from '@material-ui/core';
import { QueryElem, QueryLeafBy, QueryLeafElem, QueryLeafOp } from '../../../lib/query/Query';
import QueryBuilder from '../../querybuilder/QueryBuilder';
import { AlbumsTable, ArtistsTable, ColumnType, ItemsTable, TracksTable } from '../../tables/ResultsTable';
import { queryArtists, queryTracks, queryAlbums, queryTags } from '../../../lib/backend/queries';
import { WindowState } from '../Windows';
import { QueryResponseType, QueryResponseAlbumDetails, QueryResponseTagDetails, QueryResponseArtistDetails, QueryResponseTrackDetails, Artist, Name } from '../../../api/api';
import { ServerStreamResponseOptions } from 'http2';
import { TrackChangesSharp } from '@material-ui/icons';
import { v4 as genUuid } from 'uuid';
import stringifyList from '../../../lib/stringifyList';
var _ = require('lodash');
export enum QueryItemType {
Artists,
Tracks,
Albums,
Tags,
};
export interface ResultsForQuery {
kind: QueryItemType,
results: (
QueryResponseAlbumDetails[] |
QueryResponseArtistDetails[] |
QueryResponseTagDetails[] |
QueryResponseTrackDetails[]
),
}
export interface QueryWindowState extends WindowState {
editingQuery: boolean, // Is the editor in "edit mode"
query: QueryElem | null, // The actual on-screen query
includeTypes: QueryItemType[], // which item types do we actually request results for?
// Whenever queries change, new requests are fired to the server.
// Each request gets a unique id hash.
// In this results record, we store the query IDs which
// we want to show results for.
resultsForQueries: Record<string, ResultsForQuery | null>;
}
export enum QueryWindowStateActions {
FiredNewQueries = "firedNewQueries",
SetEditingQuery = "setEditingQuery",
ReceivedResult = "receivedResult",
}
async function getArtistNames(filter: string) {
const artists: any = await queryArtists(
filter.length > 0 ? {
a: QueryLeafBy.ArtistName,
b: '%' + filter + '%',
leafOp: QueryLeafOp.Like
} : undefined,
0, -1, QueryResponseType.Details
);
return [...(new Set([...(artists.map((a: any) => a.name))]))];
}
async function getAlbumNames(filter: string) {
const albums: any = await queryAlbums(
filter.length > 0 ? {
a: QueryLeafBy.AlbumName,
b: '%' + filter + '%',
leafOp: QueryLeafOp.Like
} : undefined,
0, -1, QueryResponseType.Details
);
return [...(new Set([...(albums.map((a: any) => a.name))]))];
}
async function getTrackNames(filter: string) {
const tracks: any = await queryTracks(
filter.length > 0 ? {
a: QueryLeafBy.TrackName,
b: '%' + filter + '%',
leafOp: QueryLeafOp.Like
} : undefined,
0, -1, QueryResponseType.Details
);
return [...(new Set([...(tracks.map((s: any) => s.name))]))];
}
async function getTagItems(): Promise<any> {
let tags: any = await queryTags(
undefined,
0, -1, QueryResponseType.Details
);
return tags;
}
export interface FireNewQueriesData {
query: QueryElem | null,
includeTypes: QueryItemType[],
resultIds: string[],
}
export interface ReceivedResultData {
result: ResultsForQuery,
id: string,
}
export function QueryWindowReducer(state: QueryWindowState, action: any) {
switch (action.type) {
case QueryWindowStateActions.ReceivedResult:
var arr = action.value as ReceivedResultData;
if (Object.keys(state.resultsForQueries).includes(arr.id)) {
//console.log("Storing result:", arr);
var _n = _.cloneDeep(state);
_n.resultsForQueries[arr.id] = arr.result;
return _n;
}
//console.log("Discarding result:", arr);
return state;
case QueryWindowStateActions.FiredNewQueries:
var newState: QueryWindowState = _.cloneDeep(state);
let _action = action.value as FireNewQueriesData;
// Invalidate results
newState.resultsForQueries = {};
// Add a null result for each of the new IDs.
// Results will be added in as they come.
_action.resultIds && _action.resultIds.forEach((r: string) => {
newState.resultsForQueries[r] = null;
})
newState.query = _action.query;
newState.includeTypes = _action.includeTypes;
return newState;
case QueryWindowStateActions.SetEditingQuery:
return { ...state, editingQuery: action.value }
default:
throw new Error("Unimplemented QueryWindow state update.")
}
}
export default function QueryWindow(props: {}) {
const [state, dispatch] = useReducer(QueryWindowReducer, {
editingQuery: false,
query: null,
resultsForQueries: {},
includeTypes: [QueryItemType.Tracks, QueryItemType.Artists, QueryItemType.Albums, QueryItemType.Tags],
});
return <QueryWindowControlled state={state} dispatch={dispatch} />
}
export function QueryWindowControlled(props: {
state: QueryWindowState,
dispatch: (action: any) => void,
}) {
let { query, editingQuery, resultsForQueries, includeTypes } = props.state;
let { dispatch } = props;
// Call this function to fire new queries and prepare to receive their results.
// This will also set the query into the window state.
const doQueries = async (_query: QueryElem | null, itemTypes: QueryItemType[]) => {
var promises: Promise<any>[] = [];
var ids: string[] = itemTypes.map((i: any) => genUuid());
var query_fns = {
[QueryItemType.Albums]: queryAlbums,
[QueryItemType.Artists]: queryArtists,
[QueryItemType.Tracks]: queryTracks,
[QueryItemType.Tags]: queryTags,
};
let stateUpdateData: FireNewQueriesData = {
query: _query,
includeTypes: itemTypes,
resultIds: ids
};
// First dispatch to the state that we are firing new queries.
// This will update the query on the window page and invalidate
// any previous results on-screen.
dispatch({
type: QueryWindowStateActions.FiredNewQueries,
value: stateUpdateData
})
if (_query) {
itemTypes.forEach((itemType: QueryItemType, idx: number) => {
(promises as any[]).push(
(async () => {
let results = (await query_fns[itemType](
_query,
0, // TODO: pagination
100,
QueryResponseType.Details
)) as (
QueryResponseAlbumDetails[] |
QueryResponseArtistDetails[] |
QueryResponseTagDetails[] |
QueryResponseTrackDetails[]);
let r: ReceivedResultData = {
id: ids[idx],
result: {
kind: itemType,
results: results
}
};
dispatch({ type: QueryWindowStateActions.ReceivedResult, value: r })
})()
);
})
}
await Promise.all(promises);
};
let setEditingQuery = (e: boolean) => {
props.dispatch({ type: QueryWindowStateActions.SetEditingQuery, value: e });
}
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
width="80%"
>
<QueryBuilder
query={query}
onChangeQuery={(q: QueryElem | null) => {
doQueries(q, includeTypes)
}}
editing={editingQuery}
onChangeEditing={setEditingQuery}
requestFunctions={{
getArtists: getArtistNames,
getTrackNames: getTrackNames,
getAlbums: getAlbumNames,
getTags: getTagItems,
}}
/>
</Box>
<Box
m={1}
width="80%"
>
{(() => {
var rr = Object.values(resultsForQueries);
rr = rr.sort((r: ResultsForQuery | null) => {
if (r === null) { return 99; }
return {
[QueryItemType.Tracks]: 0,
[QueryItemType.Albums]: 1,
[QueryItemType.Artists]: 2,
[QueryItemType.Tags]: 3
}[r.kind];
});
// TODO: the sorting is not working
return rr.map((r: ResultsForQuery | null) => <>
{r !== null && r.kind == QueryItemType.Tracks && <>
<Typography variant="h5">Tracks</Typography>
<TracksTable tracks={r.results as QueryResponseTrackDetails[]}/>
</>}
{r !== null && r.kind == QueryItemType.Albums && <>
<Typography variant="h5">Albums</Typography>
<AlbumsTable albums={r.results as QueryResponseAlbumDetails[]}/>
</>}
{r !== null && r.kind == QueryItemType.Artists && <>
<Typography variant="h5">Artists</Typography>
<ArtistsTable artists={r.results as QueryResponseArtistDetails[]}/>
</>}
{r !== null && r.kind == QueryItemType.Tags && <>
<Typography variant="h5">Tags</Typography>
<Typography>Found {r.results.length} tags.</Typography>
</>}
{r === null && <LinearProgress />}
</>);
})()}
</Box>
</Box>
}