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.
 
 
 
 

234 lines
10 KiB

import React, { useEffect, useState } from 'react';
import { AppBar, Box, Button, Dialog, DialogActions, Divider, FormControl, FormControlLabel, IconButton, Link, List, ListItem, ListItemIcon, ListItemText, MenuItem, Radio, RadioGroup, Select, Tab, Tabs, TextField, Typography } from "@material-ui/core";
import { SongMetadata } from "./SongWindow";
import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import CheckIcon from '@material-ui/icons/Check';
import SearchIcon from '@material-ui/icons/Search';
import CancelIcon from '@material-ui/icons/Cancel';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
import DeleteIcon from '@material-ui/icons/Delete';
import { $enum } from "ts-enum-util";
import { useIntegrations, IntegrationsState, IntegrationState } from '../../../lib/integration/useIntegrations';
import { IntegrationFeature, IntegrationSong } from '../../../lib/integration/Integration';
import { TabPanel } from '@material-ui/lab';
import { v1 } from 'uuid';
import { ExternalStore } from '../../../api';
let _ = require('lodash')
export function ProvideLinksWidget(props: {
providers: IntegrationState[],
metadata: SongMetadata,
store: ExternalStore,
onChange: (link: string | undefined) => void,
}) {
let defaultQuery = `${props.metadata.title}${props.metadata.artists && ` ${props.metadata.artists[0].name}`}${props.metadata.albums && ` ${props.metadata.albums[0].name}`}`;
let [selectedProviderIdx, setSelectedProviderIdx] = useState<number | undefined>(
props.providers.length > 0 ? 0 : undefined
);
let [query, setQuery] = useState<string>(defaultQuery)
let [results, setResults] = useState<IntegrationSong[] | undefined>(undefined);
let selectedProvider: IntegrationState | undefined = selectedProviderIdx !== undefined ?
props.providers[selectedProviderIdx] : undefined;
let currentLink = props.metadata.storeLinks ? props.metadata.storeLinks.find(
(l: string) => whichStore(l) === props.store
) : undefined;
// Ensure results are cleared when input state changes.
useEffect(() => {
setResults(undefined);
setQuery(defaultQuery);
}, [props.store, props.providers, props.metadata])
return <Box display="flex" flexDirection="column" alignItems="left">
<Box display="flex" alignItems="center">
<Typography>Search using:</Typography>
<Box ml={2} />
<Select
value={selectedProviderIdx}
onChange={(e: any) => setSelectedProviderIdx(e.target.value)}
>
{props.providers.map((p: IntegrationState, idx: number) => {
return <MenuItem value={idx}>{p.properties.name}</MenuItem>
})}
</Select>
</Box>
<TextField
value={query}
onChange={(e: any) => setQuery(e.target.value)}
label="Query"
fullWidth
/>
<IconButton
onClick={() => {
selectedProvider?.integration.searchSong(query, 10)
.then((songs: IntegrationSong[]) => setResults(songs))
}}
><SearchIcon /></IconButton>
{results && results.length > 0 && <Typography>Suggestions:</Typography>}
<FormControl>
<RadioGroup value={currentLink} onChange={(e: any) => props.onChange(e.target.value)}>
{results && results.map((result: IntegrationSong, idx: number) => {
let pretty = `"${result.title}"
${result.artist && ` by ${result.artist.name}`}
${result.album && ` (${result.album.name})`}`;
return <FormControlLabel
value={result.url || idx}
control={<Radio checked={(result.url || idx) === currentLink} />}
label={<Box display="flex" alignItems="center">
{pretty}
<a href={result.url || ""} target="_blank">
<IconButton><OpenInNewIcon /></IconButton>
</a>
</Box>}
/>
})}
{results && results.length === 0 && <Typography>No results were found. Try adjusting the query manually.</Typography>}
</RadioGroup>
</FormControl>
</Box >
}
export function ExternalLinksEditor(props: {
metadata: SongMetadata,
original: SongMetadata,
onChange: (v: SongMetadata) => void,
}) {
let [selectedIdx, setSelectedIdx] = useState<number>(0);
let integrations = useIntegrations();
let getLinksSet = (metadata: SongMetadata) => {
return $enum(ExternalStore).getValues().reduce((prev: any, store: string) => {
var maybeLink: string | null = null;
metadata.storeLinks && metadata.storeLinks.forEach((link: string) => {
if (whichStore(link) === store) {
maybeLink = link;
}
})
return {
...prev,
[store]: maybeLink,
}
}, {});
}
let linksSet: Record<string, string | null> = getLinksSet(props.metadata);
let originalLinksSet: Record<string, string | null> = getLinksSet(props.original);
let store = $enum(ExternalStore).getValues()[selectedIdx];
let providers: IntegrationState[] = Array.isArray(integrations.state) ?
integrations.state.filter(
(iState: IntegrationState) => (
iState.integration.getFeatures().includes(IntegrationFeature.SearchSong) &&
iState.integration.providesStoreLink() === store
)
) : [];
return <Box display="flex" width="100%">
<Box width="30%">
<List>
{$enum(ExternalStore).getValues().map((store: string, idx: number) => {
let maybeLink = linksSet[store];
let color: string | undefined =
(linksSet[store] && !originalLinksSet[store]) ? "lightgreen" :
(!linksSet[store] && originalLinksSet[store]) ? "red" :
(linksSet[store] && originalLinksSet[store] && linksSet[store] !== originalLinksSet[store]) ? "orange" :
undefined;
return <ListItem
selected={selectedIdx === idx}
onClick={(e: any) => setSelectedIdx(idx)}
button
>
<ListItemIcon>{linksSet[store] !== null ? <CheckIcon style={{ color: color }} /> : <CancelIcon style={{ color: color }} />}</ListItemIcon>
<ListItemIcon><StoreLinkIcon whichStore={store} /></ListItemIcon>
<ListItemText style={{ color: color }} primary={store} />
{maybeLink && <a href={maybeLink} target="_blank">
<ListItemIcon><IconButton><OpenInNewIcon style={{ color: color }} /></IconButton></ListItemIcon>
</a>}
{maybeLink && <ListItemIcon><IconButton
onClick={() => {
let newLinks = props.metadata.storeLinks?.filter(
(l: string) => whichStore(l) !== store
)
props.onChange({
...props.metadata,
storeLinks: newLinks,
});
}}
><DeleteIcon style={{ color: color }} />
</IconButton></ListItemIcon>}
</ListItem>
})}
</List>
</Box>
<Box ml={2} width="60%">
{providers.length === 0 ?
<Typography>None of your configured integrations provides URL links for {store}.</Typography> :
<ProvideLinksWidget
providers={providers}
metadata={props.metadata}
store={store}
onChange={(link: string | undefined) => {
let removed = props.metadata.storeLinks?.filter(
(link: string) => whichStore(link) !== store
) || [];
let newValue = link ? [...removed, link] : removed;
if (!_.isEqual(new Set(newValue), new Set(props.metadata.storeLinks || []))) {
props.onChange({
...props.metadata,
storeLinks: newValue,
})
}
}}
/>
}
</Box>
</Box >
}
export default function EditSongDialog(props: {
open: boolean,
onClose: () => void,
onSubmit: (v: SongMetadata) => void,
id: number,
metadata: SongMetadata,
}) {
enum EditSongTabs {
Details = 0,
ExternalLinks,
}
let [editingMetadata, setEditingMetadata] = useState<SongMetadata>(props.metadata);
return <Dialog
maxWidth="lg"
fullWidth
open={props.open}
onClose={props.onClose}
disableBackdropClick={true}>
<Typography variant="h5">Properties</Typography>
<Typography>Under construction</Typography>
<Divider />
<Typography variant="h5">External Links</Typography>
<ExternalLinksEditor
metadata={editingMetadata}
original={props.metadata}
onChange={(v: SongMetadata) => setEditingMetadata(v)}
/>
<Divider />
{!_.isEqual(editingMetadata, props.metadata) && <DialogActions>
<Button variant="contained" color="secondary"
onClick={() => {
props.onSubmit(editingMetadata);
props.onClose();
}}>Save all changes</Button>
<Button variant="outlined"
onClick={() => setEditingMetadata(props.metadata)}>Discard changes</Button>
</DialogActions>}
</Dialog>
}