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
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> |
|
} |