From 935614d12f13bff271d9881f28629c4286a2030d Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Wed, 23 Sep 2020 16:40:13 +0200 Subject: [PATCH 1/6] Editability improved (but submit not working), add tables to edit pages --- client/src/components/MainWindow.tsx | 29 +++- client/src/components/appbar/AppBar.tsx | 9 +- client/src/components/common/EditableText.tsx | 93 ++++++++++++ .../src/components/common/StoreLinkIcon.tsx | 28 ++++ .../components/common/SubmitChangesButton.tsx | 12 ++ client/src/components/tables/ResultsTable.tsx | 9 +- client/src/components/windows/AlbumWindow.tsx | 139 ++++++++++++++++-- .../src/components/windows/ArtistWindow.tsx | 137 +++++++++++++++-- client/src/components/windows/QueryWindow.tsx | 33 +---- client/src/components/windows/SongWindow.tsx | 109 ++++++++++++-- client/src/components/windows/TagWindow.tsx | 8 +- client/src/components/windows/Windows.tsx | 17 ++- client/src/lib/songGetters.tsx | 28 ++++ 13 files changed, 562 insertions(+), 89 deletions(-) create mode 100644 client/src/components/common/EditableText.tsx create mode 100644 client/src/components/common/StoreLinkIcon.tsx create mode 100644 client/src/components/common/SubmitChangesButton.tsx create mode 100644 client/src/lib/songGetters.tsx diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx index 970cef6..d3b73f1 100644 --- a/client/src/components/MainWindow.tsx +++ b/client/src/components/MainWindow.tsx @@ -1,5 +1,5 @@ -import React, { useReducer, useState, Reducer } from 'react'; -import { ThemeProvider, CssBaseline, createMuiTheme, withWidth } from '@material-ui/core'; +import React, { useReducer, Reducer } from 'react'; +import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; import { grey } from '@material-ui/core/colors'; import AppBar from './appbar/AppBar'; import QueryWindow from './windows/QueryWindow'; @@ -48,7 +48,6 @@ export function MainWindowReducer(state: MainWindowState, action: any) { activeTab: state.activeTab >= (newSize - 1) ? (newSize - 1) : state.activeTab, } case MainWindowStateActions.AddTab: - console.log("Add tab: ", action) return { ...state, tabStates: [...state.tabStates, action.tabState], @@ -65,17 +64,33 @@ export function MainWindowReducer(state: MainWindowState, action: any) { }) } default: - throw new Error("Unimplemented QueryWindow state update.") + throw new Error("Unimplemented MainWindow state update.") } } export default function MainWindow(props: any) { const [state, dispatch] = useReducer(MainWindowReducer, { tabStates: [ - newWindowState[WindowType.Query]() + newWindowState[WindowType.Query](), + newWindowState[WindowType.Song](), + newWindowState[WindowType.Album](), + newWindowState[WindowType.Artist](), + newWindowState[WindowType.Tag](), + ], + tabReducers: [ + newWindowReducer[WindowType.Query], + newWindowReducer[WindowType.Song], + newWindowReducer[WindowType.Album], + newWindowReducer[WindowType.Artist], + newWindowReducer[WindowType.Tag], + ], + tabTypes: [ + WindowType.Query, + WindowType.Song, + WindowType.Album, + WindowType.Artist, + WindowType.Tag, ], - tabReducers: [newWindowReducer[WindowType.Query]], - tabTypes: [WindowType.Query], activeTab: 0 }) diff --git a/client/src/components/appbar/AppBar.tsx b/client/src/components/appbar/AppBar.tsx index b7308aa..820b479 100644 --- a/client/src/components/appbar/AppBar.tsx +++ b/client/src/components/appbar/AppBar.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { AppBar as MuiAppBar, Box, Tab as MuiTab, Tabs, IconButton } from '@material-ui/core'; import CloseIcon from '@material-ui/icons/Close'; import AddIcon from '@material-ui/icons/Add'; @@ -61,7 +61,12 @@ export default function AppBar(props: IProps) { error - props.setSelectedTab(v)}> + props.setSelectedTab(v)} + variant="scrollable" + scrollButtons="auto" + > {props.tabLabels.map((l: string, idx: number) => void, + onChangeChangedValue: (v: string | null) => void, +} + +export default function EditableText(props: IProps) { + let editingValue = props.editingValue; + let defaultValue = props.defaultValue; + let changedValue = props.changedValue; + let onChangeEditingValue = props.onChangeEditingValue; + let onChangeChangedValue = props.onChangeChangedValue; + let editing = editingValue !== null; + let editingLabel = props.editingLabel; + + const theme = useTheme(); + + const [hovering, setHovering] = useState(false); + + const editButton = + onChangeEditingValue(changedValue || defaultValue)} + > + + + + + const discardChangesButton = + { + onChangeChangedValue(null); + onChangeEditingValue(null); + }} + > + + + + + if (editing) { + return + onChangeEditingValue(e.target.value)} + /> + { + onChangeChangedValue(editingValue === defaultValue ? null : editingValue); + onChangeEditingValue(null); + }} + > + + } else if (changedValue) { + return setHovering(true)} + onMouseLeave={() => setHovering(false)} + display="flex" + alignItems="center" + > + {defaultValue}→ + {changedValue} + {editButton} + {discardChangesButton} + + } + + return setHovering(true)} + onMouseLeave={() => setHovering(false)} + display="flex" + alignItems="center" + >{defaultValue}{editButton}; +} \ No newline at end of file diff --git a/client/src/components/common/StoreLinkIcon.tsx b/client/src/components/common/StoreLinkIcon.tsx new file mode 100644 index 0000000..7359ab0 --- /dev/null +++ b/client/src/components/common/StoreLinkIcon.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { ReactComponent as GPMIcon } from '../../assets/googleplaymusic_icon.svg'; + +export enum ExternalStore { + GooglePlayMusic = "GPM", +} + +export interface IProps { + whichStore: ExternalStore, +} + +export function whichStore(url: string) { + if(url.includes('play.google.com')) { + return ExternalStore.GooglePlayMusic; + } + return undefined; +} + +export default function StoreLinkIcon(props: any) { + const { whichStore, ...restProps } = props; + + switch(whichStore) { + case ExternalStore.GooglePlayMusic: + return ; + default: + throw new Error("Unknown external store: " + whichStore) + } +} \ No newline at end of file diff --git a/client/src/components/common/SubmitChangesButton.tsx b/client/src/components/common/SubmitChangesButton.tsx new file mode 100644 index 0000000..a6a794b --- /dev/null +++ b/client/src/components/common/SubmitChangesButton.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Box, Button } from '@material-ui/core'; + +export default function SubmitChangesButton(props: any) { + return + + +} \ No newline at end of file diff --git a/client/src/components/tables/ResultsTable.tsx b/client/src/components/tables/ResultsTable.tsx index 154bdaa..978a123 100644 --- a/client/src/components/tables/ResultsTable.tsx +++ b/client/src/components/tables/ResultsTable.tsx @@ -7,6 +7,7 @@ import PersonIcon from '@material-ui/icons/Person'; import AlbumIcon from '@material-ui/icons/Album'; import AudiotrackIcon from '@material-ui/icons/Audiotrack'; import LocalOfferIcon from '@material-ui/icons/LocalOffer'; +import { songGetters } from '../../lib/songGetters'; export interface SongGetters { getTitle: (song: any) => string, @@ -25,7 +26,7 @@ export interface IProps { mainDispatch: (action: any) => void, } -export function SongTable(props: IProps) { +export default function SongTable(props: IProps) { const useTableStyles = makeStyles({ table: { minWidth: 650, @@ -66,6 +67,8 @@ export function SongTable(props: IProps) { tabLabel: <>{mainArtistName}, artistId: mainArtistId, metadata: null, + songGetters: songGetters, + songsByArtist: null, }, tabReducer: newWindowReducer[WindowType.Artist], tabType: WindowType.Artist, @@ -79,6 +82,8 @@ export function SongTable(props: IProps) { tabLabel: <>{mainAlbumName}, albumId: mainAlbumId, metadata: null, + songGetters: songGetters, + songsOnAlbum: null, }, tabReducer: newWindowReducer[WindowType.Album], tabType: WindowType.Album, @@ -118,7 +123,7 @@ export function SongTable(props: IProps) { return onClickTag(tagIds[i][tagIds[i].length-1], fullTag)} + onClick={() => onClickTag(tagIds[i][tagIds[i].length - 1], fullTag)} /> }); diff --git a/client/src/components/windows/AlbumWindow.tsx b/client/src/components/windows/AlbumWindow.tsx index 2ad17a8..582b7bc 100644 --- a/client/src/components/windows/AlbumWindow.tsx +++ b/client/src/components/windows/AlbumWindow.tsx @@ -1,26 +1,39 @@ -import React, { useEffect } from 'react'; -import { Box, Typography } from '@material-ui/core'; -import AlbumIcon from '@material-ui/icons/Album'; +import React, { useEffect, useState } from 'react'; +import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core'; +import PersonIcon from '@material-ui/icons/Person'; import * as serverApi from '../../api'; import { WindowState } from './Windows'; +import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; +import EditableText from '../common/EditableText'; +import SubmitChangesButton from '../common/SubmitChangesButton'; +import SongTable, { SongGetters } from '../tables/ResultsTable'; +var _ = require('lodash'); -export interface AlbumMetadata { - name: string, -} +export type AlbumMetadata = serverApi.AlbumDetails; +export type AlbumMetadataChanges = serverApi.ModifyAlbumRequest; export interface AlbumWindowState extends WindowState { albumId: number, metadata: AlbumMetadata | null, + pendingChanges: AlbumMetadataChanges | null, + songsOnAlbum: any[] | null, + songGetters: SongGetters, } export enum AlbumWindowStateActions { SetMetadata = "SetMetadata", + SetPendingChanges = "SetPendingChanges", + SetSongs = "SetSongs", } export function AlbumWindowReducer(state: AlbumWindowState, action: any) { switch (action.type) { case AlbumWindowStateActions.SetMetadata: return { ...state, metadata: action.value } + case AlbumWindowStateActions.SetPendingChanges: + return { ...state, pendingChanges: action.value } + case AlbumWindowStateActions.SetSongs: + return { ...state, songsOnAlbum: action.value } default: throw new Error("Unimplemented AlbumWindow state update.") } @@ -63,25 +76,96 @@ export async function getAlbumMetadata(id: number) { const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) let json: any = await response.json(); let album = json.albums[0]; - return { - name: album.name - } + return album; })(); } export default function AlbumWindow(props: IProps) { let metadata = props.state.metadata; + let pendingChanges = props.state.pendingChanges; + // Effect to get the album's metadata. useEffect(() => { getAlbumMetadata(props.state.albumId) .then((m: AlbumMetadata) => { - console.log("metadata", m); props.dispatch({ type: AlbumWindowStateActions.SetMetadata, value: m }); }) - }, [props.state.metadata?.name]); + }, [metadata?.name]); + + // Effect to get the album's songs. + useEffect(() => { + if(props.state.songsOnAlbum) { return; } + + var q: serverApi.QueryRequest = { + query: { + prop: serverApi.QueryElemProperty.albumId, + propOperator: serverApi.QueryFilterOp.Eq, + propOperand: props.state.albumId, + }, + offsetsLimits: { + songOffset: 0, + songLimit: 100, + }, + ordering: { + orderBy: { + type: serverApi.OrderByType.Name, + }, + ascending: true, + }, + }; + + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(q), + }; + + (async () => { + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) + let json: any = await response.json(); + props.dispatch({ + type: AlbumWindowStateActions.SetSongs, + value: json.songs, + }); + })(); + }, [props.state.songsOnAlbum]); + + const [editingName, setEditingName] = useState(null); + const name = setEditingName(v)} + onChangeChangedValue={(v: string | null) => { + let newVal: any = { ...pendingChanges }; + if (v) { newVal.name = v } + else { delete newVal.name } + props.dispatch({ + type: AlbumWindowStateActions.SetPendingChanges, + value: newVal, + }) + }} + /> + + const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => { + const store = whichStore(link); + return store && + + + + }); + + const maybeSubmitButton = pendingChanges && Object.keys(pendingChanges).length > 0 && + return - + + + + {metadata && + + {name} + + + + {storeLinks} + + + } + + + {maybeSubmitButton} - {metadata && {metadata.name}} + + Songs in this album in your library: + + {props.state.songsOnAlbum && } + {!props.state.songsOnAlbum && } } \ No newline at end of file diff --git a/client/src/components/windows/ArtistWindow.tsx b/client/src/components/windows/ArtistWindow.tsx index d3453eb..67dfb70 100644 --- a/client/src/components/windows/ArtistWindow.tsx +++ b/client/src/components/windows/ArtistWindow.tsx @@ -1,26 +1,39 @@ -import React, { useEffect } from 'react'; -import { Box, Typography } from '@material-ui/core'; +import React, { useEffect, useState } from 'react'; +import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core'; import PersonIcon from '@material-ui/icons/Person'; import * as serverApi from '../../api'; import { WindowState } from './Windows'; +import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; +import EditableText from '../common/EditableText'; +import SubmitChangesButton from '../common/SubmitChangesButton'; +import SongTable, { SongGetters } from '../tables/ResultsTable'; +var _ = require('lodash'); -export interface ArtistMetadata { - name: string, -} +export type ArtistMetadata = serverApi.ArtistDetails; +export type ArtistMetadataChanges = serverApi.ModifyArtistRequest; export interface ArtistWindowState extends WindowState { artistId: number, metadata: ArtistMetadata | null, + pendingChanges: ArtistMetadataChanges | null, + songsByArtist: any[] | null, + songGetters: SongGetters, } export enum ArtistWindowStateActions { SetMetadata = "SetMetadata", + SetPendingChanges = "SetPendingChanges", + SetSongs = "SetSongs", } export function ArtistWindowReducer(state: ArtistWindowState, action: any) { switch (action.type) { case ArtistWindowStateActions.SetMetadata: return { ...state, metadata: action.value } + case ArtistWindowStateActions.SetPendingChanges: + return { ...state, pendingChanges: action.value } + case ArtistWindowStateActions.SetSongs: + return { ...state, songsByArtist: action.value } default: throw new Error("Unimplemented ArtistWindow state update.") } @@ -63,25 +76,96 @@ export async function getArtistMetadata(id: number) { const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) let json: any = await response.json(); let artist = json.artists[0]; - return { - name: artist.name - } + return artist; })(); } export default function ArtistWindow(props: IProps) { let metadata = props.state.metadata; + let pendingChanges = props.state.pendingChanges; + // Effect to get the artist's metadata. useEffect(() => { getArtistMetadata(props.state.artistId) .then((m: ArtistMetadata) => { - console.log("metadata", m); props.dispatch({ type: ArtistWindowStateActions.SetMetadata, value: m }); }) - }, [props.state.metadata?.name]); + }, [metadata?.name]); + + // Effect to get the artist's songs. + useEffect(() => { + if(props.state.songsByArtist) { return; } + + var q: serverApi.QueryRequest = { + query: { + prop: serverApi.QueryElemProperty.artistId, + propOperator: serverApi.QueryFilterOp.Eq, + propOperand: props.state.artistId, + }, + offsetsLimits: { + songOffset: 0, + songLimit: 100, + }, + ordering: { + orderBy: { + type: serverApi.OrderByType.Name, + }, + ascending: true, + }, + }; + + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(q), + }; + + (async () => { + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) + let json: any = await response.json(); + props.dispatch({ + type: ArtistWindowStateActions.SetSongs, + value: json.songs, + }); + })(); + }, [props.state.songsByArtist]); + + const [editingName, setEditingName] = useState(null); + const name = setEditingName(v)} + onChangeChangedValue={(v: string | null) => { + let newVal: any = { ...pendingChanges }; + if (v) { newVal.name = v } + else { delete newVal.name } + props.dispatch({ + type: ArtistWindowStateActions.SetPendingChanges, + value: newVal, + }) + }} + /> + + const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => { + const store = whichStore(link); + return store && + + + + }); + + const maybeSubmitButton = pendingChanges && Object.keys(pendingChanges).length > 0 && + return - + + + + {metadata && + + {name} + + + + {storeLinks} + + + } + + + {maybeSubmitButton} - {metadata && {metadata.name}} + + Songs by this artist in your library: + + {props.state.songsByArtist && } + {!props.state.songsByArtist && } } \ No newline at end of file diff --git a/client/src/components/windows/QueryWindow.tsx b/client/src/components/windows/QueryWindow.tsx index fa7507e..e35d233 100644 --- a/client/src/components/windows/QueryWindow.tsx +++ b/client/src/components/windows/QueryWindow.tsx @@ -3,8 +3,8 @@ import { createMuiTheme, Box, LinearProgress } from '@material-ui/core'; import { QueryElem, toApiQuery } from '../../lib/query/Query'; import QueryBuilder from '../querybuilder/QueryBuilder'; import * as serverApi from '../../api'; -import { SongTable } from '../tables/ResultsTable'; -import stringifyList from '../../lib/stringifyList'; +import SongTable from '../tables/ResultsTable'; +import { songGetters } from '../../lib/songGetters'; import { getArtists, getSongTitles, getAlbums, getTags } from '../../lib/query/Getters'; import { grey } from '@material-ui/core/colors'; import { WindowState } from './Windows'; @@ -72,35 +72,6 @@ export default function QueryWindow(props: IProps) { const loading = query && (!resultsFor || !_.isEqual(resultsFor.for, query)); const showResults = (query && resultsFor && query == resultsFor.for) ? resultsFor.results : []; - const songGetters = { - getTitle: (song: any) => song.title, - getId: (song: any) => song.songId, - getArtistNames: (song: any) => song.artists.map((a: any) => a.name), - getArtistIds: (song: any) => song.artists.map((a: any) => a.artistId), - getAlbumNames: (song: any) => song.albums.map((a: any) => a.name), - getAlbumIds: (song: any) => song.albums.map((a: any) => a.albumId), - getTagNames: (song: any) => { - // Recursively resolve the name. - const resolveTag = (tag: any) => { - var r = [tag.name]; - if (tag.parent) { r.unshift(resolveTag(tag.parent)); } - return r; - } - - return song.tags.map((tag: any) => resolveTag(tag)); - }, - getTagIds: (song: any) => { - // Recursively resolve the id. - const resolveTag = (tag: any) => { - var r = [tag.tagId]; - if (tag.parent) { r.unshift(resolveTag(tag.parent)); } - return r; - } - - return song.tags.map((tag: any) => resolveTag(tag)); - }, - } - const doQuery = async (_query: QueryElem) => { var q: serverApi.QueryRequest = { query: toApiQuery(_query), diff --git a/client/src/components/windows/SongWindow.tsx b/client/src/components/windows/SongWindow.tsx index b937b8f..6c3aa2f 100644 --- a/client/src/components/windows/SongWindow.tsx +++ b/client/src/components/windows/SongWindow.tsx @@ -1,26 +1,36 @@ -import React, { useEffect } from 'react'; -import { Box, Typography } from '@material-ui/core'; +import React, { useEffect, useState } from 'react'; +import { Box, Typography, IconButton, Button } 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'; import * as serverApi from '../../api'; import { WindowState } from './Windows'; +import { ArtistMetadata } from './ArtistWindow'; +import { AlbumMetadata } from './AlbumWindow'; +import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; +import EditableText from '../common/EditableText'; +import SubmitChangesButton from '../common/SubmitChangesButton'; -export interface SongMetadata { - title: string, -} +export type SongMetadata = serverApi.SongDetails; +export type SongMetadataChanges = serverApi.ModifySongRequest; export interface SongWindowState extends WindowState { songId: number, metadata: SongMetadata | null, + pendingChanges: SongMetadataChanges | null, } export enum SongWindowStateActions { SetMetadata = "SetMetadata", + SetPendingChanges = "SetPendingChanges", } export function SongWindowReducer(state: SongWindowState, action: any) { switch (action.type) { case SongWindowStateActions.SetMetadata: return { ...state, metadata: action.value } + case SongWindowStateActions.SetPendingChanges: + return { ...state, pendingChanges: action.value } default: throw new Error("Unimplemented SongWindow state update.") } @@ -63,25 +73,69 @@ export async function getSongMetadata(id: number) { const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) let json: any = await response.json(); let song = json.songs[0]; - return { - title: song.title - } + return song; })(); } export default function SongWindow(props: IProps) { let metadata = props.state.metadata; + let pendingChanges = props.state.pendingChanges; useEffect(() => { getSongMetadata(props.state.songId) .then((m: SongMetadata) => { - console.log("metadata", m); props.dispatch({ type: SongWindowStateActions.SetMetadata, value: m }); }) - }, [props.state.metadata?.title]); + }, [metadata?.title]); + + const [editingTitle, setEditingTitle] = useState(null); + const title = setEditingTitle(v)} + onChangeChangedValue={(v: string | null) => { + let newVal: any = { ...pendingChanges }; + if(v) { newVal.title = v } + else { delete newVal.title } + props.dispatch({ + type: SongWindowStateActions.SetPendingChanges, + value: newVal, + }) + }} + /> + + const artists = metadata?.artists && metadata?.artists.map((artist: ArtistMetadata) => { + return + {artist.name} + + }); + + const albums = metadata?.albums && metadata?.albums.map((album: AlbumMetadata) => { + return + {album.name} + + }); + + const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => { + const store = whichStore(link); + return store && + + + + }); + + const maybeSubmitButton = pendingChanges && Object.keys(pendingChanges).length > 0 && + return - + + + + {metadata && + + {title} + + + + + + {artists} + + + + + + + + {albums} + + + + + + {storeLinks} + + + } - {metadata && {metadata.title}} + {maybeSubmitButton} } \ No newline at end of file diff --git a/client/src/components/windows/TagWindow.tsx b/client/src/components/windows/TagWindow.tsx index 49c7f50..f504312 100644 --- a/client/src/components/windows/TagWindow.tsx +++ b/client/src/components/windows/TagWindow.tsx @@ -4,9 +4,7 @@ import LocalOfferIcon from '@material-ui/icons/LocalOffer'; import * as serverApi from '../../api'; import { WindowState } from './Windows'; -export interface TagMetadata { - name: string, -} +export type TagMetadata = serverApi.TagDetails; export interface TagWindowState extends WindowState { tagId: number, @@ -63,9 +61,7 @@ export async function getTagMetadata(id: number) { const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) let json: any = await response.json(); let tag = json.tags[0]; - return { - name: tag.name - } + return tag; })(); } diff --git a/client/src/components/windows/Windows.tsx b/client/src/components/windows/Windows.tsx index 77b79f3..7460ae0 100644 --- a/client/src/components/windows/Windows.tsx +++ b/client/src/components/windows/Windows.tsx @@ -9,6 +9,7 @@ import AudiotrackIcon from '@material-ui/icons/Audiotrack'; import { SongWindowReducer } from './SongWindow'; import { AlbumWindowReducer } from './AlbumWindow'; import { TagWindowReducer } from './TagWindow'; +import { songGetters } from '../../lib/songGetters'; export enum WindowType { Query = "Query", @@ -41,30 +42,38 @@ export const newWindowState = { }, [WindowType.Artist]: () => { return { - tabLabel: <>Artist, + tabLabel: <>Artist 1, artistId: 1, metadata: null, + pendingChanges: null, + songGetters: songGetters, + songsByArtist: null, } }, [WindowType.Album]: () => { return { - tabLabel: <>Album, + tabLabel: <>Album 1, albumId: 1, metadata: null, + pendingChanges: null, + songGetters: songGetters, + songsOnAlbum: null, } }, [WindowType.Song]: () => { return { - tabLabel: <>Song, + tabLabel: <>Song 1, songId: 1, metadata: null, + pendingChanges: null, } }, [WindowType.Tag]: () => { return { - tabLabel: <>Tag, + tabLabel: <>Tag 1, tagId: 1, metadata: null, + pendingChanges: null, } }, } \ No newline at end of file diff --git a/client/src/lib/songGetters.tsx b/client/src/lib/songGetters.tsx new file mode 100644 index 0000000..4f75cc9 --- /dev/null +++ b/client/src/lib/songGetters.tsx @@ -0,0 +1,28 @@ +export const songGetters = { + getTitle: (song: any) => song.title, + getId: (song: any) => song.songId, + getArtistNames: (song: any) => song.artists.map((a: any) => a.name), + getArtistIds: (song: any) => song.artists.map((a: any) => a.artistId), + getAlbumNames: (song: any) => song.albums.map((a: any) => a.name), + getAlbumIds: (song: any) => song.albums.map((a: any) => a.albumId), + getTagNames: (song: any) => { + // Recursively resolve the name. + const resolveTag = (tag: any) => { + var r = [tag.name]; + if (tag.parent) { r.unshift(resolveTag(tag.parent)); } + return r; + } + + return song.tags.map((tag: any) => resolveTag(tag)); + }, + getTagIds: (song: any) => { + // Recursively resolve the id. + const resolveTag = (tag: any) => { + var r = [tag.tagId]; + if (tag.parent) { r.unshift(resolveTag(tag.parent)); } + return r; + } + + return song.tags.map((tag: any) => resolveTag(tag)); + }, +} \ No newline at end of file -- 2.36.1 From 97eb32bfb6dd4605d9ed16f4ebeb7f0e655fc8dc Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Wed, 23 Sep 2020 16:56:29 +0200 Subject: [PATCH 2/6] Fix tags page too. --- client/src/components/tables/ResultsTable.tsx | 2 + client/src/components/windows/AlbumWindow.tsx | 6 +- client/src/components/windows/TagWindow.tsx | 158 +++++++++++++++++- client/src/components/windows/Windows.tsx | 2 + 4 files changed, 158 insertions(+), 10 deletions(-) diff --git a/client/src/components/tables/ResultsTable.tsx b/client/src/components/tables/ResultsTable.tsx index 978a123..cd56e8e 100644 --- a/client/src/components/tables/ResultsTable.tsx +++ b/client/src/components/tables/ResultsTable.tsx @@ -110,6 +110,8 @@ export default function SongTable(props: IProps) { tabLabel: <>{name}, tagId: id, metadata: null, + songGetters: songGetters, + songsWithTag: null, }, tabReducer: newWindowReducer[WindowType.Tag], tabType: WindowType.Tag, diff --git a/client/src/components/windows/AlbumWindow.tsx b/client/src/components/windows/AlbumWindow.tsx index 582b7bc..e7cdb1c 100644 --- a/client/src/components/windows/AlbumWindow.tsx +++ b/client/src/components/windows/AlbumWindow.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; -import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core'; -import PersonIcon from '@material-ui/icons/Person'; +import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core'; +import AlbumIcon from '@material-ui/icons/Album'; import * as serverApi from '../../api'; import { WindowState } from './Windows'; import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; @@ -173,7 +173,7 @@ export default function AlbumWindow(props: IProps) { mt={4} width="80%" > - + { getTagMetadata(props.state.tagId) .then((m: TagMetadata) => { - console.log("metadata", m); props.dispatch({ type: TagWindowStateActions.SetMetadata, value: m }); }) - }, [props.state.metadata?.name]); + }, [metadata?.name]); + + // Effect to get the tag's songs. + useEffect(() => { + if (props.state.songsWithTag) { return; } + + var q: serverApi.QueryRequest = { + query: { + prop: serverApi.QueryElemProperty.tagId, + propOperator: serverApi.QueryFilterOp.Eq, + propOperand: props.state.tagId, + }, + offsetsLimits: { + songOffset: 0, + songLimit: 100, + }, + ordering: { + orderBy: { + type: serverApi.OrderByType.Name, + }, + ascending: true, + }, + }; + + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(q), + }; + + (async () => { + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) + let json: any = await response.json(); + props.dispatch({ + type: TagWindowStateActions.SetSongs, + value: json.songs, + }); + })(); + }, [props.state.songsWithTag]); + + const [editingName, setEditingName] = useState(null); + const name = setEditingName(v)} + onChangeChangedValue={(v: string | null) => { + let newVal: any = { ...pendingChanges }; + if (v) { newVal.name = v } + else { delete newVal.name } + props.dispatch({ + type: TagWindowStateActions.SetPendingChanges, + value: newVal, + }) + }} + /> + const fullName = + {metadata?.fullName.map((n: string, i: number) => { + if (metadata?.fullName && i == metadata?.fullName.length - 1) { + return name; + } else if (i >= (metadata?.fullName.length || 0) - 1) { + return undefined; + } else { + return {n} /  + } + })} + + + const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => { + const store = whichStore(link); + return store && + + + + }); + + const maybeSubmitButton = pendingChanges && Object.keys(pendingChanges).length > 0 && + return - + + + + {metadata && + + {fullName} + + + + {storeLinks} + + + } + + + {maybeSubmitButton} - {metadata && {metadata.name}} + + Songs with this tag in your library: + + {props.state.songsWithTag && } + {!props.state.songsWithTag && } } \ No newline at end of file diff --git a/client/src/components/windows/Windows.tsx b/client/src/components/windows/Windows.tsx index 7460ae0..d5840e5 100644 --- a/client/src/components/windows/Windows.tsx +++ b/client/src/components/windows/Windows.tsx @@ -74,6 +74,8 @@ export const newWindowState = { tagId: 1, metadata: null, pendingChanges: null, + songGetters: songGetters, + songsWithTag: null, } }, } \ No newline at end of file -- 2.36.1 From b38efa6191699a49924dad58109daf6c5131e13d Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Wed, 23 Sep 2020 17:21:18 +0200 Subject: [PATCH 3/6] Performance improved a bit by factoring code. --- client/src/components/tables/ResultsTable.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/client/src/components/tables/ResultsTable.tsx b/client/src/components/tables/ResultsTable.tsx index cd56e8e..cce223f 100644 --- a/client/src/components/tables/ResultsTable.tsx +++ b/client/src/components/tables/ResultsTable.tsx @@ -27,12 +27,17 @@ export interface IProps { } export default function SongTable(props: IProps) { - const useTableStyles = makeStyles({ + const classes = makeStyles({ + button: { + textTransform: "none", + fontWeight: 400, + paddingLeft: '0', + textAlign: 'left', + }, table: { minWidth: 650, }, - }); - const classes = useTableStyles(); + })(); return ( @@ -131,14 +136,6 @@ export default function SongTable(props: IProps) { }); const TextCell = (props: any) => { - const classes = makeStyles({ - button: { - textTransform: "none", - fontWeight: 400, - paddingLeft: '0', - textAlign: 'left', - } - })(); return