diff --git a/client/src/components/windows/album/AlbumWindow.tsx b/client/src/components/windows/album/AlbumWindow.tsx index 28ef06d..02c2708 100644 --- a/client/src/components/windows/album/AlbumWindow.tsx +++ b/client/src/components/windows/album/AlbumWindow.tsx @@ -7,7 +7,7 @@ import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; import EditableText from '../../common/EditableText'; import SubmitChangesButton from '../../common/SubmitChangesButton'; import SongTable, { SongGetters } from '../../tables/ResultsTable'; -import { saveAlbumChanges } from '../../../lib/saveChanges'; +import { modifyAlbum } from '../../../lib/saveChanges'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { queryAlbums, querySongs } from '../../../lib/backend/queries'; import { songGetters } from '../../../lib/songGetters'; @@ -152,7 +152,7 @@ export function AlbumWindowControlled(props: { { setApplying(true); - saveAlbumChanges(props.state.id, pendingChanges || {}) + modifyAlbum(props.state.id, pendingChanges || {}) .then(() => { setApplying(false); props.dispatch({ diff --git a/client/src/components/windows/artist/ArtistWindow.tsx b/client/src/components/windows/artist/ArtistWindow.tsx index 513a556..61cd915 100644 --- a/client/src/components/windows/artist/ArtistWindow.tsx +++ b/client/src/components/windows/artist/ArtistWindow.tsx @@ -7,7 +7,7 @@ import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; import EditableText from '../../common/EditableText'; import SubmitChangesButton from '../../common/SubmitChangesButton'; import SongTable, { SongGetters } from '../../tables/ResultsTable'; -import { saveArtistChanges } from '../../../lib/saveChanges'; +import { modifyArtist } from '../../../lib/saveChanges'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { queryArtists, querySongs } from '../../../lib/backend/queries'; import { songGetters } from '../../../lib/songGetters'; @@ -156,7 +156,7 @@ export function ArtistWindowControlled(props: { { setApplying(true); - saveArtistChanges(props.state.id, pendingChanges || {}) + modifyArtist(props.state.id, pendingChanges || {}) .then(() => { setApplying(false); props.dispatch({ diff --git a/client/src/components/windows/song/EditSongDialog.tsx b/client/src/components/windows/song/EditSongDialog.tsx index 3b8b962..0351a91 100644 --- a/client/src/components/windows/song/EditSongDialog.tsx +++ b/client/src/components/windows/song/EditSongDialog.tsx @@ -1,8 +1,9 @@ import React, { useEffect, useState } from 'react'; -import { AppBar, Box, Button, Dialog, FormControl, FormControlLabel, IconButton, Link, List, ListItem, ListItemIcon, ListItemText, MenuItem, Radio, RadioGroup, Select, Tab, Tabs, TextField, Typography } from "@material-ui/core"; +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, { ExternalStore, 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'; @@ -19,13 +20,13 @@ export function ProvideLinksWidget(props: { 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( props.providers.length > 0 ? 0 : undefined ); - let [query, setQuery] = useState( - `${props.metadata.title}${props.metadata.artists && ` ${props.metadata.artists[0].name}`}${props.metadata.albums && ` ${props.metadata.albums[0].name}`}` - ) - let [results, setResults] = useState([]); + let [query, setQuery] = useState(defaultQuery) + let [results, setResults] = useState(undefined); let selectedProvider: IntegrationState | undefined = selectedProviderIdx !== undefined ? props.providers[selectedProviderIdx] : undefined; @@ -34,8 +35,16 @@ export function ProvideLinksWidget(props: { (l: string) => whichStore(l) === props.store ) : undefined; - return + // Ensure results are cleared when input state changes. + useEffect(() => { + setResults(undefined); + setQuery(defaultQuery); + }, [props.store, props.providers, props.metadata]) + + return + Search using: + - setQuery(e.target.value)} - /> - + setQuery(e.target.value)} + label="Query" + fullWidth + /> + { + selectedProvider?.integration.searchSong(query, 10) + .then((songs: IntegrationSong[]) => setResults(songs)) + }} + > + {results && results.length > 0 && Suggestions:} props.onChange(e.target.value)}> - {results.map((result: IntegrationSong, idx: number) => { + {results && results.map((result: IntegrationSong, idx: number) => { let pretty = `"${result.title}" ${result.artist && ` by ${result.artist.name}`} ${result.album && ` (${result.album.name})`}`; @@ -72,22 +84,24 @@ export function ProvideLinksWidget(props: { } /> })} + {results && results.length === 0 && No results were found. Try adjusting the query manually.} - + } export function ExternalLinksEditor(props: { metadata: SongMetadata, + original: SongMetadata, onChange: (v: SongMetadata) => void, }) { let [selectedIdx, setSelectedIdx] = useState(0); let integrations = useIntegrations(); - let linksSet: Record = - $enum(ExternalStore).getValues().reduce((prev: any, store: string) => { + let getLinksSet = (metadata: SongMetadata) => { + return $enum(ExternalStore).getValues().reduce((prev: any, store: string) => { var maybeLink: string | null = null; - props.metadata.storeLinks && props.metadata.storeLinks.forEach((link: string) => { + metadata.storeLinks && metadata.storeLinks.forEach((link: string) => { if (whichStore(link) === store) { maybeLink = link; } @@ -96,7 +110,11 @@ export function ExternalLinksEditor(props: { ...prev, [store]: maybeLink, } - }, {}) + }, {}); + } + + let linksSet: Record = getLinksSet(props.metadata); + let originalLinksSet: Record = getLinksSet(props.original); let store = $enum(ExternalStore).getValues()[selectedIdx]; let providers: IntegrationState[] = Array.isArray(integrations.state) ? @@ -113,16 +131,22 @@ export function ExternalLinksEditor(props: { {$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 setSelectedIdx(idx)} button > - {linksSet[store] !== null ? : } + {linksSet[store] !== null ? : } - + {maybeLink && - + } {maybeLink && { @@ -134,7 +158,7 @@ export function ExternalLinksEditor(props: { storeLinks: newLinks, }); }} - > + > } })} @@ -178,7 +202,6 @@ export default function EditSongDialog(props: { } let [editingMetadata, setEditingMetadata] = useState(props.metadata); - let [activeTab, setActiveTab] = useState(EditSongTabs.Details); return + Properties + Under construction + + External Links setEditingMetadata(v)} /> - {!_.isEqual(editingMetadata, props.metadata) && Changed!} + + {!_.isEqual(editingMetadata, props.metadata) && + + + } } \ No newline at end of file diff --git a/client/src/components/windows/song/SongWindow.tsx b/client/src/components/windows/song/SongWindow.tsx index b8f761d..288f307 100644 --- a/client/src/components/windows/song/SongWindow.tsx +++ b/client/src/components/windows/song/SongWindow.tsx @@ -13,6 +13,7 @@ import { querySongs } from '../../../lib/backend/queries'; import { useParams } from 'react-router'; import EditSongDialog from './EditSongDialog'; import EditIcon from '@material-ui/icons/Edit'; +import { modifySong } from '../../../lib/saveChanges'; export type SongMetadata = serverApi.SongDetails; @@ -31,7 +32,7 @@ export function SongWindowReducer(state: SongWindowState, action: any) { case SongWindowStateActions.SetMetadata: return { ...state, metadata: action.value } case SongWindowStateActions.Reload: - return { ...state, metadata: null, pendingChanges: null } + return { ...state, metadata: null } default: throw new Error("Unimplemented SongWindow state update.") } @@ -68,14 +69,16 @@ export function SongWindowControlled(props: { let [editing, setEditing] = useState(false); useEffect(() => { - getSongMetadata(songId) - .then((m: SongMetadata) => { - dispatch({ - type: SongWindowStateActions.SetMetadata, - value: m - }); - }) - }, [songId, dispatch]); + if (metadata === null) { + getSongMetadata(songId) + .then((m: SongMetadata) => { + dispatch({ + type: SongWindowStateActions.SetMetadata, + value: m + }); + }) + } + }, [songId, dispatch, metadata]); const title = {metadata?.title || "(Unknown title)"} @@ -151,7 +154,12 @@ export function SongWindowControlled(props: { {metadata && { setEditing(false); }} - onSubmit={() => { }} + onSubmit={(v: serverApi.ModifySongRequest) => { + modifySong(songId, v) + .then(() => dispatch({ + type: SongWindowStateActions.Reload + })) + }} id={songId} metadata={metadata} />} diff --git a/client/src/components/windows/tag/TagWindow.tsx b/client/src/components/windows/tag/TagWindow.tsx index 97d652a..49f283f 100644 --- a/client/src/components/windows/tag/TagWindow.tsx +++ b/client/src/components/windows/tag/TagWindow.tsx @@ -7,7 +7,7 @@ import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; import EditableText from '../../common/EditableText'; import SubmitChangesButton from '../../common/SubmitChangesButton'; import SongTable, { SongGetters } from '../../tables/ResultsTable'; -import { saveTagChanges } from '../../../lib/saveChanges'; +import { modifyTag } from '../../../lib/backend/tags'; import { queryTags, querySongs } from '../../../lib/backend/queries'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { songGetters } from '../../../lib/songGetters'; @@ -45,7 +45,7 @@ export function TagWindowReducer(state: TagWindowState, action: any) { case TagWindowStateActions.SetSongs: return { ...state, songsWithTag: action.value } case TagWindowStateActions.Reload: - return { ...state, metadata: null, pendingChanges: null, songsWithTag: null } + return { ...state, metadata: null, songsWithTag: null } default: throw new Error("Unimplemented TagWindow state update.") } @@ -176,7 +176,7 @@ export function TagWindowControlled(props: { { setApplying(true); - saveTagChanges(props.state.id, pendingChanges || {}) + modifyTag(props.state.id, pendingChanges || {}) .then(() => { setApplying(false); props.dispatch({ diff --git a/client/src/lib/integration/youtubemusic/YoutubeMusicWebScraper.tsx b/client/src/lib/integration/youtubemusic/YoutubeMusicWebScraper.tsx index f3ff258..a2deb9f 100644 --- a/client/src/lib/integration/youtubemusic/YoutubeMusicWebScraper.tsx +++ b/client/src/lib/integration/youtubemusic/YoutubeMusicWebScraper.tsx @@ -27,8 +27,6 @@ export function extractInitialData(text: string): any | undefined { // Now parse the data line. let dataline_clean = dataline.replace(/\\"/g, '"').replace(/\\\\"/g, '\\"') - console.log(dataline); - console.log(dataline_clean); let json = JSON.parse(dataline_clean); return json; diff --git a/client/src/lib/saveChanges.tsx b/client/src/lib/saveChanges.tsx index 666c5af..4a85c20 100644 --- a/client/src/lib/saveChanges.tsx +++ b/client/src/lib/saveChanges.tsx @@ -1,7 +1,7 @@ import * as serverApi from '../api'; import backendRequest from './backend/request'; -export async function saveSongChanges(id: number, change: serverApi.ModifySongRequest) { +export async function modifySong(id: number, change: serverApi.ModifySongRequest) { const requestOpts = { method: 'PUT', headers: { 'Content-Type': 'application/json' }, @@ -15,21 +15,7 @@ export async function saveSongChanges(id: number, change: serverApi.ModifySongRe } } -export async function saveTagChanges(id: number, change: serverApi.ModifyTagRequest) { - const requestOpts = { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(change), - }; - - const endpoint = serverApi.ModifyTagEndpoint.replace(":id", id.toString()); - const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + endpoint, requestOpts) - if(!response.ok) { - throw new Error("Failed to save tag changes: " + response.statusText); - } -} - -export async function saveArtistChanges(id: number, change: serverApi.ModifyArtistRequest) { +export async function modifyArtist(id: number, change: serverApi.ModifyArtistRequest) { const requestOpts = { method: 'PUT', headers: { 'Content-Type': 'application/json' }, @@ -43,7 +29,7 @@ export async function saveArtistChanges(id: number, change: serverApi.ModifyArti } } -export async function saveAlbumChanges(id: number, change: serverApi.ModifyAlbumRequest) { +export async function modifyAlbum(id: number, change: serverApi.ModifyAlbumRequest) { const requestOpts = { method: 'PUT', headers: { 'Content-Type': 'application/json' },