|
|
|
@ -1,5 +1,5 @@ |
|
|
|
|
import React, { useEffect, useState } from 'react'; |
|
|
|
|
import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core'; |
|
|
|
|
import { Box, Typography, IconButton, Button, CircularProgress, Menu, MenuItem } from '@material-ui/core'; |
|
|
|
|
import AudiotrackIcon from '@material-ui/icons/Audiotrack'; |
|
|
|
|
import PersonIcon from '@material-ui/icons/Person'; |
|
|
|
|
import AlbumIcon from '@material-ui/icons/Album'; |
|
|
|
@ -11,9 +11,18 @@ import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; |
|
|
|
|
import EditableText from '../common/EditableText'; |
|
|
|
|
import SubmitChangesButton from '../common/SubmitChangesButton'; |
|
|
|
|
import { saveSongChanges } from '../../lib/saveChanges'; |
|
|
|
|
import { uniqueByField } from '../../lib/algorithm/UniqueByField'; |
|
|
|
|
import NestedMenuItem from 'material-ui-nested-menu-item'; |
|
|
|
|
import AutocompleteSelect from '../common/AutocompleteSelect'; |
|
|
|
|
|
|
|
|
|
export type SongMetadata = serverApi.SongDetails; |
|
|
|
|
export type SongMetadataChanges = serverApi.ModifySongRequest; |
|
|
|
|
export type SongMetadataChanges = { |
|
|
|
|
title?: string, |
|
|
|
|
artists?: any[], |
|
|
|
|
albums?: any[], |
|
|
|
|
tags?: any[], |
|
|
|
|
storeLinks?: string[], |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export interface SongWindowState extends WindowState { |
|
|
|
|
songId: number, |
|
|
|
@ -81,6 +90,55 @@ export async function getSongMetadata(id: number) { |
|
|
|
|
})(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export function EditArtistMenu(props: { |
|
|
|
|
artist: any, |
|
|
|
|
pos: number[] | undefined, |
|
|
|
|
onClose: () => void, |
|
|
|
|
onRemove: () => void, |
|
|
|
|
onReplace: (newArtist: any) => void, |
|
|
|
|
}) { |
|
|
|
|
const name = props.artist ? props.artist.name : "unknown"; |
|
|
|
|
const open = Boolean(props.pos); |
|
|
|
|
|
|
|
|
|
return <Menu |
|
|
|
|
anchorReference='anchorPosition' |
|
|
|
|
anchorPosition={props.pos && { left: props.pos[0], top: props.pos[1] }} |
|
|
|
|
keepMounted |
|
|
|
|
open={open} |
|
|
|
|
onClose={props.onClose} |
|
|
|
|
> |
|
|
|
|
<MenuItem onClick={() => { props.onRemove(); props.onClose(); }}>Remove "{name}"</MenuItem> |
|
|
|
|
<NestedMenuItem |
|
|
|
|
parentMenuOpen={open} |
|
|
|
|
label="Replace..." |
|
|
|
|
> |
|
|
|
|
<AutocompleteSelect |
|
|
|
|
label="Artist" |
|
|
|
|
getNewOptions={async () => { |
|
|
|
|
return [ |
|
|
|
|
{ |
|
|
|
|
name: "Dude", |
|
|
|
|
artistId: 900, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "Sweet", |
|
|
|
|
artistId: 901, |
|
|
|
|
} |
|
|
|
|
]; |
|
|
|
|
}} |
|
|
|
|
onSubmit={(input: string, artist: any) => { |
|
|
|
|
console.log("Submitted", artist) |
|
|
|
|
props.onReplace(artist); |
|
|
|
|
props.onClose(); |
|
|
|
|
}} |
|
|
|
|
style={{ width: 300 }} |
|
|
|
|
getOptionLabel={(artist: any) => artist.name} |
|
|
|
|
onlySubmitExact={true} |
|
|
|
|
/> |
|
|
|
|
</NestedMenuItem> |
|
|
|
|
</Menu> |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export default function SongWindow(props: IProps) { |
|
|
|
|
let metadata = props.state.metadata; |
|
|
|
|
let pendingChanges = props.state.pendingChanges; |
|
|
|
@ -95,6 +153,11 @@ export default function SongWindow(props: IProps) { |
|
|
|
|
}) |
|
|
|
|
}, [metadata?.title]); |
|
|
|
|
|
|
|
|
|
const [artistMenuState, setArtistMenuState] = useState<{ |
|
|
|
|
menuPos: number[], |
|
|
|
|
artist: any, |
|
|
|
|
} | undefined>(undefined); |
|
|
|
|
|
|
|
|
|
const [editingTitle, setEditingTitle] = useState<string | null>(null); |
|
|
|
|
const title = <Typography variant="h4"><EditableText |
|
|
|
|
defaultValue={metadata?.title || "(Unknown title)"} |
|
|
|
@ -113,10 +176,21 @@ export default function SongWindow(props: IProps) { |
|
|
|
|
}} |
|
|
|
|
/></Typography> |
|
|
|
|
|
|
|
|
|
const artists = metadata?.artists && metadata?.artists.map((artist: ArtistMetadata) => { |
|
|
|
|
return <Typography> |
|
|
|
|
{artist.name} |
|
|
|
|
</Typography> |
|
|
|
|
const originalArtists = (metadata?.artists) || []; |
|
|
|
|
const newArtists = (pendingChanges?.artists) || []; |
|
|
|
|
const allArtists = uniqueByField([...originalArtists, ...newArtists], 'artistId'); |
|
|
|
|
const artists = allArtists.map((artist: any) => { |
|
|
|
|
const id = artist.artistId; |
|
|
|
|
const deleted = pendingChanges && pendingChanges.artists && pendingChanges.artists.filter((aa: any) => aa.artistId === id).length === 0; |
|
|
|
|
|
|
|
|
|
return <Button |
|
|
|
|
style={{ textTransform: 'none' }} |
|
|
|
|
onClick={(e: any) => setArtistMenuState({ menuPos: [e.pageX, e.pageY], artist: artist })} |
|
|
|
|
> |
|
|
|
|
<Typography> |
|
|
|
|
{deleted ? <del>{artist.name}</del> : artist.name} |
|
|
|
|
</Typography> |
|
|
|
|
</Button> |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const albums = metadata?.albums && metadata?.albums.map((album: AlbumMetadata) => { |
|
|
|
@ -154,50 +228,86 @@ export default function SongWindow(props: IProps) { |
|
|
|
|
{applying && <CircularProgress />} |
|
|
|
|
</Box> |
|
|
|
|
|
|
|
|
|
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap"> |
|
|
|
|
<Box |
|
|
|
|
m={1} |
|
|
|
|
mt={4} |
|
|
|
|
width="80%" |
|
|
|
|
> |
|
|
|
|
<AudiotrackIcon style={{ fontSize: 80 }} /> |
|
|
|
|
</Box> |
|
|
|
|
<Box |
|
|
|
|
m={1} |
|
|
|
|
width="80%" |
|
|
|
|
> |
|
|
|
|
{metadata && <Box> |
|
|
|
|
<Box m={2}> |
|
|
|
|
{title} |
|
|
|
|
</Box> |
|
|
|
|
<Box m={0.5}> |
|
|
|
|
<Box display="flex" alignItems="center" m={0.5}> |
|
|
|
|
<PersonIcon /> |
|
|
|
|
<Box m={0.5}> |
|
|
|
|
{artists} |
|
|
|
|
return <> |
|
|
|
|
<Box width="100%" justifyContent="center" display="flex" flexWrap="wrap"> |
|
|
|
|
<Box |
|
|
|
|
m={1} |
|
|
|
|
mt={4} |
|
|
|
|
width="80%" |
|
|
|
|
> |
|
|
|
|
<AudiotrackIcon style={{ fontSize: 80 }} /> |
|
|
|
|
</Box> |
|
|
|
|
<Box |
|
|
|
|
m={1} |
|
|
|
|
width="80%" |
|
|
|
|
> |
|
|
|
|
{metadata && <Box> |
|
|
|
|
<Box m={2}> |
|
|
|
|
{title} |
|
|
|
|
</Box> |
|
|
|
|
<Box m={0.5}> |
|
|
|
|
<Box display="flex" alignItems="center" m={0.5}> |
|
|
|
|
<PersonIcon /> |
|
|
|
|
<Box m={0.5}> |
|
|
|
|
{artists} |
|
|
|
|
</Box> |
|
|
|
|
</Box> |
|
|
|
|
</Box> |
|
|
|
|
</Box> |
|
|
|
|
<Box m={0.5}> |
|
|
|
|
<Box display="flex" alignItems="center" m={0.5}> |
|
|
|
|
<AlbumIcon /> |
|
|
|
|
<Box m={0.5}> |
|
|
|
|
{albums} |
|
|
|
|
<Box m={0.5}> |
|
|
|
|
<Box display="flex" alignItems="center" m={0.5}> |
|
|
|
|
<AlbumIcon /> |
|
|
|
|
<Box m={0.5}> |
|
|
|
|
{albums} |
|
|
|
|
</Box> |
|
|
|
|
</Box> |
|
|
|
|
</Box> |
|
|
|
|
</Box> |
|
|
|
|
<Box m={1}> |
|
|
|
|
<Box display="flex" alignItems="center" m={0.5}> |
|
|
|
|
{storeLinks} |
|
|
|
|
<Box m={1}> |
|
|
|
|
<Box display="flex" alignItems="center" m={0.5}> |
|
|
|
|
{storeLinks} |
|
|
|
|
</Box> |
|
|
|
|
</Box> |
|
|
|
|
</Box> |
|
|
|
|
</Box>} |
|
|
|
|
</Box> |
|
|
|
|
<Box |
|
|
|
|
m={1} |
|
|
|
|
width="80%" |
|
|
|
|
> |
|
|
|
|
{maybeSubmitButton} |
|
|
|
|
</Box>} |
|
|
|
|
</Box> |
|
|
|
|
<Box |
|
|
|
|
m={1} |
|
|
|
|
width="80%" |
|
|
|
|
> |
|
|
|
|
{maybeSubmitButton} |
|
|
|
|
</Box> |
|
|
|
|
</Box> |
|
|
|
|
</Box> |
|
|
|
|
<EditArtistMenu |
|
|
|
|
pos={artistMenuState?.menuPos} |
|
|
|
|
artist={artistMenuState?.artist} |
|
|
|
|
onClose={() => setArtistMenuState(undefined)} |
|
|
|
|
onRemove={() => { |
|
|
|
|
const oldArtists = props.state.pendingChanges?.artists ? |
|
|
|
|
props.state.pendingChanges.artists : |
|
|
|
|
(props.state.metadata?.artists || []); |
|
|
|
|
const newArtists = [...oldArtists] |
|
|
|
|
.filter((a: any) => (a.artistId != artistMenuState?.artist.artistId)); |
|
|
|
|
props.dispatch({ |
|
|
|
|
type: SongWindowStateActions.SetPendingChanges, |
|
|
|
|
value: { |
|
|
|
|
...props.state.pendingChanges, |
|
|
|
|
artists: newArtists, |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
}} |
|
|
|
|
onReplace={(newArtist: any) => { |
|
|
|
|
const oldArtists = props.state.pendingChanges?.artists ? |
|
|
|
|
props.state.pendingChanges.artists : |
|
|
|
|
(props.state.metadata?.artists || []); |
|
|
|
|
const withoutReplaced = [...oldArtists] |
|
|
|
|
.filter((a: any) => (a.artistId != artistMenuState?.artist.artistId)); |
|
|
|
|
const withNew = [ ...withoutReplaced, newArtist ]; |
|
|
|
|
props.dispatch({ |
|
|
|
|
type: SongWindowStateActions.SetPendingChanges, |
|
|
|
|
value: { |
|
|
|
|
...props.state.pendingChanges, |
|
|
|
|
artists: withNew, |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
</> |
|
|
|
|
} |