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; } 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 { 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 } 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[] = []; 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 { doQueries(q, includeTypes) }} editing={editingQuery} onChangeEditing={setEditingQuery} requestFunctions={{ getArtists: getArtistNames, getTrackNames: getTrackNames, getAlbums: getAlbumNames, getTags: getTagItems, }} /> {(() => { 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 && <> Tracks } {r !== null && r.kind == QueryItemType.Albums && <> Albums } {r !== null && r.kind == QueryItemType.Artists && <> Artists } {r !== null && r.kind == QueryItemType.Tags && <> Tags Found {r.results.length} tags. } {r === null && } ); })()} }