|
|
|
@ -1,29 +1,50 @@ |
|
|
|
|
import React, { useEffect, useReducer, useCallback } from 'react'; |
|
|
|
|
import { Box, LinearProgress } from '@material-ui/core'; |
|
|
|
|
import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; |
|
|
|
|
import { QueryElem, QueryLeafBy, QueryLeafElem, QueryLeafOp } from '../../../lib/query/Query'; |
|
|
|
|
import QueryBuilder from '../../querybuilder/QueryBuilder'; |
|
|
|
|
import TrackTable from '../../tables/ResultsTable'; |
|
|
|
|
import { queryArtists, queryTracks, queryAlbums, queryTags } from '../../../lib/backend/queries'; |
|
|
|
|
import { WindowState } from '../Windows'; |
|
|
|
|
import { QueryResponseType, QueryResponseAlbumDetails, QueryResponseTagDetails, QueryResponseArtistDetails, QueryResponseTrackDetails} from '../../../api/api'; |
|
|
|
|
import { QueryResponseType, QueryResponseAlbumDetails, QueryResponseTagDetails, QueryResponseArtistDetails, QueryResponseTrackDetails } from '../../../api/api'; |
|
|
|
|
import { ServerStreamResponseOptions } from 'http2'; |
|
|
|
|
import { TrackChangesSharp } from '@material-ui/icons'; |
|
|
|
|
import { v4 as genUuid } from 'uuid'; |
|
|
|
|
var _ = require('lodash'); |
|
|
|
|
|
|
|
|
|
export interface ResultsForQuery { |
|
|
|
|
for: QueryElem, |
|
|
|
|
results: any[], |
|
|
|
|
export enum QueryItemType { |
|
|
|
|
Artists, |
|
|
|
|
Tracks, |
|
|
|
|
Albums, |
|
|
|
|
Tags, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
export interface ResultsForQuery { |
|
|
|
|
kind: QueryItemType, |
|
|
|
|
results: ( |
|
|
|
|
QueryResponseAlbumDetails[] | |
|
|
|
|
QueryResponseArtistDetails[] | |
|
|
|
|
QueryResponseTagDetails[] | |
|
|
|
|
QueryResponseTrackDetails[] |
|
|
|
|
), |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export interface QueryWindowState extends WindowState { |
|
|
|
|
editingQuery: boolean, |
|
|
|
|
query: QueryElem | null, |
|
|
|
|
resultsForQuery: ResultsForQuery | null, |
|
|
|
|
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 { |
|
|
|
|
SetQuery = "setQuery", |
|
|
|
|
FiredNewQueries = "firedNewQueries", |
|
|
|
|
SetEditingQuery = "setEditingQuery", |
|
|
|
|
SetResultsForQuery = "setResultsForQuery", |
|
|
|
|
ReceivedResult = "receivedResult", |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function getArtistNames(filter: string) { |
|
|
|
@ -74,23 +95,55 @@ async function getTagItems(): Promise<any> { |
|
|
|
|
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.SetQuery: |
|
|
|
|
return { ...state, query: action.value } |
|
|
|
|
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 } |
|
|
|
|
case QueryWindowStateActions.SetResultsForQuery: |
|
|
|
|
return { ...state, resultsForQuery: action.value } |
|
|
|
|
default: |
|
|
|
|
throw new Error("Unimplemented QueryWindow state update.") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export default function QueryWindow(props: {}) { |
|
|
|
|
const [state, dispatch] = useReducer(QueryWindowReducer, { |
|
|
|
|
editingQuery: false, |
|
|
|
|
query: null, |
|
|
|
|
resultsForQuery: null, |
|
|
|
|
resultsForQueries: {}, |
|
|
|
|
includeTypes: [QueryItemType.Tracks, QueryItemType.Artists, QueryItemType.Albums, QueryItemType.Tags], |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
return <QueryWindowControlled state={state} dispatch={dispatch} /> |
|
|
|
@ -100,45 +153,70 @@ export function QueryWindowControlled(props: { |
|
|
|
|
state: QueryWindowState, |
|
|
|
|
dispatch: (action: any) => void, |
|
|
|
|
}) { |
|
|
|
|
let { query, editingQuery: editing, resultsForQuery: resultsFor } = props.state; |
|
|
|
|
let { query, editingQuery, resultsForQueries, includeTypes } = props.state; |
|
|
|
|
let { dispatch } = props; |
|
|
|
|
|
|
|
|
|
let setQuery = (q: QueryElem | null) => { |
|
|
|
|
props.dispatch({ type: QueryWindowStateActions.SetQuery, value: q }); |
|
|
|
|
} |
|
|
|
|
let setEditingQuery = (e: boolean) => { |
|
|
|
|
props.dispatch({ type: QueryWindowStateActions.SetEditingQuery, value: e }); |
|
|
|
|
} |
|
|
|
|
let setResultsForQuery = useCallback((r: ResultsForQuery | null) => { |
|
|
|
|
dispatch({ type: QueryWindowStateActions.SetResultsForQuery, value: r }); |
|
|
|
|
}, [dispatch]); |
|
|
|
|
|
|
|
|
|
const loading = query && (!resultsFor || !_.isEqual(resultsFor.for, query)); |
|
|
|
|
const showResults = (query && resultsFor && query === resultsFor.for) ? resultsFor.results : []; |
|
|
|
|
|
|
|
|
|
const doQuery = useCallback(async (_query: QueryElem) => { |
|
|
|
|
const tracks: QueryResponseTrackDetails[] = await queryTracks( |
|
|
|
|
_query, |
|
|
|
|
0, |
|
|
|
|
100, //TODO: pagination
|
|
|
|
|
QueryResponseType.Details |
|
|
|
|
) as QueryResponseTrackDetails[]; |
|
|
|
|
|
|
|
|
|
if (_.isEqual(query, _query)) { |
|
|
|
|
setResultsForQuery({ |
|
|
|
|
for: _query, |
|
|
|
|
results: tracks, |
|
|
|
|
// 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 }) |
|
|
|
|
})() |
|
|
|
|
); |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
}, [query, setResultsForQuery]); |
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
if (query) { |
|
|
|
|
doQuery(query); |
|
|
|
|
} else { |
|
|
|
|
setResultsForQuery(null); |
|
|
|
|
} |
|
|
|
|
}, [query, doQuery, setResultsForQuery]); |
|
|
|
|
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 |
|
|
|
@ -147,8 +225,10 @@ export function QueryWindowControlled(props: { |
|
|
|
|
> |
|
|
|
|
<QueryBuilder |
|
|
|
|
query={query} |
|
|
|
|
onChangeQuery={setQuery} |
|
|
|
|
editing={editing} |
|
|
|
|
onChangeQuery={(q: QueryElem | null) => { |
|
|
|
|
doQueries(q, includeTypes) |
|
|
|
|
}} |
|
|
|
|
editing={editingQuery} |
|
|
|
|
onChangeEditing={setEditingQuery} |
|
|
|
|
requestFunctions={{ |
|
|
|
|
getArtists: getArtistNames, |
|
|
|
@ -162,10 +242,15 @@ export function QueryWindowControlled(props: { |
|
|
|
|
m={1} |
|
|
|
|
width="80%" |
|
|
|
|
> |
|
|
|
|
<TrackTable |
|
|
|
|
tracks={showResults} |
|
|
|
|
/> |
|
|
|
|
{loading && <LinearProgress />} |
|
|
|
|
{Object.values(resultsForQueries).map((r: ResultsForQuery | null) => <> |
|
|
|
|
{r !== null && r.kind == QueryItemType.Tracks && <TrackTable |
|
|
|
|
tracks={r.results as QueryResponseTrackDetails[]} |
|
|
|
|
/>} |
|
|
|
|
{r !== null && r.kind == QueryItemType.Albums && <>Found {r.results.length} albums.</>} |
|
|
|
|
{r !== null && r.kind == QueryItemType.Artists && <>Found {r.results.length} artists.</>} |
|
|
|
|
{r !== null && r.kind == QueryItemType.Tags && <>Found {r.results.length} tags.</>} |
|
|
|
|
{r === null && <LinearProgress />} |
|
|
|
|
</>)} |
|
|
|
|
</Box> |
|
|
|
|
</Box> |
|
|
|
|
} |